diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index 19dfac715..b17609066 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,19 +305,45 @@ 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. +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 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. 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,13 +388,46 @@ 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. +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` 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 +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. 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 +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/Adversarial/PlannerRole.md b/examples/adversarial-implementation/components/Adversarial/PlannerRole.md index 7138e5499..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/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/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/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..bf5daaf8a 100644 --- a/examples/adversarial-implementation/components/Delivery/Reconcile.md +++ b/examples/adversarial-implementation/components/Delivery/Reconcile.md @@ -14,13 +14,14 @@ 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. - + {assignmentText} @@ -36,9 +37,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..62fae6f89 --- /dev/null +++ b/examples/adversarial-implementation/components/Delivery/Response.md @@ -0,0 +1,93 @@ +--- +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: + 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} +--- + +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} + + + + + + + + + + + + += 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/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/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..52cb79ed8 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. @@ -29,17 +31,24 @@ 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}> - + + {instructions} + +{turn.reply} + + 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/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..fe7a3eccc 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), }); @@ -98,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/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..e56ead10a 100644 --- a/examples/adversarial-implementation/components/Phase/Instructions.md +++ b/examples/adversarial-implementation/components/Phase/Instructions.md @@ -16,26 +16,35 @@ props: default: [] assignment: type: object + submission: + type: object initialize: type: boolean default: false required: - role - assignment + - submission additionalProperties: false --- +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. 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. @@ -44,7 +53,15 @@ 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 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. 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. @@ -76,8 +93,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. @@ -95,26 +110,12 @@ 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: +The frozen UTF-8 file at {props.submission.path} contains the submission schema, +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/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/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/Role.md b/examples/adversarial-implementation/components/Phase/Role.md index 5cda85d04..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/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..441327d8a --- /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 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. + +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. + +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. + +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 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. + + + + + !["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/components/Ticket/PreparationPrompt.md b/examples/adversarial-implementation/components/Ticket/PreparationPrompt.md index b5044f4b2..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/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 42e209302..fc8a5ca04 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" - + + + @@ -178,3 +180,113 @@ pwd -P + + + + + + + +```bash exec as="head" +git rev-parse HEAD +``` + + + + + + + + + + + + + +{raw} + + + + + + + + +{execution.output} + + + + + + + + + + + + + + + + + + + + + + + +```bash exec as="head" +git rev-parse HEAD +``` + + + + + + + + + + + + + + + + + + + + + + question.held)} expected={[true]} /> + + + + + +{raw} + + + + + + + + + + + + + + + + + + + + + + + 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..dbaf20e74 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,12 @@ false - - - - + + + + + + {schemaText} diff --git a/examples/adversarial-implementation/tests/Submission.test.md b/examples/adversarial-implementation/tests/Submission.test.md new file mode 100644 index 000000000..111eb87c2 --- /dev/null +++ b/examples/adversarial-implementation/tests/Submission.test.md @@ -0,0 +1,98 @@ +```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/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/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/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/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/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 + + 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 new file mode 100644 index 000000000..0c16ca9e3 --- /dev/null +++ b/examples/adversarial-implementation/tests/production-components/NativeGuide.ts @@ -0,0 +1,55 @@ +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("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."); + } + 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/examples/adversarial-implementation/tests/production-components/Phase/ProviderName.md b/examples/adversarial-implementation/tests/production-components/Phase/ProviderName.md new file mode 100644 index 000000000..ed72ec7ae --- /dev/null +++ b/examples/adversarial-implementation/tests/production-components/Phase/ProviderName.md @@ -0,0 +1,11 @@ +--- +props: + type: object + properties: + 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 4f279cfa2..6360be674 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 reviewSource = ''; +const workflowControls = { + env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" }, + disallowedTools: ["ScheduleWakeup", "Monitor"], +}; + +const scopedRoleSource = + '' + + ''; +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 })) }, @@ -543,7 +664,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"))), ); @@ -555,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(), ); @@ -763,11 +888,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 +906,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(), 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 c4a97f469..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"]; +const testedSuites = ["ProductionRole", "Preparation", "Submission", "Delivery"]; export function* runAdversarialJourney(suite: string) { const retainedEvidence = suite.startsWith("ArtifactReturn") @@ -45,8 +45,8 @@ 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", "--include", "examples/adversarial-implementation/components", 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", 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( diff --git a/specs/acp-client-spec.md b/specs/acp-client-spec.md index 77b1504f1..2533f2776 100644 --- a/specs/acp-client-spec.md +++ b/specs/acp-client-spec.md @@ -772,8 +772,11 @@ 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 -profile uses the same carried adapter, session store and host identity -coordination as ordinary calls. Interactive Sidekick configuration is unchanged. +adversarial workflow selects this provider only around Claude role calls, +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", "Monitor"]`