@deksden-com/dd-flow-cli 0.9.0-beta.104 → 0.9.0-beta.106
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 +12 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +23 -8
- package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +3 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -1
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/services/cleanup.js +81 -32
- package/dist/services/code-checks.js +18 -19
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/controller-fanout.js +12 -4
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/external-work-launch.js +2 -1
- package/dist/services/hooks.js +153 -43
- package/dist/services/lanes.js +23 -18
- package/dist/services/lifecycle-command.js +11 -2
- package/dist/services/lifecycle-invocations.js +291 -54
- package/dist/services/managed-processes.js +35 -23
- package/dist/services/merge-queue.js +71 -67
- package/dist/services/merge-server.js +5 -12
- package/dist/services/migrations.js +28 -12
- package/dist/services/plan-runtime.js +8 -4
- package/dist/services/projects.js +3 -11
- package/dist/services/protocols.js +98 -68
- package/dist/services/recovery-snapshot-database.js +43 -8
- package/dist/services/run-control-worker.js +15 -32
- package/dist/services/run-control.js +3 -17
- package/dist/services/run-controller-capture.js +58 -27
- package/dist/services/run-controller-process.js +21 -30
- package/dist/services/run-controller-recovery.js +36 -37
- package/dist/services/run-controller.js +83 -88
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +25 -14
- package/dist/services/run-recovery.js +56 -54
- package/dist/services/runs.js +218 -160
- package/dist/services/runtime-budget.js +19 -45
- package/dist/services/runtime-scope-capture.js +8 -6
- package/dist/services/runtime-scope-control.js +49 -39
- package/dist/services/runtime-scope-resume.js +134 -122
- package/dist/services/runtime-scope-worker.js +48 -49
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/stage-context.js +6 -1
- package/dist/services/stage-lifecycle.js +1 -1
- package/dist/services/stage-pause.js +13 -29
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/usage.js +2 -9
- package/dist/services/vnext-code-review.js +10 -7
- package/dist/services/vnext-code.js +9 -17
- package/dist/services/vnext-merge.js +64 -21
- package/dist/services/vnext-plan-review.js +43 -30
- package/dist/services/vnext-plan.js +28 -24
- package/dist/services/vnext-protocolize.js +183 -20
- package/dist/services/vnext-specify.js +27 -19
- package/dist/services/work-registry.js +229 -150
- package/dist/storage/database.js +286 -56
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/writer-contract.js +3 -3
- package/dist/storage/writer-migration.js +24 -41
- package/package.json +1 -1
- package/tools/repair-paused-run-status.mjs +48 -23
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
3
|
+
import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion } from "./runs.js";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
@@ -15,12 +15,13 @@ import { prepareVnextPlanArtifacts, validateVnextPlanArtifacts } from "./vnext-p
|
|
|
15
15
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
16
16
|
import { nextWorkId } from "./ids.js";
|
|
17
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
18
|
-
import {
|
|
18
|
+
import { acceptStagePacket, acceptStageReport, prepareStagePacket, prepareStageReport } from "./stage-report-renderer.js";
|
|
19
19
|
import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
20
20
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
21
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
22
|
import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
23
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
|
+
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
24
25
|
const stage = "plan-review";
|
|
25
26
|
const stageDir = vnextStageDirectory(stage);
|
|
26
27
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
@@ -115,20 +116,24 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
115
116
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
116
117
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
117
118
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
119
|
+
const reportGit = gitFacts(run.workspace_root);
|
|
120
|
+
const nextCommand = codeCommand(context, run.id, projectRoot);
|
|
118
121
|
return withStageSettlement(context, () => {
|
|
119
122
|
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
|
|
120
|
-
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, nextCommand });
|
|
124
|
+
const preparedReport = prepareStageReport(report);
|
|
125
|
+
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
126
|
+
const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
127
|
+
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
|
|
123
128
|
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_off", nextAction: "start_code" });
|
|
124
129
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_opened", mode: effective, outcome: "off" });
|
|
125
|
-
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command:
|
|
130
|
+
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: nextCommand } };
|
|
126
131
|
});
|
|
127
132
|
}
|
|
128
133
|
const workId = nextWorkId(context, project.id, "plan-review");
|
|
129
134
|
context.db.run("INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)", [workId, project.id, run.id, rootWork, reviewTask, now, now, now]);
|
|
130
135
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
|
|
131
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity })
|
|
136
|
+
const prompt = `${orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity })}\n${stageLifecycleInstruction}\n`;
|
|
132
137
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
133
138
|
fs.writeFileSync(promptPath, prompt);
|
|
134
139
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
@@ -215,7 +220,7 @@ export function recordVnextPlanReviewCapacity(context, input) {
|
|
|
215
220
|
return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
|
|
216
221
|
}
|
|
217
222
|
export function finishVnextPlanReview(context, input) {
|
|
218
|
-
return
|
|
223
|
+
return finishPlanReviewOwned(context, input);
|
|
219
224
|
}
|
|
220
225
|
function finishPlanReviewOwned(context, input) {
|
|
221
226
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -245,7 +250,15 @@ function finishPlanReviewOwned(context, input) {
|
|
|
245
250
|
if (children.some((child) => !["completed", "failed", "cancelled"].includes(child.status)))
|
|
246
251
|
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
247
252
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
248
|
-
|
|
253
|
+
const decisionReceipt = prepareDecisionReceipt(root, decision, input.decisionText);
|
|
254
|
+
const now = context.now();
|
|
255
|
+
const reportGit = gitFacts(run.workspace_root);
|
|
256
|
+
const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: decision.outcome, groups: contextFile.groups ?? [], batchChecksum: contextFile.system?.batch_checksum ?? checksum(batch), planChecksum: contextFile.system?.plan_checksum ?? planSetChecksum(home, run.workspace_root), code: {}, now, projectRoot, decision, children: latestReviewChildren(children), flow: flowCommand(context), git: reportGit });
|
|
257
|
+
const preparedReport = prepareStageReport(report);
|
|
258
|
+
const stageStatus = decision.outcome === "cancelled" ? "skipped" : "failed";
|
|
259
|
+
const completionInput = { projectRoot, runId: run.id, stage, status: stageStatus, data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
260
|
+
const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
|
|
261
|
+
return withStageSettlement(context, () => finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, parentWorkId: parent.work_id, decision, now, decisionReceipt, preparedReport, completionInput, preparedCompletion }));
|
|
249
262
|
}
|
|
250
263
|
const latestChildren = latestReviewChildren(children);
|
|
251
264
|
const evidenceFailures = reviewerEvidenceFailures(context, { projectId: project.id, parentWorkId: parent.work_id, children: latestChildren, groups, planRevision: reviewedPlanRevision, workspaceRoot: run.workspace_root, runHome: home, runId: run.id });
|
|
@@ -288,20 +301,27 @@ function finishPlanReviewOwned(context, input) {
|
|
|
288
301
|
}
|
|
289
302
|
const finalPlanChecksum = planSetChecksum(home, run.workspace_root);
|
|
290
303
|
const finalBatchChecksum = prepared.batchChecksum;
|
|
304
|
+
prepared.publish();
|
|
305
|
+
const decisionReceipt = prepareDecisionReceipt(root, decision, input.decisionText);
|
|
306
|
+
const codeParentId = rootWorkId(context, project.id, run.id);
|
|
307
|
+
const codeItems = prepareWorkBatch(context, { parentWorkId: codeParentId, file: batch, stage: "code" });
|
|
308
|
+
const reportGit = gitFacts(run.workspace_root);
|
|
309
|
+
const nextCommand = codeCommand(context, run.id, projectRoot);
|
|
291
310
|
return withStageSettlement(context, () => {
|
|
292
|
-
prepared
|
|
293
|
-
preserveDecisionReceipt(root, decision, input.decisionText);
|
|
294
|
-
const registered = registerCode(context, project.id, run.id, batch);
|
|
311
|
+
const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
|
|
295
312
|
const now = context.now();
|
|
296
313
|
settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
|
|
297
|
-
const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context) });
|
|
314
|
+
const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context), git: reportGit, nextCommand });
|
|
298
315
|
applyReviewResults(home, latestChildren, decision);
|
|
299
|
-
|
|
316
|
+
const preparedReport = prepareStageReport(report);
|
|
317
|
+
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage, prepared: decisionReceipt });
|
|
318
|
+
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
319
|
+
const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: reportGit };
|
|
320
|
+
completeFlowRunStage(context, completionInput, prepareFlowRunStageCompletion(context, completionInput));
|
|
300
321
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
301
|
-
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
302
322
|
advanceFlowRun(context, { settlementStage: "plan-review", projectRoot, runId: run.id, status: "running", verdict: "plan_review_accepted", nextAction: "start_code" });
|
|
303
323
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_review_accepted", work_id: parent.work_id, code: registered });
|
|
304
|
-
return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command:
|
|
324
|
+
return { ok: true, run_id: run.id, stage, outcome: "accepted", code: registered, next_action: "start_code", next: { kind: "start_stage", stage: "code", command: nextCommand } };
|
|
305
325
|
});
|
|
306
326
|
}
|
|
307
327
|
function preparedExisting(context, input) {
|
|
@@ -433,9 +453,8 @@ catch {
|
|
|
433
453
|
} }
|
|
434
454
|
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)
|
|
435
455
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
|
|
436
|
-
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch, stage: "code" }); }
|
|
437
456
|
function readDecision(context, projectRoot, runId, runRoot, file) { return prepareJsonSchema(file, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }).value; }
|
|
438
|
-
function
|
|
457
|
+
function prepareDecisionReceipt(root, decision, preparedText) {
|
|
439
458
|
const receipt = path.join(root, "decision.receipt.json");
|
|
440
459
|
// The CLI has already parsed and validated this decision during prepare.
|
|
441
460
|
// Persist that immutable value rather than re-opening a caller-controlled
|
|
@@ -444,9 +463,8 @@ function preserveDecisionReceipt(root, decision, preparedText) {
|
|
|
444
463
|
if (fs.existsSync(receipt)) {
|
|
445
464
|
if (!bytes.equals(fs.readFileSync(receipt)))
|
|
446
465
|
throw new AppError("decision_receipt_conflict", "The immutable PLAN-REVIEW decision receipt already contains a different payload", 2, { receipt });
|
|
447
|
-
return;
|
|
448
466
|
}
|
|
449
|
-
|
|
467
|
+
return prepareStagePacket(receipt, bytes);
|
|
450
468
|
}
|
|
451
469
|
function reviewerResult(result) { try {
|
|
452
470
|
const value = JSON.parse(result ?? "");
|
|
@@ -506,18 +524,16 @@ function settleReviewParent(context, input) {
|
|
|
506
524
|
context.db.run("UPDATE work_sessions SET status = ?, result_path = COALESCE(?, result_path), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, input.resultPath ?? null, now, now, workSession.id]);
|
|
507
525
|
}
|
|
508
526
|
function finishTerminalReview(context, input) {
|
|
509
|
-
const now =
|
|
527
|
+
const now = input.now;
|
|
510
528
|
const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
|
|
511
529
|
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") });
|
|
512
530
|
const rootWork = rootWorkId(context, input.projectId, input.run.id);
|
|
513
531
|
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]);
|
|
514
532
|
closeRunningWorkSession(context, rootWork, childStatus, now);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
533
|
+
acceptStagePacket(context, { projectId: input.projectId, runId: input.run.id, stage, prepared: input.decisionReceipt });
|
|
534
|
+
acceptStageReport(context, { projectId: input.projectId, runId: input.run.id, stage, root: input.root, prepared: input.preparedReport });
|
|
535
|
+
completeFlowRunStage(context, input.completionInput, input.preparedCompletion);
|
|
518
536
|
refreshRunWorkProjection(context, input.projectId, input.run.id);
|
|
519
|
-
const stageStatus = input.decision.outcome === "cancelled" ? "skipped" : input.decision.outcome === "failed" ? "failed" : "blocked";
|
|
520
|
-
completeFlowRunStage(context, { projectRoot: input.projectRoot, runId: input.run.id, stage, status: stageStatus, data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
521
537
|
completeFlowRun(context, { projectRoot: input.projectRoot, runId: input.run.id, status: input.decision.outcome === "cancelled" ? "cancelled" : input.decision.outcome === "failed" ? "failed" : "blocked", verdict: `plan_review_${input.decision.outcome}`, nextAction: "resolve_plan_review_blocker" });
|
|
522
538
|
appendFlowRunTimelineEvent(context, input.projectId, input.run.id, { type: "plan_review_terminal", outcome: input.decision.outcome, work_id: input.parentWorkId });
|
|
523
539
|
return { ok: true, run_id: input.run.id, stage, outcome: input.decision.outcome, next_action: "resolve_plan_review_blocker" };
|
|
@@ -534,12 +550,9 @@ function reportFor(context, input) {
|
|
|
534
550
|
const sessionCommand = `${input.flow} stat run sessions ls --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
|
|
535
551
|
const usageCommand = `${input.flow} stat usage --run ${input.run.id} --project-root ${JSON.stringify(input.projectRoot)} --json`;
|
|
536
552
|
const changedFiles = correction?.status === "applied" ? correction.changed_paths : [];
|
|
537
|
-
return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: managedLifecycleCommand(context, `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json`) } : {}) }, 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" } };
|
|
553
|
+
return { schema_id: "dd-flow/stage-report@1", run_id: input.run.id, stage, generated_at: input.now, verdict: accepted ? "done" : input.outcome, semantic: { result, acceptance: [input.outcome], changed_files: changedFiles, checks: ["accepted PLAN report", "generated CODE batch", "reviewer evidence", ...(correction ? ["mechanical correction receipt"] : [])], evidence: [], next_action: accepted ? "start_code" : "resolve_plan_review_blocker", plan_review: { requested_mode: input.requested, effective_mode: input.mode, outcome: input.outcome, groups: input.groups.map((group) => group.key), initial_plan_revision: input.initialPlanRevision ?? null, final_plan_revision: input.finalPlanRevision ?? input.initialPlanRevision ?? null, initial_plan_checksum: input.planChecksum, final_plan_checksum: input.finalPlanChecksum ?? input.planChecksum, initial_batch_checksum: input.batchChecksum, final_batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, correction, decision: input.decision ?? null, reviewer_works: input.children?.map((child) => ({ work_id: child.work_id, status: child.status, verdict: reviewerVerdict(child.result) })) ?? [] }, plan_checksum: input.finalPlanChecksum ?? input.planChecksum, batch_checksum: input.finalBatchChecksum ?? input.batchChecksum, code: input.code }, mechanical: { started_at: timing.started_at, finished_at: input.now, wall_clock_ms: timing.wall_clock_ms, git: input.git ?? gitFacts(input.run.workspace_root), session_stats_command: sessionCommand, usage_stats_command: usageCommand, ...(accepted ? { next_command: input.nextCommand ?? managedLifecycleCommand(context, `${input.flow} stage start ${input.run.id} --stage code --project-root ${JSON.stringify(input.projectRoot)} --json`) } : {}) }, 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" } };
|
|
538
554
|
}
|
|
539
555
|
function stageTiming(run, now) { const started = readIndex(run).stage_runs?.find((item) => item.stage === stage && typeof item.started_at === "string"); const startedAt = started?.started_at ?? now; const milliseconds = Date.parse(now) - Date.parse(startedAt); return { started_at: startedAt, wall_clock_ms: Number.isFinite(milliseconds) && milliseconds >= 0 ? milliseconds : null }; }
|
|
540
|
-
function writeReport(root, report) {
|
|
541
|
-
writeStageReport(root, report);
|
|
542
|
-
}
|
|
543
556
|
function codeCommand(context, runId, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} stage start ${runId} --stage code --project-root ${JSON.stringify(projectRoot)} --json`); }
|
|
544
557
|
function dispatchCommand(context, runId, projectRoot) { return `${flowCommand(context)} plan-review dispatch ${runId} --project-root ${JSON.stringify(projectRoot)} --json`; }
|
|
545
558
|
function finishCommand(context, runId, projectRoot, decision) { return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --decision-file ${JSON.stringify(decision)} --json`); }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
2
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
-
import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
4
|
+
import { withStageSettlement, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion } from "./runs.js";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { AppError } from "../shared/errors.js";
|
|
@@ -17,9 +17,10 @@ import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
|
17
17
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
18
18
|
import { nextWorkId } from "./ids.js";
|
|
19
19
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
20
|
-
import {
|
|
20
|
+
import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
|
|
21
21
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
22
22
|
import { subagentCapacityKey } from "./vnext-fanout.js";
|
|
23
|
+
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
23
24
|
export function isVnextPlanRun(context, input) {
|
|
24
25
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
25
26
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
@@ -70,19 +71,13 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
|
|
|
70
71
|
if (!context.db.get("SELECT 1 FROM work_sessions WHERE work_id = ? LIMIT 1", [rootWork.work_id])) {
|
|
71
72
|
bindRunningWorkSession(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, promptPath: path.join(root, "fixture-root.prompt.md") });
|
|
72
73
|
}
|
|
73
|
-
context.db.
|
|
74
|
-
let planWorkId;
|
|
75
|
-
try {
|
|
74
|
+
const planWorkId = context.db.writeTransaction(() => {
|
|
76
75
|
if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
|
|
77
76
|
throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
|
|
78
|
-
planWorkId = nextWorkId(context, project.id, "plan");
|
|
77
|
+
const planWorkId = nextWorkId(context, project.id, "plan");
|
|
79
78
|
context.db.run(`INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)`, [planWorkId, project.id, run.id, rootWork.work_id, planTask, now, now, now]);
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
context.db.exec("ROLLBACK");
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
79
|
+
return planWorkId;
|
|
80
|
+
});
|
|
86
81
|
// A Desktop task may start above the materialized repository. Lifecycle
|
|
87
82
|
// prompts therefore hand agents write targets as absolute paths: relative
|
|
88
83
|
// `.memory-bank/...` paths would otherwise silently land in the parent cwd.
|
|
@@ -112,7 +107,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
|
|
|
112
107
|
// This explicit final rule supersedes historical pack wording: a flow gate
|
|
113
108
|
// has only executable autonomous evidence. Human or external confirmation
|
|
114
109
|
// is neither a check nor a permitted completion condition.
|
|
115
|
-
fs.writeFileSync(promptPath, `${prompt}\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n`);
|
|
110
|
+
fs.writeFileSync(promptPath, `${prompt}\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n${stageLifecycleInstruction}\n`);
|
|
116
111
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
117
112
|
fs.writeFileSync(path.join(root, "work-context.json"), JSON.stringify({ schema_id: "dd-flow/work-context@1", system: { run_id: run.id, work_id: planWorkId, stage: "plan" }, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root }, artifacts: artifactMaterialization, input: { protocols, owned_obligations: Object.fromEntries(owned) } }, null, 2));
|
|
118
113
|
const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
@@ -135,7 +130,7 @@ export function prepareVnextPlanFinish(context, input) {
|
|
|
135
130
|
return prepared;
|
|
136
131
|
}
|
|
137
132
|
export function finishVnextPlan(context, input) {
|
|
138
|
-
return
|
|
133
|
+
return finishVnextPlanOwned(context, input);
|
|
139
134
|
}
|
|
140
135
|
function finishVnextPlanOwned(context, input) {
|
|
141
136
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -163,21 +158,30 @@ function finishVnextPlanOwned(context, input) {
|
|
|
163
158
|
prepared.publish();
|
|
164
159
|
const now = context.now();
|
|
165
160
|
const batchChecksum = checksum(batch);
|
|
166
|
-
context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
|
|
167
|
-
context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
|
|
168
|
-
refreshRunWorkProjection(context, project.id, run.id);
|
|
169
161
|
const reviewCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
170
|
-
const
|
|
162
|
+
const startedAt = stageStartedAt(home, now);
|
|
163
|
+
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: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), 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" } };
|
|
171
164
|
Object.assign(report.semantic, {
|
|
172
165
|
plan_checksums: planFiles.map((file) => ({ path: path.relative(run.workspace_root, file).split(path.sep).join("/"), sha256: checksum(file) })),
|
|
173
166
|
aspect_map_checksums: mapFiles.map((file) => ({ path: path.relative(home, file).split(path.sep).join("/"), sha256: checksum(file) }))
|
|
174
167
|
});
|
|
175
|
-
const
|
|
176
|
-
validateSchema({ schemaName: "stage-report", file:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
168
|
+
const preparedReport = prepareStageReport(report);
|
|
169
|
+
validateSchema({ schemaName: "stage-report", file: path.join(root, "stage-report.json"), data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
170
|
+
const completionInput = { 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", dataSha256: preparedReport.sha256, git: report.mechanical.git };
|
|
171
|
+
const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
|
|
172
|
+
return withStageSettlement(context, () => {
|
|
173
|
+
const current = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id]);
|
|
174
|
+
if (current?.status !== "running")
|
|
175
|
+
throw new AppError("invalid_work_state", "PLAN Work changed during finish preparation", 1);
|
|
176
|
+
context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
|
|
177
|
+
context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
|
|
178
|
+
acceptStageReport(context, { projectId: project.id, runId: run.id, stage: "plan", root, prepared: preparedReport });
|
|
179
|
+
completeFlowRunStage(context, completionInput, preparedCompletion);
|
|
180
|
+
refreshRunWorkProjection(context, project.id, run.id);
|
|
181
|
+
advanceFlowRun(context, { settlementStage: "plan", projectRoot, runId: run.id, status: "running", verdict: "planned", nextAction: "start_plan_review" });
|
|
182
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "plan_accepted", work_id: work.work_id, protocols, id: workSession.id, next_stage: "plan-review" });
|
|
183
|
+
return { ok: true, run_id: run.id, protocols, next_action: "start_plan_review", next_command: reviewCommand, next: { kind: "start_stage", stage: "plan-review", command: reviewCommand } };
|
|
184
|
+
});
|
|
181
185
|
}
|
|
182
186
|
export function validateVnextPlanArtifacts(context, input) {
|
|
183
187
|
// Validation never repairs or republishes accepted artifacts. PLAN and
|