@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111
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 +526 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -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/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -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 +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -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 +71 -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/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- 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/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
package/dist/services/runs.js
CHANGED
|
@@ -2,10 +2,12 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
6
|
+
import { reconcileRunStageReports } from "./stage-report-renderer.js";
|
|
5
7
|
import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
6
8
|
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
7
9
|
import { AppError } from "../shared/errors.js";
|
|
8
|
-
import { ensureDir, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
10
|
+
import { ensureDir, canonicalPath, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
9
11
|
import { appendAudit } from "./audit.js";
|
|
10
12
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
11
13
|
import { buildRunFlowGuidance } from "./flow-guidance.js";
|
|
@@ -15,8 +17,11 @@ import { refreshRunSessionProjection } from "./run-projection.js";
|
|
|
15
17
|
import { resolveCanonRoot } from "./canon.js";
|
|
16
18
|
import { bindCurrentEngineToRun } from "./engines.js";
|
|
17
19
|
import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
|
|
18
|
-
import {
|
|
20
|
+
import { loadVnextWorkspacePolicy } from "./vnext-workspace-policy.js";
|
|
21
|
+
import { assertSameSessionMerge, loadExecutionRoutingInput, resolveStageTransition, snapshotAgentProfiles } from "./execution-policy.js";
|
|
22
|
+
import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
|
|
19
23
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
24
|
+
import { flowCommand } from "./stage-pause.js";
|
|
20
25
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
21
26
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
22
27
|
const runIdType = "RUN";
|
|
@@ -35,22 +40,26 @@ const allowedRunFlowKinds = [
|
|
|
35
40
|
"review",
|
|
36
41
|
"custom"
|
|
37
42
|
];
|
|
38
|
-
export function
|
|
43
|
+
export function prepareFlowRunStart(context, input) {
|
|
44
|
+
const subjectId = requiredPlain(input.subjectId, "subject-id");
|
|
45
|
+
const subjectType = requiredPlain(input.subjectType, "subject-type");
|
|
46
|
+
const slug = normalizeSlug(input.slug);
|
|
47
|
+
for (const [name, value] of [["task profile", input.taskProfile], ["protocol overrides", input.protocolOverrides], ["RUN overrides", input.runOverrides]]) {
|
|
48
|
+
if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value)))
|
|
49
|
+
throw new AppError("validation", `${name} must be a JSON object`, 2);
|
|
50
|
+
}
|
|
39
51
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
52
|
+
const flowKind = parseRunFlowKind(input.flowKind);
|
|
53
|
+
const vnext = flowKind === "vnext_protocolize" || flowKind === "vnext_specify";
|
|
54
|
+
if (input.executionRoutingFile !== undefined && !vnext)
|
|
55
|
+
throw new AppError("usage", "Execution routing input is supported only for vNext RUNs", 2);
|
|
56
|
+
const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
|
|
57
|
+
const executionProfile = vnext ? snapshotVnextExecutionProfile(projectRoot, context.ddFlowHome, routingInput) : undefined;
|
|
40
58
|
const flowContract = input.flowKind === "mb-upgrade"
|
|
41
59
|
? loadUpgradeFlowContract(context, projectRoot)
|
|
42
60
|
: loadProjectFlowContract(projectRoot);
|
|
43
|
-
registerProject(context, { root: projectRoot });
|
|
44
|
-
const project = requireProjectByRoot(context, projectRoot);
|
|
45
61
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot ?? projectRoot);
|
|
46
|
-
const flowKind = parseRunFlowKind(input.flowKind);
|
|
47
|
-
const slug = normalizeSlug(input.slug);
|
|
48
|
-
const runId = nextRunId(context, project.id, slug);
|
|
49
|
-
const { shortId } = parseFullEntityId(runId);
|
|
50
62
|
const now = context.now();
|
|
51
|
-
const executionProfile = (flowKind === "vnext_protocolize" || flowKind === "vnext_specify")
|
|
52
|
-
? snapshotVnextExecutionProfile(projectRoot)
|
|
53
|
-
: undefined;
|
|
54
63
|
const flowFlags = resolveFlowFlags(flowContract, {
|
|
55
64
|
flowKind,
|
|
56
65
|
...(input.preset ? { preset: input.preset } : {}),
|
|
@@ -60,6 +69,14 @@ export function startFlowRun(context, input) {
|
|
|
60
69
|
snapshotRevision: 1,
|
|
61
70
|
now
|
|
62
71
|
});
|
|
72
|
+
return { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags };
|
|
73
|
+
}
|
|
74
|
+
export function startFlowRun(context, input, prepared = prepareFlowRunStart(context, input)) {
|
|
75
|
+
const { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags } = prepared;
|
|
76
|
+
registerProject(context, { root: projectRoot });
|
|
77
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
78
|
+
const runId = nextRunId(context, project.id, slug);
|
|
79
|
+
const { shortId } = parseFullEntityId(runId);
|
|
63
80
|
const runHome = projectRunHome(context.ddFlowHome, project.id, runId);
|
|
64
81
|
const runDirRelative = path.posix.join("runs", runId);
|
|
65
82
|
const runtimePath = projectRunJsonPath(context.ddFlowHome, project.id, runId);
|
|
@@ -69,8 +86,8 @@ export function startFlowRun(context, input) {
|
|
|
69
86
|
short_id: shortId,
|
|
70
87
|
flow_kind: flowKind,
|
|
71
88
|
subject: {
|
|
72
|
-
id:
|
|
73
|
-
title:
|
|
89
|
+
id: subjectId,
|
|
90
|
+
title: subjectId
|
|
74
91
|
},
|
|
75
92
|
project: {
|
|
76
93
|
id: project.id,
|
|
@@ -126,9 +143,10 @@ export function startFlowRun(context, input) {
|
|
|
126
143
|
};
|
|
127
144
|
ensureDir(path.dirname(runtimePath));
|
|
128
145
|
ensureDir(runHome);
|
|
146
|
+
if (routingInput)
|
|
147
|
+
fs.writeFileSync(path.join(runHome, "execution-routing.input.json"), routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
129
148
|
bindCurrentEngineToRun(context, { projectRoot, runId, runRoot: runHome });
|
|
130
149
|
const persistedIndex = persistedVnextIndex(index);
|
|
131
|
-
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
|
|
132
150
|
context.db.run(`INSERT INTO runs
|
|
133
151
|
(id, short_id, slug, project_id, project_root, workspace_root, flow_kind, subject_type, subject_id,
|
|
134
152
|
status, verdict, next_action, runtime_path, run_dir, run_index_path, run_home_path, run_root, layout_version, artifact_root_kind,
|
|
@@ -141,7 +159,7 @@ export function startFlowRun(context, input) {
|
|
|
141
159
|
project.root,
|
|
142
160
|
workspaceRoot,
|
|
143
161
|
flowKind,
|
|
144
|
-
|
|
162
|
+
subjectType,
|
|
145
163
|
index.subject.id,
|
|
146
164
|
index.status,
|
|
147
165
|
index.verdict,
|
|
@@ -162,8 +180,9 @@ export function startFlowRun(context, input) {
|
|
|
162
180
|
eventType: "flow_run.started",
|
|
163
181
|
payload: { run_id: runId, flow_kind: flowKind, subject: index.subject, workspace_root: workspaceRoot, run_home: runHome }
|
|
164
182
|
});
|
|
165
|
-
|
|
166
|
-
appendRunTimeline(
|
|
183
|
+
context.db.afterCommit(() => publishRunProjections(context, project.id, runId));
|
|
184
|
+
appendRunTimeline(context, project.id, runId, { at: now, type: "run_started", run_id: runId });
|
|
185
|
+
appendRunTimeline(context, project.id, runId, {
|
|
167
186
|
at: now,
|
|
168
187
|
type: "flow_flags_resolved",
|
|
169
188
|
run_id: runId,
|
|
@@ -173,10 +192,10 @@ export function startFlowRun(context, input) {
|
|
|
173
192
|
});
|
|
174
193
|
return { ok: true, run: flowRunSummary(requireRunById(context, project.id, runId)), index };
|
|
175
194
|
}
|
|
176
|
-
function snapshotVnextExecutionProfile(projectRoot) {
|
|
195
|
+
function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput) {
|
|
177
196
|
const profile = loadVnextExecutionProfile(projectRoot);
|
|
178
197
|
const sourcePath = executionProfilePath(projectRoot);
|
|
179
|
-
|
|
198
|
+
const frozen = freezeRunExecutionRouting(ddFlowHome, {
|
|
180
199
|
schema_id: "dd-flow/run-execution-profile@1",
|
|
181
200
|
source_path: sourcePath,
|
|
182
201
|
source_checksum: crypto.createHash("sha256").update(fs.readFileSync(sourcePath)).digest("hex"),
|
|
@@ -188,9 +207,47 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
188
207
|
merge_delivery: profile.merge_delivery,
|
|
189
208
|
merge_cleanup: profile.merge_cleanup,
|
|
190
209
|
stop_target: profile.stop_target,
|
|
191
|
-
code_bootstrap: profile.code_bootstrap
|
|
210
|
+
code_bootstrap: profile.code_bootstrap,
|
|
211
|
+
...(profile.execution ? { execution: profile.execution } : {})
|
|
192
212
|
}
|
|
193
|
-
};
|
|
213
|
+
}, routingInput);
|
|
214
|
+
if (frozen.settings.merge_mode === "same_session" && frozen.settings.stop_target === "merge_completed"
|
|
215
|
+
&& fs.existsSync(path.join(projectRoot, ".memory-bank", "dd-flow", "project-workspace.json"))
|
|
216
|
+
&& loadVnextWorkspacePolicy(projectRoot).workspace.route === "feature_worktree") {
|
|
217
|
+
throw new AppError("merge_session_workspace_conflict", "Feature-worktree MERGE targets the integration checkout; use server MERGE or a qualified new Session", 2, { project_root: projectRoot });
|
|
218
|
+
}
|
|
219
|
+
return frozen;
|
|
220
|
+
}
|
|
221
|
+
/** Fresh RUN and fresh stage-entry experiments freeze through the same resolver. */
|
|
222
|
+
export function freezeRunExecutionRouting(ddFlowHome, base, input) {
|
|
223
|
+
const execution = input?.execution ?? base.settings.execution;
|
|
224
|
+
const profiles = execution ? snapshotAgentProfiles(ddFlowHome, execution) : undefined;
|
|
225
|
+
if (execution && profiles && base.settings.merge_mode === "same_session" && base.settings.stop_target === "merge_completed") {
|
|
226
|
+
const mode = base.settings.code_review_mode;
|
|
227
|
+
assertSameSessionMerge({ ddFlowHome, policy: execution, profiles, previousStages: mode === "off" ? ["code"] : mode === "auto" ? ["code", "code-review"] : ["code-review"] });
|
|
228
|
+
}
|
|
229
|
+
return { ...base, ...(profiles ? { agent_profiles: profiles } : {}),
|
|
230
|
+
...(input ? { routing_input: { source_path: input.source_path, source_checksum: input.source_checksum, artifact: "execution-routing.input.json" } } : {}),
|
|
231
|
+
settings: { ...base.settings, ...(execution ? { execution } : {}) } };
|
|
232
|
+
}
|
|
233
|
+
/** Called only after a stage-entry import has archived and detached source owners. */
|
|
234
|
+
export function importFlowRunExecutionProfile(context, input) {
|
|
235
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
236
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
237
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
238
|
+
const index = authoritativeIndex(run);
|
|
239
|
+
const lineage = JSON.parse(fs.readFileSync(input.lineageFile, "utf8"));
|
|
240
|
+
if (lineage.schema_id !== "dd-flow/snapshot-runtime-lineage@1" || lineage.run_id !== run.id || path.resolve(lineage.restored_runtime ?? "") !== path.resolve(context.ddFlowHome)
|
|
241
|
+
|| !vnextStages.some(stage => stage.id === input.stage)
|
|
242
|
+
|| (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]))
|
|
243
|
+
|| index.stage_runs.some(stage => stage.stage === input.stage || !["done", "skipped"].includes(stage.status))
|
|
244
|
+
|| context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? AND status IN ('active','idle') LIMIT 1", [project.id, run.id])) {
|
|
245
|
+
throw new AppError("snapshot_execution_active", "Execution policy can be selected only at a detached stage-entry import", 1);
|
|
246
|
+
}
|
|
247
|
+
index.execution_profile = { ...input.profile, stage_entry_import: { stage: input.stage, lineage_file: input.lineageFile } };
|
|
248
|
+
index.updated_at = context.now();
|
|
249
|
+
persistRunState(context, project, run, index);
|
|
250
|
+
appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "execution_policy_imported", stage: input.stage, lineage_file: input.lineageFile, source_checksum: input.profile.routing_input?.source_checksum });
|
|
194
251
|
}
|
|
195
252
|
/**
|
|
196
253
|
* A vNext route may materialize a feature checkout after its semantic
|
|
@@ -200,6 +257,7 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
200
257
|
export function rebindFlowRunWorkspace(context, input) {
|
|
201
258
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
202
259
|
const run = resolveRun(context, project.id, input.runId);
|
|
260
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
203
261
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
|
|
204
262
|
const index = authoritativeIndex(run);
|
|
205
263
|
const now = context.now();
|
|
@@ -210,7 +268,7 @@ export function rebindFlowRunWorkspace(context, input) {
|
|
|
210
268
|
context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
|
|
211
269
|
persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
|
|
212
270
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason } });
|
|
213
|
-
appendRunTimeline(
|
|
271
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
|
|
214
272
|
return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
215
273
|
}
|
|
216
274
|
function loadUpgradeFlowContract(context, projectRoot) {
|
|
@@ -229,7 +287,41 @@ export function getFlowRunStatus(context, input) {
|
|
|
229
287
|
const run = resolveRun(context, project.id, input.runId);
|
|
230
288
|
const index = authoritativeIndex(run);
|
|
231
289
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
232
|
-
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
290
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
291
|
+
}
|
|
292
|
+
/** A read-only projection of the next legal lifecycle action.
|
|
293
|
+
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
294
|
+
* transition from a model's prose or a hard-coded stage array. */
|
|
295
|
+
function runContinuation(context, run, index) {
|
|
296
|
+
if (["done", "failed", "cancelled"].includes(run.status))
|
|
297
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
298
|
+
const control = runControlDirective(context, run.project_id, run.id);
|
|
299
|
+
if (control)
|
|
300
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "operator_control", command: null, control };
|
|
301
|
+
const active = index.stage_runs.find((stage) => !["done", "skipped"].includes(stage.status));
|
|
302
|
+
if (active) {
|
|
303
|
+
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]);
|
|
304
|
+
const kind = active.status === "running" ? "continue_stage" : active.status === "paused" || active.status === "waiting_for_user" ? "paused" : "blocked";
|
|
305
|
+
return { kind, stage: active.stage, attempt: active.attempt ?? null, work_id: active.pause?.work_id ?? runningWork?.work_id ?? null, reason: active.status, command: null };
|
|
306
|
+
}
|
|
307
|
+
if (run.status === "waiting_for_user" || run.status === "paused")
|
|
308
|
+
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
309
|
+
if (run.status === "blocked")
|
|
310
|
+
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
311
|
+
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
312
|
+
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
313
|
+
const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
|
|
314
|
+
if (!next)
|
|
315
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
316
|
+
const command = `${flowCommand(context)} stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
|
|
317
|
+
const profile = index.execution_profile;
|
|
318
|
+
const execution = profile?.settings.execution ? profile.agent_profiles
|
|
319
|
+
? { status: "resolved", ...resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot: run.project_root, workspaceRoot: run.workspace_root, ...(completed ? { fromStage: completed.stage } : {}), stage: next, stageSessionMode: profile.settings.stage_session_mode, mergeMode: profile.settings.merge_mode }) }
|
|
320
|
+
: { status: "unfrozen", reason: "execution_profile_not_frozen" }
|
|
321
|
+
: undefined;
|
|
322
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command, ...(execution ? { execution } : {}) };
|
|
323
|
+
}
|
|
324
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "no_vnext_continuation", command: null };
|
|
233
325
|
}
|
|
234
326
|
export function getFlowRunConfig(context, input) {
|
|
235
327
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -250,9 +342,12 @@ export function getFlowRunVariables(context, input) {
|
|
|
250
342
|
return { ok: true, run_id: run.id, variables };
|
|
251
343
|
}
|
|
252
344
|
export function setFlowRunVariable(context, input) {
|
|
345
|
+
return setRunVariable(context, input, input.value, prepareFlowRunVariable(context, input));
|
|
346
|
+
}
|
|
347
|
+
export function prepareFlowRunVariable(context, input) {
|
|
253
348
|
if (!input.key.startsWith("user."))
|
|
254
349
|
throw new AppError("validation", "Only user.* RUN variables may be set directly", 2, { key: input.key });
|
|
255
|
-
return
|
|
350
|
+
return prepareRunVariable(context, input);
|
|
256
351
|
}
|
|
257
352
|
/** Controllers own policy.* and runtime.* values; agents never call this directly. */
|
|
258
353
|
export function setRuntimeRunVariable(context, input) {
|
|
@@ -261,20 +356,7 @@ export function setRuntimeRunVariable(context, input) {
|
|
|
261
356
|
return setRunVariable(context, input, input.value);
|
|
262
357
|
}
|
|
263
358
|
export function setFlowRunConfig(context, input) {
|
|
264
|
-
|
|
265
|
-
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
266
|
-
}
|
|
267
|
-
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
268
|
-
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
269
|
-
}
|
|
270
|
-
const reason = requiredPlain(input.reason, "reason");
|
|
271
|
-
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
272
|
-
const run = resolveRun(context, project.id, input.runId);
|
|
273
|
-
const index = authoritativeIndex(run);
|
|
274
|
-
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
275
|
-
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
276
|
-
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
277
|
-
}
|
|
359
|
+
const { reason, project, run, index } = prepareFlowRunConfig(context, input);
|
|
278
360
|
const now = context.now();
|
|
279
361
|
const settings = runSettings(index);
|
|
280
362
|
const previous = input.key === "plan_review.mode" ? settings.plan_review : settings.code_review;
|
|
@@ -289,13 +371,32 @@ export function setFlowRunConfig(context, input) {
|
|
|
289
371
|
index.updated_at = now;
|
|
290
372
|
persistRunState(context, project, run, index);
|
|
291
373
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.config_set", payload: { run_id: run.id, key: input.key, value: mode, source, reason } });
|
|
292
|
-
appendRunTimeline(
|
|
374
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
293
375
|
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
294
376
|
}
|
|
377
|
+
export function prepareFlowRunConfig(context, input) {
|
|
378
|
+
if (input.key !== "plan_review.mode" && input.key !== "code_review.mode") {
|
|
379
|
+
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
380
|
+
}
|
|
381
|
+
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
382
|
+
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
383
|
+
}
|
|
384
|
+
const reason = requiredPlain(input.reason, "reason");
|
|
385
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
386
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
387
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
388
|
+
const index = authoritativeIndex(run);
|
|
389
|
+
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
390
|
+
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
391
|
+
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
392
|
+
}
|
|
393
|
+
return { reason, project, run, index };
|
|
394
|
+
}
|
|
295
395
|
/** Freeze an automatic review request once from already accepted policy or PLAN facts. */
|
|
296
396
|
export function freezeFlowRunReviewMode(context, input) {
|
|
297
397
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
298
398
|
const run = resolveRun(context, project.id, input.runId);
|
|
399
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
299
400
|
const index = authoritativeIndex(run);
|
|
300
401
|
const settings = runSettings(index);
|
|
301
402
|
const key = input.review === "plan" ? "plan_review" : "code_review";
|
|
@@ -312,8 +413,8 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
312
413
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
313
414
|
const run = resolveRun(context, project.id, input.runId);
|
|
314
415
|
const index = authoritativeIndex(run);
|
|
315
|
-
const
|
|
316
|
-
const flags =
|
|
416
|
+
const legacyRuntime = index.flow_flags ? null : readRuntimeSnapshot(run.runtime_path);
|
|
417
|
+
const flags = index.flow_flags ?? legacyRuntime?.flow_flags;
|
|
317
418
|
if (!flags) {
|
|
318
419
|
return {
|
|
319
420
|
ok: true,
|
|
@@ -331,29 +432,31 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
331
432
|
snapshot_revision: flags.snapshot_revision,
|
|
332
433
|
snapshot_checksum: flags.snapshot_checksum,
|
|
333
434
|
flow_flags: flags,
|
|
334
|
-
runtime_revision:
|
|
435
|
+
runtime_revision: index.runtime_revision ?? legacyRuntime?.runtime_revision ?? null
|
|
335
436
|
};
|
|
336
437
|
}
|
|
337
438
|
export function reviseFlowRunFlags(context, input) {
|
|
338
439
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
339
440
|
const run = resolveRun(context, project.id, input.runId);
|
|
441
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
340
442
|
const idempotencyKey = requiredPlain(input.idempotencyKey, "idempotency-key");
|
|
341
|
-
context.db.
|
|
342
|
-
|
|
343
|
-
|
|
443
|
+
const priorMutation = 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]);
|
|
444
|
+
if (priorMutation)
|
|
445
|
+
return { ...JSON.parse(priorMutation.result_json), idempotent: true };
|
|
446
|
+
// Canonical contract bytes are prepared before taking the writer. Revision
|
|
447
|
+
// and idempotency are compared again against SQL authority under the lock.
|
|
448
|
+
const contract = loadProjectFlowContract(run.project_root);
|
|
449
|
+
return context.db.writeTransaction(() => {
|
|
344
450
|
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]);
|
|
345
451
|
if (existingMutation) {
|
|
346
452
|
const result = { ...JSON.parse(existingMutation.result_json), idempotent: true };
|
|
347
|
-
context.db.exec("COMMIT");
|
|
348
|
-
committed = true;
|
|
349
453
|
return result;
|
|
350
454
|
}
|
|
351
455
|
// Read the authority only after the write lock: two callers with the same
|
|
352
456
|
// expected revision must not both pass the compare-and-set check.
|
|
353
457
|
const lockedRun = requireRunById(context, project.id, run.id);
|
|
354
458
|
const index = authoritativeIndex(lockedRun);
|
|
355
|
-
const
|
|
356
|
-
const current = runtime?.flow_flags ?? index.flow_flags;
|
|
459
|
+
const current = index.flow_flags;
|
|
357
460
|
if (!current) {
|
|
358
461
|
throw new AppError("validation", "Legacy RUN has no flow-flag snapshot; revise is unavailable", 2, { run_id: run.id });
|
|
359
462
|
}
|
|
@@ -370,7 +473,6 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
370
473
|
});
|
|
371
474
|
}
|
|
372
475
|
const now = context.now();
|
|
373
|
-
const contract = loadProjectFlowContract(lockedRun.project_root);
|
|
374
476
|
const next = resolveFlowFlags(contract, {
|
|
375
477
|
flowKind: index.flow_kind,
|
|
376
478
|
preset: input.preset ?? current.preset.applied,
|
|
@@ -426,14 +528,12 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
426
528
|
context.db.run(`INSERT INTO flow_run_flag_mutations
|
|
427
529
|
(project_id, run_id, idempotency_key, expected_revision, resulting_revision, result_json, created_at)
|
|
428
530
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [project.id, run.id, idempotencyKey, input.expectedRevision, next.snapshot_revision, JSON.stringify(result), now]);
|
|
429
|
-
context.db.exec("COMMIT");
|
|
430
|
-
committed = true;
|
|
431
531
|
appendAudit(context, {
|
|
432
532
|
projectId: project.id,
|
|
433
533
|
eventType: "flow_run.flags_revised",
|
|
434
534
|
payload: { run_id: run.id, snapshot_revision: next.snapshot_revision, snapshot_checksum: next.snapshot_checksum, ...(reason ? { reason } : {}) }
|
|
435
535
|
});
|
|
436
|
-
appendRunTimeline(
|
|
536
|
+
appendRunTimeline(context, project.id, run.id, {
|
|
437
537
|
at: now,
|
|
438
538
|
type: "flow_flags_revised",
|
|
439
539
|
run_id: run.id,
|
|
@@ -443,12 +543,7 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
443
543
|
...(reason ? { reason } : {})
|
|
444
544
|
});
|
|
445
545
|
return result;
|
|
446
|
-
}
|
|
447
|
-
catch (error) {
|
|
448
|
-
if (!committed)
|
|
449
|
-
context.db.exec("ROLLBACK");
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
546
|
+
}, "runs.reviseFlowFlags");
|
|
452
547
|
}
|
|
453
548
|
export function listFlowRuns(context, input) {
|
|
454
549
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -459,24 +554,34 @@ export function listFlowRuns(context, input) {
|
|
|
459
554
|
.map(flowRunSummary);
|
|
460
555
|
return { ok: true, project, runs };
|
|
461
556
|
}
|
|
462
|
-
export function
|
|
557
|
+
export function prepareFlowRunStageAttachment(context, input) {
|
|
463
558
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
464
559
|
const run = resolveRun(context, project.id, input.runId);
|
|
465
|
-
|
|
466
|
-
const now = context.now();
|
|
560
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
467
561
|
const stage = requiredPlain(input.stage, "stage");
|
|
468
|
-
const dir =
|
|
562
|
+
const dir = prepareStageDirectory(input.dir);
|
|
563
|
+
const index = authoritativeIndex(run);
|
|
469
564
|
const status = parseStageStatus(input.status);
|
|
470
565
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
471
566
|
assertVnextStageStart(run, index, stage, status, existing);
|
|
567
|
+
return { project, run, index, stage, dir, status, existing };
|
|
568
|
+
}
|
|
569
|
+
export function attachFlowRunStage(context, input) {
|
|
570
|
+
// Mutable stage state is re-read immediately before archiving/persisting.
|
|
571
|
+
const { project, run, index, stage, dir, status, existing } = prepareFlowRunStageAttachment(context, input);
|
|
572
|
+
const runRoot = runArtifactRoot(run);
|
|
573
|
+
if (fs.existsSync(path.join(runRoot, `.archive-${dir}.json`)))
|
|
574
|
+
archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
|
|
575
|
+
const now = context.now();
|
|
472
576
|
if (status === "running" && existing) {
|
|
473
|
-
archiveExistingStageAttempt(
|
|
577
|
+
archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
|
|
474
578
|
}
|
|
475
579
|
const stageRun = {
|
|
476
580
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
477
581
|
stage,
|
|
478
582
|
dir,
|
|
479
583
|
status,
|
|
584
|
+
...(input.inputSha256 ? { input_sha256: input.inputSha256 } : {}),
|
|
480
585
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : existing?.data_schema_id ? { data_schema_id: existing.data_schema_id } : {}),
|
|
481
586
|
updated_at: now
|
|
482
587
|
};
|
|
@@ -497,7 +602,8 @@ export function attachFlowRunStage(context, input) {
|
|
|
497
602
|
eventType: "flow_run.stage_attached",
|
|
498
603
|
payload: { run_id: run.id, stage, dir, status }
|
|
499
604
|
});
|
|
500
|
-
appendRunTimeline(
|
|
605
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_attached", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
606
|
+
context.db.afterCommit(() => reconcileRunStageReports(context, project.id, run.id, stage));
|
|
501
607
|
// A stage transition must not rewrite every Session in the RUN. A RUN may
|
|
502
608
|
// contain independent reviewer/worker sessions; their stage is set by the
|
|
503
609
|
// stage/work binding that actually owns that Session.
|
|
@@ -506,66 +612,108 @@ export function attachFlowRunStage(context, input) {
|
|
|
506
612
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
507
613
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
508
614
|
}
|
|
509
|
-
|
|
615
|
+
/** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
|
|
616
|
+
export function withStageSettlement(context, operation) {
|
|
617
|
+
return context.db.writeTransaction(operation, "stage.settle");
|
|
618
|
+
}
|
|
619
|
+
export function prepareFlowRunStageCompletion(context, input) {
|
|
620
|
+
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
621
|
+
if (value !== undefined)
|
|
622
|
+
requiredPlain(value, name);
|
|
623
|
+
}
|
|
624
|
+
if (input.aliases !== undefined) {
|
|
625
|
+
if (!Array.isArray(input.aliases))
|
|
626
|
+
throw new AppError("validation", "artifact aliases must be an array", 2);
|
|
627
|
+
for (const alias of input.aliases)
|
|
628
|
+
requiredPlain(alias, "artifact alias");
|
|
629
|
+
}
|
|
510
630
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
511
631
|
const run = resolveRun(context, project.id, input.runId);
|
|
632
|
+
if (["done", "failed", "skipped"].includes(input.status))
|
|
633
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.stage);
|
|
634
|
+
else
|
|
635
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
512
636
|
const index = authoritativeIndex(run);
|
|
513
|
-
const now = context.now();
|
|
514
637
|
const stage = requiredPlain(input.stage, "stage");
|
|
515
638
|
const status = parseStageStatus(input.status);
|
|
516
639
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
517
640
|
if (!existing) {
|
|
518
641
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
519
642
|
}
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
index.
|
|
536
|
-
index.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
643
|
+
const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
|
|
644
|
+
const dataSha256 = input.dataSha256 ?? (dataPath && fs.existsSync(dataPath) ? crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") : null);
|
|
645
|
+
if (dataSha256 && !/^[a-f0-9]{64}$/.test(dataSha256))
|
|
646
|
+
throw new AppError("validation", "Stage data SHA-256 is invalid", 2);
|
|
647
|
+
return { project, run, index, stage, status, existing, dataSha256, git: input.git ?? gitFacts(run.workspace_root), generation: ["done", "failed", "skipped"].includes(status) ? assertStageSettlementAllowed(context, project.id, run.id, stage) : assertRunMutationAllowed(context, project.id, run.id) };
|
|
648
|
+
}
|
|
649
|
+
export function completeFlowRunStage(context, input, prepared = prepareFlowRunStageCompletion(context, input)) {
|
|
650
|
+
const { project, run, stage, status, dataSha256, git, generation } = prepared;
|
|
651
|
+
const completed = context.db.writeTransaction(() => {
|
|
652
|
+
if (["done", "failed", "skipped"].includes(status))
|
|
653
|
+
assertStageSettlementAllowed(context, project.id, run.id, stage, generation);
|
|
654
|
+
else
|
|
655
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
656
|
+
const latest = requireRunById(context, project.id, run.id);
|
|
657
|
+
const index = authoritativeIndex(latest);
|
|
658
|
+
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
659
|
+
if (!existing || existing.attempt !== prepared.existing.attempt || index.runtime_revision !== prepared.index.runtime_revision) {
|
|
660
|
+
throw new AppError("stage_revision_changed", "Stage attempt changed during finish preparation", 1, { run_id: run.id, stage, expected_attempt: prepared.existing.attempt, actual_attempt: existing?.attempt ?? null });
|
|
661
|
+
}
|
|
662
|
+
const now = context.now();
|
|
663
|
+
const stageRun = {
|
|
664
|
+
...existing,
|
|
665
|
+
status,
|
|
666
|
+
...(input.stageReport ? { stage_report: input.stageReport } : {}),
|
|
667
|
+
...(input.data ? { data: input.data } : {}),
|
|
668
|
+
...(dataSha256 ? { data_sha256: dataSha256 } : {}),
|
|
669
|
+
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
|
|
670
|
+
...(input.report ? { report: input.report } : {}),
|
|
671
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
672
|
+
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
673
|
+
updated_at: now,
|
|
674
|
+
...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
|
|
675
|
+
? { completed_at: now, duration_ms: durationMs(existing.started_at ?? null, now), git }
|
|
676
|
+
: {})
|
|
677
|
+
};
|
|
678
|
+
upsertStage(index, stageRun);
|
|
679
|
+
if (status === "waiting_for_user") {
|
|
680
|
+
index.status = "waiting_for_user";
|
|
681
|
+
index.verdict = "waiting_for_user";
|
|
682
|
+
}
|
|
683
|
+
else if (status === "blocked" || status === "failed") {
|
|
684
|
+
index.status = status;
|
|
685
|
+
index.verdict = status;
|
|
686
|
+
}
|
|
687
|
+
else if (status === "done" && index.status === "waiting_for_user") {
|
|
688
|
+
index.status = "running";
|
|
689
|
+
index.verdict = "running";
|
|
690
|
+
}
|
|
691
|
+
index.current_stage = stage;
|
|
692
|
+
index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
|
|
693
|
+
index.updated_at = now;
|
|
694
|
+
index.execution = {
|
|
695
|
+
project_root: index.execution?.project_root ?? run.project_root,
|
|
696
|
+
workspace_root: index.execution?.workspace_root ?? run.workspace_root,
|
|
697
|
+
git
|
|
698
|
+
};
|
|
699
|
+
persistRunState(context, project, latest, index);
|
|
700
|
+
context.db.afterCommit(() => refreshRunSessionProjection(context, project.id, run.id));
|
|
701
|
+
appendAudit(context, {
|
|
702
|
+
projectId: project.id,
|
|
703
|
+
eventType: "flow_run.stage_completed",
|
|
704
|
+
payload: { run_id: run.id, stage, status, stage_report: input.stageReport ?? null, data: input.data ?? null }
|
|
705
|
+
});
|
|
706
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_completed", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
707
|
+
return { index, stageRun };
|
|
708
|
+
}, "runs.completeFlowRunStage");
|
|
562
709
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
563
|
-
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
710
|
+
return { ok: true, run: flowRunSummary(updatedRun), stage_run: completed.stageRun, index: completed.index, flow_guidance: guidanceForRun(context, updatedRun, completed.index) };
|
|
564
711
|
}
|
|
565
712
|
/** Pause one live stage without completing it or creating a new attempt. */
|
|
566
713
|
export function pauseFlowRunStage(context, input) {
|
|
567
714
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
568
715
|
const run = resolveRun(context, project.id, input.runId);
|
|
716
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
569
717
|
const index = authoritativeIndex(run);
|
|
570
718
|
const stage = requiredPlain(input.stage, "stage");
|
|
571
719
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -594,13 +742,14 @@ export function pauseFlowRunStage(context, input) {
|
|
|
594
742
|
persistRunState(context, project, run, index);
|
|
595
743
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
596
744
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_paused", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, reason: "waiting_for_user" } });
|
|
597
|
-
appendRunTimeline(
|
|
745
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_waiting_for_user", run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, question_path: input.questionPath });
|
|
598
746
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
599
747
|
}
|
|
600
748
|
/** Resume the same stage, Work and attempt after a user answer is recorded. */
|
|
601
749
|
export function resumeFlowRunStage(context, input) {
|
|
602
750
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
603
751
|
const run = resolveRun(context, project.id, input.runId);
|
|
752
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
604
753
|
const index = authoritativeIndex(run);
|
|
605
754
|
const stage = requiredPlain(input.stage, "stage");
|
|
606
755
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -624,19 +773,28 @@ export function resumeFlowRunStage(context, input) {
|
|
|
624
773
|
persistRunState(context, project, run, index);
|
|
625
774
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
626
775
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_resumed", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath } });
|
|
627
|
-
appendRunTimeline(
|
|
628
|
-
appendRunTimeline(
|
|
776
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "user_answer_received", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath });
|
|
777
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_resumed", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, paused_ms: pausedMs });
|
|
629
778
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
630
779
|
}
|
|
631
780
|
/** Pause the current stage for a non-user runtime blocker. */
|
|
632
|
-
export function
|
|
781
|
+
export function prepareFlowRunStageBlock(context, input) {
|
|
782
|
+
if (!["engine", "harness", "environment"].includes(input.kind) || typeof input.retryable !== "boolean")
|
|
783
|
+
throw new AppError("validation", "stage block requires a valid kind and boolean retryable", 2);
|
|
784
|
+
requiredPlain(input.code, "code");
|
|
785
|
+
requiredPlain(input.summary, "summary");
|
|
633
786
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
634
787
|
const run = resolveRun(context, project.id, input.runId);
|
|
788
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
635
789
|
const index = authoritativeIndex(run);
|
|
636
790
|
const stage = requiredPlain(input.stage, "stage");
|
|
637
791
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
638
792
|
if (!stageRun || stageRun.status !== "running")
|
|
639
793
|
throw new AppError("invalid_stage_state", "Only a running stage can be blocked", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
|
|
794
|
+
return { project, run, index, stage, stageRun };
|
|
795
|
+
}
|
|
796
|
+
export function blockFlowRunStage(context, input, prepared = prepareFlowRunStageBlock(context, input)) {
|
|
797
|
+
const { project, run, index, stage, stageRun } = prepared;
|
|
640
798
|
const now = context.now();
|
|
641
799
|
stageRun.status = "blocked";
|
|
642
800
|
stageRun.updated_at = now;
|
|
@@ -650,20 +808,24 @@ export function blockFlowRunStage(context, input) {
|
|
|
650
808
|
persistRunState(context, project, run, index);
|
|
651
809
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
652
810
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_blocked", payload: { run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, retryable: input.retryable } });
|
|
653
|
-
appendRunTimeline(
|
|
811
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_blocked", run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, summary: input.summary, retryable: input.retryable });
|
|
654
812
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
655
813
|
}
|
|
656
814
|
/** Continue the same stage after its non-user blocker was repaired externally. */
|
|
657
|
-
export function
|
|
815
|
+
export function prepareFlowRunStageUnblock(context, input) {
|
|
658
816
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
659
817
|
const run = resolveRun(context, project.id, input.runId);
|
|
818
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
660
819
|
const index = authoritativeIndex(run);
|
|
661
820
|
const stage = requiredPlain(input.stage, "stage");
|
|
662
821
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
663
822
|
if (!stageRun || stageRun.status !== "blocked" || !stageRun.blocker || stageRun.blocker.work_id !== input.workId)
|
|
664
823
|
throw new AppError("invalid_stage_state", "Stage unblock requires its matching runtime blocker", 1, { run_id: run.id, stage, work_id: input.workId, status: stageRun?.status ?? "missing" });
|
|
824
|
+
return { project, run, index, stage, stageRun, blocker: stageRun.blocker };
|
|
825
|
+
}
|
|
826
|
+
export function unblockFlowRunStage(context, input, prepared = prepareFlowRunStageUnblock(context, input)) {
|
|
827
|
+
const { project, run, index, stage, stageRun, blocker } = prepared;
|
|
665
828
|
const now = context.now();
|
|
666
|
-
const blocker = stageRun.blocker;
|
|
667
829
|
const blockedMs = durationMs(blocker.blocked_at, now) ?? 0;
|
|
668
830
|
stageRun.status = "running";
|
|
669
831
|
stageRun.updated_at = now;
|
|
@@ -678,7 +840,7 @@ export function unblockFlowRunStage(context, input) {
|
|
|
678
840
|
persistRunState(context, project, run, index);
|
|
679
841
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
680
842
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_unblocked", payload: { run_id: run.id, stage, work_id: input.workId, code: blocker.code } });
|
|
681
|
-
appendRunTimeline(
|
|
843
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_unblocked", run_id: run.id, stage, work_id: input.workId, code: blocker.code, blocked_ms: blockedMs });
|
|
682
844
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
683
845
|
}
|
|
684
846
|
function operationalRunStatus(context, projectId, runId) {
|
|
@@ -687,11 +849,36 @@ function operationalRunStatus(context, projectId, runId) {
|
|
|
687
849
|
const leaves = unfinished.filter((work) => !parents.has(work.work_id));
|
|
688
850
|
return leaves.length > 0 && leaves.every((work) => work.status === "paused") ? "paused" : "running";
|
|
689
851
|
}
|
|
690
|
-
|
|
691
|
-
|
|
852
|
+
/** Keep the logical RUN projection aligned with recovery authority. Stage/Work
|
|
853
|
+
* evidence and verdicts survive a reversible stop; terminal runs never reopen. */
|
|
854
|
+
export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
|
|
855
|
+
const run = resolveRun(context, projectId, runId);
|
|
856
|
+
if (!["running", "paused"].includes(run.status))
|
|
857
|
+
return;
|
|
858
|
+
const index = authoritativeIndex(run);
|
|
859
|
+
const status = paused || index.stage_runs.some(stage => ["paused", "waiting_for_user", "blocked"].includes(stage.status))
|
|
860
|
+
? "paused" : operationalRunStatus(context, projectId, runId);
|
|
861
|
+
if (run.status === status && index.status === status)
|
|
862
|
+
return;
|
|
863
|
+
index.status = status;
|
|
864
|
+
index.updated_at = context.now();
|
|
865
|
+
const project = requireProjectByRoot(context, run.project_root);
|
|
866
|
+
persistRunState(context, project, run, index);
|
|
867
|
+
}
|
|
868
|
+
export function prepareFlowRunCompletion(context, input) {
|
|
869
|
+
if (input.manualOverrideReason !== undefined) {
|
|
870
|
+
requiredPlain(input.manualOverrideReason, "override reason");
|
|
871
|
+
if (!["cancelled", "failed"].includes(input.status))
|
|
872
|
+
throw new AppError("validation", "Manual RUN override may only cancel or fail a RUN", 2);
|
|
873
|
+
}
|
|
874
|
+
if (input.verdict !== undefined)
|
|
875
|
+
requiredPlain(input.verdict, "verdict");
|
|
876
|
+
if (input.nextAction !== undefined)
|
|
877
|
+
requiredPlain(input.nextAction, "next action");
|
|
878
|
+
const project = requireProjectByRoot(context, input.manualOverrideReason ? canonicalPath(input.projectRoot, false) : resolveProjectRoot(input.projectRoot));
|
|
692
879
|
const run = resolveRun(context, project.id, input.runId);
|
|
880
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
693
881
|
const index = authoritativeIndex(run);
|
|
694
|
-
const now = context.now();
|
|
695
882
|
const status = parseRunStatus(input.status);
|
|
696
883
|
if (!['done', 'cancelled', 'failed'].includes(status)) {
|
|
697
884
|
throw new AppError('validation', 'completeFlowRun accepts only done, cancelled, or failed; use stage block/unblock for a recoverable blocker', 2, { status });
|
|
@@ -701,6 +888,11 @@ export function completeFlowRun(context, input) {
|
|
|
701
888
|
if (activeWork)
|
|
702
889
|
throw new AppError("active_work", "RUN cannot complete while Work remains active", 2, { run_id: run.id, work_id: activeWork.work_id, status: activeWork.status });
|
|
703
890
|
}
|
|
891
|
+
return { project, run, index, status };
|
|
892
|
+
}
|
|
893
|
+
export function completeFlowRun(context, input) {
|
|
894
|
+
const { project, run, index, status } = prepareFlowRunCompletion(context, input);
|
|
895
|
+
const now = context.now();
|
|
704
896
|
if (input.manualOverrideReason && (status === "cancelled" || status === "failed")) {
|
|
705
897
|
closeOpenStagesForOverride(index, status, now);
|
|
706
898
|
closeOpenWorksForOverride(context, project.id, run.id, status, now);
|
|
@@ -734,7 +926,7 @@ export function completeFlowRun(context, input) {
|
|
|
734
926
|
eventType: input.manualOverrideReason ? "flow_run.manual_override" : "flow_run.completed",
|
|
735
927
|
payload: { run_id: run.id, status, verdict: index.verdict, next_action: index.next_action, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) }
|
|
736
928
|
});
|
|
737
|
-
appendRunTimeline(
|
|
929
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: input.manualOverrideReason ? "run_manual_override" : "run_completed", run_id: run.id, status, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) });
|
|
738
930
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
739
931
|
return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
740
932
|
}
|
|
@@ -743,16 +935,85 @@ export function advanceFlowRun(context, input) {
|
|
|
743
935
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
744
936
|
const run = resolveRun(context, project.id, input.runId);
|
|
745
937
|
const index = authoritativeIndex(run);
|
|
938
|
+
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)))
|
|
939
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.settlementStage);
|
|
940
|
+
else
|
|
941
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
942
|
+
const control = runControlDirective(context, project.id, run.id);
|
|
746
943
|
const now = context.now();
|
|
747
944
|
index.status = input.status;
|
|
748
945
|
index.verdict = input.verdict;
|
|
749
|
-
index.next_action = input.nextAction ?? null;
|
|
946
|
+
index.next_action = control ? null : input.nextAction ?? null;
|
|
750
947
|
index.updated_at = now;
|
|
751
948
|
persistRunState(context, project, run, index);
|
|
752
|
-
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status:
|
|
753
|
-
appendRunTimeline(
|
|
949
|
+
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 } });
|
|
950
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "run_progressed", run_id: run.id, status: index.status, next_action: index.next_action });
|
|
754
951
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
755
|
-
return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
952
|
+
return { ok: true, ...(control ? { control } : {}), run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* The only legal backward edge in vNext: an integration gate exposed a
|
|
956
|
+
* product defect, so the source must be repaired and reviewed again. Normal
|
|
957
|
+
* stage start remains fail-closed for completed stages.
|
|
958
|
+
*/
|
|
959
|
+
export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
960
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
961
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
962
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
963
|
+
const index = authoritativeIndex(run);
|
|
964
|
+
const priorRepair = index.variables?.["merge.source_repair"];
|
|
965
|
+
if (priorRepair?.merge_request_id === input.mergeRequestId && priorRepair.cycle)
|
|
966
|
+
return { run_id: run.id, cycle: priorRepair.cycle };
|
|
967
|
+
const code = index.stage_runs.find((item) => item.stage === "code");
|
|
968
|
+
const review = index.stage_runs.find((item) => item.stage === "code-review");
|
|
969
|
+
const merge = index.stage_runs.find((item) => item.stage === "merge");
|
|
970
|
+
if (!code || code.status !== "done" || !review || review.status !== "done" || !merge || merge.status !== "running") {
|
|
971
|
+
throw new AppError("invalid_merge_repair_state", "Source repair requires accepted CODE/CODE-REVIEW and a running MERGE attempt", 2, {
|
|
972
|
+
run_id: run.id,
|
|
973
|
+
code: code?.status ?? null,
|
|
974
|
+
code_review: review?.status ?? null,
|
|
975
|
+
merge: merge?.status ?? null
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
const root = runArtifactRoot(run);
|
|
979
|
+
for (const stage of ["code", "code-review", "merge"])
|
|
980
|
+
archiveExistingStageAttempt(context, project.id, run.id, root, vnextStageDirectory(stage));
|
|
981
|
+
const now = context.now();
|
|
982
|
+
const restarted = {
|
|
983
|
+
...code,
|
|
984
|
+
status: "running",
|
|
985
|
+
started_at: now,
|
|
986
|
+
updated_at: now,
|
|
987
|
+
attempt: nextAttempt(code)
|
|
988
|
+
};
|
|
989
|
+
delete restarted.completed_at;
|
|
990
|
+
delete restarted.duration_ms;
|
|
991
|
+
delete restarted.stage_report;
|
|
992
|
+
delete restarted.data;
|
|
993
|
+
delete restarted.report;
|
|
994
|
+
delete restarted.artifact_aliases;
|
|
995
|
+
const cycle = `MRR-${input.mergeRequestId}`;
|
|
996
|
+
index.stage_runs = index.stage_runs.filter((item) => item.stage !== "code" && item.stage !== "code-review" && item.stage !== "merge");
|
|
997
|
+
index.stage_runs.push(restarted);
|
|
998
|
+
index.variables = { ...(index.variables ?? {}), "merge.source_repair": { cycle, merge_request_id: input.mergeRequestId, prepared_at: now } };
|
|
999
|
+
index.status = "running";
|
|
1000
|
+
index.verdict = "merge_source_repair";
|
|
1001
|
+
index.next_action = "start_code_repair";
|
|
1002
|
+
index.current_stage = "code";
|
|
1003
|
+
index.updated_at = now;
|
|
1004
|
+
index.attempts = index.stage_runs.map((item) => ({
|
|
1005
|
+
stage: item.stage,
|
|
1006
|
+
attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1,
|
|
1007
|
+
root: item.dir,
|
|
1008
|
+
archive: null,
|
|
1009
|
+
status: item.status,
|
|
1010
|
+
started_at: item.started_at ?? now,
|
|
1011
|
+
finished_at: item.completed_at ?? null
|
|
1012
|
+
}));
|
|
1013
|
+
persistRunState(context, project, run, index);
|
|
1014
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
|
|
1015
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
1016
|
+
return { run_id: run.id, cycle };
|
|
756
1017
|
}
|
|
757
1018
|
function closeOpenStagesForOverride(index, status, now) {
|
|
758
1019
|
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
@@ -800,7 +1061,7 @@ function closeRunOrchestrators(context, projectId, run, status, now) {
|
|
|
800
1061
|
context.db.run(`UPDATE flow_session_segments SET ended_at = ?
|
|
801
1062
|
WHERE project_id = ? AND run_id = ? AND session_id IN (${active.map(() => "?").join(", ")}) AND ended_at IS NULL`, [now, projectId, run.id, ...active.map((session) => session.session_id)]);
|
|
802
1063
|
for (const session of active) {
|
|
803
|
-
appendRunTimeline(
|
|
1064
|
+
appendRunTimeline(context, projectId, run.id, {
|
|
804
1065
|
at: now,
|
|
805
1066
|
type: "session_stopped",
|
|
806
1067
|
run_id: run.id,
|
|
@@ -923,7 +1184,7 @@ export function getFlowRunSessions(context, input) {
|
|
|
923
1184
|
}
|
|
924
1185
|
export function appendFlowRunTimelineEvent(context, projectId, runId, event) {
|
|
925
1186
|
const run = requireRunById(context, projectId, runId);
|
|
926
|
-
appendRunTimeline(
|
|
1187
|
+
appendRunTimeline(context, projectId, run.id, { run_id: run.id, ...event });
|
|
927
1188
|
}
|
|
928
1189
|
function guidanceForRun(context, run, index) {
|
|
929
1190
|
if (run.flow_kind === "vnext_specify")
|
|
@@ -1018,32 +1279,31 @@ function vnextSpecifyGuidance(run, index) {
|
|
|
1018
1279
|
};
|
|
1019
1280
|
}
|
|
1020
1281
|
function persistRunState(context, project, run, index, options = {}) {
|
|
1282
|
+
// Fenced productive admission is a logical pause, even while physical work
|
|
1283
|
+
// drains. Late admitted Stage results must not revive a stopped RUN.
|
|
1284
|
+
if (index.status === "running" && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1285
|
+
index.status = "paused";
|
|
1021
1286
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
index.updated_at,
|
|
1043
|
-
index.completed_at ?? null,
|
|
1044
|
-
project.id,
|
|
1045
|
-
run.id
|
|
1046
|
-
]);
|
|
1287
|
+
context.db.writeTransaction(() => {
|
|
1288
|
+
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
|
|
1289
|
+
if (!stored)
|
|
1290
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: run.id });
|
|
1291
|
+
const previous = JSON.parse(stored.index_json);
|
|
1292
|
+
const actualRevision = previous.runtime_revision ?? 0;
|
|
1293
|
+
const expectedRevision = index.runtime_revision ?? 0;
|
|
1294
|
+
if (actualRevision !== expectedRevision)
|
|
1295
|
+
throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id, expected_revision: expectedRevision, actual_revision: actualRevision });
|
|
1296
|
+
index.runtime_revision = actualRevision + 1;
|
|
1297
|
+
const history = [...(previous.flag_revision_history ?? [])];
|
|
1298
|
+
if (options.flagRevision)
|
|
1299
|
+
history.push(options.flagRevision);
|
|
1300
|
+
index.flag_revision_history = history.slice(-32);
|
|
1301
|
+
const updated = context.db.run(`UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?, updated_at = ?, completed_at = ?
|
|
1302
|
+
WHERE project_id = ? AND id = ? AND index_json = ?`, [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), runtimePath, runtimePath, index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]);
|
|
1303
|
+
if (updated.changes !== 1)
|
|
1304
|
+
throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id });
|
|
1305
|
+
context.db.afterCommit(() => publishRunProjections(context, project.id, run.id));
|
|
1306
|
+
}, "runs.persistRunState");
|
|
1047
1307
|
}
|
|
1048
1308
|
/** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
|
|
1049
1309
|
function persistedVnextIndex(index) {
|
|
@@ -1085,35 +1345,64 @@ function materializedRunVariables(index) {
|
|
|
1085
1345
|
...(index.variables ?? {})
|
|
1086
1346
|
};
|
|
1087
1347
|
}
|
|
1088
|
-
function
|
|
1348
|
+
function prepareRunVariable(context, input) {
|
|
1089
1349
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1090
1350
|
const run = resolveRun(context, project.id, input.runId);
|
|
1351
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
1091
1352
|
const index = authoritativeIndex(run);
|
|
1353
|
+
return { project, run, index };
|
|
1354
|
+
}
|
|
1355
|
+
function setRunVariable(context, input, value, prepared = prepareRunVariable(context, input)) {
|
|
1356
|
+
const { project, run, index } = prepared;
|
|
1092
1357
|
if (index.variables?.[input.key] === value)
|
|
1093
1358
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
|
|
1094
1359
|
index.variables = { ...(index.variables ?? {}), [input.key]: value };
|
|
1095
1360
|
index.updated_at = context.now();
|
|
1096
1361
|
persistRunState(context, project, run, index);
|
|
1097
|
-
appendRunTimeline(
|
|
1362
|
+
appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "run_variable_set", run_id: run.id, key: input.key });
|
|
1098
1363
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: false };
|
|
1099
1364
|
}
|
|
1100
|
-
function appendRunTimeline(
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1365
|
+
function appendRunTimeline(context, projectId, runId, event) {
|
|
1366
|
+
context.db.writeTransaction(() => {
|
|
1367
|
+
const last = context.db.get("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.timeline' AND json_extract(payload_json, '$.run_id') = ? ORDER BY id DESC LIMIT 1", [projectId, runId]);
|
|
1368
|
+
const sequence = last ? Number(JSON.parse(last.payload_json).sequence) + 1 : 1;
|
|
1369
|
+
const safeEvent = sanitizeTimelineValue(event);
|
|
1370
|
+
const enriched = { event_id: crypto.randomUUID(), sequence, at: typeof safeEvent.at === "string" ? safeEvent.at : context.now(), ...safeEvent };
|
|
1371
|
+
appendAudit(context, { projectId, eventType: "flow_run.timeline", payload: { run_id: runId, sequence, event: enriched } });
|
|
1372
|
+
context.db.afterCommit(() => publishRunProjections(context, projectId, runId));
|
|
1373
|
+
}, "runs.appendRunTimeline");
|
|
1374
|
+
}
|
|
1375
|
+
/** Rebuild projections from committed SQL after taking a per-RUN filesystem lock. */
|
|
1376
|
+
export function publishRunProjections(context, projectId, runId) {
|
|
1377
|
+
const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1378
|
+
if (!run)
|
|
1379
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1380
|
+
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, projectId, runId);
|
|
1381
|
+
ensureDir(path.dirname(runtimePath));
|
|
1382
|
+
withProjectionLock(`${runtimePath}.publish-lock`, () => {
|
|
1383
|
+
// A delayed publisher must observe the latest commit after acquiring its lock.
|
|
1384
|
+
const latest = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1385
|
+
if (!latest)
|
|
1386
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1387
|
+
const index = authoritativeIndex(latest);
|
|
1388
|
+
const mutations = context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]);
|
|
1389
|
+
const history = mutations.reverse().map((row) => ({
|
|
1390
|
+
revision: row.resulting_revision,
|
|
1391
|
+
checksum: String(JSON.parse(row.result_json).snapshot_checksum ?? ""),
|
|
1392
|
+
at: row.created_at,
|
|
1393
|
+
idempotency_key: row.idempotency_key
|
|
1394
|
+
}));
|
|
1395
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(index, index.runtime_revision ?? 1, history.length ? history : index.flag_revision_history ?? []));
|
|
1396
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.timeline' AND json_extract(payload_json, '$.run_id') = ? ORDER BY id", [projectId, runId]);
|
|
1397
|
+
if (latest.run_root) {
|
|
1398
|
+
const timelinePath = path.join(latest.run_root, "timeline.jsonl");
|
|
1399
|
+
ensureDir(path.dirname(timelinePath));
|
|
1400
|
+
const bytes = rows.map((row) => JSON.stringify(JSON.parse(row.payload_json).event)).join("\n");
|
|
1401
|
+
const temporary = `${timelinePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
1402
|
+
fs.writeFileSync(temporary, bytes ? `${bytes}\n` : "");
|
|
1403
|
+
fs.renameSync(temporary, timelinePath);
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1117
1406
|
}
|
|
1118
1407
|
function runtimeSnapshotForIndex(index, runtimeRevision, flagRevisionHistory = []) {
|
|
1119
1408
|
const rest = { ...index };
|
|
@@ -1139,10 +1428,12 @@ function readRuntimeSnapshot(file) {
|
|
|
1139
1428
|
function authoritativeIndex(run) {
|
|
1140
1429
|
try {
|
|
1141
1430
|
const state = JSON.parse(run.index_json);
|
|
1431
|
+
if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
|
|
1432
|
+
throw new Error("RUN index must contain stage_runs");
|
|
1142
1433
|
return { ...state, schema_id: runSchemaId };
|
|
1143
1434
|
}
|
|
1144
1435
|
catch (error) {
|
|
1145
|
-
throw new AppError("
|
|
1436
|
+
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
1146
1437
|
}
|
|
1147
1438
|
}
|
|
1148
1439
|
function sanitizeTimelineValue(value, key) {
|
|
@@ -1203,7 +1494,7 @@ function upsertStage(index, stageRun) {
|
|
|
1203
1494
|
}
|
|
1204
1495
|
index.stage_runs.sort((a, b) => a.order - b.order);
|
|
1205
1496
|
}
|
|
1206
|
-
function resolveRun(context, projectId, idOrAlias) {
|
|
1497
|
+
export function resolveRun(context, projectId, idOrAlias) {
|
|
1207
1498
|
if (isFullEntityId(idOrAlias)) {
|
|
1208
1499
|
return requireRunById(context, projectId, idOrAlias);
|
|
1209
1500
|
}
|
|
@@ -1297,7 +1588,7 @@ function parseStageStatus(value) {
|
|
|
1297
1588
|
}
|
|
1298
1589
|
return value;
|
|
1299
1590
|
}
|
|
1300
|
-
function
|
|
1591
|
+
export function prepareStageDirectory(value) {
|
|
1301
1592
|
const dir = requiredPlain(value, "dir");
|
|
1302
1593
|
if (!/^\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(dir)) {
|
|
1303
1594
|
throw new AppError("validation", "--dir must use NN-stage-slug format", 2, { dir });
|
|
@@ -1305,6 +1596,8 @@ function requiredStageDir(value) {
|
|
|
1305
1596
|
return dir;
|
|
1306
1597
|
}
|
|
1307
1598
|
function requiredPlain(value, label) {
|
|
1599
|
+
if (typeof value !== "string")
|
|
1600
|
+
throw new AppError("validation", `${label} must be a string`, 2);
|
|
1308
1601
|
const trimmed = value.trim();
|
|
1309
1602
|
if (!trimmed) {
|
|
1310
1603
|
throw new AppError("validation", `${label} must not be empty`, 2);
|
|
@@ -1312,6 +1605,8 @@ function requiredPlain(value, label) {
|
|
|
1312
1605
|
return trimmed;
|
|
1313
1606
|
}
|
|
1314
1607
|
function normalizeSlug(value) {
|
|
1608
|
+
if (typeof value !== "string")
|
|
1609
|
+
throw new AppError("validation", "--slug must be a string", 2);
|
|
1315
1610
|
const slug = value
|
|
1316
1611
|
.normalize("NFKD")
|
|
1317
1612
|
.replace(/[\u0300-\u036f]/g, "")
|
|
@@ -1329,26 +1624,84 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1329
1624
|
if (!fs.existsSync(absolute)) {
|
|
1330
1625
|
throw new AppError("not_found", `Workspace root does not exist: ${absolute}`, 1);
|
|
1331
1626
|
}
|
|
1627
|
+
if (!fs.statSync(absolute).isDirectory())
|
|
1628
|
+
throw new AppError("validation", "Workspace root must be a directory", 2, { workspace_root: absolute });
|
|
1332
1629
|
return fs.realpathSync(absolute);
|
|
1333
1630
|
}
|
|
1334
|
-
|
|
1631
|
+
/** Move one completed stage attempt and update only known durable path columns.
|
|
1632
|
+
* Receipt bytes remain immutable; readers resolve their new location through the DB. */
|
|
1633
|
+
export function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
|
|
1335
1634
|
const currentStageDir = path.join(runRoot, stageDir);
|
|
1336
1635
|
if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
|
|
1337
1636
|
return;
|
|
1338
1637
|
}
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1638
|
+
const journal = path.join(runRoot, `.archive-${stageDir}.json`);
|
|
1639
|
+
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
1640
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1641
|
+
const prior = row ? JSON.parse(row.index_json) : null;
|
|
1642
|
+
const live = prior?.stage_runs.find((stage) => stage.dir === stageDir);
|
|
1643
|
+
const generation = row ? assertRunMutationAllowed(context, projectId, runId) : 0;
|
|
1644
|
+
if (pending && (pending.project_id && pending.project_id !== projectId || pending.run_id && pending.run_id !== runId))
|
|
1645
|
+
throw new AppError("archive_owner_changed", "Archive journal belongs to another RUN", 1, { journal });
|
|
1646
|
+
if (pending?.generation !== undefined && pending.generation !== generation)
|
|
1647
|
+
throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal, expected_generation: pending.generation, generation });
|
|
1648
|
+
if (pending?.prior_attempt && live?.attempt !== pending.prior_attempt && live?.attempt !== pending.next_attempt)
|
|
1649
|
+
throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal, prior_attempt: pending.prior_attempt, actual_attempt: live?.attempt ?? null });
|
|
1650
|
+
const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
|
|
1651
|
+
if (entries.length === 0 && !pending) {
|
|
1341
1652
|
return;
|
|
1342
1653
|
}
|
|
1343
1654
|
let attempt = 1;
|
|
1344
1655
|
while (fs.existsSync(path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`))) {
|
|
1345
1656
|
attempt += 1;
|
|
1346
1657
|
}
|
|
1347
|
-
const archiveDir = path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1658
|
+
const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1659
|
+
const intent = pending ?? { archiveDir, entries, stage_run: live ?? null, project_id: projectId, run_id: runId, prior_attempt: live?.attempt ?? null, next_attempt: nextAttempt(live), runtime_revision: prior?.runtime_revision ?? 0, generation };
|
|
1660
|
+
if (!pending) {
|
|
1661
|
+
writeJsonFile(journal, intent);
|
|
1662
|
+
}
|
|
1348
1663
|
ensureDir(archiveDir);
|
|
1349
1664
|
for (const entry of entries) {
|
|
1350
|
-
|
|
1665
|
+
const source = path.join(currentStageDir, entry);
|
|
1666
|
+
const target = path.join(archiveDir, entry);
|
|
1667
|
+
if (fs.existsSync(source)) {
|
|
1668
|
+
if (fs.existsSync(target))
|
|
1669
|
+
throw new AppError("archive_conflict", "Both live and archived artifact exist; preserve both for recovery", 2, { source, target });
|
|
1670
|
+
fs.renameSync(source, target);
|
|
1671
|
+
}
|
|
1672
|
+
else if (!fs.existsSync(target))
|
|
1673
|
+
throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
|
|
1351
1674
|
}
|
|
1675
|
+
context.db.writeTransaction(() => {
|
|
1676
|
+
const current = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1677
|
+
const currentIndex = current ? JSON.parse(current.index_json) : null;
|
|
1678
|
+
const currentAttempt = currentIndex?.stage_runs.find((stage) => stage.dir === stageDir)?.attempt;
|
|
1679
|
+
if (currentIndex && intent.runtime_revision !== undefined && (currentIndex.runtime_revision ?? 0) !== intent.runtime_revision && currentAttempt !== intent.next_attempt)
|
|
1680
|
+
throw new AppError("archive_owner_changed", "RUN revision changed while an archive was pending", 1, { journal });
|
|
1681
|
+
if (intent.prior_attempt && currentAttempt !== intent.prior_attempt && currentAttempt !== intent.next_attempt)
|
|
1682
|
+
throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal });
|
|
1683
|
+
if (row && generation !== assertRunMutationAllowed(context, projectId, runId))
|
|
1684
|
+
throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal });
|
|
1685
|
+
if (intent.next_attempt === undefined || currentAttempt !== intent.next_attempt) {
|
|
1686
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
1687
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
1688
|
+
const rewrite = (table, column, where, values) => {
|
|
1689
|
+
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]);
|
|
1690
|
+
};
|
|
1691
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
1692
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
1693
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
1694
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
1695
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
1696
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
1697
|
+
}
|
|
1698
|
+
context.db.afterCommit(() => {
|
|
1699
|
+
if (!fs.existsSync(journal))
|
|
1700
|
+
return;
|
|
1701
|
+
// Retain original Stage facts with the immutable attempt after SQL paths commit.
|
|
1702
|
+
fs.renameSync(journal, path.join(archiveDir, "archive.json"));
|
|
1703
|
+
});
|
|
1704
|
+
});
|
|
1352
1705
|
}
|
|
1353
1706
|
function writeJsonFile(file, value) {
|
|
1354
1707
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -1373,3 +1726,5 @@ export function gitFacts(workspaceRoot) {
|
|
|
1373
1726
|
status: status === null ? "unavailable" : status.length > 0 ? "dirty" : "clean"
|
|
1374
1727
|
};
|
|
1375
1728
|
}
|
|
1729
|
+
import { assertRunMutationAllowed, assertStageSettlementAllowed, recoveryGuard } from "./run-recovery.js";
|
|
1730
|
+
import { runControlDirective } from "./run-control.js";
|