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.
- package/README.md +29 -5
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +24 -1
- package/dist/buildRuns.js +216 -70
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -10
- package/dist/index.js +4 -1
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +293 -41
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- 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
|
+
}
|
package/dist/runtimeAuth.d.ts
CHANGED
|
@@ -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>;
|
package/dist/runtimeAuth.js
CHANGED
|
@@ -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;
|
package/dist/runtimeReadiness.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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) =>
|
|
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. */
|
package/dist/sessionIdentity.js
CHANGED
|
@@ -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);
|
package/dist/sessionLifecycle.js
CHANGED
|
@@ -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 ??
|
|
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 ??
|
|
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 ??
|
|
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();
|
package/dist/stateTransfer.js
CHANGED
|
@@ -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,
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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")) {
|
package/dist/statusReporter.d.ts
CHANGED
|
@@ -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>;
|