@deksden-com/dd-flow-cli 0.4.2 → 0.5.0
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 +27 -4
- package/dist/build-info.json +6 -6
- package/dist/cli/help.js +14 -3
- package/dist/cli/run-cli.js +32 -16
- package/dist/domain/flow-contract.js +81 -2
- package/dist/domain/validation.js +56 -28
- package/dist/protocol/local-files.js +1 -16
- package/dist/schemas/code-stage-report.schema.json +2 -2
- package/dist/schemas/flow-contract.schema.json +150 -94
- package/dist/schemas/flow-run.schema.json +129 -23
- package/dist/schemas/mb-upgrade-review-data.schema.json +2 -2
- package/dist/schemas/memorybank-permissions-preflight.schema.json +13 -73
- package/dist/schemas/merge-stage-report.schema.json +2 -2
- package/dist/schemas/plan-stage-report.schema.json +38 -335
- package/dist/schemas/project-flow-pack-manifest.schema.json +4 -4
- package/dist/schemas/protocol-plan.schema.json +197 -0
- package/dist/schemas/release-impact.schema.json +9 -5
- package/dist/schemas/session-usage.schema.json +16 -0
- package/dist/schemas/stage-finish-input.schema.json +20 -0
- package/dist/schemas/stage-prompt.schema.json +31 -0
- package/dist/schemas/stage-report.schema.json +20 -0
- package/dist/schemas/stage-start-response.schema.json +29 -0
- package/dist/schemas/timeline-event.schema.json +29 -0
- package/dist/schemas/worktrunk-workspace.schema.json +19 -0
- package/dist/services/branch-context.js +9 -4
- package/dist/services/dashboard.js +51 -26
- package/dist/services/engines.js +84 -18
- package/dist/services/hooks.js +80 -246
- package/dist/services/memory-permissions.js +77 -69
- package/dist/services/plan-runtime.js +124 -0
- package/dist/services/plans.js +22 -84
- package/dist/services/projects.js +2 -1
- package/dist/services/prompts.js +26 -21
- package/dist/services/protocols.js +29 -25
- package/dist/services/run-projection.js +77 -11
- package/dist/services/runs.js +95 -61
- package/dist/services/schema-validation.js +168 -7
- package/dist/services/sessions.js +132 -68
- package/dist/services/stage-lifecycle.js +572 -0
- package/dist/services/tooling.js +285 -0
- package/dist/services/usage.js +183 -30
- package/dist/services/version-status.js +1 -1
- package/dist/services/worktrees.js +88 -39
- package/dist/storage/database.js +72 -30
- package/dist/storage/paths.js +0 -9
- package/package.json +14 -13
- package/tools/worktrunk-manifest.json +34 -0
- package/dist/schemas/flow-run-index-v3.schema.json +0 -203
- package/dist/schemas/flow-run-index.schema.json +0 -175
|
@@ -8,10 +8,11 @@ import { requireStage } from "../domain/validation.js";
|
|
|
8
8
|
import { AppError } from "../shared/errors.js";
|
|
9
9
|
import { parseJsonObject } from "../shared/json.js";
|
|
10
10
|
import { ensureReadableFile } from "../storage/database.js";
|
|
11
|
-
import { ensureDir,
|
|
11
|
+
import { ensureDir, planJsonPath, resolveProjectRoot, runtimeProtocolDir, runtimeStateJsonPath } from "../storage/paths.js";
|
|
12
12
|
import { appendAudit, getAuditEvents } from "./audit.js";
|
|
13
13
|
import { requireProjectByRoot } from "./projects.js";
|
|
14
|
-
import { ensureRuntimeProtocolFiles,
|
|
14
|
+
import { ensureRuntimeProtocolFiles, readStateFile, writeState } from "../protocol/local-files.js";
|
|
15
|
+
import { planSummary, planWithProgress, readCanonicalPlan } from "./plan-runtime.js";
|
|
15
16
|
import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
|
|
16
17
|
import { buildProtocolFlowGuidance } from "./flow-guidance.js";
|
|
17
18
|
import { lifecycleIsTerminalFailure, lifecycleIsTerminalSuccess, normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
|
|
@@ -46,7 +47,7 @@ export function registerProtocol(context, input) {
|
|
|
46
47
|
JSON.stringify(state.blockers),
|
|
47
48
|
JSON.stringify(state.active_def),
|
|
48
49
|
runtimeStateJsonPath(context.ddFlowHome, project.id, protocolId),
|
|
49
|
-
|
|
50
|
+
planJsonPath(projectRoot, protocolId),
|
|
50
51
|
existing?.created_at ?? now,
|
|
51
52
|
now
|
|
52
53
|
];
|
|
@@ -88,7 +89,7 @@ export function getProtocolStatus(context, input) {
|
|
|
88
89
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
89
90
|
const runtime = readProtocolRuntimeState(context, protocol);
|
|
90
91
|
const state = runtime.state;
|
|
91
|
-
const plan =
|
|
92
|
+
const plan = canonicalPlanIfPresent(context, protocol);
|
|
92
93
|
const runDiagnostics = protocolRunDiagnostics(context, protocol, state);
|
|
93
94
|
const queue = context.db.get("SELECT * FROM merge_queue WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
|
|
94
95
|
const lifecycle = normalizeProtocolLifecycle({ state, queueStatus: queue?.status ?? null });
|
|
@@ -99,7 +100,7 @@ export function getProtocolStatus(context, input) {
|
|
|
99
100
|
diagnostics: [...runtime.diagnostics, ...runDiagnostics.diagnostics, ...lifecycle.diagnostics],
|
|
100
101
|
latest_run: runDiagnostics.latest_run,
|
|
101
102
|
state,
|
|
102
|
-
plan: plan ?
|
|
103
|
+
plan: plan ? planSummary(context, { projectId: protocol.project_id, protocolId: protocol.id, planPath: protocol.plan_path }) : state.plan,
|
|
103
104
|
merge_queue: queue,
|
|
104
105
|
branch_context: getProtocolBranchContext(context, { protocolId: protocol.id, projectRoot: project.root }).branch_context,
|
|
105
106
|
flow_guidance: buildProtocolFlowGuidance({ state, latestRun: runDiagnostics.latest_run, queueStatus: queue?.status ?? null }),
|
|
@@ -334,11 +335,15 @@ export function readyForMerge(context, input) {
|
|
|
334
335
|
if (missing.length > 0) {
|
|
335
336
|
throw new AppError("readiness_missing_fields", "Protocol is missing explicit readiness fields", 1, { missing });
|
|
336
337
|
}
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
const blockingDefs = state.active_def.filter(isBlockingDeferral);
|
|
339
|
+
if (blockingDefs.length > 0) {
|
|
340
|
+
throw new AppError("readiness_blocked", "Protocol has open blocking deferrals", 1, { active_def: blockingDefs });
|
|
339
341
|
}
|
|
340
|
-
const plan =
|
|
341
|
-
const openRequired = plan
|
|
342
|
+
const plan = canonicalPlanIfPresent(context, protocol);
|
|
343
|
+
const openRequired = plan
|
|
344
|
+
? planWithProgress(context, { projectId: protocol.project_id, protocolId: protocol.id, planPath: protocol.plan_path }).plan.items
|
|
345
|
+
.filter((item) => item.required && !["done", "skipped"].includes(item.status))
|
|
346
|
+
: [];
|
|
342
347
|
if (openRequired.length > 0) {
|
|
343
348
|
throw new AppError("readiness_plan_open", "Protocol has open required plan items", 1, {
|
|
344
349
|
items: openRequired.map((item) => ({ id: item.id, status: item.status }))
|
|
@@ -374,6 +379,11 @@ export function readyForMerge(context, input) {
|
|
|
374
379
|
flow_guidance: buildProtocolFlowGuidance({ state: nextState, latestRun: runDiagnostics.latest_run, queueStatus: "ready" })
|
|
375
380
|
};
|
|
376
381
|
}
|
|
382
|
+
function isBlockingDeferral(value) {
|
|
383
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
384
|
+
return true;
|
|
385
|
+
return value.blocking !== false;
|
|
386
|
+
}
|
|
377
387
|
export function syncProtocolFromRun(context, input) {
|
|
378
388
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
379
389
|
const protocol = requireProtocol(context, input.protocolId, project.id);
|
|
@@ -738,7 +748,7 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
738
748
|
source: "stable_runtime_state",
|
|
739
749
|
recommended_action: "legacy protocol state_path was moved to stable runtime storage"
|
|
740
750
|
});
|
|
741
|
-
const stablePlanPath =
|
|
751
|
+
const stablePlanPath = planJsonPath(protocol.project_root, protocol.id);
|
|
742
752
|
context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE project_id = ? AND id = ?", [stableStatePath, stablePlanPath, protocol.project_id, protocol.id]);
|
|
743
753
|
protocol.state_path = stableStatePath;
|
|
744
754
|
protocol.plan_path = stablePlanPath;
|
|
@@ -752,9 +762,9 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
752
762
|
recommended_action: "runtime state reconstructed from SQLite; run cleanup scan/apply if this persists"
|
|
753
763
|
});
|
|
754
764
|
}
|
|
755
|
-
const plan = context
|
|
756
|
-
const
|
|
757
|
-
?
|
|
765
|
+
const plan = canonicalPlanIfPresent(context, protocol);
|
|
766
|
+
const summary = plan
|
|
767
|
+
? planSummary(context, { projectId: protocol.project_id, protocolId: protocol.id, planPath: protocol.plan_path })
|
|
758
768
|
: { plan_id: null, total: 0, done: 0, blocked: 0 };
|
|
759
769
|
const flowContract = loadProjectFlowContract(protocol.project_root);
|
|
760
770
|
const state = {
|
|
@@ -766,14 +776,14 @@ export function readProtocolRuntimeState(context, protocol) {
|
|
|
766
776
|
next_action: protocol.next_action,
|
|
767
777
|
route: JSON.parse(protocol.route_json),
|
|
768
778
|
workspace: JSON.parse(protocol.workspace_json),
|
|
769
|
-
plan:
|
|
779
|
+
plan: summary,
|
|
770
780
|
blockers: JSON.parse(protocol.blockers_json),
|
|
771
781
|
active_def: JSON.parse(protocol.active_def_json),
|
|
772
782
|
flow_contract: flowContract,
|
|
773
783
|
updated_at: protocol.updated_at
|
|
774
784
|
};
|
|
775
785
|
const stableStatePath = runtimeStateJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
|
|
776
|
-
const stablePlanPath =
|
|
786
|
+
const stablePlanPath = planJsonPath(protocol.project_root, protocol.id);
|
|
777
787
|
ensureDir(path.dirname(stableStatePath));
|
|
778
788
|
writeState(stableStatePath, state);
|
|
779
789
|
context.db.run("UPDATE protocols SET state_path = ?, plan_path = ? WHERE project_id = ? AND id = ?", [stableStatePath, stablePlanPath, protocol.project_id, protocol.id]);
|
|
@@ -1162,16 +1172,10 @@ function protocolStatusPayload(protocol) {
|
|
|
1162
1172
|
updated_at: protocol.updated_at
|
|
1163
1173
|
};
|
|
1164
1174
|
}
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
return {
|
|
1170
|
-
plan_id: plan.plan_id,
|
|
1171
|
-
total: plan.items.length,
|
|
1172
|
-
done: plan.items.filter((item) => item.status === "done").length,
|
|
1173
|
-
blocked: plan.items.filter((item) => item.status === "blocked").length
|
|
1174
|
-
};
|
|
1175
|
+
function canonicalPlanIfPresent(context, protocol) {
|
|
1176
|
+
if (!fs.existsSync(protocol.plan_path))
|
|
1177
|
+
return undefined;
|
|
1178
|
+
return readCanonicalPlan(protocol.plan_path, protocol.id).plan;
|
|
1175
1179
|
}
|
|
1176
1180
|
function readinessMissingFields(state) {
|
|
1177
1181
|
const missing = [];
|
|
@@ -1,29 +1,95 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export function refreshRunSessionProjection(context, projectId, runId) {
|
|
4
|
-
const run = context.db.get("SELECT id, runtime_path, run_index_path, index_json FROM flow_runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
4
|
+
const run = context.db.get("SELECT id, project_root, subject_type, subject_id, runtime_path, run_index_path, index_json FROM flow_runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
5
5
|
if (!run)
|
|
6
6
|
return;
|
|
7
7
|
const index = JSON.parse(run.index_json);
|
|
8
8
|
const sessions = context.db.all(`SELECT session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
|
|
9
9
|
created_at, updated_at, stopped_at, coverage_units_json
|
|
10
10
|
FROM flow_sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id`, [projectId, runId]).map(sessionProjection);
|
|
11
|
-
|
|
11
|
+
const plan = run.subject_type === "protocol" ? planProjection(context, projectId, run.subject_id, run.project_root) : {};
|
|
12
|
+
const workers = workerProjection(context, projectId, runId);
|
|
13
|
+
if (JSON.stringify(index.sessions ?? []) === JSON.stringify(sessions)
|
|
14
|
+
&& JSON.stringify(index.plan_ref) === JSON.stringify(plan.plan_ref)
|
|
15
|
+
&& JSON.stringify(index.plan_progress) === JSON.stringify(plan.plan_progress)
|
|
16
|
+
&& JSON.stringify(index.workers ?? {}) === JSON.stringify(workers))
|
|
12
17
|
return;
|
|
13
18
|
index.sessions = sessions;
|
|
19
|
+
if (plan.plan_ref)
|
|
20
|
+
index.plan_ref = plan.plan_ref;
|
|
21
|
+
if (plan.plan_progress)
|
|
22
|
+
index.plan_progress = plan.plan_progress;
|
|
23
|
+
index.workers = workers;
|
|
14
24
|
index.updated_at = context.now();
|
|
15
|
-
const runtimeRevision = typeof index.runtime_revision === "number" ? index.runtime_revision + 1 : 1;
|
|
16
|
-
index.runtime_revision = runtimeRevision;
|
|
17
25
|
const runtime = readJson(run.runtime_path);
|
|
18
|
-
|
|
19
|
-
runtime.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const runtimeRevision = typeof runtime?.runtime_revision === "number"
|
|
27
|
+
? runtime.runtime_revision + 1
|
|
28
|
+
: typeof index.runtime_revision === "number"
|
|
29
|
+
? index.runtime_revision + 1
|
|
30
|
+
: 1;
|
|
31
|
+
index.runtime_revision = runtimeRevision;
|
|
32
|
+
const authoritative = runtime ?? { ...index, schema_id: "dd-flow/flow-run@2" };
|
|
33
|
+
authoritative.sessions = sessions;
|
|
34
|
+
if (plan.plan_ref)
|
|
35
|
+
authoritative.plan_ref = plan.plan_ref;
|
|
36
|
+
if (plan.plan_progress)
|
|
37
|
+
authoritative.plan_progress = plan.plan_progress;
|
|
38
|
+
authoritative.workers = workers;
|
|
39
|
+
authoritative.updated_at = index.updated_at;
|
|
40
|
+
authoritative.runtime_revision = runtimeRevision;
|
|
41
|
+
writeJson(run.runtime_path, authoritative);
|
|
25
42
|
context.db.run("UPDATE flow_runs SET index_json = ?, updated_at = ? WHERE project_id = ? AND id = ?", [JSON.stringify(index), index.updated_at, projectId, runId]);
|
|
26
43
|
}
|
|
44
|
+
function planProjection(context, projectId, protocolId, projectRoot) {
|
|
45
|
+
const binding = context.db.get("SELECT plan_path, plan_revision, plan_sha256 FROM plan_bindings WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
46
|
+
if (!binding)
|
|
47
|
+
return {};
|
|
48
|
+
const planPath = path.relative(projectRoot, binding.plan_path).split(path.sep).join("/");
|
|
49
|
+
const planId = readPlanId(binding.plan_path);
|
|
50
|
+
const progress = Object.fromEntries(context.db.all("SELECT item_id, status, summary, evidence_json FROM plan_progress WHERE project_id = ? AND protocol_id = ? ORDER BY item_id", [projectId, protocolId]).map((row) => [row.item_id, {
|
|
51
|
+
status: row.status,
|
|
52
|
+
...(row.summary ? { summary: row.summary } : {}),
|
|
53
|
+
...(parseStringArray(row.evidence_json).length > 0 ? { evidence: parseStringArray(row.evidence_json) } : {})
|
|
54
|
+
}]));
|
|
55
|
+
return {
|
|
56
|
+
plan_ref: {
|
|
57
|
+
path: planPath,
|
|
58
|
+
plan_id: planId,
|
|
59
|
+
revision: binding.plan_revision,
|
|
60
|
+
sha256: binding.plan_sha256
|
|
61
|
+
},
|
|
62
|
+
plan_progress: progress
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function readPlanId(planPath) {
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
68
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.plan_id === "string") {
|
|
69
|
+
return value.plan_id;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// The canonical validator owns malformed-plan errors; projection stays bounded.
|
|
74
|
+
}
|
|
75
|
+
return "unknown-plan";
|
|
76
|
+
}
|
|
77
|
+
function workerProjection(context, projectId, runId) {
|
|
78
|
+
return Object.fromEntries(context.db.all("SELECT job_id, plan_item_id, status, worker_session_id FROM flow_jobs WHERE project_id = ? AND run_id = ? ORDER BY job_id", [projectId, runId]).map((job) => [job.job_id, {
|
|
79
|
+
units: [job.plan_item_id],
|
|
80
|
+
status: { pending: "registered", done: "completed" }[job.status] ?? job.status,
|
|
81
|
+
...(job.worker_session_id ? { session_id: job.worker_session_id } : {})
|
|
82
|
+
}]));
|
|
83
|
+
}
|
|
84
|
+
function parseStringArray(value) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(value);
|
|
87
|
+
return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string") ? parsed : [];
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
27
93
|
function sessionProjection(row) {
|
|
28
94
|
return {
|
|
29
95
|
session_id: row.session_id,
|
package/dist/services/runs.js
CHANGED
|
@@ -5,17 +5,15 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
6
6
|
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
8
|
-
import { ensureDir, projectRunHome,
|
|
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
13
|
import { checkpointRunUsage, usageForRun } from "./usage.js";
|
|
14
14
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
15
|
-
const runSchemaId = "dd-flow/flow-run
|
|
16
|
-
const
|
|
17
|
-
const oldestRunSchemaId = "dd-flow/flow-run-index@1";
|
|
18
|
-
const runtimeSchemaId = "dd-flow/flow-run@1";
|
|
15
|
+
const runSchemaId = "dd-flow/flow-run@2";
|
|
16
|
+
const runtimeSchemaId = "dd-flow/flow-run@2";
|
|
19
17
|
const runIdType = "RUN";
|
|
20
18
|
const allowedRunFlowKinds = [
|
|
21
19
|
"mb_sdlc",
|
|
@@ -51,7 +49,6 @@ export function startFlowRun(context, input) {
|
|
|
51
49
|
now
|
|
52
50
|
});
|
|
53
51
|
const runHome = projectRunHome(context.ddFlowHome, project.id, runId);
|
|
54
|
-
const runIndexAbsolute = projectRunIndexPath(context.ddFlowHome, project.id, runId);
|
|
55
52
|
const runDirRelative = path.posix.join("runs", runId);
|
|
56
53
|
const runtimePath = projectRunJsonPath(context.ddFlowHome, project.id, runId);
|
|
57
54
|
const index = {
|
|
@@ -60,16 +57,19 @@ export function startFlowRun(context, input) {
|
|
|
60
57
|
short_id: shortId,
|
|
61
58
|
flow_kind: flowKind,
|
|
62
59
|
subject: {
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
id: requiredPlain(input.subjectId, "subject-id"),
|
|
61
|
+
title: requiredPlain(input.subjectId, "subject-id")
|
|
65
62
|
},
|
|
66
63
|
project: {
|
|
67
64
|
id: project.id,
|
|
68
|
-
|
|
65
|
+
title: path.basename(project.root)
|
|
69
66
|
},
|
|
70
67
|
workspace: {
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
project_root: project.root,
|
|
69
|
+
workspace_path: workspaceRoot,
|
|
70
|
+
branch: gitFacts(workspaceRoot).branch ?? "unknown",
|
|
71
|
+
base: gitFacts(workspaceRoot).head ?? "unknown",
|
|
72
|
+
manager: "official-worktrunk"
|
|
73
73
|
},
|
|
74
74
|
run_home: {
|
|
75
75
|
storage: "dd_flow_home",
|
|
@@ -86,7 +86,7 @@ export function startFlowRun(context, input) {
|
|
|
86
86
|
},
|
|
87
87
|
stage_runs: [],
|
|
88
88
|
sessions: [],
|
|
89
|
-
artifacts:
|
|
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
90
|
status: "running",
|
|
91
91
|
verdict: "pending",
|
|
92
92
|
next_action: input.nextAction ?? null,
|
|
@@ -98,12 +98,16 @@ export function startFlowRun(context, input) {
|
|
|
98
98
|
snapshot_revision: flowFlags.snapshot_revision,
|
|
99
99
|
snapshot_checksum: flowFlags.snapshot_checksum,
|
|
100
100
|
reconciliation_status: "reconciled",
|
|
101
|
-
flow_flags: flowFlags
|
|
101
|
+
flow_flags: flowFlags,
|
|
102
|
+
current_stage: null,
|
|
103
|
+
attempts: [],
|
|
104
|
+
session_coverage: { status: "unavailable", expected: [], observed: [], missing: [] },
|
|
105
|
+
usage_coverage: { status: "unavailable", expected: [], observed: [], missing: [] },
|
|
106
|
+
timeline_path: "timeline.jsonl"
|
|
102
107
|
};
|
|
103
108
|
ensureDir(path.dirname(runtimePath));
|
|
104
109
|
ensureDir(runHome);
|
|
105
110
|
writeJsonFile(runtimePath, runtimeSnapshotForIndex(index, 1));
|
|
106
|
-
writeJsonFile(runIndexAbsolute, index);
|
|
107
111
|
context.db.run(`INSERT INTO flow_runs
|
|
108
112
|
(id, short_id, slug, project_id, project_root, workspace_root, flow_kind, subject_type, subject_id,
|
|
109
113
|
status, verdict, next_action, runtime_path, run_dir, run_index_path, run_home_path, layout_version, artifact_root_kind,
|
|
@@ -116,14 +120,14 @@ export function startFlowRun(context, input) {
|
|
|
116
120
|
project.root,
|
|
117
121
|
workspaceRoot,
|
|
118
122
|
flowKind,
|
|
119
|
-
|
|
123
|
+
requiredPlain(input.subjectType, "subject-type"),
|
|
120
124
|
index.subject.id,
|
|
121
125
|
index.status,
|
|
122
126
|
index.verdict,
|
|
123
127
|
index.next_action,
|
|
124
128
|
runtimePath,
|
|
125
129
|
runHome,
|
|
126
|
-
|
|
130
|
+
runtimePath,
|
|
127
131
|
runHome,
|
|
128
132
|
"home_run_v2",
|
|
129
133
|
"dd_flow_home",
|
|
@@ -152,14 +156,14 @@ export function getFlowRunStatus(context, input) {
|
|
|
152
156
|
const run = resolveRun(context, project.id, input.runId);
|
|
153
157
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
154
158
|
const refreshedRun = requireRunById(context, project.id, run.id);
|
|
155
|
-
const index =
|
|
159
|
+
const index = authoritativeIndex(refreshedRun);
|
|
156
160
|
const runtime = readRuntimeSnapshot(refreshedRun.runtime_path);
|
|
157
161
|
return { ok: true, run: flowRunSummary(refreshedRun), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, refreshedRun, index) };
|
|
158
162
|
}
|
|
159
163
|
export function getFlowRunFlagsStatus(context, input) {
|
|
160
164
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
161
165
|
const run = resolveRun(context, project.id, input.runId);
|
|
162
|
-
const index =
|
|
166
|
+
const index = authoritativeIndex(run);
|
|
163
167
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
164
168
|
const flags = runtime?.flow_flags ?? index.flow_flags;
|
|
165
169
|
if (!flags) {
|
|
@@ -199,7 +203,7 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
199
203
|
// Read the authority only after the write lock: two callers with the same
|
|
200
204
|
// expected revision must not both pass the compare-and-set check.
|
|
201
205
|
const lockedRun = requireRunById(context, project.id, run.id);
|
|
202
|
-
const index =
|
|
206
|
+
const index = authoritativeIndex(lockedRun);
|
|
203
207
|
const runtime = readRuntimeSnapshot(lockedRun.runtime_path);
|
|
204
208
|
const current = runtime?.flow_flags ?? index.flow_flags;
|
|
205
209
|
if (!current) {
|
|
@@ -268,7 +272,7 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
268
272
|
flow_flags: next,
|
|
269
273
|
...(reason ? { reason } : {})
|
|
270
274
|
};
|
|
271
|
-
|
|
275
|
+
persistRunState(context, project, run, index, {
|
|
272
276
|
flagRevision: { revision: next.snapshot_revision, checksum: next.snapshot_checksum, at: now, idempotency_key: idempotencyKey }
|
|
273
277
|
});
|
|
274
278
|
context.db.run(`INSERT INTO flow_run_flag_mutations
|
|
@@ -310,7 +314,7 @@ export function listFlowRuns(context, input) {
|
|
|
310
314
|
export function attachFlowRunStage(context, input) {
|
|
311
315
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
312
316
|
const run = resolveRun(context, project.id, input.runId);
|
|
313
|
-
const index =
|
|
317
|
+
const index = authoritativeIndex(run);
|
|
314
318
|
const now = context.now();
|
|
315
319
|
const stage = requiredPlain(input.stage, "stage");
|
|
316
320
|
const dir = requiredStageDir(input.dir);
|
|
@@ -325,13 +329,20 @@ export function attachFlowRunStage(context, input) {
|
|
|
325
329
|
dir,
|
|
326
330
|
status,
|
|
327
331
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : existing?.data_schema_id ? { data_schema_id: existing.data_schema_id } : {}),
|
|
328
|
-
updated_at: now
|
|
329
|
-
...(status === "running" && !existing?.started_at ? { started_at: now } : {}),
|
|
330
|
-
...(status === "running" ? { attempt: nextAttempt(existing) } : {})
|
|
332
|
+
updated_at: now
|
|
331
333
|
};
|
|
334
|
+
if (status === "running") {
|
|
335
|
+
stageRun.started_at = now;
|
|
336
|
+
delete stageRun.completed_at;
|
|
337
|
+
stageRun.duration_ms = null;
|
|
338
|
+
stageRun.git = gitFacts(run.workspace_root);
|
|
339
|
+
stageRun.attempt = nextAttempt(existing);
|
|
340
|
+
}
|
|
332
341
|
upsertStage(index, stageRun);
|
|
342
|
+
index.current_stage = stage;
|
|
343
|
+
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 }));
|
|
333
344
|
index.updated_at = now;
|
|
334
|
-
|
|
345
|
+
persistRunState(context, project, run, index);
|
|
335
346
|
appendAudit(context, {
|
|
336
347
|
projectId: project.id,
|
|
337
348
|
eventType: "flow_run.stage_attached",
|
|
@@ -354,7 +365,7 @@ export function attachFlowRunStage(context, input) {
|
|
|
354
365
|
export function completeFlowRunStage(context, input) {
|
|
355
366
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
356
367
|
const run = resolveRun(context, project.id, input.runId);
|
|
357
|
-
const index =
|
|
368
|
+
const index = authoritativeIndex(run);
|
|
358
369
|
const now = context.now();
|
|
359
370
|
const stage = requiredPlain(input.stage, "stage");
|
|
360
371
|
const status = parseStageStatus(input.status);
|
|
@@ -371,11 +382,20 @@ export function completeFlowRunStage(context, input) {
|
|
|
371
382
|
...(input.report ? { report: input.report } : {}),
|
|
372
383
|
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
373
384
|
updated_at: now,
|
|
374
|
-
...(status === "done" || status === "blocked" || status === "failed" || status === "skipped"
|
|
385
|
+
...(status === "done" || status === "blocked" || status === "failed" || status === "skipped"
|
|
386
|
+
? { completed_at: now, duration_ms: durationMs(existing.started_at ?? null, now), git: gitFacts(run.workspace_root) }
|
|
387
|
+
: {})
|
|
375
388
|
};
|
|
376
389
|
upsertStage(index, stageRun);
|
|
390
|
+
index.current_stage = stage;
|
|
391
|
+
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 }));
|
|
377
392
|
index.updated_at = now;
|
|
378
|
-
|
|
393
|
+
index.execution = {
|
|
394
|
+
project_root: index.execution?.project_root ?? run.project_root,
|
|
395
|
+
workspace_root: index.execution?.workspace_root ?? run.workspace_root,
|
|
396
|
+
git: gitFacts(run.workspace_root)
|
|
397
|
+
};
|
|
398
|
+
persistRunState(context, project, run, index);
|
|
379
399
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
380
400
|
appendAudit(context, {
|
|
381
401
|
projectId: project.id,
|
|
@@ -390,7 +410,7 @@ export function completeFlowRunStage(context, input) {
|
|
|
390
410
|
export function completeFlowRun(context, input) {
|
|
391
411
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
392
412
|
const run = resolveRun(context, project.id, input.runId);
|
|
393
|
-
const index =
|
|
413
|
+
const index = authoritativeIndex(run);
|
|
394
414
|
const now = context.now();
|
|
395
415
|
const status = parseRunStatus(input.status);
|
|
396
416
|
index.status = status;
|
|
@@ -399,8 +419,15 @@ export function completeFlowRun(context, input) {
|
|
|
399
419
|
index.updated_at = now;
|
|
400
420
|
if (["done", "blocked", "cancelled", "failed"].includes(status)) {
|
|
401
421
|
index.completed_at = now;
|
|
422
|
+
index.finished_at = now;
|
|
423
|
+
index.duration_ms = durationMs(index.started_at ?? run.created_at, now);
|
|
424
|
+
index.execution = {
|
|
425
|
+
project_root: index.execution?.project_root ?? run.project_root,
|
|
426
|
+
workspace_root: index.execution?.workspace_root ?? run.workspace_root,
|
|
427
|
+
git: gitFacts(run.workspace_root)
|
|
428
|
+
};
|
|
402
429
|
}
|
|
403
|
-
|
|
430
|
+
persistRunState(context, project, run, index);
|
|
404
431
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
405
432
|
appendAudit(context, {
|
|
406
433
|
projectId: project.id,
|
|
@@ -416,7 +443,7 @@ const timelineSections = ["stages", "events", "sessions", "usage", "artifacts"];
|
|
|
416
443
|
export function getFlowRunTimeline(context, input) {
|
|
417
444
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
418
445
|
const run = resolveRun(context, project.id, input.runId);
|
|
419
|
-
const index =
|
|
446
|
+
const index = authoritativeIndex(run);
|
|
420
447
|
const file = path.join(runArtifactRoot(run), "timeline.jsonl");
|
|
421
448
|
const events = fs.existsSync(file)
|
|
422
449
|
? fs.readFileSync(file, "utf8").split(/\r?\n/).flatMap((line) => { try {
|
|
@@ -426,7 +453,10 @@ export function getFlowRunTimeline(context, input) {
|
|
|
426
453
|
return [];
|
|
427
454
|
} })
|
|
428
455
|
: [];
|
|
429
|
-
|
|
456
|
+
if ((input.hiddenSections ?? []).length > 0) {
|
|
457
|
+
throw new AppError("validation", "Timeline sections are always included", 2, { hidden_sections: input.hiddenSections });
|
|
458
|
+
}
|
|
459
|
+
const hidden = new Set();
|
|
430
460
|
const sessions = index.sessions;
|
|
431
461
|
const elapsedMs = durationMs(index.started_at ?? run.created_at, index.completed_at ?? null);
|
|
432
462
|
const report = {
|
|
@@ -490,7 +520,7 @@ function guidanceForRun(context, run, index) {
|
|
|
490
520
|
protocolStage: state.stage,
|
|
491
521
|
...(state.flow_contract ? { contract: state.flow_contract } : {}),
|
|
492
522
|
runId: run.id,
|
|
493
|
-
runDir: index.run_home?.relative_path ??
|
|
523
|
+
runDir: index.run_home?.relative_path ?? path.posix.join("runs", run.id)
|
|
494
524
|
});
|
|
495
525
|
}
|
|
496
526
|
catch (error) {
|
|
@@ -498,11 +528,10 @@ function guidanceForRun(context, run, index) {
|
|
|
498
528
|
throw error;
|
|
499
529
|
}
|
|
500
530
|
}
|
|
501
|
-
return buildRunFlowGuidance({ stageRuns: index.stage_runs, runId: run.id, runDir: index.run_home?.relative_path ??
|
|
531
|
+
return buildRunFlowGuidance({ stageRuns: index.stage_runs, runId: run.id, runDir: index.run_home?.relative_path ?? path.posix.join("runs", run.id) });
|
|
502
532
|
}
|
|
503
|
-
function
|
|
533
|
+
function persistRunState(context, project, run, index, options = {}) {
|
|
504
534
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
505
|
-
const runIndexPath = run.run_index_path || projectRunIndexPath(context.ddFlowHome, project.id, run.id);
|
|
506
535
|
const previousRuntime = readRuntimeSnapshot(runtimePath);
|
|
507
536
|
const runtimeRevision = Math.max(index.runtime_revision ?? 0, previousRuntime?.runtime_revision ?? 0) + 1;
|
|
508
537
|
index.runtime_revision = runtimeRevision;
|
|
@@ -511,9 +540,7 @@ function persistRunIndex(context, project, run, index, options = {}) {
|
|
|
511
540
|
history.push(options.flagRevision);
|
|
512
541
|
}
|
|
513
542
|
ensureDir(path.dirname(runtimePath));
|
|
514
|
-
ensureDir(path.dirname(runIndexPath));
|
|
515
543
|
writeJsonFile(runtimePath, runtimeSnapshotForIndex(index, runtimeRevision, history));
|
|
516
|
-
writeJsonFile(runIndexPath, index);
|
|
517
544
|
context.db.run(`UPDATE flow_runs
|
|
518
545
|
SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?,
|
|
519
546
|
updated_at = ?, completed_at = ?
|
|
@@ -523,7 +550,7 @@ function persistRunIndex(context, project, run, index, options = {}) {
|
|
|
523
550
|
index.next_action,
|
|
524
551
|
JSON.stringify(index),
|
|
525
552
|
runtimePath,
|
|
526
|
-
|
|
553
|
+
runtimePath,
|
|
527
554
|
index.updated_at,
|
|
528
555
|
index.completed_at ?? null,
|
|
529
556
|
project.id,
|
|
@@ -539,10 +566,12 @@ function appendRunTimeline(runHome, event) {
|
|
|
539
566
|
const last = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : undefined;
|
|
540
567
|
sequence = typeof last?.sequence === "number" ? last.sequence : lines.length;
|
|
541
568
|
}
|
|
569
|
+
const safeEvent = sanitizeTimelineValue(event);
|
|
542
570
|
const enriched = {
|
|
543
571
|
event_id: crypto.randomUUID(),
|
|
544
572
|
sequence: sequence + 1,
|
|
545
|
-
|
|
573
|
+
at: typeof safeEvent.at === "string" ? safeEvent.at : new Date().toISOString(),
|
|
574
|
+
...safeEvent
|
|
546
575
|
};
|
|
547
576
|
fs.appendFileSync(timelinePath, `${JSON.stringify(enriched)}\n`);
|
|
548
577
|
}
|
|
@@ -552,7 +581,6 @@ function runtimeSnapshotForIndex(index, runtimeRevision, flagRevisionHistory = [
|
|
|
552
581
|
return {
|
|
553
582
|
...rest,
|
|
554
583
|
schema_id: runtimeSchemaId,
|
|
555
|
-
run_index_schema_id: index.schema_id,
|
|
556
584
|
runtime_revision: runtimeRevision,
|
|
557
585
|
...(flagRevisionHistory.length > 0 ? { flag_revision_history: flagRevisionHistory.slice(-32) } : {})
|
|
558
586
|
};
|
|
@@ -568,18 +596,26 @@ function readRuntimeSnapshot(file) {
|
|
|
568
596
|
return undefined;
|
|
569
597
|
}
|
|
570
598
|
}
|
|
571
|
-
function
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
throw new AppError("validation", "--hide must name a timeline section", 2, {
|
|
576
|
-
value,
|
|
577
|
-
allowed: timelineSections
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
hidden.add(value);
|
|
599
|
+
function authoritativeIndex(run) {
|
|
600
|
+
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
601
|
+
if (!runtime) {
|
|
602
|
+
throw new AppError("validation", "RUN authority is missing or invalid", 2, { run_id: run.id, path: run.runtime_path });
|
|
581
603
|
}
|
|
582
|
-
return
|
|
604
|
+
return { ...runtime, schema_id: runSchemaId };
|
|
605
|
+
}
|
|
606
|
+
function sanitizeTimelineValue(value, key) {
|
|
607
|
+
if (key && /(secret|password|authorization|cookie|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|transcript|prompt|tool[_-]?output|document[_-]?content)/i.test(key)) {
|
|
608
|
+
return "[REDACTED]";
|
|
609
|
+
}
|
|
610
|
+
if (typeof value === "string") {
|
|
611
|
+
return value.length > 512 ? `${value.slice(0, 512)}…[TRUNCATED]` : value;
|
|
612
|
+
}
|
|
613
|
+
if (Array.isArray(value))
|
|
614
|
+
return value.map((item) => sanitizeTimelineValue(item));
|
|
615
|
+
if (value && typeof value === "object") {
|
|
616
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, sanitizeTimelineValue(entryValue, entryKey)]));
|
|
617
|
+
}
|
|
618
|
+
return value;
|
|
583
619
|
}
|
|
584
620
|
function durationMs(startedAt, completedAt) {
|
|
585
621
|
if (!startedAt || !completedAt)
|
|
@@ -669,13 +705,6 @@ function flowRunSummary(run) {
|
|
|
669
705
|
completed_at: run.completed_at
|
|
670
706
|
};
|
|
671
707
|
}
|
|
672
|
-
function parseRunIndex(text) {
|
|
673
|
-
const index = JSON.parse(text);
|
|
674
|
-
if (index.schema_id !== runSchemaId && index.schema_id !== legacyRunSchemaId && index.schema_id !== oldestRunSchemaId) {
|
|
675
|
-
throw new AppError("validation", `Invalid run index schema: ${String(index.schema_id)}`, 2);
|
|
676
|
-
}
|
|
677
|
-
return index;
|
|
678
|
-
}
|
|
679
708
|
function parseRunFlowKind(value) {
|
|
680
709
|
const normalized = value === "mb-sdlc" ? "mb_sdlc" : value;
|
|
681
710
|
if (!allowedRunFlowKinds.includes(normalized)) {
|
|
@@ -763,12 +792,17 @@ function writeJsonFile(file, value) {
|
|
|
763
792
|
fs.renameSync(tmpFile, file);
|
|
764
793
|
}
|
|
765
794
|
function runArtifactRoot(run) {
|
|
766
|
-
return path.dirname(run.
|
|
795
|
+
return run.run_home_path ?? path.dirname(run.runtime_path);
|
|
767
796
|
}
|
|
768
797
|
function gitFacts(workspaceRoot) {
|
|
769
798
|
const read = (args) => {
|
|
770
799
|
const result = spawnSync("git", ["-C", workspaceRoot, ...args.split(" ")], { encoding: "utf8" });
|
|
771
800
|
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
772
801
|
};
|
|
773
|
-
|
|
802
|
+
const status = read("status --porcelain --untracked-files=all");
|
|
803
|
+
return {
|
|
804
|
+
branch: read("branch --show-current"),
|
|
805
|
+
head: read("rev-parse HEAD"),
|
|
806
|
+
status: status === null ? "unavailable" : status.length > 0 ? "dirty" : "clean"
|
|
807
|
+
};
|
|
774
808
|
}
|