@deksden-com/dd-flow-cli 0.4.2 → 0.6.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 +33 -4
- package/README.md +2 -2
- package/dist/build-info.json +6 -6
- package/dist/cli/help.js +16 -4
- package/dist/cli/run-cli.js +42 -17
- package/dist/domain/flow-contract.js +82 -3
- package/dist/domain/session-coverage.js +88 -0
- 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 +152 -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 +35 -0
- package/dist/schemas/stage-report.schema.json +20 -0
- package/dist/schemas/stage-start-response.schema.json +30 -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/cleanup.js +77 -0
- package/dist/services/dashboard.js +51 -26
- package/dist/services/engines.js +84 -18
- package/dist/services/hooks.js +101 -250
- package/dist/services/memory-permissions.js +77 -69
- package/dist/services/migrations.js +1 -1
- 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 +93 -13
- package/dist/services/runs.js +128 -61
- package/dist/services/schema-validation.js +168 -7
- package/dist/services/sessions.js +97 -73
- package/dist/services/stage-lifecycle.js +737 -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,109 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { reconcileSessionCoverageRows } from "../domain/session-coverage.js";
|
|
3
4
|
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]);
|
|
5
|
+
const run = context.db.get("SELECT id, status, project_root, subject_type, subject_id, runtime_path, run_index_path, index_json FROM flow_runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
5
6
|
if (!run)
|
|
6
7
|
return;
|
|
7
8
|
const index = JSON.parse(run.index_json);
|
|
8
|
-
const
|
|
9
|
+
const sessionRows = context.db.all(`SELECT session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
|
|
9
10
|
created_at, updated_at, stopped_at, coverage_units_json
|
|
10
|
-
FROM flow_sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id`, [projectId, runId])
|
|
11
|
-
|
|
11
|
+
FROM flow_sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id`, [projectId, runId]);
|
|
12
|
+
const sessions = sessionRows.map(sessionProjection);
|
|
13
|
+
const coverage = reconcileSessionCoverageRows(sessionRows);
|
|
14
|
+
const sessionCoverage = {
|
|
15
|
+
status: coverage.status,
|
|
16
|
+
expected: coverage.expected_unit_ids,
|
|
17
|
+
observed: coverage.observed_unit_ids,
|
|
18
|
+
missing: coverage.missing_unit_ids,
|
|
19
|
+
diagnostics: coverage.diagnostics
|
|
20
|
+
};
|
|
21
|
+
const plan = run.subject_type === "protocol" ? planProjection(context, projectId, run.subject_id, run.project_root) : {};
|
|
22
|
+
const workers = workerProjection(context, projectId, runId);
|
|
23
|
+
if (JSON.stringify(index.sessions ?? []) === JSON.stringify(sessions)
|
|
24
|
+
&& JSON.stringify(index.session_coverage) === JSON.stringify(sessionCoverage)
|
|
25
|
+
&& JSON.stringify(index.plan_ref) === JSON.stringify(plan.plan_ref)
|
|
26
|
+
&& JSON.stringify(index.plan_progress) === JSON.stringify(plan.plan_progress)
|
|
27
|
+
&& JSON.stringify(index.workers ?? {}) === JSON.stringify(workers))
|
|
12
28
|
return;
|
|
13
29
|
index.sessions = sessions;
|
|
30
|
+
index.session_coverage = sessionCoverage;
|
|
31
|
+
if (plan.plan_ref)
|
|
32
|
+
index.plan_ref = plan.plan_ref;
|
|
33
|
+
if (plan.plan_progress)
|
|
34
|
+
index.plan_progress = plan.plan_progress;
|
|
35
|
+
index.workers = workers;
|
|
14
36
|
index.updated_at = context.now();
|
|
15
|
-
const runtimeRevision = typeof index.runtime_revision === "number" ? index.runtime_revision + 1 : 1;
|
|
16
|
-
index.runtime_revision = runtimeRevision;
|
|
17
37
|
const runtime = readJson(run.runtime_path);
|
|
18
|
-
|
|
19
|
-
runtime.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
const runtimeRevision = typeof runtime?.runtime_revision === "number"
|
|
39
|
+
? runtime.runtime_revision + 1
|
|
40
|
+
: typeof index.runtime_revision === "number"
|
|
41
|
+
? index.runtime_revision + 1
|
|
42
|
+
: 1;
|
|
43
|
+
index.runtime_revision = runtimeRevision;
|
|
44
|
+
const authoritative = runtime ?? { ...index, schema_id: "dd-flow/flow-run@2" };
|
|
45
|
+
authoritative.sessions = sessions;
|
|
46
|
+
authoritative.session_coverage = sessionCoverage;
|
|
47
|
+
if (plan.plan_ref)
|
|
48
|
+
authoritative.plan_ref = plan.plan_ref;
|
|
49
|
+
if (plan.plan_progress)
|
|
50
|
+
authoritative.plan_progress = plan.plan_progress;
|
|
51
|
+
authoritative.workers = workers;
|
|
52
|
+
authoritative.updated_at = index.updated_at;
|
|
53
|
+
authoritative.runtime_revision = runtimeRevision;
|
|
54
|
+
if (run.status !== "discarded" || fs.existsSync(run.runtime_path))
|
|
55
|
+
writeJson(run.runtime_path, authoritative);
|
|
25
56
|
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
57
|
}
|
|
58
|
+
function planProjection(context, projectId, protocolId, projectRoot) {
|
|
59
|
+
const binding = context.db.get("SELECT plan_path, plan_revision, plan_sha256 FROM plan_bindings WHERE project_id = ? AND protocol_id = ?", [projectId, protocolId]);
|
|
60
|
+
if (!binding)
|
|
61
|
+
return {};
|
|
62
|
+
const planPath = path.relative(projectRoot, binding.plan_path).split(path.sep).join("/");
|
|
63
|
+
const planId = readPlanId(binding.plan_path);
|
|
64
|
+
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, {
|
|
65
|
+
status: row.status,
|
|
66
|
+
...(row.summary ? { summary: row.summary } : {}),
|
|
67
|
+
...(parseStringArray(row.evidence_json).length > 0 ? { evidence: parseStringArray(row.evidence_json) } : {})
|
|
68
|
+
}]));
|
|
69
|
+
return {
|
|
70
|
+
plan_ref: {
|
|
71
|
+
path: planPath,
|
|
72
|
+
plan_id: planId,
|
|
73
|
+
revision: binding.plan_revision,
|
|
74
|
+
sha256: binding.plan_sha256
|
|
75
|
+
},
|
|
76
|
+
plan_progress: progress
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function readPlanId(planPath) {
|
|
80
|
+
try {
|
|
81
|
+
const value = JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
82
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.plan_id === "string") {
|
|
83
|
+
return value.plan_id;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// The canonical validator owns malformed-plan errors; projection stays bounded.
|
|
88
|
+
}
|
|
89
|
+
return "unknown-plan";
|
|
90
|
+
}
|
|
91
|
+
function workerProjection(context, projectId, runId) {
|
|
92
|
+
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, {
|
|
93
|
+
units: [job.plan_item_id],
|
|
94
|
+
status: { pending: "registered", done: "completed" }[job.status] ?? job.status,
|
|
95
|
+
...(job.worker_session_id ? { session_id: job.worker_session_id } : {})
|
|
96
|
+
}]));
|
|
97
|
+
}
|
|
98
|
+
function parseStringArray(value) {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(value);
|
|
101
|
+
return Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string") ? parsed : [];
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
27
107
|
function sessionProjection(row) {
|
|
28
108
|
return {
|
|
29
109
|
session_id: row.session_id,
|