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
@@ -6,8 +6,9 @@ import { recoverableBuildRuns } from "../buildRuns.js";
6
6
  import { WORKFLOW_DB_FILE } from "../workflowDb.js";
7
7
  import { WorkflowReader } from "../workflowReader.js";
8
8
  import { formatWorkflowIssue } from "../outcomes.js";
9
+ import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
9
10
  function fail(message) {
10
- console.error(`foreman: ${message}`);
11
+ console.error(`${error("foreman:", { stream: process.stderr })} ${message}`);
11
12
  process.exit(1);
12
13
  }
13
14
  export function buildStatusCommand() {
@@ -24,6 +25,16 @@ export function runStatus(project) {
24
25
  const workflowPath = join(projectDir, WORKFLOW_DB_FILE);
25
26
  const workflow = existsSync(workflowPath) ? new WorkflowReader(projectDir, workflowPath) : undefined;
26
27
  const activeWorkflows = workflow?.activeRuns() ?? [];
28
+ for (const run of activeWorkflows) {
29
+ const preparation = workflow?.qaPreparation(run.runId);
30
+ if (!preparation || preparation.mode === "legacy")
31
+ continue;
32
+ console.log(`foreman: QA preparation ${preparation.mode} — ${run.runId}`);
33
+ const metrics = workflow.qaPreparationMetrics(run.runId);
34
+ console.log(` admitted ${metrics.approved}; started ${metrics.started}; first reviews ${metrics.firstReviews}; passes ${metrics.firstReviewPasses}; unknown finding causes ${metrics.causes.unknown}`);
35
+ for (const work of preparation.works)
36
+ console.log(` ${work.workId}: ${work.state}; depth ${work.depth ?? "assessment pending"}; contract ${work.digest ?? "unpublished"}${work.detail ? `; ${work.detail}` : ""}`);
37
+ }
27
38
  if (!existsSync(dir) && activeWorkflows.length === 0) {
28
39
  workflow?.close();
29
40
  fail(`no foreman runs found under ${dir}`);
@@ -52,8 +63,8 @@ export function runStatus(project) {
52
63
  const prEvents = records.filter((r) => ["pr-created", "pr-existing", "pr-failed"].includes(String(r.event)));
53
64
  const latestFailure = findLatestGitHubFailure(records);
54
65
  const batchEnd = [...records].reverse().find((r) => r.event === "batch-end");
55
- console.log(`foreman: latest run ${logs[logs.length - 1]}`);
56
- console.log(`foreman: ${steps.length} step record(s), ${escalations.length} escalation(s)`);
66
+ console.log(`${accent("foreman:")} latest run ${sanitizeTerminalText(logs[logs.length - 1] ?? "")}`);
67
+ console.log(`${accent("foreman:")} ${steps.length} step record(s), ${escalations.length} escalation(s)`);
57
68
  if (branchEvents.length || prEvents.length) {
58
69
  const completed = branchEvents.filter((r) => r.event === "branch-complete").length;
59
70
  const issues = branchEvents.filter((r) => r.event === "branch-issue").length;
@@ -61,12 +72,16 @@ export function runStatus(project) {
61
72
  console.log(`foreman: branch mode — ${completed} completed, ${issues} issue(s), ${prs} PR(s)`);
62
73
  }
63
74
  if (batchEnd) {
64
- console.log(`foreman: outcome — ${batchEnd.outcome} (${batchEnd.completed}/${batchEnd.requested})`);
75
+ const outcome = sanitizeTerminalText(String(batchEnd.outcome ?? ""));
76
+ const styledOutcome = /success|complete|pass/i.test(outcome) ? success(outcome)
77
+ : /fail|error/i.test(outcome) ? error(outcome)
78
+ : /warn|partial|blocked/i.test(outcome) ? warning(outcome) : outcome;
79
+ console.log(`${accent("foreman:")} outcome — ${styledOutcome} (${batchEnd.completed}/${batchEnd.requested})`);
65
80
  if (batchEnd.detail)
66
81
  console.log(`foreman: ${batchEnd.detail}`);
67
82
  }
68
83
  else {
69
- console.log("ai-foreman: run is still in progress or did not finish");
84
+ console.log(`${accent("ai-foreman:")} run is still in progress or did not finish`);
70
85
  }
71
86
  if (latestFailure) {
72
87
  console.log(`foreman: latest GitHub failure — ${latestFailure.code}: ${latestFailure.message}`);
@@ -1,4 +1,5 @@
1
1
  import { Command } from "commander";
2
+ import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
5
  import { parse as parseYaml } from "yaml";
@@ -1240,6 +1241,7 @@ export async function cmdPopulateCli(opts) {
1240
1241
  builder = roleBuilder.builder;
1241
1242
  viewer = printEvents(builder.events());
1242
1243
  const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, dir);
1244
+ foreman.setGraphPurpose("ticket-population");
1243
1245
  console.log(`foreman tickets: populating tickets with ${roleBuilder.runtime}`);
1244
1246
  console.log(`foreman tickets: project ${dir}`);
1245
1247
  console.log(`foreman tickets: role ${TICKET_POPULATE_ROLE}`);
@@ -1927,7 +1929,7 @@ export function buildTicketsCommand(options = {}) {
1927
1929
  evidence: opts.evidence,
1928
1930
  lastError: opts.lastError,
1929
1931
  });
1930
- console.log(`foreman tickets: updated ${ticketId}`);
1932
+ console.log(`${accent("foreman tickets:")} updated ${sanitizeTerminalText(ticketId)}`);
1931
1933
  }
1932
1934
  catch (err) {
1933
1935
  fail(String(err instanceof Error ? err.message : err));
@@ -1954,7 +1956,7 @@ export function buildTicketsCommand(options = {}) {
1954
1956
  evidence: opts.evidence,
1955
1957
  validationNotes: opts.validationNotes,
1956
1958
  });
1957
- console.log(`foreman tickets: completed ${ticketId}`);
1959
+ console.log(`${accent("foreman tickets:")} ${success("completed")} ${sanitizeTerminalText(ticketId)}`);
1958
1960
  }
1959
1961
  catch (err) {
1960
1962
  fail(String(err instanceof Error ? err.message : err));
@@ -1979,7 +1981,7 @@ export function buildTicketsCommand(options = {}) {
1979
1981
  actor: opts.actor,
1980
1982
  unblockCriteria: opts.unblockCriteria,
1981
1983
  });
1982
- console.log(`foreman tickets: blocked ${ticketId}`);
1984
+ console.log(`${accent("foreman tickets:")} ${warning("blocked")} ${sanitizeTerminalText(ticketId)}`);
1983
1985
  }
1984
1986
  catch (err) {
1985
1987
  fail(String(err instanceof Error ? err.message : err));
@@ -1998,7 +2000,7 @@ export function buildTicketsCommand(options = {}) {
1998
2000
  summary: opts.summary,
1999
2001
  actor: opts.actor,
2000
2002
  });
2001
- console.log(`foreman tickets: unblocked ${ticketId}`);
2003
+ console.log(`${accent("foreman tickets:")} ${success("unblocked")} ${sanitizeTerminalText(ticketId)}`);
2002
2004
  }
2003
2005
  catch (err) {
2004
2006
  fail(String(err instanceof Error ? err.message : err));
@@ -2188,10 +2190,10 @@ export function buildTicketsCommand(options = {}) {
2188
2190
  try {
2189
2191
  const result = cmdValidate(cwd(opts));
2190
2192
  if (result.issues.length === 0) {
2191
- console.log("foreman tickets: validation passed — all 4 passes clean");
2193
+ console.log(`${accent("foreman tickets:")} ${success("validation passed")} — all 4 passes clean`);
2192
2194
  }
2193
2195
  else {
2194
- console.log(`foreman tickets: ${result.issues.length} issue(s) found:`);
2196
+ console.log(`${accent("foreman tickets:")} ${error(`${result.issues.length} issue(s) found:`)}`);
2195
2197
  console.log(formatValidationIssues(result.issues));
2196
2198
  if (!result.clean)
2197
2199
  process.exit(1);
@@ -2221,7 +2223,7 @@ export function buildTicketsCommand(options = {}) {
2221
2223
  console.log(line);
2222
2224
  return;
2223
2225
  }
2224
- console.log("Batches: none configured. Queue is flat.");
2226
+ console.log(`${accent("Batches:")} none configured. Queue is flat.`);
2225
2227
  const rows = cmdQueue(dir, opts.limit !== undefined ? Number(opts.limit) : undefined);
2226
2228
  if (rows.length === 0) {
2227
2229
  console.log("No remaining tickets.");
@@ -30,7 +30,7 @@ export declare function parseContinuityDelta(text: string): {
30
30
  };
31
31
  export declare function mergeContinuityDeltas(deltas: readonly ContinuityDelta[]): ContinuityDelta;
32
32
  export declare function baselineContinuityDelta(nextAction?: string): ContinuityDelta;
33
- export declare function continuityInstruction(): string;
33
+ export declare function continuityInstruction(role?: "builder" | "qa"): string;
34
34
  export interface ContinuityAdapterOptions {
35
35
  adapter: BuilderAdapter;
36
36
  projectDir: string;
@@ -61,16 +61,30 @@ export interface ContinuityAdapterOptions {
61
61
  export declare class ContinuityAdapter implements BuilderAdapter {
62
62
  private readonly options;
63
63
  private adapter;
64
+ contractCapabilities(): {
65
+ sameSessionAcceptance: boolean;
66
+ nativeCompactionBarrier: boolean;
67
+ };
68
+ private contractEnforcing;
69
+ enableContractEnforcement(): void;
70
+ contractCompactionSequence(): number;
71
+ acceptContractDelivery(sequence: number): void;
64
72
  private readonly queue;
65
73
  private sourcePump?;
66
74
  private recoveryLeasePending;
75
+ private validatingFreshRecovery;
76
+ private readonly buildAuthority?;
67
77
  constructor(options: ContinuityAdapterOptions);
78
+ /** Establish an explicitly requested fresh owner before context probes or work. */
79
+ validateFreshRecovery(): Promise<void>;
68
80
  get agent(): "claude" | "codex";
69
81
  sendTurn(instruction: string, policy?: {
70
82
  handback?: boolean;
71
83
  responseOnly?: boolean;
72
84
  logicalActionId?: string;
85
+ purpose?: import("./providerPhase.js").ProviderTurnPurpose;
73
86
  }): Promise<TurnResult>;
87
+ private sendTurnOwned;
74
88
  observeEvents(listener: (event: import("./adapters/types.js").BuilderEvent) => void): () => void;
75
89
  acceptHandbackTurn(turn: TurnResult): void;
76
90
  sessionId(): string | undefined;
@@ -86,6 +100,7 @@ export declare class ContinuityAdapter implements BuilderAdapter {
86
100
  restoreNativeCompactions(compactions: NativeCompaction[]): void;
87
101
  contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
88
102
  contextUsage(): Promise<ContextUsage | undefined>;
103
+ graphRuntimeSettings(): Pick<import("./adapters/types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
89
104
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
90
105
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
91
106
  events(): AsyncIterable<import("./adapters/types.js").BuilderEvent>;
@@ -1,3 +1,5 @@
1
+ import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session.js";
2
+ import { localBuildAuthority } from "./buildAdmission.js";
1
3
  import { randomUUID, createHash } from "node:crypto";
2
4
  import { BuilderEventQueue } from "./activity.js";
3
5
  import { providerSessionKey } from "./sessionIdentity.js";
@@ -96,10 +98,11 @@ export function mergeContinuityDeltas(deltas) {
96
98
  export function baselineContinuityDelta(nextAction = "Await the first role action.") {
97
99
  return { version: 1, decisions: [], constraints: [], discoveries: [], completedActions: [], evidence: [], failures: [], blockers: [], openWork: [], nextAction };
98
100
  }
99
- export function continuityInstruction() {
101
+ export function continuityInstruction(role) {
100
102
  return [
101
103
  "Before your final STEP_STATUS line (or as the final line when STEP_STATUS is not requested), emit exactly one single-line continuity record:",
102
104
  `${CONTINUITY_MARKER} {"version":1,"decisions":[],"constraints":[],"discoveries":[],"completedActions":[],"evidence":[],"failures":[],"blockers":[],"openWork":[],"nextAction":"specific next action"}`,
105
+ ...(role === "qa" ? ["QA protocol: this continuity record is required even when the review requests only a report or status. Put it before RAFI_QA_FAILURE_REPORT_START when returning a failure envelope, outside the report JSON. Rafi strips the record before validating the report, so keep the report envelope immediately followed by STEP_STATUS."] : []),
103
106
  "Keep it concise and cumulative for facts learned this turn. Do not include credentials, hidden reasoning, or raw transcripts.",
104
107
  ].join("\n");
105
108
  }
@@ -107,12 +110,20 @@ export function continuityInstruction() {
107
110
  export class ContinuityAdapter {
108
111
  options;
109
112
  adapter;
113
+ contractCapabilities() { return this.adapter.contractCapabilities?.() ?? { sameSessionAcceptance: false, nativeCompactionBarrier: false }; }
114
+ contractEnforcing = false;
115
+ enableContractEnforcement() { this.contractEnforcing = true; this.adapter.enableContractEnforcement?.(); }
116
+ contractCompactionSequence() { return this.adapter.contractCompactionSequence?.() ?? 0; }
117
+ acceptContractDelivery(sequence) { this.adapter.acceptContractDelivery?.(sequence); }
110
118
  queue = new BuilderEventQueue();
111
119
  sourcePump;
112
120
  recoveryLeasePending;
121
+ validatingFreshRecovery = false;
122
+ buildAuthority;
113
123
  constructor(options) {
114
124
  this.options = options;
115
125
  this.adapter = options.adapter;
126
+ this.buildAuthority = localBuildAuthority(options.projectDir);
116
127
  this.recoveryLeasePending = Boolean(options.replaceRecoveryLeaseAfterCheckpoint);
117
128
  const db = new WorkflowDb(options.projectDir);
118
129
  try {
@@ -130,8 +141,42 @@ export class ContinuityAdapter {
130
141
  }
131
142
  this.pumpEvents();
132
143
  }
144
+ /** Establish an explicitly requested fresh owner before context probes or work. */
145
+ async validateFreshRecovery() {
146
+ if (!this.recoveryLeasePending)
147
+ return;
148
+ const db = new WorkflowDb(this.options.projectDir);
149
+ let owner;
150
+ try {
151
+ owner = db.roleMutationLease(this.options.runId, this.options.role);
152
+ }
153
+ finally {
154
+ db.close();
155
+ }
156
+ const current = this.adapter.sessionRef?.();
157
+ if (!owner || (current && owner.sessionKey === providerSessionKey(current))) {
158
+ this.recoveryLeasePending = false;
159
+ return;
160
+ }
161
+ this.validatingFreshRecovery = true;
162
+ try {
163
+ const result = await this.sendTurn("Fresh recovery validation only. Do not use tools, change files, implement work, or request a handoff. Acknowledge this new session and emit the required continuity record using only the supplied instructions.", { responseOnly: true });
164
+ if (result.isError || result.failure || this.recoveryLeasePending)
165
+ throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "fresh recovery did not establish a valid continuity checkpoint");
166
+ }
167
+ catch (error) {
168
+ await this.close().catch(() => { });
169
+ throw error;
170
+ }
171
+ finally {
172
+ this.validatingFreshRecovery = false;
173
+ }
174
+ }
133
175
  get agent() { return this.adapter.agent; }
134
176
  async sendTurn(instruction, policy) {
177
+ return withGraphSessionAccess(this.options.projectDir, this.adapter, () => this.sendTurnOwned(instruction, policy));
178
+ }
179
+ async sendTurnOwned(instruction, policy) {
135
180
  const db = new WorkflowDb(this.options.projectDir);
136
181
  try {
137
182
  db.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "turn_started", payload: { role: this.options.role, instructionDigest: sha(instruction), instructionBytes: Buffer.byteLength(instruction) }, authoritativeStateRevision: this.revision() });
@@ -139,12 +184,16 @@ export class ContinuityAdapter {
139
184
  finally {
140
185
  db.close();
141
186
  }
142
- const providerInstruction = `${instruction}\n\n${continuityInstruction()}`;
187
+ const providerInstruction = `${instruction}\n\n${continuityInstruction(this.options.role)}`;
143
188
  const journal = new WorkflowDb(this.options.projectDir);
144
189
  const dispatchId = `dispatch:${this.options.runId}:${this.options.role}:${randomUUID()}`;
145
190
  let original;
146
191
  try {
147
192
  journal.atomic(() => {
193
+ if (this.buildAuthority)
194
+ journal.assertBuildAdmission(this.buildAuthority);
195
+ else if (journal.buildAdmission())
196
+ throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "dispatch requires original build admission authority");
148
197
  if (process.send && process.env.RAFI_BUILD_WORKER_RUN === this.options.runId) {
149
198
  const supervisor = journal.supervisorState(this.options.runId);
150
199
  if (!process.connected || supervisor?.status !== "running" || supervisor.workerGeneration !== Number(process.env.RAFI_BUILD_WORKER_GENERATION))
@@ -153,7 +202,7 @@ export class ContinuityAdapter {
153
202
  const owner = journal.roleMutationLease(this.options.runId, this.options.role);
154
203
  const ref = this.adapter.sessionRef?.();
155
204
  const session = ref?.sessionId ?? this.adapter.sessionId();
156
- if (owner && session && (owner.providerSessionId !== session || (owner.sessionKey && ref && owner.sessionKey !== providerSessionKey(ref)))) {
205
+ if (!(this.validatingFreshRecovery && this.recoveryLeasePending && policy?.responseOnly) && owner && session && (owner.providerSessionId !== session || (owner.sessionKey && ref && owner.sessionKey !== providerSessionKey(ref)))) {
157
206
  throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "provider session no longer owns role dispatch authority");
158
207
  }
159
208
  if (journal.unresolvedRoleDispatches(this.options.runId, this.options.role).length)
@@ -162,7 +211,7 @@ export class ContinuityAdapter {
162
211
  journal.updateOperation(dispatchId, "in_progress");
163
212
  });
164
213
  try {
165
- original = await this.adapter.sendTurn(providerInstruction, policy);
214
+ original = await dispatchWithGraphAccess(this.options.projectDir, this.adapter, providerInstruction, policy, (text, p) => this.adapter.sendTurn(text, p));
166
215
  }
167
216
  catch (error) {
168
217
  journal.updateOperation(dispatchId, "uncertain", { error: String(error) });
@@ -203,7 +252,7 @@ export class ContinuityAdapter {
203
252
  if (parsed.delta && !original.isError && !original.failure) {
204
253
  this.publish(parsed.delta, "turn_completed", original);
205
254
  this.moveRecoveryLeaseAfterCheckpoint();
206
- if (this.options.durableSingleTurn) {
255
+ if (this.options.durableSingleTurn || this.validatingFreshRecovery) {
207
256
  return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
208
257
  rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
209
258
  }
@@ -213,12 +262,12 @@ export class ContinuityAdapter {
213
262
  return this.sendTurn([
214
263
  "Continue the frozen action after the accepted Rafi handoff. Do not repeat completed side effects.",
215
264
  instruction,
216
- ].join("\n\n"));
265
+ ].join("\n\n"), policy);
217
266
  }
218
267
  return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
219
268
  rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
220
269
  }
221
- if (this.options.durableSingleTurn) {
270
+ if (this.options.durableSingleTurn || (this.validatingFreshRecovery && this.recoveryLeasePending && policy?.responseOnly)) {
222
271
  const invalidDb = new WorkflowDb(this.options.projectDir);
223
272
  try {
224
273
  invalidDb.appendContinuityEvent({ runId: this.options.runId, role: "host", kind: "continuity_invalid", payload: { problems: parsed.error?.problems }, authoritativeStateRevision: this.revision() });
@@ -226,7 +275,7 @@ export class ContinuityAdapter {
226
275
  finally {
227
276
  invalidDb.close();
228
277
  }
229
- return { ...original, isError: true, text: `QA continuity record was invalid: ${parsed.error?.problems.join("; ") ?? "unknown error"}`,
278
+ return { ...original, isError: true, continuityErrors: parsed.error?.problems ?? ["unknown continuity error"], text: `QA continuity record was invalid: ${parsed.error?.problems.join("; ") ?? "unknown error"}`,
230
279
  hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
231
280
  rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
232
281
  }
@@ -240,12 +289,12 @@ export class ContinuityAdapter {
240
289
  finally {
241
290
  budget.close();
242
291
  }
243
- const repair = await this.adapter.sendTurn([
292
+ const repair = await dispatchWithGraphAccess(this.options.projectDir, this.adapter, [
244
293
  "Continuity protocol repair only. Do not run tools, repeat work, or change files.",
245
294
  `Your prior turn's continuity record was invalid: ${parsed.error?.problems.join("; ")}.`,
246
295
  continuityInstruction(),
247
296
  "Return only the continuity record.",
248
- ].join("\n"), { ...policy, responseOnly: true });
297
+ ].join("\n"), { ...policy, purpose: "response-repair", responseOnly: true }, (text, p) => this.adapter.sendTurn(text, p));
249
298
  const repaired = parseContinuityDelta(repair.text);
250
299
  if (repaired.delta && !repair.isError && !repair.failure) {
251
300
  this.publish(repaired.delta, "turn_completed_after_repair", original);
@@ -253,7 +302,7 @@ export class ContinuityAdapter {
253
302
  const successor = await this.options.handleHandoffRequest?.(original.text, this.adapter, instruction);
254
303
  if (successor && successor !== this.adapter) {
255
304
  await this.adoptValidatedSuccessor(successor);
256
- return this.sendTurn(["Continue the frozen action after the accepted Rafi handoff. Do not repeat completed side effects.", instruction].join("\n\n"));
305
+ return this.sendTurn(["Continue the frozen action after the accepted Rafi handoff. Do not repeat completed side effects.", instruction].join("\n\n"), policy);
257
306
  }
258
307
  return { ...original, text: parsed.cleanText, hostInstruction: instruction, providerInstruction: original.providerInstruction ?? providerInstruction,
259
308
  rawResponse: original.rawResponse ?? original.text, cleanedResponse: parsed.cleanText };
@@ -290,7 +339,7 @@ export class ContinuityAdapter {
290
339
  if (!this.options.createSuccessor)
291
340
  throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "continuity repair failed in the original session and no validated successor is configured");
292
341
  const successor = await this.options.createSuccessor(handoff);
293
- const accepted = await successor.sendTurn(`${handoff}\n\nReply with HANDOFF_ACCEPTED on the first line, then ${continuityInstruction()}`);
342
+ const accepted = await dispatchWithGraphAccess(this.options.projectDir, successor, `${handoff}\n\nReply with HANDOFF_ACCEPTED on the first line, then ${continuityInstruction()}`, { purpose: "contract-acceptance", responseOnly: true }, (text, p) => successor.sendTurn(text, p));
294
343
  const successorDelta = parseContinuityDelta(accepted.text);
295
344
  const firstAcceptanceLine = accepted.text.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
296
345
  if (firstAcceptanceLine !== "HANDOFF_ACCEPTED" || !successorDelta.delta || !successor.sessionId()) {
@@ -338,12 +387,19 @@ export class ContinuityAdapter {
338
387
  restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
339
388
  contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
340
389
  contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
390
+ graphRuntimeSettings() { return this.adapter.graphRuntimeSettings?.(); }
341
391
  sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
342
392
  switchSettings(settings) { return this.adapter.switchSettings ? this.adapter.switchSettings(settings) : Promise.resolve({ ok: false, error: "provider adapter does not support settings changes" }); }
343
393
  events() { return this.queue; }
344
394
  async close() { await this.adapter.close(); await this.sourcePump?.catch(() => { }); this.queue.close(); }
345
395
  /** Keep the observable wrapper and event stream stable while moving to an accepted successor. */
346
396
  async adoptValidatedSuccessor(successor) {
397
+ if (this.contractEnforcing) {
398
+ successor.enableContractEnforcement?.();
399
+ if (successor !== this.adapter)
400
+ await successor.close().catch(() => { });
401
+ throw new ContinuityRecoveryRequiredError(this.options.runId, this.options.role, "Replacement Builder requires host contract delivery before any continuation; completed turn must be reconciled without automatic replay");
402
+ }
347
403
  if (successor === this.adapter)
348
404
  return;
349
405
  const prior = this.adapter;
package/dist/foreman.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { GraphPurpose } from "rafi-spec";
1
2
  import type { BuilderAdapter, PermissionHandler, PermissionRequest, TurnResult } from "./adapters/types.js";
2
3
  import type { PermissionPolicy } from "./permissions/policy.js";
3
4
  import { type AnsweredProviderQuestion } from "./providerQuestions.js";
@@ -85,19 +86,25 @@ export declare class Foreman {
85
86
  /** Exact durable build-run scope. Observability is never an identity authority. */
86
87
  private readonly qaRunId;
87
88
  private readonly continueIndependentTickets;
89
+ private readonly builderWorktree?;
90
+ private readonly decisionPrompt?;
91
+ private graphPurpose;
92
+ setGraphPurpose(purpose: GraphPurpose): void;
88
93
  private readonly ticketsEnabled;
89
94
  private readonly notificationsEnabled;
90
95
  private readonly terminalBellEnabled;
91
96
  private readonly qaStream;
92
97
  private builderWorkSessions;
93
98
  private currentTicketId?;
99
+ private preflightPlan;
100
+ approvedPlanText(): string;
94
101
  constructor(builder: BuilderAdapter, log: Log, notifications?: boolean | ForemanNotificationOptions, qaEnabled?: boolean, qaMaxCycles?: number, projectDir?: string | undefined,
95
102
  /** @deprecated QA must be created by qaFactory in a disposable snapshot. */
96
103
  _deprecatedSameSessionReviewer?: BuilderAdapter, qaFactory?: ((cwd: string, sessionId?: string) => Promise<QaSessionHandle>) | undefined, qaSessionStrategy?: SessionStrategy, builderFactory?: ((cwd: string, sessionId?: string) => Promise<BuilderAdapter>) | undefined, builderSessionStrategy?: SessionStrategy, qaNonconvergence?: ((context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>) | undefined, beforeBuilderTurn?: ((adapter: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter>) | undefined, builderSessionBoundary?: ((adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy) => Promise<BuilderAdapter>) | undefined, qaSessionBoundary?: ((handle: QaSessionHandle, frozenAction: string, strategy: SessionStrategy, cwd: string, recovery?: QaSessionBoundaryRecovery) => Promise<QaSessionBoundaryResult>) | undefined, observeQaNativeCompactions?: ((adapter: BuilderAdapter) => Promise<void>) | undefined,
97
104
  /** Persist a Builder's provider-native event immediately after every turn. */
98
105
  observeBuilderNativeCompactions?: ((adapter: BuilderAdapter) => Promise<void>) | undefined, observer?: RunObserver | undefined, qaRuntimeContext?: unknown | undefined, qaContinuityManaged?: boolean, qaReportRecovery?: QaReportRecoveryHandler | undefined, qaResumedRecovery?: QaRecoveryPacket | undefined,
99
106
  /** Exact durable build-run scope. Observability is never an identity authority. */
100
- qaRunId?: string, continueIndependentTickets?: boolean, initialTicketId?: string);
107
+ qaRunId?: string, continueIndependentTickets?: boolean, initialTicketId?: string, builderWorktree?: string | undefined, decisionPrompt?: ((decision: import("rafi-spec").PendingHumanDecision) => Promise<string | undefined>) | undefined);
101
108
  private waitForUserInput;
102
109
  /**
103
110
  * Send one instruction and resolve any needs_input exchanges before returning.
@@ -106,8 +113,9 @@ export declare class Foreman {
106
113
  private doTurn;
107
114
  private prepareBuilderBoundary;
108
115
  private doTurnWith;
116
+ private doTurnWithOwned;
109
117
  /** Send a planning turn and return the builder's response text. Does not count toward steps. */
110
- runPreflight(n: number, ticketsContent?: string, preferredTicketId?: string): Promise<string>;
118
+ runPreflight(n: number, ticketsContent?: string, preferredTicketId?: string, executionTickets?: readonly string[]): Promise<string>;
111
119
  /** Send user feedback on the plan; builder responds with a revised list. Does not count toward steps. */
112
120
  sendPreflightFeedback(feedback: string): Promise<void>;
113
121
  /** Send one custom instruction through the same needs_input loop as a batch turn. */
@@ -140,7 +148,7 @@ export declare class Foreman {
140
148
  passCertificateId?: string;
141
149
  sourceStateDigest?: string;
142
150
  }>;
143
- /** Complete only the pending QA boundary and its tracker transition. No Builder work turn is sent. */
151
+ /** Resume the pending boundary, including authorized waiting guidance, then complete its tracker transition. */
144
152
  completePendingQaRecovery(ticketId: string): Promise<{
145
153
  outcome: "passed" | "blocked" | "needs-human" | "waived";
146
154
  detail?: string;
@@ -153,5 +161,5 @@ export declare class Foreman {
153
161
  builderAdapter(): BuilderAdapter;
154
162
  close(): Promise<void>;
155
163
  private ticketForQa;
156
- runBatch(n: number, trackerPath?: string, onTicketStart?: (ticketId: string) => void | Promise<void>, preferredTicketId?: string): Promise<BatchResult>;
164
+ runBatch(n: number, trackerPath?: string, onTicketStart?: (ticketId: string) => void | Promise<void>, preferredTicketId?: string, recoveryTickets?: readonly string[]): Promise<BatchResult>;
157
165
  }