@deksden-com/dd-flow-cli 0.9.0-beta.10 → 0.9.0-beta.103
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 +491 -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/run-cli.js +1437 -384
- package/dist/cli.js +6 -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 +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/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 +502 -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 +387 -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 +640 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +978 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +135 -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 +296 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -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/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 +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +583 -99
- 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 +165 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +952 -83
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +165 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +596 -300
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +103 -17
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1128 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -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 +368 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +136 -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 +36 -0
- package/dist/services/run-controller.js +803 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +371 -74
- package/dist/services/runtime-budget.js +439 -0
- package/dist/services/runtime-command.js +43 -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 +565 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +84 -55
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +190 -80
- package/dist/services/vnext-code.js +283 -248
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +296 -105
- package/dist/services/vnext-plan-review.js +141 -94
- package/dist/services/vnext-plan.js +142 -69
- package/dist/services/vnext-protocolize.js +132 -57
- package/dist/services/vnext-specify.js +95 -65
- package/dist/services/work-registry.js +891 -162
- 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 +459 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +24 -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";
|
|
@@ -35,22 +37,26 @@ const allowedRunFlowKinds = [
|
|
|
35
37
|
"review",
|
|
36
38
|
"custom"
|
|
37
39
|
];
|
|
38
|
-
export function
|
|
40
|
+
export function prepareFlowRunStart(context, input) {
|
|
41
|
+
const subjectId = requiredPlain(input.subjectId, "subject-id");
|
|
42
|
+
const subjectType = requiredPlain(input.subjectType, "subject-type");
|
|
43
|
+
const slug = normalizeSlug(input.slug);
|
|
44
|
+
for (const [name, value] of [["task profile", input.taskProfile], ["protocol overrides", input.protocolOverrides], ["RUN overrides", input.runOverrides]]) {
|
|
45
|
+
if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value)))
|
|
46
|
+
throw new AppError("validation", `${name} must be a JSON object`, 2);
|
|
47
|
+
}
|
|
39
48
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
49
|
+
const flowKind = parseRunFlowKind(input.flowKind);
|
|
50
|
+
const vnext = flowKind === "vnext_protocolize" || flowKind === "vnext_specify";
|
|
51
|
+
if (input.executionRoutingFile !== undefined && !vnext)
|
|
52
|
+
throw new AppError("usage", "Execution routing input is supported only for vNext RUNs", 2);
|
|
53
|
+
const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
|
|
54
|
+
const executionProfile = vnext ? snapshotVnextExecutionProfile(projectRoot, context.ddFlowHome, routingInput) : undefined;
|
|
40
55
|
const flowContract = input.flowKind === "mb-upgrade"
|
|
41
56
|
? loadUpgradeFlowContract(context, projectRoot)
|
|
42
57
|
: loadProjectFlowContract(projectRoot);
|
|
43
|
-
registerProject(context, { root: projectRoot });
|
|
44
|
-
const project = requireProjectByRoot(context, projectRoot);
|
|
45
58
|
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
59
|
const now = context.now();
|
|
51
|
-
const executionProfile = (flowKind === "vnext_protocolize" || flowKind === "vnext_specify")
|
|
52
|
-
? snapshotVnextExecutionProfile(projectRoot)
|
|
53
|
-
: undefined;
|
|
54
60
|
const flowFlags = resolveFlowFlags(flowContract, {
|
|
55
61
|
flowKind,
|
|
56
62
|
...(input.preset ? { preset: input.preset } : {}),
|
|
@@ -60,6 +66,14 @@ export function startFlowRun(context, input) {
|
|
|
60
66
|
snapshotRevision: 1,
|
|
61
67
|
now
|
|
62
68
|
});
|
|
69
|
+
return { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags };
|
|
70
|
+
}
|
|
71
|
+
export function startFlowRun(context, input, prepared = prepareFlowRunStart(context, input)) {
|
|
72
|
+
const { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags } = prepared;
|
|
73
|
+
registerProject(context, { root: projectRoot });
|
|
74
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
75
|
+
const runId = nextRunId(context, project.id, slug);
|
|
76
|
+
const { shortId } = parseFullEntityId(runId);
|
|
63
77
|
const runHome = projectRunHome(context.ddFlowHome, project.id, runId);
|
|
64
78
|
const runDirRelative = path.posix.join("runs", runId);
|
|
65
79
|
const runtimePath = projectRunJsonPath(context.ddFlowHome, project.id, runId);
|
|
@@ -69,8 +83,8 @@ export function startFlowRun(context, input) {
|
|
|
69
83
|
short_id: shortId,
|
|
70
84
|
flow_kind: flowKind,
|
|
71
85
|
subject: {
|
|
72
|
-
id:
|
|
73
|
-
title:
|
|
86
|
+
id: subjectId,
|
|
87
|
+
title: subjectId
|
|
74
88
|
},
|
|
75
89
|
project: {
|
|
76
90
|
id: project.id,
|
|
@@ -126,6 +140,8 @@ export function startFlowRun(context, input) {
|
|
|
126
140
|
};
|
|
127
141
|
ensureDir(path.dirname(runtimePath));
|
|
128
142
|
ensureDir(runHome);
|
|
143
|
+
if (routingInput)
|
|
144
|
+
fs.writeFileSync(path.join(runHome, "execution-routing.input.json"), routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
129
145
|
bindCurrentEngineToRun(context, { projectRoot, runId, runRoot: runHome });
|
|
130
146
|
const persistedIndex = persistedVnextIndex(index);
|
|
131
147
|
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
|
|
@@ -141,7 +157,7 @@ export function startFlowRun(context, input) {
|
|
|
141
157
|
project.root,
|
|
142
158
|
workspaceRoot,
|
|
143
159
|
flowKind,
|
|
144
|
-
|
|
160
|
+
subjectType,
|
|
145
161
|
index.subject.id,
|
|
146
162
|
index.status,
|
|
147
163
|
index.verdict,
|
|
@@ -173,10 +189,10 @@ export function startFlowRun(context, input) {
|
|
|
173
189
|
});
|
|
174
190
|
return { ok: true, run: flowRunSummary(requireRunById(context, project.id, runId)), index };
|
|
175
191
|
}
|
|
176
|
-
function snapshotVnextExecutionProfile(projectRoot) {
|
|
192
|
+
function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput) {
|
|
177
193
|
const profile = loadVnextExecutionProfile(projectRoot);
|
|
178
194
|
const sourcePath = executionProfilePath(projectRoot);
|
|
179
|
-
return {
|
|
195
|
+
return freezeRunExecutionRouting(ddFlowHome, {
|
|
180
196
|
schema_id: "dd-flow/run-execution-profile@1",
|
|
181
197
|
source_path: sourcePath,
|
|
182
198
|
source_checksum: crypto.createHash("sha256").update(fs.readFileSync(sourcePath)).digest("hex"),
|
|
@@ -188,9 +204,41 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
188
204
|
merge_delivery: profile.merge_delivery,
|
|
189
205
|
merge_cleanup: profile.merge_cleanup,
|
|
190
206
|
stop_target: profile.stop_target,
|
|
191
|
-
code_bootstrap: profile.code_bootstrap
|
|
207
|
+
code_bootstrap: profile.code_bootstrap,
|
|
208
|
+
...(profile.execution ? { execution: profile.execution } : {})
|
|
192
209
|
}
|
|
193
|
-
};
|
|
210
|
+
}, routingInput);
|
|
211
|
+
}
|
|
212
|
+
/** Fresh RUN and fresh stage-entry experiments freeze through the same resolver. */
|
|
213
|
+
export function freezeRunExecutionRouting(ddFlowHome, base, input) {
|
|
214
|
+
const execution = input?.execution ?? base.settings.execution;
|
|
215
|
+
const profiles = execution ? snapshotAgentProfiles(ddFlowHome, execution) : undefined;
|
|
216
|
+
if (execution && profiles && base.settings.merge_mode === "same_session" && base.settings.stop_target === "merge_completed") {
|
|
217
|
+
const mode = base.settings.code_review_mode;
|
|
218
|
+
assertSameSessionMerge({ ddFlowHome, policy: execution, profiles, previousStages: mode === "off" ? ["code"] : mode === "auto" ? ["code", "code-review"] : ["code-review"] });
|
|
219
|
+
}
|
|
220
|
+
return { ...base, ...(profiles ? { agent_profiles: profiles } : {}),
|
|
221
|
+
...(input ? { routing_input: { source_path: input.source_path, source_checksum: input.source_checksum, artifact: "execution-routing.input.json" } } : {}),
|
|
222
|
+
settings: { ...base.settings, ...(execution ? { execution } : {}) } };
|
|
223
|
+
}
|
|
224
|
+
/** Called only after a stage-entry import has archived and detached source owners. */
|
|
225
|
+
export function importFlowRunExecutionProfile(context, input) {
|
|
226
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
227
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
228
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
229
|
+
const index = authoritativeIndex(run);
|
|
230
|
+
const lineage = JSON.parse(fs.readFileSync(input.lineageFile, "utf8"));
|
|
231
|
+
if (lineage.schema_id !== "dd-flow/snapshot-runtime-lineage@1" || lineage.run_id !== run.id || path.resolve(lineage.restored_runtime ?? "") !== path.resolve(context.ddFlowHome)
|
|
232
|
+
|| !vnextStages.some(stage => stage.id === input.stage)
|
|
233
|
+
|| (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]))
|
|
234
|
+
|| index.stage_runs.some(stage => stage.stage === input.stage || !["done", "skipped"].includes(stage.status))
|
|
235
|
+
|| context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? AND status IN ('active','idle') LIMIT 1", [project.id, run.id])) {
|
|
236
|
+
throw new AppError("snapshot_execution_active", "Execution policy can be selected only at a detached stage-entry import", 1);
|
|
237
|
+
}
|
|
238
|
+
index.execution_profile = { ...input.profile, stage_entry_import: { stage: input.stage, lineage_file: input.lineageFile } };
|
|
239
|
+
index.updated_at = context.now();
|
|
240
|
+
persistRunState(context, project, run, index);
|
|
241
|
+
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
242
|
}
|
|
195
243
|
/**
|
|
196
244
|
* A vNext route may materialize a feature checkout after its semantic
|
|
@@ -200,6 +248,7 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
200
248
|
export function rebindFlowRunWorkspace(context, input) {
|
|
201
249
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
202
250
|
const run = resolveRun(context, project.id, input.runId);
|
|
251
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
203
252
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
|
|
204
253
|
const index = authoritativeIndex(run);
|
|
205
254
|
const now = context.now();
|
|
@@ -229,7 +278,41 @@ export function getFlowRunStatus(context, input) {
|
|
|
229
278
|
const run = resolveRun(context, project.id, input.runId);
|
|
230
279
|
const index = authoritativeIndex(run);
|
|
231
280
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
232
|
-
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
281
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
282
|
+
}
|
|
283
|
+
/** A read-only projection of the next legal lifecycle action.
|
|
284
|
+
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
285
|
+
* transition from a model's prose or a hard-coded stage array. */
|
|
286
|
+
function runContinuation(context, run, index) {
|
|
287
|
+
if (["done", "failed", "cancelled"].includes(run.status))
|
|
288
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
289
|
+
const control = runControlDirective(context, run.project_id, run.id);
|
|
290
|
+
if (control)
|
|
291
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "operator_control", command: null, control };
|
|
292
|
+
const active = index.stage_runs.find((stage) => !["done", "skipped"].includes(stage.status));
|
|
293
|
+
if (active) {
|
|
294
|
+
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]);
|
|
295
|
+
const kind = active.status === "running" ? "continue_stage" : active.status === "paused" || active.status === "waiting_for_user" ? "paused" : "blocked";
|
|
296
|
+
return { kind, stage: active.stage, attempt: active.attempt ?? null, work_id: active.pause?.work_id ?? runningWork?.work_id ?? null, reason: active.status, command: null };
|
|
297
|
+
}
|
|
298
|
+
if (run.status === "waiting_for_user" || run.status === "paused")
|
|
299
|
+
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
300
|
+
if (run.status === "blocked")
|
|
301
|
+
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
302
|
+
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
303
|
+
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
304
|
+
const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
|
|
305
|
+
if (!next)
|
|
306
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
307
|
+
const command = `${flowCommand(context)} stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
|
|
308
|
+
const profile = index.execution_profile;
|
|
309
|
+
const execution = profile?.settings.execution ? profile.agent_profiles
|
|
310
|
+
? { 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 }) }
|
|
311
|
+
: { status: "unfrozen", reason: "execution_profile_not_frozen" }
|
|
312
|
+
: undefined;
|
|
313
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command, ...(execution ? { execution } : {}) };
|
|
314
|
+
}
|
|
315
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "no_vnext_continuation", command: null };
|
|
233
316
|
}
|
|
234
317
|
export function getFlowRunConfig(context, input) {
|
|
235
318
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -250,9 +333,12 @@ export function getFlowRunVariables(context, input) {
|
|
|
250
333
|
return { ok: true, run_id: run.id, variables };
|
|
251
334
|
}
|
|
252
335
|
export function setFlowRunVariable(context, input) {
|
|
336
|
+
return setRunVariable(context, input, input.value, prepareFlowRunVariable(context, input));
|
|
337
|
+
}
|
|
338
|
+
export function prepareFlowRunVariable(context, input) {
|
|
253
339
|
if (!input.key.startsWith("user."))
|
|
254
340
|
throw new AppError("validation", "Only user.* RUN variables may be set directly", 2, { key: input.key });
|
|
255
|
-
return
|
|
341
|
+
return prepareRunVariable(context, input);
|
|
256
342
|
}
|
|
257
343
|
/** Controllers own policy.* and runtime.* values; agents never call this directly. */
|
|
258
344
|
export function setRuntimeRunVariable(context, input) {
|
|
@@ -261,20 +347,7 @@ export function setRuntimeRunVariable(context, input) {
|
|
|
261
347
|
return setRunVariable(context, input, input.value);
|
|
262
348
|
}
|
|
263
349
|
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
|
-
}
|
|
350
|
+
const { reason, project, run, index } = prepareFlowRunConfig(context, input);
|
|
278
351
|
const now = context.now();
|
|
279
352
|
const settings = runSettings(index);
|
|
280
353
|
const previous = input.key === "plan_review.mode" ? settings.plan_review : settings.code_review;
|
|
@@ -292,10 +365,29 @@ export function setFlowRunConfig(context, input) {
|
|
|
292
365
|
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
293
366
|
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
294
367
|
}
|
|
368
|
+
export function prepareFlowRunConfig(context, input) {
|
|
369
|
+
if (input.key !== "plan_review.mode" && input.key !== "code_review.mode") {
|
|
370
|
+
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
371
|
+
}
|
|
372
|
+
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
373
|
+
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
374
|
+
}
|
|
375
|
+
const reason = requiredPlain(input.reason, "reason");
|
|
376
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
377
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
378
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
379
|
+
const index = authoritativeIndex(run);
|
|
380
|
+
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
381
|
+
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
382
|
+
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
383
|
+
}
|
|
384
|
+
return { reason, project, run, index };
|
|
385
|
+
}
|
|
295
386
|
/** Freeze an automatic review request once from already accepted policy or PLAN facts. */
|
|
296
387
|
export function freezeFlowRunReviewMode(context, input) {
|
|
297
388
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
298
389
|
const run = resolveRun(context, project.id, input.runId);
|
|
390
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
299
391
|
const index = authoritativeIndex(run);
|
|
300
392
|
const settings = runSettings(index);
|
|
301
393
|
const key = input.review === "plan" ? "plan_review" : "code_review";
|
|
@@ -337,8 +429,9 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
337
429
|
export function reviseFlowRunFlags(context, input) {
|
|
338
430
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
339
431
|
const run = resolveRun(context, project.id, input.runId);
|
|
432
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
340
433
|
const idempotencyKey = requiredPlain(input.idempotencyKey, "idempotency-key");
|
|
341
|
-
context.db.
|
|
434
|
+
context.db.beginWriteTransaction();
|
|
342
435
|
let committed = false;
|
|
343
436
|
try {
|
|
344
437
|
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]);
|
|
@@ -459,24 +552,31 @@ export function listFlowRuns(context, input) {
|
|
|
459
552
|
.map(flowRunSummary);
|
|
460
553
|
return { ok: true, project, runs };
|
|
461
554
|
}
|
|
462
|
-
export function
|
|
555
|
+
export function prepareFlowRunStageAttachment(context, input) {
|
|
463
556
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
464
557
|
const run = resolveRun(context, project.id, input.runId);
|
|
558
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
465
559
|
const index = authoritativeIndex(run);
|
|
466
|
-
const now = context.now();
|
|
467
560
|
const stage = requiredPlain(input.stage, "stage");
|
|
468
|
-
const dir =
|
|
561
|
+
const dir = prepareStageDirectory(input.dir);
|
|
469
562
|
const status = parseStageStatus(input.status);
|
|
470
563
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
471
564
|
assertVnextStageStart(run, index, stage, status, existing);
|
|
565
|
+
return { project, run, index, stage, dir, status, existing };
|
|
566
|
+
}
|
|
567
|
+
export function attachFlowRunStage(context, input) {
|
|
568
|
+
// Mutable stage state is re-read immediately before archiving/persisting.
|
|
569
|
+
const { project, run, index, stage, dir, status, existing } = prepareFlowRunStageAttachment(context, input);
|
|
570
|
+
const now = context.now();
|
|
472
571
|
if (status === "running" && existing) {
|
|
473
|
-
archiveExistingStageAttempt(runArtifactRoot(run), dir);
|
|
572
|
+
archiveExistingStageAttempt(context, project.id, run.id, runArtifactRoot(run), dir);
|
|
474
573
|
}
|
|
475
574
|
const stageRun = {
|
|
476
575
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
477
576
|
stage,
|
|
478
577
|
dir,
|
|
479
578
|
status,
|
|
579
|
+
...(input.inputSha256 ? { input_sha256: input.inputSha256 } : {}),
|
|
480
580
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : existing?.data_schema_id ? { data_schema_id: existing.data_schema_id } : {}),
|
|
481
581
|
updated_at: now
|
|
482
582
|
};
|
|
@@ -506,24 +606,49 @@ export function attachFlowRunStage(context, input) {
|
|
|
506
606
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
507
607
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
508
608
|
}
|
|
509
|
-
|
|
609
|
+
/** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
|
|
610
|
+
export function withStageSettlement(context, operation) {
|
|
611
|
+
return context.db.writeTransaction(operation);
|
|
612
|
+
}
|
|
613
|
+
export function prepareFlowRunStageCompletion(context, input) {
|
|
614
|
+
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
615
|
+
if (value !== undefined)
|
|
616
|
+
requiredPlain(value, name);
|
|
617
|
+
}
|
|
618
|
+
if (input.aliases !== undefined) {
|
|
619
|
+
if (!Array.isArray(input.aliases))
|
|
620
|
+
throw new AppError("validation", "artifact aliases must be an array", 2);
|
|
621
|
+
for (const alias of input.aliases)
|
|
622
|
+
requiredPlain(alias, "artifact alias");
|
|
623
|
+
}
|
|
510
624
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
511
625
|
const run = resolveRun(context, project.id, input.runId);
|
|
626
|
+
if (["done", "failed", "skipped"].includes(input.status))
|
|
627
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.stage);
|
|
628
|
+
else
|
|
629
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
512
630
|
const index = authoritativeIndex(run);
|
|
513
|
-
const now = context.now();
|
|
514
631
|
const stage = requiredPlain(input.stage, "stage");
|
|
515
632
|
const status = parseStageStatus(input.status);
|
|
516
633
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
517
634
|
if (!existing) {
|
|
518
635
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
519
636
|
}
|
|
637
|
+
return { project, run, index, stage, status, existing };
|
|
638
|
+
}
|
|
639
|
+
export function completeFlowRunStage(context, input) {
|
|
640
|
+
const { project, run, index, stage, status, existing } = prepareFlowRunStageCompletion(context, input);
|
|
641
|
+
const now = context.now();
|
|
642
|
+
const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
|
|
520
643
|
const stageRun = {
|
|
521
644
|
...existing,
|
|
522
645
|
status,
|
|
523
646
|
...(input.stageReport ? { stage_report: input.stageReport } : {}),
|
|
524
647
|
...(input.data ? { data: input.data } : {}),
|
|
648
|
+
...(dataPath && fs.existsSync(dataPath) ? { data_sha256: crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") } : {}),
|
|
525
649
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
|
|
526
650
|
...(input.report ? { report: input.report } : {}),
|
|
651
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
527
652
|
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
528
653
|
updated_at: now,
|
|
529
654
|
...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
|
|
@@ -566,6 +691,7 @@ export function completeFlowRunStage(context, input) {
|
|
|
566
691
|
export function pauseFlowRunStage(context, input) {
|
|
567
692
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
568
693
|
const run = resolveRun(context, project.id, input.runId);
|
|
694
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
569
695
|
const index = authoritativeIndex(run);
|
|
570
696
|
const stage = requiredPlain(input.stage, "stage");
|
|
571
697
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -601,6 +727,7 @@ export function pauseFlowRunStage(context, input) {
|
|
|
601
727
|
export function resumeFlowRunStage(context, input) {
|
|
602
728
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
603
729
|
const run = resolveRun(context, project.id, input.runId);
|
|
730
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
604
731
|
const index = authoritativeIndex(run);
|
|
605
732
|
const stage = requiredPlain(input.stage, "stage");
|
|
606
733
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -629,14 +756,23 @@ export function resumeFlowRunStage(context, input) {
|
|
|
629
756
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
630
757
|
}
|
|
631
758
|
/** Pause the current stage for a non-user runtime blocker. */
|
|
632
|
-
export function
|
|
759
|
+
export function prepareFlowRunStageBlock(context, input) {
|
|
760
|
+
if (!["engine", "harness", "environment"].includes(input.kind) || typeof input.retryable !== "boolean")
|
|
761
|
+
throw new AppError("validation", "stage block requires a valid kind and boolean retryable", 2);
|
|
762
|
+
requiredPlain(input.code, "code");
|
|
763
|
+
requiredPlain(input.summary, "summary");
|
|
633
764
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
634
765
|
const run = resolveRun(context, project.id, input.runId);
|
|
766
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
635
767
|
const index = authoritativeIndex(run);
|
|
636
768
|
const stage = requiredPlain(input.stage, "stage");
|
|
637
769
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
638
770
|
if (!stageRun || stageRun.status !== "running")
|
|
639
771
|
throw new AppError("invalid_stage_state", "Only a running stage can be blocked", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
|
|
772
|
+
return { project, run, index, stage, stageRun };
|
|
773
|
+
}
|
|
774
|
+
export function blockFlowRunStage(context, input, prepared = prepareFlowRunStageBlock(context, input)) {
|
|
775
|
+
const { project, run, index, stage, stageRun } = prepared;
|
|
640
776
|
const now = context.now();
|
|
641
777
|
stageRun.status = "blocked";
|
|
642
778
|
stageRun.updated_at = now;
|
|
@@ -654,16 +790,20 @@ export function blockFlowRunStage(context, input) {
|
|
|
654
790
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
655
791
|
}
|
|
656
792
|
/** Continue the same stage after its non-user blocker was repaired externally. */
|
|
657
|
-
export function
|
|
793
|
+
export function prepareFlowRunStageUnblock(context, input) {
|
|
658
794
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
659
795
|
const run = resolveRun(context, project.id, input.runId);
|
|
796
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
660
797
|
const index = authoritativeIndex(run);
|
|
661
798
|
const stage = requiredPlain(input.stage, "stage");
|
|
662
799
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
663
800
|
if (!stageRun || stageRun.status !== "blocked" || !stageRun.blocker || stageRun.blocker.work_id !== input.workId)
|
|
664
801
|
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" });
|
|
802
|
+
return { project, run, index, stage, stageRun, blocker: stageRun.blocker };
|
|
803
|
+
}
|
|
804
|
+
export function unblockFlowRunStage(context, input, prepared = prepareFlowRunStageUnblock(context, input)) {
|
|
805
|
+
const { project, run, index, stage, stageRun, blocker } = prepared;
|
|
665
806
|
const now = context.now();
|
|
666
|
-
const blocker = stageRun.blocker;
|
|
667
807
|
const blockedMs = durationMs(blocker.blocked_at, now) ?? 0;
|
|
668
808
|
stageRun.status = "running";
|
|
669
809
|
stageRun.updated_at = now;
|
|
@@ -687,11 +827,36 @@ function operationalRunStatus(context, projectId, runId) {
|
|
|
687
827
|
const leaves = unfinished.filter((work) => !parents.has(work.work_id));
|
|
688
828
|
return leaves.length > 0 && leaves.every((work) => work.status === "paused") ? "paused" : "running";
|
|
689
829
|
}
|
|
690
|
-
|
|
691
|
-
|
|
830
|
+
/** Keep the logical RUN projection aligned with recovery authority. Stage/Work
|
|
831
|
+
* evidence and verdicts survive a reversible stop; terminal runs never reopen. */
|
|
832
|
+
export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
|
|
833
|
+
const run = resolveRun(context, projectId, runId);
|
|
834
|
+
if (!["running", "paused"].includes(run.status))
|
|
835
|
+
return;
|
|
836
|
+
const index = authoritativeIndex(run);
|
|
837
|
+
const status = paused || index.stage_runs.some(stage => ["paused", "waiting_for_user", "blocked"].includes(stage.status))
|
|
838
|
+
? "paused" : operationalRunStatus(context, projectId, runId);
|
|
839
|
+
if (run.status === status && index.status === status)
|
|
840
|
+
return;
|
|
841
|
+
index.status = status;
|
|
842
|
+
index.updated_at = context.now();
|
|
843
|
+
const project = requireProjectByRoot(context, run.project_root);
|
|
844
|
+
persistRunState(context, project, run, index);
|
|
845
|
+
}
|
|
846
|
+
export function prepareFlowRunCompletion(context, input) {
|
|
847
|
+
if (input.manualOverrideReason !== undefined) {
|
|
848
|
+
requiredPlain(input.manualOverrideReason, "override reason");
|
|
849
|
+
if (!["cancelled", "failed"].includes(input.status))
|
|
850
|
+
throw new AppError("validation", "Manual RUN override may only cancel or fail a RUN", 2);
|
|
851
|
+
}
|
|
852
|
+
if (input.verdict !== undefined)
|
|
853
|
+
requiredPlain(input.verdict, "verdict");
|
|
854
|
+
if (input.nextAction !== undefined)
|
|
855
|
+
requiredPlain(input.nextAction, "next action");
|
|
856
|
+
const project = requireProjectByRoot(context, input.manualOverrideReason ? canonicalPath(input.projectRoot, false) : resolveProjectRoot(input.projectRoot));
|
|
692
857
|
const run = resolveRun(context, project.id, input.runId);
|
|
858
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
693
859
|
const index = authoritativeIndex(run);
|
|
694
|
-
const now = context.now();
|
|
695
860
|
const status = parseRunStatus(input.status);
|
|
696
861
|
if (!['done', 'cancelled', 'failed'].includes(status)) {
|
|
697
862
|
throw new AppError('validation', 'completeFlowRun accepts only done, cancelled, or failed; use stage block/unblock for a recoverable blocker', 2, { status });
|
|
@@ -701,6 +866,11 @@ export function completeFlowRun(context, input) {
|
|
|
701
866
|
if (activeWork)
|
|
702
867
|
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
868
|
}
|
|
869
|
+
return { project, run, index, status };
|
|
870
|
+
}
|
|
871
|
+
export function completeFlowRun(context, input) {
|
|
872
|
+
const { project, run, index, status } = prepareFlowRunCompletion(context, input);
|
|
873
|
+
const now = context.now();
|
|
704
874
|
if (input.manualOverrideReason && (status === "cancelled" || status === "failed")) {
|
|
705
875
|
closeOpenStagesForOverride(index, status, now);
|
|
706
876
|
closeOpenWorksForOverride(context, project.id, run.id, status, now);
|
|
@@ -743,16 +913,85 @@ export function advanceFlowRun(context, input) {
|
|
|
743
913
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
744
914
|
const run = resolveRun(context, project.id, input.runId);
|
|
745
915
|
const index = authoritativeIndex(run);
|
|
916
|
+
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)))
|
|
917
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.settlementStage);
|
|
918
|
+
else
|
|
919
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
920
|
+
const control = runControlDirective(context, project.id, run.id);
|
|
746
921
|
const now = context.now();
|
|
747
922
|
index.status = input.status;
|
|
748
923
|
index.verdict = input.verdict;
|
|
749
|
-
index.next_action = input.nextAction ?? null;
|
|
924
|
+
index.next_action = control ? null : input.nextAction ?? null;
|
|
750
925
|
index.updated_at = now;
|
|
751
926
|
persistRunState(context, project, run, index);
|
|
752
|
-
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status:
|
|
753
|
-
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_progressed", run_id: run.id, status:
|
|
927
|
+
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 } });
|
|
928
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_progressed", run_id: run.id, status: index.status, next_action: index.next_action });
|
|
754
929
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
755
|
-
return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
930
|
+
return { ok: true, ...(control ? { control } : {}), run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* The only legal backward edge in vNext: an integration gate exposed a
|
|
934
|
+
* product defect, so the source must be repaired and reviewed again. Normal
|
|
935
|
+
* stage start remains fail-closed for completed stages.
|
|
936
|
+
*/
|
|
937
|
+
export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
938
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
939
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
940
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
941
|
+
const index = authoritativeIndex(run);
|
|
942
|
+
const priorRepair = index.variables?.["merge.source_repair"];
|
|
943
|
+
if (priorRepair?.merge_request_id === input.mergeRequestId && priorRepair.cycle)
|
|
944
|
+
return { run_id: run.id, cycle: priorRepair.cycle };
|
|
945
|
+
const code = index.stage_runs.find((item) => item.stage === "code");
|
|
946
|
+
const review = index.stage_runs.find((item) => item.stage === "code-review");
|
|
947
|
+
const merge = index.stage_runs.find((item) => item.stage === "merge");
|
|
948
|
+
if (!code || code.status !== "done" || !review || review.status !== "done" || !merge || merge.status !== "running") {
|
|
949
|
+
throw new AppError("invalid_merge_repair_state", "Source repair requires accepted CODE/CODE-REVIEW and a running MERGE attempt", 2, {
|
|
950
|
+
run_id: run.id,
|
|
951
|
+
code: code?.status ?? null,
|
|
952
|
+
code_review: review?.status ?? null,
|
|
953
|
+
merge: merge?.status ?? null
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
const root = runArtifactRoot(run);
|
|
957
|
+
for (const stage of ["code", "code-review", "merge"])
|
|
958
|
+
archiveExistingStageAttempt(context, project.id, run.id, root, vnextStageDirectory(stage));
|
|
959
|
+
const now = context.now();
|
|
960
|
+
const restarted = {
|
|
961
|
+
...code,
|
|
962
|
+
status: "running",
|
|
963
|
+
started_at: now,
|
|
964
|
+
updated_at: now,
|
|
965
|
+
attempt: nextAttempt(code)
|
|
966
|
+
};
|
|
967
|
+
delete restarted.completed_at;
|
|
968
|
+
delete restarted.duration_ms;
|
|
969
|
+
delete restarted.stage_report;
|
|
970
|
+
delete restarted.data;
|
|
971
|
+
delete restarted.report;
|
|
972
|
+
delete restarted.artifact_aliases;
|
|
973
|
+
const cycle = `MRR-${input.mergeRequestId}`;
|
|
974
|
+
index.stage_runs = index.stage_runs.filter((item) => item.stage !== "code" && item.stage !== "code-review" && item.stage !== "merge");
|
|
975
|
+
index.stage_runs.push(restarted);
|
|
976
|
+
index.variables = { ...(index.variables ?? {}), "merge.source_repair": { cycle, merge_request_id: input.mergeRequestId, prepared_at: now } };
|
|
977
|
+
index.status = "running";
|
|
978
|
+
index.verdict = "merge_source_repair";
|
|
979
|
+
index.next_action = "start_code_repair";
|
|
980
|
+
index.current_stage = "code";
|
|
981
|
+
index.updated_at = now;
|
|
982
|
+
index.attempts = index.stage_runs.map((item) => ({
|
|
983
|
+
stage: item.stage,
|
|
984
|
+
attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1,
|
|
985
|
+
root: item.dir,
|
|
986
|
+
archive: null,
|
|
987
|
+
status: item.status,
|
|
988
|
+
started_at: item.started_at ?? now,
|
|
989
|
+
finished_at: item.completed_at ?? null
|
|
990
|
+
}));
|
|
991
|
+
persistRunState(context, project, run, index);
|
|
992
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
|
|
993
|
+
appendRunTimeline(root, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
994
|
+
return { run_id: run.id, cycle };
|
|
756
995
|
}
|
|
757
996
|
function closeOpenStagesForOverride(index, status, now) {
|
|
758
997
|
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
@@ -1018,6 +1257,10 @@ function vnextSpecifyGuidance(run, index) {
|
|
|
1018
1257
|
};
|
|
1019
1258
|
}
|
|
1020
1259
|
function persistRunState(context, project, run, index, options = {}) {
|
|
1260
|
+
// Fenced productive admission is a logical pause, even while physical work
|
|
1261
|
+
// drains. Late admitted Stage results must not revive a stopped RUN.
|
|
1262
|
+
if (index.status === "running" && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1263
|
+
index.status = "paused";
|
|
1021
1264
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
1022
1265
|
const previousRuntime = readRuntimeSnapshot(runtimePath);
|
|
1023
1266
|
const runtimeRevision = Math.max(index.runtime_revision ?? 0, previousRuntime?.runtime_revision ?? 0) + 1;
|
|
@@ -1028,22 +1271,30 @@ function persistRunState(context, project, run, index, options = {}) {
|
|
|
1028
1271
|
}
|
|
1029
1272
|
const persistedIndex = persistedVnextIndex(index);
|
|
1030
1273
|
ensureDir(path.dirname(runtimePath));
|
|
1031
|
-
|
|
1032
|
-
|
|
1274
|
+
context.db.exec("SAVEPOINT persist_run_state");
|
|
1275
|
+
try {
|
|
1276
|
+
context.db.run(`UPDATE runs
|
|
1033
1277
|
SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?,
|
|
1034
1278
|
updated_at = ?, completed_at = ?
|
|
1035
1279
|
WHERE project_id = ? AND id = ?`, [
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1280
|
+
index.status,
|
|
1281
|
+
index.verdict,
|
|
1282
|
+
index.next_action,
|
|
1283
|
+
JSON.stringify(persistedIndex),
|
|
1284
|
+
runtimePath,
|
|
1285
|
+
runtimePath,
|
|
1286
|
+
index.updated_at,
|
|
1287
|
+
index.completed_at ?? null,
|
|
1288
|
+
project.id,
|
|
1289
|
+
run.id
|
|
1290
|
+
]);
|
|
1291
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, runtimeRevision, history));
|
|
1292
|
+
context.db.exec("RELEASE persist_run_state");
|
|
1293
|
+
}
|
|
1294
|
+
catch (error) {
|
|
1295
|
+
context.db.exec("ROLLBACK TO persist_run_state; RELEASE persist_run_state");
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1047
1298
|
}
|
|
1048
1299
|
/** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
|
|
1049
1300
|
function persistedVnextIndex(index) {
|
|
@@ -1085,10 +1336,15 @@ function materializedRunVariables(index) {
|
|
|
1085
1336
|
...(index.variables ?? {})
|
|
1086
1337
|
};
|
|
1087
1338
|
}
|
|
1088
|
-
function
|
|
1339
|
+
function prepareRunVariable(context, input) {
|
|
1089
1340
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1090
1341
|
const run = resolveRun(context, project.id, input.runId);
|
|
1342
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
1091
1343
|
const index = authoritativeIndex(run);
|
|
1344
|
+
return { project, run, index };
|
|
1345
|
+
}
|
|
1346
|
+
function setRunVariable(context, input, value, prepared = prepareRunVariable(context, input)) {
|
|
1347
|
+
const { project, run, index } = prepared;
|
|
1092
1348
|
if (index.variables?.[input.key] === value)
|
|
1093
1349
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
|
|
1094
1350
|
index.variables = { ...(index.variables ?? {}), [input.key]: value };
|
|
@@ -1139,10 +1395,12 @@ function readRuntimeSnapshot(file) {
|
|
|
1139
1395
|
function authoritativeIndex(run) {
|
|
1140
1396
|
try {
|
|
1141
1397
|
const state = JSON.parse(run.index_json);
|
|
1398
|
+
if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
|
|
1399
|
+
throw new Error("RUN index must contain stage_runs");
|
|
1142
1400
|
return { ...state, schema_id: runSchemaId };
|
|
1143
1401
|
}
|
|
1144
1402
|
catch (error) {
|
|
1145
|
-
throw new AppError("
|
|
1403
|
+
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
1146
1404
|
}
|
|
1147
1405
|
}
|
|
1148
1406
|
function sanitizeTimelineValue(value, key) {
|
|
@@ -1203,7 +1461,7 @@ function upsertStage(index, stageRun) {
|
|
|
1203
1461
|
}
|
|
1204
1462
|
index.stage_runs.sort((a, b) => a.order - b.order);
|
|
1205
1463
|
}
|
|
1206
|
-
function resolveRun(context, projectId, idOrAlias) {
|
|
1464
|
+
export function resolveRun(context, projectId, idOrAlias) {
|
|
1207
1465
|
if (isFullEntityId(idOrAlias)) {
|
|
1208
1466
|
return requireRunById(context, projectId, idOrAlias);
|
|
1209
1467
|
}
|
|
@@ -1297,7 +1555,7 @@ function parseStageStatus(value) {
|
|
|
1297
1555
|
}
|
|
1298
1556
|
return value;
|
|
1299
1557
|
}
|
|
1300
|
-
function
|
|
1558
|
+
export function prepareStageDirectory(value) {
|
|
1301
1559
|
const dir = requiredPlain(value, "dir");
|
|
1302
1560
|
if (!/^\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(dir)) {
|
|
1303
1561
|
throw new AppError("validation", "--dir must use NN-stage-slug format", 2, { dir });
|
|
@@ -1305,6 +1563,8 @@ function requiredStageDir(value) {
|
|
|
1305
1563
|
return dir;
|
|
1306
1564
|
}
|
|
1307
1565
|
function requiredPlain(value, label) {
|
|
1566
|
+
if (typeof value !== "string")
|
|
1567
|
+
throw new AppError("validation", `${label} must be a string`, 2);
|
|
1308
1568
|
const trimmed = value.trim();
|
|
1309
1569
|
if (!trimmed) {
|
|
1310
1570
|
throw new AppError("validation", `${label} must not be empty`, 2);
|
|
@@ -1312,6 +1572,8 @@ function requiredPlain(value, label) {
|
|
|
1312
1572
|
return trimmed;
|
|
1313
1573
|
}
|
|
1314
1574
|
function normalizeSlug(value) {
|
|
1575
|
+
if (typeof value !== "string")
|
|
1576
|
+
throw new AppError("validation", "--slug must be a string", 2);
|
|
1315
1577
|
const slug = value
|
|
1316
1578
|
.normalize("NFKD")
|
|
1317
1579
|
.replace(/[\u0300-\u036f]/g, "")
|
|
@@ -1329,26 +1591,59 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1329
1591
|
if (!fs.existsSync(absolute)) {
|
|
1330
1592
|
throw new AppError("not_found", `Workspace root does not exist: ${absolute}`, 1);
|
|
1331
1593
|
}
|
|
1594
|
+
if (!fs.statSync(absolute).isDirectory())
|
|
1595
|
+
throw new AppError("validation", "Workspace root must be a directory", 2, { workspace_root: absolute });
|
|
1332
1596
|
return fs.realpathSync(absolute);
|
|
1333
1597
|
}
|
|
1334
|
-
|
|
1598
|
+
/** Move one completed stage attempt and update only known durable path columns.
|
|
1599
|
+
* Receipt bytes remain immutable; readers resolve their new location through the DB. */
|
|
1600
|
+
export function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
|
|
1335
1601
|
const currentStageDir = path.join(runRoot, stageDir);
|
|
1336
1602
|
if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
|
|
1337
1603
|
return;
|
|
1338
1604
|
}
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1605
|
+
const journal = path.join(runRoot, `.archive-${stageDir}.json`);
|
|
1606
|
+
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
1607
|
+
const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
|
|
1608
|
+
if (entries.length === 0 && !pending) {
|
|
1341
1609
|
return;
|
|
1342
1610
|
}
|
|
1343
1611
|
let attempt = 1;
|
|
1344
1612
|
while (fs.existsSync(path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`))) {
|
|
1345
1613
|
attempt += 1;
|
|
1346
1614
|
}
|
|
1347
|
-
const archiveDir = path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1615
|
+
const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1616
|
+
if (!pending) {
|
|
1617
|
+
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1618
|
+
const prior = stored ? JSON.parse(stored.index_json) : null;
|
|
1619
|
+
writeJsonFile(journal, { archiveDir, entries, stage_run: prior?.stage_runs.find((stage) => stage.dir === stageDir) ?? null });
|
|
1620
|
+
}
|
|
1348
1621
|
ensureDir(archiveDir);
|
|
1349
1622
|
for (const entry of entries) {
|
|
1350
|
-
|
|
1623
|
+
const source = path.join(currentStageDir, entry);
|
|
1624
|
+
const target = path.join(archiveDir, entry);
|
|
1625
|
+
if (fs.existsSync(source)) {
|
|
1626
|
+
if (fs.existsSync(target))
|
|
1627
|
+
throw new AppError("archive_conflict", "Both live and archived artifact exist; preserve both for recovery", 2, { source, target });
|
|
1628
|
+
fs.renameSync(source, target);
|
|
1629
|
+
}
|
|
1630
|
+
else if (!fs.existsSync(target))
|
|
1631
|
+
throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
|
|
1351
1632
|
}
|
|
1633
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
1634
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
1635
|
+
const rewrite = (table, column, where, values) => {
|
|
1636
|
+
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]);
|
|
1637
|
+
};
|
|
1638
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
1639
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
1640
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
1641
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
1642
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
1643
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
1644
|
+
// Retain the original stage facts with its immutable artifacts; current
|
|
1645
|
+
// stage_runs stays a projection of the current attempt, not a second ledger.
|
|
1646
|
+
fs.renameSync(journal, path.join(archiveDir, "archive.json"));
|
|
1352
1647
|
}
|
|
1353
1648
|
function writeJsonFile(file, value) {
|
|
1354
1649
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -1373,3 +1668,5 @@ export function gitFacts(workspaceRoot) {
|
|
|
1373
1668
|
status: status === null ? "unavailable" : status.length > 0 ? "dirty" : "clean"
|
|
1374
1669
|
};
|
|
1375
1670
|
}
|
|
1671
|
+
import { assertRunMutationAllowed, assertStageSettlementAllowed, recoveryGuard } from "./run-recovery.js";
|
|
1672
|
+
import { runControlDirective } from "./run-control.js";
|