@deksden-com/dd-flow-cli 0.2.0 → 0.3.1

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 (47) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +76 -5
  3. package/dist/build-info.json +10 -6
  4. package/dist/cli/help.js +165 -20
  5. package/dist/cli/run-cli.js +427 -17
  6. package/dist/schemas/compatibility.schema.json +105 -0
  7. package/dist/schemas/engine-manifest.schema.json +61 -0
  8. package/dist/schemas/flow-guidance.schema.json +90 -0
  9. package/dist/schemas/flow-run-index.schema.json +30 -4
  10. package/dist/schemas/global-dashboard-data.schema.json +126 -0
  11. package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
  12. package/dist/schemas/mb-upgrade-migration-report.schema.json +93 -0
  13. package/dist/schemas/plan-stage-report.schema.json +83 -0
  14. package/dist/schemas/project-dashboard-data.schema.json +122 -0
  15. package/dist/schemas/project-flow-pack-manifest.schema.json +5 -1
  16. package/dist/schemas/project-summary.schema.json +73 -0
  17. package/dist/schemas/protocol-dashboard-data.schema.json +112 -0
  18. package/dist/schemas/status-report.schema.json +38 -2
  19. package/dist/schemas/version-report.schema.json +22 -0
  20. package/dist/services/build-info.js +26 -3
  21. package/dist/services/canon.js +93 -22
  22. package/dist/services/cleanup.js +45 -1
  23. package/dist/services/cli-operation-classifier.js +104 -0
  24. package/dist/services/compatibility-preflight.js +124 -0
  25. package/dist/services/config.js +31 -0
  26. package/dist/services/dashboard-targets.js +95 -0
  27. package/dist/services/dashboard.js +972 -10
  28. package/dist/services/engines.js +532 -0
  29. package/dist/services/flow-guidance.js +221 -0
  30. package/dist/services/hooks.js +1 -1
  31. package/dist/services/ids.js +106 -0
  32. package/dist/services/lanes.js +333 -1
  33. package/dist/services/merge-queue.js +106 -16
  34. package/dist/services/merge-worker.js +67 -4
  35. package/dist/services/migrations.js +231 -0
  36. package/dist/services/project-summary.js +122 -0
  37. package/dist/services/projects.js +44 -3
  38. package/dist/services/protocol-lifecycle.js +144 -0
  39. package/dist/services/protocols.js +660 -7
  40. package/dist/services/runs.js +98 -21
  41. package/dist/services/schema-validation.js +84 -4
  42. package/dist/services/sessions.js +21 -4
  43. package/dist/services/status.js +199 -1
  44. package/dist/services/version-status.js +59 -9
  45. package/dist/storage/database.js +31 -0
  46. package/dist/storage/paths.js +33 -0
  47. package/package.json +3 -2
@@ -0,0 +1,221 @@
1
+ import path from "node:path";
2
+ import { defaultFlowContract, flowContractForState } from "../domain/flow-contract.js";
3
+ import { normalizeProtocolLifecycle } from "./protocol-lifecycle.js";
4
+ export function buildProtocolFlowGuidance(input) {
5
+ const contract = flowContractForState(input.state);
6
+ const lifecycle = normalizeProtocolLifecycle({ state: input.state, queueStatus: input.queueStatus ?? null, flowContract: contract });
7
+ const stageChain = latestRunStageChain(input.latestRun);
8
+ return buildFlowGuidance({
9
+ currentStage: input.state.stage,
10
+ lifecycle,
11
+ contract,
12
+ stageChain,
13
+ runId: stringValue(input.latestRun?.id),
14
+ queueStatus: input.queueStatus ?? null
15
+ });
16
+ }
17
+ export function buildRunFlowGuidance(input) {
18
+ const contract = input.contract ?? defaultFlowContract;
19
+ const chain = input.stageRuns.map((stage) => `${String(stage.stage ?? "unknown")}:${String(stage.status ?? "unknown")}`);
20
+ const currentStage = input.protocolStage ?? inferStageFromRun(input.stageRuns);
21
+ return buildFlowGuidance({
22
+ currentStage,
23
+ lifecycle: normalizeProtocolLifecycle({ rawStage: currentStage, rawStatus: "running", flowContract: contract }),
24
+ contract,
25
+ stageChain: chain,
26
+ runId: input.runId ?? null,
27
+ runDir: input.runDir ?? null,
28
+ stageRuns: input.stageRuns
29
+ });
30
+ }
31
+ export function buildStaticFlowGuidance(input) {
32
+ const contract = input.contract ?? defaultFlowContract;
33
+ return buildFlowGuidance({
34
+ currentStage: input.stage,
35
+ lifecycle: normalizeProtocolLifecycle({ rawStage: input.stage, rawStatus: input.status ?? "running", queueStatus: input.queueStatus ?? null, flowContract: contract }),
36
+ contract,
37
+ stageChain: [],
38
+ runId: null,
39
+ queueStatus: input.queueStatus ?? null
40
+ });
41
+ }
42
+ function buildFlowGuidance(input) {
43
+ const planDone = hasDoneStage(input.stageChain, "plan");
44
+ const codeDone = hasDoneStage(input.stageChain, "code") || hasDoneStage(input.stageChain, "implementation") || hasDoneStage(input.stageChain, "readiness");
45
+ const evidence = evidencePaths(input.runId, input.runDir ?? null, input.stageRuns);
46
+ if (input.currentStage === "closed" || input.currentStage === "cancelled") {
47
+ return guidance(input, {
48
+ action: "none",
49
+ prompt: "none",
50
+ evidence: [],
51
+ guards: [{ id: "flow_terminal", status: "not_applicable", summary: `Protocol is ${input.currentStage}.` }],
52
+ missing: []
53
+ });
54
+ }
55
+ if (input.currentStage === "ready_for_merge" || input.currentStage === "queued_for_merge") {
56
+ return guidance(input, {
57
+ action: input.currentStage === "queued_for_merge" ? "wait for or run merge worker" : "run merge flow",
58
+ prompt: ".memory-bank/dd-flow/merge.md",
59
+ evidence: evidence.code.length > 0 ? evidence.code : defaultEvidence(input.runId, input.runDir ?? null, "03-code"),
60
+ guards: [
61
+ {
62
+ id: "merge_requires_ready_for_merge",
63
+ status: "pass",
64
+ summary: input.queueStatus ? `Protocol is ${input.currentStage}; queue status is ${input.queueStatus}.` : `Protocol is ${input.currentStage}.`
65
+ }
66
+ ],
67
+ missing: []
68
+ });
69
+ }
70
+ if (input.currentStage === "integration") {
71
+ return guidance(input, {
72
+ action: "finish merge job",
73
+ prompt: ".memory-bank/dd-flow/merge/job.md",
74
+ evidence: evidence.code.length > 0 ? evidence.code : defaultEvidence(input.runId, input.runDir ?? null, "03-code"),
75
+ guards: [{ id: "merge_requires_ready_for_merge", status: "pass", summary: "Merge job is in integration stage." }],
76
+ missing: []
77
+ });
78
+ }
79
+ if (input.currentStage === "readiness") {
80
+ return guidance(input, {
81
+ action: "complete readiness and mark ready for merge",
82
+ prompt: ".memory-bank/dd-flow/code/readiness.md",
83
+ evidence: evidence.code.length > 0 ? evidence.code : defaultEvidence(input.runId, input.runDir ?? null, "03-code"),
84
+ guards: [{ id: "merge_requires_ready_for_merge", status: codeDone ? "unknown" : "fail", summary: codeDone ? "Code evidence exists; readiness must still be completed." : "Code evidence is missing." }],
85
+ missing: codeDone ? ["ready_for_merge transition"] : ["code stage report", "ready_for_merge transition"]
86
+ });
87
+ }
88
+ if (input.currentStage === "implementation" || input.currentStage === "hardening") {
89
+ return guidance(input, {
90
+ action: "continue code flow",
91
+ prompt: input.currentStage === "hardening" ? ".memory-bank/dd-flow/finish.md" : ".memory-bank/dd-flow/code.md",
92
+ evidence: evidence.plan.length > 0 ? evidence.plan : defaultEvidence(input.runId, input.runDir ?? null, "02-plan"),
93
+ guards: [{
94
+ id: "code_flow_requires_plan_ready",
95
+ status: planDone ? "pass" : "unknown",
96
+ summary: planDone ? "Plan stage evidence exists." : "Plan evidence was not found in the linked run."
97
+ }],
98
+ missing: planDone ? [] : ["plan stage report", "ready_for_code verdict"]
99
+ });
100
+ }
101
+ if (input.currentStage === "plan") {
102
+ return guidance(input, {
103
+ action: planDone ? "run code flow" : "complete plan flow",
104
+ prompt: planDone ? ".memory-bank/dd-flow/code.md" : ".memory-bank/dd-flow/plan.md",
105
+ evidence: evidence.plan.length > 0 ? evidence.plan : defaultEvidence(input.runId, input.runDir ?? null, "02-plan"),
106
+ guards: [{
107
+ id: "code_flow_requires_plan_ready",
108
+ status: planDone ? "pass" : "fail",
109
+ summary: planDone ? "Plan stage report exists in linked run evidence." : "Plan stage evidence is not complete."
110
+ }],
111
+ missing: planDone ? [] : ["plan stage report", "ready_for_code verdict"]
112
+ });
113
+ }
114
+ if (input.currentStage === "specify" || input.currentStage === "priming" || input.currentStage === "prime" || input.currentStage === "registered") {
115
+ const action = input.currentStage === "registered" ? "run protocol/specify flow" : "run plan flow after specification is complete";
116
+ return guidance(input, {
117
+ action,
118
+ prompt: input.currentStage === "registered" ? ".memory-bank/dd-flow/protocol.md" : ".memory-bank/dd-flow/plan.md",
119
+ evidence: [],
120
+ guards: [{
121
+ id: "plan_requires_protocol_and_specification",
122
+ status: input.currentStage === "registered" ? "unknown" : "pass",
123
+ summary: input.currentStage === "registered" ? "Protocol is registered; specification evidence must be checked by the prompt." : "Protocol has progressed past registration."
124
+ }],
125
+ missing: input.currentStage === "registered" ? ["specification evidence"] : []
126
+ });
127
+ }
128
+ if (input.currentStage === "blocked") {
129
+ return guidance(input, {
130
+ action: "resolve blocker and continue named safe flow",
131
+ prompt: "depends_on_blocker",
132
+ evidence: [],
133
+ guards: [{ id: "flow_blocked", status: "fail", summary: "Protocol is blocked." }],
134
+ missing: ["blocker resolution"]
135
+ });
136
+ }
137
+ if (input.currentStage === "waiting_for_user") {
138
+ return guidance(input, {
139
+ action: "collect user answer",
140
+ prompt: "active_protocol_context",
141
+ evidence: [],
142
+ guards: [{ id: "flow_waiting_for_user", status: "unknown", summary: "Protocol is waiting for user input." }],
143
+ missing: ["user answer"]
144
+ });
145
+ }
146
+ return guidance(input, {
147
+ action: `continue ${input.currentStage}`,
148
+ prompt: "active_flow_prompt",
149
+ evidence: [],
150
+ guards: [{ id: "flow_guidance_unknown_stage_policy", status: "unknown", summary: `No specific guidance rule for stage ${input.currentStage}.` }],
151
+ missing: []
152
+ });
153
+ }
154
+ function guidance(input, value) {
155
+ return {
156
+ current_stage: input.currentStage,
157
+ ...(input.lifecycle ? { lifecycle: input.lifecycle } : {}),
158
+ allowed_next_stages: input.contract.transitions[input.currentStage] ?? [],
159
+ recommended_next_action: value.action,
160
+ recommended_prompt: value.prompt,
161
+ required_predecessor_evidence: value.evidence,
162
+ guards: value.guards,
163
+ blocked_if_missing: value.missing
164
+ };
165
+ }
166
+ function latestRunStageChain(run) {
167
+ return Array.isArray(run?.stage_chain) ? run.stage_chain.filter((item) => typeof item === "string") : [];
168
+ }
169
+ function hasDoneStage(chain, stage) {
170
+ return chain.includes(`${stage}:done`);
171
+ }
172
+ function inferStageFromRun(stageRuns) {
173
+ const done = new Set(stageRuns.filter((stage) => stage.status === "done").map((stage) => stage.stage));
174
+ if (done.has("merge"))
175
+ return "closed";
176
+ if (done.has("code") || done.has("implementation") || done.has("readiness"))
177
+ return "ready_for_merge";
178
+ if (done.has("plan"))
179
+ return "plan";
180
+ return "registered";
181
+ }
182
+ function evidencePaths(runId, runDir, stageRuns) {
183
+ const result = { plan: [], code: [] };
184
+ if (!stageRuns)
185
+ return result;
186
+ for (const stage of stageRuns) {
187
+ const bucket = stage.stage === "plan" ? result.plan : ["code", "implementation", "readiness"].includes(String(stage.stage)) ? result.code : null;
188
+ if (!bucket)
189
+ continue;
190
+ const dir = stage.dir ? path.posix.join(runDir ?? legacyRunDir(runId), stage.dir) : stage.dir;
191
+ for (const field of [stage.data, stage.stage_report, stage.report]) {
192
+ if (field)
193
+ bucket.push(toEvidencePath(runId, runDir, stage.dir, field, dir));
194
+ }
195
+ }
196
+ return result;
197
+ }
198
+ function toEvidencePath(runId, runDir, stageDir, field, prefixedStageDir) {
199
+ if (!runId)
200
+ return field;
201
+ if (field.startsWith(".tasks/dd-flow-runs/"))
202
+ return field;
203
+ if (field.startsWith("runs/"))
204
+ return field;
205
+ if (stageDir && field.startsWith(`${stageDir}/`)) {
206
+ return path.posix.join(runDir ?? legacyRunDir(runId), field);
207
+ }
208
+ return prefixedStageDir ? path.posix.join(prefixedStageDir, field) : field;
209
+ }
210
+ function defaultEvidence(runId, runDir, dir) {
211
+ if (!runId)
212
+ return [`<RUN>/${dir}/stage-report.json`, `<RUN>/${dir}/stage-report.html`];
213
+ const base = path.posix.join(runDir ?? legacyRunDir(runId), dir);
214
+ return [path.posix.join(base, "stage-report.json"), path.posix.join(base, "stage-report.html")];
215
+ }
216
+ function legacyRunDir(runId) {
217
+ return runId ? path.posix.join(".tasks/dd-flow-runs", runId) : "<RUN>";
218
+ }
219
+ function stringValue(value) {
220
+ return typeof value === "string" && value.length > 0 ? value : null;
221
+ }
@@ -801,7 +801,7 @@ function mergeQueueMutation(command) {
801
801
  return match?.[1] ?? null;
802
802
  }
803
803
  function mergeLaneLockMutation(command) {
804
- return /\bdd-flow\s+lane\s+lock\s+(acquire|heartbeat|release|wait)\b/.test(command) && /(?:--lane(?:\s+|=)(?:"merge"|'merge'|merge)\b)/.test(command);
804
+ return /\bdd-flow\s+lane\s+lock\s+(acquire|heartbeat|release|wait|wait-acquire)\b/.test(command) && /(?:--lane(?:\s+|=)(?:"merge"|'merge'|merge)\b)/.test(command);
805
805
  }
806
806
  function selfWorktreeRemovalTarget(command, cwd) {
807
807
  if (!/\bgit\s+worktree\s+remove\b/.test(command)) {
@@ -0,0 +1,106 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { formatFullId, parseFullEntityId } from "../domain/entity-ids.js";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { projectRunHome, resolveProjectRoot } from "../storage/paths.js";
6
+ const kindConfig = {
7
+ protocol: { type: "PRT", table: "protocols", fileRoot: ".memory-bank/protocol" },
8
+ run: { type: "RUN", table: "flow_runs", fileRoot: ".tasks/dd-flow-runs" }
9
+ };
10
+ export function previewNextEntityId(context, input) {
11
+ const projectRoot = resolveProjectRoot(input.projectRoot);
12
+ const kind = parseEntityKind(input.type);
13
+ const slug = normalizeSlug(input.slug);
14
+ const config = kindConfig[kind];
15
+ const used = new Set();
16
+ for (const id of databaseIds(context, config.table, config.type)) {
17
+ addSequence(used, id, config.type);
18
+ }
19
+ for (const id of filesystemIds(projectRoot, config.fileRoot, config.type)) {
20
+ addSequence(used, id, config.type);
21
+ }
22
+ if (kind === "run") {
23
+ for (const id of homeRunIds(context, projectRoot)) {
24
+ addSequence(used, id, config.type);
25
+ }
26
+ for (const id of filesystemIds(projectRoot, ".tasks", config.type)) {
27
+ addSequence(used, id, config.type);
28
+ }
29
+ }
30
+ for (let sequence = 1; sequence <= 999; sequence += 1) {
31
+ if (used.has(sequence))
32
+ continue;
33
+ const id = formatFullId(config.type, sequence, slug);
34
+ const parsed = parseFullEntityId(id);
35
+ return {
36
+ ok: true,
37
+ project_root: projectRoot,
38
+ entity: {
39
+ kind,
40
+ type: config.type,
41
+ id,
42
+ short_id: parsed.shortId,
43
+ slug,
44
+ sequence,
45
+ reserved: false
46
+ }
47
+ };
48
+ }
49
+ throw new AppError("validation", `${config.type} id sequence exhausted`, 2);
50
+ }
51
+ function homeRunIds(context, projectRoot) {
52
+ const project = context.db.get("SELECT id FROM projects WHERE root = ? AND status = 'active'", [projectRoot]);
53
+ if (!project) {
54
+ return [];
55
+ }
56
+ const root = projectRunHome(context.ddFlowHome, project.id, "__placeholder__").replace(/__placeholder__$/, "");
57
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
58
+ return [];
59
+ }
60
+ return fs.readdirSync(root).filter((entry) => entry.startsWith("RUN-"));
61
+ }
62
+ function parseEntityKind(value) {
63
+ const normalized = value.toLowerCase();
64
+ if (normalized === "protocol" || normalized === "prt")
65
+ return "protocol";
66
+ if (normalized === "run")
67
+ return "run";
68
+ throw new AppError("validation", "--type must be protocol or run", 2, { type: value });
69
+ }
70
+ function databaseIds(context, table, type) {
71
+ return context.db.all(`SELECT id FROM ${table} WHERE id LIKE ?`, [`${type}-%`]).map((row) => row.id);
72
+ }
73
+ function filesystemIds(projectRoot, relativeRoot, type) {
74
+ const root = path.join(projectRoot, relativeRoot);
75
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
76
+ return [];
77
+ }
78
+ return fs
79
+ .readdirSync(root)
80
+ .filter((entry) => entry.startsWith(`${type}-`))
81
+ .map((entry) => entry.replace(/\.md$/, ""));
82
+ }
83
+ function addSequence(used, id, type) {
84
+ try {
85
+ const parsed = parseFullEntityId(id);
86
+ if (parsed.type !== type)
87
+ return;
88
+ used.add(Number(parsed.shortId.slice(type.length + 1)));
89
+ }
90
+ catch {
91
+ return;
92
+ }
93
+ }
94
+ function normalizeSlug(value) {
95
+ const slug = value
96
+ .normalize("NFKD")
97
+ .replace(/[\u0300-\u036f]/g, "")
98
+ .toLowerCase()
99
+ .replace(/[^a-z0-9]+/g, "-")
100
+ .replace(/^-+|-+$/g, "")
101
+ .replace(/-{2,}/g, "-");
102
+ if (!slug) {
103
+ throw new AppError("validation", "--slug must contain at least one ASCII letter or digit", 2);
104
+ }
105
+ return slug;
106
+ }