Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/j5-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
branches:
- "j5/**"
- "codex/agent-led-pb-*"

permissions:
contents: read
Expand Down
16 changes: 10 additions & 6 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ migrations after upstream migrations.
41. Skills inventory extends case 40's single Settings page with Catalog and Installed sections sharing an environment picker. `server.ts` adds optional skill provenance fields. `ClaudeSkills.ts` appends plugin discovery from `j5/skills/claudePluginSkills.ts` and exports its existing settings-path/repository-root helpers; keep its upstream directory-name explanation intact. `ClaudeDriver.ts` passes the instance environment, `ClaudeProvider.ts` appends the bounded bundled-skill reload probe, and `CodexProvider.ts` carries optional provenance. The registry appends J5's cached path resolver at snapshot ingestion and invalidates it on explicit refresh; workspace resolution stays in the J5 refresher. On rebase, preserve plugin enablement/precedence, built-in composer visibility, status-only cache reuse, and environment/project selection. Origin and discovery-state helpers live in `packages/shared/src/j5/skillInventory.ts`; contracts remain wire-only.

42. Skills linking extends the J5 contract/RPC exports and the authorization/handler spreads from case 40 with `j5.skills.links.*`. The Settings inventory mounts J5's link preview, inspected unlink batch, confirmed original-folder deletion, and managed-record panel; search continues to use the single Skills route. The shared J5 inventory helpers classify linkable origins and discovery state; catalog and standalone linking reuse J5 root resolution, filesystem operations, and affected-provider refresh. On rebase, preserve fresh preview validation, canonical project containment, the Git exposure warning, ownership identity and forget-only recovery, stale unlink/deletion preview rejection, and release the mutation permit before refreshing providers. Catalog remains user-scoped; standalone links can target an explicitly selected project.
43. Agent-led playbooks: `apps/server/src/provider/T3OrchestrationInstructions.ts` appends J5-owned retrieval and advancement guidance. All state, YAML reading, MCP operations, and authenticated reads remain under J5-owned modules; advancement never creates turns or controls provider lifecycle.
43. Agent-led playbooks: `apps/server/src/provider/T3OrchestrationInstructions.ts` appends J5-owned retrieval and advancement guidance. All state, YAML reading, MCP operations, and authenticated reads remain under J5-owned modules; advancement never creates turns or controls provider lifecycle. Web `ChatView.tsx` mounts the J5 phase board and expands submitted `/playbook` text; `ChatComposer.tsx` and `composer-logic.ts` append an ordinary built-in command. `packages/client-runtime/package.json` exports the shared J5 presentation and expansion helper. `docs/user/composer.md` describes that text expansion. The runs overview is a section of the existing J5-owned Fleet page. A J5-owned read-scoped revision stream notifies visible clients of committed playbook mutations. The RPC group, authorization scope map, handlers, and subscription tag are appended through existing J5 integration seams; the WebSocket handlers share the server-lifetime PlaybookStore.

## Pin and upstream advance runbook

Expand Down Expand Up @@ -551,14 +551,18 @@ indicate approval. The deleted hook remains explicitly marked.
| `apps/server/src/serviceLauncher.ts` | R | H |
| `apps/server/src/telemetry/Identify.ts` | R | H |
| `apps/server/src/textGeneration/CodexTextGeneration.test.ts` | N | T |
| `apps/server/src/ws.ts` | A | 10, 11, 37 |
| `apps/server/src/ws.ts` | A | 10, 11, 37, 43 |
| `apps/web/index.html` | R | B |
| `apps/web/src/branding.test.ts` | R | B |
| `apps/web/src/branding.ts` | R | B |
| `apps/web/src/commandPaletteBus.ts` | R | 13 |
| `apps/web/src/components/ChatView.logic.test.ts` | R | 9, 19 |
| `apps/web/src/components/ChatView.logic.ts` | A | 9, 19 |
| `apps/web/src/components/ChatView.tsx` | A | 9–11, 19, 24, 34 |
| `packages/contracts/src/rpc.ts` | A | 43 |
| `packages/client-runtime/src/rpc/client.ts` | A | 43 |
| `apps/web/src/composer-logic.ts` | A | 43 |
| `apps/server/src/auth/RpcAuthorization.ts` | A | 43 |
| `apps/web/src/components/ChatView.tsx` | A | 9–11, 19, 24, 34, 43 |
| `apps/web/src/components/CommandPalette.logic.test.ts` | R | 15b, 34 |
| `apps/web/src/components/CommandPalette.logic.ts` | R | 15b, 34 |
| `apps/web/src/components/CommandPalette.tsx` | A | 13, 15b, 19, 20, 34 |
Expand All @@ -568,7 +572,7 @@ indicate approval. The deleted hook remains explicitly marked.
| `apps/web/src/components/Sidebar.logic.test.ts` | R | 16, 19, 21 |
| `apps/web/src/components/Sidebar.logic.ts` | R | 16, 19, 21, 34 |
| `apps/web/src/components/Sidebar.tsx` | A | 9, 16, 19, 21–23, 34 |
| `apps/web/src/components/chat/ChatComposer.tsx` | A | 27, 29 |
| `apps/web/src/components/chat/ChatComposer.tsx` | A | 27, 29, 43 |
| `apps/web/src/components/chat/ChatHeader.tsx` | A | 19 |
| `apps/web/src/components/chat/ComposerPrimaryActions.tsx` | A | 27 |
| `apps/web/src/components/chat/DraftHeroHeadline.tsx` | A | 9 |
Expand All @@ -588,8 +592,8 @@ indicate approval. The deleted hook remains explicitly marked.
| `apps/web/src/routes/_chat.index.tsx` | R | 9, 19, 34 |
| `apps/web/src/routes/_chat.tsx` | R | 19, 34 |
| `apps/web/tsconfig.json` | R | B |
| `docs/user/composer.md` | N | 26, 27 |
| `packages/client-runtime/package.json` | R | 27, 34 |
| `docs/user/composer.md` | N | 26, 27, 43 |
| `packages/client-runtime/package.json` | R | 27, 34, 43 |
| `packages/client-runtime/src/operations/commands.test.ts` | R | 10, 11 |
| `packages/client-runtime/src/operations/commands.ts` | R | 10, 11 |
| `packages/contracts/package.json` | N | 34 |
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AGENT_PERSONA_RPC_SCOPES } from "../j5/agents/agentPersonaRpc.ts";
import { ARTIFACT_RPC_SCOPES } from "../j5/artifacts/artifactRpc.ts";
import { SKILL_LINK_RPC_SCOPES } from "../j5/skills/skillLinkRpc.ts";
import { SKILL_CATALOG_RPC_SCOPES } from "../j5/skills/skillCatalogRpc.ts";
import { PLAYBOOK_RPC_SCOPES } from "../j5/playbooks/playbookRpc.ts";

type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];

Expand All @@ -30,6 +31,7 @@ export const RPC_REQUIRED_SCOPES = {
...ARTIFACT_RPC_SCOPES,
...SKILL_CATALOG_RPC_SCOPES,
...SKILL_LINK_RPC_SCOPES,
...PLAYBOOK_RPC_SCOPES,
[ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope,
[ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
Expand Down
86 changes: 84 additions & 2 deletions apps/server/src/j5/playbooks/PlaybookStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import { seedPlaybookOwners } from "./testFixtures.ts";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import { ThreadId } from "@t3tools/contracts";
import { AuthOrchestrationReadScope, ThreadId } from "@t3tools/contracts";
import {
J5_PLAYBOOK_WS_METHODS,
PLAYBOOK_MAX_BYTES,
PLAYBOOK_MAX_STEPS,
PLAYBOOK_RUNS_PAGE_SIZE,
type PlaybookDefinition,
type PlaybookProgress,
} from "@t3tools/contracts/j5";
import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Option from "effect/Option";
import * as Queue from "effect/Queue";
import * as Result from "effect/Result";
import * as SqlClient from "effect/unstable/sql/SqlClient";
import * as Option from "effect/Option";
import * as Stream from "effect/Stream";
import { stringify } from "yaml";

import { runJ5A2AMigrations } from "../a2a/Migrations.ts";
import { makePlaybookStore, type PlaybookMutation } from "./PlaybookStore.ts";
import { makePlaybookRpcHandlers, PLAYBOOK_RPC_SCOPES } from "./playbookRpc.ts";

const owner = ThreadId.make("thread:playbook:owner");
const otherOwner = ThreadId.make("thread:playbook:other");
Expand Down Expand Up @@ -60,6 +65,83 @@ const makeFixture = Effect.gen(function* () {
return { ...workspace, store };
});

it.effect(
"publishes committed playbook changes during a turn and ignores failures and retries",
() =>
Effect.gen(function* () {
const { store, workspaceRoot } = yield* makeFixture;
const snapshots = yield* Queue.unbounded<{
revision: number;
runs: ReadonlyArray<PlaybookProgress>;
}>();
const handlers = makePlaybookRpcHandlers(store, (_method, stream) => stream);
assert.equal(
PLAYBOOK_RPC_SCOPES[J5_PLAYBOOK_WS_METHODS.subscribeChanges],
AuthOrchestrationReadScope,
);
yield* handlers[J5_PLAYBOOK_WS_METHODS.subscribeChanges]().pipe(
Stream.runForEach((revision) =>
Effect.gen(function* () {
const { runs } = yield* store.listForThread(owner);
yield* Queue.offer(snapshots, { revision, runs });
}),
),
Effect.forkScoped,
);
assert.deepStrictEqual(yield* Queue.take(snapshots), { revision: 0, runs: [] });
const run = yield* store.start(owner, workspaceRoot, "demo", "start");
const started = yield* Queue.take(snapshots);
assert.equal(started.revision, 1);
assert.equal(started.runs[0]?.runId, run.runId);
assert.equal(started.runs[0]?.status, "active");
yield* store.start(owner, workspaceRoot, "demo", "start");
yield* Effect.flip(store.start(owner, workspaceRoot, "demo", "duplicate"));
yield* Effect.flip(
store.mutate(otherOwner, {
operation: "cancel",
runId: run.runId,
client_request_id: "foreign",
}),
);
assert.deepStrictEqual(yield* Stream.runHead(store.changes), Option.some(1));

const moves = [
{ operation: "next", expectedStepId: "research", step: "implement", status: "active" },
{ operation: "back", expectedStepId: "implement", step: "research", status: "active" },
{
operation: "reselect",
expectedStepId: "research",
stepId: "review",
step: "review",
status: "active",
},
{ operation: "complete", expectedStepId: "review", step: "review", status: "completed" },
] as const;
for (const [index, { step, status, ...movement }] of moves.entries()) {
const input = { ...movement, runId: run.runId, client_request_id: `move-${index}` };
yield* store.mutate(owner, input);
const changed = yield* Queue.take(snapshots);
assert.equal(changed.revision, index + 2);
assert.equal(changed.runs[0]?.currentStepId, step);
assert.equal(changed.runs[0]?.status, status);
yield* store.mutate(owner, input);
assert.deepStrictEqual(yield* Stream.runHead(store.changes), Option.some(index + 2));
}
const second = yield* store.start(owner, workspaceRoot, "demo", "second");
assert.equal((yield* Queue.take(snapshots)).revision, 6);
yield* store.mutate(owner, {
operation: "cancel",
runId: second.runId,
client_request_id: "cancel",
});
const cancelled = yield* Queue.take(snapshots);
assert.equal(cancelled.revision, 7);
assert.equal(cancelled.runs[0]?.status, "cancelled");
// A new subscriber receives the latest revision even if it missed the mutation.
assert.deepStrictEqual(yield* Stream.runHead(store.changes), Option.some(7));
}).pipe(Effect.scoped, Effect.provide(MemoryLayer)),
);

it.effect(
"lists owners together, filters active runs, and reads shared live definitions once per page",
() =>
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/j5/playbooks/PlaybookStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Semaphore from "effect/Semaphore";
import * as SubscriptionRef from "effect/SubscriptionRef";
import * as SqlClient from "effect/unstable/sql/SqlClient";
import { parseDocument } from "yaml";

Expand Down Expand Up @@ -68,6 +69,9 @@ export const makePlaybookStore = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const crypto = yield* Crypto.Crypto;
const revision = yield* SubscriptionRef.make(0);
const notifyChange = (result: PlaybookStepResponse) =>
result.replayed ? Effect.void : SubscriptionRef.update(revision, (value) => value + 1);
// Serialize definition edits and run mutations within this environment.
const permit = yield* Semaphore.make(1);
const now = DateTime.now.pipe(Effect.map(DateTime.formatIso));
Expand Down Expand Up @@ -358,7 +362,7 @@ export const makePlaybookStore = Effect.gen(function* () {
}),
);
return result.replayed ? yield* view(result.run, true) : present(result.run, definition);
}).pipe(permit.withPermits(1));
}).pipe(Effect.tap(notifyChange), permit.withPermits(1));
}, Effect.mapError(storageError));

const mutate = Effect.fn("PlaybookStore.mutate")(function* (
Expand Down Expand Up @@ -434,7 +438,7 @@ export const makePlaybookStore = Effect.gen(function* () {
return definition && !result.replayed
? present(result.run, definition)
: yield* view(result.run, result.replayed);
}).pipe(permit.withPermits(1));
}).pipe(Effect.tap(notifyChange), permit.withPermits(1));
}, Effect.mapError(storageError));

const current = Effect.fn("PlaybookStore.current")(function* (owner: ThreadId, runId?: string) {
Expand Down Expand Up @@ -488,6 +492,8 @@ export const makePlaybookStore = Effect.gen(function* () {
return { runs, total: counts[0]?.total ?? 0 };
}, Effect.mapError(storageError));
return {
// Include the current revision so subscribing after a mutation still refreshes the view.
changes: SubscriptionRef.changes(revision),
discover,
removeDefinition,
renameDefinition,
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/j5/playbooks/playbookRpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { AuthOrchestrationReadScope } from "@t3tools/contracts";
import { J5_PLAYBOOK_WS_METHODS } from "@t3tools/contracts/j5";
import type { ObserveRpcStream } from "../agents/agentPersonaRpc.ts";
import type { PlaybookStore } from "./PlaybookStore.ts";

export const PLAYBOOK_RPC_SCOPES = {
[J5_PLAYBOOK_WS_METHODS.subscribeChanges]: AuthOrchestrationReadScope,
} as const;

/** Share the server's store revision through the authenticated WebSocket connection. */
export function makePlaybookRpcHandlers(
store: Pick<PlaybookStore["Service"], "changes">,
observeStream: ObserveRpcStream,
) {
return {
[J5_PLAYBOOK_WS_METHODS.subscribeChanges]: () =>
observeStream(J5_PLAYBOOK_WS_METHODS.subscribeChanges, store.changes),
};
}
Loading
Loading