From 313394bd600e76c4eca7791ead2f3eac8e1b23a9 Mon Sep 17 00:00:00 2001 From: mchwang Date: Thu, 1 Oct 2026 13:49:27 -0700 Subject: [PATCH 1/2] D: pass the plan schema to Claude; refuse Codex in planning and questions (#75) Claude's planning command now carries the mounted schema as --json-schema. The container profile refuses a command whose schema text differs from the mounted file, and the adapter returns Claude's structured_output as bare JSON. Codex 0.153.4 reads files only through its shell, which read-only phases keep off. Decision (c): Codex is refused in planning and questions, so Ask is Claude-only. The decision is recorded in docs/implementation/agent-isolation.md. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- agents/adapters/claude.ts | 45 +++++++++-- agents/adapters/codex.ts | 7 +- agents/container/profile.ts | 13 ++- agents/policy.ts | 68 +++++++++++++--- docs/implementation/agent-isolation.md | 58 +++++++++++++- docs/implementation/planning-audit.md | 5 +- docs/implementation/read-only-review.md | 2 +- docs/plan-format.md | 6 +- prompts/plan-author.md | 52 ++++-------- runner/question-container.ts | 25 +++--- runner/question-worker.ts | 2 - runner/store.ts | 4 +- test/agent-adapter-cleanup.test.ts | 6 +- test/agent-adapter.test.ts | 101 +++++++++++++++++++++++- test/agent-container.test.ts | 19 ++++- test/agent-launch.test.ts | 6 +- test/agent-policy.test.ts | 69 ++++++++++++++-- test/agent-supervisor.test.ts | 36 +++++++-- test/browser/review.spec.ts | 6 +- test/question-agent.test.ts | 32 ++++---- test/questions.test.ts | 19 ++++- web/cli.ts | 2 +- web/public/app.js | 7 +- 24 files changed, 456 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index 8fb36fd1..53bc4ecc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Review agent-made Git changes one plan item at a time. The approved plan lists each item's files and acceptance checks; the review engine shows which item produced each change and flags foreign or overlapping work. -**Status:** the plan/linking library, SQLite store, and local review screen are implemented. Run `npm run demo` and open its private local URL. Ask runs Claude Code or Codex inside the locked-down agent container for read-only answers; choose the provider in Settings. Ask needs Docker, plus `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`) for Claude or a Codex `auth.json` (`CODEBOOST_CODEX_AUTH_FILE`, default `~/.codex/auth.json`). The first question builds the agent image, which can take a few minutes. A configured GitHub review can merge only after the guarded exact-head gate passes. The agent container, vendor-only network and Claude/Codex adapters are implemented ([agent isolation](docs/implementation/agent-isolation.md)); only Ask uses them so far. Automated rebasing, plan command execution, and code-writing agents are not implemented. The paired human review experiment was cancelled before results were recorded and no longer blocks roadmap work; optional future validation is tracked in [#19](https://github.com/codeabovelab/codeboost/issues/19). +**Status:** the plan/linking library, SQLite store, and local review screen are implemented. Run `npm run demo` and open its private local URL. Ask runs Claude Code inside the locked-down agent container for read-only answers; choose it in Settings. Codex cannot answer questions yet, because it reads code only by running commands ([why](docs/implementation/agent-isolation.md#codex-in-read-only-phases)). Ask needs Docker, plus `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`). The first question builds the agent image, which can take a few minutes. A configured GitHub review can merge only after the guarded exact-head gate passes. The agent container, vendor-only network and Claude/Codex adapters are implemented ([agent isolation](docs/implementation/agent-isolation.md)); only Ask uses them so far. Automated rebasing, plan command execution, and code-writing agents are not implemented. The paired human review experiment was cancelled before results were recorded and no longer blocks roadmap work; optional future validation is tracked in [#19](https://github.com/codeabovelab/codeboost/issues/19). ## Development diff --git a/agents/adapters/claude.ts b/agents/adapters/claude.ts index aca31331..92a8e299 100644 --- a/agents/adapters/claude.ts +++ b/agents/adapters/claude.ts @@ -1,15 +1,42 @@ +import { join } from 'node:path'; import type { InvocationHandle } from '../contract.ts'; -import { createClaudeCommand, createPhasePolicy } from '../policy.ts'; +import { readCapturedFile } from '../container/profile.ts'; +import { createClaudeCommand, createPhasePolicy, MAX_COMMAND_SCHEMA_BYTES } from '../policy.ts'; import { launchInvocation } from './supervisor.ts'; import { setUpProfile } from './setup.ts'; import { assertAdapterRequest, createAdapterInvocationBudget, type AgentAdapterOptions, type AgentAdapterRequest } from './types.ts'; -export function parseClaudeOutput(raw: Buffer): { text: string; providerFailed: boolean } { +/** + * Read Claude's `--output-format json` envelope. A result whose `subtype` is not `success`, or whose `is_error` is + * true, is a provider failure; it can omit `result`. With `--json-schema` (planning), a successful answer is the + * schema-validated `structured_output` object, returned as JSON text; `result` then holds only Claude's prose. + */ +export function parseClaudeOutput(raw: Buffer, structured = false): { text: string; providerFailed: boolean } { const envelope = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(raw)) as - { result?: unknown; is_error?: unknown }; - if (typeof envelope.result !== 'string' || typeof envelope.is_error !== 'boolean') + { subtype?: unknown; result?: unknown; is_error?: unknown; structured_output?: unknown }; + if (typeof envelope.is_error !== 'boolean' || (envelope.result !== undefined && typeof envelope.result !== 'string') + || (envelope.subtype !== undefined && typeof envelope.subtype !== 'string')) throw new Error('Claude returned a malformed output envelope.'); - return Object.freeze({ text: envelope.result, providerFailed: envelope.is_error }); + const prose = envelope.result ?? ''; + if (envelope.is_error || (envelope.subtype !== undefined && envelope.subtype !== 'success')) + return Object.freeze({ text: prose, providerFailed: true }); + if (!structured) { + if (envelope.result === undefined) throw new Error('Claude returned a malformed output envelope.'); + return Object.freeze({ text: prose, providerFailed: false }); + } + const answer = envelope.structured_output; + if (answer === null || typeof answer !== 'object' || Array.isArray(answer)) + throw new Error('Claude returned no structured output for a schema-constrained answer.'); + return Object.freeze({ text: JSON.stringify(answer), providerFailed: false }); +} + +/** + * Read the planning schema for `--json-schema` with the profile's bounded no-follow reader. The profile captures the + * mounted file again and refuses a command whose schema differs. + */ +export function readPlanningSchema(inputDirectory: string): string { + const { content } = readCapturedFile(join(inputDirectory, 'schema.json'), 'Planning schema', MAX_COMMAND_SCHEMA_BYTES); + return new TextDecoder('utf-8', { fatal: true }).decode(content); } export function startClaudeInvocation(request: AgentAdapterRequest, @@ -17,11 +44,13 @@ export function startClaudeInvocation(request: AgentAdapterRequest, if (!oauthToken || oauthToken.includes('\0')) throw new Error('Claude OAuth token is malformed.'); const policy = createPhasePolicy(request.invocation); const remaining = createAdapterInvocationBudget(request.invocation, options.timeoutMs); + const structured = policy.phase === 'planning'; // Invalid input throws here, before anything is allocated; only Docker setup runs inside the handle. + const command = createClaudeCommand(policy, request.prompt, + structured ? readPlanningSchema(request.inputDirectory) : undefined); assertAdapterRequest(request); return launchInvocation(request.invocation, remaining, (signal, start) => setUpProfile(request, remaining, signal, - network => ({ ...request, policy, network, command: createClaudeCommand(policy, request.prompt), - claudeToken: oauthToken }), + network => ({ ...request, policy, network, command, claudeToken: oauthToken }), profile => start(profile, { ...options, secrets: { CLAUDE_CODE_OAUTH_TOKEN: oauthToken }, - invocationBudget: remaining, decode: (_profile, raw) => parseClaudeOutput(raw) }))); + invocationBudget: remaining, decode: (_profile, raw) => parseClaudeOutput(raw, structured) }))); } diff --git a/agents/adapters/codex.ts b/agents/adapters/codex.ts index 21739953..b882f20f 100644 --- a/agents/adapters/codex.ts +++ b/agents/adapters/codex.ts @@ -26,11 +26,12 @@ export function startCodexInvocation(request: AgentAdapterRequest, if (!authFile || authFile.includes('\0')) throw new Error('Codex auth path is malformed.'); const policy = createPhasePolicy(request.invocation); const remaining = createAdapterInvocationBudget(request.invocation, options.timeoutMs); - // Invalid input throws here, before anything is allocated; only Docker setup runs inside the handle. + // Invalid input, including a planning or questions phase, throws here, before anything is allocated; only Docker + // setup runs inside the handle. + const command = createCodexCommand(policy, request.prompt); assertAdapterRequest(request); return launchInvocation(request.invocation, remaining, (signal, start) => setUpProfile(request, remaining, signal, - network => ({ ...request, policy, network, command: createCodexCommand(policy, request.prompt), - codexAuthFile: authFile, deferredOutput: true }), + network => ({ ...request, policy, network, command, codexAuthFile: authFile, deferredOutput: true }), profile => start(profile, { ...options, invocationBudget: remaining, decode: (current, _raw, maximum, timeoutMs, decodeSignal) => readCodexOutput(codexContainer(current), maximum, timeoutMs, decodeSignal) }))); diff --git a/agents/container/profile.ts b/agents/container/profile.ts index 4176b07a..f654dba3 100644 --- a/agents/container/profile.ts +++ b/agents/container/profile.ts @@ -9,7 +9,7 @@ import { assertTaskFilesystems, taskFilesystemOwner, type TaskFilesystems } from import { ownerLabelArgs, type ResourceOwner } from '../labels.ts'; import { assertVendorNetwork, removeVendorNetwork, vendorNetworkResources, type VendorNetwork } from '../network/network.ts'; -import { assertAgentCommand, assertPhasePolicy, type AgentCommand, type PhasePolicy } from '../policy.ts'; +import { assertAgentCommand, assertCommandSchema, assertPhasePolicy, type AgentCommand, type PhasePolicy } from '../policy.ts'; export interface ContainerProfile { readonly name: string; readonly args: readonly string[]; @@ -90,16 +90,20 @@ const removeOwnedDirectories = (directories: readonly string[]) => { if (failures.length) throw new AggregateError(failures, 'Profile snapshot cleanup did not settle.'); }; -const readCapturedFile = (path: string, kind: string): { identity: FileIdentity; content: Buffer } => { +/** + * Read a regular, unlinked file through one no-follow descriptor, so the path cannot be swapped between check and + * read. `O_NONBLOCK` makes a FIFO fail the regular-file check instead of blocking the open. + */ +export const readCapturedFile = (path: string, kind: string, maximum = 1024 * 1024): + { identity: FileIdentity; content: Buffer } => { let fd: number | undefined; try { - try { fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); } + try { fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ELOOP') throw new Error(`${kind} must be a direct regular file, not a link.`); throw error; } const before = fstatSync(fd); - const maximum = 1024 * 1024; if (!before.isFile() || before.nlink !== 1 || before.size > maximum) throw new Error(`${kind} must be a bounded, unlinked regular file.`); const bounded = Buffer.allocUnsafe(maximum + 1); @@ -244,6 +248,7 @@ export async function createContainerProfile(options: ProfileOptions): Promise(); -const commands = new WeakMap(); +const commands = new WeakMap(); -const command = (policy: PhasePolicy, argv: readonly string[]): AgentCommand => { +const command = (policy: PhasePolicy, argv: readonly string[], schema?: string): AgentCommand => { assertPhasePolicy(policy); const value = Object.freeze({ argv: Object.freeze([...argv]) }); - commands.set(value, Object.freeze({ policy, vendor: assertPhasePolicy(policy).vendor })); + commands.set(value, Object.freeze({ policy, vendor: assertPhasePolicy(policy).vendor, schema })); return value; }; +/** The largest answer schema a command carries in its argv. The v1 plan-edit schema is under 10 KiB. */ +export const MAX_COMMAND_SCHEMA_BYTES = 64 * 1024; + +/** + * A command that carries an answer schema in its argv must carry the exact schema the profile mounts, so the vendor + * validates against the same schema the runner validates against. + */ +export function assertCommandSchema(value: AgentCommand, mounted: Buffer): void { + const schema = commands.get(value)?.schema; + if (schema !== undefined && !Buffer.from(schema, 'utf8').equals(mounted)) + throw new Error('Command answer schema does not match the mounted schema.'); +} + export function assertAgentCommand(value: AgentCommand, policy: PhasePolicy, vendor?: InvocationInput['vendor']): readonly string[] { const identity = commands.get(value); @@ -62,16 +76,50 @@ export function dispatchApprovedCommand(policy: PhasePolicy, argv: readonly s return execute(Object.freeze([...argv])); } -export function createClaudeCommand(policy: PhasePolicy, prompt: string): AgentCommand { +/** + * Planning answers must match the request's schema, so the planning command carries it. Claude's `--json-schema` takes + * the schema text, not a path; the profile checks that text against the mounted file. Questions answer in plain text + * (their schema is `{"type":"string"}`), and the other phases return no JSON answer, so they carry no schema. + */ +export function createClaudeCommand(policy: PhasePolicy, prompt: string, schema?: string): AgentCommand { if (!prompt || prompt.includes('\0')) throw new Error('Claude prompt must be nonempty and contain no NUL.'); if (assertPhasePolicy(policy).vendor !== 'claude') throw new Error('Claude command requires a Claude invocation policy.'); + if ((policy.phase === 'planning') !== (schema !== undefined)) + throw new Error('Only the Claude planning command carries an answer schema, and it must.'); + const schemaArguments = schema === undefined ? [] : ['--json-schema', assertPlanningSchema(schema)]; const writable = policy.worktree === 'read-write'; const allowed = writable ? 'Read,Glob,Grep,Edit,Write' : 'Read,Glob,Grep'; // `--` ends option parsing, so a prompt beginning with `-` stays prompt data. - return command(policy, ['claude', '--print', '--output-format', 'json', '--restricted', '--strict-mcp-config', - '--mcp-config', '{"mcpServers":{}}', '--disable-slash-commands', '--no-chrome', '--permission-prompts', 'none', - '--permission-mode', writable ? 'acceptEdits' : 'plan', '--tools', allowed, '--allowedTools', allowed, - '--disallowedTools', 'Bash,WebFetch,WebSearch,NotebookEdit', '--add-dir', '/run/codeboost-input', '--', prompt]); + return command(policy, ['claude', '--print', '--output-format', 'json', ...schemaArguments, '--restricted', + '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}', '--disable-slash-commands', '--no-chrome', + '--permission-prompts', 'none', '--permission-mode', writable ? 'acceptEdits' : 'plan', '--tools', allowed, + '--allowedTools', allowed, '--disallowedTools', 'Bash,WebFetch,WebSearch,NotebookEdit', '--add-dir', + '/run/codeboost-input', '--', prompt], schema); +} + +/** + * A planning schema must describe a JSON object (Claude returns `structured_output` as an object) and fit one command + * argument. Returns the schema unchanged. + */ +export function assertPlanningSchema(schema: string): string { + if (schema.includes('\0') || Buffer.byteLength(schema, 'utf8') > MAX_COMMAND_SCHEMA_BYTES) + throw new Error('Planning schema must contain no NUL and fit the command schema limit.'); + let parsed: unknown; + try { parsed = JSON.parse(schema); } catch { throw new Error('Planning schema must be JSON.'); } + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed) + || (parsed as { type?: unknown }).type !== 'object') + throw new Error('Planning schema must be a JSON object schema with "type": "object".'); + return schema; +} + +/** + * Codex 0.153.4 reads files only through its shell, and planning and questions run no process. Codex would answer + * without seeing the code or the schema, so these phases are refused rather than weakened (#75; see + * docs/implementation/agent-isolation.md, "Codex in read-only phases"). + */ +export function assertCodexPhase(phase: Phase): void { + if (phase === 'planning' || phase === 'questions') + throw new Error(`Codex cannot run the ${phase} phase: it has no file-reading tool that runs no process.`); } export function codexBaseArguments(policy: PhasePolicy): readonly string[] { @@ -82,9 +130,11 @@ export function codexBaseArguments(policy: PhasePolicy): readonly string[] { export function createCodexCommand(policy: PhasePolicy, prompt: string): AgentCommand { if (!prompt || prompt.includes('\0')) throw new Error('Codex prompt must be nonempty and contain no NUL.'); + const base = codexBaseArguments(policy); + assertCodexPhase(policy.phase); const sandbox = policy.worktree === 'read-write' ? 'workspace-write' : 'read-only'; // `--` ends option parsing, so a prompt beginning with `-` stays prompt data. - return command(policy, [...codexBaseArguments(policy), 'exec', '--sandbox', sandbox, '--skip-git-repo-check', + return command(policy, [...base, 'exec', '--sandbox', sandbox, '--skip-git-repo-check', '--output-last-message', '/run/codeboost-output/final.txt', '--', prompt]); } diff --git a/docs/implementation/agent-isolation.md b/docs/implementation/agent-isolation.md index d38bf470..dedb160a 100644 --- a/docs/implementation/agent-isolation.md +++ b/docs/implementation/agent-isolation.md @@ -30,10 +30,12 @@ Each row is a T9 requirement for the Docker suite. The suite fails if any row fa | Isolation holds: non-root, no capabilities, read-only root, no host paths or secrets, vendor-only egress | `agent-container`: read-only isolation, lockdown and mount validation; `agent-network`: egress and DNS | | A read-only phase cannot write `/work` | `agent-container`: phase worktree for all five phases | | Planning and questions cannot run a process | `agent-policy`: tool sets exclude the command tool, and command dispatch refuses these phases | +| Codex does not run planning or questions, because it cannot read code there (see [Codex in read-only phases](#codex-in-read-only-phases)) | `agent-policy`: the Codex command refuses these phases; `agent-adapter`: the Codex start call refuses them and allocates nothing | +| A planning answer is checked against the mounted schema | `agent-policy`: only the Claude planning command passes `--json-schema`; `agent-container`: the profile refuses a command whose schema differs from the mounted file; `agent-adapter`: the answer is read from `structured_output` | | Task and scratch byte and inode limits hold | `agent-container`: task capacity; scratch capacity for Codex and Claude (`/tmp`, `HOME`, `CODEX_HOME`, output directory) | | Hard links and alias writes from `.git/config` and objects fail, and metadata stays unchanged | `agent-container`: metadata alias probe in planning, review and execute, with a digest of `.git` before and after | | Mountpoint replacement fails | `agent-container`: metadata and metadata alias probes (`mv` and `rm -rf` of `.git`) | -| Both vendor startup probes read the schema and return bounded valid output through their documented channel | `agent-supervisor` live probes: Codex through its output file, Claude through its stdout envelope (credentials required) | +| Both vendor startup probes read the schema and return bounded valid output through their documented channel | `agent-supervisor` live probes (credentials required): Codex in review through its output file; Claude in questions through its stdout envelope; Claude in planning returns a schema-constrained answer as bare JSON. **Known failure:** the Codex probes (here and in `agent-container`) fail, because Codex cannot read files in any phase (see [Codex in read-only phases](#codex-in-read-only-phases)). | | Hostile input stays inside the boundary | `agent-container`: repositories with links that leave the checkout are refused, links inside the checkout still work, oversized repositories fail closed; `agent-policy`: option-like prompts; `agent-proxy`: hostile CONNECT traffic; `agent-supervisor`: hostile output | ## Why the gate can fail @@ -81,6 +83,15 @@ Use only these entry points to run an agent: later failure settles the handle. `cancel()` during setup kills the in-flight Docker call. The request carries `networkAllocationId`, a UUID you choose for the vendor network. Pass the vendor credential only as the function argument. + - **Planning answers.** Use `startClaudeInvocation` for planning. It reads `schema.json` from the input directory + and passes its exact text to Claude as `--json-schema`. The container profile refuses the command if that text + differs from the file it mounts. The result's `stdout` is Claude's `structured_output` object as JSON text, with + no prose or Markdown around it. A Claude run that fails its own schema check settles with a nonzero exit code. + Questions answer in plain text and carry no schema flag. The schema file must be a regular file with one link, + at most 64 KiB of UTF-8, and a JSON object with `"type": "object"`; otherwise the start call throws. Leave out a + draft 2020-12 `$schema` line: Claude's `--json-schema` rejects it (see [plan format](../plan-format.md)). + - **Codex.** `startCodexInvocation` throws for planning and questions, and allocates nothing. See + [Codex in read-only phases](#codex-in-read-only-phases). 5. To keep a stopped writable attempt's partial output, call `exportTaskDiff(storage, { base, imageId })` before `removeTaskFilesystems`. `base` is the full ID of the commit the storage was seeded from (the clone's head). For a recovery handle, also pass the `metadataBaseline` you recorded (step 6). It returns at most 1 MiB of diff @@ -192,6 +203,42 @@ The caller must do the following: - Treat `stopReason` as the result of the invocation. A missing `stopReason` means the agent finished normally. +## Codex in read-only phases + +Decision for #75, recorded 2026-10-01. + +**Problem.** Codex 0.153.4 reads files only through its shell. D turns the shell off +(`features.shell_tool=false`), because planning and questions must not run a process +(design, "Phase enforcement"). So in these phases Codex cannot see `/work` or the +mounted schema. E4's recordings showed this: Codex planned without the code and asked +for the schema. + +**Options considered.** + +| Option | Result | +| --- | --- | +| a. Turn the shell on for Codex in read-only phases | Rejected. It breaks the rule that planning and questions run no process, and the design says to refuse a phase that an adapter cannot enforce. | +| b. Keep the shell off and put the schema and chosen files in the prompt | Rejected. The 32 KiB prompt limit caps how much code Codex sees, and E2 would have to choose the files. | +| c. Use Claude only for planning and questions | **Chosen.** | + +**What this changes.** + +- `createCodexCommand` and `startCodexInvocation` refuse the `planning` and `questions` + phases. The start call refuses before it allocates anything. +- Ask offers only Claude Code. The Store refuses Codex as the question agent. A review + database that already names Codex reads back as Codex, and Ask refuses it with a + message that tells the user to choose Claude Code. +- The Ask worker receives only `CLAUDE_CODE_OAUTH_TOKEN` as credential data. +- E4 records Claude only: one draft and one suggestion. + +**Codex in other phases.** The same shell setting applies to review, execute and fix. +On 2026-10-01 the live Codex probe in review answered that it could not read the file. +So Codex cannot read code in any phase. That is a separate decision from this one. + +**When to revisit.** Allow Codex in these phases again when a pinned Codex version has +a file-reading tool that runs no process. Then pass the mounted schema to it with +`--output-schema /run/codeboost-input/schema.json` and add a Codex planning probe. + ## First consumer: Ask Ask (`runner/question-container.ts`) is the first production caller. It follows the four entry points above in the @@ -208,8 +255,9 @@ Ask keeps the contract's identity and cleanup rules: worker reply carry that attempt and the captured context. The Store then compares the attempt before saving it. - The worker's environment is an allowlist: `PATH`, `DOCKER_HOST` and its Ask root as `TMPDIR`. Every setup subprocess, including the image build, inherits only that, so no credential, home directory, Docker config or - agent socket reaches it. The credential lookup's own variables (`CLAUDE_CODE_OAUTH_TOKEN`, - `CODEBOOST_CODEX_AUTH_FILE`, `CODEX_HOME`, `HOME`) reach the worker as data and go only to the adapters. The + agent socket reaches it. The credential lookup's only variable, `CLAUDE_CODE_OAUTH_TOKEN`, reaches the worker as + data and goes only to the Claude adapter. Ask refuses Codex before any Docker work (see + [Codex in read-only phases](#codex-in-read-only-phases)). The leftover Docker queries use the same `PATH`/`DOCKER_HOST` environment as lane D. Missing sign-in is reported before any Docker work. - Lane D's clone is a full host copy with no byte limit of its own. Before cloning, Ask measures the checkout at the @@ -286,5 +334,7 @@ against real Docker; its live case, like the vendor probes above, needs `CODEBOO ## Limits of this gate - CI does not run the live vendor probes. Run them locally with credentials before - a release that changes the image, the adapters or the prompts. + a release that changes the image, the adapters or the prompts. The two Codex probes + fail until Codex can read files in its phases; that failure is expected, not a + regression. - T9 as a whole is complete only when lane F runs every suite in required CI (F6). diff --git a/docs/implementation/planning-audit.md b/docs/implementation/planning-audit.md index 66f192e3..00942e9d 100644 --- a/docs/implementation/planning-audit.md +++ b/docs/implementation/planning-audit.md @@ -38,8 +38,9 @@ The PR body records the final validated head separately from this baseline. cancellation and shutdown need explicit lifecycle rules and controlled race tests. 3. **E4:** Exercise imports, malformed provider output, hostile prompt data, delayed responses and replay end to end through the E interface and real SQLite authority. - Synthetic provider fixtures must be labeled as such. Real recorded Claude/Codex - output and OS/container enforcement cannot be claimed from fake-provider tests. + Synthetic provider fixtures must be labeled as such. Real recorded Claude output + and OS/container enforcement cannot be claimed from fake-provider tests. Codex does + not plan (#75), so E4 records Claude only. ## Ownership and integration boundaries diff --git a/docs/implementation/read-only-review.md b/docs/implementation/read-only-review.md index 22dea4a0..23ef1e4c 100644 --- a/docs/implementation/read-only-review.md +++ b/docs/implementation/read-only-review.md @@ -65,7 +65,7 @@ Snippet-feedback validation: 176 unit/integration tests and 15 browser tests pas Scope update requested during the PR #597 walkthrough: Ask now invokes a read-only question agent. This supersedes the earlier “no agent answers” limitation only for questions. Request change still saves feedback without running edits, and answering never changes a plan revision or approval. -Open Settings and choose Claude Code or Codex. The choice persists in this review database (schema v3 adds app_settings); existing notes and snippet references survive migration. No provider is selected automatically. The installed CLI must already be signed in. The question, selected snippet, plan item, changed code, checks and bounded prior conversation are sent to the selected provider. Truncation and absent evidence are part of the context; the agent must not claim unrun tests passed. +Open Settings and choose Claude Code. (Codex was offered here until #75; it cannot answer questions because it reads code only by running commands. See [Codex in read-only phases](agent-isolation.md#codex-in-read-only-phases).) The choice persists in this review database (schema v3 adds app_settings); existing notes and snippet references survive migration. No provider is selected automatically. The installed CLI must already be signed in. The question, selected snippet, plan item, changed code, checks and bounded prior conversation are sent to the selected provider. Truncation and absent evidence are part of the context; the agent must not claim unrun tests passed. The question is saved before launch. Conversation displays Answering, then a persisted answer or an error with Retry answer. Retries reuse the question and have attempt IDs to reject late results from older attempts. At most two requests run per server; each has a two-minute deadline. Graceful shutdown cancels running answers; after a crash, pending attempts become retryable after their lease expires. A question from an older snapshot must be asked again against current code. Answers retain their provider and original question snapshot. Polling updates only notes, preserving the current draft and code selection. diff --git a/docs/plan-format.md b/docs/plan-format.md index 8a1bf7f6..8e25d38a 100644 --- a/docs/plan-format.md +++ b/docs/plan-format.md @@ -1,12 +1,12 @@ # The codeboost plan format (version 1) -**Who this is for.** Anyone who writes, imports, or builds code around a codeboost plan: people, and the Claude and Codex agents that draft plans. **What it is for.** It defines the one structure every plan must follow, so codeboost can import a plan from any source and check it the same way. It is written in plain language (ISO 24495-1:2023). +**Who this is for.** Anyone who writes, imports, or builds code around a codeboost plan: people, and the Claude agent that drafts plans. **What it is for.** It defines the one structure every plan must follow, so codeboost can import a plan from any source and check it the same way. It is written in plain language (ISO 24495-1:2023). ## Summary - A plan is a list of **plan items** for one GitHub issue. Each item says which files it will change, what changes in each file, and how to check the result. - One schema, [`schema/plan.schema.json`](../schema/plan.schema.json), defines the structure. It is the contract for three things: - 1. **Generating.** codeboost gives the schema to Claude (`claude -p --json-schema`) or Codex (`codex exec --output-schema`), so the agent's answer always has the right shape. + 1. **Generating.** codeboost gives the schema to Claude (`claude -p --json-schema`), so the agent's answer always has the right shape. Codex does not draft plans for now, because it cannot read the code without running commands ([why](implementation/agent-isolation.md#codex-in-read-only-phases)). 2. **Importing.** A plan in a YAML or JSON file, written by a person or another tool, is checked against the same schema. 3. **Suggesting.** The plan assistant's suggested edits follow a second schema, [`schema/plan-edit.schema.json`](../schema/plan-edit.schema.json). - YAML and JSON have exactly the same structure. YAML is for people; JSON is what the agents return. @@ -148,7 +148,7 @@ Issue text and agent-produced plan fields remain untrusted. The prompt builder s | Source | What happens | |---|---| -| **Claude or Codex drafts it** | codeboost runs the agent with the prompt in [`prompts/plan-author.md`](../prompts/plan-author.md) and passes the schema. The answer is a JSON plan. codeboost runs the checks after import and shows the plan on the Plans screen as a draft. | +| **Claude drafts it** | codeboost runs the agent with the prompt in [`prompts/plan-author.md`](../prompts/plan-author.md) and passes the schema. The answer is a JSON plan. codeboost runs the checks after import and shows the plan on the Plans screen as a draft. | | **You import a file** | On the Plans screen, choose "Import plan" and pick a `.yaml`, `.yml`, or `.json` file, or paste one. codeboost reads it, runs the schema and the checks after import, and saves it as the next draft revision. The file's `revision` is replaced by the next free number. | | **You edit on the Plans screen** | Each change is checked as you type. Approving saves the revision. | diff --git a/prompts/plan-author.md b/prompts/plan-author.md index 04d09a6d..aad42eac 100644 --- a/prompts/plan-author.md +++ b/prompts/plan-author.md @@ -1,25 +1,14 @@