@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81
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 +402 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1344 -376
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/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 +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +575 -293
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +835 -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 +23 -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/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -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 +368 -74
- package/dist/services/runtime-budget.js +434 -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 +99 -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 +69 -28
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +701 -164
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -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 +11 -4
- 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,18 +552,24 @@ 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 }),
|
|
@@ -506,17 +605,39 @@ export function attachFlowRunStage(context, input) {
|
|
|
506
605
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
507
606
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
508
607
|
}
|
|
509
|
-
|
|
608
|
+
/** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
|
|
609
|
+
export function withStageSettlement(context, operation) {
|
|
610
|
+
return context.db.writeTransaction(operation);
|
|
611
|
+
}
|
|
612
|
+
export function prepareFlowRunStageCompletion(context, input) {
|
|
613
|
+
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
614
|
+
if (value !== undefined)
|
|
615
|
+
requiredPlain(value, name);
|
|
616
|
+
}
|
|
617
|
+
if (input.aliases !== undefined) {
|
|
618
|
+
if (!Array.isArray(input.aliases))
|
|
619
|
+
throw new AppError("validation", "artifact aliases must be an array", 2);
|
|
620
|
+
for (const alias of input.aliases)
|
|
621
|
+
requiredPlain(alias, "artifact alias");
|
|
622
|
+
}
|
|
510
623
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
511
624
|
const run = resolveRun(context, project.id, input.runId);
|
|
625
|
+
if (["done", "failed", "skipped"].includes(input.status))
|
|
626
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.stage);
|
|
627
|
+
else
|
|
628
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
512
629
|
const index = authoritativeIndex(run);
|
|
513
|
-
const now = context.now();
|
|
514
630
|
const stage = requiredPlain(input.stage, "stage");
|
|
515
631
|
const status = parseStageStatus(input.status);
|
|
516
632
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
517
633
|
if (!existing) {
|
|
518
634
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
519
635
|
}
|
|
636
|
+
return { project, run, index, stage, status, existing };
|
|
637
|
+
}
|
|
638
|
+
export function completeFlowRunStage(context, input) {
|
|
639
|
+
const { project, run, index, stage, status, existing } = prepareFlowRunStageCompletion(context, input);
|
|
640
|
+
const now = context.now();
|
|
520
641
|
const stageRun = {
|
|
521
642
|
...existing,
|
|
522
643
|
status,
|
|
@@ -524,6 +645,7 @@ export function completeFlowRunStage(context, input) {
|
|
|
524
645
|
...(input.data ? { data: input.data } : {}),
|
|
525
646
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
|
|
526
647
|
...(input.report ? { report: input.report } : {}),
|
|
648
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
527
649
|
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
528
650
|
updated_at: now,
|
|
529
651
|
...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
|
|
@@ -566,6 +688,7 @@ export function completeFlowRunStage(context, input) {
|
|
|
566
688
|
export function pauseFlowRunStage(context, input) {
|
|
567
689
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
568
690
|
const run = resolveRun(context, project.id, input.runId);
|
|
691
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
569
692
|
const index = authoritativeIndex(run);
|
|
570
693
|
const stage = requiredPlain(input.stage, "stage");
|
|
571
694
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -601,6 +724,7 @@ export function pauseFlowRunStage(context, input) {
|
|
|
601
724
|
export function resumeFlowRunStage(context, input) {
|
|
602
725
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
603
726
|
const run = resolveRun(context, project.id, input.runId);
|
|
727
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
604
728
|
const index = authoritativeIndex(run);
|
|
605
729
|
const stage = requiredPlain(input.stage, "stage");
|
|
606
730
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -629,14 +753,23 @@ export function resumeFlowRunStage(context, input) {
|
|
|
629
753
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
630
754
|
}
|
|
631
755
|
/** Pause the current stage for a non-user runtime blocker. */
|
|
632
|
-
export function
|
|
756
|
+
export function prepareFlowRunStageBlock(context, input) {
|
|
757
|
+
if (!["engine", "harness", "environment"].includes(input.kind) || typeof input.retryable !== "boolean")
|
|
758
|
+
throw new AppError("validation", "stage block requires a valid kind and boolean retryable", 2);
|
|
759
|
+
requiredPlain(input.code, "code");
|
|
760
|
+
requiredPlain(input.summary, "summary");
|
|
633
761
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
634
762
|
const run = resolveRun(context, project.id, input.runId);
|
|
763
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
635
764
|
const index = authoritativeIndex(run);
|
|
636
765
|
const stage = requiredPlain(input.stage, "stage");
|
|
637
766
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
638
767
|
if (!stageRun || stageRun.status !== "running")
|
|
639
768
|
throw new AppError("invalid_stage_state", "Only a running stage can be blocked", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
|
|
769
|
+
return { project, run, index, stage, stageRun };
|
|
770
|
+
}
|
|
771
|
+
export function blockFlowRunStage(context, input, prepared = prepareFlowRunStageBlock(context, input)) {
|
|
772
|
+
const { project, run, index, stage, stageRun } = prepared;
|
|
640
773
|
const now = context.now();
|
|
641
774
|
stageRun.status = "blocked";
|
|
642
775
|
stageRun.updated_at = now;
|
|
@@ -654,16 +787,20 @@ export function blockFlowRunStage(context, input) {
|
|
|
654
787
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
655
788
|
}
|
|
656
789
|
/** Continue the same stage after its non-user blocker was repaired externally. */
|
|
657
|
-
export function
|
|
790
|
+
export function prepareFlowRunStageUnblock(context, input) {
|
|
658
791
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
659
792
|
const run = resolveRun(context, project.id, input.runId);
|
|
793
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
660
794
|
const index = authoritativeIndex(run);
|
|
661
795
|
const stage = requiredPlain(input.stage, "stage");
|
|
662
796
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
663
797
|
if (!stageRun || stageRun.status !== "blocked" || !stageRun.blocker || stageRun.blocker.work_id !== input.workId)
|
|
664
798
|
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" });
|
|
799
|
+
return { project, run, index, stage, stageRun, blocker: stageRun.blocker };
|
|
800
|
+
}
|
|
801
|
+
export function unblockFlowRunStage(context, input, prepared = prepareFlowRunStageUnblock(context, input)) {
|
|
802
|
+
const { project, run, index, stage, stageRun, blocker } = prepared;
|
|
665
803
|
const now = context.now();
|
|
666
|
-
const blocker = stageRun.blocker;
|
|
667
804
|
const blockedMs = durationMs(blocker.blocked_at, now) ?? 0;
|
|
668
805
|
stageRun.status = "running";
|
|
669
806
|
stageRun.updated_at = now;
|
|
@@ -687,11 +824,36 @@ function operationalRunStatus(context, projectId, runId) {
|
|
|
687
824
|
const leaves = unfinished.filter((work) => !parents.has(work.work_id));
|
|
688
825
|
return leaves.length > 0 && leaves.every((work) => work.status === "paused") ? "paused" : "running";
|
|
689
826
|
}
|
|
690
|
-
|
|
691
|
-
|
|
827
|
+
/** Keep the logical RUN projection aligned with recovery authority. Stage/Work
|
|
828
|
+
* evidence and verdicts survive a reversible stop; terminal runs never reopen. */
|
|
829
|
+
export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
|
|
830
|
+
const run = resolveRun(context, projectId, runId);
|
|
831
|
+
if (!["running", "paused"].includes(run.status))
|
|
832
|
+
return;
|
|
833
|
+
const index = authoritativeIndex(run);
|
|
834
|
+
const status = paused || index.stage_runs.some(stage => ["paused", "waiting_for_user", "blocked"].includes(stage.status))
|
|
835
|
+
? "paused" : operationalRunStatus(context, projectId, runId);
|
|
836
|
+
if (run.status === status && index.status === status)
|
|
837
|
+
return;
|
|
838
|
+
index.status = status;
|
|
839
|
+
index.updated_at = context.now();
|
|
840
|
+
const project = requireProjectByRoot(context, run.project_root);
|
|
841
|
+
persistRunState(context, project, run, index);
|
|
842
|
+
}
|
|
843
|
+
export function prepareFlowRunCompletion(context, input) {
|
|
844
|
+
if (input.manualOverrideReason !== undefined) {
|
|
845
|
+
requiredPlain(input.manualOverrideReason, "override reason");
|
|
846
|
+
if (!["cancelled", "failed"].includes(input.status))
|
|
847
|
+
throw new AppError("validation", "Manual RUN override may only cancel or fail a RUN", 2);
|
|
848
|
+
}
|
|
849
|
+
if (input.verdict !== undefined)
|
|
850
|
+
requiredPlain(input.verdict, "verdict");
|
|
851
|
+
if (input.nextAction !== undefined)
|
|
852
|
+
requiredPlain(input.nextAction, "next action");
|
|
853
|
+
const project = requireProjectByRoot(context, input.manualOverrideReason ? canonicalPath(input.projectRoot, false) : resolveProjectRoot(input.projectRoot));
|
|
692
854
|
const run = resolveRun(context, project.id, input.runId);
|
|
855
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
693
856
|
const index = authoritativeIndex(run);
|
|
694
|
-
const now = context.now();
|
|
695
857
|
const status = parseRunStatus(input.status);
|
|
696
858
|
if (!['done', 'cancelled', 'failed'].includes(status)) {
|
|
697
859
|
throw new AppError('validation', 'completeFlowRun accepts only done, cancelled, or failed; use stage block/unblock for a recoverable blocker', 2, { status });
|
|
@@ -701,6 +863,11 @@ export function completeFlowRun(context, input) {
|
|
|
701
863
|
if (activeWork)
|
|
702
864
|
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
865
|
}
|
|
866
|
+
return { project, run, index, status };
|
|
867
|
+
}
|
|
868
|
+
export function completeFlowRun(context, input) {
|
|
869
|
+
const { project, run, index, status } = prepareFlowRunCompletion(context, input);
|
|
870
|
+
const now = context.now();
|
|
704
871
|
if (input.manualOverrideReason && (status === "cancelled" || status === "failed")) {
|
|
705
872
|
closeOpenStagesForOverride(index, status, now);
|
|
706
873
|
closeOpenWorksForOverride(context, project.id, run.id, status, now);
|
|
@@ -743,16 +910,85 @@ export function advanceFlowRun(context, input) {
|
|
|
743
910
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
744
911
|
const run = resolveRun(context, project.id, input.runId);
|
|
745
912
|
const index = authoritativeIndex(run);
|
|
913
|
+
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)))
|
|
914
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.settlementStage);
|
|
915
|
+
else
|
|
916
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
917
|
+
const control = runControlDirective(context, project.id, run.id);
|
|
746
918
|
const now = context.now();
|
|
747
919
|
index.status = input.status;
|
|
748
920
|
index.verdict = input.verdict;
|
|
749
|
-
index.next_action = input.nextAction ?? null;
|
|
921
|
+
index.next_action = control ? null : input.nextAction ?? null;
|
|
750
922
|
index.updated_at = now;
|
|
751
923
|
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:
|
|
924
|
+
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 } });
|
|
925
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_progressed", run_id: run.id, status: index.status, next_action: index.next_action });
|
|
754
926
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
755
|
-
return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
927
|
+
return { ok: true, ...(control ? { control } : {}), run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* The only legal backward edge in vNext: an integration gate exposed a
|
|
931
|
+
* product defect, so the source must be repaired and reviewed again. Normal
|
|
932
|
+
* stage start remains fail-closed for completed stages.
|
|
933
|
+
*/
|
|
934
|
+
export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
935
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
936
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
937
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
938
|
+
const index = authoritativeIndex(run);
|
|
939
|
+
const priorRepair = index.variables?.["merge.source_repair"];
|
|
940
|
+
if (priorRepair?.merge_request_id === input.mergeRequestId && priorRepair.cycle)
|
|
941
|
+
return { run_id: run.id, cycle: priorRepair.cycle };
|
|
942
|
+
const code = index.stage_runs.find((item) => item.stage === "code");
|
|
943
|
+
const review = index.stage_runs.find((item) => item.stage === "code-review");
|
|
944
|
+
const merge = index.stage_runs.find((item) => item.stage === "merge");
|
|
945
|
+
if (!code || code.status !== "done" || !review || review.status !== "done" || !merge || merge.status !== "running") {
|
|
946
|
+
throw new AppError("invalid_merge_repair_state", "Source repair requires accepted CODE/CODE-REVIEW and a running MERGE attempt", 2, {
|
|
947
|
+
run_id: run.id,
|
|
948
|
+
code: code?.status ?? null,
|
|
949
|
+
code_review: review?.status ?? null,
|
|
950
|
+
merge: merge?.status ?? null
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
const root = runArtifactRoot(run);
|
|
954
|
+
for (const stage of ["code", "code-review", "merge"])
|
|
955
|
+
archiveExistingStageAttempt(context, project.id, run.id, root, vnextStageDirectory(stage));
|
|
956
|
+
const now = context.now();
|
|
957
|
+
const restarted = {
|
|
958
|
+
...code,
|
|
959
|
+
status: "running",
|
|
960
|
+
started_at: now,
|
|
961
|
+
updated_at: now,
|
|
962
|
+
attempt: nextAttempt(code)
|
|
963
|
+
};
|
|
964
|
+
delete restarted.completed_at;
|
|
965
|
+
delete restarted.duration_ms;
|
|
966
|
+
delete restarted.stage_report;
|
|
967
|
+
delete restarted.data;
|
|
968
|
+
delete restarted.report;
|
|
969
|
+
delete restarted.artifact_aliases;
|
|
970
|
+
const cycle = `MRR-${input.mergeRequestId}`;
|
|
971
|
+
index.stage_runs = index.stage_runs.filter((item) => item.stage !== "code" && item.stage !== "code-review" && item.stage !== "merge");
|
|
972
|
+
index.stage_runs.push(restarted);
|
|
973
|
+
index.variables = { ...(index.variables ?? {}), "merge.source_repair": { cycle, merge_request_id: input.mergeRequestId, prepared_at: now } };
|
|
974
|
+
index.status = "running";
|
|
975
|
+
index.verdict = "merge_source_repair";
|
|
976
|
+
index.next_action = "start_code_repair";
|
|
977
|
+
index.current_stage = "code";
|
|
978
|
+
index.updated_at = now;
|
|
979
|
+
index.attempts = index.stage_runs.map((item) => ({
|
|
980
|
+
stage: item.stage,
|
|
981
|
+
attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1,
|
|
982
|
+
root: item.dir,
|
|
983
|
+
archive: null,
|
|
984
|
+
status: item.status,
|
|
985
|
+
started_at: item.started_at ?? now,
|
|
986
|
+
finished_at: item.completed_at ?? null
|
|
987
|
+
}));
|
|
988
|
+
persistRunState(context, project, run, index);
|
|
989
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
|
|
990
|
+
appendRunTimeline(root, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
991
|
+
return { run_id: run.id, cycle };
|
|
756
992
|
}
|
|
757
993
|
function closeOpenStagesForOverride(index, status, now) {
|
|
758
994
|
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
@@ -1018,6 +1254,10 @@ function vnextSpecifyGuidance(run, index) {
|
|
|
1018
1254
|
};
|
|
1019
1255
|
}
|
|
1020
1256
|
function persistRunState(context, project, run, index, options = {}) {
|
|
1257
|
+
// Fenced productive admission is a logical pause, even while physical work
|
|
1258
|
+
// drains. Late admitted Stage results must not revive a stopped RUN.
|
|
1259
|
+
if (index.status === "running" && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1260
|
+
index.status = "paused";
|
|
1021
1261
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
1022
1262
|
const previousRuntime = readRuntimeSnapshot(runtimePath);
|
|
1023
1263
|
const runtimeRevision = Math.max(index.runtime_revision ?? 0, previousRuntime?.runtime_revision ?? 0) + 1;
|
|
@@ -1028,22 +1268,30 @@ function persistRunState(context, project, run, index, options = {}) {
|
|
|
1028
1268
|
}
|
|
1029
1269
|
const persistedIndex = persistedVnextIndex(index);
|
|
1030
1270
|
ensureDir(path.dirname(runtimePath));
|
|
1031
|
-
|
|
1032
|
-
|
|
1271
|
+
context.db.exec("SAVEPOINT persist_run_state");
|
|
1272
|
+
try {
|
|
1273
|
+
context.db.run(`UPDATE runs
|
|
1033
1274
|
SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?,
|
|
1034
1275
|
updated_at = ?, completed_at = ?
|
|
1035
1276
|
WHERE project_id = ? AND id = ?`, [
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1277
|
+
index.status,
|
|
1278
|
+
index.verdict,
|
|
1279
|
+
index.next_action,
|
|
1280
|
+
JSON.stringify(persistedIndex),
|
|
1281
|
+
runtimePath,
|
|
1282
|
+
runtimePath,
|
|
1283
|
+
index.updated_at,
|
|
1284
|
+
index.completed_at ?? null,
|
|
1285
|
+
project.id,
|
|
1286
|
+
run.id
|
|
1287
|
+
]);
|
|
1288
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, runtimeRevision, history));
|
|
1289
|
+
context.db.exec("RELEASE persist_run_state");
|
|
1290
|
+
}
|
|
1291
|
+
catch (error) {
|
|
1292
|
+
context.db.exec("ROLLBACK TO persist_run_state; RELEASE persist_run_state");
|
|
1293
|
+
throw error;
|
|
1294
|
+
}
|
|
1047
1295
|
}
|
|
1048
1296
|
/** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
|
|
1049
1297
|
function persistedVnextIndex(index) {
|
|
@@ -1085,10 +1333,15 @@ function materializedRunVariables(index) {
|
|
|
1085
1333
|
...(index.variables ?? {})
|
|
1086
1334
|
};
|
|
1087
1335
|
}
|
|
1088
|
-
function
|
|
1336
|
+
function prepareRunVariable(context, input) {
|
|
1089
1337
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1090
1338
|
const run = resolveRun(context, project.id, input.runId);
|
|
1339
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
1091
1340
|
const index = authoritativeIndex(run);
|
|
1341
|
+
return { project, run, index };
|
|
1342
|
+
}
|
|
1343
|
+
function setRunVariable(context, input, value, prepared = prepareRunVariable(context, input)) {
|
|
1344
|
+
const { project, run, index } = prepared;
|
|
1092
1345
|
if (index.variables?.[input.key] === value)
|
|
1093
1346
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
|
|
1094
1347
|
index.variables = { ...(index.variables ?? {}), [input.key]: value };
|
|
@@ -1139,10 +1392,12 @@ function readRuntimeSnapshot(file) {
|
|
|
1139
1392
|
function authoritativeIndex(run) {
|
|
1140
1393
|
try {
|
|
1141
1394
|
const state = JSON.parse(run.index_json);
|
|
1395
|
+
if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
|
|
1396
|
+
throw new Error("RUN index must contain stage_runs");
|
|
1142
1397
|
return { ...state, schema_id: runSchemaId };
|
|
1143
1398
|
}
|
|
1144
1399
|
catch (error) {
|
|
1145
|
-
throw new AppError("
|
|
1400
|
+
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
1146
1401
|
}
|
|
1147
1402
|
}
|
|
1148
1403
|
function sanitizeTimelineValue(value, key) {
|
|
@@ -1203,7 +1458,7 @@ function upsertStage(index, stageRun) {
|
|
|
1203
1458
|
}
|
|
1204
1459
|
index.stage_runs.sort((a, b) => a.order - b.order);
|
|
1205
1460
|
}
|
|
1206
|
-
function resolveRun(context, projectId, idOrAlias) {
|
|
1461
|
+
export function resolveRun(context, projectId, idOrAlias) {
|
|
1207
1462
|
if (isFullEntityId(idOrAlias)) {
|
|
1208
1463
|
return requireRunById(context, projectId, idOrAlias);
|
|
1209
1464
|
}
|
|
@@ -1297,7 +1552,7 @@ function parseStageStatus(value) {
|
|
|
1297
1552
|
}
|
|
1298
1553
|
return value;
|
|
1299
1554
|
}
|
|
1300
|
-
function
|
|
1555
|
+
export function prepareStageDirectory(value) {
|
|
1301
1556
|
const dir = requiredPlain(value, "dir");
|
|
1302
1557
|
if (!/^\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(dir)) {
|
|
1303
1558
|
throw new AppError("validation", "--dir must use NN-stage-slug format", 2, { dir });
|
|
@@ -1305,6 +1560,8 @@ function requiredStageDir(value) {
|
|
|
1305
1560
|
return dir;
|
|
1306
1561
|
}
|
|
1307
1562
|
function requiredPlain(value, label) {
|
|
1563
|
+
if (typeof value !== "string")
|
|
1564
|
+
throw new AppError("validation", `${label} must be a string`, 2);
|
|
1308
1565
|
const trimmed = value.trim();
|
|
1309
1566
|
if (!trimmed) {
|
|
1310
1567
|
throw new AppError("validation", `${label} must not be empty`, 2);
|
|
@@ -1312,6 +1569,8 @@ function requiredPlain(value, label) {
|
|
|
1312
1569
|
return trimmed;
|
|
1313
1570
|
}
|
|
1314
1571
|
function normalizeSlug(value) {
|
|
1572
|
+
if (typeof value !== "string")
|
|
1573
|
+
throw new AppError("validation", "--slug must be a string", 2);
|
|
1315
1574
|
const slug = value
|
|
1316
1575
|
.normalize("NFKD")
|
|
1317
1576
|
.replace(/[\u0300-\u036f]/g, "")
|
|
@@ -1329,26 +1588,59 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1329
1588
|
if (!fs.existsSync(absolute)) {
|
|
1330
1589
|
throw new AppError("not_found", `Workspace root does not exist: ${absolute}`, 1);
|
|
1331
1590
|
}
|
|
1591
|
+
if (!fs.statSync(absolute).isDirectory())
|
|
1592
|
+
throw new AppError("validation", "Workspace root must be a directory", 2, { workspace_root: absolute });
|
|
1332
1593
|
return fs.realpathSync(absolute);
|
|
1333
1594
|
}
|
|
1334
|
-
|
|
1595
|
+
/** Move one completed stage attempt and update only known durable path columns.
|
|
1596
|
+
* Receipt bytes remain immutable; readers resolve their new location through the DB. */
|
|
1597
|
+
export function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
|
|
1335
1598
|
const currentStageDir = path.join(runRoot, stageDir);
|
|
1336
1599
|
if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
|
|
1337
1600
|
return;
|
|
1338
1601
|
}
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1602
|
+
const journal = path.join(runRoot, `.archive-${stageDir}.json`);
|
|
1603
|
+
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
1604
|
+
const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
|
|
1605
|
+
if (entries.length === 0 && !pending) {
|
|
1341
1606
|
return;
|
|
1342
1607
|
}
|
|
1343
1608
|
let attempt = 1;
|
|
1344
1609
|
while (fs.existsSync(path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`))) {
|
|
1345
1610
|
attempt += 1;
|
|
1346
1611
|
}
|
|
1347
|
-
const archiveDir = path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1612
|
+
const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1613
|
+
if (!pending) {
|
|
1614
|
+
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1615
|
+
const prior = stored ? JSON.parse(stored.index_json) : null;
|
|
1616
|
+
writeJsonFile(journal, { archiveDir, entries, stage_run: prior?.stage_runs.find((stage) => stage.dir === stageDir) ?? null });
|
|
1617
|
+
}
|
|
1348
1618
|
ensureDir(archiveDir);
|
|
1349
1619
|
for (const entry of entries) {
|
|
1350
|
-
|
|
1620
|
+
const source = path.join(currentStageDir, entry);
|
|
1621
|
+
const target = path.join(archiveDir, entry);
|
|
1622
|
+
if (fs.existsSync(source)) {
|
|
1623
|
+
if (fs.existsSync(target))
|
|
1624
|
+
throw new AppError("archive_conflict", "Both live and archived artifact exist; preserve both for recovery", 2, { source, target });
|
|
1625
|
+
fs.renameSync(source, target);
|
|
1626
|
+
}
|
|
1627
|
+
else if (!fs.existsSync(target))
|
|
1628
|
+
throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
|
|
1351
1629
|
}
|
|
1630
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
1631
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
1632
|
+
const rewrite = (table, column, where, values) => {
|
|
1633
|
+
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]);
|
|
1634
|
+
};
|
|
1635
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
1636
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
1637
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
1638
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
1639
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
1640
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
1641
|
+
// Retain the original stage facts with its immutable artifacts; current
|
|
1642
|
+
// stage_runs stays a projection of the current attempt, not a second ledger.
|
|
1643
|
+
fs.renameSync(journal, path.join(archiveDir, "archive.json"));
|
|
1352
1644
|
}
|
|
1353
1645
|
function writeJsonFile(file, value) {
|
|
1354
1646
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -1373,3 +1665,5 @@ export function gitFacts(workspaceRoot) {
|
|
|
1373
1665
|
status: status === null ? "unavailable" : status.length > 0 ? "dirty" : "clean"
|
|
1374
1666
|
};
|
|
1375
1667
|
}
|
|
1668
|
+
import { assertRunMutationAllowed, assertStageSettlementAllowed, recoveryGuard } from "./run-recovery.js";
|
|
1669
|
+
import { runControlDirective } from "./run-control.js";
|