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
12 changes: 7 additions & 5 deletions FORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Treat these upstream areas as off-limits except for those explicit appended case

### Sanctioned appended integration cases

Against candidate upstream pin `67a2be0fdbee7afb64b691f147ed286a108c706b`, the inventory has 46 cases: 1–45 plus 15b. Existing numbers remain stable. Line coordinates and landing SHAs below describe historical integrations; use named symbols and tests after upstream movement. Branding has its own inventory in `BRANDING.md`; temporary defect patches are listed separately below.
Against candidate upstream pin `67a2be0fdbee7afb64b691f147ed286a108c706b`, the inventory has 48 cases: 1–47 plus 15b. Existing numbers remain stable. Line coordinates and landing SHAs below describe historical integrations; use named symbols and tests after upstream movement. Branding has its own inventory in `BRANDING.md`; temporary defect patches are listed separately below.

1. A1's independent Squadron communication-ledger migration lane: `apps/server/src/persistence/Layers/Sqlite.ts` runs `runJ5CompatibleUpstreamMigrations` before `runJ5A2AMigrations`, inside upstream's `layerConfig` after `initializeV2Database` has copied `state.sqlite` to `statev2.sqlite`; both run only against `statev2.sqlite`. J5 adopts that copy-on-first-start unchanged (Jackson's 2026-09-24 decision #2): an existing `statev2.sqlite` wins and `state.sqlite` is never recopied or written again, so the cutover is one-way: writes after the first V2 boot are invisible to an older binary. Snapshot before the upgrade, and roll back across it by stopping writers and moving `statev2.sqlite` aside (`docs/j5/runbooks/dogfood-runtime.md`); never run old and new binaries against the two files. J5 tooling (`fleet-load.ts`, `devDeliverySeed.ts`, `dogfood-snapshot.sh`, `dogfood-local.sh`) targets `statev2.sqlite`; `apps/server/src/j5/persistence/StateV2Cutover.test.ts` covers the copy, no-recopy and failed-first-boot retry. Case 31 defines the compatibility wrapper; J5's own migration IDs remain in separate tracking tables and run after upstream migration completion.
2. The authenticated J5 MCP registration seam: `apps/server/src/mcp/McpHttpServer.ts:31-34` imports `J5McpIntegrationLive` and `J5OrchestratorSurfaceRegistrationLive`; `:246-252` selects the J5-owned orchestration allowlist and appends the J5 communication toolkit. The complete upstream `OrchestratorToolkitRegistrationLive` remains compiled at `:229-232` but is intentionally absent from the production merge. Registration stays in `apps/server/src/j5/a2a/mcp/registration.ts`; the combined HTTP/MCP runtime is provided once at case 5's server graph. `t3_thread_read` is intentionally re-declared in J5-owned code with factual thread-state prose because the upstream Tool description contains excluded app-owned/delegation semantics; never replace it with the upstream Tool constant during a rebase. J5 likewise re-declares `orchestrator_capabilities`, so upstream's pointer to `t3_thread_launch` in its description does not reach J5 agents. Open follow-up: upstream's `t3_worktree_handoff` Tool object is still exposed as-is, and its description now tells agents to use `t3_thread_launch` with `workspaceStrategy`, a tool J5 does not expose; re-declare it in J5-owned code like `t3_thread_read`. Upstream removed `t3_thread_start`; it stays in the exclusion assertions as a regression guard. `t3_thread_wait` is deliberately absent from the J5 surface since 2026-09-14 (a turn that blocks on another thread cannot receive the platform notices that queue behind it; see `orchestratorSurface.ts` and the registration tests that assert its absence); never re-add it. `list_participants` exposes every result field in snake_case at every depth, including nested agent `thread_id`, provenance `spawned_by_participant_id` / `source_participant_id`, `placement_parent_id`, and `display_name`; never restore any upstream camelCase directory or provenance field during a rebase. Later J5 verbs extend the fork-owned toolkit without another protected-file registration or runtime provider.
Expand Down Expand Up @@ -265,6 +265,8 @@ migrations after upstream migrations.

46. Crew seat approvals in the Inbox (#264): `packages/shared/package.json` appends the `./j5/crewRuntimeRequests` subpath export, so the server reads the J5-owned selector for the approvals the Inbox answers (`packages/shared/src/j5/crewRuntimeRequests.ts` `inboxAnswerableApprovals`) without importing client-runtime. J5-owned `packages/client-runtime/src/j5/crewRuntimeRequests.test.ts` holds that selector to the live approvals of upstream's `derivePendingThreadRequests` (`packages/client-runtime/src/state/threadRequests.ts`); when an upstream advance changes which approvals the composer shows or how it describes them, that test fails and the J5 selector follows.

47. Crew seats ask their Captain, not the person (#325, Jackson's 2026-09-26 decision). `apps/server/src/orchestration-v2/ProviderAdapter.ts` appends an optional `crewSeat` to `ProviderAdapterV2RuntimePolicy` beside `agentPersonaInstructions`. J5-owned `makeAgentPersonaRuntimePolicyResolver` (`j5/agents/agentPersonaRuntime.ts`), already the only resolver behind upstream `RuntimePolicy.ts`, asks J5-owned `makeCrewSeatLookup` (`j5/a2a/crewSeatRuntime.ts`) whether an `agent`/`mcp` thread holds a seat in a live Crew, through the Crew store built over the ambient SQL client (`RuntimePolicy.ts` is untouched and its layers gain no requirement; without a SQL client nothing is a seat, and an unreadable store logs and answers no). A seat's policy gets `crewSeat: true` and the ask-your-Captain rule appended to `agentPersonaInstructions`, so it lands after Codex's Default-mode "ask the user directly" text and Claude's system prompt without editing `CodexDeveloperInstructions.ts`. Two adapter appends consume the flag through J5-owned leaf `j5/a2a/crewSeatQuestions.ts`: `makeClaudeAdapterV2`'s `openQuery` spreads `j5ClaudeCrewSeatQueryOverrides(turnInput.runtimePolicy)` into `makeClaudeQueryOptions` (`disallowedTools: ["AskUserQuestion"]`, the SDK option that already existed but was never set), and `codexThreadRuntimeParams` spreads `j5CodexCrewSeatConfig(input.runtimePolicy)` into the thread `config` beside `mcp_servers` (dotted session overrides `features.default_mode_request_user_input = false` and `tools.experimental_request_user_input.enabled = false`, sent on thread start, resume, and fork only when the t3-code MCP session is attached). Cursor, Grok, OpenCode, Pi, ACP registry agents, and Antigravity are not supported on seats yet: they get only the seat brief's instruction (`spawnFirstTurnText` in `j5/a2a/spawnIds.ts`). Captains and every other thread keep their native question tools. Tests: `j5/a2a/crewSeatRuntime.test.ts` (seat, Captain, person thread, persona seat, archived Crew, no SQL client), `j5/a2a/crewSeatQuestions.test.ts` (Codex config and developer-text order), and one `ClaudeAdapterV2.test.ts` case, "withholds AskUserQuestion only from a Crew seat", on the existing wake harness. On every rebase, verify both spreads survive and that upstream has not renamed the Codex switches or added another native question tool.

## Pin and upstream advance runbook

Current candidate pin: `67a2be0fdbee7afb64b691f147ed286a108c706b`, from `t3code/codex-turn-mapping` (frozen 2026-09-24). The previous pin was `62aef8587c0263bed5ae2b26c38d6d138a0676f6`; its integration and this advance are recorded below.
Expand Down Expand Up @@ -523,13 +525,13 @@ indicate approval. The deleted hook remains explicitly marked.
| `apps/server/src/http.ts` | R | B |
| `apps/server/src/mcp/McpHttpServer.ts` | R | 2 |
| `apps/server/src/mcp/toolkits/worktree/registration.test.ts` | A | 4, 45 |
| `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts` | A | 8, Saved-agent mentions |
| `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` | N | Saved-agent mentions |
| `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts` | A | 8, 47, Saved-agent mentions |
| `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` | N | 47, Saved-agent mentions |
| `apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts` | A | 8, 28, G, Saved-agent mentions |
| `apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts` | A | 28, G, Saved-agent mentions |
| `apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts` | A | 28, 47, G, Saved-agent mentions |
| `apps/server/src/orchestration-v2/EffectOutbox.ts` | R | 26 |
| `apps/server/src/orchestration-v2/FoundationPersistence.test.ts` | N | T |
| `apps/server/src/orchestration-v2/ProviderAdapter.ts` | R | N |
| `apps/server/src/orchestration-v2/ProviderAdapter.ts` | R | 47, N |
| `apps/server/src/orchestration-v2/ProviderTurnControlService.ts` | R | 26 |
| `apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts` | A | 28, N |
| `apps/server/src/orchestration-v2/ProviderTurnStartService.ts` | A | 28, 30, N |
Expand Down
61 changes: 61 additions & 0 deletions apps/server/src/j5/a2a/crewSeatQuestions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { assert, describe, it } from "@effect/vitest";
import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";

import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import {
buildCodexTurnStartParams,
codexThreadRuntimeParams,
} from "../../orchestration-v2/Adapters/CodexAdapterV2.ts";
import { ProviderAdapterV2RuntimePolicy } from "../../orchestration-v2/ProviderAdapter.ts";
import { CREW_SEAT_QUESTION_INSTRUCTIONS, J5_CODEX_CREW_SEAT_CONFIG } from "./crewSeatQuestions.ts";
import { withCrewSeatQuestions } from "./crewSeatRuntime.ts";

const basePolicy = ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: "/workspace",
});
const seatPolicy = withCrewSeatQuestions(basePolicy);

describe("Crew seat question tools on Codex", () => {
it("turns request_user_input off in a seat's thread config and nowhere else", () => {
const threadId = ThreadId.make("thread-codex-crew-seat");
McpProviderSession.setMcpProviderSession({
environmentId: EnvironmentId.make("environment-codex-crew-seat"),
threadId,
providerSessionId: "mcp-session-codex-crew-seat",
providerInstanceId: ProviderInstanceId.make("codex"),
endpoint: "http://127.0.0.1:43123/mcp",
authorizationHeader: "Bearer secret-codex-token",
browserToolsAvailable: true,
});
try {
const seat = codexThreadRuntimeParams({ threadId, runtimePolicy: seatPolicy }).config;
assert.strictEqual(seat?.["features.default_mode_request_user_input"], false);
assert.strictEqual(seat?.["tools.experimental_request_user_input.enabled"], false);
assert.property(seat, "mcp_servers");

const captain = codexThreadRuntimeParams({ threadId, runtimePolicy: basePolicy }).config;
for (const key of Object.keys(J5_CODEX_CREW_SEAT_CONFIG)) assert.notProperty(captain, key);
} finally {
McpProviderSession.clearMcpProviderSession(threadId);
}
});

it.effect("puts the ask-your-Captain rule after Codex's ask-the-user text", () =>
Effect.gen(function* () {
const params = yield* buildCodexTurnStartParams({
nativeThreadId: "native-crew-seat",
codexInput: [{ type: "text", text: "review the change" }],
runtimePolicy: seatPolicy,
modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" },
hasT3Mcp: true,
});
const developer = params.collaborationMode?.settings.developer_instructions ?? "";
const askUser = developer.indexOf("ask the user directly");
assert.isAtLeast(askUser, 0);
assert.isAbove(developer.indexOf(CREW_SEAT_QUESTION_INSTRUCTIONS), askUser);
}),
);
});
49 changes: 49 additions & 0 deletions apps/server/src/j5/a2a/crewSeatQuestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { ProviderAdapterV2RuntimePolicy } from "../../orchestration-v2/ProviderAdapter.ts";

/**
* Crew seats do not get native question tools (issue #325, Jackson's 2026-09-26 decision). A seat
* runs out of the person's view, so a question it raises in its own thread waits unseen. A seat
* that needs a decision asks its Captain with `send_message`; the Captain answers, or asks the
* person with its own question tool, inline in the Captain thread the person watches. Captains
* and every other thread keep their native question tools.
*
* The runtime policy carries `crewSeat` (resolved in `crewSeatRuntime.ts`); the adapters spread
* these helpers where they already build their native options. Kept as a leaf module so the
* adapters import no Crew store code.
*/

/** Appended to a seat's standing instructions, after any provider text that says to ask the user. */
export const CREW_SEAT_QUESTION_INSTRUCTIONS = `## Crew seat: questions go to your Captain

You hold a seat in a Crew, and the person does not watch this thread. When you need a decision, a clarification, or an approval, ask your Captain with \`send_message\` (\`expect_reply=true\`, with an \`intent\`), using the captain_participant_id from your crew context, then continue any work that does not depend on the answer. Your Captain answers, or asks the person. Never ask the person directly: do not end a turn on a question for the user and do not use a native question tool. This replaces any earlier instruction to ask the user directly.`;

type CrewSeatPolicy = Pick<ProviderAdapterV2RuntimePolicy, "crewSeat"> | undefined;

const isCrewSeat = (policy: CrewSeatPolicy) => policy?.crewSeat === true;

/** Claude's native question tool; the SDK removes a disallowed tool from the model's context. */
export const J5_CLAUDE_CREW_SEAT_DISALLOWED_TOOLS: ReadonlyArray<string> = ["AskUserQuestion"];

/** Spread into `makeClaudeQueryOptions` input: `disallowedTools` for a seat, nothing otherwise. */
export const j5ClaudeCrewSeatQueryOverrides = (
policy: CrewSeatPolicy,
): { readonly disallowedTools?: ReadonlyArray<string> } =>
isCrewSeat(policy) ? { disallowedTools: J5_CLAUDE_CREW_SEAT_DISALLOWED_TOOLS } : {};

/**
* Codex's `request_user_input` switches, as dotted session overrides (the same shape as
* `codex -c key=value`), so neither replaces the user's own `[features]` or `[tools]` tables.
* `features.default_mode_request_user_input` offers the tool in Default mode;
* `tools.experimental_request_user_input.enabled` is the tool's own switch. Both are booleans in
* Codex 0.155's config schema (a non-boolean value fails config load).
*/
export const J5_CODEX_CREW_SEAT_CONFIG = {
"features.default_mode_request_user_input": false,
"tools.experimental_request_user_input.enabled": false,
} as const;

/** Spread into the Codex thread `config`: the question-tool switches for a seat, nothing otherwise. */
export const j5CodexCrewSeatConfig = (
policy: CrewSeatPolicy,
): Partial<typeof J5_CODEX_CREW_SEAT_CONFIG> =>
isCrewSeat(policy) ? J5_CODEX_CREW_SEAT_CONFIG : {};
Loading
Loading