@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +33 -4
  2. package/README.md +2 -2
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/help.js +16 -4
  5. package/dist/cli/run-cli.js +42 -17
  6. package/dist/domain/flow-contract.js +82 -3
  7. package/dist/domain/session-coverage.js +88 -0
  8. package/dist/domain/validation.js +56 -28
  9. package/dist/protocol/local-files.js +1 -16
  10. package/dist/schemas/code-stage-report.schema.json +2 -2
  11. package/dist/schemas/flow-contract.schema.json +152 -94
  12. package/dist/schemas/flow-run.schema.json +129 -23
  13. package/dist/schemas/mb-upgrade-review-data.schema.json +2 -2
  14. package/dist/schemas/memorybank-permissions-preflight.schema.json +13 -73
  15. package/dist/schemas/merge-stage-report.schema.json +2 -2
  16. package/dist/schemas/plan-stage-report.schema.json +38 -335
  17. package/dist/schemas/project-flow-pack-manifest.schema.json +4 -4
  18. package/dist/schemas/protocol-plan.schema.json +197 -0
  19. package/dist/schemas/release-impact.schema.json +9 -5
  20. package/dist/schemas/session-usage.schema.json +16 -0
  21. package/dist/schemas/stage-finish-input.schema.json +20 -0
  22. package/dist/schemas/stage-prompt.schema.json +35 -0
  23. package/dist/schemas/stage-report.schema.json +20 -0
  24. package/dist/schemas/stage-start-response.schema.json +30 -0
  25. package/dist/schemas/timeline-event.schema.json +29 -0
  26. package/dist/schemas/worktrunk-workspace.schema.json +19 -0
  27. package/dist/services/branch-context.js +9 -4
  28. package/dist/services/cleanup.js +77 -0
  29. package/dist/services/dashboard.js +51 -26
  30. package/dist/services/engines.js +84 -18
  31. package/dist/services/hooks.js +101 -250
  32. package/dist/services/memory-permissions.js +77 -69
  33. package/dist/services/migrations.js +1 -1
  34. package/dist/services/plan-runtime.js +124 -0
  35. package/dist/services/plans.js +22 -84
  36. package/dist/services/projects.js +2 -1
  37. package/dist/services/prompts.js +26 -21
  38. package/dist/services/protocols.js +29 -25
  39. package/dist/services/run-projection.js +93 -13
  40. package/dist/services/runs.js +128 -61
  41. package/dist/services/schema-validation.js +168 -7
  42. package/dist/services/sessions.js +97 -73
  43. package/dist/services/stage-lifecycle.js +737 -0
  44. package/dist/services/tooling.js +285 -0
  45. package/dist/services/usage.js +183 -30
  46. package/dist/services/version-status.js +1 -1
  47. package/dist/services/worktrees.js +88 -39
  48. package/dist/storage/database.js +72 -30
  49. package/dist/storage/paths.js +0 -9
  50. package/package.json +14 -13
  51. package/tools/worktrunk-manifest.json +34 -0
  52. package/dist/schemas/flow-run-index-v3.schema.json +0 -203
  53. 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, resolveProjectRoot, runtimePlanJsonPath, runtimeProtocolDir, runtimeStateJsonPath } from "../storage/paths.js";
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, readPlanFile, readStateFile, writeState } from "../protocol/local-files.js";
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
- runtimePlanJsonPath(context.ddFlowHome, project.id, protocolId),
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 = readPlanFile(protocol.plan_path);
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 ? summarizePlan(plan) : state.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
- if (state.active_def.length > 0) {
338
- throw new AppError("readiness_blocked", "Protocol has open deferrals", 1, { active_def: state.active_def });
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 = readPlanFile(protocol.plan_path);
341
- const openRequired = plan?.items.filter((item) => item.required && !["done", "skipped"].includes(item.status)) ?? [];
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 = runtimePlanJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
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.db.get("SELECT plan_json FROM plans WHERE project_id = ? AND protocol_id = ?", [protocol.project_id, protocol.id]);
756
- const planSummary = plan
757
- ? planSummaryForState(JSON.parse(plan.plan_json))
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: planSummary,
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 = runtimePlanJsonPath(context.ddFlowHome, protocol.project_id, protocol.id);
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 summarizePlan(plan) {
1166
- return planSummaryForState(plan);
1167
- }
1168
- function planSummaryForState(plan) {
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 sessions = context.db.all(`SELECT session_id, parent_session_id, role, session_kind, worker_id, current_stage, status,
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]).map(sessionProjection);
11
- if (JSON.stringify(index.sessions ?? []) === JSON.stringify(sessions))
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
- if (runtime) {
19
- runtime.sessions = sessions;
20
- runtime.updated_at = index.updated_at;
21
- runtime.runtime_revision = runtimeRevision;
22
- }
23
- writeJson(run.run_index_path, index);
24
- writeJson(run.runtime_path, runtime ?? index);
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,