@deksden-com/dd-flow-cli 0.6.0 → 0.8.0-beta.135

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 (78) hide show
  1. package/CHANGELOG.md +636 -0
  2. package/README.md +13 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +108 -18
  5. package/dist/cli/run-cli.js +626 -49
  6. package/dist/domain/flow-contract.js +11 -0
  7. package/dist/domain/stage-catalog.js +22 -0
  8. package/dist/runtime/context.js +8 -2
  9. package/dist/schemas/code-review-decision.schema.json +26 -0
  10. package/dist/schemas/code-review-result.schema.json +14 -0
  11. package/dist/schemas/code-stage-report.schema.json +7 -2
  12. package/dist/schemas/code-verification.schema.json +14 -0
  13. package/dist/schemas/code-work-batch.schema.json +24 -0
  14. package/dist/schemas/code-work-result.schema.json +16 -0
  15. package/dist/schemas/engine-manifest.schema.json +22 -0
  16. package/dist/schemas/flow-contract.schema.json +6 -3
  17. package/dist/schemas/flow-run.schema.json +16 -122
  18. package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
  19. package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
  20. package/dist/schemas/plan-aspect-map.schema.json +22 -0
  21. package/dist/schemas/plan-review-decision.schema.json +14 -0
  22. package/dist/schemas/plan-review-result.schema.json +42 -0
  23. package/dist/schemas/protocol-plan.schema.json +15 -182
  24. package/dist/schemas/run-engine-binding.schema.json +37 -0
  25. package/dist/schemas/stage-finish-input.schema.json +16 -2
  26. package/dist/schemas/stage-prompt.schema.json +4 -4
  27. package/dist/schemas/stage-report.schema.json +8 -7
  28. package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
  29. package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
  30. package/dist/schemas/vnext-specify.schema.json +45 -0
  31. package/dist/services/branch-context.js +1 -1
  32. package/dist/services/canon.js +15 -1
  33. package/dist/services/cleanup.js +8 -8
  34. package/dist/services/cli-operation-classifier.js +60 -8
  35. package/dist/services/code-checks.js +244 -0
  36. package/dist/services/compatibility-preflight.js +1 -1
  37. package/dist/services/config.js +7 -1
  38. package/dist/services/dashboard.js +14 -14
  39. package/dist/services/engines.js +408 -30
  40. package/dist/services/eval-snapshots.js +404 -0
  41. package/dist/services/hooks.js +775 -23
  42. package/dist/services/ids.js +16 -6
  43. package/dist/services/lanes.js +1 -5
  44. package/dist/services/merge-queue.js +53 -5
  45. package/dist/services/merge-worker.js +5 -6
  46. package/dist/services/migrations.js +307 -44
  47. package/dist/services/plan-runtime.js +5 -5
  48. package/dist/services/plans.js +5 -3
  49. package/dist/services/projects.js +4 -4
  50. package/dist/services/prompts.js +1 -1
  51. package/dist/services/protocols.js +31 -10
  52. package/dist/services/run-engine-bindings.js +157 -0
  53. package/dist/services/run-projection.js +49 -13
  54. package/dist/services/runs.js +525 -58
  55. package/dist/services/schema-validation.js +116 -2
  56. package/dist/services/sessions.js +51 -12
  57. package/dist/services/stage-blocker.js +57 -0
  58. package/dist/services/stage-context.js +90 -0
  59. package/dist/services/stage-lifecycle.js +288 -77
  60. package/dist/services/stage-pause.js +175 -0
  61. package/dist/services/stage-report-renderer.js +65 -0
  62. package/dist/services/status.js +8 -3
  63. package/dist/services/usage.js +526 -18
  64. package/dist/services/vnext-code-review.js +308 -0
  65. package/dist/services/vnext-code.js +616 -0
  66. package/dist/services/vnext-contracts.js +1 -0
  67. package/dist/services/vnext-execution-profile.js +27 -0
  68. package/dist/services/vnext-fanout.js +79 -0
  69. package/dist/services/vnext-plan-review.js +499 -0
  70. package/dist/services/vnext-plan.js +576 -0
  71. package/dist/services/vnext-protocolize.js +542 -0
  72. package/dist/services/vnext-specify.js +595 -0
  73. package/dist/services/vnext-workspace-policy.js +87 -0
  74. package/dist/services/work-registry.js +499 -0
  75. package/dist/services/worktrees.js +58 -37
  76. package/dist/storage/database.js +292 -42
  77. package/dist/storage/paths.js +47 -1
  78. package/package.json +12 -12
@@ -17,6 +17,7 @@ import { preflightMemoryPermissions } from "./memory-permissions.js";
17
17
  import { registerProject } from "./projects.js";
18
18
  import { registerProtocol } from "./protocols.js";
19
19
  import { previewNextEntityId } from "./ids.js";
20
+ import { hookSessionIdentity, sessionIdForHookEvent } from "./hooks.js";
20
21
  const stagePromptSections = [
21
22
  "stage_identity",
22
23
  "authoritative_runtime_facts",
@@ -92,11 +93,26 @@ function runtimeFacts(view, projectRoot) {
92
93
  workspace_root: view.run.workspace_root,
93
94
  run_id: view.run.id,
94
95
  protocol_id: view.run.subject.id,
96
+ runtime: {
97
+ source: "dd-flow",
98
+ state: "trusted",
99
+ flow_kind: view.run.flow_kind
100
+ },
95
101
  git: view.index.execution?.git ?? { branch: null, head: null, status: "unavailable" }
96
102
  };
97
103
  }
98
104
  export function startStage(context, input) {
105
+ if (input.requireSessionBinding && !input.hookEventId) {
106
+ throw new AppError("trusted_session_binding_required", "This stage requires a trusted harness lifecycle binding; run it from a controlled Session with the dd-flow adapter enabled", 1);
107
+ }
99
108
  const projectRoot = resolveProjectRoot(input.projectRoot);
109
+ let requiredSessionId = null;
110
+ let requiredIdentity = null;
111
+ if (input.requireSessionBinding) {
112
+ registerProject(context, { root: projectRoot });
113
+ requiredIdentity = hookSessionIdentity(context, requireProjectByRoot(context, projectRoot).id, input.hookEventId);
114
+ requiredSessionId = requiredIdentity.sessionId;
115
+ }
100
116
  const runId = input.bootstrap ? bootstrapStageRun(context, projectRoot, input) : input.runId;
101
117
  if (!runId)
102
118
  throw new AppError("usage", "stage start requires a RUN id or --bootstrap", 2);
@@ -117,12 +133,25 @@ export function startStage(context, input) {
117
133
  const preflight = stagePreflight(projectRoot, stageRoot);
118
134
  if (preflight.ok !== true)
119
135
  throw new AppError("permission_preflight_failed", "Stage workspace is not writable", 1, { preflight });
120
- syncProtocolLifecycle(context, attached.run.project_id, attached.run.subject.id, input.stage, "running");
121
- if (input.sessionId) {
136
+ if (attached.run.subject.type === "protocol") {
137
+ syncProtocolLifecycle(context, attached.run.project_id, attached.run.subject.id, input.stage, "running");
138
+ }
139
+ const sessionId = requiredSessionId ?? (input.hookEventId ? sessionIdForHookEvent(context, attached.run.project_id, input.hookEventId) : null);
140
+ if (sessionId) {
141
+ const identity = requiredIdentity ?? (input.hookEventId ? hookSessionIdentity(context, attached.run.project_id, input.hookEventId) : null);
122
142
  registerFlowSession(context, {
123
- sessionId: input.sessionId,
143
+ sessionId,
124
144
  payloadJson: JSON.stringify({
125
145
  project_root: projectRoot,
146
+ harness: identity?.harness ?? "codex-desktop",
147
+ provider_session_id: identity?.providerSessionId ?? sessionId,
148
+ agent_id: identity?.agentId ?? null,
149
+ provider: identity?.provider ?? null,
150
+ model: identity?.model ?? null,
151
+ reasoning: identity?.reasoning ?? null,
152
+ mode: identity?.mode ?? null,
153
+ agent_type: identity?.agentType ?? null,
154
+ parent_session_id: identity?.parentSessionId ?? null,
126
155
  flow_kind: flowKindForStage(input.stage),
127
156
  run_id: attached.run.id,
128
157
  protocol_id: attached.run.subject.id,
@@ -138,11 +167,16 @@ export function startStage(context, input) {
138
167
  const promptPath = path.join(stageRoot, "stage-prompt.md");
139
168
  const prompt = composeStagePrompt(context, projectRoot, before, attached, input.stage, dir, preflight);
140
169
  atomicWrite(promptPath, prompt);
170
+ const semanticInputPath = path.join(stageRoot, "stage-input.json");
171
+ if (!fs.existsSync(semanticInputPath)) {
172
+ atomicWrite(semanticInputPath, semanticInputTemplate(input.stage));
173
+ }
141
174
  const promptDataPath = path.join(stageRoot, "stage-prompt.json");
142
175
  const planPath = planJsonPath(projectRoot, attached.run.subject.id);
143
176
  const aspectMapPath = path.join(stageRoot, "aspect-map.json");
144
177
  const attempt = attached.index.stage_runs?.find((stage) => stage.stage === input.stage)?.attempt ?? "try-001";
145
178
  const attemptNumber = Number(attempt.replace("try-", "")) || 1;
179
+ const sources = stageInstructionSources(context, projectRoot, attached.run.flow_kind, input.stage);
146
180
  atomicWrite(promptDataPath, {
147
181
  schema_id: "dd-flow/stage-prompt@2",
148
182
  run_id: attached.run.id,
@@ -157,16 +191,32 @@ export function startStage(context, input) {
157
191
  stage: stageRoot,
158
192
  protocol: attached.run.subject.id,
159
193
  intake: path.join(runHome, "intake"),
194
+ input: semanticInputPath,
160
195
  ...(input.stage === "plan" ? { plan: planPath, "aspect-map": aspectMapPath } : {})
161
196
  },
162
197
  write_boundary: { current: "@stage", archive: attempt, archive_writable: false },
163
- source_fragments: stageInstructionSources(context, projectRoot, attached.run.flow_kind, input.stage).map((source) => source.label),
198
+ source_fragments: sources.map((source) => source.label),
164
199
  authoritative_facts: runtimeFacts(attached, projectRoot),
165
- preflight,
166
- required_context: stageInstructionSources(context, projectRoot, attached.run.flow_kind, input.stage).map((source) => source.label),
200
+ preflight: {
201
+ compatibility: {
202
+ status: "checked",
203
+ operation: `stage.${input.stage}`,
204
+ project_root: projectRoot
205
+ },
206
+ permissions: preflight,
207
+ session_binding: {
208
+ status: sessionId ? "bound" : "unavailable",
209
+ session_id: sessionId
210
+ }
211
+ },
212
+ required_context: sources.map((source) => ({
213
+ path: source.label,
214
+ reason: "Stage-specific canonical instruction source",
215
+ stop_condition: "Stop when the source no longer contains unresolved requirements for this stage"
216
+ })),
167
217
  worker_prompt_markdown: prompt
168
218
  });
169
- validateSchema({ schemaName: "stage-prompt", file: promptDataPath, projectRoot });
219
+ validateSchema({ schemaName: "stage-prompt", file: promptDataPath, projectRoot, runId: attached.run.id, ddFlowHome: context.ddFlowHome });
170
220
  atomicWrite(path.join(stageRoot, "stage-start.json"), {
171
221
  schema_id: "dd-flow/stage-start@2",
172
222
  run_id: attached.run.id,
@@ -185,6 +235,7 @@ export function startStage(context, input) {
185
235
  stage_root: stageRoot,
186
236
  archive_path: null,
187
237
  prompt_path: promptPath,
238
+ semantic_input_path: semanticInputPath,
188
239
  aliases: {
189
240
  project: projectRoot,
190
241
  workspace: attached.run.workspace_root,
@@ -196,7 +247,7 @@ export function startStage(context, input) {
196
247
  },
197
248
  ...(input.stage === "plan" ? { plan_ref: planPath, aspect_map_ref: aspectMapPath } : {}),
198
249
  resolved_context: { run: attached.run, stage: { name: input.stage, dir, status: "running" } },
199
- next_command: `dd-flow stage finish ${attached.run.id} --stage ${input.stage} --outcome done --project-root ${JSON.stringify(projectRoot)} --json`,
250
+ next_command: stageFinishCommand(attached.run, projectRoot, input.stage),
200
251
  permission_probe: preflight,
201
252
  run: attached.run,
202
253
  prompt: { path: promptPath, data_path: promptDataPath },
@@ -220,48 +271,71 @@ export function finishStage(context, input) {
220
271
  ensureWithin(runHome, stageRoot, "stage root");
221
272
  fs.mkdirSync(stageRoot, { recursive: true });
222
273
  const semanticFile = resolveStageFile(input.semanticFile ?? "@stage/stage-input.json", stageRoot, runHome);
223
- validateSchema({ schemaName: "stage-finish-input", file: semanticFile, projectRoot });
224
- const semantic = readSemanticFile(semanticFile, runHome);
225
- const status = input.outcome ?? "done";
226
- if (!["done", "blocked", "failed"].includes(status)) {
227
- throw new AppError("validation", "Stage finish status must be done, blocked, or failed", 2, { status });
274
+ const receipt = beginFinishReceipt(context, stageRoot, semanticFile, view.run.id, input.stage);
275
+ try {
276
+ validateSchema({ schemaName: "stage-finish-input", file: semanticFile, projectRoot, runId, ddFlowHome: context.ddFlowHome });
277
+ const semantic = readSemanticFile(semanticFile, runHome);
278
+ const status = stringValue(semantic.status) ?? "done";
279
+ if (!["done", "waiting_for_user", "blocked", "failed"].includes(status)) {
280
+ throw new AppError("validation", "Stage finish status must be done, waiting_for_user, blocked, or failed", 2, { status });
281
+ }
282
+ validateStageOutcome(input.stage, status, semantic);
283
+ const coverage = workerCoverage(context, view.run.project_id, view.run.id);
284
+ if (coverage.jobs.missing?.length) {
285
+ throw new AppError("worker_jobs_incomplete", "Stage finish requires all declared worker jobs", 1, { coverage });
286
+ }
287
+ const lint = runTargetedMemoryBankLint(projectRoot);
288
+ const planFinish = input.stage === "plan"
289
+ ? preparePlanFinish(context, projectRoot, view, stageRoot)
290
+ : undefined;
291
+ const report = buildStageReport(context, view, input.stage, status, semantic, stageRoot, lint, planFinish);
292
+ const dataPath = path.join(stageRoot, "stage-report.json");
293
+ const reportPath = path.join(stageRoot, "stage-report.md");
294
+ const htmlPath = path.join(stageRoot, "stage-report.html");
295
+ atomicWrite(dataPath, report);
296
+ validateSchema({ schemaName: stageReportSchemaName(input.stage, planFinish), file: dataPath, projectRoot, runId, ddFlowHome: context.ddFlowHome });
297
+ atomicWrite(reportPath, renderMarkdown(report));
298
+ atomicWrite(htmlPath, renderHtml(projectRoot, report));
299
+ if (view.run.subject.type === "protocol") {
300
+ syncProtocolLifecycle(context, view.run.project_id, view.run.subject.id, input.stage, status, stringValue(semantic.next_action));
301
+ }
302
+ const summaryPath = updateProtocolSummary(context, projectRoot, view, report, dataPath, htmlPath, reportPath);
303
+ const completed = completeFlowRunStage(context, {
304
+ projectRoot,
305
+ runId,
306
+ stage: input.stage,
307
+ status,
308
+ stageReport: htmlPath,
309
+ data: dataPath,
310
+ dataSchemaId: String(report.schema_id),
311
+ report: reportPath
312
+ });
313
+ finishReceipt(receipt, { outcome: "accepted", result: { status, report: dataPath } });
314
+ appendAudit(context, {
315
+ projectId: view.run.project_id,
316
+ ...(view.run.subject.type === "protocol" ? { protocolId: view.run.subject.id } : {}),
317
+ eventType: "stage.finish.accepted",
318
+ payload: { run_id: runId, stage: input.stage, status, receipt: receipt.path, semantic_sha256: receipt.semantic_sha256 }
319
+ });
320
+ return {
321
+ ok: true,
322
+ schema_id: "dd-flow/stage-finish@1",
323
+ ...(completed && typeof completed === "object" && !Array.isArray(completed) ? completed : {}),
324
+ artifacts: { json: dataPath, markdown: reportPath, html: htmlPath, finish_receipt: receipt.path, ...(summaryPath ? { protocol_summary: summaryPath } : {}) },
325
+ gates: { worker_coverage: coverage, targeted_lint: lint },
326
+ generated: true
327
+ };
228
328
  }
229
- const coverage = workerCoverage(context, view.run.project_id, view.run.id);
230
- if (coverage.status !== "complete") {
231
- throw new AppError("worker_coverage_incomplete", "Stage finish requires complete worker coverage", 1, { coverage });
329
+ catch (error) {
330
+ finishReceipt(receipt, { outcome: "rejected", error: finishError(error) });
331
+ appendAudit(context, {
332
+ projectId: view.run.project_id,
333
+ ...(view.run.subject.type === "protocol" ? { protocolId: view.run.subject.id } : {}),
334
+ eventType: "stage.finish.rejected",
335
+ payload: { run_id: runId, stage: input.stage, receipt: receipt.path, semantic_sha256: receipt.semantic_sha256, error: finishError(error) }
336
+ });
337
+ throw error;
232
338
  }
233
- const lint = runTargetedMemoryBankLint(projectRoot, semantic);
234
- const planFinish = input.stage === "plan"
235
- ? preparePlanFinish(context, projectRoot, view, stageRoot)
236
- : undefined;
237
- const report = buildStageReport(context, view, input.stage, status, semantic, stageRoot, lint, planFinish);
238
- const dataPath = path.join(stageRoot, "stage-report.json");
239
- const reportPath = path.join(stageRoot, "stage-report.md");
240
- const htmlPath = path.join(stageRoot, "stage-report.html");
241
- atomicWrite(dataPath, report);
242
- validateSchema({ schemaName: planFinish ? "plan-stage-report" : "stage-report", file: dataPath, projectRoot });
243
- atomicWrite(reportPath, renderMarkdown(report));
244
- atomicWrite(htmlPath, renderHtml(projectRoot, report));
245
- syncProtocolLifecycle(context, view.run.project_id, view.run.subject.id, input.stage, status, stringValue(semantic.next_action));
246
- const summaryPath = updateProtocolSummary(context, projectRoot, view, report, dataPath, htmlPath, reportPath);
247
- const completed = completeFlowRunStage(context, {
248
- projectRoot,
249
- runId,
250
- stage: input.stage,
251
- status,
252
- stageReport: htmlPath,
253
- data: dataPath,
254
- dataSchemaId: String(report.schema_id),
255
- report: reportPath
256
- });
257
- return {
258
- ok: true,
259
- schema_id: "dd-flow/stage-finish@1",
260
- ...(completed && typeof completed === "object" && !Array.isArray(completed) ? completed : {}),
261
- artifacts: { json: dataPath, markdown: reportPath, html: htmlPath, ...(summaryPath ? { protocol_summary: summaryPath } : {}) },
262
- gates: { worker_coverage: coverage, targeted_lint: lint },
263
- generated: true
264
- };
265
339
  }
266
340
  function runView(value) {
267
341
  if (!value || typeof value !== "object")
@@ -277,12 +351,9 @@ function syncProtocolLifecycle(context, projectId, protocolId, stage, status, ne
277
351
  return;
278
352
  const protocol = requireProtocol(context, protocolId, projectId);
279
353
  const { state } = readProtocolRuntimeState(context, protocol);
280
- const currentRank = protocolStageRank(state.stage);
281
- const requestedRank = protocolStageRank(targetStage);
282
- const completedTarget = status === "done" ? nextStageAfterFinish(targetStage) : targetStage;
283
- const completedRank = protocolStageRank(completedTarget);
284
- const effectiveStage = completedRank >= currentRank ? completedTarget : state.stage;
285
- const effectiveStatus = status === "done" ? "running" : status;
354
+ const transition = lifecycleTransition(targetStage, status);
355
+ const effectiveStage = transition.stage;
356
+ const effectiveStatus = transition.status;
286
357
  const effectiveNextAction = nextAction ?? nextActionForProtocolStage(effectiveStage);
287
358
  if (effectiveStage === state.stage && effectiveStatus === state.status && effectiveNextAction === state.next_action)
288
359
  return;
@@ -303,7 +374,7 @@ function syncProtocolLifecycle(context, projectId, protocolId, stage, status, ne
303
374
  from: state.stage,
304
375
  to: effectiveStage,
305
376
  status: effectiveStatus,
306
- requested_rank: requestedRank
377
+ outcome: status
307
378
  }
308
379
  });
309
380
  }
@@ -316,11 +387,18 @@ function protocolStageForRunStage(stage) {
316
387
  return "implementation";
317
388
  return null;
318
389
  }
319
- function nextStageAfterFinish(stage) {
320
- return stage === "specify" ? "plan" : "implementation";
321
- }
322
- function protocolStageRank(stage) {
323
- return { registered: 0, specify: 1, plan: 2, implementation: 3 }[stage] ?? -1;
390
+ function lifecycleTransition(stage, status) {
391
+ if (status === "running")
392
+ return { stage, status };
393
+ if (status === "waiting_for_user" || status === "blocked" || status === "failed")
394
+ return { stage, status };
395
+ if (status !== "done")
396
+ throw new AppError("invalid_transition", `Unknown stage outcome: ${status}`, 2);
397
+ return stage === "specify"
398
+ ? { stage: "plan", status: "running" }
399
+ : stage === "plan"
400
+ ? { stage: "implementation", status: "running" }
401
+ : { stage: "readiness", status: "running" };
324
402
  }
325
403
  function nextActionForProtocolStage(stage) {
326
404
  if (stage === "registered" || stage === "specify")
@@ -352,6 +430,8 @@ export function defaultStageDir(stage, flowKind) {
352
430
  return known[normalized] ?? `03-${normalized.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`;
353
431
  }
354
432
  function dataSchemaForStage(stage) {
433
+ if (stage === "specify")
434
+ return "dd-flow/stage-finish-input@1";
355
435
  if (stage === "plan")
356
436
  return "dd-flow/plan-stage-report@5";
357
437
  if (stage === "code" || stage === "implementation")
@@ -360,6 +440,39 @@ function dataSchemaForStage(stage) {
360
440
  return "dd-flow/merge-stage-report@2";
361
441
  return "dd-flow/code-stage-report@2";
362
442
  }
443
+ function stageReportSchemaName(stage, planFinish) {
444
+ if (planFinish)
445
+ return "plan-stage-report";
446
+ if (stage === "code" || stage === "implementation")
447
+ return "code-stage-report";
448
+ if (stage === "merge")
449
+ return "merge-stage-report";
450
+ return "stage-report";
451
+ }
452
+ function stageFinishCommand(run, projectRoot, stage) {
453
+ const compatibilityMode = run.flow_kind === "mb-upgrade" ? " --compatibility-mode mb-upgrade" : "";
454
+ return `dd-flow stage finish ${run.id} --stage ${stage} --project-root ${JSON.stringify(projectRoot)}${compatibilityMode} --json`;
455
+ }
456
+ function semanticInputTemplate(stage) {
457
+ const waiting = stage === "specify";
458
+ return {
459
+ schema_id: "dd-flow/stage-finish-input@1",
460
+ status: waiting ? "waiting_for_user" : "done",
461
+ result: "Replace this placeholder with the stage result.",
462
+ acceptance: ["Replace with the acceptance evidence for this stage."],
463
+ checks: ["Replace with a check actually performed."],
464
+ evidence: ["Replace with a durable evidence path or reference."],
465
+ ...(waiting ? {
466
+ questions: [{
467
+ id: "Q-001",
468
+ question: "Replace with an unresolved product decision.",
469
+ impact: "Replace with the consequence of leaving it unresolved.",
470
+ recommendation: "Replace with the smallest recommended decision."
471
+ }]
472
+ } : {}),
473
+ next_action: waiting ? "waiting_for_user: obtain the clarification packet" : "Continue to the next approved stage."
474
+ };
475
+ }
363
476
  function composeStagePrompt(context, projectRoot, before, current, stage, dir, preflight) {
364
477
  const project = requireProjectByRoot(context, projectRoot);
365
478
  const runHome = runHomePath(current.run);
@@ -367,8 +480,6 @@ function composeStagePrompt(context, projectRoot, before, current, stage, dir, p
367
480
  return fs.existsSync(absolute) ? `### ${label}\n\n${fs.readFileSync(absolute, "utf8").trim()}` : `### ${label}\n\n(unavailable)`;
368
481
  }).join("\n\n");
369
482
  const protocol = safeProtocolState(context, current.run.project_id, current.run.subject.id);
370
- const requiredContext = stageInstructionSources(context, projectRoot, current.run.flow_kind, stage)
371
- .map(({ label }) => `- Read: \`${label}\``).join("\n");
372
483
  return [
373
484
  "<stage_identity>",
374
485
  "# Stage work packet",
@@ -405,7 +516,7 @@ function composeStagePrompt(context, projectRoot, before, current, stage, dir, p
405
516
  "</applicable_instructions>",
406
517
  "",
407
518
  "<required_context>",
408
- requiredContext || "- No additional stage-specific source is required.",
519
+ "The bounded canonical instructions above are already included in this packet. Do not reopen them, prime.md, indexes, CLI help, Git status, or runtime state unless new semantic evidence makes a specific source necessary.",
409
520
  "</required_context>",
410
521
  "",
411
522
  "<work_contract>",
@@ -414,14 +525,16 @@ function composeStagePrompt(context, projectRoot, before, current, stage, dir, p
414
525
  "</work_contract>",
415
526
  "",
416
527
  "<completion_contract>",
417
- `Write semantic output to \`@stage/stage-input.json\`, then run: \`dd-flow stage finish ${current.run.id} --stage ${stage} --outcome done --project-root ${JSON.stringify(projectRoot)} --json\`.`,
528
+ `Write semantic output to \`@stage/stage-input.json\`, then run: \`${stageFinishCommand(current.run, projectRoot, stage)}\`.`,
529
+ "`@stage/stage-input.json` already contains the only valid field layout. Replace every placeholder; do not add mechanical fields such as changed_files. Set `status` to `done`, `waiting_for_user`, `blocked`, or `failed`. For `waiting_for_user`, preserve each question as a structured item in `questions` with id, question, impact, and recommended answer when known. For SPECIFY, `done` means there are no unresolved structured questions.",
418
530
  "The CLI must generate validated JSON, Markdown, HTML and protocol-summary evidence.",
531
+ "If finish rejects a required semantic status or a successful receipt disagrees with it, stop immediately with `flow_contract_conflict`: do not rewrite the result to another permitted status, retry finish, repair RUN state, or start a downstream stage.",
419
532
  "</completion_contract>",
420
533
  ].join("\n");
421
534
  }
422
535
  function stageInstructionSources(context, projectRoot, flowKind, stage) {
423
536
  if (flowKind === "mb-upgrade") {
424
- const canon = resolveCanonRoot(context);
537
+ const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
425
538
  if (!canon.ok || !canon.canon) {
426
539
  throw new AppError("canon_unavailable", "mb-upgrade stage prompt requires the canonical Memory Bank", 1, {
427
540
  blockers: canon.blockers,
@@ -440,7 +553,9 @@ function stageInstructionSources(context, projectRoot, flowKind, stage) {
440
553
  .map((file) => ({ label: `canonical:${file}`, absolute: path.join(flowRoot, file) }));
441
554
  }
442
555
  const filesByStage = {
443
- specify: [".memory-bank/dd-flow/mb-sdlc/specify/stage.md"],
556
+ specify: [
557
+ ".memory-bank/dd-flow/mb-sdlc/specify/packet.md"
558
+ ],
444
559
  plan: [".memory-bank/dd-flow/plan.md"],
445
560
  code: [".memory-bank/dd-flow/code.md", ".memory-bank/dd-flow/mb-sdlc/code/implement.md"],
446
561
  implementation: [".memory-bank/dd-flow/code.md", ".memory-bank/dd-flow/mb-sdlc/code/implement.md"],
@@ -469,6 +584,49 @@ function readSemanticFile(file, runHome) {
469
584
  throw new AppError("validation", "Semantic input must be a JSON object", 2);
470
585
  return value;
471
586
  }
587
+ function validateStageOutcome(stage, status, semantic) {
588
+ if (stage !== "specify")
589
+ return;
590
+ const questions = Array.isArray(semantic.questions) ? semantic.questions : [];
591
+ if (status === "waiting_for_user" && questions.length === 0) {
592
+ throw new AppError("semantic_status_inconsistent", "SPECIFY waiting_for_user requires at least one structured question", 2);
593
+ }
594
+ if (status === "done" && questions.length > 0) {
595
+ throw new AppError("semantic_status_inconsistent", "SPECIFY done cannot contain unresolved structured questions", 2);
596
+ }
597
+ }
598
+ function beginFinishReceipt(context, stageRoot, semanticFile, runId, stage) {
599
+ const receiptDir = path.join(stageRoot, "finish-receipts");
600
+ fs.mkdirSync(receiptDir, { recursive: true });
601
+ const number = fs.readdirSync(receiptDir).filter((file) => /^\d{3}\.json$/u.test(file)).length + 1;
602
+ const receiptPath = path.join(receiptDir, `${String(number).padStart(3, "0")}.json`);
603
+ let raw = null;
604
+ try {
605
+ raw = fs.readFileSync(semanticFile, "utf8");
606
+ }
607
+ catch { /* receipt records the missing input below */ }
608
+ const semanticSha = raw === null ? null : crypto.createHash("sha256").update(raw).digest("hex");
609
+ atomicWrite(receiptPath, {
610
+ schema_id: "dd-flow/stage-finish-receipt@1",
611
+ run_id: runId,
612
+ stage,
613
+ attempted_at: context.now(),
614
+ semantic_file: semanticFile,
615
+ semantic_sha256: semanticSha,
616
+ semantic_input_raw: raw,
617
+ outcome: "pending"
618
+ });
619
+ return { path: receiptPath, semantic_sha256: semanticSha };
620
+ }
621
+ function finishReceipt(receipt, outcome) {
622
+ const current = JSON.parse(fs.readFileSync(receipt.path, "utf8"));
623
+ atomicWrite(receipt.path, { ...current, ...outcome, finished_at: new Date().toISOString() });
624
+ }
625
+ function finishError(error) {
626
+ if (error instanceof AppError)
627
+ return { code: error.code, message: error.message, details: error.details };
628
+ return { code: "unexpected", message: String(error) };
629
+ }
472
630
  function resolveStageFile(file, stageRoot, runHome) {
473
631
  const candidate = file === "@stage" || file.startsWith("@stage/")
474
632
  ? path.join(stageRoot, file.slice("@stage".length).replace(/^[/\\]/u, ""))
@@ -478,7 +636,7 @@ function resolveStageFile(file, stageRoot, runHome) {
478
636
  }
479
637
  function buildStageReport(context, view, stage, status, semantic, stageRoot, lint, planFinish) {
480
638
  const result = stringValue(semantic.result) ?? `Stage ${stage} finished with status ${status}.`;
481
- const changedFiles = stringArray(semantic.changed_files, gitChangedFiles(view.run.workspace_root));
639
+ const changedFiles = gitChangedFiles(view.run.workspace_root);
482
640
  const checks = stringArray(semantic.checks, []);
483
641
  const evidence = stringArray(semantic.evidence, []);
484
642
  const acceptance = stringArray(semantic.acceptance, []);
@@ -541,12 +699,41 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
541
699
  }
542
700
  };
543
701
  }
702
+ if (stage === "code" || stage === "implementation") {
703
+ return {
704
+ schema_id: "dd-flow/code-stage-report@2",
705
+ run: { run_id: view.run.id, run_state: view.run.run_index_path },
706
+ stage: { name: stage, dir: path.basename(stageRoot), status },
707
+ project: { id: view.run.project_id, title: path.basename(view.run.project_root) },
708
+ subject: { id: view.run.subject.id, title: view.run.subject.id },
709
+ flow_flags: flowFlagsReportProjection(view.index.flow_flags),
710
+ overall: {
711
+ verdict: status === "done" ? "accepted" : status,
712
+ summary: result,
713
+ next_action: stringValue(semantic.next_action) ?? "Proceed to readiness."
714
+ },
715
+ breadcrumbs: [{ label: "RUN", href: view.run.run_index_path, status: "available" }],
716
+ implemented_goals: [{ title: `Stage ${stage}`, summary: result }],
717
+ acceptance_scenarios: acceptance.map((summary, index) => ({
718
+ id: `SCN-${stage}-${index + 1}`,
719
+ title: `Acceptance ${index + 1}`,
720
+ verdict: "accepted",
721
+ steps: [{ title: "Stage finish", summary }],
722
+ evidence: evidence.map((item) => ({ path: item, label: item }))
723
+ })),
724
+ changed_files: changedFiles.map((file) => ({ path: file, label: file })),
725
+ checks: checks.map((name) => ({ name, status: "passed" })),
726
+ review: { verdict: status === "done" ? "accepted" : status, findings: [] },
727
+ defs: Array.isArray(semantic.def_outcomes) ? semantic.def_outcomes : []
728
+ };
729
+ }
544
730
  return {
545
- schema_id: "dd-flow/stage-report@1",
731
+ schema_id: "dd-flow/stage-report@2",
546
732
  run_id: view.run.id,
547
733
  stage,
548
734
  generated_at: finishedAt,
549
735
  verdict: status,
736
+ summary: result,
550
737
  semantic: {
551
738
  result,
552
739
  acceptance,
@@ -554,6 +741,7 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
554
741
  checks,
555
742
  evidence,
556
743
  next_action: stringValue(semantic.next_action) ?? "Resolve the stage result before continuing.",
744
+ ...(Array.isArray(semantic.questions) ? { questions: semantic.questions } : {}),
557
745
  ...(Array.isArray(semantic.reviewer_findings) ? { reviewer_findings: semantic.reviewer_findings } : {}),
558
746
  ...(Array.isArray(semantic.def_outcomes) ? { def_outcomes: semantic.def_outcomes } : {})
559
747
  },
@@ -568,8 +756,7 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
568
756
  artifacts: {
569
757
  json: path.join(stageRoot, "stage-report.json"),
570
758
  markdown: path.join(stageRoot, "stage-report.md"),
571
- html: path.join(stageRoot, "stage-report.html"),
572
- summary: path.join(stageRoot, "stage-report.md")
759
+ html: path.join(stageRoot, "stage-report.html")
573
760
  },
574
761
  validation: {
575
762
  permission_scope: "known_targets_only",
@@ -611,6 +798,28 @@ function stringValue(value) {
611
798
  function stringArray(value, fallback) {
612
799
  return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : fallback;
613
800
  }
801
+ function flowFlagsReportProjection(value) {
802
+ if (!value) {
803
+ return {
804
+ flow_kind: "unknown",
805
+ snapshot_revision: 1,
806
+ resolution_status: "legacy_incomplete",
807
+ values: {},
808
+ snapshot_checksum: "0".repeat(64)
809
+ };
810
+ }
811
+ return {
812
+ ...(value.contract ? { contract: value.contract } : {}),
813
+ flow_kind: value.flow_kind ?? "unknown",
814
+ ...(value.preset ? { preset: value.preset } : {}),
815
+ snapshot_revision: value.snapshot_revision ?? 1,
816
+ resolution_status: value.resolution_status ?? "legacy_incomplete",
817
+ values: value.values ?? {},
818
+ ...(value.snapshot_checksum ? { snapshot_checksum: value.snapshot_checksum } : { snapshot_checksum: "0".repeat(64) }),
819
+ ...(Array.isArray(value.floors_applied) ? { floors_applied: value.floors_applied } : {}),
820
+ ...(value.resolved_at ? { resolved_at: value.resolved_at } : {})
821
+ };
822
+ }
614
823
  function gitChangedFiles(workspaceRoot) {
615
824
  const result = spawnSync("git", ["-C", workspaceRoot, "status", "--short"], { encoding: "utf8" });
616
825
  if (result.status !== 0)
@@ -618,7 +827,7 @@ function gitChangedFiles(workspaceRoot) {
618
827
  return result.stdout.split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).trim()).filter(Boolean);
619
828
  }
620
829
  function workerCoverage(context, projectId, runId) {
621
- const sessions = context.db.all("SELECT * FROM flow_sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id", [projectId, runId]);
830
+ const sessions = context.db.all("SELECT * FROM sessions WHERE project_id = ? AND run_id = ? ORDER BY created_at, session_id", [projectId, runId]);
622
831
  const report = reconcileSessionCoverage(sessions);
623
832
  const jobs = context.db.all("SELECT job_id, status FROM flow_jobs WHERE project_id = ? AND run_id = ? ORDER BY job_id", [projectId, runId]);
624
833
  const missingJobs = jobs.filter((job) => job.status !== "done").map((job) => job.job_id);
@@ -632,9 +841,8 @@ function workerCoverage(context, projectId, runId) {
632
841
  jobs: { expected: jobs.map((job) => job.job_id), missing: missingJobs }
633
842
  };
634
843
  }
635
- function runTargetedMemoryBankLint(projectRoot, semantic) {
636
- const declared = stringArray(semantic.changed_files, []);
637
- const files = [...new Set(declared.filter((file) => file.startsWith(".memory-bank/") && !file.split(/[\\/]/u).some((part) => part === ".." || part.startsWith(".env"))))];
844
+ export function runTargetedMemoryBankLint(projectRoot, selectedFiles) {
845
+ const files = [...new Set((selectedFiles ?? gitChangedFiles(projectRoot)).filter((file) => file.startsWith(".memory-bank/") && file.toLowerCase().endsWith(".md") && !file.split(/[\\/]/u).some((part) => part === ".." || part.startsWith(".env"))))];
638
846
  for (const file of files)
639
847
  ensureWithin(projectRoot, path.resolve(projectRoot, file), "lint target");
640
848
  if (files.length === 0) {
@@ -681,16 +889,19 @@ function renderMarkdown(report) {
681
889
  }
682
890
  function renderHtml(projectRoot, report) {
683
891
  const plan = report.schema_id === "dd-flow/plan-stage-report@5";
892
+ const specify = report.schema_id === "dd-flow/stage-report@2" && report.stage === "specify";
684
893
  const templatePath = path.join(projectRoot, plan
685
894
  ? ".memory-bank/dd-flow/mb-sdlc/plan/stage-report-template.html"
686
- : ".memory-bank/dd-flow/mb-sdlc/code/stage-report-template.html");
895
+ : specify
896
+ ? ".memory-bank/dd-flow/mb-sdlc/specify/stage-report-template.html"
897
+ : ".memory-bank/dd-flow/mb-sdlc/code/stage-report-template.html");
687
898
  if (!fs.existsSync(templatePath))
688
- throw new AppError("stage_report_template_missing", "Code stage report template is missing", 1, { path: templatePath });
899
+ throw new AppError("stage_report_template_missing", "Stage report template is missing", 1, { path: templatePath });
689
900
  const template = fs.readFileSync(templatePath, "utf8");
690
901
  const canonicalMarker = "__STAGE_REPORT_DATA__";
691
902
  const legacyMarker = "__CODE_DASHBOARD_DATA__";
692
903
  if (!template.includes(canonicalMarker) && (!template.includes("script id=\"code-data\"") || !template.includes(legacyMarker))) {
693
- throw new AppError("stage_report_template_invalid", "Code stage report template is missing required anchors", 1, { path: templatePath });
904
+ throw new AppError("stage_report_template_invalid", "Stage report template is missing required anchors", 1, { path: templatePath });
694
905
  }
695
906
  const embedded = JSON.stringify(report).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
696
907
  return template.replace(canonicalMarker, embedded).replace(legacyMarker, embedded);