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
package/dist/statusReporter.js
CHANGED
|
@@ -189,6 +189,7 @@ export class RoleStatusAdapter {
|
|
|
189
189
|
restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
|
|
190
190
|
contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
|
|
191
191
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
192
|
+
graphRuntimeSettings() { return this.adapter.graphRuntimeSettings?.(); }
|
|
192
193
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
193
194
|
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
|
194
195
|
events() { return this.adapter.events(); }
|
|
@@ -7,6 +7,12 @@ export declare function superviseStart(projectDir: string, runId: string, policy
|
|
|
7
7
|
steps?: string;
|
|
8
8
|
stacks?: string;
|
|
9
9
|
detach?: boolean;
|
|
10
|
+
launchToken?: string;
|
|
11
|
+
startArgs?: string[];
|
|
10
12
|
}): Promise<boolean>;
|
|
11
13
|
/** Deliberately narrow: established work requires the normal recovery flow. */
|
|
12
14
|
export declare function canRestartPreparation(db: WorkflowDb, runId: string): boolean;
|
|
15
|
+
/** Cleanup cannot hide the original failure or erase an uncertain launch. */
|
|
16
|
+
export declare function finishStartAdmission(projectDir: string, runId: string): void;
|
|
17
|
+
/** Re-exec inherits run selection, but transfers only the newly reserved capability. */
|
|
18
|
+
export declare function normalizeNestedLaunchArguments(args: readonly string[], token: string): string[];
|
package/dist/supervisedStart.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { maintainBuildAdmission, stopBuildAdmissionHeartbeat, launchDigest, localBuildAuthority } from "./buildAdmission.js";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
1
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
4
|
import { closeSync, mkdirSync, openSync } from "node:fs";
|
|
3
5
|
import { join } from "node:path";
|
|
4
6
|
import { DurableSupervisor } from "./supervisor.js";
|
|
5
7
|
import { WorkflowDb } from "./workflowDb.js";
|
|
6
|
-
import { processStartIdentity } from "./processIdentity.js";
|
|
7
8
|
import { cancelOwnedRuntimeProbes } from "./runtimeReadiness.js";
|
|
8
9
|
/** Worker IPC is created by the parent; an environment flag alone cannot bypass supervision. */
|
|
9
10
|
export function supervisedRunId() {
|
|
@@ -11,6 +12,28 @@ export function supervisedRunId() {
|
|
|
11
12
|
}
|
|
12
13
|
/** Both ordinary and branch starts enter through this process boundary. */
|
|
13
14
|
export async function superviseStart(projectDir, runId, policy, selection) {
|
|
15
|
+
const preparation = new WorkflowDb(projectDir);
|
|
16
|
+
const invocationDigest = launchDigest(selection?.startArgs ?? ["start", projectDir]);
|
|
17
|
+
try {
|
|
18
|
+
const environmentToken = process.env.RAFI_BUILD_LAUNCH_TOKEN;
|
|
19
|
+
if (environmentToken && selection?.launchToken && environmentToken !== selection.launchToken)
|
|
20
|
+
throw new Error("Conflicting build launch contexts");
|
|
21
|
+
const launchToken = environmentToken ?? selection?.launchToken;
|
|
22
|
+
if (launchToken) {
|
|
23
|
+
preparation.claimBuildLaunch(runId, launchToken, supervisedRunId() ? "worker" : "coordinator", invocationDigest);
|
|
24
|
+
delete process.env.RAFI_BUILD_LAUNCH_TOKEN;
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
preparation.acquireBuildAdmission(runId, policy.supervisorEnabled && process.argv.includes("start") ? "coordinator" : "worker");
|
|
28
|
+
const run = preparation.ensureRun(runId, "build");
|
|
29
|
+
policy = preparation.freezeAutonomyPolicy(runId, policy);
|
|
30
|
+
if (selection?.startArgs && !run.state.runId && !run.state.startArgs)
|
|
31
|
+
preparation.transition(runId, { checkpoint: run.checkpoint, state: { ...run.state, startArgs: selection.startArgs }, event: "build_preparation_saved" });
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
preparation.close();
|
|
35
|
+
}
|
|
36
|
+
maintainBuildAdmission(localBuildAuthority(projectDir));
|
|
14
37
|
if (supervisedRunId()) {
|
|
15
38
|
const heartbeat = setInterval(() => { if (process.connected)
|
|
16
39
|
process.send?.({ kind: "rafi-worker-heartbeat", runId }); }, 5_000);
|
|
@@ -60,7 +83,7 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
60
83
|
if (!policy.supervisorEnabled) {
|
|
61
84
|
const db = new WorkflowDb(projectDir);
|
|
62
85
|
try {
|
|
63
|
-
db.putSupervisorState(runId, {
|
|
86
|
+
db.putSupervisorState(runId, { generation: 0, workerGeneration: 0, checkpointRestarts: 0, runRestarts: 0, ...db.supervisorState(runId), status: "disabled" });
|
|
64
87
|
}
|
|
65
88
|
finally {
|
|
66
89
|
db.close();
|
|
@@ -74,19 +97,55 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
74
97
|
args.push("--steps", selection.steps);
|
|
75
98
|
if (!args.some(arg => arg === "--stacks" || arg.startsWith("--stacks=")) && selection?.stacks)
|
|
76
99
|
args.push("--stacks", selection.stacks);
|
|
100
|
+
const gate = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./buildLaunchGate.ts" : "./buildLaunchGate.js", import.meta.url));
|
|
101
|
+
const gatedArgs = (token) => [...process.execArgv, gate, process.argv[1], projectDir, token, ...normalizeNestedLaunchArguments(args.slice(process.execArgv.length + 1), token)];
|
|
77
102
|
if (selection?.detach && !process.env.RAFI_DETACHED_SUPERVISOR_RUN) {
|
|
78
103
|
const directory = join(projectDir, ".rafi", "logs");
|
|
79
104
|
mkdirSync(directory, { recursive: true });
|
|
80
105
|
const path = join(directory, `supervisor-${runId}.log`);
|
|
81
106
|
const fd = openSync(path, "a", 0o600);
|
|
107
|
+
const launchDb = new WorkflowDb(projectDir);
|
|
108
|
+
const authority = localBuildAuthority(projectDir);
|
|
109
|
+
const launch = launchDb.reserveBuildLaunch(authority, "coordinator", invocationDigest, "registered-v2");
|
|
110
|
+
launchDb.dispatchBuildLaunch(authority, launch.token);
|
|
82
111
|
try {
|
|
83
|
-
const parent = spawn(process.execPath,
|
|
84
|
-
env: { ...process.env, RAFI_DETACHED_SUPERVISOR_RUN: runId }, stdio: ["ignore", fd, fd] });
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
const parent = spawn(process.execPath, gatedArgs(launch.token), { cwd: process.cwd(), detached: true,
|
|
113
|
+
env: { ...process.env, RAFI_DETACHED_SUPERVISOR_RUN: runId, RAFI_BUILD_LAUNCH_TOKEN: launch.token }, stdio: ["ignore", fd, fd, "ipc"] });
|
|
114
|
+
try {
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
const timer = setTimeout(() => { parent.kill("SIGTERM"); reject(new Error("Detached supervisor startup timed out; use rafi resume to reconcile")); }, 15_000);
|
|
117
|
+
parent.once("message", () => {
|
|
118
|
+
try {
|
|
119
|
+
launchDb.acknowledgeBuildLaunchChild(authority, launch.token, parent.pid);
|
|
120
|
+
parent.send("rafi-launch-ack", error => { clearTimeout(timer); if (error)
|
|
121
|
+
reject(error);
|
|
122
|
+
else
|
|
123
|
+
resolve(); });
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
parent.kill("SIGTERM");
|
|
128
|
+
reject(error);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
parent.once("error", error => { clearTimeout(timer); if (!parent.pid)
|
|
132
|
+
launchDb.failBuildLaunch(authority, launch.token); reject(error); });
|
|
133
|
+
parent.once("exit", () => { clearTimeout(timer); launchDb.reconcileBuildLaunches(runId, authority); reject(new Error("Detached supervisor exited during startup; use rafi resume")); });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
// Startup rejection can be followed by a late exit/error. Remove DB
|
|
138
|
+
// callbacks before the outer finally closes the connection in either case.
|
|
139
|
+
parent.removeAllListeners("exit");
|
|
140
|
+
parent.removeAllListeners("message");
|
|
141
|
+
parent.removeAllListeners("error");
|
|
142
|
+
parent.on("error", () => { });
|
|
143
|
+
parent.unref();
|
|
144
|
+
}
|
|
87
145
|
console.log(`rafi: launched supervisor for ${runId}; output: ${path}`);
|
|
88
146
|
}
|
|
89
147
|
finally {
|
|
148
|
+
launchDb.close();
|
|
90
149
|
closeSync(fd);
|
|
91
150
|
}
|
|
92
151
|
return true;
|
|
@@ -95,16 +154,39 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
95
154
|
db.ensureRun(runId, "build");
|
|
96
155
|
db.freezeAutonomyPolicy(runId, policy);
|
|
97
156
|
let exitCode = 2;
|
|
98
|
-
|
|
157
|
+
let stopRequestSource;
|
|
158
|
+
const supervisor = new DurableSupervisor({ projectDir, runId, policy, admission: localBuildAuthority(projectDir),
|
|
99
159
|
checkpoint: () => db.getRun(runId)?.checkpoint ?? "preparing",
|
|
100
160
|
spawnWorker: generation => {
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
let authority = localBuildAuthority(projectDir);
|
|
162
|
+
if (db.buildAdmission()?.token !== authority.token) {
|
|
163
|
+
authority = db.reacquireBuildCoordinator(runId);
|
|
164
|
+
}
|
|
165
|
+
const launch = db.reserveBuildLaunch(authority, "worker", invocationDigest, "registered-v2");
|
|
166
|
+
db.dispatchBuildLaunch(authority, launch.token);
|
|
167
|
+
const child = spawn(process.execPath, gatedArgs(launch.token), {
|
|
168
|
+
cwd: process.cwd(), env: { ...process.env, RAFI_BUILD_WORKER_RUN: runId, RAFI_BUILD_WORKER_GENERATION: String(generation), RAFI_BUILD_LAUNCH_TOKEN: launch.token },
|
|
103
169
|
stdio: ["inherit", "inherit", "inherit", "ipc"], detached: process.platform !== "win32",
|
|
104
170
|
});
|
|
171
|
+
const startup = setTimeout(() => child.kill("SIGTERM"), 15_000);
|
|
172
|
+
child.on("message", message => {
|
|
173
|
+
if (!message || typeof message !== "object" || !("kind" in message) || message.kind !== "rafi-launch-registered")
|
|
174
|
+
return;
|
|
175
|
+
try {
|
|
176
|
+
db.acknowledgeBuildLaunchChild(authority, launch.token, child.pid);
|
|
177
|
+
child.send("rafi-launch-ack", () => { });
|
|
178
|
+
clearTimeout(startup);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
child.kill("SIGTERM");
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
child.once("exit", () => clearTimeout(startup));
|
|
185
|
+
child.once("error", () => clearTimeout(startup));
|
|
105
186
|
let lastHeartbeat = Date.now();
|
|
106
187
|
let stopping = false;
|
|
107
188
|
const readinessChildren = new Map();
|
|
189
|
+
const readinessTags = new Map();
|
|
108
190
|
let killTimer;
|
|
109
191
|
const stop = () => {
|
|
110
192
|
if (stopping)
|
|
@@ -120,8 +202,10 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
120
202
|
}
|
|
121
203
|
catch { /* exited */ }
|
|
122
204
|
};
|
|
123
|
-
const watchdog = setInterval(() => { if (Date.now() - lastHeartbeat > 120_000)
|
|
124
|
-
|
|
205
|
+
const watchdog = setInterval(() => { if (Date.now() - lastHeartbeat > 120_000) {
|
|
206
|
+
stopRequestSource = "heartbeat-timeout";
|
|
207
|
+
stop();
|
|
208
|
+
} }, 5_000);
|
|
125
209
|
watchdog.unref();
|
|
126
210
|
child.on("message", message => { if (message && typeof message === "object" && "kind" in message && message.kind === "rafi-worker-heartbeat")
|
|
127
211
|
lastHeartbeat = Date.now(); });
|
|
@@ -131,50 +215,78 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
131
215
|
const value = message;
|
|
132
216
|
if (!Number.isSafeInteger(value.pid) || Number(value.pid) <= 1)
|
|
133
217
|
return;
|
|
134
|
-
if (value.
|
|
218
|
+
if (typeof value.tag === "string")
|
|
219
|
+
readinessTags.set(Number(value.pid), value.tag);
|
|
220
|
+
if (value.active === false) {
|
|
135
221
|
readinessChildren.delete(Number(value.pid));
|
|
222
|
+
readinessTags.delete(Number(value.pid));
|
|
223
|
+
}
|
|
136
224
|
else if (typeof value.processStart === "string")
|
|
137
225
|
readinessChildren.set(Number(value.pid), value.processStart);
|
|
138
226
|
});
|
|
139
227
|
const result = new Promise(resolve => {
|
|
140
|
-
child.once("error", error => {
|
|
228
|
+
child.once("error", error => {
|
|
229
|
+
clearInterval(watchdog);
|
|
230
|
+
if (child.pid) {
|
|
231
|
+
stop();
|
|
232
|
+
return;
|
|
233
|
+
} // exit handler owns cleanup and settlement
|
|
234
|
+
db.failBuildLaunch(authority, launch.token);
|
|
235
|
+
resolve({ kind: "failed", detail: error.message });
|
|
236
|
+
});
|
|
141
237
|
child.once("exit", async (code, signal) => {
|
|
142
238
|
clearInterval(watchdog);
|
|
143
239
|
if (killTimer)
|
|
144
240
|
clearTimeout(killTimer);
|
|
145
241
|
exitCode = code ?? 2;
|
|
146
242
|
try {
|
|
243
|
+
const exitState = db.getRun(runId);
|
|
244
|
+
if (exitState)
|
|
245
|
+
db.transitionSupervisor(runId, generation, { checkpoint: exitState.checkpoint, event: "worker_exit", payload: { code, signal, stopRequested: stopping, source: stopRequestSource ?? "unknown", at: new Date().toISOString(), generation } });
|
|
246
|
+
// Durable records are authoritative even when the worker never sent IPC.
|
|
247
|
+
const deadline = Date.now() + 5000;
|
|
248
|
+
const owned = db.readinessProcesses(runId).filter(row => {
|
|
249
|
+
const launchChild = db.buildLaunch(launch.token)?.claimant;
|
|
250
|
+
return launchChild && row.owner === launchChild.token;
|
|
251
|
+
});
|
|
252
|
+
const readinessWasActive = owned.length > 0;
|
|
253
|
+
const readinessCleanup = owned.length ? await db.reconcileReadiness(runId, undefined, deadline) : [];
|
|
254
|
+
const readinessQuiescent = readinessCleanup.length === 0;
|
|
255
|
+
if (!readinessQuiescent) {
|
|
256
|
+
exitCode = 2;
|
|
257
|
+
resolve({ kind: "waiting_for_human" });
|
|
258
|
+
console.error(`rafi: readiness cleanup remains unresolved; use rafi resume (${readinessCleanup.join(", ")})`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const launchOutcome = db.reconcileBuildLaunches(runId, authority);
|
|
262
|
+
if (launchOutcome === "retired" && db.buildLaunch(launch.token)?.state === "failed") {
|
|
263
|
+
resolve({ kind: "crashed", detail: "Worker exited before claiming its launch; authorization retired safely" });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
147
266
|
if (signal || stopping) {
|
|
148
267
|
try {
|
|
149
268
|
process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
|
|
150
269
|
}
|
|
151
270
|
catch { /* owned group exited */ }
|
|
152
|
-
const readinessWasActive = readinessChildren.size > 0;
|
|
153
|
-
let readinessQuiescent = readinessWasActive;
|
|
154
|
-
const readinessCleanup = [];
|
|
155
|
-
for (const [pid, identity] of readinessChildren) {
|
|
156
|
-
const cleanup = await stopOwnedReadinessGroup(pid, identity);
|
|
157
|
-
if (cleanup !== "quiescent") {
|
|
158
|
-
readinessQuiescent = false;
|
|
159
|
-
readinessCleanup.push(cleanup);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
271
|
const workerQuiescent = await ownedGroupIsQuiescent(child.pid);
|
|
163
272
|
const preparationReconciled = canRestartPreparation(db, runId);
|
|
164
273
|
if (!stopping && readinessQuiescent && workerQuiescent && preparationReconciled) {
|
|
165
|
-
db.
|
|
274
|
+
db.transitionSupervisor(runId, generation, { status: "paused", checkpoint: "preparation-reconciled", event: "worker_preparation_reconciled", payload: { generation, signal, noImplementationAuthority: true } });
|
|
166
275
|
resolve({ kind: "crashed", detail: "Preparation worker exited before acquiring implementation authority; owned process group is quiescent" });
|
|
167
276
|
return;
|
|
168
277
|
}
|
|
169
278
|
// Process death does not establish that remote side effects stopped.
|
|
170
279
|
// Never replay an unacknowledged provider action automatically.
|
|
171
280
|
const uncertain = [...db.unresolvedRoleDispatches(runId, "builder"), ...db.unresolvedRoleDispatches(runId, "qa")];
|
|
172
|
-
db.
|
|
281
|
+
db.transitionSupervisor(runId, generation, { status: "paused", checkpoint: uncertain.length ? "uncertain-worker-dispatch" : "worker-interrupted", event: "worker_reconciliation_required", payload: { generation, signal, uncertainDispatches: uncertain.map(item => item.idempotencyKey), readinessWasActive, readinessQuiescent, workerQuiescent, preparationReconciled, readinessCleanup, readinessOwnershipVerified: [...readinessChildren.values()].every(value => value !== "unavailable") } });
|
|
173
282
|
resolve({ kind: "waiting_for_human" });
|
|
174
283
|
}
|
|
175
284
|
else {
|
|
176
285
|
const state = db.getRun(runId);
|
|
177
|
-
if (code === 0 && state?.status
|
|
286
|
+
if (code === 0 && state?.status === "cancelled") {
|
|
287
|
+
resolve({ kind: "completed" });
|
|
288
|
+
}
|
|
289
|
+
else if (code === 0 && state?.status !== "completed") {
|
|
178
290
|
exitCode = 2;
|
|
179
291
|
resolve({ kind: "waiting_for_human" });
|
|
180
292
|
}
|
|
@@ -191,15 +303,16 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
191
303
|
return { pid: child.pid, result, stop };
|
|
192
304
|
},
|
|
193
305
|
});
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
process.once("
|
|
306
|
+
const stopInt = () => { stopRequestSource = "parent-SIGINT"; supervisor.requestStop(); };
|
|
307
|
+
const stopTerm = () => { stopRequestSource = "parent-SIGTERM"; supervisor.requestStop(); };
|
|
308
|
+
process.once("SIGINT", stopInt);
|
|
309
|
+
process.once("SIGTERM", stopTerm);
|
|
197
310
|
try {
|
|
198
311
|
await supervisor.run();
|
|
199
312
|
}
|
|
200
313
|
finally {
|
|
201
|
-
process.off("SIGINT",
|
|
202
|
-
process.off("SIGTERM",
|
|
314
|
+
process.off("SIGINT", stopInt);
|
|
315
|
+
process.off("SIGTERM", stopTerm);
|
|
203
316
|
db.close();
|
|
204
317
|
}
|
|
205
318
|
process.exitCode = exitCode;
|
|
@@ -208,13 +321,7 @@ export async function superviseStart(projectDir, runId, policy, selection) {
|
|
|
208
321
|
/** Deliberately narrow: established work requires the normal recovery flow. */
|
|
209
322
|
export function canRestartPreparation(db, runId) {
|
|
210
323
|
const run = db.getRun(runId);
|
|
211
|
-
|
|
212
|
-
return false;
|
|
213
|
-
if (run.state.version !== undefined || db.pendingHumanDecisions(runId).length || db.operations(runId).length || db.handoffs(runId).length)
|
|
214
|
-
return false;
|
|
215
|
-
if (db.roleMutationLease(runId, "builder") || db.roleMutationLease(runId, "qa"))
|
|
216
|
-
return false;
|
|
217
|
-
return db.branchResumeSessions(false).length === 0;
|
|
324
|
+
return Boolean(run && ["durable-baseline", "preparing", "preparation-reconciled"].includes(run.checkpoint) && db.preparationEligibility(runId).eligible);
|
|
218
325
|
}
|
|
219
326
|
async function ownedGroupIsQuiescent(pid) {
|
|
220
327
|
if (process.platform === "win32")
|
|
@@ -245,32 +352,41 @@ async function ownedGroupIsQuiescent(pid) {
|
|
|
245
352
|
} while (Date.now() < until);
|
|
246
353
|
return false;
|
|
247
354
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return `readiness process identity changed: ${expectedStart} -> ${actual}`;
|
|
256
|
-
}
|
|
257
|
-
catch (error) {
|
|
258
|
-
if (error.code !== "ESRCH")
|
|
259
|
-
return `readiness liveness unavailable: ${String(error)}`;
|
|
260
|
-
}
|
|
355
|
+
/** Cleanup cannot hide the original failure or erase an uncertain launch. */
|
|
356
|
+
export function finishStartAdmission(projectDir, runId) {
|
|
357
|
+
const authority = localBuildAuthority(projectDir);
|
|
358
|
+
if (!authority || authority.runId !== runId)
|
|
359
|
+
return;
|
|
360
|
+
stopBuildAdmissionHeartbeat(authority);
|
|
361
|
+
const db = new WorkflowDb(projectDir);
|
|
261
362
|
try {
|
|
262
|
-
|
|
363
|
+
if (db.buildAdmission()?.token === authority.token) {
|
|
364
|
+
try {
|
|
365
|
+
db.releaseBuildAdmission(authority);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
console.error(`rafi: retaining build ownership for reconciliation: ${String(error)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
263
371
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return `readiness group cleanup unavailable: ${String(error)}`;
|
|
372
|
+
finally {
|
|
373
|
+
db.close();
|
|
267
374
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
375
|
+
}
|
|
376
|
+
/** Re-exec inherits run selection, but transfers only the newly reserved capability. */
|
|
377
|
+
export function normalizeNestedLaunchArguments(args, token) {
|
|
378
|
+
const result = [];
|
|
379
|
+
for (let i = 0; i < args.length; i++) {
|
|
380
|
+
if (args[i] === "--launch-token") {
|
|
381
|
+
i++;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (args[i].startsWith("--launch-token="))
|
|
385
|
+
continue;
|
|
386
|
+
result.push(args[i]);
|
|
273
387
|
}
|
|
274
|
-
|
|
275
|
-
|
|
388
|
+
// Ordinary supervised start gets its run from the acknowledged IPC gate.
|
|
389
|
+
if (result.some(arg => arg === "--preparation-run" || arg === "--recover-run" || arg.startsWith("--preparation-run=") || arg.startsWith("--recover-run=")))
|
|
390
|
+
result.push("--launch-token", token);
|
|
391
|
+
return result;
|
|
276
392
|
}
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BuildAdmission } from "./buildAdmission.js";
|
|
1
2
|
import type { ResolvedAutonomyPolicy, SupervisorState } from "rafi-spec";
|
|
2
3
|
export type WorkerOutcome = {
|
|
3
4
|
kind: "completed";
|
|
@@ -21,6 +22,7 @@ export interface DurableSupervisorOptions {
|
|
|
21
22
|
projectDir: string;
|
|
22
23
|
runId: string;
|
|
23
24
|
policy: ResolvedAutonomyPolicy;
|
|
25
|
+
admission?: BuildAdmission;
|
|
24
26
|
spawnWorker: (generation: number) => Promise<SupervisorWorkerHandle> | SupervisorWorkerHandle;
|
|
25
27
|
checkpoint: () => string;
|
|
26
28
|
sleep?: (milliseconds: number) => Promise<void>;
|
package/dist/supervisor.js
CHANGED
|
@@ -25,7 +25,12 @@ export class DurableSupervisor {
|
|
|
25
25
|
db.close();
|
|
26
26
|
return { kind: "stopped" };
|
|
27
27
|
}
|
|
28
|
+
let ownedAdmission;
|
|
28
29
|
try {
|
|
30
|
+
if (this.options.admission)
|
|
31
|
+
db.assertBuildAdmission(this.options.admission);
|
|
32
|
+
else
|
|
33
|
+
ownedAdmission = db.acquireBuildAdmission(this.options.runId, "coordinator");
|
|
29
34
|
state = db.atomic(() => {
|
|
30
35
|
for (const other of db.runningSupervisors()) {
|
|
31
36
|
if (other.runId !== this.options.runId && other.state.pid && processLooksLive(other.state.pid, other.state.processStart))
|
|
@@ -43,7 +48,13 @@ export class DurableSupervisor {
|
|
|
43
48
|
});
|
|
44
49
|
}
|
|
45
50
|
catch (error) {
|
|
46
|
-
|
|
51
|
+
try {
|
|
52
|
+
if (ownedAdmission)
|
|
53
|
+
db.releaseBuildAdmission(ownedAdmission);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
47
58
|
throw error;
|
|
48
59
|
}
|
|
49
60
|
const saveState = () => db.atomic(() => {
|
|
@@ -88,6 +99,13 @@ export class DurableSupervisor {
|
|
|
88
99
|
saveState();
|
|
89
100
|
return outcome;
|
|
90
101
|
}
|
|
102
|
+
const eligibility = db.preparationEligibility(this.options.runId);
|
|
103
|
+
if (!eligibility.eligible) {
|
|
104
|
+
state = { ...state, status: "waiting_for_human" };
|
|
105
|
+
saveState();
|
|
106
|
+
db.transitionSupervisor(this.options.runId, state.workerGeneration, { status: "paused", checkpoint: "worker-reconciliation-required", event: "worker_restart_withheld", payload: { reasons: eligibility.reasons } });
|
|
107
|
+
return { kind: "waiting_for_human" };
|
|
108
|
+
}
|
|
91
109
|
const checkpointLimit = this.options.policy.limits.workerRestartsPerCheckpoint;
|
|
92
110
|
const runLimit = this.options.policy.limits.workerRestartsPerRun;
|
|
93
111
|
if (state.checkpointRestarts >= checkpointLimit || state.runRestarts >= runLimit) {
|
|
@@ -105,7 +123,13 @@ export class DurableSupervisor {
|
|
|
105
123
|
}
|
|
106
124
|
finally {
|
|
107
125
|
clearInterval(heartbeat);
|
|
108
|
-
|
|
126
|
+
try {
|
|
127
|
+
if (ownedAdmission && db.buildAdmission()?.token === ownedAdmission.token)
|
|
128
|
+
db.releaseBuildAdmission(ownedAdmission);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
db.close();
|
|
132
|
+
}
|
|
109
133
|
}
|
|
110
134
|
}
|
|
111
135
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Small, dependency-free terminal styling and display sanitization helpers. */
|
|
2
|
+
export type TerminalColorRole = "accent" | "success" | "warning" | "error" | "info";
|
|
3
|
+
export interface TerminalStyleStream {
|
|
4
|
+
readonly isTTY?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface TerminalStyleContext {
|
|
7
|
+
readonly stream?: TerminalStyleStream;
|
|
8
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
9
|
+
readonly platform?: NodeJS.Platform;
|
|
10
|
+
/** Declared data output is always plain, including FORCE_COLOR sessions. */
|
|
11
|
+
readonly machineReadable?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function terminalColorEnabled(context?: TerminalStyleContext): boolean;
|
|
14
|
+
export declare function styleTerminalText(role: TerminalColorRole, text: string, context?: TerminalStyleContext): string;
|
|
15
|
+
export declare function accent(text: string, context?: TerminalStyleContext): string;
|
|
16
|
+
export declare function success(text: string, context?: TerminalStyleContext): string;
|
|
17
|
+
export declare function warning(text: string, context?: TerminalStyleContext): string;
|
|
18
|
+
export declare function error(text: string, context?: TerminalStyleContext): string;
|
|
19
|
+
export declare function info(text: string, context?: TerminalStyleContext): string;
|
|
20
|
+
/** Remove terminal control sequences while retaining intended line breaks. */
|
|
21
|
+
export declare function sanitizeTerminalText(value: string): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Small, dependency-free terminal styling and display sanitization helpers. */
|
|
2
|
+
const ANSI = {
|
|
3
|
+
accent: "\x1b[36m",
|
|
4
|
+
success: "\x1b[32m",
|
|
5
|
+
warning: "\x1b[33m",
|
|
6
|
+
error: "\x1b[31m",
|
|
7
|
+
info: "\x1b[34m",
|
|
8
|
+
};
|
|
9
|
+
const RESET = "\x1b[39m";
|
|
10
|
+
export function terminalColorEnabled(context = {}) {
|
|
11
|
+
if (context.machineReadable)
|
|
12
|
+
return false;
|
|
13
|
+
const env = context.env ?? process.env;
|
|
14
|
+
const stream = context.stream ?? process.stdout;
|
|
15
|
+
if (Object.hasOwn(env, "NO_COLOR"))
|
|
16
|
+
return false;
|
|
17
|
+
if (env.TERM?.toLowerCase() === "dumb")
|
|
18
|
+
return false;
|
|
19
|
+
if (env.FORCE_COLOR === "0")
|
|
20
|
+
return false;
|
|
21
|
+
if (["1", "2", "3"].includes(env.FORCE_COLOR ?? ""))
|
|
22
|
+
return true;
|
|
23
|
+
if (!stream.isTTY)
|
|
24
|
+
return false;
|
|
25
|
+
// Modern Windows terminals advertise ANSI support. A bare legacy console TTY
|
|
26
|
+
// is not enough evidence, so leave it readable and unstyled.
|
|
27
|
+
if ((context.platform ?? process.platform) === "win32") {
|
|
28
|
+
return Boolean(env.WT_SESSION || env.ANSICON || env.ConEmuANSI === "ON" || env.TERM);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export function styleTerminalText(role, text, context = {}) {
|
|
33
|
+
const safeText = sanitizeTerminalText(text);
|
|
34
|
+
return terminalColorEnabled(context) ? `${ANSI[role]}${safeText}${RESET}` : safeText;
|
|
35
|
+
}
|
|
36
|
+
export function accent(text, context) { return styleTerminalText("accent", text, context); }
|
|
37
|
+
export function success(text, context) { return styleTerminalText("success", text, context); }
|
|
38
|
+
export function warning(text, context) { return styleTerminalText("warning", text, context); }
|
|
39
|
+
export function error(text, context) { return styleTerminalText("error", text, context); }
|
|
40
|
+
export function info(text, context) { return styleTerminalText("info", text, context); }
|
|
41
|
+
/** Remove terminal control sequences while retaining intended line breaks. */
|
|
42
|
+
export function sanitizeTerminalText(value) {
|
|
43
|
+
return value
|
|
44
|
+
// CSI, OSC, DCS/SOS/PM/APC, and two-byte ESC controls.
|
|
45
|
+
.replace(/\x1b\](?:[^\x07\x1b]|\x1b(?!\\))*(?:\x07|\x1b\\)?/gs, "")
|
|
46
|
+
.replace(/\x1b(?:[PX^_](?:.|\n)*?\x1b\\|\[[0-?]*[ -/]*[@-~]|[@-_])/g, "")
|
|
47
|
+
.replace(/\x9d[^\x07]*(?:\x07)?/g, "")
|
|
48
|
+
.replace(/\x9b[0-?]*[ -/]*[@-~]/g, "")
|
|
49
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
50
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { StructuredPlanV1 } from "rafi-spec";
|
|
|
2
2
|
import type { TicketDef } from "./tickets/ticketSchema.js";
|
|
3
3
|
import { type DeliveryConfig } from "./tickets/delivery.js";
|
|
4
4
|
export interface TicketSliceProposal {
|
|
5
|
+
qa_preparation?: TicketDef["qa_preparation"];
|
|
5
6
|
slice_ref: string;
|
|
6
7
|
title: string;
|
|
7
8
|
area: string;
|
package/dist/ticketPopulation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canonicalContractJson } from "./qaVerificationContract.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -30,6 +31,9 @@ export function validateTicketPopulationProposal(proposal, plan, existing) {
|
|
|
30
31
|
const planned = new Set(plan.slices.map((slice) => slice.slice_ref));
|
|
31
32
|
const mapped = new Set();
|
|
32
33
|
for (const ticket of proposal.tickets) {
|
|
34
|
+
const depth = plan.slices.find(slice => slice.slice_ref === ticket.slice_ref)?.qa_preparation;
|
|
35
|
+
if (ticket.qa_preparation && (!depth || canonicalContractJson(ticket.qa_preparation) !== canonicalContractJson(depth)))
|
|
36
|
+
issues.push(`ticket-maker cannot override planner QA depth for ${ticket.slice_ref}`);
|
|
33
37
|
if (!planned.has(ticket.slice_ref))
|
|
34
38
|
issues.push(`unknown slice mapping ${ticket.slice_ref}`);
|
|
35
39
|
if (mapped.has(ticket.slice_ref))
|
|
@@ -64,7 +68,7 @@ export function materializeTicketPopulation(proposal, plan, existing) {
|
|
|
64
68
|
const materialized = proposal.tickets.map((slice, index) => {
|
|
65
69
|
const retained = byPlanSlice.get(`${plan.plan_id}:${slice.slice_ref}`);
|
|
66
70
|
return {
|
|
67
|
-
...(retained ?? {}), id: sliceToTicket.get(slice.slice_ref), order: retained?.order ?? nextOrder(existing, index),
|
|
71
|
+
...(retained ?? {}), qa_preparation: structuredClone(plan.slices.find(item => item.slice_ref === slice.slice_ref)?.qa_preparation), id: sliceToTicket.get(slice.slice_ref), order: retained?.order ?? nextOrder(existing, index),
|
|
68
72
|
title: slice.title, area: slice.area, priority: slice.priority, size: slice.size, risk: slice.risk,
|
|
69
73
|
depends_on: slice.depends_on.map((ref) => sliceToTicket.get(ref)), summary: slice.summary,
|
|
70
74
|
acceptance: [...slice.acceptance], required_tests: [...slice.required_tests], likely_files: [...slice.likely_files],
|
package/dist/tickets/commands.js
CHANGED
|
@@ -259,8 +259,8 @@ export function cmdBlock(projectDir, ticketId, opts) {
|
|
|
259
259
|
ctx.db.upsertState(ticketId, {
|
|
260
260
|
status: "blocked",
|
|
261
261
|
blocked_by_json: JSON.stringify(merged),
|
|
262
|
-
blocker_type: opts.blockerType ?? "dependency",
|
|
263
|
-
blocker_notes: opts.unblockCriteria ??
|
|
262
|
+
blocker_type: opts.blockerType ?? (merged.length ? "dependency" : "external"),
|
|
263
|
+
blocker_notes: opts.unblockCriteria ?? opts.summary ?? "Resolve the external prerequisite before continuing",
|
|
264
264
|
first_blocked_at: state?.first_blocked_at ?? now,
|
|
265
265
|
last_checked_at: now,
|
|
266
266
|
last_worked_at: now,
|
package/dist/tickets/queue.js
CHANGED
|
@@ -31,7 +31,7 @@ export function buildNextQueue(ticketDefs, states, queueLimit) {
|
|
|
31
31
|
blockedBy: blockedBy.length ? blockedBy.join(", ") : "None",
|
|
32
32
|
size: ticket.size,
|
|
33
33
|
risk: ticket.risk,
|
|
34
|
-
nextAction: state?.next_action ?? defaultNextAction(ticket, displayStatus, blockedBy),
|
|
34
|
+
nextAction: state?.next_action ?? (displayStatus === "blocked" && !blockedBy.length ? `Resolve blocker: ${state?.blocker_notes ?? "external prerequisite requires recovery"}` : defaultNextAction(ticket, displayStatus, blockedBy)),
|
|
35
35
|
requiredTests: ticket.required_tests.join("; "),
|
|
36
36
|
evidence: state?.evidence ?? "N/A until implemented.",
|
|
37
37
|
likelyFiles: ticket.likely_files.length ? ticket.likely_files.join(", ") : "unknown",
|