@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.9
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 +15 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +1 -1
- package/dist/cli/run-cli.js +22 -1
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/services/engines.js +4 -4
- package/dist/services/eval-snapshots.js +5 -3
- package/dist/services/harness-config.js +66 -0
- package/dist/services/merge-server.js +3 -2
- package/dist/services/prompts.js +4 -2
- package/dist/services/run-engine-bindings.js +19 -61
- package/dist/services/runs.js +21 -3
- package/dist/services/schema-validation.js +11 -11
- package/dist/services/stage-lifecycle.js +15 -8
- package/dist/services/stage-pause.js +4 -4
- package/dist/services/vnext-code-review.js +37 -31
- package/dist/services/vnext-code.js +40 -13
- package/dist/services/vnext-fanout.js +5 -12
- package/dist/services/vnext-merge.js +42 -23
- package/dist/services/vnext-plan-review.js +20 -23
- package/dist/services/vnext-plan.js +12 -12
- package/dist/services/vnext-protocolize.js +6 -6
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +8 -8
- package/dist/storage/database.js +3 -0
- package/package.json +12 -12
|
@@ -16,13 +16,10 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
|
16
16
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
17
17
|
import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
18
18
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
|
-
import {
|
|
19
|
+
import { readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
20
20
|
const stage = "plan-review";
|
|
21
21
|
const stageDir = vnextStageDirectory(stage);
|
|
22
22
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
23
|
-
const capacityProbeFanoutSize = capacityProbe.fanout_size;
|
|
24
|
-
const capacityProbeHoldSeconds = capacityProbe.probe_hold_seconds;
|
|
25
|
-
const capacityProbeDeadlineSeconds = capacityProbe.cleanup_deadline_seconds;
|
|
26
23
|
export function isVnextPlanReviewRun(context, input) {
|
|
27
24
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
28
25
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
@@ -126,7 +123,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
126
123
|
if (capacity.pending)
|
|
127
124
|
return capacity.response;
|
|
128
125
|
if (capacity.availableSlots < 1)
|
|
129
|
-
throw new AppError("
|
|
126
|
+
throw new AppError("subagent_capacity_unqualified", "PLAN-REVIEW requires one qualified native subagent slot", 1, { run_id: run.id });
|
|
130
127
|
const existing = context.db.all("SELECT work_id, task, status, result, created_at, launch_policy FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
|
|
131
128
|
const latest = (group) => existing.find((item) => item.task === group.task);
|
|
132
129
|
const pending = groups.filter((group) => !latest(group));
|
|
@@ -162,14 +159,14 @@ export function recordVnextPlanReviewCapacity(context, input) {
|
|
|
162
159
|
const prior = variables.variables[reviewCapacityKey];
|
|
163
160
|
if (typeof prior === "number") {
|
|
164
161
|
if (prior === input.availableSlots)
|
|
165
|
-
return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "
|
|
162
|
+
return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "harness_qualification", reused: true } };
|
|
166
163
|
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
|
|
167
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots,
|
|
168
|
-
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "
|
|
164
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, source: "harness_qualification" });
|
|
165
|
+
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "harness_qualification", refreshed: true } };
|
|
169
166
|
}
|
|
170
167
|
setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
|
|
171
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots,
|
|
172
|
-
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "
|
|
168
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, source: "harness_qualification" });
|
|
169
|
+
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
|
|
173
170
|
}
|
|
174
171
|
export function finishVnextPlanReview(context, input) {
|
|
175
172
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -183,8 +180,8 @@ export function finishVnextPlanReview(context, input) {
|
|
|
183
180
|
if (!parent || parent.status !== "running")
|
|
184
181
|
throw new AppError("invalid_work_state", "PLAN-REVIEW has no running parent Work", 2);
|
|
185
182
|
const decisionPath = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
|
|
186
|
-
const decision = readDecision(context, projectRoot, run.id, decisionPath);
|
|
187
|
-
const children = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?
|
|
183
|
+
const decision = readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
184
|
+
const children = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
|
|
188
185
|
const contextFile = readJson(path.join(root, "work-context.json"));
|
|
189
186
|
const groups = contextFile.groups ?? [];
|
|
190
187
|
const reviewedPlanRevision = contextFile.system?.plan_revision;
|
|
@@ -267,7 +264,7 @@ function orchestratorPrompt(context, input) {
|
|
|
267
264
|
const decision = path.join(input.root, "decision.json");
|
|
268
265
|
const revision = currentPlanRevision(input.home, input.run.workspace_root);
|
|
269
266
|
const workspaceContract = ["<workspace_contract>", `- route: ${input.workspaceRoute.route}`, `- feature branch: ${input.workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${input.workspaceRoute.base_ref ?? "not applicable"}`, `- read/write workspace: ${input.run.workspace_root}`, "The CLI verified this frozen route. All plan and correction writes belong in the named workspace; project root remains only the stable lifecycle identity. Do not create, switch, merge or delete branches/worktrees.", "</workspace_contract>"].join("\n");
|
|
270
|
-
return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch
|
|
267
|
+
return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, "After dispatch, launch at most the qualified capacity at once. If more independent reviewer Works remain, wait for the current wave to settle, then start the unchanged queued Works in the next wave. A reviewer launch rejected before it starts is not review evidence: do not create a replacement; wait for a running wave to settle and start that same queued Work. Each reviewer must be a genuinely fresh harness child Session. The lifecycle adapter binds that observed Session; do not bind or supply a Session ID manually. Reviewers are read-only and must not create children. As soon as a reviewer result is accepted, release that reviewer Session when the harness permits; do not let finished disposable workers occupy slots before the next wave.", "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input with no reasonable default, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. For an accepted correction, increment PLAN revision and update only plan.json and the relevant aspect map. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@3", outcome: "accepted | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why." }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
|
|
271
268
|
}
|
|
272
269
|
function reviewGroups(home, workspaceRoot) {
|
|
273
270
|
const root = path.join(home, "03-plan");
|
|
@@ -327,15 +324,15 @@ function plannedReviewWaves(groups, capacity) {
|
|
|
327
324
|
}
|
|
328
325
|
return waves;
|
|
329
326
|
}
|
|
330
|
-
export function
|
|
331
|
-
return { ok: true, run_id: input.runId, stage, outcome: "
|
|
327
|
+
export function capacityQualificationResponse(context, input) {
|
|
328
|
+
return { ok: true, run_id: input.runId, stage, outcome: "qualified_capacity_required", next_action: "qualify_harness_profile_then_record_capacity", next: { record_command: capacityRecordCommand(context, input.runId, input.projectRoot, "<qualified-native-child-count>"), retry_command: dispatchCommand(context, input.runId, input.projectRoot) } };
|
|
332
329
|
}
|
|
333
330
|
function ensureReviewCapacity(context, input) {
|
|
334
331
|
const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.runId });
|
|
335
332
|
const known = variables.variables[reviewCapacityKey];
|
|
336
333
|
if (typeof known === "number" && Number.isInteger(known) && known >= 0)
|
|
337
334
|
return { pending: false, availableSlots: known };
|
|
338
|
-
return { pending: true, response:
|
|
335
|
+
return { pending: true, response: capacityQualificationResponse(context, { projectRoot: input.projectRoot, runId: input.runId }) };
|
|
339
336
|
}
|
|
340
337
|
function protocolIdsForReview(home) {
|
|
341
338
|
const report = readJson(path.join(home, "02-protocolize", "stage-report.json"));
|
|
@@ -385,7 +382,7 @@ catch {
|
|
|
385
382
|
function rootWorkId(context, projectId, runId) { const root = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!root)
|
|
386
383
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
|
|
387
384
|
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch }); }
|
|
388
|
-
function readDecision(context, projectRoot, runId, file) { validateSchema({ schemaName: "plan-review-decision", file, projectRoot, runId, ddFlowHome: context.ddFlowHome }); return readJson(file); }
|
|
385
|
+
function readDecision(context, projectRoot, runId, runRoot, file) { validateSchema({ schemaName: "plan-review-decision", file, projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }); return readJson(file); }
|
|
389
386
|
function preserveDecisionReceipt(root, source) {
|
|
390
387
|
const receipt = path.join(root, "decision.receipt.json");
|
|
391
388
|
const bytes = fs.readFileSync(source);
|
|
@@ -454,14 +451,14 @@ function finishTerminalReview(context, input) {
|
|
|
454
451
|
const now = context.now();
|
|
455
452
|
const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
|
|
456
453
|
// A terminal decision is structured cancellation of the review subtree. It
|
|
457
|
-
// must settle
|
|
458
|
-
//
|
|
454
|
+
// must settle semantic reviewers before its parent Work can settle; otherwise
|
|
455
|
+
// the RUN contains abandoned active Work/Session links.
|
|
459
456
|
settleReviewChildren(context, { projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, reason: `PLAN-REVIEW terminal decision: ${input.decision.summary}`, now });
|
|
460
457
|
settleReviewParent(context, { projectRoot: input.projectRoot, projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, result: input.decision.summary, reason: `plan_review_${input.decision.outcome}`, resultPath: path.join(input.root, "stage-report.json") });
|
|
461
458
|
const rootWork = rootWorkId(context, input.projectId, input.run.id);
|
|
462
459
|
context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [childStatus, input.decision.summary, now, now, rootWork]);
|
|
463
460
|
closeRunningWorkSession(context, rootWork, childStatus, now);
|
|
464
|
-
const settledChildren = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?
|
|
461
|
+
const settledChildren = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [input.projectId, input.run.id, input.parentWorkId]);
|
|
465
462
|
const report = reportFor({ run: input.run, mode: input.contextFile.system?.effective_mode ?? "standard", requested: input.contextFile.system?.requested_mode ?? "auto", outcome: input.decision.outcome, groups: input.contextFile.groups ?? [], batchChecksum: input.contextFile.system?.batch_checksum ?? checksum(input.batch), planChecksum: input.contextFile.system?.plan_checksum ?? planSetChecksum(requireHome(input.run), input.run.workspace_root), code: {}, now, projectRoot: input.projectRoot, decision: input.decision, children: latestReviewChildren(settledChildren), flow: flowCommand(context) });
|
|
466
463
|
writeReport(input.root, report);
|
|
467
464
|
refreshRunWorkProjection(context, input.projectId, input.run.id);
|
|
@@ -500,10 +497,10 @@ function codeCommand(context, runId, projectRoot) { return `${flowCommand(contex
|
|
|
500
497
|
function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
501
498
|
function finishCommand(context, runId, projectRoot, decision) { return `${flowCommand(context)} stage finish ${runId} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --decision-file ${JSON.stringify(decision)} --json`; }
|
|
502
499
|
function capacityRecordCommand(context, runId, projectRoot, availableSlots) { return `${flowCommand(context)} run capacity record ${runId} --available-slots ${availableSlots} --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
503
|
-
function requireRun(context, projectRoot, runId) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, project_id, workspace_root,
|
|
500
|
+
function requireRun(context, projectRoot, runId) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, project_id, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]); if (!run)
|
|
504
501
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
505
|
-
function requireHome(run) { if (!run.
|
|
506
|
-
throw new AppError("runtime_missing", "RUN
|
|
502
|
+
function requireHome(run) { if (!run.run_root)
|
|
503
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
507
504
|
function read(file) { if (!fs.existsSync(file))
|
|
508
505
|
throw new AppError("not_found", "Required vNext prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
|
|
509
506
|
function readJson(file) { try {
|
|
@@ -74,16 +74,16 @@ export function startVnextPlan(context, input) {
|
|
|
74
74
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage: "plan", workId: planWorkId, projectRoot });
|
|
75
75
|
const pauseCommandTemplate = stagePauseCommandTemplate(pauseCommand);
|
|
76
76
|
const validationCommands = protocols.flatMap((_, index) => [
|
|
77
|
-
`${flowCommand(context)} schema validate --schema vnext-protocol-plan --file ${JSON.stringify(planPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --json`,
|
|
78
|
-
`${flowCommand(context)} schema validate --schema plan-aspect-map --file ${JSON.stringify(mapPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --json`
|
|
77
|
+
`${flowCommand(context)} schema validate --schema vnext-protocol-plan --file ${JSON.stringify(planPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --run ${run.id} --json`,
|
|
78
|
+
`${flowCommand(context)} schema validate --schema plan-aspect-map --file ${JSON.stringify(mapPaths[index])} --project-root ${JSON.stringify(run.workspace_root)} --run ${run.id} --json`
|
|
79
79
|
]);
|
|
80
80
|
const runVariables = getFlowRunVariables(context, { projectRoot, runId: run.id });
|
|
81
81
|
const measuredCapacity = runVariables.variables[subagentCapacityKey];
|
|
82
82
|
const mergeRequired = runEndsAtMerge(context, projectRoot, run.id);
|
|
83
83
|
const capacityContext = typeof measuredCapacity === "number" && Number.isInteger(measuredCapacity) && measuredCapacity >= 0
|
|
84
|
-
? `- The
|
|
85
|
-
: "- Reviewer capacity is not
|
|
86
|
-
const reviewGroupingRule = "Group only semantically compatible applicable aspects, preserving real trust, irreversible, high-risk and hard-dependency boundaries. Prefer the fewest groups that retain independent review value, normally one review wave. Put two or three compatible aspects in a group; do not create one group per aspect merely for convenience. A later PLAN-REVIEW dispatch
|
|
84
|
+
? `- The qualified reviewer capacity is ${measuredCapacity}. This is a runtime fact for later PLAN-REVIEW dispatch; do not repeat qualification or invent a different value.`
|
|
85
|
+
: "- Reviewer capacity is not qualified yet. PLAN must not qualify or launch reviewers; an external harness controller supplies it before fan-out.";
|
|
86
|
+
const reviewGroupingRule = "Group only semantically compatible applicable aspects, preserving real trust, irreversible, high-risk and hard-dependency boundaries. Prefer the fewest groups that retain independent review value, normally one review wave. Put two or three compatible aspects in a group; do not create one group per aspect merely for convenience. A later PLAN-REVIEW dispatch uses externally qualified capacity to schedule these semantic groups into waves; do not invent a capacity value here.";
|
|
87
87
|
const checkProfile = path.join(run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json");
|
|
88
88
|
const policyMergeAliases = codeCheckProfile?.mandatory_by_gate.merge ?? [];
|
|
89
89
|
const mergeContract = mergeRequired
|
|
@@ -136,7 +136,7 @@ export function finishVnextPlan(context, input) {
|
|
|
136
136
|
const reviewCommand = `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json`;
|
|
137
137
|
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage: "plan", generated_at: now, verdict: "done", semantic: { result: `Accepted ${protocols.length} executable PLAN artifact${protocols.length === 1 ? "" : "s"}.`, acceptance: protocols, changed_files: [...planFiles.map((file) => path.relative(projectRoot, file)), ...mapFiles.map((file) => runRef(run.id, home, file)), runRef(run.id, home, batch)], checks: ["protocol-plan schema", "aspect-map schema", "cross-artifact references", "generated CODE batch"], evidence: [runRef(run.id, home, path.join(root, "stage-report.json"))], next_action: "start_plan_review", plans: planFiles.map((file) => path.relative(projectRoot, file)), aspect_maps: mapFiles.map((file) => runRef(run.id, home, file)), code_work_batch: runRef(run.id, home, batch), batch_checksum: batchChecksum }, mechanical: { started_at: stageStartedAt(home, now), finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(stageStartedAt(home, now))), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, next_command: reviewCommand }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
|
|
138
138
|
const reportJson = writeStageReport(root, report).json;
|
|
139
|
-
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
139
|
+
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
140
140
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
141
141
|
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
|
|
142
142
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
|
|
@@ -159,7 +159,7 @@ export function validateVnextPlanArtifacts(context, input) {
|
|
|
159
159
|
for (const [index, file] of planFiles.entries()) {
|
|
160
160
|
const protocolId = input.protocols[index];
|
|
161
161
|
try {
|
|
162
|
-
validateSchema({ schemaName: "vnext-protocol-plan", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId });
|
|
162
|
+
validateSchema({ schemaName: "vnext-protocol-plan", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
|
|
163
163
|
const value = readPlan(file);
|
|
164
164
|
assertPlanIdentity(value, planIdentity(input.home, input.runId, protocolId, ownership.get(protocolId) ?? []), file);
|
|
165
165
|
validatePlanSemantics(file, new Set(ownership.get(protocolId) ?? []), obligations);
|
|
@@ -180,7 +180,7 @@ export function validateVnextPlanArtifacts(context, input) {
|
|
|
180
180
|
for (const file of mapFiles) {
|
|
181
181
|
try {
|
|
182
182
|
normalizeAspectMapRefs(file, workspaceRoot, input.home, input.runId);
|
|
183
|
-
validateSchema({ schemaName: "plan-aspect-map", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome });
|
|
183
|
+
validateSchema({ schemaName: "plan-aspect-map", file, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
|
|
184
184
|
validateAspectMap(file, input.protocols, workspaceRoot);
|
|
185
185
|
}
|
|
186
186
|
catch (error) {
|
|
@@ -202,7 +202,7 @@ export function validateVnextPlanArtifacts(context, input) {
|
|
|
202
202
|
const projection = projectCodeWorkBatch({ home: input.home, workspaceRoot, runId: input.runId, plans, protocols: input.protocols, ...(frozenDocumentBaselines ? { frozenDocumentBaselines } : {}) });
|
|
203
203
|
validateProjectedPaths(projection, workspaceRoot, input.home, input.runId);
|
|
204
204
|
fs.writeFileSync(temporaryBatch, `${JSON.stringify(projection, null, 2)}\n`);
|
|
205
|
-
validateSchema({ schemaName: "code-work-batch", file: temporaryBatch, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId });
|
|
205
|
+
validateSchema({ schemaName: "code-work-batch", file: temporaryBatch, projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
|
|
206
206
|
validateWorkBatchFile(temporaryBatch);
|
|
207
207
|
if (input.publishBatch !== false) {
|
|
208
208
|
fs.renameSync(temporaryBatch, batch);
|
|
@@ -231,10 +231,10 @@ export function validateVnextCodeHandoff(context, input) {
|
|
|
231
231
|
const planTask = "Produce accepted plan.json and aspect-map.json artifacts.";
|
|
232
232
|
function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@6", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], planned_write_areas: ["apps/api/src/"], stop_conditions: ["Stop if accepted scope conflicts with current truth."] }, verification: { check_refs: ["CHK-P1-TEST"] } }], acceptance: [{ criterion_id: "AC-001", plan_item_ids: ["P1"], changed_surfaces: ["apps/api/src/example.ts"], path: "Exercise the accepted user path.", environment: "Local test environment.", fixtures: [], cleanup: "No persistent fixture.", check_refs: ["CHK-P1-TEST"], expected_evidence: ["Focused check passes."], proof_limits: ["Manual production evidence is not claimed."], gate: "work" }] }; }
|
|
233
233
|
function aspectMapExample(protocolId) { return { $schema: "plan-aspect-map.schema.json", schema_id: "dd-flow/plan-aspect-map@3", protocol_id: protocolId, plan_id: "PLAN-001", plan_revision: 1, catalog_ref: { path: ".memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects" }, routing: { initial_state: "orchestrator_local", selected_route: "local_compact", reason: "One genuinely small semantic unit.", groups: [] }, review_groups: [], aspects: [{ aspect_id: "example_aspect", applicability: "not_applicable", reason: "Only an example; use the supplied real catalog.", planned_artifact_refs: [] }] }; }
|
|
234
|
-
function requireRun(context, root, id) { const project = requireProjectByRoot(context, root); const run = context.db.get("SELECT id, project_id, workspace_root,
|
|
234
|
+
function requireRun(context, root, id) { const project = requireProjectByRoot(context, root); const run = context.db.get("SELECT id, project_id, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
|
|
235
235
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
236
|
-
function requireHome(run) { if (!run.
|
|
237
|
-
throw new AppError("runtime_missing", "RUN
|
|
236
|
+
function requireHome(run) { if (!run.run_root)
|
|
237
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
238
238
|
function protocolIds(home) {
|
|
239
239
|
const report = JSON.parse(fs.readFileSync(path.join(home, "02-protocolize", "stage-report.json"), "utf8"));
|
|
240
240
|
const ids = report.semantic?.acceptance;
|
|
@@ -135,7 +135,7 @@ export function finishVnextProtocolize(context, input) {
|
|
|
135
135
|
const root = path.join(requireRunHome(run), "02-protocolize");
|
|
136
136
|
const resultFile = path.resolve(input.resultFile);
|
|
137
137
|
inside(root, resultFile);
|
|
138
|
-
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
138
|
+
validateSchema({ schemaName: "vnext-protocolize-result", file: resultFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
139
139
|
const result = readResult(resultFile);
|
|
140
140
|
if (result.outcome !== "protocolized")
|
|
141
141
|
throw new AppError("validation", "PROTOCOLIZE may finish only as protocolized; use stage pause for every user question", 2);
|
|
@@ -162,7 +162,7 @@ export function finishVnextProtocolize(context, input) {
|
|
|
162
162
|
const reportMarkdown = path.join(root, "stage-report.md");
|
|
163
163
|
const reportHtml = path.join(root, "stage-report.html");
|
|
164
164
|
writeStageReport(root, report);
|
|
165
|
-
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
165
|
+
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireRunHome(run) });
|
|
166
166
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "protocolize-result.json", dataSchemaId: "dd-flow/vnext-protocolize-result@3", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
167
167
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "protocol_documents_materialized", work_id: work.work_id, protocol_ids: protocolIds, stage });
|
|
168
168
|
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "protocolized", nextAction: "start_plan" });
|
|
@@ -318,7 +318,7 @@ function readResult(file) {
|
|
|
318
318
|
function acceptedSpecifyObligations(context, projectRoot, runId, specifyPath) {
|
|
319
319
|
if (!fs.existsSync(specifyPath))
|
|
320
320
|
throw new AppError("not_found", "PROTOCOLIZE requires accepted specify.json", 1, { path: specifyPath });
|
|
321
|
-
validateSchema({ schemaName: "vnext-specify", file: specifyPath, projectRoot, ddFlowHome: context.ddFlowHome, runId });
|
|
321
|
+
validateSchema({ schemaName: "vnext-specify", file: specifyPath, projectRoot, ddFlowHome: context.ddFlowHome, runId, runRoot: requireRunHome(requireRun(context, projectRoot, runId)) });
|
|
322
322
|
const specify = readVnextSpecifyResult(specifyPath);
|
|
323
323
|
return [
|
|
324
324
|
...specify.requirements.map((obligation) => ({ ...obligation, kind: "requirement" })),
|
|
@@ -498,10 +498,10 @@ function featureIndex(epicRoot, featureSlug) {
|
|
|
498
498
|
}
|
|
499
499
|
function requireWork(context, id) { const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]); if (!work)
|
|
500
500
|
throw new AppError("not_found", "Work is not registered", 1, { work_id: id }); return work; }
|
|
501
|
-
function requireRun(context, projectRoot, id) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, short_id, slug, project_id, workspace_root,
|
|
501
|
+
function requireRun(context, projectRoot, id) { const project = requireProjectByRoot(context, projectRoot); const run = context.db.get("SELECT id, short_id, slug, project_id, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
|
|
502
502
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: id }); return run; }
|
|
503
|
-
function requireRunHome(run) { if (!run.
|
|
504
|
-
throw new AppError("runtime_missing", "RUN
|
|
503
|
+
function requireRunHome(run) { if (!run.run_root)
|
|
504
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
505
505
|
function inside(root, file) { const relative = path.relative(root, file); if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
506
506
|
throw new AppError("path_escape", "Result must be inside the protocolize workspace", 2); }
|
|
507
507
|
function writeJson(file, value) { const tmp = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(tmp, file); }
|
|
@@ -141,7 +141,7 @@ export function submitVnextSpecify(context, input) {
|
|
|
141
141
|
throw new AppError("not_found", "--result-file must point to an existing file inside the SPECIFY workspace", 1, { result_file: input.resultFile });
|
|
142
142
|
}
|
|
143
143
|
try {
|
|
144
|
-
validateSchema({ schemaName: "vnext-specify", file: candidateFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
144
|
+
validateSchema({ schemaName: "vnext-specify", file: candidateFile, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
|
|
145
145
|
const result = readVnextSpecifyResult(candidateFile);
|
|
146
146
|
validateObligations(result, candidateFile);
|
|
147
147
|
const normalizedResult = `${JSON.stringify(result, null, 2)}\n`;
|
|
@@ -169,7 +169,7 @@ export function submitVnextSpecify(context, input) {
|
|
|
169
169
|
const htmlPath = path.join(stageRoot, "stage-report.html");
|
|
170
170
|
const report = buildStageReport({ run, work, workSession, outcome, result, resultMarkdown: renderedMarkdown, now, stageRoot, resultFile });
|
|
171
171
|
writeStageReport(stageRoot, report);
|
|
172
|
-
validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
172
|
+
validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
|
|
173
173
|
completeFlowRunStage(context, {
|
|
174
174
|
projectRoot,
|
|
175
175
|
runId: run.id,
|
|
@@ -268,7 +268,7 @@ function readIntake(input) {
|
|
|
268
268
|
}
|
|
269
269
|
function requireRun(context, projectRoot, runId) {
|
|
270
270
|
const project = requireProjectByRoot(context, projectRoot);
|
|
271
|
-
const run = context.db.get("SELECT id, project_id, project_root, workspace_root,
|
|
271
|
+
const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, runId]);
|
|
272
272
|
if (!run)
|
|
273
273
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
274
274
|
return run;
|
|
@@ -280,9 +280,9 @@ function runStatus(context, projectId, runId) {
|
|
|
280
280
|
return run.status;
|
|
281
281
|
}
|
|
282
282
|
function requiredRunHome(run) {
|
|
283
|
-
if (!run.
|
|
284
|
-
throw new AppError("runtime_missing", "RUN has no portable
|
|
285
|
-
return run.
|
|
283
|
+
if (!run.run_root)
|
|
284
|
+
throw new AppError("runtime_missing", "RUN has no portable artifact root", 1, { run_id: run.id });
|
|
285
|
+
return run.run_root;
|
|
286
286
|
}
|
|
287
287
|
function requireWork(context, workId) {
|
|
288
288
|
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [workId]);
|
|
@@ -423,7 +423,7 @@ function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, r
|
|
|
423
423
|
const candidate = `${resultPath}.candidate-${process.pid}`;
|
|
424
424
|
fs.writeFileSync(candidate, result);
|
|
425
425
|
try {
|
|
426
|
-
validateSchema({ schemaName: work.result_schema.replace(/^dd-flow\//, "").replace(/@\d+$/, ""), file: candidate, projectRoot, runId });
|
|
426
|
+
validateSchema({ schemaName: work.result_schema.replace(/^dd-flow\//, "").replace(/@\d+$/, ""), file: candidate, projectRoot, runId, runRoot: runHome });
|
|
427
427
|
}
|
|
428
428
|
finally {
|
|
429
429
|
fs.rmSync(candidate, { force: true });
|
|
@@ -472,7 +472,7 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
|
|
|
472
472
|
function renderWorkerPrompt(context, work, run, dependencies) {
|
|
473
473
|
const command = flowCommand(context);
|
|
474
474
|
const packet = codePacket(work);
|
|
475
|
-
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies the materialization and then executes the check.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
475
|
+
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies the materialization and then executes the check.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
476
476
|
if (packet)
|
|
477
477
|
codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
|
|
478
478
|
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<hard_write_boundary>", `HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements, non-goals and stop_conditions are semantic hard boundaries. planned_write_areas is not.", "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", "The CLI runs every declared required check before accepting this Work. A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", "Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then call Finish again. Do not invent a cause that does not appear in the retained output.", "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command, piping your JSON object to stdin: ${command} work finish ${work.work_id} --result-stdin --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or semantic-contract blocker: ${command} work fail ${work.work_id} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
|
|
@@ -573,10 +573,10 @@ function requireWork(context, id) { ensureWorkRegistry(context); const exact = c
|
|
|
573
573
|
return exact; if (!/^WRK-\d{3,}$/.test(id))
|
|
574
574
|
throw new AppError("not_found", "Work is not registered", 1, { work_id: id }); const matches = context.db.all(`SELECT ${workColumns} FROM works WHERE work_id LIKE ? ORDER BY work_id`, [`${id}-%`]); if (matches.length !== 1)
|
|
575
575
|
throw new AppError(matches.length ? "ambiguous_work_alias" : "not_found", matches.length ? "Short Work alias is ambiguous" : "Work is not registered", 1, { work_id: id, matches: matches.map((work) => work.work_id) }); return matches[0]; }
|
|
576
|
-
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT r.id, r.
|
|
577
|
-
throw new AppError("runtime_missing", "RUN
|
|
578
|
-
function requireRunHome(run) { if (!run.
|
|
579
|
-
throw new AppError("runtime_missing", "RUN
|
|
576
|
+
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT r.id, r.run_root, r.workspace_root, p.root AS project_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE r.project_id = ? AND r.id = ?", [projectId, runId]); if (!run?.run_root)
|
|
577
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: runId }); return run; }
|
|
578
|
+
function requireRunHome(run) { if (!run.run_root)
|
|
579
|
+
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: run.id }); return run.run_root; }
|
|
580
580
|
function parseDependencies(work) { try {
|
|
581
581
|
const value = JSON.parse(work.depends_on_json);
|
|
582
582
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : [];
|
|
@@ -620,8 +620,8 @@ function readJson(file) { try {
|
|
|
620
620
|
catch (error) {
|
|
621
621
|
throw new AppError("validation", `Invalid JSON file: ${String(error)}`, 2, { file });
|
|
622
622
|
} }
|
|
623
|
-
export function refreshRunWorkProjection(context, projectId, runId) { refreshRunSessionProjection(context, projectId, runId); const run = context.db.get("SELECT
|
|
624
|
-
const obsolete = path.join(run.
|
|
623
|
+
export function refreshRunWorkProjection(context, projectId, runId) { refreshRunSessionProjection(context, projectId, runId); const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (run?.run_root) {
|
|
624
|
+
const obsolete = path.join(run.run_root, "work.json");
|
|
625
625
|
if (fs.existsSync(obsolete))
|
|
626
626
|
fs.rmSync(obsolete);
|
|
627
627
|
} }
|
package/dist/storage/database.js
CHANGED
|
@@ -564,6 +564,7 @@ function migrate(db, dbPath) {
|
|
|
564
564
|
target_branch TEXT NOT NULL,
|
|
565
565
|
enqueue_target_head TEXT,
|
|
566
566
|
execution_target_head TEXT,
|
|
567
|
+
accepted_tree TEXT,
|
|
567
568
|
integration_commit TEXT,
|
|
568
569
|
execution_route TEXT NOT NULL,
|
|
569
570
|
status TEXT NOT NULL,
|
|
@@ -808,6 +809,8 @@ function migrate(db, dbPath) {
|
|
|
808
809
|
db.prepare("UPDATE usage SET cache_read_input_tokens = cached_input_tokens WHERE cache_read_input_tokens IS NULL AND cached_input_tokens IS NOT NULL").run();
|
|
809
810
|
ensureColumn(db, "runs", "run_home_path", "ALTER TABLE runs ADD COLUMN run_home_path TEXT");
|
|
810
811
|
ensureColumn(db, "runs", "run_root", "ALTER TABLE runs ADD COLUMN run_root TEXT");
|
|
812
|
+
db.prepare("UPDATE runs SET run_root = COALESCE(run_root, run_home_path, run_dir) WHERE run_root IS NULL OR run_root = ''").run();
|
|
813
|
+
ensureColumn(db, "merge_requests", "accepted_tree", "ALTER TABLE merge_requests ADD COLUMN accepted_tree TEXT");
|
|
811
814
|
ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
|
|
812
815
|
ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
|
|
813
816
|
ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.9",
|
|
4
4
|
"description": "Mechanical runtime CLI for dd-flow workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,15 +16,6 @@
|
|
|
16
16
|
"node": ">=26.0.0",
|
|
17
17
|
"pnpm": ">=10.0.0"
|
|
18
18
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
|
|
21
|
-
"typecheck": "tsc --noEmit",
|
|
22
|
-
"lint": "eslint . --max-warnings=0",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"changeset": "changeset",
|
|
25
|
-
"version-packages": "changeset version",
|
|
26
|
-
"release": "pnpm run build && changeset publish"
|
|
27
|
-
},
|
|
28
19
|
"devDependencies": {
|
|
29
20
|
"@changesets/cli": "^2.31.0",
|
|
30
21
|
"@eslint/js": "^9.39.1",
|
|
@@ -43,5 +34,14 @@
|
|
|
43
34
|
"publishConfig": {
|
|
44
35
|
"access": "public"
|
|
45
36
|
},
|
|
46
|
-
"license": "MIT"
|
|
47
|
-
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"lint": "eslint . --max-warnings=0",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"changeset": "changeset",
|
|
44
|
+
"version-packages": "changeset version",
|
|
45
|
+
"release": "pnpm run build && changeset publish"
|
|
46
|
+
}
|
|
47
|
+
}
|