ai-foreman 1.7.20 → 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 (189) hide show
  1. package/README.md +12 -2
  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 +90 -4
  20. package/dist/buildAdmission.js +2 -0
  21. package/dist/buildAssignment.d.ts +33 -0
  22. package/dist/buildAssignment.js +215 -0
  23. package/dist/buildInterventions.d.ts +90 -0
  24. package/dist/buildInterventions.js +334 -0
  25. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  26. package/dist/buildOwnershipReconciliation.js +41 -0
  27. package/dist/buildOwnershipRepair.d.ts +33 -0
  28. package/dist/buildOwnershipRepair.js +111 -0
  29. package/dist/buildRuns.d.ts +6 -0
  30. package/dist/buildRuns.js +23 -17
  31. package/dist/buildSyntheticWork.d.ts +2 -0
  32. package/dist/buildSyntheticWork.js +30 -0
  33. package/dist/buildWorkAdmission.d.ts +17 -0
  34. package/dist/buildWorkAdmission.js +57 -0
  35. package/dist/buildWorkAuthorization.d.ts +2 -0
  36. package/dist/buildWorkAuthorization.js +23 -0
  37. package/dist/buildWorkContext.d.ts +3 -0
  38. package/dist/buildWorkContext.js +6 -0
  39. package/dist/buildWorkMigration.d.ts +9 -0
  40. package/dist/buildWorkMigration.js +206 -0
  41. package/dist/buildWorkReconciliation.d.ts +12 -0
  42. package/dist/buildWorkReconciliation.js +70 -0
  43. package/dist/builderGuidanceFollowup.d.ts +15 -0
  44. package/dist/builderGuidanceFollowup.js +34 -0
  45. package/dist/cli/doctor.js +3 -2
  46. package/dist/cli/graph.d.ts +2 -0
  47. package/dist/cli/graph.js +75 -0
  48. package/dist/cli/manager.d.ts +8 -0
  49. package/dist/cli/manager.js +234 -12
  50. package/dist/cli/recovery.js +3 -2
  51. package/dist/cli/runtimeAuthPrompt.js +28 -8
  52. package/dist/cli/start.js +152 -48
  53. package/dist/cli/status.js +20 -5
  54. package/dist/cli/tickets.js +9 -7
  55. package/dist/continuity.d.ts +11 -0
  56. package/dist/continuity.js +22 -6
  57. package/dist/foreman.d.ts +10 -2
  58. package/dist/foreman.js +330 -39
  59. package/dist/graph/boundary.d.ts +15 -0
  60. package/dist/graph/boundary.js +50 -0
  61. package/dist/graph/bridge.d.ts +9 -0
  62. package/dist/graph/bridge.js +146 -0
  63. package/dist/graph/bridge.py +205 -0
  64. package/dist/graph/config.d.ts +11 -0
  65. package/dist/graph/config.js +40 -0
  66. package/dist/graph/corpus.d.ts +16 -0
  67. package/dist/graph/corpus.js +188 -0
  68. package/dist/graph/derived.d.ts +17 -0
  69. package/dist/graph/derived.js +73 -0
  70. package/dist/graph/host.d.ts +4 -0
  71. package/dist/graph/host.js +160 -0
  72. package/dist/graph/install.d.ts +3 -0
  73. package/dist/graph/install.js +50 -0
  74. package/dist/graph/lifecycle.d.ts +11 -0
  75. package/dist/graph/lifecycle.js +124 -0
  76. package/dist/graph/maintenance.d.ts +43 -0
  77. package/dist/graph/maintenance.js +301 -0
  78. package/dist/graph/manager.d.ts +9 -0
  79. package/dist/graph/manager.js +92 -0
  80. package/dist/graph/preparation.d.ts +41 -0
  81. package/dist/graph/preparation.js +43 -0
  82. package/dist/graph/read.d.ts +27 -0
  83. package/dist/graph/read.js +152 -0
  84. package/dist/graph/semantic.d.ts +17 -0
  85. package/dist/graph/semantic.js +27 -0
  86. package/dist/graph/session.d.ts +8 -0
  87. package/dist/graph/session.js +92 -0
  88. package/dist/graph/sources.d.ts +4 -0
  89. package/dist/graph/sources.js +11 -0
  90. package/dist/graph/storage.d.ts +38 -0
  91. package/dist/graph/storage.js +159 -0
  92. package/dist/graph/turn.d.ts +17 -0
  93. package/dist/graph/turn.js +360 -0
  94. package/dist/graph/util.d.ts +5 -0
  95. package/dist/graph/util.js +51 -0
  96. package/dist/handoffs.d.ts +1 -0
  97. package/dist/handoffs.js +5 -1
  98. package/dist/humanDecision.d.ts +21 -2
  99. package/dist/humanDecision.js +169 -11
  100. package/dist/index.js +2 -0
  101. package/dist/installOwnership.d.ts +46 -0
  102. package/dist/installOwnership.js +440 -0
  103. package/dist/linuxProbeInventory.d.ts +12 -0
  104. package/dist/linuxProbeInventory.js +48 -0
  105. package/dist/log.d.ts +1 -1
  106. package/dist/log.js +7 -1
  107. package/dist/managerEvidence.d.ts +25 -0
  108. package/dist/managerEvidence.js +326 -0
  109. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  110. package/dist/managerEvidenceArtifacts.js +118 -0
  111. package/dist/managerPacket.d.ts +2 -0
  112. package/dist/managerPacket.js +23 -0
  113. package/dist/markers.d.ts +1 -1
  114. package/dist/markers.js +2 -2
  115. package/dist/observability.js +1 -1
  116. package/dist/processIdentity.js +7 -0
  117. package/dist/providerPhase.d.ts +13 -0
  118. package/dist/providerPhase.js +26 -0
  119. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  120. package/dist/qaAuthorizedDisposition.js +26 -0
  121. package/dist/qaBuildGate.d.ts +4 -0
  122. package/dist/qaBuildGate.js +216 -0
  123. package/dist/qaBuilderCoverage.d.ts +5 -0
  124. package/dist/qaBuilderCoverage.js +78 -0
  125. package/dist/qaContractCoverage.d.ts +10 -0
  126. package/dist/qaContractCoverage.js +55 -0
  127. package/dist/qaContractDelivery.d.ts +23 -0
  128. package/dist/qaContractDelivery.js +145 -0
  129. package/dist/qaContractFreshness.d.ts +15 -0
  130. package/dist/qaContractFreshness.js +36 -0
  131. package/dist/qaDeliveryJournal.d.ts +10 -0
  132. package/dist/qaDeliveryJournal.js +16 -1
  133. package/dist/qaEffectiveConfig.d.ts +20 -0
  134. package/dist/qaEffectiveConfig.js +40 -0
  135. package/dist/qaEvidenceReader.d.ts +14 -0
  136. package/dist/qaEvidenceReader.js +176 -0
  137. package/dist/qaFailureDelivery.d.ts +2 -0
  138. package/dist/qaFailureDelivery.js +107 -10
  139. package/dist/qaHandbackMigration.d.ts +1 -1
  140. package/dist/qaHandbackMigration.js +3 -3
  141. package/dist/qaPreparation.d.ts +26 -0
  142. package/dist/qaPreparation.js +150 -0
  143. package/dist/qaPreparationInputs.d.ts +8 -0
  144. package/dist/qaPreparationInputs.js +47 -0
  145. package/dist/qaPreparationMetrics.d.ts +145 -0
  146. package/dist/qaPreparationMetrics.js +120 -0
  147. package/dist/qaPreparationPolicy.d.ts +5 -0
  148. package/dist/qaPreparationPolicy.js +51 -0
  149. package/dist/qaPreparationStore.d.ts +157 -0
  150. package/dist/qaPreparationStore.js +412 -0
  151. package/dist/qaProtocolV2.d.ts +28 -0
  152. package/dist/qaProtocolV2.js +12 -0
  153. package/dist/qaReview.d.ts +6 -0
  154. package/dist/qaReview.js +234 -24
  155. package/dist/qaTimeline.d.ts +18 -0
  156. package/dist/qaTimeline.js +123 -0
  157. package/dist/qaVerificationContract.d.ts +17 -0
  158. package/dist/qaVerificationContract.js +236 -0
  159. package/dist/readinessCleanup.js +12 -5
  160. package/dist/recoveryGuidance.d.ts +9 -0
  161. package/dist/recoveryGuidance.js +13 -0
  162. package/dist/runtimeAuth.d.ts +1 -0
  163. package/dist/runtimeAuth.js +5 -1
  164. package/dist/runtimeReadiness.d.ts +1 -0
  165. package/dist/runtimeReadiness.js +13 -1
  166. package/dist/sessionIdentity.d.ts +2 -0
  167. package/dist/sessionIdentity.js +14 -0
  168. package/dist/sessionLifecycle.js +3 -3
  169. package/dist/sources/sourceRegistry.js +7 -0
  170. package/dist/stateTransfer.js +50 -1
  171. package/dist/statusReporter.d.ts +1 -0
  172. package/dist/statusReporter.js +1 -0
  173. package/dist/supervisedStart.js +18 -8
  174. package/dist/terminalStyle.d.ts +21 -0
  175. package/dist/terminalStyle.js +50 -0
  176. package/dist/ticketPopulation.d.ts +1 -0
  177. package/dist/ticketPopulation.js +5 -1
  178. package/dist/tickets/commands.js +2 -2
  179. package/dist/tickets/queue.js +1 -1
  180. package/dist/tickets/ticketSchema.d.ts +198 -0
  181. package/dist/tickets/ticketSchema.js +2 -0
  182. package/dist/workflowDb.d.ts +51 -5
  183. package/dist/workflowDb.js +338 -7
  184. package/dist/workflowReader.d.ts +109 -2
  185. package/dist/workflowReader.js +48 -4
  186. package/package.json +84 -7
  187. package/scripts/copy-graph-assets.mjs +3 -0
  188. package/scripts/graph-benchmark.mjs +20 -0
  189. package/scripts/graph-evaluation.mjs +85 -0
@@ -1,3 +1,4 @@
1
+ export { withGraphDerivedAccess } from "./graph/derived.js";
1
2
  import { type PermissionConfig } from "./config.js";
2
3
  import { Log } from "./log.js";
3
4
  import { type StepStatus } from "./foreman.js";
@@ -6,10 +7,13 @@ import type { AnsweredProviderQuestion } from "./providerQuestions.js";
6
7
  import { type RoleBundle } from "./roles.js";
7
8
  import type { AgentRuntime } from "./runtimeAuth.js";
8
9
  import type { AgentRoleDefaultsV1, ProviderSessionRefV1 } from "rafi-spec";
10
+ import type { GraphPurpose } from "rafi-spec";
9
11
  export type { EffortLevel } from "./adapters/types.js";
10
12
  export declare const VALID_EFFORT: readonly EffortLevel[];
11
13
  export interface RoleBuilderOptions {
14
+ graphPurpose?: GraphPurpose;
12
15
  projectDir: string;
16
+ configRoot?: string;
13
17
  role: string;
14
18
  agent?: string;
15
19
  model?: string;
@@ -21,6 +25,7 @@ export interface RoleBuilderOptions {
21
25
  log?: Log;
22
26
  permissionConfig?: PermissionConfig;
23
27
  extraSkills?: string[];
28
+ preloadedSkillContent?: BuilderAdapterOptions["preloadedSkillContent"];
24
29
  sandboxMode?: BuilderAdapterOptions["sandboxMode"];
25
30
  /** Disable recovery-session persistence for isolated, non-recoverable roles such as Manager. */
26
31
  persistSessionBindings?: boolean;
@@ -33,6 +38,7 @@ export interface RoleBuilderOptions {
33
38
  onProviderQuestion?: (request: import("./adapters/types.js").PermissionRequest) => void;
34
39
  }
35
40
  export interface RoleBuilder {
41
+ graphRoot?: string;
36
42
  builder: BuilderAdapter;
37
43
  runtime: AgentRuntime;
38
44
  model?: string;
@@ -65,5 +71,6 @@ export declare function makeLogPath(projectDir: string, label: string): string;
65
71
  export declare function mergeSkills(roleSkills: readonly string[], extraSkills?: readonly string[]): string[];
66
72
  export declare function readOnlyPermissionConfig(): PermissionConfig;
67
73
  export declare function createRoleBuilder(opts: RoleBuilderOptions): Promise<RoleBuilder>;
74
+ export declare function sendRoleGraphTurn(role: RoleBuilder, instruction: string, purpose: GraphPurpose, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
68
75
  export declare function readRoleDefaultsForExecution(projectDir: string, role: string): AgentRoleDefaultsV1 | undefined;
69
76
  export declare function runRoleInstruction(opts: RoleInstructionRunOptions): Promise<RoleInstructionRunResult>;
package/dist/agentRun.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { withGraphDerivedAccess } from "./graph/derived.js";
1
2
  import { existsSync, readFileSync, statSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { loadConfig, DEFAULT_CONFIG } from "./config.js";
@@ -15,6 +16,7 @@ import { resolveAgentForProject } from "./cli/runtimeSelection.js";
15
16
  import { parse as parseYaml } from "yaml";
16
17
  import { captureWorkspaceIdentity, createProviderSessionRef, resolveUniqueSessionBinding } from "./sessionIdentity.js";
17
18
  import { WorkflowDb } from "./workflowDb.js";
19
+ import { graphContext, sendGraphTurn } from "./graph/turn.js";
18
20
  export const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
19
21
  export function assertEffortLevel(effort) {
20
22
  if (effort && !VALID_EFFORT.includes(effort)) {
@@ -107,13 +109,14 @@ export async function createRoleBuilder(opts) {
107
109
  if (!existsSync(opts.projectDir)) {
108
110
  throw new Error(`project directory not found: ${opts.projectDir}`);
109
111
  }
110
- const saved = readRoleDefaultsForExecution(opts.projectDir, opts.role);
112
+ const executionRoot = opts.configRoot ?? opts.projectDir;
113
+ const saved = readRoleDefaultsForExecution(executionRoot, opts.role);
111
114
  if (opts.resumeSessionRef && opts.resumeSessionId && opts.resumeSessionRef.sessionId !== opts.resumeSessionId) {
112
115
  throw new Error("resume session ID does not match the supplied location-scoped reference");
113
116
  }
114
117
  let requestedResumeRef = opts.resumeSessionRef;
115
118
  if (!requestedResumeRef && opts.resumeSessionId) {
116
- const bindingDb = new WorkflowDb(opts.projectDir);
119
+ const bindingDb = new WorkflowDb(executionRoot);
117
120
  try {
118
121
  requestedResumeRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(opts.resumeSessionId), opts.resumeSessionId);
119
122
  }
@@ -124,19 +127,19 @@ export async function createRoleBuilder(opts) {
124
127
  const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
125
128
  const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
126
129
  assertEffortLevel(effectiveEffort);
127
- let runtime = resolveAgentForProject(opts.projectDir, opts.agent ?? requestedResumeRef?.provider ?? saved?.make);
130
+ let runtime = resolveAgentForProject(executionRoot, opts.agent ?? requestedResumeRef?.provider ?? saved?.make);
128
131
  if (requestedResumeRef && requestedResumeRef.provider !== runtime) {
129
132
  throw new Error(`session ${requestedResumeRef.sessionId} belongs to ${requestedResumeRef.provider}, but ${runtime} was selected`);
130
133
  }
131
134
  if (requestedResumeRef && requestedResumeRef.role !== configurableRole(opts.role)) {
132
135
  throw new Error(`session ${requestedResumeRef.sessionId} belongs to role ${requestedResumeRef.role}, not ${configurableRole(opts.role)}`);
133
136
  }
134
- const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
135
- const log = opts.log ?? new Log(makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-")));
136
- const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.projectDir });
137
+ const config = loadConfig(join(executionRoot, "foreman.yaml"));
138
+ const log = opts.log ?? new Log(makeLogPath(executionRoot, opts.label.replace(/\s+/g, "-")));
139
+ const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.configRoot ?? opts.projectDir });
137
140
  const initialModel = opts.model ?? (saved?.model !== "default" ? saved?.model : undefined) ?? roleBundle.model ?? undefined;
138
141
  const effort = effectiveEffort ?? roleBundle.effort ?? undefined;
139
- const ready = await ensureRuntimeReadyForCommand(opts.projectDir, runtime, {
142
+ const ready = await ensureRuntimeReadyForCommand(executionRoot, runtime, {
140
143
  label: opts.label,
141
144
  yes: Boolean(opts.yes),
142
145
  allowSwitch: requestedResumeRef || opts.resumeSessionId ? false : opts.allowSwitch,
@@ -151,7 +154,7 @@ export async function createRoleBuilder(opts) {
151
154
  const makeAdapter = async (nextRuntime, resumeSessionId, resumeSessionRef) => {
152
155
  let scopedRef = resumeSessionRef;
153
156
  if (!scopedRef && resumeSessionId) {
154
- const bindingDb = new WorkflowDb(opts.projectDir);
157
+ const bindingDb = new WorkflowDb(executionRoot);
155
158
  try {
156
159
  scopedRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(resumeSessionId), resumeSessionId);
157
160
  }
@@ -165,7 +168,7 @@ export async function createRoleBuilder(opts) {
165
168
  stream: opts.role,
166
169
  generation: 0,
167
170
  cwd: opts.projectDir,
168
- configRoot: opts.projectDir,
171
+ configRoot: opts.configRoot ?? opts.projectDir,
169
172
  workspaceIdentity: captureWorkspaceIdentity(opts.projectDir),
170
173
  source: "legacy-inferred",
171
174
  });
@@ -175,7 +178,7 @@ export async function createRoleBuilder(opts) {
175
178
  }
176
179
  const adapterOpts = {
177
180
  cwd: opts.projectDir,
178
- configRoot: opts.projectDir,
181
+ configRoot: opts.configRoot ?? opts.projectDir,
179
182
  runtimeExecutable,
180
183
  runtimePhase: phaseForRole(opts.role),
181
184
  onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
@@ -196,6 +199,7 @@ export async function createRoleBuilder(opts) {
196
199
  sandboxMode: opts.sandboxMode,
197
200
  systemPromptAppend: roleBundle.system || undefined,
198
201
  skills: skills.length > 0 ? skills : undefined,
202
+ preloadedSkillContent: opts.preloadedSkillContent,
199
203
  };
200
204
  const adapter = nextRuntime === "codex"
201
205
  ? new CodexAdapter(adapterOpts)
@@ -221,7 +225,7 @@ export async function createRoleBuilder(opts) {
221
225
  const persistSessionRef = (ref) => {
222
226
  if (opts.persistSessionBindings === false)
223
227
  return;
224
- const bindingDb = new WorkflowDb(opts.projectDir);
228
+ const bindingDb = new WorkflowDb(executionRoot);
225
229
  try {
226
230
  bindingDb.recordProviderSessionBinding(ref);
227
231
  }
@@ -239,7 +243,7 @@ export async function createRoleBuilder(opts) {
239
243
  enabled: interactive,
240
244
  allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId && !requestedResumeRef,
241
245
  recreate: async (nextRuntime, resumeSessionId, resumeSessionRef) => {
242
- const nextReady = await ensureRuntimeReadyForCommand(opts.projectDir, nextRuntime, {
246
+ const nextReady = await ensureRuntimeReadyForCommand(executionRoot, nextRuntime, {
243
247
  label: opts.label,
244
248
  allowSwitch: false,
245
249
  model: nextRuntime === runtime ? model : undefined,
@@ -251,7 +255,13 @@ export async function createRoleBuilder(opts) {
251
255
  },
252
256
  onSessionRef: persistSessionRef,
253
257
  });
254
- return { builder, runtime, model, effort, roleBundle, skills, log };
258
+ return { builder, runtime, model, effort, roleBundle, skills, log, graphRoot: opts.configRoot ?? opts.projectDir };
259
+ }
260
+ export function sendRoleGraphTurn(role, instruction, purpose, policy) {
261
+ const root = role.graphRoot;
262
+ if (!root)
263
+ return role.builder.sendTurn(instruction, policy);
264
+ return sendGraphTurn(role.builder, instruction, graphContext(root, role.builder.sessionRef?.()?.cwd ?? root, purpose, instruction), policy);
255
265
  }
256
266
  function phaseForRole(role) {
257
267
  if (role === "planner")
@@ -322,6 +332,7 @@ export async function runRoleInstruction(opts) {
322
332
  const viewer = printEvents(builder.events());
323
333
  const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
324
334
  const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, opts.projectDir);
335
+ foreman.setGraphPurpose(opts.graphPurpose ?? { planner: "planning", discovery: "discovery", "ticket-maker": "ticket-population", uninstaller: "uninstall-analysis" }[opts.role] ?? "implementation");
325
336
  try {
326
337
  const turn = await foreman.runInstruction(opts.instruction);
327
338
  await builder.close();
@@ -34,6 +34,7 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
34
34
  restoreNativeCompactions(compactions: NativeCompaction[]): void;
35
35
  contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
36
36
  contextUsage(): Promise<ContextUsage | undefined>;
37
+ graphRuntimeSettings(): Pick<import("../adapters/types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
37
38
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
38
39
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
39
40
  events(): AsyncIterable<BuilderEvent>;
@@ -63,6 +63,7 @@ export class CurrentWorkflowGuardAdapter {
63
63
  restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
64
64
  contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
65
65
  contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
66
+ graphRuntimeSettings() { return this.adapter.graphRuntimeSettings?.(); }
66
67
  sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
67
68
  switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
68
69
  events() { return this.adapter.events(); }
@@ -1,3 +1,4 @@
1
+ import { graphCheckoutBoundary } from "./git.js";
1
2
  import { currentWorktreeBranch, findWorktreeForBranch, hasWorktreeChanges, mergeBranchToLocalBase, runGit } from "./git.js";
2
3
  import { captureProspectiveGitTree } from "../qaSnapshot.js";
3
4
  export class DirectMergeSourceChangedError extends Error {
@@ -75,6 +76,7 @@ export function removeDirectMergeWorktree(projectDir, intent, worktree) {
75
76
  verifyDirectMergeWorktree(projectDir, intent);
76
77
  // Let Git reject edits made even after the preceding verification. Never
77
78
  // force-remove or recursively delete a finalization worktree on failure.
79
+ graphCheckoutBoundary(projectDir, worktree, "worktree-removed");
78
80
  runGit(projectDir, ["worktree", "remove", worktree]);
79
81
  }
80
82
  export function hasExactStagedDirectMerge(projectDir, intent) {
@@ -95,8 +97,10 @@ export function hasExactStagedDirectMerge(projectDir, intent) {
95
97
  }
96
98
  export function executeDirectMerge(projectDir, intent, message) {
97
99
  const completed = reconcileDirectMerge(projectDir, intent);
98
- if (completed)
100
+ if (completed) {
101
+ graphCheckoutBoundary(projectDir, projectDir, "merge-or-rebase");
99
102
  return completed;
103
+ }
100
104
  verifyDirectMergeWorktree(projectDir, intent);
101
105
  const indexTree = runGit(projectDir, ["write-tree"]).stdout;
102
106
  const baseTree = runGit(projectDir, ["rev-parse", `${intent.baseCommit}^{tree}`]).stdout;
@@ -107,6 +111,7 @@ export function executeDirectMerge(projectDir, intent, message) {
107
111
  // Git prepared the merge before the host crashed, but commit did not.
108
112
  // Only commit the exact tree frozen in the durable intent.
109
113
  runGit(projectDir, ["commit", "-m", message]);
114
+ graphCheckoutBoundary(projectDir, projectDir, "merge-or-rebase");
110
115
  }
111
116
  else {
112
117
  // Squash/merge publish the immutable source commit, even if an external
@@ -16,6 +16,7 @@ export declare function ensureCleanBaseWorktree(cwd: string, opts?: BaseWorktree
16
16
  export declare function ensureForemanExcluded(cwd: string): void;
17
17
  export declare function createTicketWorktree(projectDir: string, runId: string, branch: string, base: string): string;
18
18
  export declare function findWorktreeForBranch(projectDir: string, branch: string): string | undefined;
19
+ export declare function graphCheckoutBoundary(projectDir: string, workspace: string, reason: "merge-or-rebase" | "worktree-created" | "worktree-removed"): void;
19
20
  export declare function removeTicketWorktree(projectDir: string, worktreePath: string): void;
20
21
  export declare function currentWorktreeBranch(worktreePath: string): string;
21
22
  export declare function hasTrackerChanges(worktreePath: string, paths?: {
@@ -1,3 +1,4 @@
1
+ import { invalidateGraphWorkspace } from "../graph/lifecycle.js";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
4
  import { dirname, join } from "node:path";
@@ -52,6 +53,7 @@ export function createTicketWorktree(projectDir, runId, branch, base) {
52
53
  const worktreePath = join(projectDir, ".foreman", "worktrees", runId, branch.replace(/\//g, "__"));
53
54
  mkdirSync(dirname(worktreePath), { recursive: true });
54
55
  runGit(projectDir, ["worktree", "add", "-b", branch, worktreePath, base]);
56
+ graphCheckoutBoundary(projectDir, worktreePath, "worktree-created");
55
57
  return worktreePath;
56
58
  }
57
59
  export function findWorktreeForBranch(projectDir, branch) {
@@ -67,7 +69,16 @@ export function findWorktreeForBranch(projectDir, branch) {
67
69
  }
68
70
  return undefined;
69
71
  }
72
+ export function graphCheckoutBoundary(projectDir, workspace, reason) {
73
+ try {
74
+ invalidateGraphWorkspace(projectDir, workspace, reason);
75
+ }
76
+ catch (error) {
77
+ console.error(`rafi graph: checkout metadata invalidation failed: ${String(error)}`);
78
+ }
79
+ }
70
80
  export function removeTicketWorktree(projectDir, worktreePath) {
81
+ graphCheckoutBoundary(projectDir, worktreePath, "worktree-removed");
71
82
  try {
72
83
  runGit(projectDir, ["worktree", "remove", "--force", worktreePath]);
73
84
  }
@@ -120,8 +131,10 @@ export function pushBranch(worktreePath, branch) {
120
131
  export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message, method = "squash") {
121
132
  if (method === "rebase") {
122
133
  const branchWorktree = findWorktreeForBranch(projectDir, branch);
123
- if (branchWorktree)
134
+ if (branchWorktree) {
124
135
  runGit(branchWorktree, ["rebase", baseBranch]);
136
+ graphCheckoutBoundary(projectDir, branchWorktree, "merge-or-rebase");
137
+ }
125
138
  else
126
139
  runGit(projectDir, ["rebase", baseBranch, branch]);
127
140
  }
@@ -140,6 +153,7 @@ export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message,
140
153
  else {
141
154
  runGit(projectDir, ["merge", "--ff-only", branch]);
142
155
  }
156
+ graphCheckoutBoundary(projectDir, projectDir, "merge-or-rebase");
143
157
  return runGit(projectDir, ["rev-parse", "--short", "HEAD"]).stdout;
144
158
  }
145
159
  /** Compatibility export for callers compiled against the older helper. */
@@ -79,8 +79,10 @@ export declare function runBranchPlan(opts: BranchRunnerOptions): Promise<Branch
79
79
  export declare function buildBranchTicketInstruction(node: BranchPlanNode, trackerPaths?: {
80
80
  progressDoc?: string;
81
81
  archiveDoc?: string;
82
+ validationChecklist?: string[];
82
83
  }): string;
83
84
  export declare function buildBranchTicketResumeInstruction(node: BranchPlanNode, trackerPaths?: {
84
85
  progressDoc?: string;
85
86
  archiveDoc?: string;
87
+ validationChecklist?: string[];
86
88
  }): string;
@@ -1,3 +1,8 @@
1
+ import { renderBuildWorkContext } from "../buildWorkContext.js";
2
+ import { loadTicketSetupConfigWithDefaults } from "../tickets/setupConfig.js";
3
+ import { deliverBuilderGuidanceFollowup } from "../builderGuidanceFollowup.js";
4
+ import { qaDigest } from "../qaProtocolV2.js";
5
+ import { loadTickets } from "../tickets/ticketLoader.js";
1
6
  import { formatRecoveryCommand } from "../recoveryGuidance.js";
2
7
  import { WorkflowReader } from "../workflowReader.js";
3
8
  import { buildScopeRevision } from "../buildApproval.js";
@@ -15,6 +20,7 @@ import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranch
15
20
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
16
21
  import { join, resolve } from "node:path";
17
22
  import { WorkflowDb } from "../workflowDb.js";
23
+ import { assertBuildAssignmentReconciled, BuildAssignmentRejected } from "../buildAssignment.js";
18
24
  import { currentActivity, withActivityPhase } from "../activity.js";
19
25
  import { SessionUnavailableError } from "../adapters/sessionFailure.js";
20
26
  import { SessionUnavailableContinuityError } from "../continuity.js";
@@ -46,6 +52,7 @@ function deliverySessionPath(projectDir, unitId) {
46
52
  return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
47
53
  }
48
54
  export async function runBranchPlan(opts) {
55
+ assertBuildAssignmentReconciled(opts.projectDir, opts.runId);
49
56
  let qaResumedRecovery = opts.qaResumedRecovery;
50
57
  if (qaResumedRecovery) {
51
58
  const matches = opts.plan.nodes.filter((node) => node.ticket.id === qaResumedRecovery.manifest.ticketId);
@@ -165,6 +172,15 @@ export async function runBranchPlan(opts) {
165
172
  const commitOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "commit");
166
173
  const completionOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "ticket-complete");
167
174
  let completedDirectMerge = finalizationRecovery ? protocolDb.operation(directMergeOperation) : undefined;
175
+ try {
176
+ if (finalizationRecovery)
177
+ protocolDb.assertFinalizationControls(opts.runId, node.ticket.id);
178
+ }
179
+ catch (error) {
180
+ protocolDb.close();
181
+ summaries.push(summaryFor(node, "needs-human", error instanceof Error ? error.message : String(error)));
182
+ continue;
183
+ }
168
184
  if (completedDirectMerge?.status === "in_progress") {
169
185
  try {
170
186
  const intent = readDirectMergeIntent(completedDirectMerge.intent);
@@ -271,6 +287,13 @@ export async function runBranchPlan(opts) {
271
287
  continue;
272
288
  }
273
289
  }
290
+ const admissionDb = new WorkflowDb(opts.projectDir);
291
+ try {
292
+ admissionDb.assertAdmittedWork(opts.runId, node.ticket.id);
293
+ }
294
+ finally {
295
+ admissionDb.close();
296
+ }
274
297
  opts.log.write("branch-start", {
275
298
  ticket: node.ticket.id,
276
299
  branch: node.branch,
@@ -308,7 +331,7 @@ export async function runBranchPlan(opts) {
308
331
  summary: resumeSession ? `Resuming branch ${node.branch}` : `Starting branch ${node.branch}`,
309
332
  });
310
333
  });
311
- let ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, opts.trackerPaths) : buildBranchTicketInstruction(node, opts.trackerPaths);
334
+ let ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, { ...opts.trackerPaths, validationChecklist: loadTicketSetupConfigWithDefaults(opts.projectDir).build.validation_checklist }) : buildBranchTicketInstruction(node, { ...opts.trackerPaths, validationChecklist: loadTicketSetupConfigWithDefaults(opts.projectDir).build.validation_checklist });
312
335
  if (continuations.length)
313
336
  ticketInstruction += "\n\nScoped answers authorizing this ticket continuation:\n" + continuations.map(decision => `${decision.prompt}\nAnswer: ${decision.answer ?? decision.selectedChoiceId}`).join("\n");
314
337
  // Reattach the predecessor even for a `fresh` strategy so the host can
@@ -337,7 +360,7 @@ export async function runBranchPlan(opts) {
337
360
  if (!qaOnlyRecovery)
338
361
  builderWorkSessions += 1;
339
362
  viewer = opts.observeBuilder?.(builder);
340
- const foreman = new Foreman(builder, opts.log, { desktop: opts.notificationsEnabled, terminalBell: opts.terminalBellEnabled ?? true }, opts.qaEnabled, 3, opts.projectDir, undefined, undefined, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined, undefined, undefined, undefined, async (adapter) => opts.observeBuilderNativeCompactions?.(adapter, worktreePath), opts.observer, undefined, false, undefined, undefined, opts.runId, opts.continueIndependentTickets ?? true, node.ticket.id);
363
+ const foreman = new Foreman(builder, opts.log, { desktop: opts.notificationsEnabled, terminalBell: opts.terminalBellEnabled ?? true }, opts.qaEnabled, 3, opts.projectDir, undefined, opts.createQa, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined, undefined, undefined, undefined, async (adapter) => opts.observeBuilderNativeCompactions?.(adapter, worktreePath), opts.observer, undefined, false, undefined, undefined, opts.runId, opts.continueIndependentTickets ?? true, node.ticket.id, worktreePath);
341
364
  const turn = async () => {
342
365
  const db = new WorkflowDb(opts.projectDir);
343
366
  try {
@@ -450,6 +473,54 @@ export async function runBranchPlan(opts) {
450
473
  observeNativeCompactions: opts.observeQaNativeCompactions,
451
474
  resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa"),
452
475
  evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
476
+ resumeBuilderGuidance: qaOnlyRecovery,
477
+ deliverBuilderFollowup: (instruction, followup) => deliverBuilderGuidanceFollowup({ projectDir: opts.projectDir, runId: opts.runId, ticketId: node.ticket.id, worktree: worktreePath }, instruction, followup, {
478
+ prepare: async (instruction) => {
479
+ if (!builder)
480
+ throw new Error("Builder session unavailable");
481
+ const strategy = opts.builderSessionStrategy ?? "compact";
482
+ if (opts.builderSessionBoundary)
483
+ builder = await opts.builderSessionBoundary(builder, instruction, strategy, worktreePath);
484
+ else if (strategy === "fresh") {
485
+ await builder.close();
486
+ builder = await opts.createBuilder(worktreePath);
487
+ }
488
+ else if (builder.sessionId()) {
489
+ const compacted = await compactWithRetry(builder);
490
+ if (!compacted.ok) {
491
+ await builder.close();
492
+ builder = await opts.createBuilder(worktreePath);
493
+ }
494
+ }
495
+ if (opts.beforeBuilderTurn)
496
+ builder = await opts.beforeBuilderTurn(builder, instruction, worktreePath);
497
+ return builder;
498
+ },
499
+ validateRequirements: () => {
500
+ const current = loadTickets(resolveTicketPaths(loadTicketsConfig(opts.projectDir), opts.projectDir).tickets).find(ticket => ticket.id === node.ticket.id);
501
+ if (!current || qaDigest("admitted-requirements", current) !== followup.requirementsDigest)
502
+ throw new BuildAssignmentRejected("Ticket requirements changed during session preparation; renewed scope approval is required");
503
+ },
504
+ recordSession: adapter => {
505
+ const sessionId = adapter.sessionId(), sessionRef = adapter.sessionRef?.();
506
+ if (!sessionId)
507
+ return;
508
+ builderStream = { sessionId, ...(sessionRef ? { sessionRef } : {}), worktreePath };
509
+ const db = new WorkflowDb(opts.projectDir);
510
+ try {
511
+ db.recordBranchResumeSession(opts.runId, { ticket: node.ticket.id, branch: node.branch, base: node.baseBranch, worktreePath, sessionId, sessionRef, logPath: "structured-recovery", deliveryUnitId: node.deliveryUnitId });
512
+ }
513
+ finally {
514
+ db.close();
515
+ }
516
+ opts.recordBuilderSession?.(sessionRef ?? sessionId, node.ticket.id, worktreePath);
517
+ },
518
+ completed: async (operationId, adapter) => {
519
+ builderWorkSessions += 1;
520
+ await opts.observeBuilderNativeCompactions?.(adapter, worktreePath);
521
+ opts.log.write("qa-fix", { kind: "builder-guidance-followup", ticket: node.ticket.id, operationId });
522
+ },
523
+ }),
453
524
  deliverFailure: async (request) => {
454
525
  if (!builder)
455
526
  return { ok: false, detail: "Builder session unavailable" };
@@ -530,6 +601,13 @@ export async function runBranchPlan(opts) {
530
601
  isError: false,
531
602
  });
532
603
  }
604
+ const finalControlsDb = new WorkflowDb(opts.projectDir);
605
+ try {
606
+ finalControlsDb.assertFinalizationControls(opts.runId, node.ticket.id);
607
+ }
608
+ finally {
609
+ finalControlsDb.close();
610
+ }
533
611
  if (opts.qaEnabled && !qaWaived && !finalizationRecovery) {
534
612
  if (!qaPassCertificateId || !qaSourceStateDigest)
535
613
  throw new Error("QA passed without a durable pass certificate");
@@ -760,6 +838,10 @@ export async function runBranchPlan(opts) {
760
838
  await observeNode(opts, node, "cleanup", "removing ticket worktree", () => removeTicketWorktree(opts.projectDir, worktreePath));
761
839
  }
762
840
  catch (err) {
841
+ if (err instanceof BuildAssignmentRejected) {
842
+ summaries.push(summaryFor(node, "needs-human", err.message));
843
+ return summaries;
844
+ }
763
845
  if (err instanceof SessionUnavailableError || err instanceof SessionUnavailableContinuityError) {
764
846
  const message = err.message;
765
847
  opts.log.write("branch-issue", { ticket: node.ticket.id, code: "session_unavailable", message, blocking: true });
@@ -1027,6 +1109,8 @@ export function buildBranchTicketInstruction(node, trackerPaths = {}) {
1027
1109
 
1028
1110
  ${node.ticket.id}: ${node.ticket.title}
1029
1111
 
1112
+ ${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
1113
+
1030
1114
  Summary:
1031
1115
  ${node.ticket.summary}
1032
1116
 
@@ -1047,7 +1131,7 @@ Branch-mode rules:
1047
1131
  - If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
1048
1132
  - End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
1049
1133
 
1050
- QA will happen in this same builder session after implementation.
1134
+ Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
1051
1135
 
1052
1136
  ${MARKER_SPEC}`;
1053
1137
  }
@@ -1057,6 +1141,8 @@ export function buildBranchTicketResumeInstruction(node, trackerPaths = {}) {
1057
1141
 
1058
1142
  ${node.ticket.id}: ${node.ticket.title}
1059
1143
 
1144
+ ${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
1145
+
1060
1146
  Resume from the current repository state. Inspect the worktree if needed, then finish only this ticket.
1061
1147
 
1062
1148
  Branch-mode rules:
@@ -1067,7 +1153,7 @@ Branch-mode rules:
1067
1153
  - If another selected ticket is required before this one can be completed, stop and end with STEP_STATUS: blocked | ticket="${node.ticket.id}" branch_dependency="<ticket-id>" reason="<why>".
1068
1154
  - End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
1069
1155
 
1070
- QA will happen in this same builder session after implementation.
1156
+ Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
1071
1157
 
1072
1158
  ${MARKER_SPEC}`;
1073
1159
  }
@@ -1,3 +1,4 @@
1
+ import { registerHandbackWriter } from "./qaHandbackMigration.js";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { createHash, randomUUID } from "node:crypto";
3
4
  import { realpathSync } from "node:fs";
@@ -56,6 +57,7 @@ export function maintainBuildAdmission(authority) {
56
57
  try {
57
58
  db = new Database(join(authority.project, ".rafi/recovery.sqlite3"), { fileMustExist: true });
58
59
  db.function("rafi_protocol_v3", () => 1);
60
+ registerHandbackWriter(db);
59
61
  const changed = db.prepare("UPDATE build_admission SET record_json=json_set(record_json,'$.heartbeatAt',?) WHERE singleton=1 AND json_extract(record_json,'$.token')=?").run(new Date().toISOString(), authority.token);
60
62
  if (changed.changes !== 1)
61
63
  stopBuildAdmissionHeartbeat(authority);
@@ -0,0 +1,33 @@
1
+ import type { BuilderAdapter } from "./adapters/types.js";
2
+ import type { StepStatus } from "./foreman.js";
3
+ import type { TurnResult } from "./adapters/types.js";
4
+ import { type FrozenQaSourceState } from "./qaSnapshot.js";
5
+ /** Work responses confirm an assignment; they never select the next ticket. */
6
+ export declare function validateBuildAssignment(ticketId: string, status: StepStatus, responseText?: string): string | undefined;
7
+ export interface BuildAssignmentCapture {
8
+ operationId: string;
9
+ runId: string;
10
+ ticketId: string;
11
+ worktree: string;
12
+ before: BuildSourceState;
13
+ instruction: string;
14
+ guidanceIds: string[];
15
+ }
16
+ type BuildSourceState = Pick<FrozenQaSourceState, "digest" | "head" | "contentDigest" | "capturedAt" | "pathInventory" | "stagedDiff" | "unstagedDiff" | "untracked"> & {
17
+ captureMode: "git" | "filesystem";
18
+ omissions?: string[];
19
+ };
20
+ export declare class BuildAssignmentRejected extends Error {
21
+ }
22
+ export interface BuilderGuidanceFollowup {
23
+ reviewAttemptId: string;
24
+ requirementsDigest: string;
25
+ maximum: number;
26
+ authorizationId?: string;
27
+ }
28
+ export declare function assertBuildAssignmentReconciled(projectDir: string, runId: string, worktree?: string): void;
29
+ /** Commit response/source bytes before interpreting any scoped outcome. */
30
+ export declare function beginBuildAssignment(projectDir: string, runId: string, ticketId: string, worktree: string, instruction: string, followup?: BuilderGuidanceFollowup, adapter?: BuilderAdapter): BuildAssignmentCapture;
31
+ export declare function finishBuildAssignment(projectDir: string, assignment: BuildAssignmentCapture, result: TurnResult, status: StepStatus): string | undefined;
32
+ export declare function captureBuildSource(worktree: string): BuildSourceState;
33
+ export {};