ai-foreman 1.7.19 → 1.7.21

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 (205) hide show
  1. package/README.md +29 -5
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +92 -5
  20. package/dist/buildAdmission.d.ts +61 -0
  21. package/dist/buildAdmission.js +286 -0
  22. package/dist/buildApproval.d.ts +2 -0
  23. package/dist/buildApproval.js +17 -0
  24. package/dist/buildAssignment.d.ts +33 -0
  25. package/dist/buildAssignment.js +215 -0
  26. package/dist/buildInterventions.d.ts +90 -0
  27. package/dist/buildInterventions.js +334 -0
  28. package/dist/buildLaunchGate.d.ts +1 -0
  29. package/dist/buildLaunchGate.js +36 -0
  30. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  31. package/dist/buildOwnershipReconciliation.js +41 -0
  32. package/dist/buildOwnershipRepair.d.ts +33 -0
  33. package/dist/buildOwnershipRepair.js +111 -0
  34. package/dist/buildRuns.d.ts +24 -1
  35. package/dist/buildRuns.js +216 -70
  36. package/dist/buildSyntheticWork.d.ts +2 -0
  37. package/dist/buildSyntheticWork.js +30 -0
  38. package/dist/buildWorkAdmission.d.ts +17 -0
  39. package/dist/buildWorkAdmission.js +57 -0
  40. package/dist/buildWorkAuthorization.d.ts +2 -0
  41. package/dist/buildWorkAuthorization.js +23 -0
  42. package/dist/buildWorkContext.d.ts +3 -0
  43. package/dist/buildWorkContext.js +6 -0
  44. package/dist/buildWorkMigration.d.ts +9 -0
  45. package/dist/buildWorkMigration.js +206 -0
  46. package/dist/buildWorkReconciliation.d.ts +12 -0
  47. package/dist/buildWorkReconciliation.js +70 -0
  48. package/dist/builderGuidanceFollowup.d.ts +15 -0
  49. package/dist/builderGuidanceFollowup.js +34 -0
  50. package/dist/cli/doctor.js +3 -2
  51. package/dist/cli/graph.d.ts +2 -0
  52. package/dist/cli/graph.js +75 -0
  53. package/dist/cli/manager.d.ts +8 -0
  54. package/dist/cli/manager.js +234 -12
  55. package/dist/cli/recovery.js +3 -2
  56. package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
  57. package/dist/cli/runtimeAuthPrompt.js +41 -7
  58. package/dist/cli/start.d.ts +8 -0
  59. package/dist/cli/start.js +2091 -1849
  60. package/dist/cli/status.js +20 -5
  61. package/dist/cli/tickets.js +9 -7
  62. package/dist/continuity.d.ts +16 -1
  63. package/dist/continuity.js +68 -12
  64. package/dist/foreman.d.ts +12 -4
  65. package/dist/foreman.js +397 -60
  66. package/dist/graph/boundary.d.ts +15 -0
  67. package/dist/graph/boundary.js +50 -0
  68. package/dist/graph/bridge.d.ts +9 -0
  69. package/dist/graph/bridge.js +146 -0
  70. package/dist/graph/bridge.py +205 -0
  71. package/dist/graph/config.d.ts +11 -0
  72. package/dist/graph/config.js +40 -0
  73. package/dist/graph/corpus.d.ts +16 -0
  74. package/dist/graph/corpus.js +188 -0
  75. package/dist/graph/derived.d.ts +17 -0
  76. package/dist/graph/derived.js +73 -0
  77. package/dist/graph/host.d.ts +4 -0
  78. package/dist/graph/host.js +160 -0
  79. package/dist/graph/install.d.ts +3 -0
  80. package/dist/graph/install.js +50 -0
  81. package/dist/graph/lifecycle.d.ts +11 -0
  82. package/dist/graph/lifecycle.js +124 -0
  83. package/dist/graph/maintenance.d.ts +43 -0
  84. package/dist/graph/maintenance.js +301 -0
  85. package/dist/graph/manager.d.ts +9 -0
  86. package/dist/graph/manager.js +92 -0
  87. package/dist/graph/preparation.d.ts +41 -0
  88. package/dist/graph/preparation.js +43 -0
  89. package/dist/graph/read.d.ts +27 -0
  90. package/dist/graph/read.js +152 -0
  91. package/dist/graph/semantic.d.ts +17 -0
  92. package/dist/graph/semantic.js +27 -0
  93. package/dist/graph/session.d.ts +8 -0
  94. package/dist/graph/session.js +92 -0
  95. package/dist/graph/sources.d.ts +4 -0
  96. package/dist/graph/sources.js +11 -0
  97. package/dist/graph/storage.d.ts +38 -0
  98. package/dist/graph/storage.js +159 -0
  99. package/dist/graph/turn.d.ts +17 -0
  100. package/dist/graph/turn.js +360 -0
  101. package/dist/graph/util.d.ts +5 -0
  102. package/dist/graph/util.js +51 -0
  103. package/dist/handoffs.d.ts +1 -0
  104. package/dist/handoffs.js +5 -1
  105. package/dist/humanDecision.d.ts +21 -2
  106. package/dist/humanDecision.js +169 -10
  107. package/dist/index.js +4 -1
  108. package/dist/installOwnership.d.ts +46 -0
  109. package/dist/installOwnership.js +440 -0
  110. package/dist/linuxProbeInventory.d.ts +12 -0
  111. package/dist/linuxProbeInventory.js +48 -0
  112. package/dist/log.d.ts +1 -1
  113. package/dist/log.js +7 -1
  114. package/dist/managerEvidence.d.ts +25 -0
  115. package/dist/managerEvidence.js +326 -0
  116. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  117. package/dist/managerEvidenceArtifacts.js +118 -0
  118. package/dist/managerPacket.d.ts +2 -0
  119. package/dist/managerPacket.js +23 -0
  120. package/dist/markers.d.ts +1 -1
  121. package/dist/markers.js +2 -2
  122. package/dist/observability.js +1 -1
  123. package/dist/processIdentity.d.ts +21 -1
  124. package/dist/processIdentity.js +87 -5
  125. package/dist/providerPhase.d.ts +13 -0
  126. package/dist/providerPhase.js +26 -0
  127. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  128. package/dist/qaAuthorizedDisposition.js +26 -0
  129. package/dist/qaBuildGate.d.ts +4 -0
  130. package/dist/qaBuildGate.js +216 -0
  131. package/dist/qaBuilderCoverage.d.ts +5 -0
  132. package/dist/qaBuilderCoverage.js +78 -0
  133. package/dist/qaContractCoverage.d.ts +10 -0
  134. package/dist/qaContractCoverage.js +55 -0
  135. package/dist/qaContractDelivery.d.ts +23 -0
  136. package/dist/qaContractDelivery.js +145 -0
  137. package/dist/qaContractFreshness.d.ts +15 -0
  138. package/dist/qaContractFreshness.js +36 -0
  139. package/dist/qaDeliveryJournal.d.ts +10 -0
  140. package/dist/qaDeliveryJournal.js +16 -1
  141. package/dist/qaEffectiveConfig.d.ts +20 -0
  142. package/dist/qaEffectiveConfig.js +40 -0
  143. package/dist/qaEvidenceReader.d.ts +14 -0
  144. package/dist/qaEvidenceReader.js +176 -0
  145. package/dist/qaFailureDelivery.d.ts +2 -0
  146. package/dist/qaFailureDelivery.js +107 -10
  147. package/dist/qaHandbackMigration.d.ts +1 -1
  148. package/dist/qaHandbackMigration.js +3 -3
  149. package/dist/qaPreparation.d.ts +26 -0
  150. package/dist/qaPreparation.js +150 -0
  151. package/dist/qaPreparationInputs.d.ts +8 -0
  152. package/dist/qaPreparationInputs.js +47 -0
  153. package/dist/qaPreparationMetrics.d.ts +145 -0
  154. package/dist/qaPreparationMetrics.js +120 -0
  155. package/dist/qaPreparationPolicy.d.ts +5 -0
  156. package/dist/qaPreparationPolicy.js +51 -0
  157. package/dist/qaPreparationStore.d.ts +157 -0
  158. package/dist/qaPreparationStore.js +412 -0
  159. package/dist/qaProtocolV2.d.ts +28 -0
  160. package/dist/qaProtocolV2.js +12 -0
  161. package/dist/qaReview.d.ts +6 -0
  162. package/dist/qaReview.js +293 -41
  163. package/dist/qaTimeline.d.ts +18 -0
  164. package/dist/qaTimeline.js +123 -0
  165. package/dist/qaVerificationContract.d.ts +17 -0
  166. package/dist/qaVerificationContract.js +236 -0
  167. package/dist/readinessCleanup.d.ts +19 -0
  168. package/dist/readinessCleanup.js +122 -0
  169. package/dist/readinessGate.d.ts +1 -0
  170. package/dist/readinessGate.js +82 -0
  171. package/dist/recoveryGuidance.d.ts +17 -0
  172. package/dist/recoveryGuidance.js +38 -0
  173. package/dist/runtimeAuth.d.ts +1 -0
  174. package/dist/runtimeAuth.js +5 -1
  175. package/dist/runtimeReadiness.d.ts +13 -0
  176. package/dist/runtimeReadiness.js +206 -10
  177. package/dist/sessionIdentity.d.ts +2 -0
  178. package/dist/sessionIdentity.js +14 -0
  179. package/dist/sessionLifecycle.js +3 -3
  180. package/dist/sources/sourceRegistry.js +7 -0
  181. package/dist/stateTransfer.js +85 -8
  182. package/dist/statusReporter.d.ts +1 -0
  183. package/dist/statusReporter.js +1 -0
  184. package/dist/supervisedStart.d.ts +6 -0
  185. package/dist/supervisedStart.js +178 -62
  186. package/dist/supervisor.d.ts +2 -0
  187. package/dist/supervisor.js +26 -2
  188. package/dist/terminalStyle.d.ts +21 -0
  189. package/dist/terminalStyle.js +50 -0
  190. package/dist/ticketPopulation.d.ts +1 -0
  191. package/dist/ticketPopulation.js +5 -1
  192. package/dist/tickets/commands.js +2 -2
  193. package/dist/tickets/queue.js +1 -1
  194. package/dist/tickets/ticketSchema.d.ts +198 -0
  195. package/dist/tickets/ticketSchema.js +2 -0
  196. package/dist/windowsProbeJob.d.ts +12 -0
  197. package/dist/windowsProbeJob.js +144 -0
  198. package/dist/workflowDb.d.ts +121 -6
  199. package/dist/workflowDb.js +874 -30
  200. package/dist/workflowReader.d.ts +116 -2
  201. package/dist/workflowReader.js +131 -4
  202. package/package.json +89 -8
  203. package/scripts/copy-graph-assets.mjs +3 -0
  204. package/scripts/graph-benchmark.mjs +20 -0
  205. package/scripts/graph-evaluation.mjs +85 -0
@@ -1,16 +1,26 @@
1
+ import { dispatchWithGraphAccess } from "../graph/session.js";
2
+ import { createManagerGraphEvidence, parseManagerGraphRequest } from "../graph/manager.js";
3
+ import { prepareOwnershipRepair, repairOwnership } from "../buildOwnershipRepair.js";
4
+ import { BuildInterventionControl, parseManagerAction } from "../buildInterventions.js";
1
5
  import { randomUUID } from "node:crypto";
2
6
  import { createInterface } from "node:readline/promises";
3
7
  import { stdin as input, stdout as output } from "node:process";
4
8
  import { resolve } from "node:path";
9
+ import { join } from "node:path";
10
+ import { mkdtempSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { ManagerEvidenceService, parseManagerEvidenceRequestV2 } from "../managerEvidence.js";
13
+ import { buildManagerEvidencePacketV2 } from "../managerPacket.js";
5
14
  import { Command } from "commander";
6
15
  import { createRoleBuilder, readOnlyPermissionConfig } from "../agentRun.js";
7
16
  import { Log } from "../log.js";
8
17
  import { buildManagerEvidencePacket, buildManagerProjectPacket } from "../managerPacket.js";
9
18
  import { ManagerSessionRecorder } from "../observability.js";
10
19
  import { collectManagerProjectDiagnostics, executeManagerEvidenceRequest, MANAGER_LOOKUP_MAX_ROUNDS, parseManagerEvidenceRequest, resolveManagerQuestionRuns } from "../projectDiagnostics.js";
20
+ import { accent, sanitizeTerminalText } from "../terminalStyle.js";
11
21
  export function buildManagerCommand(options = {}) {
12
22
  return new Command("manager")
13
- .description("Ask a read-only Manager about all retained builds in a project.")
23
+ .description("Inspect retained builds and enqueue explicitly scoped Builder/QA guidance.")
14
24
  .argument(options.requireProject ? "<project>" : "[project]", "project directory")
15
25
  .option("--run <run-id>", "set the initial focused build run")
16
26
  .option("--ask <question>", "ask one question and exit")
@@ -19,6 +29,7 @@ export function buildManagerCommand(options = {}) {
19
29
  .option("--effort <level>", "low | medium | high | xhigh")
20
30
  .option("--fast", "use the provider fast mode")
21
31
  .option("--external <mode>", "auto | on | off", "auto")
32
+ .addHelpText("after", "\nHost evidence commands (also accepted by --ask):\n /qa-work <run>\n /qa-attempts <run> <ticket>\n /qa-report <run> <ticket> <attempt> [occurrence]\n /qa-timeline <run> <ticket>\n /qa-conflicts <run>\n /qa-instruction <run> <work> <instruction>\n /qa-repair-plan <run> <work>\n /qa-repair <JSON request>\n /more <cursor>\n /artifact <handle>\n /qa-export <run> <ticket> <attempt> [occurrence]\n /qa-export <handle> Export protected raw bytes to a private local file.\nScoped controls:\n /guide-builder <run> <work> <text>\n /guide-qa <run> <work> <text>\n /guide-both <run> <work> <text>\n /pause <run> <work> [run]\n /withdraw <run> <work> <instruction>\n /supersede <run> <work> <instruction> <text>\n /request-attempt <run> <work> <reason>\n /answer-question <run> <work> <decision> <revision> <text>\n")
22
33
  .action(async (project, opts) => {
23
34
  const root = options.resolveProject ? options.resolveProject(project) : resolve(project ?? ".");
24
35
  await runManager(root, {
@@ -30,15 +41,27 @@ export function buildManagerCommand(options = {}) {
30
41
  export async function runManager(projectDir, options) {
31
42
  if (!options.ask && (!input.isTTY || !output.isTTY))
32
43
  throw new Error("non-TTY Manager use requires --ask <question>");
44
+ const evidence = new ManagerEvidenceService(projectDir);
45
+ if (options.ask && (options.ask.trim().startsWith("/") || parseManagerAction(options.ask))) {
46
+ try {
47
+ if (!executeManagerHostCommand(evidence, options.ask, text => output.write(text), true))
48
+ throw new Error("Unknown Manager host command; see manager --help");
49
+ }
50
+ finally {
51
+ evidence.close();
52
+ }
53
+ return;
54
+ }
33
55
  const initialCollection = collectManagerProjectDiagnostics(projectDir, { initialFocusRunId: options.runId, question: options.ask, external: "off" });
34
56
  const initialReport = initialCollection.report;
35
57
  let currentFocusRunId = initialReport.initialFocusRunId;
36
58
  let referencedRunIds = [currentFocusRunId];
37
- output.write(`rafi manager: ${initialReport.totalRunCount} retained build run${initialReport.totalRunCount === 1 ? "" : "s"}\n`);
38
- output.write(`rafi manager: verified active run ${initialReport.verifiedActiveRunId ?? "none"}\n`);
39
- output.write(`rafi manager: initial focus ${currentFocusRunId}\n`);
59
+ const managerLabel = (text) => accent(text, { stream: output, env: process.env, machineReadable: Boolean(options.ask) });
60
+ output.write(`${managerLabel("rafi manager")}: ${initialReport.totalRunCount} retained build run${initialReport.totalRunCount === 1 ? "" : "s"}\n`);
61
+ output.write(`${managerLabel("rafi manager")}: verified active run ${sanitizeTerminalText(initialReport.verifiedActiveRunId ?? "none")}\n`);
62
+ output.write(`${managerLabel("rafi manager")}: initial focus ${sanitizeTerminalText(currentFocusRunId)}\n`);
40
63
  if (initialReport.staleRecoveryRunIds.length)
41
- output.write(`rafi manager: stale recovery state ${initialReport.staleRecoveryRunIds.join(", ")}\n`);
64
+ output.write(`${managerLabel("rafi manager")}: stale recovery state ${initialReport.staleRecoveryRunIds.map(sanitizeTerminalText).join(", ")}\n`);
42
65
  const metadata = new ManagerSessionRecorder(projectDir);
43
66
  const managerSessionId = randomUUID();
44
67
  const startedAt = new Date().toISOString();
@@ -52,6 +75,9 @@ export async function runManager(projectDir, options) {
52
75
  const activeRole = role = await createRoleBuilder({ projectDir, role: "manager", agent: options.agent, model: options.model, effort: options.effort, fast: options.fast,
53
76
  yes: true, allowSwitch: false, label: "Manager", log: new Log(), permissionConfig: denyManagerTools(), sandboxMode: "read-only", persistSessionBindings: false });
54
77
  const ask = async (question) => {
78
+ if (executeManagerHostCommand(evidence, question, text => output.write(text), Boolean(options.ask)))
79
+ return;
80
+ let emittedMachineOutput = false;
55
81
  const refreshed = collectManagerProjectDiagnostics(projectDir, { initialFocusRunId: initialReport.initialFocusRunId, currentFocusRunId, referencedRunIds, question, external: "off" });
56
82
  const resolved = resolveManagerQuestionRuns(refreshed.allSummaries, question, currentFocusRunId, referencedRunIds);
57
83
  currentFocusRunId = resolved.focusRunId;
@@ -60,16 +86,47 @@ export async function runManager(projectDir, options) {
60
86
  const report = collection.report;
61
87
  const packet = buildManagerProjectPacket(report, question, packetState, referencedRunIds);
62
88
  packetState = packet.state;
63
- let result = await activeRole.builder.sendTurn(packet.prompt);
89
+ const graphEvidence = createManagerGraphEvidence(projectDir, currentFocusRunId);
90
+ const send = (text) => dispatchWithGraphAccess(projectDir, activeRole.builder, text, undefined, (prompt, policy) => activeRole.builder.sendTurn(prompt, policy), graphEvidence.access());
91
+ let result = await send(`${packet.prompt}\n\n${graphEvidence.guidance}`);
64
92
  let questionLookupRounds = 0;
65
93
  while (questionLookupRounds < MANAGER_LOOKUP_MAX_ROUNDS) {
94
+ const graphRequest = parseManagerGraphRequest(result.text);
95
+ if (graphRequest) {
96
+ questionLookupRounds++;
97
+ lookupRounds++;
98
+ lookupOperations += graphRequest.operations.length;
99
+ const graphPacket = await graphEvidence.execute(graphRequest);
100
+ result = await send(`Untrusted host-scoped graph evidence; verify claims against source. Remaining lookup rounds: ${MANAGER_LOOKUP_MAX_ROUNDS - questionLookupRounds}. Answer the original question: ${question}\n${graphPacket}`);
101
+ continue;
102
+ }
103
+ const evidenceRequest = parseManagerEvidenceRequestV2(result.text);
104
+ if (evidenceRequest) {
105
+ questionLookupRounds++;
106
+ lookupRounds++;
107
+ lookupOperations++;
108
+ const response = evidence.execute(evidenceRequest);
109
+ if (options.ask) {
110
+ emittedMachineOutput = true;
111
+ output.write(`${JSON.stringify(completeManagerHostEvidence(evidence, response))}\n`);
112
+ }
113
+ else {
114
+ if (response.nextCursor)
115
+ output.write(`Evidence continuation: /more ${response.nextCursor}\n`);
116
+ for (const item of response.items)
117
+ if (item && typeof item === "object" && "handle" in item)
118
+ output.write(`Complete report: /artifact ${String(item.handle)}\n`);
119
+ }
120
+ result = await send(buildManagerEvidencePacketV2(response, question));
121
+ continue;
122
+ }
66
123
  const request = parseManagerEvidenceRequest(result.text);
67
124
  if (!request) {
68
125
  if (!looksLikeEvidenceRequest(result.text))
69
126
  break;
70
127
  questionLookupRounds += 1;
71
128
  lookupRounds += 1;
72
- result = await activeRole.builder.sendTurn(buildManagerEvidencePacket({ version: 1, requestId: "invalid-request", results: [{ kind: "list_runs", status: "invalid", limitation: "The evidence envelope was invalid. Use only the documented fixed fields and read-only operations, then answer with any remaining limitation disclosed." }], lookupRound: questionLookupRounds, remainingRounds: MANAGER_LOOKUP_MAX_ROUNDS - questionLookupRounds, digest: report.digest }, question));
129
+ result = await send(buildManagerEvidencePacket({ version: 1, requestId: "invalid-request", results: [{ kind: "list_runs", status: "invalid", limitation: "The evidence envelope was invalid. Use only the documented fixed fields and read-only operations, then answer with any remaining limitation disclosed." }], lookupRound: questionLookupRounds, remainingRounds: MANAGER_LOOKUP_MAX_ROUNDS - questionLookupRounds, digest: report.digest }, question));
73
130
  continue;
74
131
  }
75
132
  questionLookupRounds += 1;
@@ -77,14 +134,34 @@ export async function runManager(projectDir, options) {
77
134
  lookupOperations += Math.min(6, request.operations.length);
78
135
  const response = executeManagerEvidenceRequest(projectDir, request, collection, questionLookupRounds);
79
136
  packetState.lastEvidenceScope = request.operations.flatMap(operation => "runIds" in operation ? operation.runIds : []);
80
- result = await activeRole.builder.sendTurn(buildManagerEvidencePacket(response, question));
137
+ result = await send(buildManagerEvidencePacket(response, question));
81
138
  }
82
139
  const unfulfilled = parseManagerEvidenceRequest(result.text);
140
+ const pendingEvidence = parseManagerEvidenceRequestV2(result.text);
141
+ if (pendingEvidence) {
142
+ const page = evidence.execute(pendingEvidence);
143
+ if (options.ask) {
144
+ emittedMachineOutput = true;
145
+ output.write(`${JSON.stringify(completeManagerHostEvidence(evidence, page))}\n`);
146
+ }
147
+ else
148
+ output.write(`${JSON.stringify(page)}\n${page.nextCursor ? `Continue with /more ${page.nextCursor}` : "Browse reports with /qa-attempts <run> <ticket> and /qa-report <run> <ticket> <attempt>"}\n`);
149
+ }
83
150
  if (unfulfilled) {
84
151
  const limitation = { version: 1, requestId: unfulfilled.requestId, results: unfulfilled.operations.slice(0, 6).map(operation => ({ kind: operation.kind, status: "limited", limitation: "the two-round evidence lookup budget is exhausted; answer with this limitation disclosed" })), lookupRound: MANAGER_LOOKUP_MAX_ROUNDS, remainingRounds: 0, digest: report.digest };
85
- result = await activeRole.builder.sendTurn(buildManagerEvidencePacket(limitation, question));
152
+ result = await send(buildManagerEvidencePacket(limitation, question));
153
+ }
154
+ if (!pendingEvidence) {
155
+ const answer = parseManagerEvidenceRequest(result.text) || parseManagerGraphRequest(result.text) || looksLikeEvidenceRequest(result.text)
156
+ ? "Manager could not complete an answer within the bounded evidence lookup budget. Continue with /qa-attempts <run> <ticket> or /qa-report <run> <ticket> <attempt>."
157
+ : result.text.trim();
158
+ // One-shot evidence requests can mix JSON with prose, so add the label
159
+ // only when this answer stream has not emitted machine-readable data.
160
+ // Model-provided text is sanitized only for display, never for storage.
161
+ if (!emittedMachineOutput)
162
+ output.write(`${accent("Manager:", { stream: output, env: process.env })} `);
163
+ output.write(`${sanitizeTerminalText(answer)}\n`);
86
164
  }
87
- output.write(`${parseManagerEvidenceRequest(result.text) || looksLikeEvidenceRequest(result.text) ? "Manager could not complete an answer within the bounded evidence lookup budget; the available project report remains partial." : result.text.trim()}\n`);
88
165
  usage = await activeRole.builder.sessionUsage?.() ?? usage;
89
166
  metadata.record({ sessionId: managerSessionId, runId: initialReport.initialFocusRunId, provider: activeRole.builder.agent, startedAt, reportDigest: report.digest,
90
167
  inputTokens: usage?.inputTokens, outputTokens: usage?.outputTokens, costUsd: usage?.authoritativeCostUsd, scope: "project", latestFocusRunId: currentFocusRunId,
@@ -99,11 +176,11 @@ export async function runManager(projectDir, options) {
99
176
  readline.on("SIGINT", onInterrupt);
100
177
  try {
101
178
  while (true) {
102
- const question = await readline.question("manager> ");
179
+ const question = await readline.question(`${accent("manager>", { stream: output, env: process.env })} `);
103
180
  if (question.trim() === "/exit")
104
181
  break;
105
182
  if (question.trim())
106
- await ask(question.trim());
183
+ await ask(question);
107
184
  }
108
185
  }
109
186
  catch (error) {
@@ -124,7 +201,152 @@ export async function runManager(projectDir, options) {
124
201
  finally {
125
202
  await role?.builder.close().catch(() => { });
126
203
  metadata.close();
204
+ evidence.close();
205
+ }
206
+ }
207
+ /** Only original user commands enter here. Model output and retained reports cannot export raw bytes. */
208
+ export function executeManagerHostCommand(service, command, write, complete = false) {
209
+ if (command.trim().startsWith("/qa-repair ")) {
210
+ write(`${JSON.stringify(repairOwnership(service.projectDir, JSON.parse(command.trim().slice("/qa-repair ".length)), command))}\n`);
211
+ return true;
212
+ }
213
+ if (command.trim().startsWith("/qa-repair-plan ")) {
214
+ const args = command.trim().split(/\s+/);
215
+ if (args.length !== 3)
216
+ throw new Error("/qa-repair-plan requires run and work identities");
217
+ write(`${JSON.stringify(prepareOwnershipRepair(service.projectDir, args[1], args[2]))}\n`);
218
+ return true;
219
+ }
220
+ const proposed = parseManagerAction(command);
221
+ if (proposed) {
222
+ const control = new BuildInterventionControl(service.projectDir, proposed.runId, proposed.workId);
223
+ try {
224
+ const request = parseManagerAction(command, proposed.requestId, control.revision());
225
+ const queued = control.enqueue(request, command);
226
+ write(`${JSON.stringify(queued)}\n${request.action === "withdraw" ? "Withdrawn before reservation." : "Queued at the next eligible safe boundary. An active provider turn continues; a stopped owner must resume to consume this instruction."}\n`);
227
+ }
228
+ finally {
229
+ control.close();
230
+ }
231
+ return true;
232
+ }
233
+ if (!command.trim().startsWith("/"))
234
+ return false;
235
+ const [name, ...args] = command.trim().split(/\s+/);
236
+ if (name === "/artifact" || name === "/qa-export" && args.length === 1) {
237
+ if (args.length !== 1)
238
+ throw new Error(`${name} requires one host artifact handle`);
239
+ if (name === "/artifact")
240
+ write(`${JSON.stringify(service.artifacts.metadata(args[0]))}\n${service.artifacts.bytes(args[0]).toString("utf8")}\n`);
241
+ else {
242
+ const path = join(mkdtempSync(join(tmpdir(), "rafi-qa-evidence-")), "protected-evidence.txt");
243
+ writeFileSync(path, service.artifacts.bytes(args[0], true), { mode: 0o600 });
244
+ write(`Protected raw evidence exported to ${path}\n${JSON.stringify(service.artifacts.metadata(args[0]))}\n`);
245
+ }
246
+ return true;
247
+ }
248
+ if (name === "/more") {
249
+ if (args.length !== 1)
250
+ throw new Error("/more requires one cursor");
251
+ const page = service.more(args[0]);
252
+ write(`${JSON.stringify(complete ? completeManagerHostEvidence(service, page) : page)}\n`);
253
+ return true;
254
+ }
255
+ const requestId = randomUUID();
256
+ const runId = args[0] ?? "";
257
+ const workId = args[1] ?? "";
258
+ let page;
259
+ if (name === "/qa-work" || name === "/qa-conflicts") {
260
+ if (args.length !== 1)
261
+ throw new Error(`${name} requires a run ID`);
262
+ page = service.execute({ version: 2, requestId, operation: { kind: name === "/qa-work" ? "list_build_work" : "get_ownership_conflicts", runId } });
263
+ }
264
+ else if (name === "/qa-attempts" || name === "/qa-timeline") {
265
+ if (args.length !== 2)
266
+ throw new Error(`${name} requires run and ticket IDs`);
267
+ page = service.execute({ version: 2, requestId, operation: { kind: name === "/qa-attempts" ? "list_qa_attempts" : "get_qa_timeline", runId, workId } });
268
+ }
269
+ else if (name === "/qa-instruction") {
270
+ if (args.length !== 3)
271
+ throw new Error("/qa-instruction requires run, work and instruction identities");
272
+ page = service.execute({ version: 2, requestId, operation: { kind: "get_intervention_status", runId, workId, instructionId: args[2] } });
273
+ }
274
+ else if (name === "/qa-report" || name === "/qa-export") {
275
+ if (args.length < 3 || args.length > 4)
276
+ throw new Error(`${name} requires run, ticket, attempt and optional occurrence IDs`);
277
+ const attemptId = args[2];
278
+ const attempts = service.execute({ version: 2, requestId, operation: { kind: "list_qa_attempts", runId, workId } });
279
+ const rows = completeManagerHostEvidence(service, attempts).items;
280
+ const attempt = rows.find(row => row && typeof row === "object" && "attemptId" in row && row.attemptId === attemptId);
281
+ if (!attempt)
282
+ throw new Error("Attempt is not retained in this run/ticket; use /qa-attempts first");
283
+ if (!attempt.reports.length) {
284
+ write(`${JSON.stringify(rows.find(row => row && typeof row === "object" && "attemptId" in row && row.attemptId === attemptId))}\nNo retained failure report body; inspect verification or attempt status above.\n`);
285
+ return true;
286
+ }
287
+ const occurrenceId = args[3] ?? (attempt.reports.length === 1 ? attempt.reports[0].occurrenceId : undefined);
288
+ if (!occurrenceId)
289
+ throw new Error("Report identity is ambiguous; specify a retained occurrence ID");
290
+ page = service.execute({ version: 2, requestId, operation: { kind: "get_qa_report", runId, workId, attemptId, occurrenceId, snapshotId: attempts.snapshotId } });
291
+ const artifact = page.items[0];
292
+ if (artifact?.handle) {
293
+ if (name === "/qa-export") {
294
+ const path = join(mkdtempSync(join(tmpdir(), "rafi-qa-evidence-")), "protected-evidence.txt");
295
+ writeFileSync(path, service.artifacts.bytes(artifact.handle, true), { mode: 0o600 });
296
+ write(`Protected raw evidence exported to ${path}\n${JSON.stringify({ ...service.artifacts.metadata(artifact.handle), snapshotId: page.snapshotId, asOf: page.asOf })}\n`);
297
+ }
298
+ else
299
+ write(`${JSON.stringify({ ...service.artifacts.metadata(artifact.handle), snapshotId: page.snapshotId, asOf: page.asOf, availability: page.availability, omissions: page.omissions.filter(item => !item.startsWith("Complete host rendering:")) })}\n${service.artifacts.bytes(artifact.handle).toString("utf8")}\n`);
300
+ return true;
301
+ }
302
+ }
303
+ else
304
+ return false;
305
+ write(`${JSON.stringify(complete ? completeManagerHostEvidence(service, page) : page)}\n`);
306
+ if (!complete && page.nextCursor)
307
+ write(`Continue with /more ${page.nextCursor}\n`);
308
+ return true;
309
+ }
310
+ /** Drain one pinned snapshot and expand every referenced artifact before host close.
311
+ * Errors are returned as incomplete output; an expired snapshot is never refreshed.
312
+ * This host output is deliberately independent of bounded model packets. */
313
+ export function completeManagerHostEvidence(service, first) {
314
+ const result = structuredClone(first);
315
+ result.redactions = service.hostRedactions(first.snapshotId);
316
+ result.items = [];
317
+ const seen = new Set();
318
+ let page = first;
319
+ while (true) {
320
+ for (const item of page.items) {
321
+ const row = item;
322
+ if (row?.metadataArtifact)
323
+ result.items.push(service.hostMetadataItem(row.metadataArtifact.handle));
324
+ else if (row?.handle) {
325
+ if (seen.has(row.handle))
326
+ continue;
327
+ seen.add(row.handle);
328
+ const { handle: _, ...metadata } = service.artifacts.metadata(row.handle);
329
+ result.items.push({ ...metadata, body: service.artifacts.bytes(row.handle).toString("utf8") });
330
+ }
331
+ else
332
+ result.items.push(item);
333
+ }
334
+ if (!page.nextCursor || page.error)
335
+ break;
336
+ const next = service.more(page.nextCursor);
337
+ if (!next.error && next.snapshotId !== first.snapshotId)
338
+ throw new Error("Evidence continuation changed snapshot identity");
339
+ page = next;
340
+ }
341
+ delete result.nextCursor;
342
+ result.complete = page.complete;
343
+ if (page.error) {
344
+ result.error = page.error;
345
+ result.restartAction = page.restartAction;
346
+ result.omissions.push(...page.omissions);
127
347
  }
348
+ result.omissions = result.omissions.filter(item => !item.startsWith("Complete host rendering:") && !item.includes("additional redaction spans are available"));
349
+ return result;
128
350
  }
129
351
  function denyManagerTools() {
130
352
  return { ...readOnlyPermissionConfig(), allowBash: [], allowTools: [], escalateBash: [""], escalateTools: ["Read", "Glob", "Grep", "Edit", "Write", "MultiEdit", "NotebookEdit", "TodoWrite", "WebFetch", "WebSearch", "Bash"] };
@@ -1,5 +1,6 @@
1
1
  import { isLiveProcessIdentity } from "../processIdentity.js";
2
2
  import { Command } from "commander";
3
+ import { accent, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
3
4
  import { resolve } from "node:path";
4
5
  import { readBuildRuns } from "../buildRuns.js";
5
6
  import { WorkflowDb } from "../workflowDb.js";
@@ -16,8 +17,8 @@ export function buildAttachCommand() {
16
17
  const db = new WorkflowDb(projectDir);
17
18
  try {
18
19
  const supervisor = db.supervisorState(run.runId);
19
- console.log(`rafi: run ${run.runId}`);
20
- console.log(`rafi: phase ${"phase" in run && typeof run.phase === "string" ? run.phase : run.checkpoint}; status ${run.status}`);
20
+ console.log(`${accent("rafi:")} run ${sanitizeTerminalText(run.runId)}`);
21
+ console.log(`${accent("rafi:")} phase ${sanitizeTerminalText("phase" in run && typeof run.phase === "string" ? run.phase : run.checkpoint)}; status ${run.status === "completed" ? success(run.status) : warning(run.status)}`);
21
22
  console.log(`rafi: supervisor ${supervisor?.status ?? "unavailable"}${supervisor?.pid ? ` pid=${supervisor.pid}` : ""}`);
22
23
  const attempts = db.recoveryAttempts(run.runId);
23
24
  console.log(`rafi: recovery attempts ${attempts.length}`);
@@ -1,4 +1,5 @@
1
1
  import { RuntimeAuthError, type AgentRuntime } from "../runtimeAuth.js";
2
+ import { type BuildReadinessContext } from "../runtimeReadiness.js";
2
3
  import type { RuntimeProbeResult } from "rafi-spec";
3
4
  export type RuntimeCommandRecoveryChoice = "retry" | "switch" | "cancel";
4
5
  export interface RuntimeCommandRecoveryContext {
@@ -7,6 +8,7 @@ export interface RuntimeCommandRecoveryContext {
7
8
  }
8
9
  export interface RuntimeReadyForCommandOptions {
9
10
  label: string;
11
+ build?: BuildReadinessContext;
10
12
  timeoutMs?: number;
11
13
  durable?: {
12
14
  projectDir: string;
@@ -27,4 +29,7 @@ export interface RuntimeReadyForCommandResult {
27
29
  fellBack: boolean;
28
30
  executable: string;
29
31
  }
32
+ export declare function ensureBuildRuntimeReadyForCommand(projectDir: string, runtime: AgentRuntime, opts: RuntimeReadyForCommandOptions & {
33
+ build: BuildReadinessContext;
34
+ }): Promise<RuntimeReadyForCommandResult>;
30
35
  export declare function ensureRuntimeReadyForCommand(projectDir: string, runtime: AgentRuntime, labelOrOptions: string | RuntimeReadyForCommandOptions): Promise<RuntimeReadyForCommandResult>;
@@ -1,17 +1,27 @@
1
1
  import { select, isCancel, log } from "@clack/prompts";
2
2
  import { requireClaudeSDK } from "../adapters/claude.js";
3
3
  import { checkRuntimeReady, RuntimeAuthError, runtimeCommandLabel, } from "../runtimeAuth.js";
4
- import { resolveExecutablePath } from "../runtimeReadiness.js";
4
+ import { BuildOwnershipError } from "../buildAdmission.js";
5
+ import { RuntimeCleanupError, runtimeCleanupRecoveryHelp, resolveExecutablePath } from "../runtimeReadiness.js";
5
6
  import { otherRuntime, runtimeDisplayName } from "./runtimeSelection.js";
6
7
  import { currentActivity } from "../activity.js";
7
8
  import { durableHumanDecision } from "../humanDecision.js";
8
9
  import { WorkflowDb } from "../workflowDb.js";
9
10
  import { createHash } from "node:crypto";
11
+ export async function ensureBuildRuntimeReadyForCommand(projectDir, runtime, opts) {
12
+ if (!opts.build?.authority)
13
+ throw new BuildOwnershipError("stale-owner", "Build readiness requires original owning-project context");
14
+ return ensureRuntimeReadyForCommand(projectDir, runtime, opts);
15
+ }
10
16
  export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrOptions) {
11
17
  const opts = typeof labelOrOptions === "string"
12
18
  ? { label: labelOrOptions }
13
19
  : labelOrOptions;
14
- const check = opts.check ?? ((projectDir, runtime) => checkRuntimeReady(projectDir, runtime, { onTrace: opts.onTrace, timeoutMs: opts.timeoutMs }));
20
+ const check = opts.check ?? (async (projectDir, runtime) => {
21
+ if (opts.durable && !opts.build)
22
+ throw new BuildOwnershipError("stale-owner", "Build readiness requires its original owning-project context");
23
+ return checkRuntimeReady(projectDir, runtime, { build: opts.build, onTrace: opts.onTrace, timeoutMs: opts.timeoutMs });
24
+ });
15
25
  const checkClaudeSdk = opts.checkClaudeSdk ?? requireClaudeSDK;
16
26
  const allowSwitch = opts.allowSwitch !== false;
17
27
  const nonInteractive = !opts.choose && (Boolean(opts.yes) || !process.stdin.isTTY || !process.stdout.isTTY);
@@ -26,6 +36,8 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
26
36
  return { runtime, model: opts.model, fellBack: false, executable };
27
37
  }
28
38
  catch (err) {
39
+ if (err instanceof BuildOwnershipError || (err instanceof RuntimeCleanupError && opts.build))
40
+ throw err;
29
41
  const failure = err instanceof RuntimeAuthError
30
42
  ? err
31
43
  : new RuntimeAuthError({
@@ -34,20 +46,21 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
34
46
  stderr: err instanceof Error ? err.message : String(err),
35
47
  cause: err,
36
48
  });
37
- if (nonInteractive && !opts.durable) {
49
+ if (nonInteractive && !opts.durable && !(failure.cleanupUnverified && process.stdin.isTTY && process.stdout.isTTY)) {
38
50
  throw failure;
39
51
  }
40
52
  const fallbackRuntime = otherRuntime(runtime);
53
+ const canSwitch = allowSwitch && !failure.cleanupUnverified;
41
54
  const choice = opts.durable && !opts.choose
42
- ? await durableRuntimeRecovery(opts.durable, failure, opts.label, fallbackRuntime, allowSwitch)
55
+ ? await durableRuntimeRecovery(opts.durable, failure, opts.label, fallbackRuntime, canSwitch)
43
56
  : opts.choose
44
- ? await opts.choose(failure, { otherRuntime: fallbackRuntime, allowSwitch })
45
- : await promptRuntimeRecovery(failure, opts.label, fallbackRuntime, allowSwitch);
57
+ ? await opts.choose(failure, { otherRuntime: fallbackRuntime, allowSwitch: canSwitch })
58
+ : await promptRuntimeRecovery(failure, opts.label, fallbackRuntime, canSwitch);
46
59
  if (choice === "retry") {
47
60
  currentActivity()?.note(`rafi: retrying ${runtime} readiness check`);
48
61
  continue;
49
62
  }
50
- if (choice === "switch" && allowSwitch) {
63
+ if (choice === "switch" && canSwitch) {
51
64
  currentActivity()?.note(`rafi: checking fallback runtime ${fallbackRuntime}`);
52
65
  try {
53
66
  const probe = await check(projectDir, fallbackRuntime);
@@ -64,6 +77,8 @@ export async function ensureRuntimeReadyForCommand(projectDir, runtime, labelOrO
64
77
  return { runtime: fallbackRuntime, model: undefined, fellBack: true, executable };
65
78
  }
66
79
  catch (switchErr) {
80
+ if (switchErr instanceof BuildOwnershipError || (switchErr instanceof RuntimeCleanupError && opts.build))
81
+ throw switchErr;
67
82
  const switchFailure = switchErr instanceof RuntimeAuthError
68
83
  ? switchErr
69
84
  : new RuntimeAuthError({
@@ -116,6 +131,25 @@ async function durableRuntimeRecovery(scope, failure, label, fallbackRuntime, al
116
131
  }
117
132
  async function promptRuntimeRecovery(err, label, fallbackRuntime, allowSwitch) {
118
133
  log.error(err.message);
134
+ if (err.cleanupUnverified) {
135
+ while (true) {
136
+ const choice = await select({
137
+ message: `${label} is paused until probe cleanup is verified. Choose a resolution.`,
138
+ options: [
139
+ { value: "retry", label: "Recheck cleanup after repairing process visibility" },
140
+ { value: "help", label: "Show platform-specific resolution approaches" },
141
+ { value: "cancel", label: "Cancel deliberately; keep project files" },
142
+ ],
143
+ });
144
+ if (isCancel(choice) || choice === "cancel")
145
+ return "cancel";
146
+ if (choice === "help") {
147
+ log.info(runtimeCleanupRecoveryHelp());
148
+ continue;
149
+ }
150
+ return "retry";
151
+ }
152
+ }
119
153
  log.info("Cancel stops this command and keeps project files in place. It does not uninstall packages, delete generated files, or change configuration.");
120
154
  if (!allowSwitch) {
121
155
  log.info("Switching runtimes is disabled while resuming because session IDs are runtime-specific.");
@@ -16,6 +16,14 @@ export declare function formatResumeGuidance(executable: "rafi" | "ai-foreman",
16
16
  }>;
17
17
  }>;
18
18
  }): string[];
19
+ /** Save parsed CLI values as argv, never as executable shell text. */
20
+ export declare function savedStartArguments(command: Command, projectDir: string, opts: Record<string, unknown>): string[];
21
+ /** Pure validation shared by interactive start and persisted preparation. */
22
+ export declare function validateStartOptions(opts: Record<string, any>, requireCount: boolean): void;
23
+ /** Validate generated established recovery before reserving execution authority. */
24
+ export declare function validateRecoveryArguments(args: readonly string[], projectDir: string): void;
25
+ /** Validate persisted preparation argv without executing a command or provider. */
26
+ export declare function validatePreparationArguments(args: readonly string[], projectDir: string): string[];
19
27
  export declare function buildStartCommand(): Command;
20
28
  export declare function createQaNonconvergenceHandler(projectDir: string, noninteractive: boolean, planner?: AgentRoleDefaultsV1, observer?: () => RunObserver | undefined, activeRunId?: () => string): (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
21
29
  export declare function createQaReportRecoveryHandler(projectDir: string, noninteractive: boolean, observer?: () => RunObserver | undefined): QaReportRecoveryHandler;