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,3 +1,4 @@
1
+ import type { ProviderTurnPurpose } from "../providerPhase.js";
1
2
  /**
2
3
  * Agent-agnostic interface over a coding agent ("builder").
3
4
  */
@@ -233,6 +234,11 @@ export interface BuilderAdapterOptions {
233
234
  fast?: boolean;
234
235
  /** Codex sandbox mode. Defaults to workspace-write for implementation runs. */
235
236
  sandboxMode?: "workspace-write" | "read-only";
237
+ /** Explicit opt-in to per-command sandbox escalation. QA remains offline/never. */
238
+ approvalPolicy?: "never" | "on-request";
239
+ approvalTimeoutMs?: number;
240
+ /** Build-scoped explicit grant; ignored for read-only/QA roles. */
241
+ networkAccess?: boolean;
236
242
  /** Role system text to append to the harness system prompt (from .rafi/compiled or library). */
237
243
  systemPromptAppend?: string;
238
244
  /** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
@@ -273,12 +279,21 @@ export interface BuilderAdapterOptions {
273
279
  }) => void;
274
280
  }
275
281
  export interface BuilderAdapter {
282
+ graphRuntimeSettings?(): Pick<BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
276
283
  readonly agent: "claude" | "codex";
284
+ contractCapabilities?(): {
285
+ sameSessionAcceptance: boolean;
286
+ nativeCompactionBarrier: boolean;
287
+ };
288
+ enableContractEnforcement?(): void;
289
+ contractCompactionSequence?(): number;
290
+ acceptContractDelivery?(sequence: number): void;
277
291
  /** Send one instruction; resolves when that turn completes. */
278
292
  sendTurn(text: string, policy?: {
279
293
  handback?: boolean;
280
294
  responseOnly?: boolean;
281
295
  logicalActionId?: string;
296
+ purpose?: ProviderTurnPurpose;
282
297
  }): Promise<TurnResult>;
283
298
  /** Synchronous observation through the event owner. Terminal precedes sendTurn resolution. */
284
299
  observeEvents?(listener: (event: BuilderEvent) => void): () => void;
@@ -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,9 @@
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";
6
+ import { formatRecoveryCommand } from "../recoveryGuidance.js";
1
7
  import { WorkflowReader } from "../workflowReader.js";
2
8
  import { buildScopeRevision } from "../buildApproval.js";
3
9
  import { beginQaFinalization, completeQaFinalization, verifyPendingQaFinalizationSource, compactWithRetry, runIsolatedQa } from "../qaReview.js";
@@ -14,6 +20,7 @@ import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranch
14
20
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
15
21
  import { join, resolve } from "node:path";
16
22
  import { WorkflowDb } from "../workflowDb.js";
23
+ import { assertBuildAssignmentReconciled, BuildAssignmentRejected } from "../buildAssignment.js";
17
24
  import { currentActivity, withActivityPhase } from "../activity.js";
18
25
  import { SessionUnavailableError } from "../adapters/sessionFailure.js";
19
26
  import { SessionUnavailableContinuityError } from "../continuity.js";
@@ -45,6 +52,7 @@ function deliverySessionPath(projectDir, unitId) {
45
52
  return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
46
53
  }
47
54
  export async function runBranchPlan(opts) {
55
+ assertBuildAssignmentReconciled(opts.projectDir, opts.runId);
48
56
  let qaResumedRecovery = opts.qaResumedRecovery;
49
57
  if (qaResumedRecovery) {
50
58
  const matches = opts.plan.nodes.filter((node) => node.ticket.id === qaResumedRecovery.manifest.ticketId);
@@ -164,6 +172,15 @@ export async function runBranchPlan(opts) {
164
172
  const commitOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "commit");
165
173
  const completionOperation = finalizationOperationKey(protocolDb, opts.runId, node.ticket.id, "ticket-complete");
166
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
+ }
167
184
  if (completedDirectMerge?.status === "in_progress") {
168
185
  try {
169
186
  const intent = readDirectMergeIntent(completedDirectMerge.intent);
@@ -270,6 +287,13 @@ export async function runBranchPlan(opts) {
270
287
  continue;
271
288
  }
272
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
+ }
273
297
  opts.log.write("branch-start", {
274
298
  ticket: node.ticket.id,
275
299
  branch: node.branch,
@@ -307,7 +331,7 @@ export async function runBranchPlan(opts) {
307
331
  summary: resumeSession ? `Resuming branch ${node.branch}` : `Starting branch ${node.branch}`,
308
332
  });
309
333
  });
310
- 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 });
311
335
  if (continuations.length)
312
336
  ticketInstruction += "\n\nScoped answers authorizing this ticket continuation:\n" + continuations.map(decision => `${decision.prompt}\nAnswer: ${decision.answer ?? decision.selectedChoiceId}`).join("\n");
313
337
  // Reattach the predecessor even for a `fresh` strategy so the host can
@@ -336,7 +360,7 @@ export async function runBranchPlan(opts) {
336
360
  if (!qaOnlyRecovery)
337
361
  builderWorkSessions += 1;
338
362
  viewer = opts.observeBuilder?.(builder);
339
- 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);
340
364
  const turn = async () => {
341
365
  const db = new WorkflowDb(opts.projectDir);
342
366
  try {
@@ -449,6 +473,54 @@ export async function runBranchPlan(opts) {
449
473
  observeNativeCompactions: opts.observeQaNativeCompactions,
450
474
  resolveBlocked: (adapter, reason) => foreman.resolveBlocker(adapter, reason, "qa"),
451
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
+ }),
452
524
  deliverFailure: async (request) => {
453
525
  if (!builder)
454
526
  return { ok: false, detail: "Builder session unavailable" };
@@ -529,6 +601,13 @@ export async function runBranchPlan(opts) {
529
601
  isError: false,
530
602
  });
531
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
+ }
532
611
  if (opts.qaEnabled && !qaWaived && !finalizationRecovery) {
533
612
  if (!qaPassCertificateId || !qaSourceStateDigest)
534
613
  throw new Error("QA passed without a durable pass certificate");
@@ -759,6 +838,10 @@ export async function runBranchPlan(opts) {
759
838
  await observeNode(opts, node, "cleanup", "removing ticket worktree", () => removeTicketWorktree(opts.projectDir, worktreePath));
760
839
  }
761
840
  catch (err) {
841
+ if (err instanceof BuildAssignmentRejected) {
842
+ summaries.push(summaryFor(node, "needs-human", err.message));
843
+ return summaries;
844
+ }
762
845
  if (err instanceof SessionUnavailableError || err instanceof SessionUnavailableContinuityError) {
763
846
  const message = err.message;
764
847
  opts.log.write("branch-issue", { ticket: node.ticket.id, code: "session_unavailable", message, blocking: true });
@@ -818,7 +901,7 @@ function invalidateUnpublishedMergeDrift(projectDir, runId, ticketId, operationI
818
901
  // and source edits, but retire its dispatch slot before scheduling new QA.
819
902
  db.updateOperation(operationId, "failed", { error: error.message });
820
903
  const paused = db.invalidateQaFinalization(runId, ticketId, head.revision, error.message, intent.branch);
821
- return new Error(`${error.message}. A complete QA recheck is required. Resume with: rafi build:resume ${projectDir} --run ${runId} --ticket ${ticketId} --qa-revision ${paused.revision} --fresh-with-handoff`);
904
+ return new Error(`${error.message}. A complete QA recheck is required. Resume with: ${formatRecoveryCommand(projectDir)}`);
822
905
  }
823
906
  catch {
824
907
  return error;
@@ -1026,6 +1109,8 @@ export function buildBranchTicketInstruction(node, trackerPaths = {}) {
1026
1109
 
1027
1110
  ${node.ticket.id}: ${node.ticket.title}
1028
1111
 
1112
+ ${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
1113
+
1029
1114
  Summary:
1030
1115
  ${node.ticket.summary}
1031
1116
 
@@ -1046,7 +1131,7 @@ Branch-mode rules:
1046
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>".
1047
1132
  - End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
1048
1133
 
1049
- QA will happen in this same builder session after implementation.
1134
+ Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
1050
1135
 
1051
1136
  ${MARKER_SPEC}`;
1052
1137
  }
@@ -1056,6 +1141,8 @@ export function buildBranchTicketResumeInstruction(node, trackerPaths = {}) {
1056
1141
 
1057
1142
  ${node.ticket.id}: ${node.ticket.title}
1058
1143
 
1144
+ ${renderBuildWorkContext(node.ticket, trackerPaths.validationChecklist ?? [])}
1145
+
1059
1146
  Resume from the current repository state. Inspect the worktree if needed, then finish only this ticket.
1060
1147
 
1061
1148
  Branch-mode rules:
@@ -1066,7 +1153,7 @@ Branch-mode rules:
1066
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>".
1067
1154
  - End with STEP_STATUS: done | ticket="${node.ticket.id}" summary="<what changed>" when the ticket is implemented.
1068
1155
 
1069
- QA will happen in this same builder session after implementation.
1156
+ Final QA runs independently in a separate disposable snapshot and fresh reviewer conversation.
1070
1157
 
1071
1158
  ${MARKER_SPEC}`;
1072
1159
  }
@@ -0,0 +1,61 @@
1
+ import type { ProjectLease } from "./workflowDb.js";
2
+ import Database from "better-sqlite3";
3
+ export declare class BuildOwnershipError extends Error {
4
+ readonly code: "stale-owner" | "unknown-owner" | "active-owner" | "invalid-launch";
5
+ constructor(code: "stale-owner" | "unknown-owner" | "active-owner" | "invalid-launch", message: string);
6
+ }
7
+ export interface BuildAdmission {
8
+ token: string;
9
+ runId: string;
10
+ project: string;
11
+ pid: number;
12
+ host: string;
13
+ processStart: string;
14
+ phase: "coordinator" | "worker";
15
+ heartbeatAt: string;
16
+ }
17
+ export interface BuildLaunch {
18
+ token: string;
19
+ runId: string;
20
+ project: string;
21
+ launcher: string;
22
+ role: BuildAdmission["phase"];
23
+ digest: string;
24
+ state: "reserved" | "dispatching" | "claimed" | "failed";
25
+ claimant?: BuildAdmission;
26
+ createdAt: string;
27
+ protocol?: "registered-v2";
28
+ child?: {
29
+ pid: number;
30
+ processStart: string;
31
+ host: string;
32
+ };
33
+ acknowledged?: boolean;
34
+ }
35
+ export declare function canonicalProject(project: string): string;
36
+ export declare function originalBuildLease(project: string): ProjectLease | undefined;
37
+ export declare function rememberOriginalLease(project: string, lease: ProjectLease): void;
38
+ export declare function forgetOriginalLease(project: string, lease: ProjectLease): void;
39
+ export declare function withBuildInvocation<T>(work: () => T): T;
40
+ export declare function localBuildAuthority(project: string): BuildAdmission | undefined;
41
+ export declare function rememberBuildAuthority(authority: BuildAdmission): void;
42
+ export declare function forgetBuildAuthority(authority: BuildAdmission): void;
43
+ export declare function launchDigest(args: readonly string[]): string;
44
+ export declare function checkBuildOwnershipSchema(db: Database.Database): void;
45
+ export declare function maintainBuildAdmission(authority: BuildAdmission): void;
46
+ export declare function stopBuildAdmissionHeartbeat(authority: BuildAdmission): void;
47
+ /** Additive, transactional storage. Readers never invoke this migration. */
48
+ export declare function migrateBuildAdmission(db: Database.Database): void;
49
+ export declare function readAdmission(db: Database.Database): BuildAdmission | undefined;
50
+ export declare function assertAdmission(db: Database.Database, authority: BuildAdmission): void;
51
+ export declare function acquireAdmission(db: Database.Database, project: string, runId: string, phase: BuildAdmission["phase"]): BuildAdmission;
52
+ export declare function releaseAdmission(db: Database.Database, authority: BuildAdmission): void;
53
+ export declare function reserveLaunch(db: Database.Database, authority: BuildAdmission, role: BuildAdmission["phase"], digest: string, protocol?: BuildLaunch["protocol"]): BuildLaunch;
54
+ export declare function readLaunch(db: Database.Database, token: string): BuildLaunch | undefined;
55
+ export declare function setLaunchState(db: Database.Database, authority: BuildAdmission, token: string, state: "dispatching" | "failed"): void;
56
+ export declare function claimLaunch(db: Database.Database, project: string, runId: string, token: string, role: BuildAdmission["phase"], digest: string): BuildAdmission;
57
+ /** Child registration grants no mutation/provider authority. Competes atomically with retirement. */
58
+ export declare function registerLaunchChild(db: Database.Database, token: string, project: string): void;
59
+ export declare function acknowledgeLaunchChild(db: Database.Database, authority: BuildAdmission, token: string, pid: number): void;
60
+ /** Restricted retirement only; does not grant execution authority or replay work. */
61
+ export declare function reconcileLaunch(db: Database.Database, token: string, original?: BuildAdmission): "retired" | "claimed" | "unknown";