@deksden-com/dd-flow-cli 0.2.0 → 0.3.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 (33) hide show
  1. package/README.md +2 -0
  2. package/dist/build-info.json +10 -6
  3. package/dist/cli/help.js +70 -13
  4. package/dist/cli/run-cli.js +140 -10
  5. package/dist/schemas/compatibility.schema.json +26 -0
  6. package/dist/schemas/flow-guidance.schema.json +73 -0
  7. package/dist/schemas/flow-run-index.schema.json +30 -4
  8. package/dist/schemas/global-dashboard-data.schema.json +68 -0
  9. package/dist/schemas/mb-sdlc-review-report.schema.json +242 -0
  10. package/dist/schemas/plan-stage-report.schema.json +83 -0
  11. package/dist/schemas/project-dashboard-data.schema.json +98 -0
  12. package/dist/schemas/project-flow-pack-manifest.schema.json +5 -1
  13. package/dist/schemas/protocol-dashboard-data.schema.json +89 -0
  14. package/dist/schemas/status-report.schema.json +34 -0
  15. package/dist/schemas/version-report.schema.json +22 -0
  16. package/dist/services/build-info.js +26 -3
  17. package/dist/services/canon.js +93 -22
  18. package/dist/services/cleanup.js +14 -1
  19. package/dist/services/config.js +25 -0
  20. package/dist/services/dashboard.js +655 -10
  21. package/dist/services/flow-guidance.js +214 -0
  22. package/dist/services/ids.js +106 -0
  23. package/dist/services/merge-queue.js +9 -1
  24. package/dist/services/merge-worker.js +31 -2
  25. package/dist/services/projects.js +7 -1
  26. package/dist/services/protocols.js +637 -6
  27. package/dist/services/runs.js +98 -21
  28. package/dist/services/schema-validation.js +84 -4
  29. package/dist/services/status.js +189 -1
  30. package/dist/services/version-status.js +28 -2
  31. package/dist/storage/database.js +6 -0
  32. package/dist/storage/paths.js +21 -0
  33. package/package.json +1 -1
@@ -0,0 +1,214 @@
1
+ import path from "node:path";
2
+ import { defaultFlowContract, flowContractForState } from "../domain/flow-contract.js";
3
+ export function buildProtocolFlowGuidance(input) {
4
+ const contract = flowContractForState(input.state);
5
+ const stageChain = latestRunStageChain(input.latestRun);
6
+ return buildFlowGuidance({
7
+ currentStage: input.state.stage,
8
+ contract,
9
+ stageChain,
10
+ runId: stringValue(input.latestRun?.id),
11
+ queueStatus: input.queueStatus ?? null
12
+ });
13
+ }
14
+ export function buildRunFlowGuidance(input) {
15
+ const contract = input.contract ?? defaultFlowContract;
16
+ const chain = input.stageRuns.map((stage) => `${String(stage.stage ?? "unknown")}:${String(stage.status ?? "unknown")}`);
17
+ const currentStage = input.protocolStage ?? inferStageFromRun(input.stageRuns);
18
+ return buildFlowGuidance({
19
+ currentStage,
20
+ contract,
21
+ stageChain: chain,
22
+ runId: input.runId ?? null,
23
+ runDir: input.runDir ?? null,
24
+ stageRuns: input.stageRuns
25
+ });
26
+ }
27
+ export function buildStaticFlowGuidance(input) {
28
+ return buildFlowGuidance({
29
+ currentStage: input.stage,
30
+ contract: input.contract ?? defaultFlowContract,
31
+ stageChain: [],
32
+ runId: null,
33
+ queueStatus: input.queueStatus ?? null
34
+ });
35
+ }
36
+ function buildFlowGuidance(input) {
37
+ const planDone = hasDoneStage(input.stageChain, "plan");
38
+ const codeDone = hasDoneStage(input.stageChain, "code") || hasDoneStage(input.stageChain, "implementation") || hasDoneStage(input.stageChain, "readiness");
39
+ const evidence = evidencePaths(input.runId, input.runDir ?? null, input.stageRuns);
40
+ if (input.currentStage === "closed" || input.currentStage === "cancelled") {
41
+ return guidance(input, {
42
+ action: "none",
43
+ prompt: "none",
44
+ evidence: [],
45
+ guards: [{ id: "flow_terminal", status: "not_applicable", summary: `Protocol is ${input.currentStage}.` }],
46
+ missing: []
47
+ });
48
+ }
49
+ if (input.currentStage === "ready_for_merge" || input.currentStage === "queued_for_merge") {
50
+ return guidance(input, {
51
+ action: input.currentStage === "queued_for_merge" ? "wait for or run merge worker" : "run merge flow",
52
+ prompt: ".memory-bank/dd-flow/merge.md",
53
+ evidence: evidence.code.length > 0 ? evidence.code : defaultEvidence(input.runId, input.runDir ?? null, "03-code"),
54
+ guards: [
55
+ {
56
+ id: "merge_requires_ready_for_merge",
57
+ status: "pass",
58
+ summary: input.queueStatus ? `Protocol is ${input.currentStage}; queue status is ${input.queueStatus}.` : `Protocol is ${input.currentStage}.`
59
+ }
60
+ ],
61
+ missing: []
62
+ });
63
+ }
64
+ if (input.currentStage === "integration") {
65
+ return guidance(input, {
66
+ action: "finish merge job",
67
+ prompt: ".memory-bank/dd-flow/merge/job.md",
68
+ evidence: evidence.code.length > 0 ? evidence.code : defaultEvidence(input.runId, input.runDir ?? null, "03-code"),
69
+ guards: [{ id: "merge_requires_ready_for_merge", status: "pass", summary: "Merge job is in integration stage." }],
70
+ missing: []
71
+ });
72
+ }
73
+ if (input.currentStage === "readiness") {
74
+ return guidance(input, {
75
+ action: "complete readiness and mark ready for merge",
76
+ prompt: ".memory-bank/dd-flow/code/readiness.md",
77
+ evidence: evidence.code.length > 0 ? evidence.code : defaultEvidence(input.runId, input.runDir ?? null, "03-code"),
78
+ 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." }],
79
+ missing: codeDone ? ["ready_for_merge transition"] : ["code stage report", "ready_for_merge transition"]
80
+ });
81
+ }
82
+ if (input.currentStage === "implementation" || input.currentStage === "hardening") {
83
+ return guidance(input, {
84
+ action: "continue code flow",
85
+ prompt: input.currentStage === "hardening" ? ".memory-bank/dd-flow/finish.md" : ".memory-bank/dd-flow/code.md",
86
+ evidence: evidence.plan.length > 0 ? evidence.plan : defaultEvidence(input.runId, input.runDir ?? null, "02-plan"),
87
+ guards: [{
88
+ id: "code_flow_requires_plan_ready",
89
+ status: planDone ? "pass" : "unknown",
90
+ summary: planDone ? "Plan stage evidence exists." : "Plan evidence was not found in the linked run."
91
+ }],
92
+ missing: planDone ? [] : ["plan stage report", "ready_for_code verdict"]
93
+ });
94
+ }
95
+ if (input.currentStage === "plan") {
96
+ return guidance(input, {
97
+ action: planDone ? "run code flow" : "complete plan flow",
98
+ prompt: planDone ? ".memory-bank/dd-flow/code.md" : ".memory-bank/dd-flow/plan.md",
99
+ evidence: evidence.plan.length > 0 ? evidence.plan : defaultEvidence(input.runId, input.runDir ?? null, "02-plan"),
100
+ guards: [{
101
+ id: "code_flow_requires_plan_ready",
102
+ status: planDone ? "pass" : "fail",
103
+ summary: planDone ? "Plan stage report exists in linked run evidence." : "Plan stage evidence is not complete."
104
+ }],
105
+ missing: planDone ? [] : ["plan stage report", "ready_for_code verdict"]
106
+ });
107
+ }
108
+ if (input.currentStage === "specify" || input.currentStage === "priming" || input.currentStage === "prime" || input.currentStage === "registered") {
109
+ const action = input.currentStage === "registered" ? "run protocol/specify flow" : "run plan flow after specification is complete";
110
+ return guidance(input, {
111
+ action,
112
+ prompt: input.currentStage === "registered" ? ".memory-bank/dd-flow/protocol.md" : ".memory-bank/dd-flow/plan.md",
113
+ evidence: [],
114
+ guards: [{
115
+ id: "plan_requires_protocol_and_specification",
116
+ status: input.currentStage === "registered" ? "unknown" : "pass",
117
+ summary: input.currentStage === "registered" ? "Protocol is registered; specification evidence must be checked by the prompt." : "Protocol has progressed past registration."
118
+ }],
119
+ missing: input.currentStage === "registered" ? ["specification evidence"] : []
120
+ });
121
+ }
122
+ if (input.currentStage === "blocked") {
123
+ return guidance(input, {
124
+ action: "resolve blocker and continue named safe flow",
125
+ prompt: "depends_on_blocker",
126
+ evidence: [],
127
+ guards: [{ id: "flow_blocked", status: "fail", summary: "Protocol is blocked." }],
128
+ missing: ["blocker resolution"]
129
+ });
130
+ }
131
+ if (input.currentStage === "waiting_for_user") {
132
+ return guidance(input, {
133
+ action: "collect user answer",
134
+ prompt: "active_protocol_context",
135
+ evidence: [],
136
+ guards: [{ id: "flow_waiting_for_user", status: "unknown", summary: "Protocol is waiting for user input." }],
137
+ missing: ["user answer"]
138
+ });
139
+ }
140
+ return guidance(input, {
141
+ action: `continue ${input.currentStage}`,
142
+ prompt: "active_flow_prompt",
143
+ evidence: [],
144
+ guards: [{ id: "flow_guidance_unknown_stage_policy", status: "unknown", summary: `No specific guidance rule for stage ${input.currentStage}.` }],
145
+ missing: []
146
+ });
147
+ }
148
+ function guidance(input, value) {
149
+ return {
150
+ current_stage: input.currentStage,
151
+ allowed_next_stages: input.contract.transitions[input.currentStage] ?? [],
152
+ recommended_next_action: value.action,
153
+ recommended_prompt: value.prompt,
154
+ required_predecessor_evidence: value.evidence,
155
+ guards: value.guards,
156
+ blocked_if_missing: value.missing
157
+ };
158
+ }
159
+ function latestRunStageChain(run) {
160
+ return Array.isArray(run?.stage_chain) ? run.stage_chain.filter((item) => typeof item === "string") : [];
161
+ }
162
+ function hasDoneStage(chain, stage) {
163
+ return chain.includes(`${stage}:done`);
164
+ }
165
+ function inferStageFromRun(stageRuns) {
166
+ const done = new Set(stageRuns.filter((stage) => stage.status === "done").map((stage) => stage.stage));
167
+ if (done.has("merge"))
168
+ return "closed";
169
+ if (done.has("code") || done.has("implementation") || done.has("readiness"))
170
+ return "ready_for_merge";
171
+ if (done.has("plan"))
172
+ return "plan";
173
+ return "registered";
174
+ }
175
+ function evidencePaths(runId, runDir, stageRuns) {
176
+ const result = { plan: [], code: [] };
177
+ if (!stageRuns)
178
+ return result;
179
+ for (const stage of stageRuns) {
180
+ const bucket = stage.stage === "plan" ? result.plan : ["code", "implementation", "readiness"].includes(String(stage.stage)) ? result.code : null;
181
+ if (!bucket)
182
+ continue;
183
+ const dir = stage.dir ? path.posix.join(runDir ?? legacyRunDir(runId), stage.dir) : stage.dir;
184
+ for (const field of [stage.data, stage.stage_report, stage.report]) {
185
+ if (field)
186
+ bucket.push(toEvidencePath(runId, runDir, stage.dir, field, dir));
187
+ }
188
+ }
189
+ return result;
190
+ }
191
+ function toEvidencePath(runId, runDir, stageDir, field, prefixedStageDir) {
192
+ if (!runId)
193
+ return field;
194
+ if (field.startsWith(".tasks/dd-flow-runs/"))
195
+ return field;
196
+ if (field.startsWith("runs/"))
197
+ return field;
198
+ if (stageDir && field.startsWith(`${stageDir}/`)) {
199
+ return path.posix.join(runDir ?? legacyRunDir(runId), field);
200
+ }
201
+ return prefixedStageDir ? path.posix.join(prefixedStageDir, field) : field;
202
+ }
203
+ function defaultEvidence(runId, runDir, dir) {
204
+ if (!runId)
205
+ return [`<RUN>/${dir}/stage-report.json`, `<RUN>/${dir}/stage-report.html`];
206
+ const base = path.posix.join(runDir ?? legacyRunDir(runId), dir);
207
+ return [path.posix.join(base, "stage-report.json"), path.posix.join(base, "stage-report.html")];
208
+ }
209
+ function legacyRunDir(runId) {
210
+ return runId ? path.posix.join(".tasks/dd-flow-runs", runId) : "<RUN>";
211
+ }
212
+ function stringValue(value) {
213
+ return typeof value === "string" && value.length > 0 ? value : null;
214
+ }
@@ -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
+ }
@@ -2,7 +2,7 @@ import { loadProjectFlowContract } from "../domain/flow-contract.js";
2
2
  import { AppError } from "../shared/errors.js";
3
3
  import { appendAudit } from "./audit.js";
4
4
  import { requireProjectByRoot } from "./projects.js";
5
- import { persistProtocolState, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
5
+ import { persistProtocolState, protocolRunDiagnostics, readProtocolRuntimeState, requireProtocol } from "./protocols.js";
6
6
  import { resolveProjectRoot } from "../storage/paths.js";
7
7
  import { acquireLaneLock, ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, requireLaneLockOwner } from "./lanes.js";
8
8
  import { stopMergeWorker, stoppedMergeWorkerState } from "./sessions.js";
@@ -349,6 +349,14 @@ function transitionClaimedProtocolToIntegration(context, protocolId, workerId, n
349
349
  allowed: ["ready_for_merge", "queued_for_merge"]
350
350
  });
351
351
  }
352
+ const diagnostics = protocolRunDiagnostics(context, protocol, state).diagnostics;
353
+ const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
354
+ if (blockingDiagnostics.length > 0) {
355
+ throw new AppError("merge_protocol_run_mismatch", "Cannot claim a protocol while protocol and run evidence disagree", 1, {
356
+ protocol_id: protocolId,
357
+ diagnostics: blockingDiagnostics
358
+ });
359
+ }
352
360
  persistProtocolState(context, protocol, {
353
361
  ...state,
354
362
  stage: "integration",
@@ -4,6 +4,8 @@ import { appendAudit } from "./audit.js";
4
4
  import { ensureLaneWorkspace, acquireLaneLock, releaseLaneLock, expireProjectLaneLocks } from "./lanes.js";
5
5
  import { claimNextMergeJob, queueForProject } from "./merge-queue.js";
6
6
  import { requireProjectByRoot } from "./projects.js";
7
+ import { buildStaticFlowGuidance } from "./flow-guidance.js";
8
+ import { readProtocolRuntimeState, requireProtocol } from "./protocols.js";
7
9
  import { registerFlowSession, stopMergeWorker } from "./sessions.js";
8
10
  export function getMergeWorkerStatus(context, input) {
9
11
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -11,7 +13,7 @@ export function getMergeWorkerStatus(context, input) {
11
13
  ok: true,
12
14
  project: { id: project.id, root: project.root },
13
15
  merge_worker: detectMergeWorkerState(context, project.id),
14
- queue: queueForProject(context, project.id)
16
+ queue: queueForProject(context, project.id).map((job) => withJobGuidance(context, { ...job }))
15
17
  };
16
18
  }
17
19
  export function startMergeWorker(context, input) {
@@ -101,7 +103,34 @@ export function oneShotMergeClaim(context, input) {
101
103
  reason: "one-shot merge no job"
102
104
  });
103
105
  }
104
- return { ok: true, claimed: Boolean(claimed.job), mode: "one_shot", job: claimed.job ?? null, lock: claimed.job ? lock.lock : null };
106
+ return {
107
+ ok: true,
108
+ claimed: Boolean(claimed.job),
109
+ mode: "one_shot",
110
+ job: claimed.job ? withJobGuidance(context, { ...claimed.job }) : null,
111
+ lock: claimed.job ? lock.lock : null,
112
+ flow_guidance: claimed.job ? withJobGuidance(context, { ...claimed.job }).flow_guidance : undefined
113
+ };
114
+ }
115
+ function withJobGuidance(context, job) {
116
+ const protocolId = typeof job.protocol_id === "string" ? job.protocol_id : "";
117
+ if (!protocolId)
118
+ return job;
119
+ try {
120
+ const protocol = requireProtocol(context, protocolId);
121
+ const state = readProtocolRuntimeState(context, protocol).state;
122
+ return {
123
+ ...job,
124
+ flow_guidance: buildStaticFlowGuidance({
125
+ stage: state.stage,
126
+ ...(state.flow_contract ? { contract: state.flow_contract } : {}),
127
+ queueStatus: String(job.status ?? "")
128
+ })
129
+ };
130
+ }
131
+ catch {
132
+ return job;
133
+ }
105
134
  }
106
135
  function detectMergeWorkerState(context, projectId) {
107
136
  expireProjectLaneLocks(context, projectId);
@@ -6,6 +6,8 @@ import { projectRuntimeRoot, resolveProjectRoot } from "../storage/paths.js";
6
6
  import { appendAudit } from "./audit.js";
7
7
  import { activeCodexSessionBindingsForProject, activeFlowSessionBindingsForProject, codexHomeProfilesForProject, codexHookEventsForProject, hookStatusForProject } from "./hooks.js";
8
8
  import { dashboardMarkdownPath, globalDashboardMarkdownPath, readProjectConfig } from "./config.js";
9
+ import { loadProjectFlowContract } from "../domain/flow-contract.js";
10
+ import { buildStaticFlowGuidance } from "./flow-guidance.js";
9
11
  export function registerProject(context, input) {
10
12
  const root = resolveProjectRoot(input.root);
11
13
  const existing = findProjectByRoot(context, root);
@@ -154,10 +156,14 @@ export function getProjectStatus(context, input) {
154
156
  if (!project) {
155
157
  throw new AppError("not_found", `Project is not registered: ${root}`, 1);
156
158
  }
159
+ const flowContract = loadProjectFlowContract(root);
157
160
  const protocols = context.db.all(`SELECT id, status, stage, next_action, updated_at
158
161
  FROM protocols
159
162
  WHERE project_id = ?
160
- ORDER BY updated_at DESC`, [project.id]);
163
+ ORDER BY updated_at DESC`, [project.id]).map((protocol) => ({
164
+ ...protocol,
165
+ flow_guidance: buildStaticFlowGuidance({ stage: String(protocol.stage), contract: flowContract })
166
+ }));
161
167
  const mergeQueue = context.db.all(`SELECT protocol_id, status, claimed_by_session_id, claimed_at, attempts_count, last_reason, completed_at,
162
168
  created_at, updated_at
163
169
  FROM merge_queue