@deksden-com/dd-flow-cli 0.6.0 → 0.8.0-beta.135
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 +636 -0
- package/README.md +13 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +108 -18
- package/dist/cli/run-cli.js +626 -49
- package/dist/domain/flow-contract.js +11 -0
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-contract.schema.json +6 -3
- package/dist/schemas/flow-run.schema.json +16 -122
- package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
- package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-prompt.schema.json +4 -4
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/canon.js +15 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +60 -8
- package/dist/services/code-checks.js +244 -0
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +14 -14
- package/dist/services/engines.js +408 -30
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +775 -23
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -5
- package/dist/services/merge-queue.js +53 -5
- package/dist/services/merge-worker.js +5 -6
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +5 -5
- package/dist/services/plans.js +5 -3
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +1 -1
- package/dist/services/protocols.js +31 -10
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +525 -58
- package/dist/services/schema-validation.js +116 -2
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +288 -77
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/status.js +8 -3
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +308 -0
- package/dist/services/vnext-code.js +616 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +576 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +499 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +292 -42
- package/dist/storage/paths.js +47 -1
- package/package.json +12 -12
package/dist/services/runs.js
CHANGED
|
@@ -3,17 +3,20 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
6
|
-
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
6
|
+
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
8
|
import { ensureDir, 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";
|
|
12
12
|
import { readProtocolRuntimeState, requireProtocol } from "./protocols.js";
|
|
13
|
-
import {
|
|
13
|
+
import { recalculateRunUsage } from "./usage.js";
|
|
14
14
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { resolveCanonRoot } from "./canon.js";
|
|
16
|
+
import { bindCurrentEngineToRun } from "./engines.js";
|
|
17
|
+
import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
|
|
18
|
+
const runSchemaId = "dd-flow/flow-run@3";
|
|
19
|
+
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
17
20
|
const runIdType = "RUN";
|
|
18
21
|
const allowedRunFlowKinds = [
|
|
19
22
|
"mb_sdlc",
|
|
@@ -25,12 +28,16 @@ const allowedRunFlowKinds = [
|
|
|
25
28
|
"mb-distill",
|
|
26
29
|
"mb-upgrade-review",
|
|
27
30
|
"mb-sdlc-review",
|
|
31
|
+
"vnext_specify",
|
|
32
|
+
"vnext_protocolize",
|
|
28
33
|
"review",
|
|
29
34
|
"custom"
|
|
30
35
|
];
|
|
31
36
|
export function startFlowRun(context, input) {
|
|
32
37
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
33
|
-
const flowContract =
|
|
38
|
+
const flowContract = input.flowKind === "mb-upgrade"
|
|
39
|
+
? loadUpgradeFlowContract(context, projectRoot)
|
|
40
|
+
: loadProjectFlowContract(projectRoot);
|
|
34
41
|
registerProject(context, { root: projectRoot });
|
|
35
42
|
const project = requireProjectByRoot(context, projectRoot);
|
|
36
43
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot ?? projectRoot);
|
|
@@ -39,6 +46,9 @@ export function startFlowRun(context, input) {
|
|
|
39
46
|
const runId = nextRunId(context, project.id, slug);
|
|
40
47
|
const { shortId } = parseFullEntityId(runId);
|
|
41
48
|
const now = context.now();
|
|
49
|
+
const executionProfile = (flowKind === "vnext_protocolize" || flowKind === "vnext_specify")
|
|
50
|
+
? snapshotVnextExecutionProfile(projectRoot)
|
|
51
|
+
: undefined;
|
|
42
52
|
const flowFlags = resolveFlowFlags(flowContract, {
|
|
43
53
|
flowKind,
|
|
44
54
|
...(input.preset ? { preset: input.preset } : {}),
|
|
@@ -86,7 +96,6 @@ export function startFlowRun(context, input) {
|
|
|
86
96
|
},
|
|
87
97
|
stage_runs: [],
|
|
88
98
|
sessions: [],
|
|
89
|
-
artifacts: { stage_prompt: path.join(runHome, "stage-prompt.md"), stage_report_json: path.join(runHome, "stage-report.json"), stage_report_md: path.join(runHome, "stage-report.md"), stage_report_html: path.join(runHome, "stage-report.html"), summary: path.join(runHome, "summary.md") },
|
|
90
99
|
status: "running",
|
|
91
100
|
verdict: "pending",
|
|
92
101
|
next_action: input.nextAction ?? null,
|
|
@@ -99,20 +108,30 @@ export function startFlowRun(context, input) {
|
|
|
99
108
|
snapshot_checksum: flowFlags.snapshot_checksum,
|
|
100
109
|
reconciliation_status: "reconciled",
|
|
101
110
|
flow_flags: flowFlags,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
settings: {
|
|
112
|
+
plan_review: { mode: executionProfile?.settings.plan_review_mode ?? "auto", source: "default", reason: "Project execution profile default.", updated_at: now },
|
|
113
|
+
code_review: { mode: executionProfile?.settings.code_review_mode ?? "auto", source: "default", reason: "Project execution profile default.", updated_at: now }
|
|
114
|
+
},
|
|
115
|
+
...(executionProfile ? { execution_profile: executionProfile } : {}),
|
|
116
|
+
artifacts: {
|
|
117
|
+
stage_prompt: "stage-prompt.md",
|
|
118
|
+
stage_report_json: "stage-report.json",
|
|
119
|
+
stage_report_md: "stage-report.md",
|
|
120
|
+
stage_report_html: "stage-report.html",
|
|
121
|
+
summary: "summary.md"
|
|
122
|
+
},
|
|
106
123
|
timeline_path: "timeline.jsonl"
|
|
107
124
|
};
|
|
108
125
|
ensureDir(path.dirname(runtimePath));
|
|
109
126
|
ensureDir(runHome);
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
bindCurrentEngineToRun(context, { projectRoot, runId, runHome });
|
|
128
|
+
const persistedIndex = persistedVnextIndex(index);
|
|
129
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
|
|
130
|
+
context.db.run(`INSERT INTO runs
|
|
112
131
|
(id, short_id, slug, project_id, project_root, workspace_root, flow_kind, subject_type, subject_id,
|
|
113
|
-
status, verdict, next_action, runtime_path, run_dir, run_index_path, run_home_path, layout_version, artifact_root_kind,
|
|
132
|
+
status, verdict, next_action, runtime_path, run_dir, run_index_path, run_home_path, run_root, layout_version, artifact_root_kind,
|
|
114
133
|
index_json, created_at, updated_at, completed_at)
|
|
115
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`, [
|
|
134
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`, [
|
|
116
135
|
runId,
|
|
117
136
|
shortId,
|
|
118
137
|
slug,
|
|
@@ -129,9 +148,10 @@ export function startFlowRun(context, input) {
|
|
|
129
148
|
runHome,
|
|
130
149
|
runtimePath,
|
|
131
150
|
runHome,
|
|
151
|
+
runHome,
|
|
132
152
|
"home_run_v2",
|
|
133
153
|
"dd_flow_home",
|
|
134
|
-
JSON.stringify(
|
|
154
|
+
JSON.stringify(persistedIndex),
|
|
135
155
|
now,
|
|
136
156
|
now
|
|
137
157
|
]);
|
|
@@ -151,14 +171,121 @@ export function startFlowRun(context, input) {
|
|
|
151
171
|
});
|
|
152
172
|
return { ok: true, run: flowRunSummary(requireRunById(context, project.id, runId)), index };
|
|
153
173
|
}
|
|
174
|
+
function snapshotVnextExecutionProfile(projectRoot) {
|
|
175
|
+
const profile = loadVnextExecutionProfile(projectRoot);
|
|
176
|
+
const sourcePath = executionProfilePath(projectRoot);
|
|
177
|
+
return {
|
|
178
|
+
schema_id: "dd-flow/run-execution-profile@1",
|
|
179
|
+
source_path: sourcePath,
|
|
180
|
+
source_checksum: crypto.createHash("sha256").update(fs.readFileSync(sourcePath)).digest("hex"),
|
|
181
|
+
settings: {
|
|
182
|
+
stage_session_mode: profile.stage_session_mode,
|
|
183
|
+
plan_review_mode: profile.plan_review_mode,
|
|
184
|
+
code_review_mode: profile.code_review_mode,
|
|
185
|
+
stop_target: profile.stop_target,
|
|
186
|
+
code_bootstrap: profile.code_bootstrap
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A vNext route may materialize a feature checkout after its semantic
|
|
192
|
+
* PROTOCOLIZE decision. Keep project identity stable while moving the RUN's
|
|
193
|
+
* concrete write workspace before PLAN is allowed to start.
|
|
194
|
+
*/
|
|
195
|
+
export function rebindFlowRunWorkspace(context, input) {
|
|
196
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
197
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
198
|
+
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
|
|
199
|
+
const index = authoritativeIndex(run);
|
|
200
|
+
const now = context.now();
|
|
201
|
+
const facts = gitFacts(workspaceRoot);
|
|
202
|
+
index.workspace = { ...index.workspace, workspace_path: workspaceRoot, branch: facts.branch ?? "unknown", base: facts.head ?? "unknown", manager: "official-worktrunk" };
|
|
203
|
+
index.execution = { ...index.execution, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
204
|
+
index.updated_at = now;
|
|
205
|
+
context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
|
|
206
|
+
persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
|
|
207
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason } });
|
|
208
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
|
|
209
|
+
return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
210
|
+
}
|
|
211
|
+
function loadUpgradeFlowContract(context, projectRoot) {
|
|
212
|
+
const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
|
|
213
|
+
if (!canon.ok || !canon.canon) {
|
|
214
|
+
throw new AppError("canon_unavailable", "mb-upgrade RUN requires the pinned canonical Memory Bank", 1, {
|
|
215
|
+
project_root: projectRoot,
|
|
216
|
+
blockers: canon.blockers,
|
|
217
|
+
bootstrap: canon.bootstrap
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return loadCanonicalFlowContract(canon.canon.flow_root);
|
|
221
|
+
}
|
|
154
222
|
export function getFlowRunStatus(context, input) {
|
|
155
223
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
156
224
|
const run = resolveRun(context, project.id, input.runId);
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
225
|
+
const index = authoritativeIndex(run);
|
|
226
|
+
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
227
|
+
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
228
|
+
}
|
|
229
|
+
export function getFlowRunConfig(context, input) {
|
|
230
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
231
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
232
|
+
const index = authoritativeIndex(run);
|
|
233
|
+
return { ok: true, run_id: run.id, settings: runSettings(index) };
|
|
234
|
+
}
|
|
235
|
+
/** RUN variables are one small shared context bag; policy values are projected, not duplicated. */
|
|
236
|
+
export function getFlowRunVariables(context, input) {
|
|
237
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
238
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
239
|
+
const variables = materializedRunVariables(authoritativeIndex(run));
|
|
240
|
+
if (input.key) {
|
|
241
|
+
if (!(input.key in variables))
|
|
242
|
+
throw new AppError("not_found", "RUN variable is not set", 1, { key: input.key, run_id: run.id });
|
|
243
|
+
return { ok: true, run_id: run.id, key: input.key, value: variables[input.key] };
|
|
244
|
+
}
|
|
245
|
+
return { ok: true, run_id: run.id, variables };
|
|
246
|
+
}
|
|
247
|
+
export function setFlowRunVariable(context, input) {
|
|
248
|
+
if (!input.key.startsWith("user."))
|
|
249
|
+
throw new AppError("validation", "Only user.* RUN variables may be set directly", 2, { key: input.key });
|
|
250
|
+
return setRunVariable(context, input, input.value);
|
|
251
|
+
}
|
|
252
|
+
/** Controllers own policy.* and runtime.* values; agents never call this directly. */
|
|
253
|
+
export function setRuntimeRunVariable(context, input) {
|
|
254
|
+
if (!input.key.startsWith("runtime."))
|
|
255
|
+
throw new AppError("validation", "Controller runtime variables must use runtime.*", 2, { key: input.key });
|
|
256
|
+
return setRunVariable(context, input, input.value);
|
|
257
|
+
}
|
|
258
|
+
export function setFlowRunConfig(context, input) {
|
|
259
|
+
if (input.key !== "plan_review.mode" && input.key !== "code_review.mode") {
|
|
260
|
+
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
261
|
+
}
|
|
262
|
+
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
263
|
+
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
264
|
+
}
|
|
265
|
+
const reason = requiredPlain(input.reason, "reason");
|
|
266
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
267
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
268
|
+
const index = authoritativeIndex(run);
|
|
269
|
+
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
270
|
+
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
271
|
+
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
272
|
+
}
|
|
273
|
+
const now = context.now();
|
|
274
|
+
const settings = runSettings(index);
|
|
275
|
+
const previous = input.key === "plan_review.mode" ? settings.plan_review : settings.code_review;
|
|
276
|
+
const mode = input.value;
|
|
277
|
+
const source = input.source ?? "user_instruction";
|
|
278
|
+
if (previous.mode === mode && previous.reason === reason && previous.source === source) {
|
|
279
|
+
return { ok: true, run_id: run.id, settings, idempotent: true };
|
|
280
|
+
}
|
|
281
|
+
index.settings = input.key === "plan_review.mode"
|
|
282
|
+
? { ...settings, plan_review: { mode, source, reason, updated_at: now } }
|
|
283
|
+
: { ...settings, code_review: { mode, source, reason, updated_at: now } };
|
|
284
|
+
index.updated_at = now;
|
|
285
|
+
persistRunState(context, project, run, index);
|
|
286
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.config_set", payload: { run_id: run.id, key: input.key, value: mode, source, reason } });
|
|
287
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
288
|
+
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
162
289
|
}
|
|
163
290
|
export function getFlowRunFlagsStatus(context, input) {
|
|
164
291
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -305,7 +432,7 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
305
432
|
export function listFlowRuns(context, input) {
|
|
306
433
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
307
434
|
const runs = context.db
|
|
308
|
-
.all(`SELECT * FROM
|
|
435
|
+
.all(`SELECT * FROM runs
|
|
309
436
|
WHERE project_id = ?
|
|
310
437
|
ORDER BY updated_at DESC, id DESC`, [project.id])
|
|
311
438
|
.map(flowRunSummary);
|
|
@@ -349,16 +476,11 @@ export function attachFlowRunStage(context, input) {
|
|
|
349
476
|
payload: { run_id: run.id, stage, dir, status }
|
|
350
477
|
});
|
|
351
478
|
appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_attached", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
project.id,
|
|
357
|
-
run.id
|
|
358
|
-
]);
|
|
479
|
+
// A stage transition must not rewrite every Session in the RUN. A RUN may
|
|
480
|
+
// contain independent reviewer/worker sessions; their stage is set by the
|
|
481
|
+
// stage/work binding that actually owns that Session.
|
|
482
|
+
if (status === "running")
|
|
359
483
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
360
|
-
checkpointRunUsage(context, { projectId: project.id, runId: run.id, checkpoint: "stage_started", stage, stageAttempt: stageRun.attempt ?? null });
|
|
361
|
-
}
|
|
362
484
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
363
485
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
364
486
|
}
|
|
@@ -382,11 +504,23 @@ export function completeFlowRunStage(context, input) {
|
|
|
382
504
|
...(input.report ? { report: input.report } : {}),
|
|
383
505
|
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
384
506
|
updated_at: now,
|
|
385
|
-
...(status === "done" || status === "blocked" || status === "failed" || status === "skipped"
|
|
507
|
+
...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
|
|
386
508
|
? { completed_at: now, duration_ms: durationMs(existing.started_at ?? null, now), git: gitFacts(run.workspace_root) }
|
|
387
509
|
: {})
|
|
388
510
|
};
|
|
389
511
|
upsertStage(index, stageRun);
|
|
512
|
+
if (status === "waiting_for_user") {
|
|
513
|
+
index.status = "waiting_for_user";
|
|
514
|
+
index.verdict = "waiting_for_user";
|
|
515
|
+
}
|
|
516
|
+
else if (status === "blocked" || status === "failed") {
|
|
517
|
+
index.status = status;
|
|
518
|
+
index.verdict = status;
|
|
519
|
+
}
|
|
520
|
+
else if (status === "done" && index.status === "waiting_for_user") {
|
|
521
|
+
index.status = "running";
|
|
522
|
+
index.verdict = "running";
|
|
523
|
+
}
|
|
390
524
|
index.current_stage = stage;
|
|
391
525
|
index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
|
|
392
526
|
index.updated_at = now;
|
|
@@ -403,16 +537,147 @@ export function completeFlowRunStage(context, input) {
|
|
|
403
537
|
payload: { run_id: run.id, stage, status, stage_report: input.stageReport ?? null, data: input.data ?? null }
|
|
404
538
|
});
|
|
405
539
|
appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_completed", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
406
|
-
checkpointRunUsage(context, { projectId: project.id, runId: run.id, checkpoint: "stage_finished", stage, stageAttempt: stageRun.attempt ?? null });
|
|
407
540
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
408
541
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
409
542
|
}
|
|
543
|
+
/** Pause one live stage without completing it or creating a new attempt. */
|
|
544
|
+
export function pauseFlowRunStage(context, input) {
|
|
545
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
546
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
547
|
+
const index = authoritativeIndex(run);
|
|
548
|
+
const stage = requiredPlain(input.stage, "stage");
|
|
549
|
+
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
550
|
+
if (!stageRun || stageRun.status !== "running") {
|
|
551
|
+
throw new AppError("invalid_stage_state", "Only a running stage can pause", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
|
|
552
|
+
}
|
|
553
|
+
const now = context.now();
|
|
554
|
+
stageRun.status = "paused";
|
|
555
|
+
stageRun.updated_at = now;
|
|
556
|
+
stageRun.pause = {
|
|
557
|
+
id: input.pauseId,
|
|
558
|
+
reason: "waiting_for_user",
|
|
559
|
+
work_id: input.workId,
|
|
560
|
+
question_path: input.questionPath,
|
|
561
|
+
answer_path: null,
|
|
562
|
+
paused_at: now,
|
|
563
|
+
resumed_at: null
|
|
564
|
+
};
|
|
565
|
+
index.pause = stageRun.pause;
|
|
566
|
+
index.status = operationalRunStatus(context, project.id, run.id);
|
|
567
|
+
index.verdict = index.status === "paused" ? "paused" : "running";
|
|
568
|
+
index.next_action = "await_user_answer";
|
|
569
|
+
index.current_stage = stage;
|
|
570
|
+
index.updated_at = now;
|
|
571
|
+
index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
|
|
572
|
+
persistRunState(context, project, run, index);
|
|
573
|
+
refreshRunSessionProjection(context, project.id, run.id);
|
|
574
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_paused", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, reason: "waiting_for_user" } });
|
|
575
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_waiting_for_user", run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, question_path: input.questionPath });
|
|
576
|
+
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
577
|
+
}
|
|
578
|
+
/** Resume the same stage, Work and attempt after a user answer is recorded. */
|
|
579
|
+
export function resumeFlowRunStage(context, input) {
|
|
580
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
581
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
582
|
+
const index = authoritativeIndex(run);
|
|
583
|
+
const stage = requiredPlain(input.stage, "stage");
|
|
584
|
+
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
585
|
+
if (!stageRun || stageRun.status !== "paused" || !stageRun.pause || stageRun.pause.work_id !== input.workId) {
|
|
586
|
+
throw new AppError("invalid_stage_state", "Stage resume requires its matching paused Work", 1, { run_id: run.id, stage, work_id: input.workId, status: stageRun?.status ?? "missing" });
|
|
587
|
+
}
|
|
588
|
+
const now = context.now();
|
|
589
|
+
const pause = stageRun.pause;
|
|
590
|
+
const pausedMs = durationMs(pause.paused_at, now) ?? 0;
|
|
591
|
+
stageRun.status = "running";
|
|
592
|
+
stageRun.updated_at = now;
|
|
593
|
+
stageRun.paused_ms = (stageRun.paused_ms ?? 0) + pausedMs;
|
|
594
|
+
delete stageRun.pause;
|
|
595
|
+
index.pause = undefined;
|
|
596
|
+
index.status = "running";
|
|
597
|
+
index.verdict = "running";
|
|
598
|
+
index.next_action = `continue_${stage}`;
|
|
599
|
+
index.current_stage = stage;
|
|
600
|
+
index.updated_at = now;
|
|
601
|
+
index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
|
|
602
|
+
persistRunState(context, project, run, index);
|
|
603
|
+
refreshRunSessionProjection(context, project.id, run.id);
|
|
604
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_resumed", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath } });
|
|
605
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "user_answer_received", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath });
|
|
606
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_resumed", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, paused_ms: pausedMs });
|
|
607
|
+
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
608
|
+
}
|
|
609
|
+
/** Pause the current stage for a non-user runtime blocker. */
|
|
610
|
+
export function blockFlowRunStage(context, input) {
|
|
611
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
612
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
613
|
+
const index = authoritativeIndex(run);
|
|
614
|
+
const stage = requiredPlain(input.stage, "stage");
|
|
615
|
+
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
616
|
+
if (!stageRun || stageRun.status !== "running")
|
|
617
|
+
throw new AppError("invalid_stage_state", "Only a running stage can be blocked", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
|
|
618
|
+
const now = context.now();
|
|
619
|
+
stageRun.status = "blocked";
|
|
620
|
+
stageRun.updated_at = now;
|
|
621
|
+
stageRun.blocker = { kind: input.kind, code: input.code, summary: input.summary, retryable: input.retryable, work_id: input.workId, blocked_at: now, resumed_at: null };
|
|
622
|
+
index.blocker = stageRun.blocker;
|
|
623
|
+
index.status = "paused";
|
|
624
|
+
index.verdict = "paused";
|
|
625
|
+
index.next_action = "resolve_blocker_then_unblock_same_stage";
|
|
626
|
+
index.current_stage = stage;
|
|
627
|
+
index.updated_at = now;
|
|
628
|
+
persistRunState(context, project, run, index);
|
|
629
|
+
refreshRunSessionProjection(context, project.id, run.id);
|
|
630
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_blocked", payload: { run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, retryable: input.retryable } });
|
|
631
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_blocked", run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, summary: input.summary, retryable: input.retryable });
|
|
632
|
+
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
633
|
+
}
|
|
634
|
+
/** Continue the same stage after its non-user blocker was repaired externally. */
|
|
635
|
+
export function unblockFlowRunStage(context, input) {
|
|
636
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
637
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
638
|
+
const index = authoritativeIndex(run);
|
|
639
|
+
const stage = requiredPlain(input.stage, "stage");
|
|
640
|
+
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
641
|
+
if (!stageRun || stageRun.status !== "blocked" || !stageRun.blocker || stageRun.blocker.work_id !== input.workId)
|
|
642
|
+
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" });
|
|
643
|
+
const now = context.now();
|
|
644
|
+
const blocker = stageRun.blocker;
|
|
645
|
+
const blockedMs = durationMs(blocker.blocked_at, now) ?? 0;
|
|
646
|
+
stageRun.status = "running";
|
|
647
|
+
stageRun.updated_at = now;
|
|
648
|
+
stageRun.paused_ms = (stageRun.paused_ms ?? 0) + blockedMs;
|
|
649
|
+
delete stageRun.blocker;
|
|
650
|
+
index.blocker = undefined;
|
|
651
|
+
index.status = "running";
|
|
652
|
+
index.verdict = "running";
|
|
653
|
+
index.next_action = `continue_${stage}`;
|
|
654
|
+
index.current_stage = stage;
|
|
655
|
+
index.updated_at = now;
|
|
656
|
+
persistRunState(context, project, run, index);
|
|
657
|
+
refreshRunSessionProjection(context, project.id, run.id);
|
|
658
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_unblocked", payload: { run_id: run.id, stage, work_id: input.workId, code: blocker.code } });
|
|
659
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "stage_unblocked", run_id: run.id, stage, work_id: input.workId, code: blocker.code, blocked_ms: blockedMs });
|
|
660
|
+
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
661
|
+
}
|
|
662
|
+
function operationalRunStatus(context, projectId, runId) {
|
|
663
|
+
const unfinished = context.db.all("SELECT work_id, status, parent_work_id FROM works WHERE project_id = ? AND run_id = ? AND status IN ('created', 'running', 'paused')", [projectId, runId]);
|
|
664
|
+
const parents = new Set(unfinished.map((work) => work.parent_work_id).filter((id) => Boolean(id)));
|
|
665
|
+
const leaves = unfinished.filter((work) => !parents.has(work.work_id));
|
|
666
|
+
return leaves.length > 0 && leaves.every((work) => work.status === "paused") ? "paused" : "running";
|
|
667
|
+
}
|
|
410
668
|
export function completeFlowRun(context, input) {
|
|
411
669
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
412
670
|
const run = resolveRun(context, project.id, input.runId);
|
|
413
671
|
const index = authoritativeIndex(run);
|
|
414
672
|
const now = context.now();
|
|
415
673
|
const status = parseRunStatus(input.status);
|
|
674
|
+
if (!['done', 'blocked', 'cancelled', 'failed'].includes(status)) {
|
|
675
|
+
throw new AppError('validation', 'completeFlowRun accepts terminal statuses only; use advanceFlowRun for a live RUN', 2, { status });
|
|
676
|
+
}
|
|
677
|
+
if (input.manualOverrideReason && (status === "cancelled" || status === "failed")) {
|
|
678
|
+
closeOpenStagesForOverride(index, status, now);
|
|
679
|
+
closeOpenWorksForOverride(context, project.id, run.id, status, now);
|
|
680
|
+
}
|
|
416
681
|
index.status = status;
|
|
417
682
|
index.verdict = input.verdict ?? (status === "done" ? "accepted" : status);
|
|
418
683
|
index.next_action = input.nextAction ?? null;
|
|
@@ -429,27 +694,80 @@ export function completeFlowRun(context, input) {
|
|
|
429
694
|
}
|
|
430
695
|
persistRunState(context, project, run, index);
|
|
431
696
|
closeRunOrchestrators(context, project.id, run, status, now);
|
|
697
|
+
const usageProjection = recalculateRunUsage(context, { projectId: project.id, runId: run.id });
|
|
698
|
+
const usage = {
|
|
699
|
+
status: usageProjection.status ?? "unavailable",
|
|
700
|
+
session_count: Array.isArray(usageProjection.sessions) ? usageProjection.sessions.length : 0,
|
|
701
|
+
coverage: usageProjection.coverage ?? {},
|
|
702
|
+
session_reconciliation: usageProjection.session_reconciliation ?? {}
|
|
703
|
+
};
|
|
432
704
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
433
705
|
appendAudit(context, {
|
|
434
706
|
projectId: project.id,
|
|
435
|
-
eventType: "flow_run.completed",
|
|
436
|
-
payload: { run_id: run.id, status, verdict: index.verdict, next_action: index.next_action }
|
|
707
|
+
eventType: input.manualOverrideReason ? "flow_run.manual_override" : "flow_run.completed",
|
|
708
|
+
payload: { run_id: run.id, status, verdict: index.verdict, next_action: index.next_action, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) }
|
|
437
709
|
});
|
|
438
|
-
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_completed", run_id: run.id, status });
|
|
439
|
-
|
|
710
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: input.manualOverrideReason ? "run_manual_override" : "run_completed", run_id: run.id, status, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) });
|
|
711
|
+
const updatedRun = requireRunById(context, project.id, run.id);
|
|
712
|
+
return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
713
|
+
}
|
|
714
|
+
/** Update a live RUN between stages without manufacturing terminal evidence. */
|
|
715
|
+
export function advanceFlowRun(context, input) {
|
|
716
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
717
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
718
|
+
const index = authoritativeIndex(run);
|
|
719
|
+
const now = context.now();
|
|
720
|
+
index.status = input.status;
|
|
721
|
+
index.verdict = input.verdict;
|
|
722
|
+
index.next_action = input.nextAction ?? null;
|
|
723
|
+
index.updated_at = now;
|
|
724
|
+
persistRunState(context, project, run, index);
|
|
725
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status: input.status, verdict: input.verdict, next_action: index.next_action } });
|
|
726
|
+
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_progressed", run_id: run.id, status: input.status, next_action: index.next_action });
|
|
440
727
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
441
728
|
return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
442
729
|
}
|
|
730
|
+
function closeOpenStagesForOverride(index, status, now) {
|
|
731
|
+
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
732
|
+
for (const stage of index.stage_runs) {
|
|
733
|
+
if (stage.status !== "running" && stage.status !== "pending")
|
|
734
|
+
continue;
|
|
735
|
+
stage.status = stageStatus;
|
|
736
|
+
stage.updated_at = now;
|
|
737
|
+
stage.completed_at = now;
|
|
738
|
+
stage.duration_ms = durationMs(stage.started_at ?? null, now);
|
|
739
|
+
}
|
|
740
|
+
index.attempts = index.stage_runs.map((stage) => ({
|
|
741
|
+
stage: stage.stage,
|
|
742
|
+
attempt_number: Number((stage.attempt ?? "try-001").replace("try-", "")) || 1,
|
|
743
|
+
root: stage.dir,
|
|
744
|
+
archive: null,
|
|
745
|
+
status: stage.status,
|
|
746
|
+
started_at: stage.started_at ?? now,
|
|
747
|
+
finished_at: stage.completed_at ?? null
|
|
748
|
+
}));
|
|
749
|
+
}
|
|
750
|
+
function closeOpenWorksForOverride(context, projectId, runId, status, now) {
|
|
751
|
+
const workStatus = status === "cancelled" ? "cancelled" : "failed";
|
|
752
|
+
context.db.run(`UPDATE works
|
|
753
|
+
SET status = ?, updated_at = ?, completed_at = ?
|
|
754
|
+
WHERE project_id = ? AND run_id = ?
|
|
755
|
+
AND status IN ('created', 'running')`, [workStatus, now, now, projectId, runId]);
|
|
756
|
+
context.db.run(`UPDATE work_sessions
|
|
757
|
+
SET status = ?, updated_at = ?, completed_at = ?
|
|
758
|
+
WHERE work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)
|
|
759
|
+
AND status <> 'completed'`, [workStatus, now, now, projectId, runId]);
|
|
760
|
+
}
|
|
443
761
|
function closeRunOrchestrators(context, projectId, run, status, now) {
|
|
444
762
|
if (!["done", "blocked", "cancelled", "failed"].includes(status))
|
|
445
763
|
return;
|
|
446
|
-
const active = context.db.all(`SELECT session_id FROM
|
|
764
|
+
const active = context.db.all(`SELECT session_id FROM sessions
|
|
447
765
|
WHERE project_id = ? AND run_id = ? AND session_kind = 'orchestrator'
|
|
448
766
|
AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [projectId, run.id]);
|
|
449
767
|
if (active.length === 0)
|
|
450
768
|
return;
|
|
451
769
|
const reason = `run_${status}`;
|
|
452
|
-
context.db.run(`UPDATE
|
|
770
|
+
context.db.run(`UPDATE sessions SET status = 'stopped', stop_reason = ?, updated_at = ?, stopped_at = ?
|
|
453
771
|
WHERE project_id = ? AND run_id = ? AND session_kind = 'orchestrator'
|
|
454
772
|
AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')`, [reason, now, now, projectId, run.id]);
|
|
455
773
|
context.db.run(`UPDATE flow_session_segments SET ended_at = ?
|
|
@@ -514,7 +832,7 @@ export function getFlowRunTimeline(context, input) {
|
|
|
514
832
|
if (!hidden.has("sessions"))
|
|
515
833
|
report.sessions = sessions;
|
|
516
834
|
if (!hidden.has("usage"))
|
|
517
|
-
report.usage =
|
|
835
|
+
report.usage = { status: "available_via_stat_usage", command: `dd-flow stat usage --run ${run.id} --project-root ${JSON.stringify(run.project_root)} --json` };
|
|
518
836
|
if (!hidden.has("artifacts"))
|
|
519
837
|
report.artifacts = index.stage_runs.map((stage) => ({
|
|
520
838
|
stage: stage.stage,
|
|
@@ -526,16 +844,51 @@ export function getFlowRunTimeline(context, input) {
|
|
|
526
844
|
}));
|
|
527
845
|
return report;
|
|
528
846
|
}
|
|
529
|
-
export function
|
|
847
|
+
export function getStoredFlowRunUsage(context, input) {
|
|
848
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
849
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
850
|
+
const index = authoritativeIndex(run);
|
|
851
|
+
const stageNames = input.stage?.split(",").map((value) => value.trim()).filter(Boolean) ?? [];
|
|
852
|
+
const stageRuns = stageNames.map((name) => index.stage_runs.find((item) => item.stage === name));
|
|
853
|
+
if (stageNames.length && stageRuns.some((stage) => !stage?.started_at))
|
|
854
|
+
throw new AppError("not_found", "RUN stage timing is unavailable", 1, { run_id: run.id, stages: stageNames });
|
|
855
|
+
const starts = stageRuns.map((stage) => stage.started_at).sort();
|
|
856
|
+
const finishes = stageRuns.map((stage) => stage.completed_at).filter((value) => Boolean(value)).sort();
|
|
857
|
+
const usage = recalculateRunUsage(context, {
|
|
858
|
+
projectId: project.id,
|
|
859
|
+
runId: run.id,
|
|
860
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
861
|
+
...(starts.length ? { stage: { name: stageNames.join(","), startedAt: starts[0], completedAt: finishes.length === stageRuns.length ? finishes.at(-1) : null } } : {})
|
|
862
|
+
});
|
|
863
|
+
refreshRunSessionProjection(context, project.id, run.id);
|
|
864
|
+
return usage;
|
|
865
|
+
}
|
|
866
|
+
export function getFlowRunSessions(context, input) {
|
|
530
867
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
531
868
|
const run = resolveRun(context, project.id, input.runId);
|
|
532
|
-
|
|
869
|
+
const sessions = context.db.all(`SELECT s.session_id, s.harness, s.provider_session_id, s.parent_session_id,
|
|
870
|
+
s.agent_id, s.worker_id, s.provider, s.model, s.reasoning, s.mode,
|
|
871
|
+
s.agent_type, s.transcript_path,
|
|
872
|
+
COUNT(ws.id) AS work_link_count
|
|
873
|
+
FROM sessions s
|
|
874
|
+
JOIN work_sessions ws ON ws.session_id = s.session_id
|
|
875
|
+
JOIN works w ON w.work_id = ws.work_id
|
|
876
|
+
WHERE w.project_id = ? AND w.run_id = ?
|
|
877
|
+
GROUP BY s.session_id, s.harness, s.provider_session_id, s.parent_session_id,
|
|
878
|
+
s.agent_id, s.worker_id, s.provider, s.model, s.reasoning, s.mode,
|
|
879
|
+
s.agent_type, s.transcript_path
|
|
880
|
+
ORDER BY s.created_at, s.session_id`, [project.id, run.id]);
|
|
881
|
+
return { ok: true, schema_id: "dd-flow/run-sessions@3", run_id: run.id, sessions };
|
|
533
882
|
}
|
|
534
883
|
export function appendFlowRunTimelineEvent(context, projectId, runId, event) {
|
|
535
884
|
const run = requireRunById(context, projectId, runId);
|
|
536
885
|
appendRunTimeline(runArtifactRoot(run), { run_id: run.id, ...event });
|
|
537
886
|
}
|
|
538
887
|
function guidanceForRun(context, run, index) {
|
|
888
|
+
if (run.flow_kind === "vnext_specify")
|
|
889
|
+
return vnextSpecifyGuidance(run, index);
|
|
890
|
+
if (run.flow_kind === "vnext_protocolize")
|
|
891
|
+
return vnextProtocolizeGuidance(run, index);
|
|
539
892
|
if (run.status === "discarded") {
|
|
540
893
|
return buildRunFlowGuidance({
|
|
541
894
|
stageRuns: index.stage_runs,
|
|
@@ -550,7 +903,9 @@ function guidanceForRun(context, run, index) {
|
|
|
550
903
|
const state = readProtocolRuntimeState(context, protocol).state;
|
|
551
904
|
return buildRunFlowGuidance({
|
|
552
905
|
stageRuns: index.stage_runs,
|
|
553
|
-
protocolStage: state.
|
|
906
|
+
protocolStage: state.status === "waiting_for_user" || state.status === "blocked" || state.status === "failed"
|
|
907
|
+
? state.status
|
|
908
|
+
: state.stage,
|
|
554
909
|
...(state.flow_contract ? { contract: state.flow_contract } : {}),
|
|
555
910
|
runId: run.id,
|
|
556
911
|
runDir: index.run_home?.relative_path ?? path.posix.join("runs", run.id)
|
|
@@ -563,6 +918,62 @@ function guidanceForRun(context, run, index) {
|
|
|
563
918
|
}
|
|
564
919
|
return buildRunFlowGuidance({ stageRuns: index.stage_runs, runId: run.id, runDir: index.run_home?.relative_path ?? path.posix.join("runs", run.id) });
|
|
565
920
|
}
|
|
921
|
+
function vnextProtocolizeGuidance(run, index) {
|
|
922
|
+
const specifyDone = index.stage_runs.some((stage) => stage.stage === "specify" && stage.status === "done");
|
|
923
|
+
const protocolizeDone = index.stage_runs.some((stage) => stage.stage === "protocolize" && stage.status === "done");
|
|
924
|
+
const planDone = index.stage_runs.some((stage) => stage.stage === "plan" && stage.status === "done");
|
|
925
|
+
const planReviewDone = index.stage_runs.some((stage) => stage.stage === "plan-review" && stage.status === "done");
|
|
926
|
+
const codeDone = index.stage_runs.some((stage) => stage.stage === "code" && stage.status === "done");
|
|
927
|
+
const codeReviewDone = index.stage_runs.some((stage) => stage.stage === "code-review" && stage.status === "done");
|
|
928
|
+
const nextStage = !specifyDone ? "specify" : !protocolizeDone ? "protocolize" : !planDone ? "plan" : !planReviewDone ? "plan-review" : !codeDone ? "code" : !codeReviewDone ? "code-review" : null;
|
|
929
|
+
const currentStage = index.current_stage ?? [...index.stage_runs].reverse().find((stage) => stage.status === "running" || stage.status === "done")?.stage ?? nextStage;
|
|
930
|
+
const terminal = run.status === "done" || run.status === "cancelled" || run.status === "failed" || run.status === "discarded";
|
|
931
|
+
const paused = run.status === "paused";
|
|
932
|
+
const blocked = paused && Boolean(index.blocker);
|
|
933
|
+
return {
|
|
934
|
+
current_stage: currentStage,
|
|
935
|
+
lifecycle: { flow: "mb_sdlc_vnext_protocolize", stage: currentStage, substage: null, status: run.status, terminal, source: "vnext_run" },
|
|
936
|
+
allowed_next_stages: terminal || paused || !nextStage ? [] : [nextStage],
|
|
937
|
+
recommended_next_action: terminal || !nextStage ? "none" : blocked ? "resolve_blocker_then_unblock_same_stage" : paused ? "await_user_answer_then_resume_same_stage" : `start_${nextStage.replace("-", "_")}`,
|
|
938
|
+
recommended_prompt: terminal || !nextStage ? "none" : blocked ? "unblock_command_from_blocker" : paused ? "resume_command_from_pause" : `.memory-bank/dd-flow/vnext/${nextStage}.md`,
|
|
939
|
+
required_predecessor_evidence: !specifyDone ? [] : !protocolizeDone ? ["01-specify/specify.json"] : !planDone ? ["01-specify/specify.json", "02-protocolize/protocolize-result.json"] : !planReviewDone ? ["03-plan/stage-report.json"] : !codeDone ? ["04-plan-review/stage-report.json"] : ["05-code/stage-report.json"],
|
|
940
|
+
guards: [],
|
|
941
|
+
blocked_if_missing: []
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function vnextSpecifyGuidance(run, index) {
|
|
945
|
+
const terminal = ["done", "failed", "cancelled", "discarded"].includes(run.status);
|
|
946
|
+
const next = run.status === "paused" && index.blocker
|
|
947
|
+
? "resolve_blocker_then_unblock_same_stage"
|
|
948
|
+
: run.status === "paused"
|
|
949
|
+
? "await_user_answer_then_resume_same_stage"
|
|
950
|
+
: run.status === "waiting_for_user"
|
|
951
|
+
? "await_user_answer"
|
|
952
|
+
: run.status === "done"
|
|
953
|
+
? "start_protocolize"
|
|
954
|
+
: run.status === "running"
|
|
955
|
+
? "complete_work_session"
|
|
956
|
+
: terminal
|
|
957
|
+
? "none"
|
|
958
|
+
: "inspect_run_state";
|
|
959
|
+
return {
|
|
960
|
+
current_stage: "specify",
|
|
961
|
+
lifecycle: {
|
|
962
|
+
flow: "mb_sdlc_vnext_specify",
|
|
963
|
+
stage: "specify",
|
|
964
|
+
substage: null,
|
|
965
|
+
status: run.status,
|
|
966
|
+
terminal,
|
|
967
|
+
source: "vnext_run"
|
|
968
|
+
},
|
|
969
|
+
allowed_next_stages: terminal ? [] : ["specify"],
|
|
970
|
+
recommended_next_action: next,
|
|
971
|
+
recommended_prompt: terminal ? "none" : ".memory-bank/dd-flow/vnext/specify.md",
|
|
972
|
+
required_predecessor_evidence: [],
|
|
973
|
+
guards: [],
|
|
974
|
+
blocked_if_missing: []
|
|
975
|
+
};
|
|
976
|
+
}
|
|
566
977
|
function persistRunState(context, project, run, index, options = {}) {
|
|
567
978
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
568
979
|
const previousRuntime = readRuntimeSnapshot(runtimePath);
|
|
@@ -572,16 +983,17 @@ function persistRunState(context, project, run, index, options = {}) {
|
|
|
572
983
|
if (options.flagRevision) {
|
|
573
984
|
history.push(options.flagRevision);
|
|
574
985
|
}
|
|
986
|
+
const persistedIndex = persistedVnextIndex(index);
|
|
575
987
|
ensureDir(path.dirname(runtimePath));
|
|
576
|
-
writeJsonFile(runtimePath, runtimeSnapshotForIndex(
|
|
577
|
-
context.db.run(`UPDATE
|
|
988
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, runtimeRevision, history));
|
|
989
|
+
context.db.run(`UPDATE runs
|
|
578
990
|
SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?,
|
|
579
991
|
updated_at = ?, completed_at = ?
|
|
580
992
|
WHERE project_id = ? AND id = ?`, [
|
|
581
993
|
index.status,
|
|
582
994
|
index.verdict,
|
|
583
995
|
index.next_action,
|
|
584
|
-
JSON.stringify(
|
|
996
|
+
JSON.stringify(persistedIndex),
|
|
585
997
|
runtimePath,
|
|
586
998
|
runtimePath,
|
|
587
999
|
index.updated_at,
|
|
@@ -590,6 +1002,58 @@ function persistRunState(context, project, run, index, options = {}) {
|
|
|
590
1002
|
run.id
|
|
591
1003
|
]);
|
|
592
1004
|
}
|
|
1005
|
+
/** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
|
|
1006
|
+
function persistedVnextIndex(index) {
|
|
1007
|
+
if (index.flow_kind !== "vnext_specify" && index.flow_kind !== "vnext_protocolize")
|
|
1008
|
+
return index;
|
|
1009
|
+
const persisted = structuredClone(index);
|
|
1010
|
+
delete persisted.current_stage;
|
|
1011
|
+
delete persisted.attempts;
|
|
1012
|
+
delete persisted.workers;
|
|
1013
|
+
delete persisted.session_coverage;
|
|
1014
|
+
delete persisted.usage_coverage;
|
|
1015
|
+
return persisted;
|
|
1016
|
+
}
|
|
1017
|
+
function runSettings(index) {
|
|
1018
|
+
const planReview = index.settings?.plan_review;
|
|
1019
|
+
const codeReview = index.settings?.code_review;
|
|
1020
|
+
if (planReview && codeReview)
|
|
1021
|
+
return { plan_review: planReview, code_review: codeReview };
|
|
1022
|
+
return {
|
|
1023
|
+
plan_review: {
|
|
1024
|
+
mode: "auto",
|
|
1025
|
+
source: "default",
|
|
1026
|
+
reason: "No explicit review preference was recorded.",
|
|
1027
|
+
updated_at: index.created_at
|
|
1028
|
+
},
|
|
1029
|
+
code_review: codeReview ?? {
|
|
1030
|
+
mode: index.execution_profile?.settings.code_review_mode ?? "auto",
|
|
1031
|
+
source: "default",
|
|
1032
|
+
reason: "No explicit review preference was recorded.",
|
|
1033
|
+
updated_at: index.created_at
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function materializedRunVariables(index) {
|
|
1038
|
+
const settings = runSettings(index);
|
|
1039
|
+
return {
|
|
1040
|
+
"policy.plan_review.requested_mode": settings.plan_review.mode,
|
|
1041
|
+
"policy.code_review.requested_mode": settings.code_review.mode,
|
|
1042
|
+
...(index.variables ?? {})
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function setRunVariable(context, input, value) {
|
|
1046
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1047
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
1048
|
+
const index = authoritativeIndex(run);
|
|
1049
|
+
if (index.variables?.[input.key] === value)
|
|
1050
|
+
return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
|
|
1051
|
+
index.variables = { ...(index.variables ?? {}), [input.key]: value };
|
|
1052
|
+
index.updated_at = context.now();
|
|
1053
|
+
persistRunState(context, project, run, index);
|
|
1054
|
+
appendRunTimeline(runArtifactRoot(run), { at: index.updated_at, type: "run_variable_set", run_id: run.id, key: input.key });
|
|
1055
|
+
return { ok: true, run_id: run.id, key: input.key, value, idempotent: false };
|
|
1056
|
+
}
|
|
593
1057
|
function appendRunTimeline(runHome, event) {
|
|
594
1058
|
ensureDir(runHome);
|
|
595
1059
|
const timelinePath = path.join(runHome, "timeline.jsonl");
|
|
@@ -630,11 +1094,13 @@ function readRuntimeSnapshot(file) {
|
|
|
630
1094
|
}
|
|
631
1095
|
}
|
|
632
1096
|
function authoritativeIndex(run) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1097
|
+
try {
|
|
1098
|
+
const state = JSON.parse(run.index_json);
|
|
1099
|
+
return { ...state, schema_id: runSchemaId };
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
throw new AppError("validation", "RUN mechanical state in SQLite is missing or invalid", 2, { run_id: run.id, cause: String(error) });
|
|
636
1103
|
}
|
|
637
|
-
return { ...runtime, schema_id: runSchemaId };
|
|
638
1104
|
}
|
|
639
1105
|
function sanitizeTimelineValue(value, key) {
|
|
640
1106
|
if (key && /(secret|password|authorization|cookie|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|transcript|prompt|tool[_-]?output|document[_-]?content)/i.test(key)) {
|
|
@@ -677,7 +1143,7 @@ function resolveRun(context, projectId, idOrAlias) {
|
|
|
677
1143
|
return requireRunById(context, projectId, idOrAlias);
|
|
678
1144
|
}
|
|
679
1145
|
if (isShortEntityId(idOrAlias)) {
|
|
680
|
-
const matches = context.db.all("SELECT * FROM
|
|
1146
|
+
const matches = context.db.all("SELECT * FROM runs WHERE project_id = ? AND short_id = ? ORDER BY updated_at DESC", [
|
|
681
1147
|
projectId,
|
|
682
1148
|
idOrAlias
|
|
683
1149
|
]);
|
|
@@ -692,14 +1158,14 @@ function resolveRun(context, projectId, idOrAlias) {
|
|
|
692
1158
|
throw new AppError("not_found", `Run is not registered: ${idOrAlias}`, 1, { run_id: idOrAlias });
|
|
693
1159
|
}
|
|
694
1160
|
function requireRunById(context, projectId, runId) {
|
|
695
|
-
const run = context.db.get("SELECT * FROM
|
|
1161
|
+
const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
696
1162
|
if (!run) {
|
|
697
1163
|
throw new AppError("not_found", `Run is not registered: ${runId}`, 1, { run_id: runId });
|
|
698
1164
|
}
|
|
699
1165
|
return run;
|
|
700
1166
|
}
|
|
701
1167
|
function nextRunId(context, projectId, slug) {
|
|
702
|
-
const rows = context.db.all("SELECT id FROM
|
|
1168
|
+
const rows = context.db.all("SELECT id FROM runs WHERE project_id = ? AND id LIKE 'RUN-%'", [projectId]);
|
|
703
1169
|
const max = rows.reduce((value, row) => {
|
|
704
1170
|
try {
|
|
705
1171
|
const parsed = parseFullEntityId(row.id);
|
|
@@ -749,19 +1215,19 @@ function parseRunFlowKind(value) {
|
|
|
749
1215
|
return normalized;
|
|
750
1216
|
}
|
|
751
1217
|
function parseRunStatus(value) {
|
|
752
|
-
if (!["running", "done", "blocked", "cancelled", "failed"].includes(value)) {
|
|
1218
|
+
if (!["running", "paused", "waiting_for_user", "done", "blocked", "cancelled", "failed"].includes(value)) {
|
|
753
1219
|
throw new AppError("validation", "run status is not supported", 2, {
|
|
754
1220
|
status: value,
|
|
755
|
-
allowed: ["running", "done", "blocked", "cancelled", "failed"]
|
|
1221
|
+
allowed: ["running", "paused", "waiting_for_user", "done", "blocked", "cancelled", "failed"]
|
|
756
1222
|
});
|
|
757
1223
|
}
|
|
758
1224
|
return value;
|
|
759
1225
|
}
|
|
760
1226
|
function parseStageStatus(value) {
|
|
761
|
-
if (!["pending", "running", "done", "blocked", "skipped", "failed"].includes(value)) {
|
|
1227
|
+
if (!["pending", "running", "paused", "waiting_for_user", "done", "blocked", "skipped", "failed"].includes(value)) {
|
|
762
1228
|
throw new AppError("validation", "stage status is not supported", 2, {
|
|
763
1229
|
status: value,
|
|
764
|
-
allowed: ["pending", "running", "done", "blocked", "skipped", "failed"]
|
|
1230
|
+
allowed: ["pending", "running", "paused", "waiting_for_user", "done", "blocked", "skipped", "failed"]
|
|
765
1231
|
});
|
|
766
1232
|
}
|
|
767
1233
|
return value;
|
|
@@ -827,12 +1293,13 @@ function writeJsonFile(file, value) {
|
|
|
827
1293
|
function runArtifactRoot(run) {
|
|
828
1294
|
return run.run_home_path ?? path.dirname(run.runtime_path);
|
|
829
1295
|
}
|
|
830
|
-
function gitFacts(workspaceRoot) {
|
|
1296
|
+
export function gitFacts(workspaceRoot) {
|
|
831
1297
|
const read = (args) => {
|
|
832
1298
|
const result = spawnSync("git", ["-C", workspaceRoot, ...args.split(" ")], { encoding: "utf8" });
|
|
833
1299
|
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
834
1300
|
};
|
|
835
|
-
const
|
|
1301
|
+
const statusResult = spawnSync("git", ["-C", workspaceRoot, "status", "--porcelain", "--untracked-files=all"], { encoding: "utf8" });
|
|
1302
|
+
const status = statusResult.status === 0 ? statusResult.stdout.trim() : null;
|
|
836
1303
|
return {
|
|
837
1304
|
branch: read("branch --show-current"),
|
|
838
1305
|
head: read("rev-parse HEAD"),
|