From 28905f86e772b37a59f3afd8cbb1e5052ef14b3f Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 21:16:02 -0500 Subject: [PATCH 01/15] Stage phase submission instructions in a frozen guide --- examples/adversarial-implementation/README.md | 33 ++++-- .../components/Phase/Correct.md | 1 + .../components/Phase/Instructions.md | 35 ++---- .../components/Phase/Request.md | 3 +- .../components/Phase/Run.md | 2 + .../components/Phase/SubmissionGuide.ts | 52 +++++++++ .../Phase/SubmissionInstructions.md | 40 +++++++ .../tests/Delivery.test.md | 4 +- .../tests/Friction.test.md | 3 +- .../tests/ProductionRole.test.md | 109 ++++++++++++++++-- .../tests/agents/api-conflict-correction.md | 2 +- .../tests/agents/decision-planner-held.md | 2 +- .../tests/agents/decision-planner.md | 4 +- .../tests/agents/initialized-role.md | 3 +- .../tests/agents/preparation-architect.md | 2 +- .../tests/agents/preparation-implementor.md | 2 +- .../tests/agents/preparation-planner.md | 4 +- .../tests/agents/submission-result.md | 15 +++ .../production-components/NativeGuide.ts | 53 +++++++++ .../cli/tests/support/adversarial-journey.ts | 3 + 20 files changed, 321 insertions(+), 51 deletions(-) create mode 100644 examples/adversarial-implementation/components/Phase/SubmissionGuide.ts create mode 100644 examples/adversarial-implementation/components/Phase/SubmissionInstructions.md create mode 100644 examples/adversarial-implementation/tests/agents/submission-result.md create mode 100644 examples/adversarial-implementation/tests/production-components/NativeGuide.ts diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index 19dfac715..b1a91efc7 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -361,13 +361,32 @@ from claiming readiness or merging independently. ## Prepare requests without metadata repair turns -These rules apply to separate phase requests and Ticket role calls. -`Phase.Request` renders the complete prompt before entering an Agent, including -initial repository instructions, role instructions, assignment, selected text and -response schema. A missing selected component or input fails there; a packet -above 256000 characters refuses before provider dispatch. The selected evidence -keeps its separate 64000-character bound; the complete-prompt ceiling also accommodates -mandatory initialization instructions and the response schema. Nothing is truncated. +These rules apply to separate phase requests and Ticket role calls. Codex authors +prose committed to workflow documents, including guides, component instructions +and prompt templates. Claude roles may write internal runtime handoffs, hold +prompts and summaries. +`Phase.Run` freezes the submission guide in its reserved attempt directory. +`Phase.Request` checks that guide and prepares the phase prompt before entering +an Agent. The prompt includes initial repository and role instructions, +the assignment, selected text and an absolute reference to `submission-guide.md` +in the attempt directory. The guide contains the complete submission instructions +and the current role- and phase-specific JSON contract. Read it before returning +any result, including an early blocker, source conflict or unanswered decision. +The same retained native call reads the file and returns the result; staging the +guide adds no model call. + +The guide and its hash receipt are runner-owned. Preparation refuses occupied +paths and confirms that the published text is readable under current File +permissions and protection. After saving each raw reply, the workflow verifies +the guide and receipt against the expected submission instructions before it +accepts a result or starts a correction. Missing, unreadable or changed files +hold the attempt with its raw reply preserved. Corrections retain their existing +inline repair packet, original session and allowance. + +A missing selected component or input fails before provider dispatch. A prompt +above 256000 characters refuses without truncation. The guide has its own +256000-character and UTF-8-byte bounds. Selected evidence keeps its separate +64000-character allowance; moving submission instructions does not enlarge it. Provider configuration and client compatibility retain their existing checks before a turn starts. No paid prompt probes installation or repairs launch metadata. diff --git a/examples/adversarial-implementation/components/Phase/Correct.md b/examples/adversarial-implementation/components/Phase/Correct.md index 61734ee93..cd69431ab 100644 --- a/examples/adversarial-implementation/components/Phase/Correct.md +++ b/examples/adversarial-implementation/components/Phase/Correct.md @@ -80,6 +80,7 @@ shasum -a 256 -- {quoted}/request.json {quoted}/diagnostic.json {quoted}/origina {raw} + diff --git a/examples/adversarial-implementation/components/Phase/Instructions.md b/examples/adversarial-implementation/components/Phase/Instructions.md index f763ce210..ae6cf8648 100644 --- a/examples/adversarial-implementation/components/Phase/Instructions.md +++ b/examples/adversarial-implementation/components/Phase/Instructions.md @@ -16,12 +16,15 @@ props: default: [] assignment: type: object + submission: + type: object initialize: type: boolean default: false required: - role - assignment + - submission additionalProperties: false --- @@ -44,7 +47,9 @@ You remain the retained repository {props.role}. The previous owner's request is Do not draft, create or edit PR or issue titles or descriptions. The Sidekick running Codex owns that metadata and publication. Return technical decisions, code and evidence only; API documentation stays within the design assignment. Describe user-directed amendments separately from implementation defects, cite the recorded approval evidence and keep the existing correction allowances. This restriction also applies when a general role guide mentions opening a draft PR. -Execute the assigned phase through its stated deliverables. An assigned test or artifact that has not yet been written is remaining work. Continue it within this call. Use blocked only for a concrete external condition you cannot resolve within the assignment: quota, permission, provider failure, unavailable external dependency or genuinely missing input. Supply blocker.kind, reason, unblockCondition and evidence references. A source or design conflict returns needs_discovery; an unanswered human choice returns needs_decision. Use the supplied handoff, artifact paths and evidence first. Store every new plan, handoff, report, candidate and command receipt under assignment.artifactDirectory, the phase directory in this ticket workspace. Keep the source checkout and native cwd unchanged. Historical/source references retain their original locations; do not move or rewrite them. Return absolute references to new artifacts. Leave the runner-owned assignment.json, raw.txt, result.json, status.json and event files unchanged. Tie additional reads to a missing fact or an affected invariant. Reopen earlier conclusions when a named change invalidates them. State the missing fact when the assignment is insufficient. Retain mandatory repository and role requirements. +Codex authors prose committed to workflow documents, including guides, component instructions and prompt templates. Claude roles may write internal runtime handoffs, hold prompts and summaries. If the assignment needs workflow-document prose that Codex has not supplied, return needs_discovery with the required authoring work. + +Execute the assigned phase through its stated deliverables. An assigned test or artifact that has not yet been written is remaining work. Continue it within this call. Use blocked only for a concrete external condition you cannot resolve within the assignment: quota, permission, provider failure, unavailable external dependency or genuinely missing input. Supply blocker.kind, reason, unblockCondition and evidence references. A source or design conflict returns needs_discovery; an unanswered human choice returns needs_decision. Use the supplied handoff, artifact paths and evidence first. Store every new plan, handoff, report, candidate and command receipt under assignment.artifactDirectory, the phase directory in this ticket workspace. Keep the source checkout and native cwd unchanged. Preserve first failures, command receipts and the exact source inputs actually tested. An uncommitted candidate needs its own retained identity; a base HEAD is not its tested revision. Historical/source references retain their original locations; do not move or rewrite them. Return absolute references to new artifacts. Leave the runner-owned assignment.json, raw.txt, result.json, status.json, submission-guide.md, submission-guide.json and event files unchanged. Tie additional reads to a missing fact or an affected invariant. Reopen earlier conclusions when a named change invalidates them. State the missing fact when the assignment is insufficient. Retain mandatory repository and role requirements. Resolve the assigned design or planning question. Return the bounded proposal or accepted plan and the next owner's exact request. Return needs_decision for an unanswered human decision. Existing design and plan approval gates still apply. @@ -95,26 +100,10 @@ Recorded inputs: Prepared assignment: -Return assessment on completed implementation, changes_required, missing_evidence and escalate. For changes_required or missing_evidence, assess only the finite correction being requested, not the original feature. Keep already settled responsibilities closed; a missing test witness does not by itself reopen the production architecture. Completed implementation likewise reports the actual correction rather than inheriting the whole feature scope. Assess actual impact: mechanical, architecture and acceptance booleans; one short impact reason and individual evidence references, plus risk:null unless a concrete consequential uncertainty or risk needs additional reasoning. For high effort, risk contains its own short reason and supporting evidence. Merely touching concurrency, cancellation, durable state, replay or a public boundary is not that reason. Test-only changes can affect acceptance. A mechanical assessment has no architecture, acceptance or complexity impact. New non-mechanical feature work includes Planner acceptance; correction work requires only affected responsibilities. Keep evidence references in artifacts or already recorded documents. Do not request a reviewer or reasoning effort in prose to override this assessment. - -Return escalate only from medium Discovery or Review when a concrete newly discovered risk or uncertainty prevents completing this assignment. Keep the same revision and impact flags, supply risk with the specific reason and supporting evidence, and leave the checkout clean. The workflow resumes the same original role/session at high. High cannot escalate again. Implementation uses its assigned provider/effort and returns needs_discovery for a scope conflict. - -The workflow supplies omitted phase and work, the assigned revision for read-only phases, and the accepted continuation for complete/pass. Explicit conflicting metadata is refused. Implementation must report the actual resulting revision; evidence must name the inputs actually tested. - -Return next.phase and next.role explicitly when the outcome changes the prepared route. For complete/pass, copy assignment.continuation exactly; completed implementation routing is derived from the returned assessment and replaces that predicted continuation. For escalate, copy the current phase and role; the workflow derives and checks that same-owner continuation. For changes_required or missing_evidence, select implementation/implementor. For needs_discovery with apiConflict, select discovery/planner; otherwise select discovery/architect. For blocked or needs_decision, select hold/null. The program verifies this gate before another role can run; ready is a phase-loop result, not delivery approval. - -Keep summary to 1200 characters. Address handoff to next.role, or ROOT when next.phase is hold/ready. Write only that owner's next bounded action and necessary artifact references in at most 250 words and 1800 characters. Put command receipts in evidence and detailed reports in artifact files. Select only the short documents the next owner needs in handoffInputs. Each selected handoffInputs path must appear in your returned artifacts or the already recorded documents. A file existing on disk alone does not make it a recorded document. Keep full test logs, patches and historical ledgers in artifacts; listing an artifact does not send its contents. The selected-input limit is 64000 UTF-16 units for the recipient's entire owed packet, including undelivered accepted inputs and role guides. Use the recipient's recorded remaining allowance for new handoff material. Keep binding clauses, their conditions, exceptions, commands, identities and unresolved findings exact; reference full supporting artifacts. Ticket.Prepare checks the actual packet before dispatch and retains its receipt. This selected-input allowance is separate from the 256000-character complete-prompt ceiling. Use a new reference for changed contents. The workflow supplies accepted inputs once per retained role; do not request their repetition without a named change. Do not repeat the summary, command list, prior prompts, full plan, contract packet or narrative history in handoff. Omit blocker outside blocked outcomes. - -Reference individual UTF-8 text or JSON files in artifacts, evidence and handoffInputs. Do not return a directory or binary path as a document reference: the workflow reads each reference as a text file. For a group of files, save a text index naming the individual files and return that index. - -Before returning, check the required fields, assigned work/phase, actual revision, next role and phase, and character limits against the contract below. Omit optional metadata you cannot establish instead of guessing. - -Return exactly one JSON object conforming to this contract, without Markdown fences or surrounding prose. The handoff field is the complete ready-to-send request for the next owner; only that field becomes the next request. Artifacts and evidence travel as structured references. For a command on an uncommitted candidate, use evidence.revision=null and candidate=the retained candidate/command metadata path; a base HEAD is not its tested revision. Use a commit SHA only when the tested inputs correspond to that commit. Report observations as observations; schema validation does not establish test results, approvals or delivery readiness. - - - - - !["phase","work",...(props.assignment.phase === "implementation" ? [] : ["revision"])].includes(name)),"discoveryRequests","discoveryGaps"],additionalProperties:false,properties:{...schemas.result.properties,discoveryRequests:{...schemas.result.properties.discoveryRequests,items:authorRequest},discoveryGaps:{...schemas.result.properties.discoveryGaps,items:authorGap},phase:{const:props.assignment.phase},work:{const:props.assignment.work},outcome:phaseOutcome,contract:props.role === "architect" && props.assignment.phase === "discovery" ? schemas.result.properties.contract : undefined},$defs:schemas.result.$defs}} /> - -{resultContract} +Submission guide: +Read the frozen UTF-8 file at {props.submission.path} before returning any result, +including an early blocker, source conflict or unanswered decision. Use the same +native call to read it and return the result; no separate formatting turn is +scheduled. It contains the complete submission instructions and the exact JSON +contract for this role and phase. Leave the guide and its receipt unchanged. diff --git a/examples/adversarial-implementation/components/Phase/Request.md b/examples/adversarial-implementation/components/Phase/Request.md index c4de4c76d..613cd7d3e 100644 --- a/examples/adversarial-implementation/components/Phase/Request.md +++ b/examples/adversarial-implementation/components/Phase/Request.md @@ -13,6 +13,7 @@ props: returns: {type: string} --- - + + 256000}> input.path + " (" + input.content.length + ")").join(", ") + ". Preserve the prepared assignment and reduce the request before dispatch; nothing was truncated or sent. No provider prompt began."} /> diff --git a/examples/adversarial-implementation/components/Phase/Run.md b/examples/adversarial-implementation/components/Phase/Run.md index c076b3c98..946e45632 100644 --- a/examples/adversarial-implementation/components/Phase/Run.md +++ b/examples/adversarial-implementation/components/Phase/Run.md @@ -128,12 +128,14 @@ mkdir -- {quotedAttempt} + {raw} + diff --git a/examples/adversarial-implementation/components/Phase/SubmissionGuide.ts b/examples/adversarial-implementation/components/Phase/SubmissionGuide.ts new file mode 100644 index 000000000..a6bac6e80 --- /dev/null +++ b/examples/adversarial-implementation/components/Phase/SubmissionGuide.ts @@ -0,0 +1,52 @@ +import { Component } from "@executablemd/core/api"; +import { z } from "zod"; +import { clear, describe, descriptor, read, same, unused, write } from "../Ticket/text-storage.ts"; + +export const props = { + type: "object", + properties: { + repository: { type: "string" }, + directory: { type: "string" }, + mode: { enum: ["prepare", "check"], default: "prepare" }, + }, + required: ["repository", "directory"], + additionalProperties: false, +}; +const input = z + .object({ + repository: z.string(), + directory: z.string().startsWith("/"), + mode: z.enum(["prepare", "check"]).default("prepare"), + }) + .strict(); +const limit = 256_000; + +export default function* SubmissionGuide(value: unknown) { + const { repository, directory, mode } = input.parse(value); + const acquired = yield* Component.operations.tryContent(); + if (acquired.failure !== undefined) { + throw acquired.failure; + } + const text = acquired.text; + const expected = describe(directory + "/submission-guide.md", text); + if (text.length > limit || expected.utf8Bytes > limit) { + throw new Error("The submission guide exceeds 256000 characters or UTF-8 bytes."); + } + yield* clear(text); + const receipt = directory + "/submission-guide.json"; + if (mode === "prepare") { + if (!(yield* unused(repository, expected.path)) || !(yield* unused(repository, receipt))) { + throw new Error("The submission guide is occupied or unavailable; preserve this attempt."); + } + yield* write(repository, expected.path, text); + yield* write(repository, receipt, JSON.stringify(expected)); + } + const recorded = descriptor.parse(JSON.parse(yield* read(repository, receipt, limit))); + const retained = yield* read(repository, expected.path, limit); + if (!same(expected, recorded) || !same(expected, describe(expected.path, retained))) { + throw new Error( + "The frozen submission guide changed; preserve the raw reply. No successor is admitted.", + ); + } + return expected; +} diff --git a/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md b/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md new file mode 100644 index 000000000..5a5f2ffe0 --- /dev/null +++ b/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md @@ -0,0 +1,40 @@ +--- +props: + type: object + properties: + role: {enum: [architect, planner, implementor]} + assignment: {type: object} + required: [role, assignment] + additionalProperties: false +--- + + +# Submit a phase result + +Read this guide before returning from this assignment, including an early blocker, +source conflict or unanswered decision. It applies to this role's current native +agent call. Return the existing phase result in that call. + +Return assessment on completed implementation, changes_required, missing_evidence and escalate. For changes_required or missing_evidence, assess only the finite correction being requested, not the original feature. Keep already settled responsibilities closed; a missing test witness does not by itself reopen the production architecture. Completed implementation likewise reports the actual correction rather than inheriting the whole feature scope. Assess actual impact: mechanical, architecture and acceptance booleans; one short impact reason and individual evidence references, plus risk:null unless a concrete consequential uncertainty or risk needs additional reasoning. For high effort, risk contains its own short reason and supporting evidence. Merely touching concurrency, cancellation, durable state, replay or a public boundary is not that reason. Test-only changes can affect acceptance. A mechanical assessment has no architecture, acceptance or complexity impact. New non-mechanical feature work includes Planner acceptance; correction work requires only affected responsibilities. Keep evidence references in artifacts or already recorded documents. Do not request a reviewer or reasoning effort in prose to override this assessment. + +Return escalate only from medium Discovery or Review when a concrete newly discovered risk or uncertainty prevents completing this assignment. Keep the same revision and impact flags, supply risk with the specific reason and supporting evidence, and leave the checkout clean. The workflow resumes the same original role/session at high. High cannot escalate again. Implementation uses its assigned provider/effort and returns needs_discovery for a scope conflict. + +The workflow supplies omitted phase and work, the assigned revision for read-only phases, and the accepted continuation for complete/pass. Explicit conflicting metadata is refused. Implementation must report the actual resulting revision; evidence must name the inputs actually tested. + +Return next.phase and next.role explicitly when the outcome changes the prepared route. For complete/pass, copy assignment.continuation exactly; completed implementation routing is derived from the returned assessment and replaces that predicted continuation. For escalate, copy the current phase and role; the workflow derives and checks that same-owner continuation. For changes_required or missing_evidence, select implementation/implementor. For needs_discovery with apiConflict, select discovery/planner; otherwise select discovery/architect. For blocked or needs_decision, select hold/null. The program verifies this gate before another role can run; ready is a phase-loop result, not delivery approval. + +Keep summary to 1200 characters. Address handoff to next.role, or ROOT when next.phase is hold/ready. Write only that owner's next bounded action and necessary artifact references in at most 250 words and 1800 characters. Put command receipts in evidence and detailed reports in artifact files. Select only the short documents the next owner needs in handoffInputs. Each selected handoffInputs path must appear in your returned artifacts or the already recorded documents. A file existing on disk alone does not make it a recorded document. Keep full test logs, patches and historical ledgers in artifacts; listing an artifact does not send its contents. The selected-input limit is 64000 UTF-16 units for the recipient's entire owed packet, including undelivered accepted inputs and role guides. Use the recipient's recorded remaining allowance for new handoff material. Keep binding clauses, their conditions, exceptions, commands, identities and unresolved findings exact; reference full supporting artifacts. Ticket.Prepare checks the actual packet before dispatch and retains its receipt. This selected-input allowance is separate from the 256000-character complete-prompt ceiling. Use a new reference for changed contents. The workflow supplies accepted inputs once per retained role; do not request their repetition without a named change. Do not repeat the summary, command list, prior prompts, full plan, contract packet or narrative history in handoff. Omit blocker outside blocked outcomes. + +Reference individual UTF-8 text or JSON files in artifacts, evidence and handoffInputs. Do not return a directory or binary path as a document reference: the workflow reads each reference as a text file. For a group of files, save a text index naming the individual files and return that index. + +Before returning, check the required fields, assigned work/phase, actual revision, next role and phase, and character limits against the contract below. Omit optional metadata you cannot establish instead of guessing. + +Return exactly one JSON object conforming to this contract, without Markdown fences or surrounding prose. The handoff field is the complete ready-to-send request for the next owner; only that field becomes the next request. Artifacts and evidence travel as structured references. For a command on an uncommitted candidate, use evidence.revision=null and candidate=the retained candidate/command metadata path; a base HEAD is not its tested revision. Use a commit SHA only when the tested inputs correspond to that commit. Report observations as observations; schema validation does not establish test results, approvals or delivery readiness. + + + + + !["phase","work",...(props.assignment.phase === "implementation" ? [] : ["revision"])].includes(name)),"discoveryRequests","discoveryGaps"],additionalProperties:false,properties:{...schemas.result.properties,discoveryRequests:{...schemas.result.properties.discoveryRequests,items:authorRequest},discoveryGaps:{...schemas.result.properties.discoveryGaps,items:authorGap},phase:{const:props.assignment.phase},work:{const:props.assignment.work},outcome:phaseOutcome,contract:props.role === "architect" && props.assignment.phase === "discovery" ? schemas.result.properties.contract : undefined},$defs:schemas.result.$defs}} /> + +{resultContract} + diff --git a/examples/adversarial-implementation/tests/Delivery.test.md b/examples/adversarial-implementation/tests/Delivery.test.md index 42e209302..1b6a881c4 100644 --- a/examples/adversarial-implementation/tests/Delivery.test.md +++ b/examples/adversarial-implementation/tests/Delivery.test.md @@ -87,7 +87,9 @@ printf '%s' "$XMD_EXAMPLE_COMMAND" - + + + diff --git a/examples/adversarial-implementation/tests/Friction.test.md b/examples/adversarial-implementation/tests/Friction.test.md index 7fefbd9a1..f0caff020 100644 --- a/examples/adversarial-implementation/tests/Friction.test.md +++ b/examples/adversarial-implementation/tests/Friction.test.md @@ -128,10 +128,11 @@ git rev-parse HEAD - + {raw} + {correction.output} diff --git a/examples/adversarial-implementation/tests/ProductionRole.test.md b/examples/adversarial-implementation/tests/ProductionRole.test.md index 987e3756c..a80b1cc3c 100644 --- a/examples/adversarial-implementation/tests/ProductionRole.test.md +++ b/examples/adversarial-implementation/tests/ProductionRole.test.md @@ -18,9 +18,13 @@ pwd -P - + + + - + + + @@ -37,13 +41,21 @@ pwd -P - + + + - + + + - + + + - + + + @@ -70,9 +82,10 @@ pwd -P ``` - + {raw} + @@ -155,10 +168,11 @@ false - + - + + {schemaText} @@ -171,3 +185,80 @@ false incident.issue)} expected={[1075,1081]} /> incident.validationCode === "metadata-8" && !incident.canonicalResultCreated && !incident.implementationStarted && !incident.rawRewritten && incident.responseCorrections === 0 && incident.source.startsWith("/private/tmp/"))} expected={true} /> + + + + + + + + + + + + + + +{schemaText} + + + + + + + + + + + + + + + + +```bash exec as="head" +git -C {quoted} rev-parse HEAD +``` + + + + + + + + + + + + + 0} expected={true} /> + +{deliveryText} + + + + + 0} expected={true} /> + +{execution.output} + + + + + + + + + + + + + + +Changed before dispatch. +A directory cannot supply the guide. + + + + + diff --git a/examples/adversarial-implementation/tests/agents/api-conflict-correction.md b/examples/adversarial-implementation/tests/agents/api-conflict-correction.md index d1c3949a4..5fa7dc813 100644 --- a/examples/adversarial-implementation/tests/agents/api-conflict-correction.md +++ b/examples/adversarial-implementation/tests/agents/api-conflict-correction.md @@ -1,5 +1,5 @@ - + {text} {assignment.request} diff --git a/examples/adversarial-implementation/tests/agents/decision-planner-held.md b/examples/adversarial-implementation/tests/agents/decision-planner-held.md index b7197ad34..76796ff15 100644 --- a/examples/adversarial-implementation/tests/agents/decision-planner-held.md +++ b/examples/adversarial-implementation/tests/agents/decision-planner-held.md @@ -1,4 +1,4 @@ - + {text} diff --git a/examples/adversarial-implementation/tests/agents/decision-planner.md b/examples/adversarial-implementation/tests/agents/decision-planner.md index 38527ce4b..f01778c98 100644 --- a/examples/adversarial-implementation/tests/agents/decision-planner.md +++ b/examples/adversarial-implementation/tests/agents/decision-planner.md @@ -1,9 +1,9 @@ - + {text} - + {text} diff --git a/examples/adversarial-implementation/tests/agents/initialized-role.md b/examples/adversarial-implementation/tests/agents/initialized-role.md index a5d4e2e00..6ed6e1f74 100644 --- a/examples/adversarial-implementation/tests/agents/initialized-role.md +++ b/examples/adversarial-implementation/tests/agents/initialized-role.md @@ -1,3 +1,4 @@ - request.instructions.includes(text))}> + request.instructions.includes(text))}> + initialization received diff --git a/examples/adversarial-implementation/tests/agents/preparation-architect.md b/examples/adversarial-implementation/tests/agents/preparation-architect.md index 25c928fa3..cc908ce88 100644 --- a/examples/adversarial-implementation/tests/agents/preparation-architect.md +++ b/examples/adversarial-implementation/tests/agents/preparation-architect.md @@ -1,5 +1,5 @@ - + {text} {assignment.request} diff --git a/examples/adversarial-implementation/tests/agents/preparation-implementor.md b/examples/adversarial-implementation/tests/agents/preparation-implementor.md index 15dc51713..5f2c976bd 100644 --- a/examples/adversarial-implementation/tests/agents/preparation-implementor.md +++ b/examples/adversarial-implementation/tests/agents/preparation-implementor.md @@ -3,7 +3,7 @@ - + {text} {assignment.request} diff --git a/examples/adversarial-implementation/tests/agents/preparation-planner.md b/examples/adversarial-implementation/tests/agents/preparation-planner.md index 00e1712d6..d00f8754e 100644 --- a/examples/adversarial-implementation/tests/agents/preparation-planner.md +++ b/examples/adversarial-implementation/tests/agents/preparation-planner.md @@ -1,10 +1,10 @@ - + {text} {assignment.request} - + {text} {assignment.request} diff --git a/examples/adversarial-implementation/tests/agents/submission-result.md b/examples/adversarial-implementation/tests/agents/submission-result.md new file mode 100644 index 000000000..03441b9c6 --- /dev/null +++ b/examples/adversarial-implementation/tests/agents/submission-result.md @@ -0,0 +1,15 @@ + + + +{text} + +not a phase result + + + + + + +{packetText} + + diff --git a/examples/adversarial-implementation/tests/production-components/NativeGuide.ts b/examples/adversarial-implementation/tests/production-components/NativeGuide.ts new file mode 100644 index 000000000..abee5d125 --- /dev/null +++ b/examples/adversarial-implementation/tests/production-components/NativeGuide.ts @@ -0,0 +1,53 @@ +import { Agent, Component } from "@executablemd/core/api"; +import { ensureDir, rm } from "@effectionx/fs"; +import { z } from "zod"; +import { read, write } from "../../components/Ticket/text-storage.ts"; + +export const props = { + type: "object", + properties: { variant: { type: "string" } }, + required: ["variant"], + additionalProperties: false, +}; + +export default function* NativeGuide(input: unknown) { + const { variant } = z.object({ variant: z.string() }).parse(input); + let calls = 0; + let path: string | undefined; + yield* Agent.around({ + *prompt(args, next) { + calls++; + const text = args[0]; + path ??= text.split("Read the frozen UTF-8 file at ")[1]?.split(" before returning")[0]; + if (path === undefined || (calls === 1 && text.includes('"$defs"'))) { + throw new Error("The native request did not stage its submission guide."); + } + const guide = yield* read("/", path); + if ( + ![ + '"$defs"', + "including an early blocker", + "Test-only changes can affect acceptance.", + "A mechanical assessment has no architecture, acceptance or complexity impact.", + "a public boundary is not that reason", + ].every((rule) => guide.includes(rule)) + ) { + throw new Error("The staged guide is unreadable or incomplete."); + } + if (variant === "changed" || (variant === "correction-changed" && calls === 2)) { + yield* write("/", path, "Changed during the native turn."); + } + if (variant === "missing" || variant === "unreadable") { + yield* rm(path); + } + if (variant === "unreadable") { + yield* ensureDir(path); + } + return yield* next(...args); + }, + }); + yield* Component.operations.content(); + if (calls !== (variant === "correction-changed" ? 2 : 1)) { + throw new Error("The staged submission used an additional native call."); + } +} diff --git a/packages/cli/tests/support/adversarial-journey.ts b/packages/cli/tests/support/adversarial-journey.ts index c4a97f469..0ef7525c2 100644 --- a/packages/cli/tests/support/adversarial-journey.ts +++ b/packages/cli/tests/support/adversarial-journey.ts @@ -48,6 +48,9 @@ export function* runAdversarialJourney(suite: string) { testedSuites.includes(suite) ? "examples/adversarial-implementation/components" : "examples/adversarial-implementation/tests/components", + ...(suite === "ProductionRole" + ? ["--include", "examples/adversarial-implementation/tests/production-components"] + : []), "--include", "examples/adversarial-implementation/components", ], From 03c67eb50021d01b5eb0e50a1673c69d54e61b0d Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 21:27:33 -0500 Subject: [PATCH 02/15] Limit workflow prose authoring holds to Claude --- .../adversarial-implementation/components/Phase/Instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/adversarial-implementation/components/Phase/Instructions.md b/examples/adversarial-implementation/components/Phase/Instructions.md index ae6cf8648..6cfdded6f 100644 --- a/examples/adversarial-implementation/components/Phase/Instructions.md +++ b/examples/adversarial-implementation/components/Phase/Instructions.md @@ -47,7 +47,7 @@ You remain the retained repository {props.role}. The previous owner's request is Do not draft, create or edit PR or issue titles or descriptions. The Sidekick running Codex owns that metadata and publication. Return technical decisions, code and evidence only; API documentation stays within the design assignment. Describe user-directed amendments separately from implementation defects, cite the recorded approval evidence and keep the existing correction allowances. This restriction also applies when a general role guide mentions opening a draft PR. -Codex authors prose committed to workflow documents, including guides, component instructions and prompt templates. Claude roles may write internal runtime handoffs, hold prompts and summaries. If the assignment needs workflow-document prose that Codex has not supplied, return needs_discovery with the required authoring work. +Codex authors prose committed to workflow documents, including guides, component instructions and prompt templates. Claude roles may write internal runtime handoffs, hold prompts and summaries. If your selected provider is Claude and the assignment needs workflow-document prose that Codex has not supplied, return needs_discovery with the required authoring work. Execute the assigned phase through its stated deliverables. An assigned test or artifact that has not yet been written is remaining work. Continue it within this call. Use blocked only for a concrete external condition you cannot resolve within the assignment: quota, permission, provider failure, unavailable external dependency or genuinely missing input. Supply blocker.kind, reason, unblockCondition and evidence references. A source or design conflict returns needs_discovery; an unanswered human choice returns needs_decision. Use the supplied handoff, artifact paths and evidence first. Store every new plan, handoff, report, candidate and command receipt under assignment.artifactDirectory, the phase directory in this ticket workspace. Keep the source checkout and native cwd unchanged. Preserve first failures, command receipts and the exact source inputs actually tested. An uncommitted candidate needs its own retained identity; a base HEAD is not its tested revision. Historical/source references retain their original locations; do not move or rewrite them. Return absolute references to new artifacts. Leave the runner-owned assignment.json, raw.txt, result.json, status.json, submission-guide.md, submission-guide.json and event files unchanged. Tie additional reads to a missing fact or an affected invariant. Reopen earlier conclusions when a named change invalidates them. State the missing fact when the assignment is insufficient. Retain mandatory repository and role requirements. From 8561c3641f45351ecd289a9566ff62f398ca7243 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 21:47:48 -0500 Subject: [PATCH 03/15] Run submission regression cases in their own bounded suite --- .../tests/ProductionRole.test.md | 77 ------------------ .../tests/Submission.test.md | 79 +++++++++++++++++++ .../cli/tests/adversarial-submission.test.ts | 7 ++ .../cli/tests/support/adversarial-journey.ts | 4 +- 4 files changed, 88 insertions(+), 79 deletions(-) create mode 100644 examples/adversarial-implementation/tests/Submission.test.md create mode 100644 packages/cli/tests/adversarial-submission.test.ts diff --git a/examples/adversarial-implementation/tests/ProductionRole.test.md b/examples/adversarial-implementation/tests/ProductionRole.test.md index a80b1cc3c..32a87e446 100644 --- a/examples/adversarial-implementation/tests/ProductionRole.test.md +++ b/examples/adversarial-implementation/tests/ProductionRole.test.md @@ -185,80 +185,3 @@ false incident.issue)} expected={[1075,1081]} /> incident.validationCode === "metadata-8" && !incident.canonicalResultCreated && !incident.implementationStarted && !incident.rawRewritten && incident.responseCorrections === 0 && incident.source.startsWith("/private/tmp/"))} expected={true} /> - - - - - - - - - - - - - - -{schemaText} - - - - - - - - - - - - - - - - -```bash exec as="head" -git -C {quoted} rev-parse HEAD -``` - - - - - - - - - - - - - 0} expected={true} /> - -{deliveryText} - - - - - 0} expected={true} /> - -{execution.output} - - - - - - - - - - - - - - -Changed before dispatch. -A directory cannot supply the guide. - - - - - diff --git a/examples/adversarial-implementation/tests/Submission.test.md b/examples/adversarial-implementation/tests/Submission.test.md new file mode 100644 index 000000000..39c0e6571 --- /dev/null +++ b/examples/adversarial-implementation/tests/Submission.test.md @@ -0,0 +1,79 @@ +```bash exec as="location" +pwd -P +``` + + + + + + + + + + + + + +{schemaText} + + + + + + + + + + + + + + + + +```bash exec as="head" +git -C {quoted} rev-parse HEAD +``` + + + + + + + + + + + + + 0} expected={true} /> + +{deliveryText} + + + + + 0} expected={true} /> + +{execution.output} + + + + + + + + + + + + + + +Changed before dispatch. +A directory cannot supply the guide. + + + + + diff --git a/packages/cli/tests/adversarial-submission.test.ts b/packages/cli/tests/adversarial-submission.test.ts new file mode 100644 index 000000000..85aca680e --- /dev/null +++ b/packages/cli/tests/adversarial-submission.test.ts @@ -0,0 +1,7 @@ +import { describe, it } from "@executablemd/test-support/bdd"; +import { runAdversarialJourney } from "./support/adversarial-journey.ts"; + +describe("adversarial implementation example", () => { + it("runs the Submission Markdown journeys without native models", () => + runAdversarialJourney("Submission")); +}); diff --git a/packages/cli/tests/support/adversarial-journey.ts b/packages/cli/tests/support/adversarial-journey.ts index 0ef7525c2..575698c1d 100644 --- a/packages/cli/tests/support/adversarial-journey.ts +++ b/packages/cli/tests/support/adversarial-journey.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url"; import { env } from "@executablemd/runtime"; import { readTextFile } from "@effectionx/fs"; -const testedSuites = ["ProductionRole", "Preparation"]; +const testedSuites = ["ProductionRole", "Preparation", "Submission"]; export function* runAdversarialJourney(suite: string) { const retainedEvidence = suite.startsWith("ArtifactReturn") @@ -48,7 +48,7 @@ export function* runAdversarialJourney(suite: string) { testedSuites.includes(suite) ? "examples/adversarial-implementation/components" : "examples/adversarial-implementation/tests/components", - ...(suite === "ProductionRole" + ...(suite === "Submission" ? ["--include", "examples/adversarial-implementation/tests/production-components"] : []), "--include", From 70627470b63f01464c0f8044c18fd2085f8c49a7 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 21:55:36 -0500 Subject: [PATCH 04/15] Include submission tests in the cross-runtime coverage assertion --- scripts/tests/runtime-exclusions.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/tests/runtime-exclusions.test.ts b/scripts/tests/runtime-exclusions.test.ts index 06eadfb62..0b812d97d 100644 --- a/scripts/tests/runtime-exclusions.test.ts +++ b/scripts/tests/runtime-exclusions.test.ts @@ -163,6 +163,7 @@ describe("the applicable corpus", () => { "adversarial-preparation", "adversarial-production-role", "adversarial-session-continuity", + "adversarial-submission", "adversarial-workflow", ].map((name) => `packages/cli/tests/${name}.test.ts`); expect( From 2de425dd02f00df1354c82f7cbd9443d67be94fd Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 9 Nov 2026 23:36:25 -0500 Subject: [PATCH 05/15] Keep workflow work rules upfront and repair review summary overflow --- examples/adversarial-implementation/README.md | 28 +++++- .../components/Adversarial/PlannerRole.md | 4 +- .../components/Delivery/Dispatch.md | 2 +- .../components/Delivery/Prepare.md | 2 +- .../components/Delivery/Reconcile.md | 7 +- .../components/Delivery/Response.md | 86 +++++++++++++++++++ .../components/Delivery/ReviewState.ts | 34 ++++++-- .../components/Delivery/Role.md | 8 +- .../components/Delivery/Selection.md | 2 +- .../components/Delivery/Validate.md | 38 ++++++++ .../components/Delivery/records.ts | 1 + .../components/Phase/Instructions.md | 24 ++++-- .../components/Phase/Provider.md | 14 +++ .../components/Phase/Role.md | 2 +- .../Phase/SubmissionInstructions.md | 10 +-- .../components/Ticket/PreparationPrompt.md | 2 +- .../tests/Delivery.test.md | 50 +++++++++++ .../tests/ProductionRole.test.md | 5 +- .../tests/Submission.test.md | 4 + .../tests/agents/summary-correction.md | 10 +++ .../tests/components/Fixture/Composition.ts | 18 ++++ .../tests/components/Fixture/Fallback.md | 6 +- .../production-components/NativeGuide.ts | 4 +- .../production-components/Phase/Provider.md | 10 +++ packages/acp/src/provider.ts | 18 ++++ packages/acp/tests/provider.test.ts | 40 +++++++++ packages/acp/tests/released-runtime.test.ts | 40 +++++++++ packages/acp/vendor/acpx/MANIFEST.json | 8 +- packages/acp/vendor/acpx/PROVENANCE.md | 9 ++ .../acp/vendor/acpx/generated/ipc-BbYFsKOj.js | 9 ++ .../generated/queue-owner-runtime-B0mPqOu4.js | 14 ++- .../generated/session-options-BlnP3WAc.d.ts | 6 ++ packages/cli/src/agent-stack.ts | 10 +++ .../cli/tests/support/adversarial-journey.ts | 9 +- scripts/tests/acpx-vendor.test.ts | 1 + specs/acp-client-spec.md | 24 ++++++ 36 files changed, 507 insertions(+), 52 deletions(-) create mode 100644 examples/adversarial-implementation/components/Delivery/Response.md create mode 100644 examples/adversarial-implementation/components/Delivery/Validate.md create mode 100644 examples/adversarial-implementation/components/Phase/Provider.md create mode 100644 examples/adversarial-implementation/tests/agents/summary-correction.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/Composition.ts create mode 100644 examples/adversarial-implementation/tests/production-components/Phase/Provider.md diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index b1a91efc7..e25372212 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -63,7 +63,8 @@ retains the selected providers and all three budgets through continuation. consumer-driven API repairs, each from zero through ten. `maxResponseCorrections` bounds extra response-format calls per completed assignment, from zero through three. Zero admits no extra response call. It applies to ordinary planning, -implementation, selected feedback reviews and foundation replies, including +implementation, selected feedback reviews, independent delivery reviews and +foundation replies, including repairs by their original owner. It authorizes no transport retry or repetition of uncertain productive work. Continuing a Ticket keeps its consumed source correction count and recorded allocation; it does not restart a failed attempt. @@ -304,6 +305,19 @@ Independent review summaries allow 1500 characters; detailed observations belong in `findings`. The request states this bound alongside the exact revision and clause IDs. Ordinary phase and API-foundation summaries retain their own bounds. +An otherwise-valid joined review with only an oversized summary can consume the +work item's recorded `maxResponseCorrections` allowance. Each correction stays +in the selected reviewer conversation with the same resolved model and effort. +It shortens only `summary`; every other result field is frozen. Zero permits no +extra call. The workflow records the original raw reply and rejection, reserves +each correction before dispatch, and saves its request, native outcome, reply, +source observations and disposition separately. Exhaustion, changed substance, +changed source or uncertain work holds explicitly; none selects a fallback +reviewer or repeats the completed review. Reconciliation validates this saved +chain without prompting. Historical assignments without a recorded allowance +have zero; a new workflow does not retroactively accept their rejected replies. + + Use `--props-action reconcile` when the selected joined review has a saved execution but no accepted receipt. Only the selection's attempt qualifies: the original reviewer, or Astra after a retained transition from an eligible Fable @@ -315,8 +329,8 @@ continues to hold. Status uses this same validation boundary to read receipts. Reconciliation is limited to independent review from a work item launched under the same response contract. Its assignment retains the exact response schema; a -missing or different schema refuses rather than reinterpreting an old reply. It cannot repair or adopt an ordinary rejected role -verdict, rewrite its raw rejection or partial work, or reset a correction budget. +missing or different schema refuses rather than reinterpreting an old reply. It validates only corrections recorded under the original review assignment. +It cannot adopt an ordinary rejected role verdict, rewrite its raw rejection or partial work, or reset a correction budget. A current schema accepting older text does not retroactively approve that work. Preserve the original rejection and require an explicit applicable schema-amendment decision, recording the old bound, amended bound, exact raw @@ -361,6 +375,14 @@ from claiming readiness or merging independently. ## Prepare requests without metadata repair turns +Claude workflow-role calls use the CLI's scoped `claude-workflow-role` provider. +It disables native shell/subagent backgrounding and denies `ScheduleWakeup`; +ordinary interactive Sidekick calls keep their defaults. A retained session that +lacks matching creation settings refuses before another prompt. Each role joins +its owned processes and returns its result in the same call. The upfront brief +contains work, evidence and lifetime rules; the frozen submission guide contains +response schema, limits and routing and may wait until a result is being prepared. + These rules apply to separate phase requests and Ticket role calls. Codex authors prose committed to workflow documents, including guides, component instructions and prompt templates. Claude roles may write internal runtime handoffs, hold diff --git a/examples/adversarial-implementation/components/Adversarial/PlannerRole.md b/examples/adversarial-implementation/components/Adversarial/PlannerRole.md index 7138e5499..9eb45b098 100644 --- a/examples/adversarial-implementation/components/Adversarial/PlannerRole.md +++ b/examples/adversarial-implementation/components/Adversarial/PlannerRole.md @@ -12,7 +12,7 @@ props: - + You remain the original Planner. This is response-only correction {props.correction.ordinal} with {props.correction.remaining} remaining. Shorten only the rejected summary or handoff to its stated bound. Every other result value is frozen by the supplied schema, including the API, outcome, source revision, artifacts, assessment and command evidence. @@ -48,4 +48,4 @@ Return exactly one JSON object satisfying this contract. Evidence names actual c - + diff --git a/examples/adversarial-implementation/components/Delivery/Dispatch.md b/examples/adversarial-implementation/components/Delivery/Dispatch.md index a81e117ad..b11fb96b1 100644 --- a/examples/adversarial-implementation/components/Delivery/Dispatch.md +++ b/examples/adversarial-implementation/components/Delivery/Dispatch.md @@ -16,7 +16,7 @@ A release that fails ends the run and leaves the attempt reserved, so it holds for reconciliation and can never select a replacement. - + diff --git a/examples/adversarial-implementation/components/Delivery/Prepare.md b/examples/adversarial-implementation/components/Delivery/Prepare.md index 8798a687c..69a9da1a1 100644 --- a/examples/adversarial-implementation/components/Delivery/Prepare.md +++ b/examples/adversarial-implementation/components/Delivery/Prepare.md @@ -15,7 +15,7 @@ returns: {type: object} clause.id)).size !== policy.acceptance.length || policy.acceptance.some(clause => !props.task.inputs.includes(clause.source))}> clause.id).filter((id,index,ids) => ids.indexOf(id) !== index).join(", ") + ". Missing source references: " + [...new Set(policy.acceptance.filter(clause => !props.task.inputs.includes(clause.source)).map(clause => clause.source))].join(", ") + ". No provider prompt began."} /> - + Object.values(props.task.sessions).includes(entry.reviewer.session))}> diff --git a/examples/adversarial-implementation/components/Delivery/Reconcile.md b/examples/adversarial-implementation/components/Delivery/Reconcile.md index e04ee59dd..6085becbf 100644 --- a/examples/adversarial-implementation/components/Delivery/Reconcile.md +++ b/examples/adversarial-implementation/components/Delivery/Reconcile.md @@ -20,7 +20,7 @@ attempt's retained outcome. - + {assignmentText} @@ -36,9 +36,8 @@ attempt's retained outcome. - - -{extracted.json} + + !result.acceptance.includes(clause.id)) || result.verdict === "pass" && result.findings.some(finding => finding.blocking)}> diff --git a/examples/adversarial-implementation/components/Delivery/Response.md b/examples/adversarial-implementation/components/Delivery/Response.md new file mode 100644 index 000000000..9b047f127 --- /dev/null +++ b/examples/adversarial-implementation/components/Delivery/Response.md @@ -0,0 +1,86 @@ +--- +description: 'Shorten only an oversized independent-review summary, or validate its saved correction chain without prompting. Use .' +props: + type: object + properties: + delivery: {type: object} + revision: {type: string} + reviewer: {type: object} + attempt: {type: string} + raw: {type: string} + dispatch: {type: boolean, default: false} + required: [delivery, revision, reviewer, attempt, raw] + additionalProperties: false +returns: {type: object} +--- + +The original review stays unchanged. Each extra call belongs to the same +reviewer, model and native conversation, and consumes the recorded response +allowance before dispatch. Reconciliation reads that chain and sends no prompt. + + + +{assignmentText} + + 3 || limit !== (props.delivery.maxResponseCorrections ?? 0)}> + + + + + + + + +{outcomeText} + + + += limit}> + + + [key,key === "summary" ? value : {const:original[key]}]))}} /> + + 64000}> + + + + + +```bash exec as="reserved" +mkdir -- {quoted} +``` + + + +Shorten only the summary of your completed independent review to at most {schemas.review.properties.summary.maxLength} characters. This is response-only correction {round}, with {limit-round} remaining. Do not inspect source, run commands, repeat the review, publish or change any other field. Preserve the verdict, findings, clause IDs and revision exactly. Return one JSON object satisfying the frozen schema below. + + + + +{turn.reply} + + + + + +{requestText} + + +{returnedText} + + + + +{beforeText} +{afterText} + + + + + + + + + + + diff --git a/examples/adversarial-implementation/components/Delivery/ReviewState.ts b/examples/adversarial-implementation/components/Delivery/ReviewState.ts index 2206912ea..1d05e792f 100644 --- a/examples/adversarial-implementation/components/Delivery/ReviewState.ts +++ b/examples/adversarial-implementation/components/Delivery/ReviewState.ts @@ -216,6 +216,9 @@ function assignmentFor(selection: Selection, reviewer: Reviewer) { reviewer, acceptance: selection.policy.acceptance, responseSchema: selection.responseSchema, + ...(selection.maxResponseCorrections === undefined + ? {} + : { maxResponseCorrections: selection.maxResponseCorrections }), }; } @@ -238,12 +241,27 @@ export default function* ReviewState(value: unknown): Operation { ); } -function* observe(directory: string, expected: Selection, replacement: boolean): Operation { +function* observe( + directory: string, + expected: Selection, + replacement: boolean, +): Operation { const selectionText = yield* read(directory, "selection.json"); const recorded = parsed(selectionSchema, selectionText); const selection = - selectionText === null ? "absent" : recorded !== null && same(recorded, expected) ? "matches" : "mismatch"; - const empty = { selection, chain: expected.chain, attempts: [], candidate: null, effective: null, transition: null }; + selectionText === null + ? "absent" + : recorded !== null && same(recorded, expected) + ? "matches" + : "mismatch"; + const empty = { + selection, + chain: expected.chain, + attempts: [], + candidate: null, + effective: null, + transition: null, + }; if (selection !== "matches") { return { ...empty, next: "uncertain", hold: null }; } @@ -274,7 +292,8 @@ function* observe(directory: string, expected: Selection, replacement: boolean): next = "uncertain"; } else { const transitionText = yield* read(replacementDirectory, "transition.json"); - const proven = transitionText !== null && same(parsed(z.unknown(), transitionText), transition); + const proven = + transitionText !== null && same(parsed(z.unknown(), transitionText), transition); const replaced = yield* attempt( replacementDirectory, 1, @@ -298,7 +317,9 @@ function* observe(directory: string, expected: Selection, replacement: boolean): : "The replacement transition differs from the primary attempt's eligible failure.", }; attempts.push(settled); - candidate = proven ? { position: 1, directory: replacementDirectory, reviewer: second.reviewer } : null; + candidate = proven + ? { position: 1, directory: replacementDirectory, reviewer: second.reviewer } + : null; next = !proven ? "uncertain" : settled.state === "review" @@ -331,7 +352,8 @@ function* observe(directory: string, expected: Selection, replacement: boolean): holdRecord.work === expected.work && holdRecord.revision === expected.revision && holdRecord.chain === expected.chain && - (dispositionText === null || (disposition !== null && same(disposition.hold, holdRecord))), + (dispositionText === null || + (disposition !== null && same(disposition.hold, holdRecord))), answer: disposition?.answer ?? null, evidence: [ `${directory}/hold.json`, diff --git a/examples/adversarial-implementation/components/Delivery/Role.md b/examples/adversarial-implementation/components/Delivery/Role.md index 03c89edf8..e80709fff 100644 --- a/examples/adversarial-implementation/components/Delivery/Role.md +++ b/examples/adversarial-implementation/components/Delivery/Role.md @@ -35,11 +35,15 @@ Return exactly one JSON object satisfying this schema. acceptance lists every cl 64000}> - + {instructions} - + +{turn.reply} + + + diff --git a/examples/adversarial-implementation/components/Delivery/Selection.md b/examples/adversarial-implementation/components/Delivery/Selection.md index b591c5c86..3f7bb6438 100644 --- a/examples/adversarial-implementation/components/Delivery/Selection.md +++ b/examples/adversarial-implementation/components/Delivery/Selection.md @@ -19,4 +19,4 @@ reviewer is the only one. - + diff --git a/examples/adversarial-implementation/components/Delivery/Validate.md b/examples/adversarial-implementation/components/Delivery/Validate.md new file mode 100644 index 000000000..b0adfc0e5 --- /dev/null +++ b/examples/adversarial-implementation/components/Delivery/Validate.md @@ -0,0 +1,38 @@ +--- +description: 'Check one independent review without changing it. Use .' +props: + type: object + properties: + text: {type: string} + revision: {type: string} + acceptance: {type: array} + frozen: {type: [object, 'null'], default: null} + required: [text, revision, acceptance] + additionalProperties: false +returns: {type: object} +--- + + + + + + + +{extracted.json} + + + + + + [key,key === "summary" ? value : {const:frozen[key]}]))}} /> + + !result.acceptance.includes(clause.id)) || result.verdict === "pass" && result.findings.some(finding => finding.blocking) || !correspondence.ok}> + + + schemas.review.properties.summary.maxLength,result,reason:result.summary.length > schemas.review.properties.summary.maxLength ? "The review summary exceeds its recorded limit." : null}} /> + + + + + + diff --git a/examples/adversarial-implementation/components/Delivery/records.ts b/examples/adversarial-implementation/components/Delivery/records.ts index 94b463c36..2607137da 100644 --- a/examples/adversarial-implementation/components/Delivery/records.ts +++ b/examples/adversarial-implementation/components/Delivery/records.ts @@ -81,6 +81,7 @@ export const selectionSchema = z.strictObject({ policy: z.record(z.string(), z.unknown()), inputs: z.array(z.record(z.string(), z.unknown())), responseSchema: z.record(z.string(), z.unknown()), + maxResponseCorrections: z.number().int().min(0).max(3).optional(), chain: z.enum(["fable-astra", "astra-final", "single"]), reviewers: z.array(entrySchema).min(1).max(2), }); diff --git a/examples/adversarial-implementation/components/Phase/Instructions.md b/examples/adversarial-implementation/components/Phase/Instructions.md index 6cfdded6f..b4a60adb7 100644 --- a/examples/adversarial-implementation/components/Phase/Instructions.md +++ b/examples/adversarial-implementation/components/Phase/Instructions.md @@ -29,6 +29,10 @@ props: --- +Finish this native call with its JSON result. Join every process you start before +returning; a progress message or a future wakeup does not complete the assignment. +Run long checks in the foreground and retain their completion output and status. + You are the repository {props.role}. Perform the assigned phase using the repository's existing permissions and role boundaries. @@ -49,7 +53,13 @@ Do not draft, create or edit PR or issue titles or descriptions. The Sidekick ru Codex authors prose committed to workflow documents, including guides, component instructions and prompt templates. Claude roles may write internal runtime handoffs, hold prompts and summaries. If your selected provider is Claude and the assignment needs workflow-document prose that Codex has not supplied, return needs_discovery with the required authoring work. -Execute the assigned phase through its stated deliverables. An assigned test or artifact that has not yet been written is remaining work. Continue it within this call. Use blocked only for a concrete external condition you cannot resolve within the assignment: quota, permission, provider failure, unavailable external dependency or genuinely missing input. Supply blocker.kind, reason, unblockCondition and evidence references. A source or design conflict returns needs_discovery; an unanswered human choice returns needs_decision. Use the supplied handoff, artifact paths and evidence first. Store every new plan, handoff, report, candidate and command receipt under assignment.artifactDirectory, the phase directory in this ticket workspace. Keep the source checkout and native cwd unchanged. Preserve first failures, command receipts and the exact source inputs actually tested. An uncommitted candidate needs its own retained identity; a base HEAD is not its tested revision. Historical/source references retain their original locations; do not move or rewrite them. Return absolute references to new artifacts. Leave the runner-owned assignment.json, raw.txt, result.json, status.json, submission-guide.md, submission-guide.json and event files unchanged. Tie additional reads to a missing fact or an affected invariant. Reopen earlier conclusions when a named change invalidates them. State the missing fact when the assignment is insufficient. Retain mandatory repository and role requirements. +Execute the assigned phase through its stated deliverables. An assigned test or artifact that has not yet been written is remaining work. Continue it within this call. Use blocked only for a concrete external condition you cannot resolve within the assignment: quota, permission, provider failure, unavailable external dependency or genuinely missing input. Supply blocker.kind, reason, unblockCondition and evidence references. A source or design conflict returns needs_discovery; an unanswered human choice returns needs_decision. Use the supplied handoff, artifact paths and evidence first. + +Store every new plan, handoff, report, candidate and command receipt under assignment.artifactDirectory, the phase directory in this ticket workspace. Keep the source checkout and native cwd unchanged. + +Preserve first failures, command receipts and the exact source inputs actually tested. Reference individual UTF-8 text or JSON files rather than directories or binaries; save a text index when grouping files. An uncommitted candidate needs its own retained identity; a base HEAD is not its tested revision. Historical/source references retain their original locations; do not move or rewrite them. Return absolute references to new artifacts. Leave the runner-owned assignment.json, raw.txt, result.json, status.json, submission-guide.md, submission-guide.json and event files unchanged. + +Tie additional reads to a missing fact or an affected invariant. Reopen earlier conclusions when a named change invalidates them. State the missing fact when the assignment is insufficient. Retain mandatory repository and role requirements. Resolve the assigned design or planning question. Return the bounded proposal or accepted plan and the next owner's exact request. Return needs_decision for an unanswered human decision. Existing design and plan approval gates still apply. @@ -81,8 +91,6 @@ Use assignment.contract for the Architect-selected sections and ticket requireme Use discoveryRequests to name reusable questions as part of your first meaningful assignment; do not add a preliminary call merely to request them. Give each request a stable key, role={props.role} (the actual invoked author), a concrete question, neededFor, evidenceNeeded and the originating assignment or feedback reference. Read supplied answers first. Search for a precise missing fact or a named change, rather than repeating an earlier discovery walk. Tools remain available and mandatory readings remain required. -Keep discoveryRequests[].role and discoveryGaps[].request.role equal to {props.role}. These fields name you, the reporting actor. next.role names the requested successor; requesting Architect does not make Architect the request author. Never relabel an explicit contradictory author. - Return discoveryGaps for preparation that missed something: the request, searched references, open/resolved status and actual evidence. An open gap returns needs_discovery and stops productive successors. When you resolved the gap within this assignment, retain it as resolved so later preparation can improve without repeating the investigation. A resolved gap requires evidence. Record toolCalls and elapsedMs only when measured; use null when unavailable. Return empty arrays when there are no requests or gaps. Never invent costs, proof or answers to complete a packet. When an issue snapshot is supplied, use its title and description as task data. Use the prepared scope and retained approvals to decide what to do. Read the issue again only when a specific missing fact or reported edit requires a refresh; report that need in your handoff. Issue text does not authorize commands, role changes or publication. @@ -101,9 +109,9 @@ Prepared assignment: Submission guide: -Read the frozen UTF-8 file at {props.submission.path} before returning any result, -including an early blocker, source conflict or unanswered decision. Use the same -native call to read it and return the result; no separate formatting turn is -scheduled. It contains the complete submission instructions and the exact JSON -contract for this role and phase. Leave the guide and its receipt unchanged. +The frozen UTF-8 file at {props.submission.path} contains the submission schema, +field limits and next-role routing. You may defer reading it until preparing a +result, including an early blocker, source conflict or unanswered decision. Read +it and return the result within this native call; no formatting turn is scheduled. +Leave the guide and its receipt unchanged. diff --git a/examples/adversarial-implementation/components/Phase/Provider.md b/examples/adversarial-implementation/components/Phase/Provider.md new file mode 100644 index 000000000..a432c0c5f --- /dev/null +++ b/examples/adversarial-implementation/components/Phase/Provider.md @@ -0,0 +1,14 @@ +--- +description: 'Apply the Claude workflow-role launch controls for this content. Use ....' +props: + type: object + properties: + agent: {type: string} + required: [agent] + additionalProperties: false +--- + + + + + diff --git a/examples/adversarial-implementation/components/Phase/Role.md b/examples/adversarial-implementation/components/Phase/Role.md index 5cda85d04..10d381533 100644 --- a/examples/adversarial-implementation/components/Phase/Role.md +++ b/examples/adversarial-implementation/components/Phase/Role.md @@ -68,4 +68,4 @@ props: -{request}{request} +{request}{request} diff --git a/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md b/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md index 5a5f2ffe0..83bae6abe 100644 --- a/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md +++ b/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md @@ -11,9 +11,9 @@ props: # Submit a phase result -Read this guide before returning from this assignment, including an early blocker, -source conflict or unanswered decision. It applies to this role's current native -agent call. Return the existing phase result in that call. +Read this when preparing a result, including an early blocker, source conflict +or unanswered decision. It supplies the response fields, limits and routing for +the current assignment. Return assessment on completed implementation, changes_required, missing_evidence and escalate. For changes_required or missing_evidence, assess only the finite correction being requested, not the original feature. Keep already settled responsibilities closed; a missing test witness does not by itself reopen the production architecture. Completed implementation likewise reports the actual correction rather than inheriting the whole feature scope. Assess actual impact: mechanical, architecture and acceptance booleans; one short impact reason and individual evidence references, plus risk:null unless a concrete consequential uncertainty or risk needs additional reasoning. For high effort, risk contains its own short reason and supporting evidence. Merely touching concurrency, cancellation, durable state, replay or a public boundary is not that reason. Test-only changes can affect acceptance. A mechanical assessment has no architecture, acceptance or complexity impact. New non-mechanical feature work includes Planner acceptance; correction work requires only affected responsibilities. Keep evidence references in artifacts or already recorded documents. Do not request a reviewer or reasoning effort in prose to override this assessment. @@ -25,11 +25,11 @@ Return next.phase and next.role explicitly when the outcome changes the prepared Keep summary to 1200 characters. Address handoff to next.role, or ROOT when next.phase is hold/ready. Write only that owner's next bounded action and necessary artifact references in at most 250 words and 1800 characters. Put command receipts in evidence and detailed reports in artifact files. Select only the short documents the next owner needs in handoffInputs. Each selected handoffInputs path must appear in your returned artifacts or the already recorded documents. A file existing on disk alone does not make it a recorded document. Keep full test logs, patches and historical ledgers in artifacts; listing an artifact does not send its contents. The selected-input limit is 64000 UTF-16 units for the recipient's entire owed packet, including undelivered accepted inputs and role guides. Use the recipient's recorded remaining allowance for new handoff material. Keep binding clauses, their conditions, exceptions, commands, identities and unresolved findings exact; reference full supporting artifacts. Ticket.Prepare checks the actual packet before dispatch and retains its receipt. This selected-input allowance is separate from the 256000-character complete-prompt ceiling. Use a new reference for changed contents. The workflow supplies accepted inputs once per retained role; do not request their repetition without a named change. Do not repeat the summary, command list, prior prompts, full plan, contract packet or narrative history in handoff. Omit blocker outside blocked outcomes. -Reference individual UTF-8 text or JSON files in artifacts, evidence and handoffInputs. Do not return a directory or binary path as a document reference: the workflow reads each reference as a text file. For a group of files, save a text index naming the individual files and return that index. +Keep discoveryRequests[].role and discoveryGaps[].request.role equal to {props.role}. These fields name you, the reporting actor. next.role names the requested successor; requesting Architect does not make Architect the request author. Never relabel an explicit contradictory author. Before returning, check the required fields, assigned work/phase, actual revision, next role and phase, and character limits against the contract below. Omit optional metadata you cannot establish instead of guessing. -Return exactly one JSON object conforming to this contract, without Markdown fences or surrounding prose. The handoff field is the complete ready-to-send request for the next owner; only that field becomes the next request. Artifacts and evidence travel as structured references. For a command on an uncommitted candidate, use evidence.revision=null and candidate=the retained candidate/command metadata path; a base HEAD is not its tested revision. Use a commit SHA only when the tested inputs correspond to that commit. Report observations as observations; schema validation does not establish test results, approvals or delivery readiness. +Return exactly one JSON object conforming to this contract, without Markdown fences or surrounding prose. The handoff field is the complete ready-to-send request for the next owner; only that field becomes the next request. Artifacts and evidence travel as structured references. For an uncommitted candidate, report evidence.revision=null and candidate=the retained candidate/command metadata path. Report observations as observations; schema validation does not establish test results, approvals or delivery readiness. diff --git a/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md b/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md index b5044f4b2..00598ac38 100644 --- a/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md +++ b/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md @@ -36,4 +36,4 @@ Return exactly one JSON object satisfying: 256000}> -{request} +{request} diff --git a/examples/adversarial-implementation/tests/Delivery.test.md b/examples/adversarial-implementation/tests/Delivery.test.md index 1b6a881c4..ae994a1cc 100644 --- a/examples/adversarial-implementation/tests/Delivery.test.md +++ b/examples/adversarial-implementation/tests/Delivery.test.md @@ -180,3 +180,53 @@ pwd -P + + + + + + + +```bash exec as="head" +git rev-parse HEAD +``` + + + + + + + + + + + + + +{raw} + + + + + + + + +{execution.output} + + + + + + + + + + + + + + + + + diff --git a/examples/adversarial-implementation/tests/ProductionRole.test.md b/examples/adversarial-implementation/tests/ProductionRole.test.md index 32a87e446..dbaf20e74 100644 --- a/examples/adversarial-implementation/tests/ProductionRole.test.md +++ b/examples/adversarial-implementation/tests/ProductionRole.test.md @@ -169,9 +169,10 @@ false - - + + + {schemaText} diff --git a/examples/adversarial-implementation/tests/Submission.test.md b/examples/adversarial-implementation/tests/Submission.test.md index 39c0e6571..3b9c969da 100644 --- a/examples/adversarial-implementation/tests/Submission.test.md +++ b/examples/adversarial-implementation/tests/Submission.test.md @@ -10,6 +10,10 @@ pwd -P + + + + diff --git a/examples/adversarial-implementation/tests/agents/summary-correction.md b/examples/adversarial-implementation/tests/agents/summary-correction.md new file mode 100644 index 000000000..6f8a70118 --- /dev/null +++ b/examples/adversarial-implementation/tests/agents/summary-correction.md @@ -0,0 +1,10 @@ + +{initial.text} + + + + +{text} + + + diff --git a/examples/adversarial-implementation/tests/components/Fixture/Composition.ts b/examples/adversarial-implementation/tests/components/Fixture/Composition.ts new file mode 100644 index 000000000..489368059 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/Composition.ts @@ -0,0 +1,18 @@ +import { Component } from "@executablemd/core/api"; +import { useCompositionComponents } from "@executablemd/git"; +import { useRunComposition } from "@executablemd/git/deno"; +import { z } from "zod"; + +export const props = { + type: "object", + properties: { repository: { type: "string" }, directory: { type: "string" } }, + required: ["repository", "directory"], + additionalProperties: false, +}; + +export default function* Composition(input: unknown) { + const selected = z.strictObject({ repository: z.string(), directory: z.string() }).parse(input); + yield* useCompositionComponents(); + yield* useRunComposition({ root: selected.directory, cwd: selected.repository }); + yield* Component.operations.content(); +} diff --git a/examples/adversarial-implementation/tests/components/Fixture/Fallback.md b/examples/adversarial-implementation/tests/components/Fixture/Fallback.md index cb9a071c6..5ed33b62b 100644 --- a/examples/adversarial-implementation/tests/components/Fixture/Fallback.md +++ b/examples/adversarial-implementation/tests/components/Fixture/Fallback.md @@ -45,7 +45,7 @@ git symbolic-ref --short HEAD - + @@ -105,7 +105,7 @@ printf '%s' "$reached" {primaryText} - + {replacementText} @@ -210,7 +210,7 @@ fi -{JSON.stringify(stray)} +{JSON.stringify(stray)} diff --git a/examples/adversarial-implementation/tests/production-components/NativeGuide.ts b/examples/adversarial-implementation/tests/production-components/NativeGuide.ts index abee5d125..0c16ca9e3 100644 --- a/examples/adversarial-implementation/tests/production-components/NativeGuide.ts +++ b/examples/adversarial-implementation/tests/production-components/NativeGuide.ts @@ -18,7 +18,9 @@ export default function* NativeGuide(input: unknown) { *prompt(args, next) { calls++; const text = args[0]; - path ??= text.split("Read the frozen UTF-8 file at ")[1]?.split(" before returning")[0]; + path ??= text + .split("The frozen UTF-8 file at ")[1] + ?.split(" contains the submission schema")[0]; if (path === undefined || (calls === 1 && text.includes('"$defs"'))) { throw new Error("The native request did not stage its submission guide."); } diff --git a/examples/adversarial-implementation/tests/production-components/Phase/Provider.md b/examples/adversarial-implementation/tests/production-components/Phase/Provider.md new file mode 100644 index 000000000..413390fb8 --- /dev/null +++ b/examples/adversarial-implementation/tests/production-components/Phase/Provider.md @@ -0,0 +1,10 @@ +--- +props: + type: object + properties: + agent: {type: string} + required: [agent] + additionalProperties: false +--- + + diff --git a/packages/acp/src/provider.ts b/packages/acp/src/provider.ts index 740cbf233..0b79a61a4 100644 --- a/packages/acp/src/provider.ts +++ b/packages/acp/src/provider.ts @@ -1826,6 +1826,24 @@ function* useAcpxProviderState( prepared: Prepared, intent: EnsureIntent, ): Operation { + if (newSessionOptions?.disallowedTools !== undefined) { + const launchOptionsRecord = yield* until(store.load(prepared.sessionKey)); + if (launchOptionsRecord !== undefined) { + const stored = launchOptionsRecord.acpx?.session_options; + const denialsMatch = + JSON.stringify(stored?.disallowed_tools) === + JSON.stringify(newSessionOptions.disallowedTools); + const environmentMatches = Object.entries(newSessionOptions.env ?? {}).every( + ([key, value]) => stored?.env?.[key] === value, + ); + if (!denialsMatch || !environmentMatches) { + throw new AcpxSessionPolicyError( + "The launchOptionsRecord session was not created with this provider's tool and environment controls. " + + "Preserve it; this profile does not retrofit existing conversations.", + ); + } + } + } if (prepared.kind === "existing") { return prepared.entry; } diff --git a/packages/acp/tests/provider.test.ts b/packages/acp/tests/provider.test.ts index 8288e0005..daab9f989 100644 --- a/packages/acp/tests/provider.test.ts +++ b/packages/acp/tests/provider.test.ts @@ -108,6 +108,46 @@ function* collectPrompt( } describe("Tier AP — ACPX provider", () => { + it("workflow-role controls refuse retained sessions that lack their creation settings", function* () { + const key = deriveSessionKey("scribe-cmd", CWD); + const controls = { + env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" }, + disallowedTools: ["ScheduleWakeup"], + }; + for (const compatible of [false, true]) { + yield* scoped(function* () { + yield* useFlatWorld(CWD); + const harness = createFakeRuntime(); + const record = makeRecord("scribe-cmd", CWD); + if (compatible) { + record.acpx = { + session_options: { env: controls.env, disallowed_tools: controls.disallowedTools }, + }; + } + const factory = createAcpxProvider({ + createRuntime: harness.create, + sessionStore: makeStore({ [key]: record }), + agentRegistry: makeRegistry({ scribe: "scribe-cmd" }), + newSessionOptions: controls, + }); + yield* factory({ defaultAgent: "scribe", permissionMode: "deny-all" }, stubCoordinator()); + let failure: unknown; + try { + yield* collectPrompt("continue the original assignment"); + } catch (error) { + failure = error; + } + expect(harness.turns.length).toBe(compatible ? 1 : 0); + if (!compatible) { + expect(harness.ensureCalls).toEqual([]); + expect(failure).toBeInstanceOf(AcpxSessionPolicyError); + } else { + expect(failure).toBeUndefined(); + } + }); + } + }); + it("1039 A3: a message-only upstream failure supplies no inferred diagnosis", function* () { const harness = createFakeRuntime(); harness.script({ result: { status: "failed", error: { message: "Internal error" } } }); diff --git a/packages/acp/tests/released-runtime.test.ts b/packages/acp/tests/released-runtime.test.ts index 05dc84d73..a801e61a8 100644 --- a/packages/acp/tests/released-runtime.test.ts +++ b/packages/acp/tests/released-runtime.test.ts @@ -341,6 +341,46 @@ describe( "released ACPX production consumers", { sanitizeOps: false, sanitizeResources: false }, () => { + it("workflow role deny lists and environment survive a cold session reopen", function* () { + const current = yield* world(); + const controls = { + disallowedTools: ["ScheduleWakeup"], + env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" }, + }; + for (const reopened of [false, true]) { + yield* scoped(function* () { + const runtime = createAcpRuntime({ + cwd: current.dir, + sessionStore: current.store, + agentRegistry: current.registry, + permissionMode: "deny-all", + }); + yield* ensure(() => until(runtime.shutdown())); + const handle = yield* until( + runtime.ensureSession({ + sessionKey: "controlled-role", + agent: "scribe", + mode: "persistent", + ...(reopened ? {} : { sessionOptions: controls }), + }), + ); + const turn = runtime.startTurn({ + handle, + text: "hello", + mode: "prompt", + requestId: reopened ? "continued" : "initial", + }); + for (const _event of yield* each(stream(turn.events))) { + yield* each.next(); + } + yield* until(turn.result); + const record = yield* until(current.store.load("controlled-role")); + expect(record?.acpx?.session_options?.disallowed_tools).toEqual(controls.disallowedTools); + expect(record?.acpx?.session_options?.env).toEqual(controls.env); + }); + } + }); + it("released child environments are snapshots, isolated and absent from retained state", function* () { const current = yield* world(); const parent = process.env.CLAUDE_CODE_EXECUTABLE; diff --git a/packages/acp/vendor/acpx/MANIFEST.json b/packages/acp/vendor/acpx/MANIFEST.json index 0cbcc9cc5..6f2e2dc31 100644 --- a/packages/acp/vendor/acpx/MANIFEST.json +++ b/packages/acp/vendor/acpx/MANIFEST.json @@ -31,7 +31,7 @@ { "path": "PROVENANCE.md", "kind": "provenance", - "sha256": "f1b825fc9603337eba9d12d607127892ba0ff33c54a20066f7c72bdaaa300ac2" + "sha256": "9766d8f7199310533aabaa970d83c9e6130ec5481312289758995fbccd8cf22b" }, { "path": "generated/agent-registry-Cz9fD9H1.js", @@ -51,12 +51,12 @@ { "path": "generated/ipc-BbYFsKOj.js", "kind": "generated", - "sha256": "423450379d7f13a5efb4d5e62e2f224e5420a96ea2b1e94efca5aef563a12c31" + "sha256": "34d624c639593611bce5849617f846abb4bb784e6228d00d90c523812005f841" }, { "path": "generated/queue-owner-runtime-B0mPqOu4.js", "kind": "generated", - "sha256": "e2bd0fd71b49e51697d42e7f4dae7a97aee525dfe17ddbeab8efae2d93951b6c" + "sha256": "86ba2dd9430627db263f1c986b8733f34d37bd06bf6cf6a3ae600d63e3b756a5" }, { "path": "generated/runtime.d.ts", @@ -71,7 +71,7 @@ { "path": "generated/session-options-BlnP3WAc.d.ts", "kind": "generated", - "sha256": "e4484e22e1549b1297d276370c5542f8ef5cf569cdadff347f0c39111817b7aa" + "sha256": "d0df6fcd566d0179151aa7fa4aef04e25dd4042461d2609384b9cf8843ebd854" }, { "path": "generated/watch-bQXyUy3z.js", diff --git a/packages/acp/vendor/acpx/PROVENANCE.md b/packages/acp/vendor/acpx/PROVENANCE.md index 702a1ff8d..1e2511dd0 100644 --- a/packages/acp/vendor/acpx/PROVENANCE.md +++ b/packages/acp/vendor/acpx/PROVENANCE.md @@ -246,3 +246,12 @@ Keep the materialization patch and packaging exceptions until a released API preserves explicit backend acceptance, non-asserting occupancy and the joined asserting-save barrier. Two released seams never authorize deleting that third contract or the complete vendor directory. + +### Claude workflow-role tool controls + +The carried session-option path forwards `disallowedTools` to Claude's existing +SDK option and retains it as `disallowed_tools` beside the session environment. +Normalization, record readback and IPC keep that field. This changes launch-option +plumbing only; the Claude adapter and SDK producer inputs are unchanged. The +`claude-workflow-role` CLI profile selects these options for workflow roles. +Ordinary interactive sessions retain their original defaults. diff --git a/packages/acp/vendor/acpx/generated/ipc-BbYFsKOj.js b/packages/acp/vendor/acpx/generated/ipc-BbYFsKOj.js index 0f0c404f3..0123db38b 100644 --- a/packages/acp/vendor/acpx/generated/ipc-BbYFsKOj.js +++ b/packages/acp/vendor/acpx/generated/ipc-BbYFsKOj.js @@ -1696,6 +1696,8 @@ function assignParsedSessionOptions(state, raw) { const parsedSessionOptions = {}; if (typeof sessionOptions.model === "string") parsedSessionOptions.model = sessionOptions.model; if (isStringArray(sessionOptions.allowed_tools)) parsedSessionOptions.allowed_tools = [...sessionOptions.allowed_tools]; + // XMD workflow role controls: preserve the explicit tool deny list. + if (isStringArray(sessionOptions.disallowed_tools)) parsedSessionOptions.disallowed_tools = [...sessionOptions.disallowed_tools]; if (isPositiveInteger$1(sessionOptions.max_turns)) parsedSessionOptions.max_turns = sessionOptions.max_turns; assignSessionOptionSystemPrompt(parsedSessionOptions, sessionOptions.system_prompt); const env = parseStringMap(sessionOptions.env); @@ -3935,6 +3937,13 @@ function parseSessionOptions(value) { const sessionOptions = {}; if (!assignSessionModel(sessionOptions, record.model)) return null; if (!assignSessionAllowedTools(sessionOptions, record.allowedTools)) return null; + // XMD workflow role controls: preserve the explicit tool deny list. + if (record.disallowedTools != null) { + // XMD workflow role controls: preserve the explicit tool deny list. + if (!isStringArray(record.disallowedTools)) return null; + // XMD workflow role controls: preserve the explicit tool deny list. + sessionOptions.disallowedTools = [...record.disallowedTools]; + } if (!assignSessionMaxTurns(sessionOptions, record.maxTurns)) return null; if (!assignSessionSystemPrompt(sessionOptions, record.systemPrompt)) return null; if (!assignSessionEnv(sessionOptions, record.env)) return null; diff --git a/packages/acp/vendor/acpx/generated/queue-owner-runtime-B0mPqOu4.js b/packages/acp/vendor/acpx/generated/queue-owner-runtime-B0mPqOu4.js index f65ddb85c..33f02df08 100644 --- a/packages/acp/vendor/acpx/generated/queue-owner-runtime-B0mPqOu4.js +++ b/packages/acp/vendor/acpx/generated/queue-owner-runtime-B0mPqOu4.js @@ -1266,6 +1266,9 @@ function resolveClaudeCodeSettingSources(env = process.env) { function assignClaudeCodeOptions(target, options) { if (typeof options.model === "string" && options.model.trim().length > 0) target.model = options.model; if (Array.isArray(options.allowedTools)) target.allowedTools = [...options.allowedTools]; + // XMD role tool denials reach the supported Claude SDK option. + // XMD workflow role controls: preserve the explicit tool deny list. + if (Array.isArray(options.disallowedTools)) target.disallowedTools = [...options.disallowedTools]; if (typeof options.maxTurns === "number") target.maxTurns = options.maxTurns; } function assignClaudeCodeSystemPrompt(target, systemPrompt) { @@ -4420,6 +4423,8 @@ function cloneSessionOptions(options) { return { model: options.model, allowed_tools: options.allowed_tools ? [...options.allowed_tools] : void 0, + // XMD workflow role controls: keep denials when turn state is cloned. + disallowed_tools: options.disallowed_tools ? [...options.disallowed_tools] : void 0, max_turns: options.max_turns, ...options.system_prompt !== void 0 ? { system_prompt: cloneSystemPromptOption(options.system_prompt) } : {}, ...options.env !== void 0 ? { env: { ...options.env } } : {} @@ -4672,6 +4677,8 @@ function mergeSessionOptions(preferred, fallback) { const merged = { ...fallback }; assignDefinedOption(merged, "model", preferred?.model); assignDefinedOption(merged, "allowedTools", preferred?.allowedTools); + // XMD workflow role controls: preserve the explicit tool deny list. + assignDefinedOption(merged, "disallowedTools", preferred?.disallowedTools); assignDefinedOption(merged, "maxTurns", preferred?.maxTurns); assignDefinedOption(merged, "systemPrompt", preferred?.systemPrompt); assignDefinedOption(merged, "env", mergeEnvRecords(fallback?.env, preferred?.env)); @@ -4710,6 +4717,8 @@ function sessionOptionsFromRecord(record) { const sessionOptions = {}; assignDefinedOption(sessionOptions, "model", nonEmptyString(stored.model)); assignDefinedOption(sessionOptions, "allowedTools", storedAllowedTools(stored.allowed_tools)); + // XMD workflow role controls: preserve the explicit tool deny list. + assignDefinedOption(sessionOptions, "disallowedTools", storedAllowedTools(stored.disallowed_tools)); assignDefinedOption(sessionOptions, "maxTurns", storedMaxTurns(stored.max_turns)); assignDefinedOption(sessionOptions, "systemPrompt", normalizeSystemPromptOption(stored.system_prompt)); assignDefinedOption(sessionOptions, "env", storedEnvRecord(stored.env)); @@ -4719,6 +4728,8 @@ function persistedSessionOptions(options) { const next = { model: nonEmptyString(options.model), allowed_tools: Array.isArray(options.allowedTools) ? [...options.allowedTools] : void 0, + // XMD workflow role controls: preserve the explicit tool deny list. + disallowed_tools: Array.isArray(options.disallowedTools) ? [...options.disallowedTools] : void 0, max_turns: typeof options.maxTurns === "number" ? options.maxTurns : void 0, system_prompt: normalizeSystemPromptOption(options.systemPrompt), env: storedEnvRecord(options.env) @@ -4726,7 +4737,8 @@ function persistedSessionOptions(options) { return hasPersistedSessionOptions(next) ? next : void 0; } function hasPersistedSessionOptions(options) { - return options.model !== void 0 || options.allowed_tools !== void 0 || options.max_turns !== void 0 || options.system_prompt !== void 0 || options.env !== void 0; + // XMD workflow role controls: preserve the explicit tool deny list. + return options.model !== void 0 || options.allowed_tools !== void 0 || options.disallowed_tools !== void 0 || options.max_turns !== void 0 || options.system_prompt !== void 0 || options.env !== void 0; } function storedEnvRecord(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return; diff --git a/packages/acp/vendor/acpx/generated/session-options-BlnP3WAc.d.ts b/packages/acp/vendor/acpx/generated/session-options-BlnP3WAc.d.ts index dbbac25d3..7a31d29b1 100644 --- a/packages/acp/vendor/acpx/generated/session-options-BlnP3WAc.d.ts +++ b/packages/acp/vendor/acpx/generated/session-options-BlnP3WAc.d.ts @@ -166,6 +166,8 @@ type AcpClientOptions = { sessionOptions?: { model?: string; allowedTools?: string[]; + // XMD workflow role controls: preserve the explicit tool deny list. + disallowedTools?: string[]; maxTurns?: number; systemPrompt?: string | { append: string; @@ -282,6 +284,8 @@ type SessionAcpxState = { session_options?: { model?: string; allowed_tools?: string[]; + // XMD workflow role controls: preserve the explicit tool deny list. + disallowed_tools?: string[]; max_turns?: number; system_prompt?: string | { append: string; @@ -336,6 +340,8 @@ type SystemPromptOption = string | { type SessionAgentOptions = { model?: string; allowedTools?: string[]; + // XMD workflow role controls: preserve the explicit tool deny list. + disallowedTools?: string[]; maxTurns?: number; systemPrompt?: SystemPromptOption; /** diff --git a/packages/cli/src/agent-stack.ts b/packages/cli/src/agent-stack.ts index fe5527e79..e00108d81 100644 --- a/packages/cli/src/agent-stack.ts +++ b/packages/cli/src/agent-stack.ts @@ -194,6 +194,16 @@ export function* installAgentProviderStack( ): Operation { const acpx = selectedFactory ?? createAcpxProvider(acp ?? hostAcpDependencies(stack)); yield* registerAgentProvider("acpx", acpx); + yield* registerAgentProvider( + "claude-workflow-role", + createAcpxProvider({ + ...(acp ?? hostAcpDependencies(stack)), + newSessionOptions: { + env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" }, + disallowedTools: ["ScheduleWakeup"], + }, + }), + ); // The trusted host selects its own root provider by name. Document-level // selection goes through the installation protocol; this is the host saying diff --git a/packages/cli/tests/support/adversarial-journey.ts b/packages/cli/tests/support/adversarial-journey.ts index 575698c1d..a92a40e73 100644 --- a/packages/cli/tests/support/adversarial-journey.ts +++ b/packages/cli/tests/support/adversarial-journey.ts @@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url"; import { env } from "@executablemd/runtime"; import { readTextFile } from "@effectionx/fs"; -const testedSuites = ["ProductionRole", "Preparation", "Submission"]; +const testedSuites = ["ProductionRole", "Preparation", "Submission", "Delivery"]; export function* runAdversarialJourney(suite: string) { const retainedEvidence = suite.startsWith("ArtifactReturn") @@ -45,12 +45,9 @@ export function* runAdversarialJourney(suite: string) { `\n${source}\n`, ]), "--include", - testedSuites.includes(suite) - ? "examples/adversarial-implementation/components" + testedSuites.includes(suite) && suite !== "Delivery" + ? "examples/adversarial-implementation/tests/production-components" : "examples/adversarial-implementation/tests/components", - ...(suite === "Submission" - ? ["--include", "examples/adversarial-implementation/tests/production-components"] - : []), "--include", "examples/adversarial-implementation/components", ], diff --git a/scripts/tests/acpx-vendor.test.ts b/scripts/tests/acpx-vendor.test.ts index 344bd3c25..f838626eb 100644 --- a/scripts/tests/acpx-vendor.test.ts +++ b/scripts/tests/acpx-vendor.test.ts @@ -41,6 +41,7 @@ const SEAMS: readonly string[] = [ "XMD prompt diagnostic:", "XMD prompt usage:", "XMD requested model:", + "XMD workflow role controls:", "XMD exact-model replay:", ]; diff --git a/specs/acp-client-spec.md b/specs/acp-client-spec.md index 954b19ecb..aa6d7ff44 100644 --- a/specs/acp-client-spec.md +++ b/specs/acp-client-spec.md @@ -730,6 +730,30 @@ Registering a provider only makes a factory **resolvable**; it does not install that provider into the `Agent` Api. Operations still report a missing provider until one is installed as a root provider or by AgentProvider. +### Claude workflow-role profile + +The CLI registers `claude-workflow-role` alongside ordinary `acpx`. The +adversarial workflow selects this provider only around Claude role calls, +including foundation, document preparation, independent review and response-only +correction. It uses the same carried adapter, session store and host identity +coordination as ordinary calls. Interactive Sidekick configuration is unchanged. + +The profile sets `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` in the child session +environment and supplies `disallowedTools: ["ScheduleWakeup"]` through ACPX to +Claude's supported SDK option. ACPX retains the deny list with the session's +creation options, restores it on reconnect and carries it through IPC. These +settings remove the native shell/subagent background parameter, automatic shell +backgrounding and the wakeup tool; they do not claim to disable every possible +background service or MCP task. The role still owns every process it starts and +must join it before returning its result. A foreground timeout is a failed +command, not a promise of a later notification or an accepted role result. + +Session creation options do not retrofit an existing conversation. Before ensure +or prompt, a provider with an explicit deny list compares that list and every +selected environment value with the retained session options. A missing or +different value refuses rather than silently reconfiguring, replacing or +resetting the session. Fresh sessions record these settings on creation. + ### Seeded configuration `installAgentComponents({ defaultAgent, permissionMode })` seeds the From 2cc94e2032c62bb98abb9dd2bd3761ef19fe8b3d Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 9 Nov 2026 23:41:47 -0500 Subject: [PATCH 06/15] Clarify the retained-session control refusal --- packages/acp/src/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/acp/src/provider.ts b/packages/acp/src/provider.ts index 0b79a61a4..df7a06852 100644 --- a/packages/acp/src/provider.ts +++ b/packages/acp/src/provider.ts @@ -1838,7 +1838,7 @@ function* useAcpxProviderState( ); if (!denialsMatch || !environmentMatches) { throw new AcpxSessionPolicyError( - "The launchOptionsRecord session was not created with this provider's tool and environment controls. " + + "The retained session was not created with this provider's tool and environment controls. " + "Preserve it; this profile does not retrofit existing conversations.", ); } From b12d4e7674bbfe6f69a113119d4952427fea922e Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 00:04:08 -0500 Subject: [PATCH 07/15] Prepare complete direct review fixtures for response validation --- .../tests/adversarial-session-continuity.test.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/cli/tests/adversarial-session-continuity.test.ts b/packages/cli/tests/adversarial-session-continuity.test.ts index 4f279cfa2..2e2d7ba74 100644 --- a/packages/cli/tests/adversarial-session-continuity.test.ts +++ b/packages/cli/tests/adversarial-session-continuity.test.ts @@ -455,6 +455,7 @@ function* reviewWorld(fake: FakeAcp, create = fake.create): Operation' + ""; const reviewSource = ''; @@ -543,7 +544,7 @@ describe( choices: ["fable", "claude-fable-5-1[1m]"], attempt, }); - expect(ran.ok).toBe(true); + expect(ran).toMatchObject({ ok: true }); const settled = outcome.parse( JSON.parse(yield* readTextFile(join(attempt, "outcome.json"))), ); @@ -763,11 +764,6 @@ describe( ); function* reviewAt(sha: string) { const attempt = join(harness.delivery.directory as string, `review-${sha}`); - yield* ensureDir(attempt); - yield* writeTextFile( - join(attempt, "assignment.json"), - JSON.stringify({ work: "production-review", revision: sha, reviewer }), - ); const ran = yield* harness.run(roleSource, { delivery: harness.delivery, revision: sha, @@ -786,13 +782,14 @@ describe( ).toBe("unavailable"); expect(fake.prompts).toEqual([]); fake.configuration(catalog(["fable"])); - fake.script({ reply: review }); + const currentReview = review.replace(revision, "c".repeat(40)); + fake.script({ reply: currentReview }); const first = yield* reviewAt("c".repeat(40)); - expect(first.ran.ok).toBe(true); + expect(first.ran).toMatchObject({ ok: true }); expect(fake.prompts).toHaveLength(1); expect( outcome.parse(JSON.parse(yield* readTextFile(join(first.attempt, "outcome.json")))).turn, - ).toEqual({ state: "reply", reply: review }); + ).toEqual({ state: "reply", reply: currentReview }); const established = z .object({ name: z.string(), From 35b9329e7c462e7d5b268f340d16c5cadea21169 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 04:50:36 -0500 Subject: [PATCH 08/15] Install workflow role providers in their authored session scope --- examples/adversarial-implementation/README.md | 3 +- .../components/Adversarial/PlannerRole.md | 6 +- .../components/Delivery/Role.md | 5 +- .../components/Phase/Provider.md | 14 -- .../components/Phase/ProviderName.md | 12 ++ .../components/Phase/Role.md | 3 +- .../components/Ticket/PreparationPrompt.md | 3 +- .../Phase/{Provider.md => ProviderName.md} | 3 +- .../adversarial-session-continuity.test.ts | 126 +++++++++++++++++- packages/cli/tests/support/fake-acp.ts | 13 ++ 10 files changed, 163 insertions(+), 25 deletions(-) delete mode 100644 examples/adversarial-implementation/components/Phase/Provider.md create mode 100644 examples/adversarial-implementation/components/Phase/ProviderName.md rename examples/adversarial-implementation/tests/production-components/Phase/{Provider.md => ProviderName.md} (56%) diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index e25372212..f2a47dddb 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -376,7 +376,8 @@ from claiming readiness or merging independently. ## Prepare requests without metadata repair turns Claude workflow-role calls use the CLI's scoped `claude-workflow-role` provider. -It disables native shell/subagent backgrounding and denies `ScheduleWakeup`; +Each role installs it directly around its authored session, covering initial and +continued prompts. It disables native shell/subagent backgrounding and denies `ScheduleWakeup`; ordinary interactive Sidekick calls keep their defaults. A retained session that lacks matching creation settings refuses before another prompt. Each role joins its owned processes and returns its result in the same call. The upfront brief diff --git a/examples/adversarial-implementation/components/Adversarial/PlannerRole.md b/examples/adversarial-implementation/components/Adversarial/PlannerRole.md index 9eb45b098..ff3b300a7 100644 --- a/examples/adversarial-implementation/components/Adversarial/PlannerRole.md +++ b/examples/adversarial-implementation/components/Adversarial/PlannerRole.md @@ -12,7 +12,7 @@ props: - + You remain the original Planner. This is response-only correction {props.correction.ordinal} with {props.correction.remaining} remaining. Shorten only the rejected summary or handoff to its stated bound. Every other result value is frozen by the supplied schema, including the API, outcome, source revision, artifacts, assessment and command evidence. @@ -48,4 +48,6 @@ Return exactly one JSON object satisfying this contract. Evidence names actual c - + + +{request}{request} diff --git a/examples/adversarial-implementation/components/Delivery/Role.md b/examples/adversarial-implementation/components/Delivery/Role.md index e80709fff..76b4e6af4 100644 --- a/examples/adversarial-implementation/components/Delivery/Role.md +++ b/examples/adversarial-implementation/components/Delivery/Role.md @@ -35,7 +35,8 @@ Return exactly one JSON object satisfying this schema. acceptance lists every cl 64000}> - + + @@ -46,4 +47,4 @@ Return exactly one JSON object satisfying this schema. acceptance lists every cl {turn.reply} - + diff --git a/examples/adversarial-implementation/components/Phase/Provider.md b/examples/adversarial-implementation/components/Phase/Provider.md deleted file mode 100644 index a432c0c5f..000000000 --- a/examples/adversarial-implementation/components/Phase/Provider.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -description: 'Apply the Claude workflow-role launch controls for this content. Use ....' -props: - type: object - properties: - agent: {type: string} - required: [agent] - additionalProperties: false ---- - - - - - diff --git a/examples/adversarial-implementation/components/Phase/ProviderName.md b/examples/adversarial-implementation/components/Phase/ProviderName.md new file mode 100644 index 000000000..7329fd836 --- /dev/null +++ b/examples/adversarial-implementation/components/Phase/ProviderName.md @@ -0,0 +1,12 @@ +--- +description: 'Select the registered provider name for a workflow role. Use and wrap the authored role session in .' +props: + type: object + properties: + agent: {type: string} + required: [agent] + additionalProperties: false +returns: {type: string} +--- + + diff --git a/examples/adversarial-implementation/components/Phase/Role.md b/examples/adversarial-implementation/components/Phase/Role.md index 10d381533..1f9f6f046 100644 --- a/examples/adversarial-implementation/components/Phase/Role.md +++ b/examples/adversarial-implementation/components/Phase/Role.md @@ -68,4 +68,5 @@ props: -{request}{request} + +{request}{request} diff --git a/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md b/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md index 00598ac38..5331a2214 100644 --- a/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md +++ b/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md @@ -36,4 +36,5 @@ Return exactly one JSON object satisfying: 256000}> -{request} + +{request}{request} diff --git a/examples/adversarial-implementation/tests/production-components/Phase/Provider.md b/examples/adversarial-implementation/tests/production-components/Phase/ProviderName.md similarity index 56% rename from examples/adversarial-implementation/tests/production-components/Phase/Provider.md rename to examples/adversarial-implementation/tests/production-components/Phase/ProviderName.md index 413390fb8..ed72ec7ae 100644 --- a/examples/adversarial-implementation/tests/production-components/Phase/Provider.md +++ b/examples/adversarial-implementation/tests/production-components/Phase/ProviderName.md @@ -5,6 +5,7 @@ props: agent: {type: string} required: [agent] additionalProperties: false +returns: {type: string} --- - + diff --git a/packages/cli/tests/adversarial-session-continuity.test.ts b/packages/cli/tests/adversarial-session-continuity.test.ts index 2e2d7ba74..f758a92d7 100644 --- a/packages/cli/tests/adversarial-session-continuity.test.ts +++ b/packages/cli/tests/adversarial-session-continuity.test.ts @@ -34,7 +34,7 @@ import { openReplSession, submitReplEntry } from "../src/repl/session.ts"; import type { ReplSession, ReplSessionOptions } from "../src/repl/session.ts"; import type { ReplQuestion } from "../src/repl/elicitation.ts"; import { createFakeAcp, makeStore } from "./support/fake-acp.ts"; -import type { FakeAcp } from "./support/fake-acp.ts"; +import type { FakeAcp, FakeStore } from "./support/fake-acp.ts"; import { AGENT_SOURCE, world } from "../../acp/tests/released-peer.ts"; import PhaseSession, { props as phaseSessionProps, @@ -381,6 +381,7 @@ const captured = z.strictObject({ interface ReviewWorld { root: string; + store: FakeStore; delivery: Record; asked: unknown[]; run(source: string, props: Record): Operation<{ ok: boolean; text: string }>; @@ -391,6 +392,7 @@ function* reviewWorld(fake: FakeAcp, create = fake.create): Operation' + + ''; +const continuedRoleSource = + ''; + +describe("production workflow provider scope", () => { + it("applies controls through each actual Claude role and retains them across cold reentry", function* () { + for (const role of ["phase", "foundation", "preparation", "delivery"]) { + const fake = createFakeAcp(); + fake.configuration(catalog(["fixture-default", "opus", "fable"], ["medium", "high"])); + const harness = yield* reviewWorld(fake); + const assignment = { + repository: harness.delivery.repository, + artifactDirectory: join(harness.root, "submission"), + identityDirectory: join(harness.root, "sessions"), + phase: "implementation", + work: "provider-scope", + revision, + request: "Use the saved evidence.", + assessment: { + mechanical: false, + architecture: false, + acceptance: true, + risk: null, + reason: "Inspect the accepted behavior.", + evidence: ["accepted.md"], + }, + }; + yield* ensureDir(assignment.artifactDirectory); + const props = { assignment }; + let source = scopedRoleSource; + let continued = continuedRoleSource; + if (role === "foundation") { + source = + ''; + continued = + ''; + } else if (role === "preparation") { + fake.script({ reply: "Seeded the original owner." }); + expect( + yield* harness.run(`${source}`, props), + ).toMatchObject({ ok: true }); + source = + ''; + continued = source; + } else if (role === "delivery") { + source = roleSource; + continued = source; + } + const selected = role === "delivery" ? "fable" : "opus"; + const callProps = + role === "delivery" + ? { + delivery: harness.delivery, + revision, + reviewer: { + provider: "claude", + model: selected, + effort: "high", + session: "controlled-owner", + }, + choices: [selected], + attempt: join(harness.root, "delivery", "attempt"), + } + : props; + for (const text of [source, continued]) { + fake.script({ reply: role === "delivery" ? review : "Completed the assigned work." }); + const ran = yield* harness.run( + `${text}`, + callProps, + ); + if (!ran.ok) { + throw new Error(`${role}: ${ran.text}`); + } + const ensured = fake.ensured.at(-1)!; + expect(ensured.sessionOptions).toEqual(workflowControls); + expect(ensured.requestedModel).toBe(selected); + expect(harness.store.records.get(ensured.sessionKey)?.acpx?.session_options).toEqual({ + model: selected, + env: workflowControls.env, + disallowed_tools: workflowControls.disallowedTools, + }); + expect(fake.running(ensured.sessionKey)).toEqual({ model: selected, effort: "high" }); + } + expect(fake.turns).toHaveLength(role === "preparation" ? 3 : 2); + expect(new Set(fake.turns.map((entry) => entry.handle.sessionKey)).size).toBe(1); + } + }); + + it("keeps ordinary acpx unchanged and refuses to retrofit its retained owner", function* () { + const fake = createFakeAcp(); + fake.configuration(catalog(["opus"], ["high"])); + const harness = yield* reviewWorld(fake); + fake.script({ reply: "Original ordinary owner." }); + const ordinary = + 'Use the original ordinary provider.'; + expect(yield* harness.run(ordinary, {})).toMatchObject({ ok: true }); + expect(fake.ensured[0]?.sessionOptions).toBeUndefined(); + const before = JSON.stringify([...harness.store.records]); + const controlled = + '' + + ordinary + + ""; + const refused = yield* harness.run(controlled, {}); + expect(refused).toMatchObject({ ok: false }); + expect(refused.text).toContain("does not retrofit existing conversations"); + expect(fake.prompts).toHaveLength(1); + expect(fake.ensured).toHaveLength(1); + expect(JSON.stringify([...harness.store.records])).toBe(before); + }); +}); + function catalog(models: string[], efforts: string[] = ["low", "high"]) { return { model: { selected: models[0]!, choices: models.map((id) => ({ id })) }, diff --git a/packages/cli/tests/support/fake-acp.ts b/packages/cli/tests/support/fake-acp.ts index fd7bc6a7c..454ef9726 100644 --- a/packages/cli/tests/support/fake-acp.ts +++ b/packages/cli/tests/support/fake-acp.ts @@ -779,6 +779,19 @@ export function createFakeAcp(): FakeAcp { cumulative_token_usage: {}, request_token_usage: {}, }; + const sessionOptions = input.sessionOptions; + if (sessionOptions !== undefined || input.requestedModel !== undefined) { + record.acpx = { + session_options: { + model: input.requestedModel ?? sessionOptions?.model, + allowed_tools: sessionOptions?.allowedTools, + disallowed_tools: sessionOptions?.disallowedTools, + max_turns: sessionOptions?.maxTurns, + system_prompt: sessionOptions?.systemPrompt, + env: sessionOptions?.env, + }, + }; + } if (deferred) { record.sessionMaterialization = { state: "pending", From 8c0cab5487dea32358a956d6cd8011d88bbc2e63 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 05:08:10 -0500 Subject: [PATCH 09/15] Deny deferred Monitor work in workflow role calls --- examples/adversarial-implementation/README.md | 3 ++- .../adversarial-implementation/components/Delivery/Role.md | 2 ++ packages/cli/src/agent-stack.ts | 2 +- packages/cli/tests/adversarial-session-continuity.test.ts | 6 +++++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index f2a47dddb..ce8ef48f0 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -377,7 +377,8 @@ from claiming readiness or merging independently. Claude workflow-role calls use the CLI's scoped `claude-workflow-role` provider. Each role installs it directly around its authored session, covering initial and -continued prompts. It disables native shell/subagent backgrounding and denies `ScheduleWakeup`; +continued prompts. It disables native shell/subagent backgrounding and denies +`ScheduleWakeup` and `Monitor`; ordinary interactive Sidekick calls keep their defaults. A retained session that lacks matching creation settings refuses before another prompt. Each role joins its owned processes and returns its result in the same call. The upfront brief diff --git a/examples/adversarial-implementation/components/Delivery/Role.md b/examples/adversarial-implementation/components/Delivery/Role.md index 76b4e6af4..2449ca3ee 100644 --- a/examples/adversarial-implementation/components/Delivery/Role.md +++ b/examples/adversarial-implementation/components/Delivery/Role.md @@ -19,6 +19,8 @@ saved to `outcome.json` before that release, including when the turn fails. +Finish this native call with its review JSON result. Run required checks in the foreground and join every process you start before returning. A progress message, monitor or future notification does not complete the review. + Perform one independent correctness review of {props.revision} in {props.delivery.repository}. Read the repository instructions and inspect that exact commit and its affected consumers. Keep source unchanged. Reuse applicable evidence; run only focused evidence that distinguishes a finding. Do not run broad confidence suites or wait for CI. Do not delegate, implement, publish comments, or merge. Report a concrete blocker rather than substituting a provider or repeating an uncertain call. Review these recorded acceptance clauses against their original selected sources. A derived check cannot strengthen or replace them. Report a material ambiguity as blocked. diff --git a/packages/cli/src/agent-stack.ts b/packages/cli/src/agent-stack.ts index e00108d81..fdeac9df5 100644 --- a/packages/cli/src/agent-stack.ts +++ b/packages/cli/src/agent-stack.ts @@ -200,7 +200,7 @@ export function* installAgentProviderStack( ...(acp ?? hostAcpDependencies(stack)), newSessionOptions: { env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" }, - disallowedTools: ["ScheduleWakeup"], + disallowedTools: ["ScheduleWakeup", "Monitor"], }, }), ); diff --git a/packages/cli/tests/adversarial-session-continuity.test.ts b/packages/cli/tests/adversarial-session-continuity.test.ts index f758a92d7..6360be674 100644 --- a/packages/cli/tests/adversarial-session-continuity.test.ts +++ b/packages/cli/tests/adversarial-session-continuity.test.ts @@ -464,7 +464,7 @@ const reviewSource = const workflowControls = { env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" }, - disallowedTools: ["ScheduleWakeup"], + disallowedTools: ["ScheduleWakeup", "Monitor"], }; const scopedRoleSource = @@ -676,6 +676,10 @@ describe( expect(settled.turn).toEqual({ state: "reply", reply: review }); expect(fake.prompts).toHaveLength(1); expect(fake.prompts[0]).toContain(`independent correctness review of ${revision}`); + expect(fake.prompts[0]).toContain("Finish this native call with its review JSON result."); + expect(fake.prompts[0]).toContain( + "Run required checks in the foreground and join every process you start before returning.", + ); expect(fake.prompts[0]).toContain( JSON.stringify(acceptance, null, 2).split("\n")[2]!.trim(), ); From ea562974b67d435cd69abec5022f7ce9d1624b6d Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 05:32:14 -0500 Subject: [PATCH 10/15] Avoid repeating identical selected role instructions --- examples/adversarial-implementation/README.md | 4 +++- .../components/Phase/Instructions.md | 8 +++++--- .../tests/Submission.test.md | 13 +++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index ce8ef48f0..0b7da26c3 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -382,7 +382,9 @@ continued prompts. It disables native shell/subagent backgrounding and denies ordinary interactive Sidekick calls keep their defaults. A retained session that lacks matching creation settings refuses before another prompt. Each role joins its owned processes and returns its result in the same call. The upfront brief -contains work, evidence and lifetime rules; the frozen submission guide contains +contains work, evidence and lifetime rules. Identical role instructions selected +as input are delivered once; a different captured version retains the current +role brief too. The frozen submission guide contains response schema, limits and routing and may wait until a result is being prepared. These rules apply to separate phase requests and Ticket role calls. Codex authors diff --git a/examples/adversarial-implementation/components/Phase/Instructions.md b/examples/adversarial-implementation/components/Phase/Instructions.md index b4a60adb7..971bf5227 100644 --- a/examples/adversarial-implementation/components/Phase/Instructions.md +++ b/examples/adversarial-implementation/components/Phase/Instructions.md @@ -39,10 +39,12 @@ You are the repository {props.role}. Perform the assigned phase using the reposi Repository instructions: + Role instructions: - - - + input.path === props.assignment.repository + "/.agents/" + props.role + ".md" && input.content === roleInstructions)}> +Use the selected role instructions below. +{roleInstructions} + Use the provider, model and effort explicitly selected by this workflow; historical role-guide defaults do not replace that allocation. diff --git a/examples/adversarial-implementation/tests/Submission.test.md b/examples/adversarial-implementation/tests/Submission.test.md index 3b9c969da..72d737d53 100644 --- a/examples/adversarial-implementation/tests/Submission.test.md +++ b/examples/adversarial-implementation/tests/Submission.test.md @@ -81,3 +81,16 @@ git -C {quoted} rev-parse HEAD + + + + + + + + + + + + + From 4260c7382f62ccaed5fad647f796c370a053d00b Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 05:36:58 -0500 Subject: [PATCH 11/15] Compare raw role guide contents without trimming --- .../components/Phase/Instructions.md | 2 +- .../adversarial-implementation/tests/Submission.test.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/adversarial-implementation/components/Phase/Instructions.md b/examples/adversarial-implementation/components/Phase/Instructions.md index 971bf5227..ae5806651 100644 --- a/examples/adversarial-implementation/components/Phase/Instructions.md +++ b/examples/adversarial-implementation/components/Phase/Instructions.md @@ -39,7 +39,7 @@ You are the repository {props.role}. Perform the assigned phase using the reposi Repository instructions: - + Role instructions: input.path === props.assignment.repository + "/.agents/" + props.role + ".md" && input.content === roleInstructions)}> Use the selected role instructions below. diff --git a/examples/adversarial-implementation/tests/Submission.test.md b/examples/adversarial-implementation/tests/Submission.test.md index 72d737d53..9d33df425 100644 --- a/examples/adversarial-implementation/tests/Submission.test.md +++ b/examples/adversarial-implementation/tests/Submission.test.md @@ -83,13 +83,13 @@ git -C {quoted} rev-parse HEAD - + - - + + - + From 6f69a4e408372628ab9f0f60ec8a0edc7dfee62e Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 06:14:12 -0500 Subject: [PATCH 12/15] Default submission guide reading to result preparation --- examples/adversarial-implementation/README.md | 6 ++++-- .../components/Phase/Instructions.md | 8 +++++--- .../adversarial-implementation/tests/Submission.test.md | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index 0b7da26c3..45e42c201 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -384,8 +384,10 @@ lacks matching creation settings refuses before another prompt. Each role joins its owned processes and returns its result in the same call. The upfront brief contains work, evidence and lifetime rules. Identical role instructions selected as input are delivered once; a different captured version retains the current -role brief too. The frozen submission guide contains -response schema, limits and routing and may wait until a result is being prepared. +role brief too. The frozen submission guide contains response schema, limits and +routing. Reading defaults to result preparation; an earlier read must resolve a +concrete reporting requirement that blocks a work decision. Every result, +including an early blocker, requires the complete guide within the same call. These rules apply to separate phase requests and Ticket role calls. Codex authors prose committed to workflow documents, including guides, component instructions diff --git a/examples/adversarial-implementation/components/Phase/Instructions.md b/examples/adversarial-implementation/components/Phase/Instructions.md index ae5806651..e56ead10a 100644 --- a/examples/adversarial-implementation/components/Phase/Instructions.md +++ b/examples/adversarial-implementation/components/Phase/Instructions.md @@ -112,8 +112,10 @@ Prepared assignment: Submission guide: The frozen UTF-8 file at {props.submission.path} contains the submission schema, -field limits and next-role routing. You may defer reading it until preparing a -result, including an early blocker, source conflict or unanswered decision. Read -it and return the result within this native call; no formatting turn is scheduled. +field limits and next-role routing. Read it when you begin preparing the phase +result. Open it earlier only if a reporting requirement blocks a concrete +decision in the assigned work. Read the complete guide before any result, +including an early blocker, source conflict or unanswered decision, and return +within this native call; no formatting turn is scheduled. Leave the guide and its receipt unchanged. diff --git a/examples/adversarial-implementation/tests/Submission.test.md b/examples/adversarial-implementation/tests/Submission.test.md index 9d33df425..01af1aa0a 100644 --- a/examples/adversarial-implementation/tests/Submission.test.md +++ b/examples/adversarial-implementation/tests/Submission.test.md @@ -12,7 +12,8 @@ pwd -P - + + From 40d6cf0874afa5186ce8f4db6453d23f84450818 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 07:10:49 -0500 Subject: [PATCH 13/15] Scope API conflict submission guidance to its reporting actor --- .../components/Phase/SubmissionInstructions.md | 4 ++-- examples/adversarial-implementation/tests/Submission.test.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md b/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md index 83bae6abe..441327d8a 100644 --- a/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md +++ b/examples/adversarial-implementation/components/Phase/SubmissionInstructions.md @@ -17,7 +17,7 @@ the current assignment. Return assessment on completed implementation, changes_required, missing_evidence and escalate. For changes_required or missing_evidence, assess only the finite correction being requested, not the original feature. Keep already settled responsibilities closed; a missing test witness does not by itself reopen the production architecture. Completed implementation likewise reports the actual correction rather than inheriting the whole feature scope. Assess actual impact: mechanical, architecture and acceptance booleans; one short impact reason and individual evidence references, plus risk:null unless a concrete consequential uncertainty or risk needs additional reasoning. For high effort, risk contains its own short reason and supporting evidence. Merely touching concurrency, cancellation, durable state, replay or a public boundary is not that reason. Test-only changes can affect acceptance. A mechanical assessment has no architecture, acceptance or complexity impact. New non-mechanical feature work includes Planner acceptance; correction work requires only affected responsibilities. Keep evidence references in artifacts or already recorded documents. Do not request a reviewer or reasoning effort in prose to override this assessment. -Return escalate only from medium Discovery or Review when a concrete newly discovered risk or uncertainty prevents completing this assignment. Keep the same revision and impact flags, supply risk with the specific reason and supporting evidence, and leave the checkout clean. The workflow resumes the same original role/session at high. High cannot escalate again. Implementation uses its assigned provider/effort and returns needs_discovery for a scope conflict. +Return escalate only from medium Discovery or Review when a concrete newly discovered risk or uncertainty prevents completing this assignment. Keep the same revision and impact flags, supply risk with the specific reason and supporting evidence, and leave the checkout clean. The workflow resumes the same original role/session at high. High cannot escalate again. Implementation uses its assigned provider/effort and returns needs_discovery for a scope conflict. Only the Implementor during implementation may include apiConflict, with a saved reproducer in artifacts and failed evidence. The workflow supplies omitted phase and work, the assigned revision for read-only phases, and the accepted continuation for complete/pass. Explicit conflicting metadata is refused. Implementation must report the actual resulting revision; evidence must name the inputs actually tested. @@ -34,7 +34,7 @@ Return exactly one JSON object conforming to this contract, without Markdown fen - !["phase","work",...(props.assignment.phase === "implementation" ? [] : ["revision"])].includes(name)),"discoveryRequests","discoveryGaps"],additionalProperties:false,properties:{...schemas.result.properties,discoveryRequests:{...schemas.result.properties.discoveryRequests,items:authorRequest},discoveryGaps:{...schemas.result.properties.discoveryGaps,items:authorGap},phase:{const:props.assignment.phase},work:{const:props.assignment.work},outcome:phaseOutcome,contract:props.role === "architect" && props.assignment.phase === "discovery" ? schemas.result.properties.contract : undefined},$defs:schemas.result.$defs}} /> + !["phase","work",...(props.assignment.phase === "implementation" ? [] : ["revision"])].includes(name)),"discoveryRequests","discoveryGaps"],additionalProperties:false,properties:{...schemas.result.properties,discoveryRequests:{...schemas.result.properties.discoveryRequests,items:authorRequest},discoveryGaps:{...schemas.result.properties.discoveryGaps,items:authorGap},phase:{const:props.assignment.phase},work:{const:props.assignment.work},outcome:phaseOutcome,apiConflict:props.role === "implementor" && props.assignment.phase === "implementation" ? schemas.result.properties.apiConflict : undefined,contract:props.role === "architect" && props.assignment.phase === "discovery" ? schemas.result.properties.contract : undefined},$defs:schemas.result.$defs}} /> {resultContract} diff --git a/examples/adversarial-implementation/tests/Submission.test.md b/examples/adversarial-implementation/tests/Submission.test.md index 01af1aa0a..111eb87c2 100644 --- a/examples/adversarial-implementation/tests/Submission.test.md +++ b/examples/adversarial-implementation/tests/Submission.test.md @@ -23,6 +23,7 @@ pwd -P + From 15e1251641d2ae2966eaf4c841eb2fd066acc473 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 08:42:32 -0500 Subject: [PATCH 14/15] Continue a progress-only independent review once --- examples/adversarial-implementation/README.md | 15 ++++- .../components/Delivery/Finish.md | 58 +++++++++++++++++++ .../components/Delivery/Reconcile.md | 3 +- .../components/Delivery/Response.md | 29 ++++++---- .../components/Delivery/Role.md | 4 +- .../components/Delivery/Schema.md | 2 +- .../adversarial-implementation/delivery.md | 7 ++- .../tests/Delivery.test.md | 44 ++++++++++++++ .../tests/agents/review-completion.md | 27 +++++++++ specs/acp-client-spec.md | 7 ++- 10 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 examples/adversarial-implementation/components/Delivery/Finish.md create mode 100644 examples/adversarial-implementation/tests/agents/review-completion.md diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index 45e42c201..b17609066 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -305,6 +305,19 @@ Independent review summaries allow 1500 characters; detailed observations belong in `findings`. The request states this bound alongside the exact revision and clause IDs. Ordinary phase and API-foundation summaries retain their own bounds. +A joined reply containing only `{"status":"in_progress"}` gets one completion +turn in the same native conversation, with the same reviewer, resolved model, +effort and exact source revision. The workflow saves the original reply, checks +that the checkout is clean and reserves `completion/` before sending a prompt +with a five-minute deadline. A second progress reply, unavailable conversation, +failed turn or changed source holds the review. Unmarked prose and malformed +verdicts do not authorize a completion turn. Status and reconciliation read the +saved evidence without prompting; a later invocation never starts an unused or +interrupted completion turn. + +The completion turn has its own allowance. If it returns a review whose only +defect is summary length, the original formatting allowance still applies. + An otherwise-valid joined review with only an oversized summary can consume the work item's recorded `maxResponseCorrections` allowance. Each correction stays in the selected reviewer conversation with the same resolved model and effort. @@ -322,7 +335,7 @@ Use `--props-action reconcile` when the selected joined review has a saved execution but no accepted receipt. Only the selection's attempt qualifies: the original reviewer, or Astra after a retained transition from an eligible Fable failure. It checks that attempt's assignment, joined execution, unchanged raw -reply, current schema, clauses and source before creating the missing receipt, +reply, any saved completion, current schema, clauses and source before creating the missing receipt, and saves a raw reply the execution joined but never wrote. It never shortens text, changes a verdict, repeats a model call or overwrites a contradictory receipt. Missing or uncertain execution continues to hold. Status uses this same validation boundary to read receipts. diff --git a/examples/adversarial-implementation/components/Delivery/Finish.md b/examples/adversarial-implementation/components/Delivery/Finish.md new file mode 100644 index 000000000..8e0c43404 --- /dev/null +++ b/examples/adversarial-implementation/components/Delivery/Finish.md @@ -0,0 +1,58 @@ +--- +description: 'Finish an incomplete review once in its original conversation, or read that saved turn. Use .' +props: + type: object + properties: + delivery: {type: object} + revision: {type: string, pattern: '^[0-9a-f]{40}$'} + reviewer: {type: object} + catalog: {type: object} + attempt: {type: string, pattern: '^/'} + dispatch: {type: boolean} + required: [delivery, revision, reviewer, catalog, attempt, dispatch] + additionalProperties: false +returns: {type: string} +--- + +The initial turn has returned an explicit progress reply. Reserve the one +completion turn before prompting. Its reply and source evidence stay separate +from the first turn and any later summary corrections. + + + + + + + + + + +```bash exec as="reserved" +mkdir -- {quoted} +``` + + + +Continue your unfinished independent review of {props.revision} from this conversation. Finish only the checks still needed. Keep source unchanged, run checks in the foreground and join them before returning. Return the final verdict for the original acceptance clauses as one JSON object. If you cannot finish, report the concrete blocker in the verdict. This is the only completion turn. + + + + +{turn.reply} + + + + +{savedRequest} + + +{savedOutcome} + + + + +{beforeText} +{afterText} +{reply} + + diff --git a/examples/adversarial-implementation/components/Delivery/Reconcile.md b/examples/adversarial-implementation/components/Delivery/Reconcile.md index 6085becbf..bf5daaf8a 100644 --- a/examples/adversarial-implementation/components/Delivery/Reconcile.md +++ b/examples/adversarial-implementation/components/Delivery/Reconcile.md @@ -14,7 +14,8 @@ returns: {type: object} Only the retained selection's attempt can supply the receipt: the original reviewer, or its replacement after a retained transition from an eligible failure. A joined reply that was never saved as `raw.txt` is completed from the -attempt's retained outcome. +attempt's retained outcome. If it was a progress reply, only the saved +completion turn can supply the review. diff --git a/examples/adversarial-implementation/components/Delivery/Response.md b/examples/adversarial-implementation/components/Delivery/Response.md index 9b047f127..62fae6f89 100644 --- a/examples/adversarial-implementation/components/Delivery/Response.md +++ b/examples/adversarial-implementation/components/Delivery/Response.md @@ -1,5 +1,5 @@ --- -description: 'Shorten only an oversized independent-review summary, or validate its saved correction chain without prompting. Use .' +description: 'Finish a review after an explicit progress reply, repair an oversized summary, or validate the saved replies without prompting. Use .' props: type: object properties: @@ -14,32 +14,39 @@ props: returns: {type: object} --- -The original review stays unchanged. Each extra call belongs to the same -reviewer, model and native conversation, and consumes the recorded response -allowance before dispatch. Reconciliation reads that chain and sends no prompt. +A progress reply can use one separate completion turn. Formatting corrections +start with the completed review and consume only the recorded formatting +allowance. Reconciliation reads both records and sends no prompt. {assignmentText} 3 || limit !== (props.delivery.maxResponseCorrections ?? 0)}> - + +{outcomeText} + + + + +{props.raw} + + + + + - + - -{outcomeText} - - = limit}> [key,key === "summary" ? value : {const:original[key]}]))}} /> - + 64000}> diff --git a/examples/adversarial-implementation/components/Delivery/Role.md b/examples/adversarial-implementation/components/Delivery/Role.md index 2449ca3ee..52cb79ed8 100644 --- a/examples/adversarial-implementation/components/Delivery/Role.md +++ b/examples/adversarial-implementation/components/Delivery/Role.md @@ -31,7 +31,9 @@ Original captured source text: Keep summary within {schemas.review.properties.summary.maxLength} characters, including spaces and newlines. Put detailed observations in findings; do not compress the whole report into summary. Check revision against the assigned full SHA and acceptance against the recorded clause IDs before returning. -Return exactly one JSON object satisfying this schema. acceptance lists every clause ID actually examined. pass requires no blocking finding. The workflow supplies work, reviewer, attempt and completion metadata. +If you must end the turn before the verdict is ready, return exactly {JSON.stringify(schemas.progress.const)} on its own. The workflow can then give this same conversation one chance to finish. + +Otherwise, return exactly one JSON object satisfying this schema. acceptance lists every clause ID actually examined. pass requires no blocking finding. The workflow supplies work, reviewer, attempt and completion metadata. 64000}> diff --git a/examples/adversarial-implementation/components/Delivery/Schema.md b/examples/adversarial-implementation/components/Delivery/Schema.md index bcfe715ac..009d4438c 100644 --- a/examples/adversarial-implementation/components/Delivery/Schema.md +++ b/examples/adversarial-implementation/components/Delivery/Schema.md @@ -5,4 +5,4 @@ returns: {type: object} - + diff --git a/examples/adversarial-implementation/delivery.md b/examples/adversarial-implementation/delivery.md index 1637a3133..ea366dadc 100644 --- a/examples/adversarial-implementation/delivery.md +++ b/examples/adversarial-implementation/delivery.md @@ -10,9 +10,10 @@ props: --- Read the original delivery record and perform only its computed next action. -A status read starts no agent. Review preserves every attempt and never repeats -one: an unavailable Fable reviewer is replaced once by Codex Astra, and a review -nobody could complete is held and asked about. Readiness permits no merge. +A status read starts no agent. An explicit progress reply gets one continuation +in the same conversation. An unavailable Fable reviewer is replaced once by +Codex Astra. The workflow preserves each attempt and asks about a review it +cannot complete. Readiness permits no merge. {recordText} diff --git a/examples/adversarial-implementation/tests/Delivery.test.md b/examples/adversarial-implementation/tests/Delivery.test.md index ae994a1cc..da8ed458e 100644 --- a/examples/adversarial-implementation/tests/Delivery.test.md +++ b/examples/adversarial-implementation/tests/Delivery.test.md @@ -230,3 +230,47 @@ git rev-parse HEAD + + + + + + +```bash exec as="head" +git rev-parse HEAD +``` + + + + + + + + + + + + +{raw} + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/adversarial-implementation/tests/agents/review-completion.md b/examples/adversarial-implementation/tests/agents/review-completion.md new file mode 100644 index 000000000..27f21af9c --- /dev/null +++ b/examples/adversarial-implementation/tests/agents/review-completion.md @@ -0,0 +1,27 @@ + +{initial.text} + + +The checks are still running. + + + + + + + + + + + + + + + + + + +{text} + + + diff --git a/specs/acp-client-spec.md b/specs/acp-client-spec.md index aa6d7ff44..4d367b9f5 100644 --- a/specs/acp-client-spec.md +++ b/specs/acp-client-spec.md @@ -734,9 +734,10 @@ until one is installed as a root provider or by AgentProvider. The CLI registers `claude-workflow-role` alongside ordinary `acpx`. The adversarial workflow selects this provider only around Claude role calls, -including foundation, document preparation, independent review and response-only -correction. It uses the same carried adapter, session store and host identity -coordination as ordinary calls. Interactive Sidekick configuration is unchanged. +including foundation, document preparation, independent review and its completion +and response-only correction turns. It uses the same carried adapter, session +store and host identity coordination as ordinary calls. Interactive Sidekick +configuration is unchanged. The profile sets `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` in the child session environment and supplies `disallowedTools: ["ScheduleWakeup"]` through ACPX to From 5e474e607eb0e6319e4f42ac39fb8a78ac33d706 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 10 Nov 2026 09:24:05 -0500 Subject: [PATCH 15/15] Retain holds after single-reviewer continuation failures --- .../components/Delivery/Review.md | 14 ++++++++++- .../components/Delivery/records.ts | 2 +- .../tests/Delivery.test.md | 24 +++++++++++++++---- .../tests/components/Delivery/Role.md | 7 ++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/examples/adversarial-implementation/components/Delivery/Review.md b/examples/adversarial-implementation/components/Delivery/Review.md index 330830114..daae17bd9 100644 --- a/examples/adversarial-implementation/components/Delivery/Review.md +++ b/examples/adversarial-implementation/components/Delivery/Review.md @@ -44,7 +44,19 @@ mkdir -- {quotedReplacement} - + + + + + + + +{raw} + + + + + diff --git a/examples/adversarial-implementation/components/Delivery/records.ts b/examples/adversarial-implementation/components/Delivery/records.ts index 2607137da..fe7a3eccc 100644 --- a/examples/adversarial-implementation/components/Delivery/records.ts +++ b/examples/adversarial-implementation/components/Delivery/records.ts @@ -99,7 +99,7 @@ export const holdSchema = z.strictObject({ schema: z.literal("delivery-review-hold.v1"), work: z.string().min(1), revision: z.string().regex(/^[0-9a-f]{40}$/), - chain: z.enum(["fable-astra", "astra-final"]), + chain: selectionSchema.shape.chain, summary: z.string().min(1), unknown: z.string().min(1), attempts: z.array(attemptSchema).min(1).max(2), diff --git a/examples/adversarial-implementation/tests/Delivery.test.md b/examples/adversarial-implementation/tests/Delivery.test.md index da8ed458e..fc8a5ca04 100644 --- a/examples/adversarial-implementation/tests/Delivery.test.md +++ b/examples/adversarial-implementation/tests/Delivery.test.md @@ -241,14 +241,29 @@ git rev-parse HEAD ``` - + - - + + + + + + + + + + + + + + question.held)} expected={[true]} /> + + + {raw} @@ -259,7 +274,6 @@ git rev-parse HEAD - @@ -269,6 +283,8 @@ git rev-parse HEAD + + diff --git a/examples/adversarial-implementation/tests/components/Delivery/Role.md b/examples/adversarial-implementation/tests/components/Delivery/Role.md index 841597a75..e2d17bf88 100644 --- a/examples/adversarial-implementation/tests/components/Delivery/Role.md +++ b/examples/adversarial-implementation/tests/components/Delivery/Role.md @@ -9,6 +9,11 @@ props: {type: object, properties: {delivery: {type: object}, revision: {type: st clause.id),findings:["repair","false-pass"].includes(work) ? [{blocking:true,description:"A blocking regression remains."}] : []}} /> + + +{raw} + + @@ -20,3 +25,5 @@ props: {type: object, properties: {delivery: {type: object}, revision: {type: st + +