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
@@ -0,0 +1,17 @@
1
+ export type RecoveryCommandFamily = "rafi" | "ai-foreman";
2
+ export declare function withRecoveryCommandFamily<T>(family: RecoveryCommandFamily, work: () => T): T;
3
+ export declare function shellArgument(value: string, platform?: NodeJS.Platform): string;
4
+ /** POSIX shell command (PowerShell on Windows). The owning project is supplied explicitly, never a QA snapshot. */
5
+ export declare function formatRecoveryCommand(projectDir: string, family?: RecoveryCommandFamily, cwd?: string, session?: {
6
+ id: string;
7
+ steps: number;
8
+ }): string;
9
+ /** Fixed choices are executable; custom answers use the owning CLI's input flow. */
10
+ export declare function formatDecisionCommands(projectDir: string, runId: string, decision: {
11
+ decisionId: string;
12
+ choices: Array<{
13
+ id: string;
14
+ label: string;
15
+ }>;
16
+ }, family?: RecoveryCommandFamily): string[];
17
+ export declare function formatExactRunRecovery(projectDir: string, runId: string, family?: RecoveryCommandFamily): string;
@@ -0,0 +1,38 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ const commandFamily = new AsyncLocalStorage();
5
+ export function withRecoveryCommandFamily(family, work) { return commandFamily.run(family, work); }
6
+ export function shellArgument(value, platform = process.platform) {
7
+ if (platform === "win32")
8
+ return `'${value.replace(/'/g, "''")}'`;
9
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replace(/'/g, `'"'"'`)}'`;
10
+ }
11
+ function canonical(path) { try {
12
+ return realpathSync(resolve(path));
13
+ }
14
+ catch {
15
+ return resolve(path);
16
+ } }
17
+ /** POSIX shell command (PowerShell on Windows). The owning project is supplied explicitly, never a QA snapshot. */
18
+ export function formatRecoveryCommand(projectDir, family = commandFamily.getStore() ?? "rafi", cwd = process.cwd(), session) {
19
+ if (family === "ai-foreman") {
20
+ if (session)
21
+ return `ai-foreman start ${shellArgument(resolve(projectDir))} --steps ${session.steps} --resume ${shellArgument(session.id)}`;
22
+ return `ai-foreman manager ${shellArgument(resolve(projectDir))}`;
23
+ }
24
+ return canonical(projectDir) === canonical(cwd) ? "rafi resume" : `rafi resume ${shellArgument(resolve(projectDir))}`;
25
+ }
26
+ /** Fixed choices are executable; custom answers use the owning CLI's input flow. */
27
+ export function formatDecisionCommands(projectDir, runId, decision, family = commandFamily.getStore() ?? "rafi") {
28
+ return decision.choices.flatMap(choice => choice.id === "custom"
29
+ ? [family === "ai-foreman"
30
+ ? ` Custom response: use ${family} build:decide ${shellArgument(resolve(projectDir))} --run ${shellArgument(runId)} --decision ${shellArgument(decision.decisionId)} --choice custom with --answer followed by your shell-quoted response.`
31
+ : ` Custom response: run ${formatExactRunRecovery(projectDir, runId, family)} in an interactive terminal.`]
32
+ : [` ${choice.label}`, ` ${family} build:decide ${shellArgument(resolve(projectDir))} --run ${shellArgument(runId)} --decision ${shellArgument(decision.decisionId)} --choice ${shellArgument(choice.id)}`]);
33
+ }
34
+ export function formatExactRunRecovery(projectDir, runId, family = commandFamily.getStore() ?? "rafi") {
35
+ if (family === "ai-foreman")
36
+ return `ai-foreman manager ${shellArgument(resolve(projectDir))} --run ${shellArgument(runId)}`;
37
+ return `rafi resume ${shellArgument(resolve(projectDir))} --run ${shellArgument(runId)}`;
38
+ }
@@ -14,6 +14,7 @@ export declare class RuntimeAuthError extends Error {
14
14
  readonly stdout: string;
15
15
  readonly stderr: string;
16
16
  readonly authLikely: boolean;
17
+ readonly cleanupUnverified: boolean;
17
18
  constructor(opts: RuntimeAuthErrorOptions);
18
19
  }
19
20
  export declare function checkRuntimeReady(projectDir: string, runtime: AgentRuntime, options?: ProbeRuntimeOptions): Promise<import("rafi-spec").RuntimeProbeResult>;
@@ -1,10 +1,11 @@
1
- import { formatRuntimeProbeFailure, probeRuntime } from "./runtimeReadiness.js";
1
+ import { formatRuntimeProbeFailure, probeRuntime, RuntimeCleanupError, runtimeCleanupRecoveryHelp } from "./runtimeReadiness.js";
2
2
  export class RuntimeAuthError extends Error {
3
3
  runtime;
4
4
  exitCode;
5
5
  stdout;
6
6
  stderr;
7
7
  authLikely;
8
+ cleanupUnverified;
8
9
  constructor(opts) {
9
10
  super(formatRuntimeAuthFailure(opts), { cause: opts.cause });
10
11
  this.name = "RuntimeAuthError";
@@ -13,6 +14,7 @@ export class RuntimeAuthError extends Error {
13
14
  this.stdout = opts.stdout ?? "";
14
15
  this.stderr = opts.stderr ?? "";
15
16
  this.authLikely = isRuntimeAuthFailure(`${this.stderr}\n${this.stdout}`);
17
+ this.cleanupUnverified = opts.cause instanceof RuntimeCleanupError;
16
18
  }
17
19
  }
18
20
  export async function checkRuntimeReady(projectDir, runtime, options = {}) {
@@ -67,6 +69,8 @@ export function runtimeRepairCommands(runtime) {
67
69
  ].join("\n");
68
70
  }
69
71
  export function formatRuntimeAuthFailure(opts) {
72
+ if (opts.cause instanceof RuntimeCleanupError)
73
+ return `${opts.cause.message}\n\n${opts.context} is paused before further provider work. This is a process cleanup problem; changing login or provider will not clear it.\n\n${runtimeCleanupRecoveryHelp()}`;
70
74
  const output = [opts.stderr, opts.stdout].filter(Boolean).join("\n").trim();
71
75
  const exit = opts.exitCode === undefined || opts.exitCode === null ? "unknown" : String(opts.exitCode);
72
76
  const authLine = isRuntimeAuthFailure(output)
@@ -1,10 +1,22 @@
1
+ import { type BuildAdmission } from "./buildAdmission.js";
1
2
  import type { RuntimeProbeCategory, RuntimeProbePhase, RuntimeProbeResult } from "rafi-spec";
2
3
  import type { AgentRuntime } from "./runtimeAuth.js";
3
4
  export declare const RUNTIME_PROBE_TIMEOUT_MS = 120000;
4
5
  export declare const RUNTIME_DIAGNOSTIC_LIMIT: number;
5
6
  /** Used when a supervised worker loses its parent, including detached probes. */
6
7
  export declare function cancelOwnedRuntimeProbes(): void;
8
+ export interface BuildReadinessContext {
9
+ project: string;
10
+ runId: string;
11
+ authority: BuildAdmission;
12
+ }
13
+ export declare class RuntimeCleanupError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ export declare function runtimeCleanupRecoveryHelp(): string;
7
17
  export interface ProbeRuntimeOptions {
18
+ /** Omit only for standalone authentication/capability commands. Build callers use probeBuildRuntime. */
19
+ build?: BuildReadinessContext;
8
20
  phase?: RuntimeProbePhase;
9
21
  timeoutMs?: number;
10
22
  maxDiagnosticsBytes?: number;
@@ -22,6 +34,7 @@ export interface ProbeRuntimeOptions {
22
34
  }
23
35
  /** Resolve the executable Node will launch without involving a shell. */
24
36
  export declare function resolveExecutablePath(command: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
37
+ export declare function probeBuildRuntime(cwd: string, runtime: AgentRuntime, build: BuildReadinessContext, opts?: Omit<ProbeRuntimeOptions, "build">): Promise<RuntimeProbeResult>;
25
38
  export declare function probeRuntime(cwd: string, runtime: AgentRuntime, opts?: ProbeRuntimeOptions): Promise<RuntimeProbeResult>;
26
39
  export declare function classifyRuntimeFailure(text: string, phase?: RuntimeProbePhase): RuntimeProbeCategory;
27
40
  export declare function classifyClaudeSdkFailure(error: string | undefined, status: number | null | undefined, diagnostics: string): RuntimeProbeCategory;
@@ -1,15 +1,35 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { cleanupReadiness } from "./readinessCleanup.js";
3
+ import { windowsProbeCommand } from "./windowsProbeJob.js";
4
+ import { randomUUID } from "node:crypto";
5
+ import { BuildOwnershipError, canonicalProject } from "./buildAdmission.js";
6
+ import { WorkflowDb } from "./workflowDb.js";
1
7
  import { spawn } from "node:child_process";
2
8
  import { accessSync, constants, statSync } from "node:fs";
3
9
  import { delimiter, isAbsolute, resolve } from "node:path";
4
10
  import { currentActivity, withActivityPhase } from "./activity.js";
5
- import { processStartIdentity } from "./processIdentity.js";
11
+ import { classifyProcess, processStartIdentity } from "./processIdentity.js";
6
12
  export const RUNTIME_PROBE_TIMEOUT_MS = 120_000;
7
13
  export const RUNTIME_DIAGNOSTIC_LIMIT = 8 * 1024;
8
14
  const RELEVANT_ENV = /^(ANTHROPIC|CLAUDE|CODEX|OPENAI|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|SSL_CERT_FILE|NODE_EXTRA_CA_CERTS)(_|$)/i;
9
15
  const ownedProbes = new Set();
16
+ const pendingStandaloneCleanup = new Map();
10
17
  /** Used when a supervised worker loses its parent, including detached probes. */
11
18
  export function cancelOwnedRuntimeProbes() { for (const cancel of ownedProbes)
12
19
  cancel(); }
20
+ export class RuntimeCleanupError extends Error {
21
+ constructor(message) { super(message); this.name = "RuntimeCleanupError"; }
22
+ }
23
+ export function runtimeCleanupRecoveryHelp() {
24
+ return [
25
+ "Keep this Rafi process open while repairing visibility in another terminal, then choose retry.",
26
+ "Linux/WSL: check that /proc is mounted and readable by this user. Use a native Linux Node and provider installation in the same WSL distribution; inspect command -v node and command -v claude (or codex).",
27
+ "macOS: check that ps is available on PATH and process inspection is permitted by the terminal or host sandbox.",
28
+ "Windows: check that Windows PowerShell and process/Job Object inspection are available to the same user.",
29
+ "Retry first reconciles the original probe; no new provider starts until cleanup is verified. Avoid restarting Rafi to bypass this check or killing unrelated processes.",
30
+ "You may cancel deliberately; generated files remain available for a later create walkthrough.",
31
+ ].join("\n");
32
+ }
13
33
  /** Resolve the executable Node will launch without involving a shell. */
14
34
  export function resolveExecutablePath(command, env = process.env, platform = process.platform) {
15
35
  const hasPathSeparator = command.includes("/") || command.includes("\\");
@@ -57,10 +77,24 @@ function isRunnableFile(path, platform) {
57
77
  return false;
58
78
  }
59
79
  }
80
+ export async function probeBuildRuntime(cwd, runtime, build, opts = {}) {
81
+ if (!build?.authority)
82
+ throw new BuildOwnershipError("stale-owner", "Build readiness requires original admission authority");
83
+ return probeRuntime(cwd, runtime, { ...opts, build });
84
+ }
60
85
  export async function probeRuntime(cwd, runtime, opts = {}) {
61
86
  return withActivityPhase(`checking ${runtime} runtime`, () => probeRuntimeInternal(cwd, runtime, opts));
62
87
  }
63
88
  async function probeRuntimeInternal(cwd, runtime, opts) {
89
+ // Do not overlap a new probe with cleanup that a previous standalone call
90
+ // could not verify. Keep the original identities, never manufacture authority.
91
+ const cleanupDeadline = Date.now() + 5000;
92
+ for (const [id, row] of pendingStandaloneCleanup) {
93
+ const evidence = await cleanupReadiness(row, cleanupDeadline);
94
+ if (evidence.state !== "quiescent")
95
+ throw new RuntimeCleanupError(`Previous standalone probe cleanup is unverified: ${evidence.reason}. Restore process visibility and retry.`);
96
+ pendingStandaloneCleanup.delete(id);
97
+ }
64
98
  const env = opts.env ?? process.env;
65
99
  const command = runtime === "claude" ? "claude" : "codex";
66
100
  const executable = resolveExecutablePath(command, env) ?? command;
@@ -70,7 +104,31 @@ async function probeRuntimeInternal(cwd, runtime, opts) {
70
104
  const phase = opts.phase ?? "readiness";
71
105
  const limit = opts.maxDiagnosticsBytes ?? RUNTIME_DIAGNOSTIC_LIMIT;
72
106
  const timeoutMs = opts.timeoutMs ?? RUNTIME_PROBE_TIMEOUT_MS;
73
- return new Promise((resolveResult) => {
107
+ const authority = opts.build?.authority;
108
+ if (opts.build && (!authority || opts.build.runId !== authority.runId || canonicalProject(opts.build.project) !== authority.project))
109
+ throw new BuildOwnershipError("stale-owner", "Invalid build readiness owner");
110
+ const ownerProject = opts.build?.project;
111
+ const useHost = Boolean(authority) || process.platform !== "win32";
112
+ const tag = randomUUID();
113
+ let windows;
114
+ if (process.platform === "win32" && !authority) {
115
+ try {
116
+ windows = windowsProbeCommand(tag, executable, args, cwd, processStartIdentity());
117
+ }
118
+ catch (error) {
119
+ throw new RuntimeCleanupError(`Windows process containment could not initialize: ${String(error)}`);
120
+ }
121
+ }
122
+ const ownership = ownerProject ? new WorkflowDb(ownerProject) : undefined;
123
+ let ownedProcess;
124
+ try {
125
+ if (authority && ownership)
126
+ ownedProcess = ownership.beginOwnedPreparationProcess(authority, tag, true);
127
+ }
128
+ finally {
129
+ ownership?.close();
130
+ }
131
+ return new Promise((resolveResult, rejectResult) => {
74
132
  let output = Buffer.alloc(0);
75
133
  let timedOut = false;
76
134
  let settled = false;
@@ -83,11 +141,72 @@ async function probeRuntimeInternal(cwd, runtime, opts) {
83
141
  }
84
142
  catch { /* non-authoritative diagnostics */ }
85
143
  };
86
- const spawnOpts = { cwd, env, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" };
87
- const child = spawn(executable, args, spawnOpts);
144
+ const spawnOpts = { cwd: useHost ? process.cwd() : cwd, env: { ...(useHost ? { ...process.env, RAFI_PROBE_ENV: JSON.stringify(env) } : env), RAFI_PROBE_OWNER: tag, ...(windows ? { RAFI_WINDOWS_PROBE_CONFIG: windows.config } : {}) }, stdio: useHost ? ["ignore", "pipe", "pipe", "ipc"] : ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" };
145
+ const gate = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./readinessGate.ts" : "./readinessGate.js", import.meta.url));
146
+ const helperNodeArgs = import.meta.url.endsWith(".ts") ? ["--import", import.meta.resolve("tsx")] : [];
147
+ const child = spawn(useHost ? process.execPath : windows?.executable ?? executable, useHost ? [...helperNodeArgs, gate, ownerProject ?? "--standalone", tag, executable, cwd, ...args] : windows?.args ?? args, spawnOpts);
148
+ const childStart = child.pid ? processStartIdentity(child.pid) : undefined;
149
+ let startupError;
150
+ let providerSpawnError;
151
+ let providerResult;
152
+ let startupTimer;
153
+ if (useHost) {
154
+ startupTimer = setTimeout(() => { startupError = new RuntimeCleanupError("Readiness helper startup timed out"); abort(); }, 15_000);
155
+ child.on("message", message => {
156
+ if (!message || typeof message !== "object" || !("kind" in message))
157
+ return;
158
+ if (message.kind === "rafi-probe-result") {
159
+ const reported = message;
160
+ if ((reported.exitCode === null || Number.isInteger(reported.exitCode)) && (reported.signal === null || typeof reported.signal === "string")) {
161
+ providerResult = reported;
162
+ setTimeout(() => { if (!settled) {
163
+ child.stdout?.destroy();
164
+ child.stderr?.destroy();
165
+ void finish(providerResult.exitCode, providerResult.signal);
166
+ } }, 100).unref();
167
+ }
168
+ return;
169
+ }
170
+ if (message.kind === "rafi-probe-spawn-error") {
171
+ const reported = message;
172
+ providerSpawnError = Object.assign(new Error(reported.message ?? "Provider could not start"), { code: reported.code });
173
+ return;
174
+ }
175
+ if (message.kind !== "rafi-probe-registered")
176
+ return;
177
+ let journal;
178
+ try {
179
+ if (authority && ownedProcess)
180
+ journal = new WorkflowDb(ownerProject, undefined, { runId: authority.runId });
181
+ if (cancelled || timedOut || settled) {
182
+ if (authority && ownedProcess)
183
+ journal?.revokeReadinessHelper(ownedProcess, authority);
184
+ child.kill("SIGKILL");
185
+ return;
186
+ }
187
+ if (authority && ownedProcess)
188
+ journal.authorizeReadinessHelper(authority, ownedProcess, child.pid);
189
+ child.send("rafi-probe-authorized", error => { if (error) {
190
+ startupError = error;
191
+ abort();
192
+ } });
193
+ clearTimeout(startupTimer);
194
+ }
195
+ catch (error) {
196
+ startupError = new RuntimeCleanupError(`Readiness authorization failed: ${String(error)}`);
197
+ abort();
198
+ }
199
+ finally {
200
+ journal?.close();
201
+ }
202
+ });
203
+ }
88
204
  const notifySupervisor = (active) => {
89
205
  if (process.send && process.connected && process.env.RAFI_BUILD_WORKER_RUN && child.pid) {
90
- process.send({ kind: "rafi-readiness-child", active, pid: child.pid, processStart: active ? processStartIdentity(child.pid) : undefined });
206
+ try {
207
+ process.send({ kind: "rafi-readiness-child", active, tag, pid: child.pid, processStart: active ? processStartIdentity(child.pid) : undefined }, () => { });
208
+ }
209
+ catch { /* Durable ownership survives a disconnected supervisor. */ }
91
210
  }
92
211
  };
93
212
  notifySupervisor(true);
@@ -103,7 +222,7 @@ async function probeRuntimeInternal(cwd, runtime, opts) {
103
222
  child.stdout?.on("data", append);
104
223
  child.stderr?.on("data", append);
105
224
  const killOwnedGroup = (signal) => {
106
- if (!child.pid)
225
+ if (!child.pid || classifyProcess(child.pid, childStart).state !== "live")
107
226
  return;
108
227
  try {
109
228
  if (process.platform !== "win32")
@@ -114,6 +233,8 @@ async function probeRuntimeInternal(cwd, runtime, opts) {
114
233
  catch { /* owned process/group already exited */ }
115
234
  };
116
235
  const stop = () => {
236
+ if (settled || forceTimer)
237
+ return;
117
238
  trace(cancelled ? "cancelled" : "timeout", { pid: child.pid });
118
239
  killOwnedGroup("SIGTERM");
119
240
  forceTimer = setTimeout(() => {
@@ -128,12 +249,14 @@ async function probeRuntimeInternal(cwd, runtime, opts) {
128
249
  timer.unref();
129
250
  const abort = () => { cancelled = true; clearTimeout(timer); stop(); };
130
251
  ownedProbes.add(abort);
131
- const finish = (exitCode, signal, spawnError) => {
252
+ const finish = async (exitCode, signal, spawnError) => {
132
253
  if (settled)
133
254
  return;
134
255
  settled = true;
135
- ownedProbes.delete(abort);
136
- notifySupervisor(false);
256
+ if (providerResult) {
257
+ exitCode = providerResult.exitCode;
258
+ signal = providerResult.signal;
259
+ }
137
260
  clearTimeout(timer);
138
261
  // The direct child can close stdio while a descendant that ignored TERM
139
262
  // remains alive with redirected pipes. Complete cleanup before dropping
@@ -144,7 +267,71 @@ async function probeRuntimeInternal(cwd, runtime, opts) {
144
267
  }
145
268
  clearTimeout(forceTimer);
146
269
  opts.signal?.removeEventListener("abort", abort);
270
+ // Probe completion and process cleanup are independent. Clean on success too.
271
+ clearTimeout(startupTimer);
272
+ let cleaned = Boolean(spawnError && !child.pid);
273
+ let cleanupReason = "ownership reconciliation did not verify shutdown";
274
+ if (authority && ownedProcess) {
275
+ let journal;
276
+ try {
277
+ journal = new WorkflowDb(ownerProject, undefined, { runId: authority.runId });
278
+ const record = journal.readinessProcesses(authority.runId).find(row => row.id === ownedProcess);
279
+ if (record && JSON.parse(record.outcome_json ?? "{}").startup !== "authorized" && !cancelled && !timedOut)
280
+ startupError ??= new RuntimeCleanupError("Readiness helper exited before authorization");
281
+ const unresolved = await journal.reconcileReadiness(authority.runId, authority);
282
+ cleaned = !unresolved.includes(ownedProcess);
283
+ }
284
+ catch {
285
+ cleaned = false;
286
+ }
287
+ finally {
288
+ journal?.close();
289
+ }
290
+ }
291
+ else if (child.pid) {
292
+ const row = { id: tag, run_id: "standalone", owner: "standalone", pid: child.pid, process_start: childStart ?? null, host: (await import("node:os")).hostname(), state: "running", outcome_json: JSON.stringify({ protocol: windows ? "windows-job-v1" : "tagged-v2" }) };
293
+ const evidence = await cleanupReadiness(row);
294
+ cleanupReason = evidence.reason;
295
+ if (evidence.state !== "quiescent") {
296
+ pendingStandaloneCleanup.set(tag, row);
297
+ // The trusted holder exits on disconnect; retain the record until OS
298
+ // evidence proves it and all supported descendants have stopped.
299
+ if (child.connected)
300
+ child.disconnect();
301
+ }
302
+ cleaned = evidence.state === "quiescent";
303
+ }
304
+ trace("owned-child-cleanup", { pid: child.pid });
305
+ ownedProbes.delete(abort);
306
+ if (cleaned)
307
+ notifySupervisor(false);
147
308
  trace("settled", { exitCode, signal });
309
+ if (cleaned && authority && ownedProcess) {
310
+ let journal;
311
+ try {
312
+ journal = new WorkflowDb(ownerProject, undefined, { runId: authority.runId });
313
+ journal.finishOwnedPreparationProcess(authority, ownedProcess, Boolean(spawnError && !child.pid), { ready: !timedOut && !cancelled && exitCode === 0 && /(?:^|\s)OK(?:\s|$)/.test(output.toString("utf8")), exitCode, timedOut, cancelled });
314
+ cleaned = cleaned && journal.unresolvedPreparationProcesses(authority.runId).length === 0;
315
+ }
316
+ catch (error) {
317
+ cleaned = false;
318
+ trace("ownership-reconciliation-required");
319
+ }
320
+ finally {
321
+ journal?.close();
322
+ }
323
+ }
324
+ if (startupError) {
325
+ rejectResult(new RuntimeCleanupError(`${startupError.message}${cleaned ? "; cleanup verified; retry readiness" : authority ? "; cleanup requires rafi resume" : "; standalone cleanup is unverified; restore process visibility and retry"}`));
326
+ return;
327
+ }
328
+ if (!cleaned) {
329
+ rejectResult(new RuntimeCleanupError(authority
330
+ ? "Readiness process cleanup is unverified; use rafi resume in the owning project to reconcile before continuing"
331
+ : `Standalone probe cleanup is unverified (${cleanupReason}); restore process visibility and retry. No new probe will start until cleanup is verified.`));
332
+ return;
333
+ }
334
+ spawnError ??= providerSpawnError;
148
335
  const diagnostics = sanitizeDiagnostics(spawnError?.message
149
336
  ? `${spawnError.message}\n${output.toString("utf8")}`
150
337
  : output.toString("utf8"), limit);
@@ -171,7 +358,16 @@ async function probeRuntimeInternal(cwd, runtime, opts) {
171
358
  });
172
359
  };
173
360
  child.once("error", (error) => finish(null, null, error));
174
- child.once("exit", (code, signal) => trace("exit", { exitCode: code, signal }));
361
+ child.once("exit", (code, signal) => {
362
+ trace("exit", { exitCode: code, signal });
363
+ // Inherited pipes cannot keep a finished direct process waiting for the
364
+ // entire provider timeout. Drain buffered output, then clean descendants.
365
+ setTimeout(() => { if (!settled) {
366
+ child.stdout?.destroy();
367
+ child.stderr?.destroy();
368
+ void finish(code, signal);
369
+ } }, 100).unref();
370
+ });
175
371
  child.once("close", (code, signal) => { trace("stdio-closed", { exitCode: code, signal }); finish(code, signal); });
176
372
  opts.signal?.addEventListener("abort", abort, { once: true });
177
373
  if (opts.signal?.aborted)
@@ -23,6 +23,8 @@ export declare function canonicalSessionPath(path: string): string;
23
23
  * administrative Git directory (not in the checked-out tree), so normal edits
24
24
  * and commits do not change it while removal/recreation cannot inherit it.
25
25
  */
26
+ /** Existing identity only: graph reads must never initialize a worktree marker. */
27
+ export declare function readWorkspaceIdentity(cwd: string): string | undefined;
26
28
  export declare function captureWorkspaceIdentity(cwd: string): string | undefined;
27
29
  export declare function createProviderSessionRef(input: CreateProviderSessionRefInput): ProviderSessionRefV1;
28
30
  /** Identity used by accounting, compaction, settings acknowledgements, and mutation leases. */
@@ -17,6 +17,20 @@ export function canonicalSessionPath(path) {
17
17
  * administrative Git directory (not in the checked-out tree), so normal edits
18
18
  * and commits do not change it while removal/recreation cannot inherit it.
19
19
  */
20
+ /** Existing identity only: graph reads must never initialize a worktree marker. */
21
+ export function readWorkspaceIdentity(cwd) {
22
+ try {
23
+ const canonicalCwd = canonicalSessionPath(cwd);
24
+ const gitDir = canonicalSessionPath(execFileSync("git", ["rev-parse", "--absolute-git-dir"], { cwd: canonicalCwd, encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] }).trim());
25
+ const token = readFileSync(join(gitDir, "rafi-workspace-id-v1"), "utf8").trim();
26
+ if (!/^[0-9a-f-]{36}$/i.test(token))
27
+ return undefined;
28
+ return createHash("sha256").update(`${canonicalCwd}\0${gitDir}\0${token}`).digest("hex");
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
20
34
  export function captureWorkspaceIdentity(cwd) {
21
35
  try {
22
36
  const canonicalCwd = canonicalSessionPath(cwd);
@@ -79,7 +79,7 @@ export class RoleSessionController {
79
79
  }
80
80
  effectiveThreshold() {
81
81
  if (!this.thresholdController)
82
- return this.options.settings.auto_compact_threshold_percent ?? 50;
82
+ return this.options.settings.auto_compact_threshold_percent ?? 65;
83
83
  return this.thresholdController.effectiveThreshold();
84
84
  }
85
85
  async next(durableHandoff, nextSettings) {
@@ -781,11 +781,11 @@ export class ThresholdCompactionController {
781
781
  ? effective
782
782
  : undefined;
783
783
  }
784
- threshold() { return this.runOnlyThreshold ?? this.providerEffectiveThreshold ?? this.settings.auto_compact_threshold_percent ?? 50; }
784
+ threshold() { return this.runOnlyThreshold ?? this.providerEffectiveThreshold ?? this.settings.auto_compact_threshold_percent ?? 65; }
785
785
  now() { return this.options.now?.() ?? new Date(); }
786
786
  }
787
787
  function normalizeContextSettings(settings) {
788
- const threshold = settings.auto_compact_threshold_percent ?? 50;
788
+ const threshold = settings.auto_compact_threshold_percent ?? 65;
789
789
  const maximum = settings.compact_maximum ?? 10;
790
790
  if (!Number.isInteger(threshold) || threshold < 1 || threshold > 99)
791
791
  throw new Error("auto_compact_threshold_percent must be an integer from 1 to 99");
@@ -1,3 +1,4 @@
1
+ import { reconcileGraphSourceSelection } from "../graph/sources.js";
1
2
  import { execFile, execFileSync } from "node:child_process";
2
3
  import { promisify } from "node:util";
3
4
  import { createHash } from "node:crypto";
@@ -67,6 +68,12 @@ export function saveSourceRegistry(projectDir, registry) {
67
68
  writeFileSync(configPath, stringify(raw, { lineWidth: 100 }), "utf8");
68
69
  backfillUniqueTicketSourceIds(root, registry);
69
70
  ensurePrivateCacheIgnored(root);
71
+ try {
72
+ reconcileGraphSourceSelection(root);
73
+ }
74
+ catch (error) {
75
+ console.error(`rafi graph: registered-source selection unavailable: ${String(error)}. Existing graph evidence must not substitute a later capture.`);
76
+ }
70
77
  }
71
78
  export function addPendingSourceDescription(registry, description, now = new Date()) {
72
79
  const value = description.trim();
@@ -1,14 +1,18 @@
1
+ import { registerQaGraphJournalWriter } from "./qaDeliveryJournal.js";
2
+ import { normalizeTransferredGraphState } from "./graph/storage.js";
3
+ import { WorkflowReader } from "./workflowReader.js";
4
+ import { classifyProcess } from "./processIdentity.js";
1
5
  import Database from "better-sqlite3";
2
6
  import { registerHandbackWriter } from "./qaHandbackMigration.js";
3
7
  import { createHash, randomUUID } from "node:crypto";
4
- import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
8
+ import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
5
9
  import { tmpdir } from "node:os";
6
10
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
11
  import { spawnSync } from "node:child_process";
8
12
  import { gzipSync, gunzipSync } from "node:zlib";
9
13
  import { StateDb } from "./tickets/stateDb.js";
10
14
  import { DEFAULT_TICKETS_CONFIG } from "./tickets/config.js";
11
- import { WorkflowDb, readCurrentWorkflowLease, WORKFLOW_DB_FILE } from "./workflowDb.js";
15
+ import { WorkflowDb, WORKFLOW_DB_FILE } from "./workflowDb.js";
12
16
  import { OBSERVABILITY_DB_FILE } from "./observability.js";
13
17
  export const STATE_BUNDLE_FORMAT = "rafi-state-bundle.v1";
14
18
  export const STATE_MANIFEST_PATH = "rafi-state-manifest.v1.json";
@@ -154,6 +158,29 @@ export async function importStateBundle(projectDir, bundleFile, opts = {}) {
154
158
  mkdirSync(dirname(out), { recursive: true });
155
159
  writeFileSync(out, Buffer.from(entry.data, "base64"));
156
160
  }
161
+ const workflowPath = join(staged, WORKFLOW_DB_FILE);
162
+ if (existsSync(workflowPath)) {
163
+ const preview = new Database(workflowPath, { readonly: true, fileMustExist: true });
164
+ try {
165
+ if (preview.pragma("integrity_check", { simple: true }) !== "ok" || preview.pragma("foreign_key_check").length)
166
+ throw new Error("Imported workflow database integrity failure");
167
+ if (Number(preview.pragma("user_version", { simple: true })) > 4)
168
+ throw new Error("Imported workflow requires a newer Rafi writer");
169
+ if (tableExists(preview, "build_work_scope"))
170
+ for (const { name } of preview.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'qa_%'").all()) {
171
+ if (!/^qa_[a-z_]+$/.test(name))
172
+ throw new Error("Unexpected imported QA table");
173
+ const columns = preview.prepare(`PRAGMA table_info(${name})`).all();
174
+ if (columns.some(column => column.name === "run_id") && columns.some(column => column.name === "ticket_id") && preview.prepare(`SELECT 1 FROM ${name} q WHERE NOT EXISTS(SELECT 1 FROM build_work_scope s WHERE s.run_id=q.run_id AND s.work_id=q.ticket_id) LIMIT 1`).get())
175
+ throw new Error(`Imported QA work is outside the ownership registry: ${name}`);
176
+ }
177
+ }
178
+ finally {
179
+ preview.close();
180
+ rmSync(workflowPath + "-shm", { force: true });
181
+ rmSync(workflowPath + "-wal", { force: true });
182
+ }
183
+ }
157
184
  const backup = backupExistingState(root, opts.now ?? new Date());
158
185
  backupDir = backup?.durableBackupDir;
159
186
  rollbackDir = backup?.snapshotDir;
@@ -237,12 +264,18 @@ function assertDbBackedTickets(root) {
237
264
  }
238
265
  }
239
266
  function assertNoLiveLease(root, operation = "export") {
240
- const lease = readCurrentWorkflowLease(root);
241
- if (!lease)
242
- return;
243
- const ageMs = Date.now() - new Date(lease.heartbeatAt).getTime();
244
- if (ageMs <= 45_000)
245
- throw new Error(`refusing to ${operation} while live workflow lease exists for run ${lease.runId}; stop or pause the active run first, then retry the state ${operation}`);
267
+ const reader = new WorkflowReader(root);
268
+ try {
269
+ const admission = reader.buildAdmission();
270
+ if (admission && classifyProcess(admission.pid, admission.processStart, admission.host).state !== "dead")
271
+ throw new Error(`refusing to ${operation} while live or unknown build admission exists for run ${admission.runId}`);
272
+ const lease = reader.currentLease();
273
+ if (lease && classifyProcess(lease.pid, lease.processStart, lease.host).state !== "dead")
274
+ throw new Error(`refusing to ${operation} while live workflow lease exists for run ${lease.runId}; stop or pause the active run first, then retry the state ${operation}`);
275
+ }
276
+ finally {
277
+ reader.close();
278
+ }
246
279
  }
247
280
  function runCurrentMigrations(root) {
248
281
  if (existsSync(join(root, DEFAULT_TICKETS_CONFIG.paths.stateDb))) {
@@ -288,6 +321,15 @@ async function stageSqliteBackup(root, rel, dest) {
288
321
  const db = new Database(source, { readonly: true, fileMustExist: true });
289
322
  try {
290
323
  await db.backup(dest);
324
+ if (rel === WORKFLOW_DB_FILE) {
325
+ const stagedDb = new Database(dest);
326
+ try {
327
+ normalizeTransferredGraphState(stagedDb);
328
+ }
329
+ finally {
330
+ stagedDb.close();
331
+ }
332
+ }
291
333
  }
292
334
  finally {
293
335
  db.close();
@@ -398,8 +440,43 @@ function rewriteWorkflowDb(root, sourceRoot) {
398
440
  if (!existsSync(path))
399
441
  return;
400
442
  const db = new Database(path);
443
+ normalizeTransferredGraphState(db);
401
444
  registerHandbackWriter(db);
445
+ registerQaGraphJournalWriter(db);
446
+ db.function("rafi_work_authority", () => 1);
447
+ if (tableExists(db, "build_project_identity")) {
448
+ const prior = db.prepare("SELECT project_id,canonical_root FROM build_project_identity WHERE singleton=1").get();
449
+ db.transaction(() => {
450
+ db.exec("CREATE TABLE IF NOT EXISTS build_project_rebindings(sequence INTEGER PRIMARY KEY AUTOINCREMENT,project_id TEXT NOT NULL,previous_root TEXT NOT NULL,new_root TEXT NOT NULL,rebound_at TEXT NOT NULL)");
451
+ for (const action of ["INSERT", "UPDATE", "DELETE"])
452
+ db.exec(`CREATE TRIGGER IF NOT EXISTS rebinding_protocol_${action} BEFORE ${action} ON build_project_rebindings BEGIN SELECT CASE WHEN rafi_writer_protocol()<>4 THEN RAISE(ABORT,'incompatible project rebinding writer') END; END`);
453
+ for (const action of ["UPDATE", "DELETE"])
454
+ db.exec(`CREATE TRIGGER IF NOT EXISTS rebinding_immutable_${action} BEFORE ${action} ON build_project_rebindings BEGIN SELECT RAISE(ABORT,'Project rebinding history is immutable'); END`);
455
+ db.prepare("INSERT INTO build_project_rebindings(project_id,previous_root,new_root,rebound_at) VALUES(?,?,?,?)").run(prior.project_id, prior.canonical_root, realpathSync(root), new Date().toISOString());
456
+ db.prepare("UPDATE build_project_identity SET canonical_root=? WHERE singleton=1").run(realpathSync(root));
457
+ })();
458
+ }
459
+ // This connection only rewrites an offline imported snapshot. It cannot
460
+ // dispatch; preserve imported ownership as unknown instead of adopting it.
461
+ const importedOwner = tableExists(db, "build_admission") ? db.prepare("SELECT record_json FROM build_admission WHERE singleton=1").get() : undefined;
462
+ const owner = importedOwner ? JSON.parse(importedOwner.record_json) : undefined;
463
+ db.function("rafi_protocol_v3", () => 1);
464
+ db.function("rafi_build_lease_owner", () => "");
465
+ db.function("rafi_build_lease_generation", () => -1);
466
+ db.function("rafi_build_writer_run", () => "");
467
+ db.function("rafi_build_writer_token", () => owner?.token ?? "");
402
468
  try {
469
+ if (owner) {
470
+ db.exec("CREATE TABLE IF NOT EXISTS imported_build_ownership(record_json TEXT NOT NULL,imported_at TEXT NOT NULL)");
471
+ db.prepare("INSERT INTO imported_build_ownership VALUES(?,?)").run(importedOwner.record_json, new Date().toISOString());
472
+ db.prepare("UPDATE build_admission SET record_json=? WHERE singleton=1").run(JSON.stringify({ ...owner, project: root, host: `imported:${owner.host}` }));
473
+ }
474
+ if (tableExists(db, "build_retry_lineage"))
475
+ db.transaction(() => {
476
+ db.exec("DROP TRIGGER IF EXISTS build_lineage_immutable");
477
+ db.prepare("UPDATE build_retry_lineage SET project=? WHERE project=?").run(root, sourceRoot);
478
+ db.exec("CREATE TRIGGER build_lineage_immutable BEFORE UPDATE ON build_retry_lineage BEGIN SELECT RAISE(ABORT,'Retry lineage is immutable'); END;");
479
+ })();
403
480
  rewriteSqlJsonColumn(db, "workflow_runs", "run_id", ["original_work_json", "remaining_work_json", "state_json"], sourceRoot, root);
404
481
  rewriteSqlJsonColumn(db, "branch_resume_sessions", "rowid", ["session_json"], sourceRoot, root);
405
482
  if (tableExists(db, "qa_recovery_heads")) {
@@ -90,6 +90,7 @@ export declare class RoleStatusAdapter implements BuilderAdapter {
90
90
  restoreNativeCompactions(compactions: NativeCompaction[]): void;
91
91
  contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
92
92
  contextUsage(): Promise<ContextUsage | undefined>;
93
+ graphRuntimeSettings(): Pick<import("./adapters/types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
93
94
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
94
95
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
95
96
  events(): AsyncIterable<BuilderEvent>;