ai-foreman 1.7.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +86 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -7
package/dist/foreman.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GraphPurpose } from "rafi-spec";
1
+ import type { GraphPurpose, OutputContract } from "rafi-spec";
2
2
  import type { BuilderAdapter, PermissionHandler, PermissionRequest, TurnResult } from "./adapters/types.js";
3
3
  import type { PermissionPolicy } from "./permissions/policy.js";
4
4
  import { type AnsweredProviderQuestion } from "./providerQuestions.js";
@@ -20,11 +20,18 @@ export interface StepStatus {
20
20
  branchDependency?: string;
21
21
  error?: string;
22
22
  }
23
+ /**
24
+ * Workflow-specific completion is opt-in. Callers may preserve a valid
25
+ * structured response before the generic strict STEP_STATUS repair path runs.
26
+ */
27
+ export type InstructionCompletion = (text: string) => StepStatus | undefined;
23
28
  /**
24
29
  * Pull the STEP_STATUS marker out of a turn's final text.
25
30
  * Format: `STEP_STATUS: <kind> | key="value" key="value"`.
26
31
  */
27
- export declare function parseStepStatus(text: string): StepStatus;
32
+ export declare function parseStepStatus(text: string, options?: {
33
+ requireFinalLine?: boolean;
34
+ }): StepStatus;
28
35
  /** Heuristic: did a marker-less turn end by asking the human something? */
29
36
  export declare function looksLikeQuestion(text: string): boolean;
30
37
  export { MARKER_SPEC } from "./markers.js";
@@ -60,7 +67,7 @@ export interface ForemanNotificationOptions {
60
67
  desktop: boolean;
61
68
  terminalBell: boolean;
62
69
  }
63
- /** Drives one builder through a batch of N steps via the STEP_STATUS protocol. */
70
+ /** Drives one builder through contracted native operations. */
64
71
  export declare class BuildInterruptedError extends Error {
65
72
  constructor();
66
73
  }
@@ -126,12 +133,20 @@ export declare class Foreman {
126
133
  /** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
127
134
  sendPreflightFeedback(feedback: string): Promise<void>;
128
135
  /** Send one custom instruction through the same needs_input loop as a batch turn. */
129
- runInstruction(instruction: string): Promise<{
136
+ runInstruction(instruction: string, options: {
137
+ completion?: InstructionCompletion;
138
+ output: OutputContract;
139
+ }): Promise<{
140
+ result: TurnResult;
141
+ status: StepStatus;
142
+ }>;
143
+ /** The sole convenience path that selects the Builder's work-report contract. */
144
+ runBuilderWork(instruction: string): Promise<{
130
145
  result: TurnResult;
131
146
  status: StepStatus;
132
147
  }>;
133
148
  /** Ask an already-active role session to propose choices for its reported blocker. */
134
- resolveBlocker(adapter: BuilderAdapter, reason: string, mode?: "builder" | "qa"): Promise<{
149
+ resolveBlocker(adapter: BuilderAdapter, reason: string, mode: "builder" | "qa", output: OutputContract): Promise<{
135
150
  result: TurnResult;
136
151
  status: StepStatus;
137
152
  }>;
package/dist/foreman.js CHANGED
@@ -7,12 +7,13 @@ import { renderBuildWorkContext } from "./buildWorkContext.js";
7
7
  import { loadTicketSetupConfigWithDefaults } from "./tickets/setupConfig.js";
8
8
  import { deliverBuilderGuidanceFollowup } from "./builderGuidanceFollowup.js";
9
9
  import { graphContext, sendGraphTurn } from "./graph/turn.js";
10
+ import { workReportOutput, preflightOutput, qaVerdictOutput } from "rafi-spec";
11
+ import { operationInstruction, operationStatus, operationPayload, nativePolicy, structuredTurnErrorMessage } from "./structuredOperation.js";
10
12
  import { qaDigest } from "./qaProtocolV2.js";
11
13
  import { BuildControlBoundary } from "./buildInterventions.js";
12
14
  import { buildScopeRevision } from "./buildApproval.js";
13
15
  import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions, decisionResponse } from "./humanDecision.js";
14
16
  import { select, text, isCancel } from "@clack/prompts";
15
- import { MARKER_SPEC, QA_MARKER_SPEC } from "./markers.js";
16
17
  import { countProviderQuestions, handleProviderQuestionTool } from "./providerQuestions.js";
17
18
  import { bestEffortDiagnostic } from "./questionTrace.js";
18
19
  import { signalAttention } from "./notify.js";
@@ -30,22 +31,6 @@ import { join, relative } from "node:path";
30
31
  import { SessionUnavailableError, recoverableBuilderTurnError, sessionUnavailableErrorFromFailure } from "./adapters/sessionFailure.js";
31
32
  import { WorkflowDb } from "./workflowDb.js";
32
33
  import { assertBuildAssignmentReconciled, beginBuildAssignment, BuildAssignmentRejected, captureBuildSource, finishBuildAssignment, validateBuildAssignment } from "./buildAssignment.js";
33
- /** Compare Builder product state without Rafi's own tracker bookkeeping. */
34
- function builderProductDigest(source, hostOwnedPaths) {
35
- const ignored = new Set(hostOwnedPaths.map(path => path.replace(/\\/g, "/").replace(/^\.\//, "")));
36
- const hostOwned = (path) => {
37
- const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
38
- return ignored.has(normalized)
39
- || [".foreman/", ".rafi/", ".tickets/", ".codex/", ".agents/", "docs-rafi/"]
40
- .some(prefix => normalized.startsWith(prefix))
41
- || [".gitignore", "AGENTS.md", "rafi-config.yaml", "foreman.yaml"].includes(normalized);
42
- };
43
- const inventory = source.pathInventory
44
- .filter(item => !hostOwned(item.path))
45
- .map(item => ({ ...item, path: item.path.replace(/\\/g, "/") }))
46
- .sort((a, b) => a.path.localeCompare(b.path));
47
- return createHash("sha256").update(JSON.stringify(inventory)).digest("hex");
48
- }
49
34
  /** Phrases that suggest the builder ended its turn by asking the human. */
50
35
  const QUESTION_HINTS = [
51
36
  "should i",
@@ -60,7 +45,7 @@ const QUESTION_HINTS = [
60
45
  * Pull the STEP_STATUS marker out of a turn's final text.
61
46
  * Format: `STEP_STATUS: <kind> | key="value" key="value"`.
62
47
  */
63
- export function parseStepStatus(text) {
48
+ export function parseStepStatus(text, options = {}) {
64
49
  const markerLines = text.split(/\r?\n/).filter((line) => /^\s*STEP_STATUS:/.test(line));
65
50
  const markerCount = markerLines.length;
66
51
  if (markerCount > 1) {
@@ -69,12 +54,14 @@ export function parseStepStatus(text) {
69
54
  const lines = text.trimEnd().split(/\r?\n/).filter((line) => line.trim().length > 0);
70
55
  const last = lines[lines.length - 1] ?? "";
71
56
  if (!last.includes("STEP_STATUS:")) {
72
- if (markerCount === 1) {
57
+ if (markerCount === 1 && options.requireFinalLine !== false) {
73
58
  return { kind: "unknown", error: "STEP_STATUS marker was not the final non-empty line" };
74
59
  }
75
- return { kind: "unknown" };
60
+ if (markerCount === 0)
61
+ return { kind: "unknown" };
76
62
  }
77
- const match = last.match(/^STEP_STATUS:\s*(done|blocked|plan_complete|needs_input|qa_pass|qa_fail)\b\s*(?:\|\s*(.*))?$/i);
63
+ const marker = options.requireFinalLine === false ? markerLines[0]?.trim() ?? "" : last;
64
+ const match = marker.match(/^STEP_STATUS:\s*(done|blocked|plan_complete|needs_input|qa_pass|qa_fail)\b\s*(?:\|\s*(.*))?$/i);
78
65
  if (!match) {
79
66
  return { kind: "unknown", error: "malformed STEP_STATUS marker" };
80
67
  }
@@ -153,7 +140,7 @@ export function buildPrimer(n, trackerPath, ticketsEnabled = false, preferredTic
153
140
  `\n- Foreman records completion and regenerates the tracker after independent QA passes. Do not run ticket-complete commands or mark tracker records done yourself.` +
154
141
  `\n- When available and permitted, \`foreman tickets discover --summary "..." --rationale "..."\` can log newly discovered work and \`foreman tickets update <id> --next-action "..."\` can record optional mid-turn notes.` +
155
142
  `\n- If a ticket CLI is unavailable, include those notes in your response; it is not an implementation blocker.` +
156
- `\n- Always include ticket="<id>" in your STEP_STATUS marker so foreman can update the tracker.`;
143
+ `\n- The host binds your result to the assigned ticket; report completion only for that exact work.`;
157
144
  }
158
145
  else if (trackerPath) {
159
146
  trackerRule = `\n- After completing each ticket or step, update its status in the ticket progress tracker at \`${trackerPath}\`, following the Standard Update Workflow documented in that file.`;
@@ -168,14 +155,14 @@ export function buildPrimer(n, trackerPath, ticketsEnabled = false, preferredTic
168
155
 
169
156
  Rules:
170
157
  - Do exactly ONE ticket or step this turn, then stop.
171
- - ${MARKER_SPEC}
172
- - If a tool action is denied by foreman policy, do not retry the denied form; report it via the blocked marker. The one exception is a required test that you changed by adding a pipe, redirection, substitution, or wrapper: retry the exact required-test command without that extra shell syntax before reporting it blocked.${trackerRule}${preferredTicketRule}
158
+ - Return the result using the selected native JSON output contract.
159
+ - If a tool action is denied by foreman policy, do not retry the denied form; report it using the blocked result. The one exception is a required test that you changed by adding a pipe, redirection, substitution, or wrapper: retry the exact required-test command without that extra shell syntax before reporting it blocked.${trackerRule}${preferredTicketRule}
173
160
 
174
161
  This is step 1 of ${n}. Implement the next ticket or step now.`;
175
162
  }
176
163
  /** Instruction sent on turns 2–N. */
177
164
  export function buildNextStepInstruction(i, n) {
178
- return `Implement the next ticket or step now (exactly one) — this is step ${i} of ${n}. Then end with the STEP_STATUS marker line.`;
165
+ return `Implement the next ticket or step now (exactly one) — this is step ${i} of ${n}. Return the completed work-report through the selected native JSON contract.`;
179
166
  }
180
167
  /** QA review turn: ask the builder to triple-check the ticket or step it just completed. */
181
168
  export function buildQaInstruction() {
@@ -192,10 +179,7 @@ The independent QA workspace is a disposable snapshot and may have a detached HE
192
179
 
193
180
  If an otherwise required test is denied only because you added a pipe, redirection, substitution, or wrapper, retry the exact required-test command without that extra shell syntax. Do not retry other denied actions.
194
181
 
195
- If everything is solid, end with STEP_STATUS: qa_pass.
196
- If anything is off, return the required RAFI_QA_FAILURE_REPORT_START/RAFI_QA_FAILURE_REPORT_END JSON envelope immediately before STEP_STATUS: qa_fail. The report must contain every check, every blocking finding, and all nonblocking observations. Do NOT fix issues on this turn — just report them. Foreman will instruct the Builder to fix them next.
197
-
198
- ${QA_MARKER_SPEC}`;
182
+ Return a native qa-verdict result. Use a passed payload only when verification succeeds. A failed payload must contain the full report: every check, blocking finding, and nonblocking observation. Do not fix issues during this review; the Builder receives failures separately.`;
199
183
  }
200
184
  /** Follow-up turn after qa_fail: have the builder implement the listed fixes. */
201
185
  export function buildQaFixInstruction(issues) {
@@ -203,7 +187,7 @@ export function buildQaFixInstruction(issues) {
203
187
 
204
188
  ${issues}
205
189
 
206
- Fix every one of them now. Then end with STEP_STATUS: done so foreman can re-run QA on the fixes. Triple-check that your fixes actually resolve the issues before emitting done.`;
190
+ Fix every one of them now. Submit the selected native remediation result so Foreman can rerun independent QA. Verify that your fixes resolve the reported issues.`;
207
191
  }
208
192
  /** Pre-flight planning turn: ask the builder to list its next N steps without implementing anything. */
209
193
  export function buildPlanningTurn(n, ticketsContent, preferredTicketId) {
@@ -213,7 +197,7 @@ export function buildPlanningTurn(n, ticketsContent, preferredTicketId) {
213
197
  const preferred = preferredTicketId
214
198
  ? ` The first item must be ticket ${preferredTicketId}; this is an interrupted ticket being resumed.`
215
199
  : "";
216
- return `${header}Before we begin, list the next ${n} ticket(s) or step(s) you plan to implement, in order.${preferred} Be specific — reference ticket IDs or titles where applicable. Do not implement anything yet; output the numbered list only. Do not emit a STEP_STATUS marker on this turn.`;
200
+ return `${header}Before we begin, list the next ${n} ticket(s) or step(s) you plan to implement, in order.${preferred} Be specific — reference ticket IDs or titles where applicable. Do not implement anything yet; return the ordered steps through the build-preflight response.`;
217
201
  }
218
202
  /** Builds the permission callback: classify, log, allow or escalate. */
219
203
  export function createPermissionHandler(policy, log, opts = {}) {
@@ -274,11 +258,11 @@ export function createPermissionHandler(policy, log, opts = {}) {
274
258
  behavior: "deny",
275
259
  message: `Foreman policy: this action needs human approval (${verdict.reason}) ` +
276
260
  `and was NOT performed. Do not retry this exact action. An allow-listed alternative is permitted; for project tests use a literal in-worktree cd, project-local PYTHONPATH, existing virtualenv Python, or uv run --offline --no-sync with an allowed test command. If no permitted alternative can complete the step, ` +
277
- `end your turn with: STEP_STATUS: blocked | reason="needs human approval: ${verdict.reason}".`,
261
+ `return a native blocked result explaining the required approval: ${verdict.reason}.`,
278
262
  };
279
263
  };
280
264
  }
281
- /** Drives one builder through a batch of N steps via the STEP_STATUS protocol. */
265
+ /** Drives one builder through contracted native operations. */
282
266
  export class BuildInterruptedError extends Error {
283
267
  constructor() { super("Build interrupted; current work is preserved for recovery"); this.name = "BuildInterruptedError"; }
284
268
  }
@@ -390,7 +374,7 @@ export class Foreman {
390
374
  * The returned result and status always reflect the final (non-needs_input) turn.
391
375
  */
392
376
  async doTurn(instruction) {
393
- return this.doTurnWith(this.builder, instruction);
377
+ return this.runBuilderWork(instruction);
394
378
  }
395
379
  async prepareBuilderBoundary(frozenAction) {
396
380
  if (!this.builderFactory || !this.projectDir)
@@ -438,19 +422,24 @@ export class Foreman {
438
422
  this.builder = await this.builderFactory(this.projectDir);
439
423
  this.log.write("branch-session", { role: "builder", transition: "missing-session-fresh", continuityLost: true });
440
424
  }
441
- async doTurnWith(adapter, instruction, mode = "builder") {
425
+ async doTurnWith(adapter, instruction, mode = "builder", output, completion) {
442
426
  return this.projectDir
443
- ? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode))
444
- : this.doTurnWithOwned(adapter, instruction, mode);
427
+ ? withGraphSessionAccess(this.projectDir, adapter, () => this.doTurnWithOwned(adapter, instruction, mode, output, completion))
428
+ : this.doTurnWithOwned(adapter, instruction, mode, output, completion);
445
429
  }
446
- async doTurnWithOwned(adapter, instruction, mode = "builder") {
430
+ async doTurnWithOwned(adapter, instruction, mode = "builder", output, completion) {
431
+ if (completion)
432
+ throw new Error("Text completion predicates cannot authorize native operation results");
447
433
  const ticket = mode === "builder" ? this.currentTicketId : undefined;
434
+ const statusOf = (turn) => operationStatus(turn, output, ticket);
448
435
  const hostToolObservations = [];
449
436
  const scopedInstruction = (text) => !ticket || text.includes(`Ticket scope: ${ticket}.`) ? text
450
- : `${text}\nTicket scope: ${ticket}. Do not substitute another ticket. Include ticket="${ticket}" in any STEP_STATUS marker.`;
437
+ : `${text}\nTicket scope: ${ticket}. Do not substitute another ticket.`;
451
438
  instruction = scopedInstruction(instruction);
439
+ instruction += `\n\n${operationInstruction(output)}`;
452
440
  const send = async (text, policy) => {
453
441
  this.assertNotInterrupted();
442
+ policy = nativePolicy(output, { ...policy, dispatchId: undefined, journalRunId: this.qaRunId });
454
443
  const context = ticket && this.projectDir ? renderBuildWorkContext(this.ticketForQa(0, ticket), loadTicketSetupConfigWithDefaults(this.projectDir).build.validation_checklist) : "";
455
444
  const contractContext = ticket && this.projectDir ? await ensureBuilderContract(this.projectDir, this.qaRunId, ticket, this.builderWorktree ?? this.projectDir, adapter, this.qaFactory ? (...args) => this.createOwnedQa(...args) : undefined) : "";
456
445
  this.assertNotInterrupted();
@@ -470,8 +459,8 @@ export class Foreman {
470
459
  finally {
471
460
  stopHostObservation();
472
461
  }
473
- const parsed = parseStepStatus(response.text);
474
- const applicable = parsed.kind === "unknown" && looksLikeQuestion(response.text) ? { kind: "needs_input", question: lastLine(response.text) } : parsed;
462
+ const parsed = statusOf(response);
463
+ const applicable = parsed;
475
464
  if (!this.interruptionRequested && assignment && !response.isError && !response.failure && ["done", "plan_complete"].includes(applicable.kind))
476
465
  await collectBuilderContractCoverage(this.projectDir, assignment.runId, assignment.ticketId, assignment.worktree, adapter, assignment.operationId, submitted, response, hostToolObservations, initialSourceDigest);
477
466
  const rejection = assignment ? finishBuildAssignment(this.projectDir, assignment, response, applicable) : undefined;
@@ -494,55 +483,16 @@ export class Foreman {
494
483
  this.builder = await this.beforeBuilderTurn(this.builder, instruction);
495
484
  adapter = this.builder;
496
485
  }
497
- const turnPolicy = { logicalActionId: this.currentTicketId ?? createHash("sha256").update(instruction).digest("hex") };
486
+ const turnPolicy = { logicalActionId: randomUUID() };
498
487
  let result = await send(instruction, turnPolicy);
499
488
  await this.observeBuilderNative(adapter);
500
- let status = parseStepStatus(result.text);
489
+ let status = statusOf(result);
501
490
  if (ticket && validateBuildAssignment(ticket, status))
502
491
  return { result, status };
503
492
  if (result.isError || result.failure)
504
493
  return { result: { ...result, isError: true }, status };
505
- if (status.kind === "unknown") {
506
- if (!status.error && looksLikeQuestion(result.text)) {
507
- status = { kind: "needs_input", question: lastLine(result.text), choices: ["Continue", "Cancel"] };
508
- }
509
- else {
510
- if (adapter === this.builder && this.beforeBuilderTurn) {
511
- this.builder = await this.beforeBuilderTurn(this.builder, instruction);
512
- adapter = this.builder;
513
- }
514
- if (this.projectDir) {
515
- const budget = new WorkflowDb(this.projectDir);
516
- try {
517
- if (!budget.reserveRecoveryAllowance(this.qaRunId, turnPolicy.logicalActionId, "protocol-correction", budget.autonomyPolicy(this.qaRunId)?.limits.protocolCorrections ?? 1))
518
- return { result, status: { kind: "blocked", reason: "shared protocol correction budget exhausted" } };
519
- }
520
- finally {
521
- budget.close();
522
- }
523
- }
524
- const correction = mode === "qa"
525
- ? "Protocol correction only: based on the review already completed, return exactly one final STEP_STATUS: qa_pass or STEP_STATUS: qa_fail marker. Do not repeat QA, tests, tools, or compaction."
526
- : "Protocol correction only: based on the work already completed, return exactly one final STEP_STATUS: done, plan_complete, blocked, or needs_input marker. Do not repeat implementation, tools, or compaction.";
527
- result = await send([
528
- correction,
529
- `The previous marker was rejected: ${JSON.stringify(status.error ?? "missing final marker")}.`,
530
- "STEP_STATUS is a separate plain-text line, not JSON. Do not append braces, commas, a code fence, or commentary after it. Keep the continuity JSON on its own preceding line.",
531
- ...(mode === "builder" ? [
532
- `Only if the assigned work is complete, the exact marker form is: STEP_STATUS: done${ticket ? ` | ticket="${ticket}"` : ""}`,
533
- `If blocked, use: STEP_STATUS: blocked | ${ticket ? `ticket="${ticket}" ` : ""}reason="the actual blocker"`,
534
- "Choose the status supported by the work already performed; do not invent completion.",
535
- ] : ["If successful, use exactly: STEP_STATUS: qa_pass", "If failed, retain the required failure report and use exactly: STEP_STATUS: qa_fail"]),
536
- ].join("\n"), { ...turnPolicy, responseOnly: true });
537
- await this.observeBuilderNative(adapter);
538
- if (result.isError || result.failure)
539
- return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
540
- status = parseStepStatus(result.text);
541
- if (status.kind === "unknown")
542
- status = { ...status, error: `protocol correction exhausted: ${status.error ?? "missing final marker"}` };
543
- }
544
- }
545
- let blockerExplanations = 0;
494
+ if (status.kind === "unknown")
495
+ return { result: { ...result, isError: true }, status };
546
496
  while (true) {
547
497
  if (ticket && validateBuildAssignment(ticket, status))
548
498
  return { result, status };
@@ -625,53 +575,11 @@ export class Foreman {
625
575
  await this.observeBuilderNative(adapter);
626
576
  if (result.isError || result.failure)
627
577
  return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
628
- status = parseStepStatus(result.text);
578
+ status = statusOf(result);
629
579
  if (ticket && validateBuildAssignment(ticket, status))
630
580
  return { result, status };
631
581
  }
632
- if (status.kind !== "blocked" || (status.reason?.startsWith("user chose safe pause") || status.reason?.includes("Rafi is waiting for input")) || !process.stdin.isTTY || !process.stdout.isTTY)
633
- break;
634
- if (blockerExplanations++ >= 1)
635
- break;
636
- if (this.projectDir) {
637
- const budget = new WorkflowDb(this.projectDir);
638
- try {
639
- if (!budget.reserveRecoveryAllowance(this.qaRunId, this.currentTicketId ?? "instruction", "blocker-explanation", 1))
640
- break;
641
- }
642
- finally {
643
- budget.close();
644
- }
645
- }
646
- const reason = status.reason ?? "the agent reported an unspecified blocker";
647
- this.log.write("blocked-recovery", { reason, role: adapter === this.builder ? "builder" : "qa" });
648
- const recoveryInstruction = buildBlockerRecoveryInstruction(reason);
649
- if (adapter === this.builder && this.beforeBuilderTurn) {
650
- this.builder = await this.beforeBuilderTurn(this.builder, scopedInstruction(recoveryInstruction));
651
- adapter = this.builder;
652
- }
653
- result = await send(recoveryInstruction, turnPolicy);
654
- await this.observeBuilderNative(adapter);
655
- if (result.isError || result.failure)
656
- return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
657
- status = parseStepStatus(result.text);
658
- if (status.kind === "unknown") {
659
- if (this.projectDir && this.qaRunId) {
660
- const budget = new WorkflowDb(this.projectDir);
661
- try {
662
- if (!budget.reserveRecoveryAllowance(this.qaRunId, turnPolicy.logicalActionId, "protocol-correction", budget.autonomyPolicy(this.qaRunId)?.limits.protocolCorrections ?? 1))
663
- break;
664
- }
665
- finally {
666
- budget.close();
667
- }
668
- }
669
- result = await send('Protocol correction only: return the blocker approaches now using exactly one final STEP_STATUS: needs_input marker with question="..." and choices="recommended (Recommended)|alternative|alternative". Do not repeat tools or implementation.', { ...turnPolicy, responseOnly: true });
670
- await this.observeBuilderNative(adapter);
671
- if (result.isError || result.failure)
672
- return { result: { ...result, isError: true }, status: { kind: "blocked", reason: result.text } };
673
- status = parseStepStatus(result.text);
674
- }
582
+ break;
675
583
  }
676
584
  return { result, status };
677
585
  }
@@ -682,7 +590,8 @@ export class Foreman {
682
590
  if (this.beforeBuilderTurn)
683
591
  this.builder = await this.beforeBuilderTurn(this.builder, instruction);
684
592
  this.assertNotInterrupted();
685
- const result = this.projectDir ? await sendGraphTurn(this.builder, instruction, graphContext(this.projectDir, this.builderWorktree ?? this.projectDir, "build-preflight", instruction), { purpose: "planning" }) : await this.builder.sendTurn(instruction, { purpose: "planning" });
593
+ const policy = nativePolicy(preflightOutput, { purpose: "planning", journalRunId: this.qaRunId });
594
+ const result = this.projectDir ? await sendGraphTurn(this.builder, `${instruction}\n\n${operationInstruction(preflightOutput)}`, graphContext(this.projectDir, this.builderWorktree ?? this.projectDir, "build-preflight", instruction), policy) : await this.builder.sendTurn(instruction, policy);
686
595
  this.assertNotInterrupted();
687
596
  await this.observeBuilderNative(this.builder);
688
597
  this.log.write("preflight", {
@@ -691,9 +600,9 @@ export class Foreman {
691
600
  isError: result.isError,
692
601
  });
693
602
  if (result.isError)
694
- throw new Error(result.text);
695
- this.preflightPlan = result.text;
696
- return result.text;
603
+ throw new Error(structuredTurnErrorMessage(result, "Build preflight"));
604
+ this.preflightPlan = operationPayload(result, preflightOutput).steps.map((step, index) => `${index + 1}. ${step.ticket ? step.ticket + ": " : ""}${step.work}`).join("\n");
605
+ return this.preflightPlan;
697
606
  }
698
607
  /** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
699
608
  async sendPreflightFeedback(feedback) {
@@ -701,21 +610,26 @@ export class Foreman {
701
610
  if (this.beforeBuilderTurn)
702
611
  this.builder = await this.beforeBuilderTurn(this.builder, feedback);
703
612
  this.assertNotInterrupted();
704
- const result = this.projectDir ? await sendGraphTurn(this.builder, feedback, graphContext(this.projectDir, this.builderWorktree ?? this.projectDir, "build-preflight", feedback), { purpose: "planning" }) : await this.builder.sendTurn(feedback, { purpose: "planning" });
613
+ const policy = nativePolicy(preflightOutput, { purpose: "planning", journalRunId: this.qaRunId });
614
+ const result = this.projectDir ? await sendGraphTurn(this.builder, feedback, graphContext(this.projectDir, this.builderWorktree ?? this.projectDir, "build-preflight", feedback), policy) : await this.builder.sendTurn(feedback, policy);
705
615
  this.assertNotInterrupted();
706
616
  await this.observeBuilderNative(this.builder);
707
617
  this.log.write("preflight", { feedback: true, costUsd: result.costUsd, isError: result.isError });
708
618
  if (result.isError)
709
- throw new Error(result.text);
710
- this.preflightPlan = result.text;
619
+ throw new Error(structuredTurnErrorMessage(result, "Build preflight"));
620
+ this.preflightPlan = operationPayload(result, preflightOutput).steps.map((step, index) => `${index + 1}. ${step.ticket ? step.ticket + ": " : ""}${step.work}`).join("\n");
711
621
  }
712
622
  /** Send one custom instruction through the same needs_input loop as a batch turn. */
713
- async runInstruction(instruction) {
714
- return this.doTurn(instruction);
623
+ async runInstruction(instruction, options) {
624
+ return this.doTurnWith(this.builder, instruction, "builder", options.output, options.completion);
625
+ }
626
+ /** The sole convenience path that selects the Builder's work-report contract. */
627
+ async runBuilderWork(instruction) {
628
+ return this.doTurnWith(this.builder, instruction, "builder", workReportOutput);
715
629
  }
716
630
  /** Ask an already-active role session to propose choices for its reported blocker. */
717
- async resolveBlocker(adapter, reason, mode = "builder") {
718
- return this.doTurnWith(adapter, buildBlockerRecoveryInstruction(reason), mode);
631
+ async resolveBlocker(adapter, reason, mode, output) {
632
+ return this.doTurnWith(adapter, buildBlockerRecoveryInstruction(reason), mode, output);
719
633
  }
720
634
  async observeBuilderNative(adapter) {
721
635
  if (adapter === this.builder)
@@ -795,7 +709,7 @@ export class Foreman {
795
709
  },
796
710
  evidence: ({ cycle, outcome, detail, qaDiff }) => this.log.write("qa", { stepIndex, cycle, outcome, detail, qaDiff, disposable: true }),
797
711
  onNonconvergence: this.qaNonconvergence,
798
- resolveBlocked: (adapter, reason) => this.resolveBlocker(adapter, reason, "qa"),
712
+ resolveBlocked: (adapter, reason) => this.resolveBlocker(adapter, reason, "qa", qaVerdictOutput),
799
713
  });
800
714
  if (resumedRecovery) {
801
715
  const recoveryDb = new WorkflowDb(this.projectDir);
@@ -1106,7 +1020,7 @@ export class Foreman {
1106
1020
  ? buildPrimer(n, trackerPath, this.ticketsEnabled, pendingTicketId)
1107
1021
  : buildNextStepInstruction(i, n);
1108
1022
  if (pendingTicketId)
1109
- instruction += `\n\nAssigned ticket: ${pendingTicketId}. Implement exactly this ticket. Do not substitute another ticket. End with ticket="${pendingTicketId}" in the STEP_STATUS marker.`;
1023
+ instruction += `\n\nAssigned ticket: ${pendingTicketId}. Implement exactly this ticket. Do not substitute another ticket. Return the native work report for this assigned ticket.`;
1110
1024
  const continuations = answered.filter(decision => decision.interruptionId === `ticket:${pendingTicketId}`);
1111
1025
  const continuationKeys = [];
1112
1026
  if (continuations.length) {
@@ -1128,14 +1042,6 @@ export class Foreman {
1128
1042
  }
1129
1043
  if (i > 1)
1130
1044
  await this.prepareBuilderBoundary(instruction);
1131
- // Current-branch builds do not always populate builderWorktree. The
1132
- // project directory is still the authoritative source boundary, so a
1133
- // plan-only resumed turn must be compared there before QA can run.
1134
- const builderSourceRoot = this.builderWorktree ?? this.projectDir;
1135
- const hostOwnedSourcePaths = this.projectDir && this.ticketsEnabled
1136
- ? generatedTrackerDirtyPaths(loadTicketsConfig(this.projectDir).paths)
1137
- : [];
1138
- const sourceBeforeTurn = builderSourceRoot ? captureBuildSource(builderSourceRoot) : undefined;
1139
1045
  this.builderWorkSessions += 1;
1140
1046
  const turn = () => this.doTurn(instruction);
1141
1047
  let response;
@@ -1178,41 +1084,6 @@ export class Foreman {
1178
1084
  detail = `builder turn errored: ${result.text.slice(0, 200)}`;
1179
1085
  break;
1180
1086
  }
1181
- // A resumed provider can answer the mandatory primer with a planning
1182
- // response while still emitting a done marker. Do not send unchanged
1183
- // source to QA or mark the ticket complete. Retry the bounded
1184
- // implementation once; if it still produces no work, stop as a real
1185
- // Builder blocker.
1186
- if (["done", "plan_complete"].includes(status.kind) && pendingTicketId && sourceBeforeTurn && builderSourceRoot) {
1187
- const sourceAfterTurn = captureBuildSource(builderSourceRoot);
1188
- const beforeProductDigest = builderProductDigest(sourceBeforeTurn, hostOwnedSourcePaths);
1189
- const afterProductDigest = builderProductDigest(sourceAfterTurn, hostOwnedSourcePaths);
1190
- // plan_complete is explicitly a planning response. Even if the
1191
- // provider touched bookkeeping while planning, never send that state
1192
- // to QA as if implementation were complete.
1193
- const responseIsPlanningOnly = /\bplan(?:ning)?\s+only\b|\bdo not implement(?: yet)?\b|\bawait(?:ing)? (?:approval|go-ahead)\b/i.test(result.text)
1194
- && !/\bimplemented\b|\badded\b|\bchanged\b/i.test(result.text);
1195
- if (status.kind === "plan_complete" || responseIsPlanningOnly || afterProductDigest === beforeProductDigest) {
1196
- this.log.write("step", { index: i, statusKind: "plan_only_without_source_change", ticket: pendingTicketId });
1197
- const retry = await this.doTurn(`The previous turn did not complete implementation. Implement the assigned ticket now: ${pendingTicketId}. Inspect the current files, make the required changes and tests, then report truthful evidence with STEP_STATUS: done | ticket="${pendingTicketId}" summary="...". Do not return another plan-only response.`);
1198
- result = retry.result;
1199
- status = retry.status;
1200
- if (result.isError) {
1201
- const recoverable = recoverableBuilderTurnError(result);
1202
- if (recoverable)
1203
- throw recoverable;
1204
- outcome = "blocked";
1205
- detail = `builder implementation retry errored: ${result.text.slice(0, 200)}`;
1206
- break;
1207
- }
1208
- const sourceAfterRetry = captureBuildSource(builderSourceRoot);
1209
- if (status.kind === "plan_complete" || (status.kind === "done" && builderProductDigest(sourceAfterRetry, hostOwnedSourcePaths) === beforeProductDigest)) {
1210
- outcome = "needs-human";
1211
- detail = `Builder returned planning-only responses without changing product source for ${pendingTicketId}`;
1212
- break;
1213
- }
1214
- }
1215
- }
1216
1087
  if (status.kind === "done" || status.kind === "plan_complete") {
1217
1088
  let qaSummary;
1218
1089
  let qaWaived = false;
@@ -1356,7 +1227,7 @@ export class Foreman {
1356
1227
  ? status.error
1357
1228
  : looksLikeQuestion(result.text)
1358
1229
  ? `builder ended with a question: ${lastLine(result.text)}`
1359
- : "builder did not emit a STEP_STATUS marker";
1230
+ : "builder did not submit a valid native operation result";
1360
1231
  break;
1361
1232
  }
1362
1233
  if (completed >= n && this.projectDir) {
@@ -1390,8 +1261,7 @@ function buildBlockerRecoveryInstruction(reason) {
1390
1261
  `You reported this blocker: ${reason}`,
1391
1262
  "Do not end the run. Analyze the durable state and propose two or three safe, materially different approaches the user can choose from.",
1392
1263
  "Put your recommended approach first and end its label with (Recommended). Include the important consequence or tradeoff in every choice label.",
1393
- "Do not perform more implementation or QA work until the user chooses. End exactly with:",
1394
- 'STEP_STATUS: needs_input | question="How should Rafi unblock this work?" choices="recommended approach and consequence (Recommended)|alternative and consequence|another alternative and consequence"',
1264
+ "Do not perform more implementation or QA work until the user chooses. Return a native needs-input result with the question and concrete choices.",
1395
1265
  "After the user answers, apply that guidance in this same session. If still blocked, report blocked again so Rafi can offer a new set of approaches.",
1396
1266
  ].join("\n\n");
1397
1267
  }
@@ -11,5 +11,5 @@ export interface GraphPendingTask {
11
11
  }
12
12
  /** A completed editing assignment plus host-observed source change is required.
13
13
  * Changed bytes alone are deliberately insufficient to establish this trigger. */
14
- export declare function queueGraphMaintenance(root: string, workspace: string, logicalTaskId: string, response: string, sourceChanged: boolean): void;
14
+ export declare function queueGraphMaintenance(root: string, workspace: string, logicalTaskId: string, response: string | boolean, sourceChanged: boolean): void;
15
15
  export declare function maintainGraphTask(root: string, workspace: string, logicalTaskId: string, host: BuilderAdapter): Promise<unknown>;
@@ -9,7 +9,7 @@ export const GRAPH_CHANGE_SIGNAL = "RAFI_GRAPH_MAINTENANCE: meaningful-indexed-c
9
9
  /** A completed editing assignment plus host-observed source change is required.
10
10
  * Changed bytes alone are deliberately insufficient to establish this trigger. */
11
11
  export function queueGraphMaintenance(root, workspace, logicalTaskId, response, sourceChanged) {
12
- if (!sourceChanged || !response.split(/\r?\n/).some(line => line.trim() === GRAPH_CHANGE_SIGNAL))
12
+ if (!sourceChanged || !(typeof response === "boolean" ? response : response.split(/\r?\n/).some(line => line.trim() === GRAPH_CHANGE_SIGNAL)))
13
13
  return;
14
14
  const effective = loadGraphConfig(root);
15
15
  if (!effective.enabled || effective.config?.maintenance !== "selective")
@@ -1,9 +1,11 @@
1
+ import { semanticExtractionOutput } from "rafi-spec";
2
+ import { nativePolicy, operationInstruction, operationPayload, journalStructuredDispatch } from "../structuredOperation.js";
1
3
  import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { ClaudeAdapter } from "../adapters/claude.js";
4
6
  import { CodexAdapter } from "../adapters/codex.js";
5
7
  import { WorkflowDb } from "../workflowDb.js";
6
- import { parseSemanticResult, semanticExchange, SEMANTIC_INSTRUCTIONS } from "./semantic.js";
8
+ import { validateSemanticResult, semanticExchange, SEMANTIC_INSTRUCTIONS } from "./semantic.js";
7
9
  import { canonical, confined, digest, bytesDigest } from "./util.js";
8
10
  /** Fresh packet-only worker, using the selected host's exact runtime settings. */
9
11
  export function graphSemanticHost(root, host, createWorker = async (options) => host.agent === "codex" ? new CodexAdapter(options) : ClaudeAdapter.create(options)) {
@@ -101,29 +103,16 @@ export function graphSemanticHost(root, host, createWorker = async (options) =>
101
103
  store.put("job", id, { version: 1, state: "returned-unvalidated", operationId, requestId: request.operationId,
102
104
  inputDigest: request.inputDigest, sessionId: worker?.sessionId(),
103
105
  observations: observations.map(t => ({ turnId: t.turnId, usage: t.usage, costUsd: t.costAuthoritative ? t.costUsd : undefined,
104
- responseDigest: bytesDigest(t.rawResponse ?? t.text), failed: Boolean(t.isError || t.failure) })) });
106
+ responseDigest: bytesDigest(JSON.stringify({ structured: t.structured, rawResponse: t.rawResponse ?? t.text })), failed: Boolean(t.isError || t.failure) })) });
105
107
  };
106
- const turn = await Promise.race([worker.sendTurn(canonical(request), { responseOnly: true, handback: true, logicalActionId: request.operationId }), stopped]);
108
+ const turn = await Promise.race([journalStructuredDispatch(root, worker, `${canonical(request)}\n${operationInstruction(semanticExtractionOutput)}`, nativePolicy(semanticExtractionOutput, { responseOnly: true, handback: true, logicalActionId: request.operationId }), (prompt, policy) => worker.sendTurn(prompt, policy)), stopped]);
107
109
  recordObservation(turn);
108
110
  check();
109
- accountOutput(turn.cleanedResponse ?? turn.text);
110
111
  if (turn.isError || turn.failure)
111
112
  throw new Error(`Semantic provider failed: ${turn.text.slice(0, 500)}`);
112
- let parsed;
113
- try {
114
- parsed = parseSemanticResult(turn.cleanedResponse ?? turn.text, request);
115
- }
116
- catch (error) {
117
- check();
118
- store.put("job", id, { version: 1, state: "repair-intended", operationId, requestId: request.operationId, inputDigest: request.inputDigest, initialResponseDigest: bytesDigest(turn.rawResponse ?? turn.text), observations: observations.map(t => ({ turnId: t.turnId, usage: t.usage, costUsd: t.costAuthoritative ? t.costUsd : undefined })) });
119
- const repaired = await Promise.race([worker.sendTurn(`Format correction only. Preserve your source observations; no tools or new extraction. Return strict JSON matching ${canonical({ operationId: request.operationId, inputDigest: request.inputDigest })}. Validation: ${String(error)}`, { responseOnly: true, handback: true, logicalActionId: request.operationId }), stopped]);
120
- recordObservation(repaired);
121
- check();
122
- accountOutput(repaired.cleanedResponse ?? repaired.text);
123
- if (repaired.isError || repaired.failure)
124
- throw new Error("Semantic format correction failed; reconciliation required");
125
- parsed = parseSemanticResult(repaired.cleanedResponse ?? repaired.text, request);
126
- }
113
+ const payload = operationPayload(turn, semanticExtractionOutput);
114
+ accountOutput(JSON.stringify(payload));
115
+ const parsed = validateSemanticResult(payload, request);
127
116
  const known = observations.every(t => t.usage?.scope === "turn-delta" && t.usage.inputTokens !== undefined && t.usage.outputTokens !== undefined);
128
117
  const authoritative = observations.every(t => t.costAuthoritative === true);
129
118
  store.put("job", id, { version: 1, state: "completed", operationId, sessionId: worker.sessionId(),
@@ -1,6 +1,6 @@
1
1
  import { type ManagerGraphRequestV3 } from "rafi-spec";
2
2
  import type { GraphDerivedAccess } from "./derived.js";
3
- export declare const MANAGER_GRAPH_GUIDANCE = "For substantial architecture/dependency questions, request graph evidence through this additional host envelope: {\"kind\":\"manager_graph_evidence_request\",\"version\":3,\"requestId\":\"unique-id\",\"operations\":[{\"operation\":\"query\",\"query\":\"specific source concepts\"}]}. Do not mix envelopes. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.";
3
+ export declare const MANAGER_GRAPH_GUIDANCE = "For substantial architecture/dependency questions, request graph evidence through a native evidence-request result with evidenceType graph whose request is: {\"kind\":\"manager_graph_evidence_request\",\"version\":3,\"requestId\":\"unique-id\",\"operations\":[{\"operation\":\"query\",\"query\":\"specific source concepts\"}]}. Submit one result per turn. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.";
4
4
  export declare function parseManagerGraphRequest(text: string): ManagerGraphRequestV3 | undefined;
5
5
  export declare function createManagerGraphEvidence(configRoot: string, runId?: string): {
6
6
  guidance: string;
@@ -4,7 +4,7 @@ import { loadGraphConfig } from "./config.js";
4
4
  import { captureGraphCorpus } from "./corpus.js";
5
5
  import { acquireGraphView, graphUnavailable, readGraph } from "./read.js";
6
6
  import { canonical } from "./util.js";
7
- export const MANAGER_GRAPH_GUIDANCE = `For substantial architecture/dependency questions, request graph evidence through this additional host envelope: {"kind":"manager_graph_evidence_request","version":3,"requestId":"unique-id","operations":[{"operation":"query","query":"specific source concepts"}]}. Do not mix envelopes. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.`;
7
+ export const MANAGER_GRAPH_GUIDANCE = `For substantial architecture/dependency questions, request graph evidence through a native evidence-request result with evidenceType graph whose request is: {"kind":"manager_graph_evidence_request","version":3,"requestId":"unique-id","operations":[{"operation":"query","query":"specific source concepts"}]}. Submit one result per turn. This shares your existing evidence lookup budget. Only the host chooses project/source scope. An optional receiptId can select previously delivered evidence belonging to the host-selected run; unavailable historical caches stay unavailable. Current-source graph evidence cannot establish a historical run's implementation or cause. Graph output never authorizes controls.`;
8
8
  export function parseManagerGraphRequest(text) {
9
9
  if (Buffer.byteLength(text) > 8192 || !text.trim().startsWith("{"))
10
10
  return undefined;