@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.74
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/CHANGELOG.md +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +470 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +120 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +211 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +223 -215
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +592 -154
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- package/tools/repair-paused-run-status.mjs +59 -0
package/dist/services/runs.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
6
6
|
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
|
-
import { ensureDir, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
8
|
+
import { ensureDir, canonicalPath, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
9
9
|
import { appendAudit } from "./audit.js";
|
|
10
10
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
11
11
|
import { buildRunFlowGuidance } from "./flow-guidance.js";
|
|
@@ -15,8 +15,10 @@ import { refreshRunSessionProjection } from "./run-projection.js";
|
|
|
15
15
|
import { resolveCanonRoot } from "./canon.js";
|
|
16
16
|
import { bindCurrentEngineToRun } from "./engines.js";
|
|
17
17
|
import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
|
|
18
|
-
import {
|
|
18
|
+
import { assertSameSessionMerge, loadExecutionRoutingInput, resolveStageTransition, snapshotAgentProfiles } from "./execution-policy.js";
|
|
19
|
+
import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
|
|
19
20
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
21
|
+
import { flowCommand } from "./stage-pause.js";
|
|
20
22
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
21
23
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
22
24
|
const runIdType = "RUN";
|
|
@@ -37,20 +39,22 @@ const allowedRunFlowKinds = [
|
|
|
37
39
|
];
|
|
38
40
|
export function startFlowRun(context, input) {
|
|
39
41
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
42
|
+
const flowKind = parseRunFlowKind(input.flowKind);
|
|
43
|
+
const vnext = flowKind === "vnext_protocolize" || flowKind === "vnext_specify";
|
|
44
|
+
if (input.executionRoutingFile !== undefined && !vnext)
|
|
45
|
+
throw new AppError("usage", "Execution routing input is supported only for vNext RUNs", 2);
|
|
46
|
+
const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
|
|
47
|
+
const executionProfile = vnext ? snapshotVnextExecutionProfile(projectRoot, context.ddFlowHome, routingInput) : undefined;
|
|
40
48
|
const flowContract = input.flowKind === "mb-upgrade"
|
|
41
49
|
? loadUpgradeFlowContract(context, projectRoot)
|
|
42
50
|
: loadProjectFlowContract(projectRoot);
|
|
43
51
|
registerProject(context, { root: projectRoot });
|
|
44
52
|
const project = requireProjectByRoot(context, projectRoot);
|
|
45
53
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot ?? projectRoot);
|
|
46
|
-
const flowKind = parseRunFlowKind(input.flowKind);
|
|
47
54
|
const slug = normalizeSlug(input.slug);
|
|
48
55
|
const runId = nextRunId(context, project.id, slug);
|
|
49
56
|
const { shortId } = parseFullEntityId(runId);
|
|
50
57
|
const now = context.now();
|
|
51
|
-
const executionProfile = (flowKind === "vnext_protocolize" || flowKind === "vnext_specify")
|
|
52
|
-
? snapshotVnextExecutionProfile(projectRoot)
|
|
53
|
-
: undefined;
|
|
54
58
|
const flowFlags = resolveFlowFlags(flowContract, {
|
|
55
59
|
flowKind,
|
|
56
60
|
...(input.preset ? { preset: input.preset } : {}),
|
|
@@ -126,7 +130,9 @@ export function startFlowRun(context, input) {
|
|
|
126
130
|
};
|
|
127
131
|
ensureDir(path.dirname(runtimePath));
|
|
128
132
|
ensureDir(runHome);
|
|
129
|
-
|
|
133
|
+
if (routingInput)
|
|
134
|
+
fs.writeFileSync(path.join(runHome, "execution-routing.input.json"), routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
135
|
+
bindCurrentEngineToRun(context, { projectRoot, runId, runRoot: runHome });
|
|
130
136
|
const persistedIndex = persistedVnextIndex(index);
|
|
131
137
|
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
|
|
132
138
|
context.db.run(`INSERT INTO runs
|
|
@@ -173,10 +179,10 @@ export function startFlowRun(context, input) {
|
|
|
173
179
|
});
|
|
174
180
|
return { ok: true, run: flowRunSummary(requireRunById(context, project.id, runId)), index };
|
|
175
181
|
}
|
|
176
|
-
function snapshotVnextExecutionProfile(projectRoot) {
|
|
182
|
+
function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput) {
|
|
177
183
|
const profile = loadVnextExecutionProfile(projectRoot);
|
|
178
184
|
const sourcePath = executionProfilePath(projectRoot);
|
|
179
|
-
return {
|
|
185
|
+
return freezeRunExecutionRouting(ddFlowHome, {
|
|
180
186
|
schema_id: "dd-flow/run-execution-profile@1",
|
|
181
187
|
source_path: sourcePath,
|
|
182
188
|
source_checksum: crypto.createHash("sha256").update(fs.readFileSync(sourcePath)).digest("hex"),
|
|
@@ -188,9 +194,41 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
188
194
|
merge_delivery: profile.merge_delivery,
|
|
189
195
|
merge_cleanup: profile.merge_cleanup,
|
|
190
196
|
stop_target: profile.stop_target,
|
|
191
|
-
code_bootstrap: profile.code_bootstrap
|
|
197
|
+
code_bootstrap: profile.code_bootstrap,
|
|
198
|
+
...(profile.execution ? { execution: profile.execution } : {})
|
|
192
199
|
}
|
|
193
|
-
};
|
|
200
|
+
}, routingInput);
|
|
201
|
+
}
|
|
202
|
+
/** Fresh RUN and fresh stage-entry experiments freeze through the same resolver. */
|
|
203
|
+
export function freezeRunExecutionRouting(ddFlowHome, base, input) {
|
|
204
|
+
const execution = input?.execution ?? base.settings.execution;
|
|
205
|
+
const profiles = execution ? snapshotAgentProfiles(ddFlowHome, execution) : undefined;
|
|
206
|
+
if (execution && profiles && base.settings.merge_mode === "same_session" && base.settings.stop_target === "merge_completed") {
|
|
207
|
+
const mode = base.settings.code_review_mode;
|
|
208
|
+
assertSameSessionMerge({ ddFlowHome, policy: execution, profiles, previousStages: mode === "off" ? ["code"] : mode === "auto" ? ["code", "code-review"] : ["code-review"] });
|
|
209
|
+
}
|
|
210
|
+
return { ...base, ...(profiles ? { agent_profiles: profiles } : {}),
|
|
211
|
+
...(input ? { routing_input: { source_path: input.source_path, source_checksum: input.source_checksum, artifact: "execution-routing.input.json" } } : {}),
|
|
212
|
+
settings: { ...base.settings, ...(execution ? { execution } : {}) } };
|
|
213
|
+
}
|
|
214
|
+
/** Called only after a stage-entry import has archived and detached source owners. */
|
|
215
|
+
export function importFlowRunExecutionProfile(context, input) {
|
|
216
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
217
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
218
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
219
|
+
const index = authoritativeIndex(run);
|
|
220
|
+
const lineage = JSON.parse(fs.readFileSync(input.lineageFile, "utf8"));
|
|
221
|
+
if (lineage.schema_id !== "dd-flow/snapshot-runtime-lineage@1" || lineage.run_id !== run.id || path.resolve(lineage.restored_runtime ?? "") !== path.resolve(context.ddFlowHome)
|
|
222
|
+
|| !vnextStages.some(stage => stage.id === input.stage)
|
|
223
|
+
|| (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_controllers'") && context.db.get("SELECT 1 FROM run_controllers WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id]))
|
|
224
|
+
|| index.stage_runs.some(stage => stage.stage === input.stage || !["done", "skipped"].includes(stage.status))
|
|
225
|
+
|| context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? AND status IN ('active','idle') LIMIT 1", [project.id, run.id])) {
|
|
226
|
+
throw new AppError("snapshot_execution_active", "Execution policy can be selected only at a detached stage-entry import", 1);
|
|
227
|
+
}
|
|
228
|
+
index.execution_profile = { ...input.profile, stage_entry_import: { stage: input.stage, lineage_file: input.lineageFile } };
|
|
229
|
+
index.updated_at = context.now();
|
|
230
|
+
persistRunState(context, project, run, index);
|
|
231
|
+
appendRunTimeline(run.run_root, { at: index.updated_at, type: "execution_policy_imported", stage: input.stage, lineage_file: input.lineageFile, source_checksum: input.profile.routing_input?.source_checksum });
|
|
194
232
|
}
|
|
195
233
|
/**
|
|
196
234
|
* A vNext route may materialize a feature checkout after its semantic
|
|
@@ -200,6 +238,7 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
200
238
|
export function rebindFlowRunWorkspace(context, input) {
|
|
201
239
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
202
240
|
const run = resolveRun(context, project.id, input.runId);
|
|
241
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
203
242
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
|
|
204
243
|
const index = authoritativeIndex(run);
|
|
205
244
|
const now = context.now();
|
|
@@ -229,7 +268,41 @@ export function getFlowRunStatus(context, input) {
|
|
|
229
268
|
const run = resolveRun(context, project.id, input.runId);
|
|
230
269
|
const index = authoritativeIndex(run);
|
|
231
270
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
232
|
-
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
271
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
272
|
+
}
|
|
273
|
+
/** A read-only projection of the next legal lifecycle action.
|
|
274
|
+
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
275
|
+
* transition from a model's prose or a hard-coded stage array. */
|
|
276
|
+
function runContinuation(context, run, index) {
|
|
277
|
+
if (["done", "failed", "cancelled"].includes(run.status))
|
|
278
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
279
|
+
const control = runControlDirective(context, run.project_id, run.id);
|
|
280
|
+
if (control)
|
|
281
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "operator_control", command: null, control };
|
|
282
|
+
const active = index.stage_runs.find((stage) => !["done", "skipped"].includes(stage.status));
|
|
283
|
+
if (active) {
|
|
284
|
+
const runningWork = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND status = 'running' ORDER BY updated_at DESC LIMIT 1", [run.project_id, run.id]);
|
|
285
|
+
const kind = active.status === "running" ? "continue_stage" : active.status === "paused" || active.status === "waiting_for_user" ? "paused" : "blocked";
|
|
286
|
+
return { kind, stage: active.stage, attempt: active.attempt ?? null, work_id: active.pause?.work_id ?? runningWork?.work_id ?? null, reason: active.status, command: null };
|
|
287
|
+
}
|
|
288
|
+
if (run.status === "waiting_for_user" || run.status === "paused")
|
|
289
|
+
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
290
|
+
if (run.status === "blocked")
|
|
291
|
+
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
292
|
+
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
293
|
+
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
294
|
+
const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
|
|
295
|
+
if (!next)
|
|
296
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
297
|
+
const command = `${flowCommand(context)} stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
|
|
298
|
+
const profile = index.execution_profile;
|
|
299
|
+
const execution = profile?.settings.execution ? profile.agent_profiles
|
|
300
|
+
? { status: "resolved", ...resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, ...(completed ? { fromStage: completed.stage } : {}), stage: next, stageSessionMode: profile.settings.stage_session_mode, mergeMode: profile.settings.merge_mode }) }
|
|
301
|
+
: { status: "unfrozen", reason: "execution_profile_not_frozen" }
|
|
302
|
+
: undefined;
|
|
303
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command, ...(execution ? { execution } : {}) };
|
|
304
|
+
}
|
|
305
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "no_vnext_continuation", command: null };
|
|
233
306
|
}
|
|
234
307
|
export function getFlowRunConfig(context, input) {
|
|
235
308
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -270,6 +343,7 @@ export function setFlowRunConfig(context, input) {
|
|
|
270
343
|
const reason = requiredPlain(input.reason, "reason");
|
|
271
344
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
272
345
|
const run = resolveRun(context, project.id, input.runId);
|
|
346
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
273
347
|
const index = authoritativeIndex(run);
|
|
274
348
|
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
275
349
|
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
@@ -292,6 +366,23 @@ export function setFlowRunConfig(context, input) {
|
|
|
292
366
|
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
293
367
|
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
294
368
|
}
|
|
369
|
+
/** Freeze an automatic review request once from already accepted policy or PLAN facts. */
|
|
370
|
+
export function freezeFlowRunReviewMode(context, input) {
|
|
371
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
372
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
373
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
374
|
+
const index = authoritativeIndex(run);
|
|
375
|
+
const settings = runSettings(index);
|
|
376
|
+
const key = input.review === "plan" ? "plan_review" : "code_review";
|
|
377
|
+
const current = settings[key];
|
|
378
|
+
if (current.mode !== "auto")
|
|
379
|
+
return;
|
|
380
|
+
const now = context.now();
|
|
381
|
+
index.settings = { ...settings, [key]: { mode: input.mode, source: input.source, reason: requiredPlain(input.reason, "reason"), updated_at: now } };
|
|
382
|
+
index.updated_at = now;
|
|
383
|
+
persistRunState(context, project, run, index);
|
|
384
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.review_mode_frozen", payload: { run_id: run.id, review: input.review, mode: input.mode, source: input.source, reason: input.reason } });
|
|
385
|
+
}
|
|
295
386
|
export function getFlowRunFlagsStatus(context, input) {
|
|
296
387
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
297
388
|
const run = resolveRun(context, project.id, input.runId);
|
|
@@ -321,8 +412,9 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
321
412
|
export function reviseFlowRunFlags(context, input) {
|
|
322
413
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
323
414
|
const run = resolveRun(context, project.id, input.runId);
|
|
415
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
324
416
|
const idempotencyKey = requiredPlain(input.idempotencyKey, "idempotency-key");
|
|
325
|
-
context.db.
|
|
417
|
+
context.db.beginWriteTransaction();
|
|
326
418
|
let committed = false;
|
|
327
419
|
try {
|
|
328
420
|
const existingMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
|
|
@@ -446,6 +538,7 @@ export function listFlowRuns(context, input) {
|
|
|
446
538
|
export function attachFlowRunStage(context, input) {
|
|
447
539
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
448
540
|
const run = resolveRun(context, project.id, input.runId);
|
|
541
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
449
542
|
const index = authoritativeIndex(run);
|
|
450
543
|
const now = context.now();
|
|
451
544
|
const stage = requiredPlain(input.stage, "stage");
|
|
@@ -454,7 +547,7 @@ export function attachFlowRunStage(context, input) {
|
|
|
454
547
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
455
548
|
assertVnextStageStart(run, index, stage, status, existing);
|
|
456
549
|
if (status === "running" && existing) {
|
|
457
|
-
archiveExistingStageAttempt(runArtifactRoot(run), dir);
|
|
550
|
+
archiveExistingStageAttempt(context, project.id, run.id, runArtifactRoot(run), dir);
|
|
458
551
|
}
|
|
459
552
|
const stageRun = {
|
|
460
553
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
@@ -490,9 +583,17 @@ export function attachFlowRunStage(context, input) {
|
|
|
490
583
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
491
584
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
492
585
|
}
|
|
586
|
+
/** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
|
|
587
|
+
export function withStageSettlement(context, operation) {
|
|
588
|
+
return context.db.writeTransaction(operation);
|
|
589
|
+
}
|
|
493
590
|
export function completeFlowRunStage(context, input) {
|
|
494
591
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
495
592
|
const run = resolveRun(context, project.id, input.runId);
|
|
593
|
+
if (["done", "failed", "skipped"].includes(input.status))
|
|
594
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.stage);
|
|
595
|
+
else
|
|
596
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
496
597
|
const index = authoritativeIndex(run);
|
|
497
598
|
const now = context.now();
|
|
498
599
|
const stage = requiredPlain(input.stage, "stage");
|
|
@@ -508,6 +609,7 @@ export function completeFlowRunStage(context, input) {
|
|
|
508
609
|
...(input.data ? { data: input.data } : {}),
|
|
509
610
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
|
|
510
611
|
...(input.report ? { report: input.report } : {}),
|
|
612
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
511
613
|
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
512
614
|
updated_at: now,
|
|
513
615
|
...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
|
|
@@ -550,6 +652,7 @@ export function completeFlowRunStage(context, input) {
|
|
|
550
652
|
export function pauseFlowRunStage(context, input) {
|
|
551
653
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
552
654
|
const run = resolveRun(context, project.id, input.runId);
|
|
655
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
553
656
|
const index = authoritativeIndex(run);
|
|
554
657
|
const stage = requiredPlain(input.stage, "stage");
|
|
555
658
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -585,6 +688,7 @@ export function pauseFlowRunStage(context, input) {
|
|
|
585
688
|
export function resumeFlowRunStage(context, input) {
|
|
586
689
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
587
690
|
const run = resolveRun(context, project.id, input.runId);
|
|
691
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
588
692
|
const index = authoritativeIndex(run);
|
|
589
693
|
const stage = requiredPlain(input.stage, "stage");
|
|
590
694
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -616,6 +720,7 @@ export function resumeFlowRunStage(context, input) {
|
|
|
616
720
|
export function blockFlowRunStage(context, input) {
|
|
617
721
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
618
722
|
const run = resolveRun(context, project.id, input.runId);
|
|
723
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
619
724
|
const index = authoritativeIndex(run);
|
|
620
725
|
const stage = requiredPlain(input.stage, "stage");
|
|
621
726
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -641,6 +746,7 @@ export function blockFlowRunStage(context, input) {
|
|
|
641
746
|
export function unblockFlowRunStage(context, input) {
|
|
642
747
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
643
748
|
const run = resolveRun(context, project.id, input.runId);
|
|
749
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
644
750
|
const index = authoritativeIndex(run);
|
|
645
751
|
const stage = requiredPlain(input.stage, "stage");
|
|
646
752
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -671,9 +777,26 @@ function operationalRunStatus(context, projectId, runId) {
|
|
|
671
777
|
const leaves = unfinished.filter((work) => !parents.has(work.work_id));
|
|
672
778
|
return leaves.length > 0 && leaves.every((work) => work.status === "paused") ? "paused" : "running";
|
|
673
779
|
}
|
|
780
|
+
/** Keep the logical RUN projection aligned with recovery authority. Stage/Work
|
|
781
|
+
* evidence and verdicts survive a reversible stop; terminal runs never reopen. */
|
|
782
|
+
export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
|
|
783
|
+
const run = resolveRun(context, projectId, runId);
|
|
784
|
+
if (!["running", "paused"].includes(run.status))
|
|
785
|
+
return;
|
|
786
|
+
const index = authoritativeIndex(run);
|
|
787
|
+
const status = paused || index.stage_runs.some(stage => ["paused", "waiting_for_user", "blocked"].includes(stage.status))
|
|
788
|
+
? "paused" : operationalRunStatus(context, projectId, runId);
|
|
789
|
+
if (run.status === status && index.status === status)
|
|
790
|
+
return;
|
|
791
|
+
index.status = status;
|
|
792
|
+
index.updated_at = context.now();
|
|
793
|
+
const project = requireProjectByRoot(context, run.project_root);
|
|
794
|
+
persistRunState(context, project, run, index);
|
|
795
|
+
}
|
|
674
796
|
export function completeFlowRun(context, input) {
|
|
675
|
-
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
797
|
+
const project = requireProjectByRoot(context, input.manualOverrideReason ? canonicalPath(input.projectRoot, false) : resolveProjectRoot(input.projectRoot));
|
|
676
798
|
const run = resolveRun(context, project.id, input.runId);
|
|
799
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
677
800
|
const index = authoritativeIndex(run);
|
|
678
801
|
const now = context.now();
|
|
679
802
|
const status = parseRunStatus(input.status);
|
|
@@ -727,16 +850,85 @@ export function advanceFlowRun(context, input) {
|
|
|
727
850
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
728
851
|
const run = resolveRun(context, project.id, input.runId);
|
|
729
852
|
const index = authoritativeIndex(run);
|
|
853
|
+
if (input.settlementStage && input.status === "running" && index.current_stage === input.settlementStage && index.stage_runs.some(stage => stage.stage === input.settlementStage && ["done", "skipped"].includes(stage.status)))
|
|
854
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.settlementStage);
|
|
855
|
+
else
|
|
856
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
857
|
+
const control = runControlDirective(context, project.id, run.id);
|
|
730
858
|
const now = context.now();
|
|
731
859
|
index.status = input.status;
|
|
732
860
|
index.verdict = input.verdict;
|
|
733
|
-
index.next_action = input.nextAction ?? null;
|
|
861
|
+
index.next_action = control ? null : input.nextAction ?? null;
|
|
734
862
|
index.updated_at = now;
|
|
735
863
|
persistRunState(context, project, run, index);
|
|
736
|
-
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status:
|
|
737
|
-
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_progressed", run_id: run.id, status:
|
|
864
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status: index.status, verdict: input.verdict, next_action: index.next_action } });
|
|
865
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_progressed", run_id: run.id, status: index.status, next_action: index.next_action });
|
|
738
866
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
739
|
-
return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
867
|
+
return { ok: true, ...(control ? { control } : {}), run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* The only legal backward edge in vNext: an integration gate exposed a
|
|
871
|
+
* product defect, so the source must be repaired and reviewed again. Normal
|
|
872
|
+
* stage start remains fail-closed for completed stages.
|
|
873
|
+
*/
|
|
874
|
+
export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
875
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
876
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
877
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
878
|
+
const index = authoritativeIndex(run);
|
|
879
|
+
const priorRepair = index.variables?.["merge.source_repair"];
|
|
880
|
+
if (priorRepair?.merge_request_id === input.mergeRequestId && priorRepair.cycle)
|
|
881
|
+
return { run_id: run.id, cycle: priorRepair.cycle };
|
|
882
|
+
const code = index.stage_runs.find((item) => item.stage === "code");
|
|
883
|
+
const review = index.stage_runs.find((item) => item.stage === "code-review");
|
|
884
|
+
const merge = index.stage_runs.find((item) => item.stage === "merge");
|
|
885
|
+
if (!code || code.status !== "done" || !review || review.status !== "done" || !merge || merge.status !== "running") {
|
|
886
|
+
throw new AppError("invalid_merge_repair_state", "Source repair requires accepted CODE/CODE-REVIEW and a running MERGE attempt", 2, {
|
|
887
|
+
run_id: run.id,
|
|
888
|
+
code: code?.status ?? null,
|
|
889
|
+
code_review: review?.status ?? null,
|
|
890
|
+
merge: merge?.status ?? null
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
const root = runArtifactRoot(run);
|
|
894
|
+
for (const stage of ["code", "code-review", "merge"])
|
|
895
|
+
archiveExistingStageAttempt(context, project.id, run.id, root, vnextStageDirectory(stage));
|
|
896
|
+
const now = context.now();
|
|
897
|
+
const restarted = {
|
|
898
|
+
...code,
|
|
899
|
+
status: "running",
|
|
900
|
+
started_at: now,
|
|
901
|
+
updated_at: now,
|
|
902
|
+
attempt: nextAttempt(code)
|
|
903
|
+
};
|
|
904
|
+
delete restarted.completed_at;
|
|
905
|
+
delete restarted.duration_ms;
|
|
906
|
+
delete restarted.stage_report;
|
|
907
|
+
delete restarted.data;
|
|
908
|
+
delete restarted.report;
|
|
909
|
+
delete restarted.artifact_aliases;
|
|
910
|
+
const cycle = `MRR-${input.mergeRequestId}`;
|
|
911
|
+
index.stage_runs = index.stage_runs.filter((item) => item.stage !== "code" && item.stage !== "code-review" && item.stage !== "merge");
|
|
912
|
+
index.stage_runs.push(restarted);
|
|
913
|
+
index.variables = { ...(index.variables ?? {}), "merge.source_repair": { cycle, merge_request_id: input.mergeRequestId, prepared_at: now } };
|
|
914
|
+
index.status = "running";
|
|
915
|
+
index.verdict = "merge_source_repair";
|
|
916
|
+
index.next_action = "start_code_repair";
|
|
917
|
+
index.current_stage = "code";
|
|
918
|
+
index.updated_at = now;
|
|
919
|
+
index.attempts = index.stage_runs.map((item) => ({
|
|
920
|
+
stage: item.stage,
|
|
921
|
+
attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1,
|
|
922
|
+
root: item.dir,
|
|
923
|
+
archive: null,
|
|
924
|
+
status: item.status,
|
|
925
|
+
started_at: item.started_at ?? now,
|
|
926
|
+
finished_at: item.completed_at ?? null
|
|
927
|
+
}));
|
|
928
|
+
persistRunState(context, project, run, index);
|
|
929
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
|
|
930
|
+
appendRunTimeline(root, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
931
|
+
return { run_id: run.id, cycle };
|
|
740
932
|
}
|
|
741
933
|
function closeOpenStagesForOverride(index, status, now) {
|
|
742
934
|
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
@@ -1002,6 +1194,10 @@ function vnextSpecifyGuidance(run, index) {
|
|
|
1002
1194
|
};
|
|
1003
1195
|
}
|
|
1004
1196
|
function persistRunState(context, project, run, index, options = {}) {
|
|
1197
|
+
// Fenced productive admission is a logical pause, even while physical work
|
|
1198
|
+
// drains. Late admitted Stage results must not revive a stopped RUN.
|
|
1199
|
+
if (index.status === "running" && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1200
|
+
index.status = "paused";
|
|
1005
1201
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
1006
1202
|
const previousRuntime = readRuntimeSnapshot(runtimePath);
|
|
1007
1203
|
const runtimeRevision = Math.max(index.runtime_revision ?? 0, previousRuntime?.runtime_revision ?? 0) + 1;
|
|
@@ -1012,22 +1208,30 @@ function persistRunState(context, project, run, index, options = {}) {
|
|
|
1012
1208
|
}
|
|
1013
1209
|
const persistedIndex = persistedVnextIndex(index);
|
|
1014
1210
|
ensureDir(path.dirname(runtimePath));
|
|
1015
|
-
|
|
1016
|
-
|
|
1211
|
+
context.db.exec("SAVEPOINT persist_run_state");
|
|
1212
|
+
try {
|
|
1213
|
+
context.db.run(`UPDATE runs
|
|
1017
1214
|
SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?,
|
|
1018
1215
|
updated_at = ?, completed_at = ?
|
|
1019
1216
|
WHERE project_id = ? AND id = ?`, [
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1217
|
+
index.status,
|
|
1218
|
+
index.verdict,
|
|
1219
|
+
index.next_action,
|
|
1220
|
+
JSON.stringify(persistedIndex),
|
|
1221
|
+
runtimePath,
|
|
1222
|
+
runtimePath,
|
|
1223
|
+
index.updated_at,
|
|
1224
|
+
index.completed_at ?? null,
|
|
1225
|
+
project.id,
|
|
1226
|
+
run.id
|
|
1227
|
+
]);
|
|
1228
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, runtimeRevision, history));
|
|
1229
|
+
context.db.exec("RELEASE persist_run_state");
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
context.db.exec("ROLLBACK TO persist_run_state; RELEASE persist_run_state");
|
|
1233
|
+
throw error;
|
|
1234
|
+
}
|
|
1031
1235
|
}
|
|
1032
1236
|
/** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
|
|
1033
1237
|
function persistedVnextIndex(index) {
|
|
@@ -1072,6 +1276,7 @@ function materializedRunVariables(index) {
|
|
|
1072
1276
|
function setRunVariable(context, input, value) {
|
|
1073
1277
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1074
1278
|
const run = resolveRun(context, project.id, input.runId);
|
|
1279
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
1075
1280
|
const index = authoritativeIndex(run);
|
|
1076
1281
|
if (index.variables?.[input.key] === value)
|
|
1077
1282
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
|
|
@@ -1187,7 +1392,7 @@ function upsertStage(index, stageRun) {
|
|
|
1187
1392
|
}
|
|
1188
1393
|
index.stage_runs.sort((a, b) => a.order - b.order);
|
|
1189
1394
|
}
|
|
1190
|
-
function resolveRun(context, projectId, idOrAlias) {
|
|
1395
|
+
export function resolveRun(context, projectId, idOrAlias) {
|
|
1191
1396
|
if (isFullEntityId(idOrAlias)) {
|
|
1192
1397
|
return requireRunById(context, projectId, idOrAlias);
|
|
1193
1398
|
}
|
|
@@ -1245,7 +1450,7 @@ function flowRunSummary(run) {
|
|
|
1245
1450
|
runtime_path: run.runtime_path,
|
|
1246
1451
|
run_dir: run.run_dir,
|
|
1247
1452
|
run_index_path: run.run_index_path,
|
|
1248
|
-
|
|
1453
|
+
run_root: run.run_root,
|
|
1249
1454
|
layout_version: run.layout_version ?? null,
|
|
1250
1455
|
artifact_root_kind: run.artifact_root_kind ?? null,
|
|
1251
1456
|
created_at: run.created_at,
|
|
@@ -1315,24 +1520,55 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1315
1520
|
}
|
|
1316
1521
|
return fs.realpathSync(absolute);
|
|
1317
1522
|
}
|
|
1318
|
-
|
|
1523
|
+
/** Move one completed stage attempt and update only known durable path columns.
|
|
1524
|
+
* Receipt bytes remain immutable; readers resolve their new location through the DB. */
|
|
1525
|
+
export function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
|
|
1319
1526
|
const currentStageDir = path.join(runRoot, stageDir);
|
|
1320
1527
|
if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
|
|
1321
1528
|
return;
|
|
1322
1529
|
}
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1530
|
+
const journal = path.join(runRoot, `.archive-${stageDir}.json`);
|
|
1531
|
+
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
1532
|
+
const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
|
|
1533
|
+
if (entries.length === 0 && !pending) {
|
|
1325
1534
|
return;
|
|
1326
1535
|
}
|
|
1327
1536
|
let attempt = 1;
|
|
1328
1537
|
while (fs.existsSync(path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`))) {
|
|
1329
1538
|
attempt += 1;
|
|
1330
1539
|
}
|
|
1331
|
-
const archiveDir = path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1540
|
+
const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1541
|
+
if (!pending) {
|
|
1542
|
+
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1543
|
+
const prior = stored ? JSON.parse(stored.index_json) : null;
|
|
1544
|
+
writeJsonFile(journal, { archiveDir, entries, stage_run: prior?.stage_runs.find((stage) => stage.dir === stageDir) ?? null });
|
|
1545
|
+
}
|
|
1332
1546
|
ensureDir(archiveDir);
|
|
1333
1547
|
for (const entry of entries) {
|
|
1334
|
-
|
|
1548
|
+
const source = path.join(currentStageDir, entry);
|
|
1549
|
+
const target = path.join(archiveDir, entry);
|
|
1550
|
+
if (fs.existsSync(source)) {
|
|
1551
|
+
if (fs.existsSync(target))
|
|
1552
|
+
throw new AppError("archive_conflict", "Both live and archived artifact exist; preserve both for recovery", 2, { source, target });
|
|
1553
|
+
fs.renameSync(source, target);
|
|
1554
|
+
}
|
|
1555
|
+
else if (!fs.existsSync(target))
|
|
1556
|
+
throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
|
|
1335
1557
|
}
|
|
1558
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
1559
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
1560
|
+
const rewrite = (table, column, where, values) => {
|
|
1561
|
+
context.db.run(`UPDATE ${table} SET ${column} = ? || substr(${column}, ?) WHERE ${column} LIKE ? AND substr(${column}, ?, 4) != 'try-' AND ${where}`, [newPrefix, oldPrefix.length + 1, `${oldPrefix}%`, oldPrefix.length + 1, ...values]);
|
|
1562
|
+
};
|
|
1563
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
1564
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
1565
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
1566
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
1567
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
1568
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
1569
|
+
// Retain the original stage facts with its immutable artifacts; current
|
|
1570
|
+
// stage_runs stays a projection of the current attempt, not a second ledger.
|
|
1571
|
+
fs.renameSync(journal, path.join(archiveDir, "archive.json"));
|
|
1336
1572
|
}
|
|
1337
1573
|
function writeJsonFile(file, value) {
|
|
1338
1574
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -1340,7 +1576,9 @@ function writeJsonFile(file, value) {
|
|
|
1340
1576
|
fs.renameSync(tmpFile, file);
|
|
1341
1577
|
}
|
|
1342
1578
|
function runArtifactRoot(run) {
|
|
1343
|
-
|
|
1579
|
+
if (!run.run_root)
|
|
1580
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: run.id });
|
|
1581
|
+
return run.run_root;
|
|
1344
1582
|
}
|
|
1345
1583
|
export function gitFacts(workspaceRoot) {
|
|
1346
1584
|
const read = (args) => {
|
|
@@ -1355,3 +1593,5 @@ export function gitFacts(workspaceRoot) {
|
|
|
1355
1593
|
status: status === null ? "unavailable" : status.length > 0 ? "dirty" : "clean"
|
|
1356
1594
|
};
|
|
1357
1595
|
}
|
|
1596
|
+
import { assertRunMutationAllowed, assertStageSettlementAllowed, recoveryGuard } from "./run-recovery.js";
|
|
1597
|
+
import { runControlDirective } from "./run-control.js";
|