@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.
- package/CHANGELOG.md +636 -0
- package/README.md +13 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +108 -18
- package/dist/cli/run-cli.js +626 -49
- package/dist/domain/flow-contract.js +11 -0
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-contract.schema.json +6 -3
- package/dist/schemas/flow-run.schema.json +16 -122
- package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
- package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-prompt.schema.json +4 -4
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/canon.js +15 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +60 -8
- package/dist/services/code-checks.js +244 -0
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +14 -14
- package/dist/services/engines.js +408 -30
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +775 -23
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -5
- package/dist/services/merge-queue.js +53 -5
- package/dist/services/merge-worker.js +5 -6
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +5 -5
- package/dist/services/plans.js +5 -3
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +1 -1
- package/dist/services/protocols.js +31 -10
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +525 -58
- package/dist/services/schema-validation.js +116 -2
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +288 -77
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/status.js +8 -3
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +308 -0
- package/dist/services/vnext-code.js +616 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +576 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +499 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +292 -42
- package/dist/storage/paths.js +47 -1
- 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
|
-
|
|
121
|
-
|
|
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
|
|
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:
|
|
198
|
+
source_fragments: sources.map((source) => source.label),
|
|
164
199
|
authoritative_facts: runtimeFacts(attached, projectRoot),
|
|
165
|
-
preflight
|
|
166
|
-
|
|
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:
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
281
|
-
const
|
|
282
|
-
const
|
|
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
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
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 =
|
|
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@
|
|
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
|
|
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,
|
|
636
|
-
const
|
|
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
|
-
:
|
|
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", "
|
|
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", "
|
|
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);
|