@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111
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 +526 -0
- package/README.md +68 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +305 -0
- package/dist/cli/help.js +89 -13
- package/dist/cli/hook-ingress.js +77 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/native-hook-ingress.js +48 -0
- package/dist/cli/run-cli.js +1465 -384
- package/dist/cli.js +8 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +52 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +26 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +226 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +500 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +149 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +563 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +475 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +394 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +219 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +645 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +982 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +327 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +108 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +71 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-work-batch.schema.json +3 -3
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +132 -39
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +589 -106
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +173 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +994 -97
- package/dist/services/execution-policy.js +181 -0
- package/dist/services/external-work-launch.js +166 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +702 -327
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +113 -18
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1379 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +302 -34
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +66 -28
- package/dist/services/migrations.js +37 -16
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +20 -15
- package/dist/services/prompts.js +25 -16
- package/dist/services/protocols.js +160 -86
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +142 -0
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +351 -0
- package/dist/services/run-control.js +888 -0
- package/dist/services/run-controller-adapter.js +231 -0
- package/dist/services/run-controller-capture.js +167 -0
- package/dist/services/run-controller-process.js +180 -0
- package/dist/services/run-controller-recovery.js +221 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +798 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +142 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +80 -0
- package/dist/services/run-recovery.js +345 -0
- package/dist/services/runs.js +536 -181
- package/dist/services/runtime-budget.js +413 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +460 -0
- package/dist/services/runtime-scope-resume.js +577 -0
- package/dist/services/runtime-scope-stop.js +159 -0
- package/dist/services/runtime-scope-worker.js +242 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +50 -19
- package/dist/services/stage-lifecycle.js +100 -103
- package/dist/services/stage-pause.js +88 -75
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +35 -98
- package/dist/services/vnext-code-review.js +197 -84
- package/dist/services/vnext-code.js +278 -251
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +172 -17
- package/dist/services/vnext-merge.js +345 -111
- package/dist/services/vnext-plan-review.js +171 -111
- package/dist/services/vnext-plan.js +180 -87
- package/dist/services/vnext-protocolize.js +320 -74
- package/dist/services/vnext-specify.js +115 -77
- package/dist/services/work-registry.js +1001 -187
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +88 -36
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +692 -35
- 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/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +24 -13
- package/tools/repair-paused-run-status.mjs +84 -0
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
6
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
7
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
7
8
|
import { ensureVnextWorkStorage } from "../storage/database.js";
|
|
8
|
-
import { resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
|
|
9
|
+
import { projectHome, resolveProjectRoot, writeJsonAtomic } from "../storage/paths.js";
|
|
9
10
|
import { preflightMemoryPermissions } from "./memory-permissions.js";
|
|
10
11
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
11
|
-
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, startFlowRun } from "./runs.js";
|
|
12
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunStatus, prepareFlowRunStageCompletion, prepareFlowRunStart, startFlowRun, withStageSettlement } from "./runs.js";
|
|
13
|
+
import { resolveStageTransition } from "./execution-policy.js";
|
|
12
14
|
import { validateSchema } from "./schema-validation.js";
|
|
13
15
|
import { bindRunningWorkSession, refreshRunWorkProjection } from "./work-registry.js";
|
|
14
16
|
import { nextWorkId } from "./ids.js";
|
|
15
17
|
import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
16
|
-
import {
|
|
18
|
+
import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
|
|
19
|
+
import { prepareError, prepareJsonSchema, prepareJsonText, prepareOutputDirectory, prepareOutputFile, prepareTextFile } from "../cli/input-preparation.js";
|
|
20
|
+
import { hookSessionIdentity } from "./hooks.js";
|
|
21
|
+
import { assertRecoveryHookCurrent, assertRunMutationAllowed } from "./run-recovery.js";
|
|
22
|
+
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
17
23
|
const flowId = "mb-sdlc-vnext-specify";
|
|
18
24
|
const protocolizeFlowId = "mb-sdlc-vnext-protocolize";
|
|
19
25
|
const flowVersion = 4;
|
|
@@ -27,28 +33,57 @@ export function isVnextSpecifyRun(context, input) {
|
|
|
27
33
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
28
34
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind IN ('vnext_specify', 'vnext_protocolize')", [project.id, input.runId]));
|
|
29
35
|
}
|
|
30
|
-
export function
|
|
31
|
-
ensureVnextWorkStorage(context.db);
|
|
36
|
+
export function prepareVnextSpecifyStart(context, input) {
|
|
32
37
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
33
|
-
registerProject(context, { root: projectRoot });
|
|
34
38
|
const intake = readIntake(input);
|
|
35
39
|
const discussion = intake.markdown.trim();
|
|
36
40
|
if (!discussion)
|
|
37
41
|
throw new AppError("validation", "SPECIFY intake must not be empty", 2);
|
|
38
42
|
const flow = assertFlowDefinition(projectRoot);
|
|
39
43
|
const template = readRequiredFile(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "specify.md"), "vNext SPECIFY prompt template");
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
const startInput = { projectRoot, flowKind: flow.id === protocolizeFlowId ? "vnext_protocolize" : "vnext_specify", subjectType: "discussion", subjectId: "SPECIFY", slug: input.slug ?? "specify", nextAction: "await_work_session" };
|
|
45
|
+
const start = input.runId ? undefined : prepareFlowRunStart(context, startInput);
|
|
46
|
+
if (input.runId)
|
|
47
|
+
assertSpecifyNotStarted(context, projectRoot, input.runId);
|
|
48
|
+
const run = input.runId ? requireRun(context, projectRoot, input.runId) : undefined;
|
|
49
|
+
if (!run) {
|
|
50
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
51
|
+
prepareOutputDirectory(project ? path.join(projectHome(context.ddFlowHome, project.id), "runs") : path.join(context.ddFlowHome, "projects"));
|
|
52
|
+
}
|
|
53
|
+
if (run) {
|
|
54
|
+
assertRunMutationAllowed(context, run.project_id, run.id);
|
|
55
|
+
const home = requiredRunHome(run), stageRoot = path.join(home, "01-specify");
|
|
56
|
+
for (const relative of ["intake/discussion.md", "01-specify/prompt.md", "01-specify/work-context.json"])
|
|
57
|
+
prepareOutputFile(path.join(home, relative));
|
|
58
|
+
if (input.externalContext)
|
|
59
|
+
prepareExternalStageContext({ stageRoot, loaded: input.externalContext });
|
|
60
|
+
}
|
|
61
|
+
const preflight = preflightMemoryPermissions({ root: projectRoot, memoryBank: ".memory-bank", flow: "custom", mode: "read", targets: [{ path: ".memory-bank", mode: "read" }] });
|
|
62
|
+
if (preflight.ok !== true)
|
|
63
|
+
throw new AppError("permission_preflight_failed", "SPECIFY project context is not readable", 1, { preflight });
|
|
64
|
+
const grounding = projectGrounding(projectRoot);
|
|
65
|
+
const handoff = run ? executionSnapshot(context, run.project_id, run.id, projectRoot, run.workspace_root) : executionSnapshotFromProfile(context, start?.executionProfile, projectRoot, projectRoot);
|
|
66
|
+
return { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff };
|
|
67
|
+
}
|
|
68
|
+
function assertSpecifyNotStarted(context, projectRoot, runId) {
|
|
69
|
+
const existing = getFlowRunStatus(context, { projectRoot, runId });
|
|
70
|
+
if (existing.index?.stage_runs?.some(entry => entry.stage === stageId))
|
|
71
|
+
throw new AppError("stage_already_started", "SPECIFY is already started for this RUN", 1, { run_id: runId });
|
|
72
|
+
}
|
|
73
|
+
export function launchVnextSpecify(context, input, prepared = prepareVnextSpecifyStart(context, input)) {
|
|
74
|
+
const { projectRoot, intake, discussion, template, startInput, start, preflight, grounding, handoff } = prepared;
|
|
75
|
+
if (input.runId)
|
|
76
|
+
assertSpecifyNotStarted(context, projectRoot, input.runId);
|
|
77
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
78
|
+
const identity = hookSessionIdentity(context, project.id, input.hookEventId);
|
|
79
|
+
if (input.runId) {
|
|
80
|
+
assertRunMutationAllowed(context, project.id, input.runId);
|
|
81
|
+
assertRecoveryHookCurrent(context, project.id, input.runId, identity);
|
|
82
|
+
}
|
|
83
|
+
ensureVnextWorkStorage(context.db);
|
|
84
|
+
registerProject(context, { root: projectRoot });
|
|
85
|
+
const started = input.runId ? null : startFlowRun(context, startInput, start);
|
|
48
86
|
const run = requireRun(context, projectRoot, input.runId ?? started.run.id);
|
|
49
|
-
const existingStages = getFlowRunStatus(context, { projectRoot, runId: run.id });
|
|
50
|
-
if (existingStages.index?.stage_runs?.some((entry) => entry.stage === stageId))
|
|
51
|
-
throw new AppError("stage_already_started", "SPECIFY is already started for this RUN", 1, { run_id: run.id });
|
|
52
87
|
const runHome = requiredRunHome(run);
|
|
53
88
|
const stageRoot = path.join(runHome, "01-specify");
|
|
54
89
|
const intakeDir = path.join(runHome, "intake");
|
|
@@ -61,11 +96,6 @@ export function launchVnextSpecify(context, input) {
|
|
|
61
96
|
const contextPath = path.join(stageRoot, "work-context.json");
|
|
62
97
|
const workId = nextWorkId(context, run.project_id, "root");
|
|
63
98
|
const now = context.now();
|
|
64
|
-
const preflight = writablePreflight(projectRoot, stageRoot);
|
|
65
|
-
if (preflight.ok !== true)
|
|
66
|
-
throw new AppError("permission_preflight_failed", "SPECIFY workspace is not writable", 1, { preflight });
|
|
67
|
-
const grounding = projectGrounding(projectRoot);
|
|
68
|
-
const handoff = executionSnapshot(context, run.id);
|
|
69
99
|
const workContext = {
|
|
70
100
|
schema_id: "dd-flow/work-context@1",
|
|
71
101
|
system: { run_id: run.id, work_id: workId, stage: stageId },
|
|
@@ -82,7 +112,7 @@ export function launchVnextSpecify(context, input) {
|
|
|
82
112
|
};
|
|
83
113
|
writeJson(contextPath, workContext);
|
|
84
114
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage: stageId, workId, projectRoot });
|
|
85
|
-
const prompt = renderPrompt({ workId, runId: run.id, projectRoot, stageRoot, resultPath, discussion, template, workContext, grounding, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand),
|
|
115
|
+
const prompt = renderPrompt({ workId, runId: run.id, projectRoot, stageRoot, resultPath, discussion, template, workContext, grounding, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), finishCommand: managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${run.id} --project-root ${JSON.stringify(projectRoot)} --stage specify --result-file ${JSON.stringify(path.join(stageRoot, "specify-result.json"))} --outcome specified --json`) });
|
|
86
116
|
fs.writeFileSync(promptPath, prompt);
|
|
87
117
|
const externalContext = applyExternalStageContext({ stageRoot, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
88
118
|
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)
|
|
@@ -126,7 +156,9 @@ export function submitVnextSpecify(context, input) {
|
|
|
126
156
|
if (work.project_id !== project.id)
|
|
127
157
|
throw new AppError("project_mismatch", "Work does not belong to --project-root", 1, { work_id: work.work_id });
|
|
128
158
|
if (work.status !== "running") {
|
|
129
|
-
|
|
159
|
+
// This check precedes all result reads and mutations. A later legal state
|
|
160
|
+
// transition may make a new invocation appropriate, but never replays this one.
|
|
161
|
+
throw new AppError("invalid_work_state", "Work is not waiting for a SPECIFY result", 1, { work_id: work.work_id, status: work.status, retryable_no_effect: true });
|
|
130
162
|
}
|
|
131
163
|
const run = requireRun(context, projectRoot, work.run_id);
|
|
132
164
|
if (runStatus(context, project.id, run.id) !== "running") {
|
|
@@ -156,21 +188,14 @@ export function submitVnextSpecify(context, input) {
|
|
|
156
188
|
const handoff = continuesToProtocolize ? executionSnapshotFromPath(path.join(stageRoot, "work-context.json"), work.work_id) : null;
|
|
157
189
|
const sameSessionContinuation = handoff?.stage_handoff.effective === "same_session";
|
|
158
190
|
const terminal = !continuesToProtocolize && (outcome === "specified" || outcome === "failed" || outcome === "cancelled");
|
|
159
|
-
if (terminal) {
|
|
160
|
-
context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [outcome === "specified" ? "completed" : outcome, normalizedResult, now, now, work.work_id]);
|
|
161
|
-
context.db.run("UPDATE work_sessions SET status = ?, result_path = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [outcome === "specified" ? "completed" : outcome, resultFile, now, now, work.work_id]);
|
|
162
|
-
}
|
|
163
191
|
const workSession = latestWorkSession(context, work.work_id);
|
|
164
|
-
if (workSession && !sameSessionContinuation) {
|
|
165
|
-
context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [resultFile, now, now, workSession.id]);
|
|
166
|
-
}
|
|
167
192
|
const reportPath = path.join(stageRoot, "stage-report.json");
|
|
168
193
|
const markdownPath = path.join(stageRoot, "stage-report.md");
|
|
169
194
|
const htmlPath = path.join(stageRoot, "stage-report.html");
|
|
170
195
|
const report = buildStageReport({ run, work, workSession, outcome, result, resultMarkdown: renderedMarkdown, now, stageRoot, resultFile });
|
|
171
|
-
|
|
172
|
-
validateSchema({ schemaName: "stage-report", file: reportPath, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
|
|
173
|
-
|
|
196
|
+
const preparedReport = prepareStageReport(report);
|
|
197
|
+
validateSchema({ schemaName: "stage-report", file: reportPath, data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: runHome });
|
|
198
|
+
const completion = {
|
|
174
199
|
projectRoot,
|
|
175
200
|
runId: run.id,
|
|
176
201
|
stage: stageId,
|
|
@@ -178,7 +203,20 @@ export function submitVnextSpecify(context, input) {
|
|
|
178
203
|
data: "specify.json",
|
|
179
204
|
dataSchemaId: "dd-flow/specify@1",
|
|
180
205
|
report: "stage-report.md",
|
|
181
|
-
stageReport: "stage-report.html"
|
|
206
|
+
stageReport: "stage-report.html",
|
|
207
|
+
dataSha256: crypto.createHash("sha256").update(fs.readFileSync(resultFile)).digest("hex")
|
|
208
|
+
};
|
|
209
|
+
const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
|
|
210
|
+
withStageSettlement(context, () => {
|
|
211
|
+
const current = requireWork(context, work.work_id);
|
|
212
|
+
if (current.status !== "running" || current.updated_at !== work.updated_at)
|
|
213
|
+
throw new AppError("work_generation_stale", "SPECIFY Work changed during finish preparation", 1, { work_id: work.work_id });
|
|
214
|
+
if (terminal && context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running' AND updated_at = ?", [outcome === "specified" ? "completed" : outcome, normalizedResult, now, now, work.work_id, work.updated_at]).changes !== 1)
|
|
215
|
+
throw new AppError("work_generation_stale", "SPECIFY Work changed during finish preparation", 1, { work_id: work.work_id });
|
|
216
|
+
if (workSession && !sameSessionContinuation && context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ? AND status = 'running' AND updated_at = ?", [resultFile, now, now, workSession.id, workSession.updated_at]).changes !== 1)
|
|
217
|
+
throw new AppError("work_generation_stale", "SPECIFY Session changed during finish preparation", 1, { work_id: work.work_id, work_session_id: workSession.id });
|
|
218
|
+
acceptStageReport(context, { projectId: project.id, runId: run.id, stage: stageId, root: stageRoot, prepared: preparedReport });
|
|
219
|
+
completeFlowRunStage(context, completion, preparedCompletion);
|
|
182
220
|
});
|
|
183
221
|
const updatedWork = requireWork(context, work.work_id);
|
|
184
222
|
const updatedWorkSession = workSession ? requireWorkSession(context, workSession.id) : null;
|
|
@@ -187,9 +225,9 @@ export function submitVnextSpecify(context, input) {
|
|
|
187
225
|
appendFlowRunTimelineEvent(context, run.project_id, run.id, { type: "work_completed", work_id: work.work_id, outcome, stage: stageId });
|
|
188
226
|
if (continuesToProtocolize) {
|
|
189
227
|
const nextWork = requireWork(context, work.work_id);
|
|
190
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
|
|
228
|
+
advanceFlowRun(context, { settlementStage: "specify", projectRoot, runId: run.id, status: "running", verdict: "specified", nextAction: "start_protocolize" });
|
|
191
229
|
refreshRunWorkProjection(context, run.project_id, run.id);
|
|
192
|
-
const nextCommand = `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json
|
|
230
|
+
const nextCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
193
231
|
return {
|
|
194
232
|
ok: true,
|
|
195
233
|
schema_id: "dd-flow/vnext-work-submit@1",
|
|
@@ -241,13 +279,23 @@ export function finishVnextSpecifyStage(context, input) {
|
|
|
241
279
|
const run = requireRun(context, projectRoot, input.runId);
|
|
242
280
|
const stageRoot = path.join(requiredRunHome(run), "01-specify");
|
|
243
281
|
const acceptedResult = path.join(stageRoot, "specify.json");
|
|
244
|
-
const
|
|
282
|
+
const source = input.resultFile ? path.resolve(input.resultFile) : acceptedResult;
|
|
283
|
+
const prepared = input.result
|
|
284
|
+
? { file: source, value: input.result }
|
|
285
|
+
: input.resultJson !== undefined
|
|
286
|
+
? prepareJsonText(input.resultJson, { file: "<stdin>", label: "SPECIFY result" })
|
|
287
|
+
: prepareJsonSchema(source, "SPECIFY result", { schemaName: "vnext-specify", projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
|
|
288
|
+
try {
|
|
289
|
+
validateSchema({ schemaName: "vnext-specify", file: prepared.file, data: prepared.value, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requiredRunHome(run) });
|
|
290
|
+
validateObligations(prepared.value, prepared.file);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return prepareError(error, { file: prepared.file, label: "SPECIFY result" });
|
|
294
|
+
}
|
|
245
295
|
const candidate = path.join(stageRoot, `.specify-candidate-${crypto.randomUUID()}.json`);
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
if (input.resultFile && path.resolve(input.resultFile) === acceptedResult)
|
|
250
|
-
fs.rmSync(acceptedResult, { force: true });
|
|
296
|
+
// Candidate storage starts only after the exact input has passed schema and
|
|
297
|
+
// semantic checks; it is never the caller's source file.
|
|
298
|
+
writeText(candidate, `${JSON.stringify(prepared.value, null, 2)}\n`);
|
|
251
299
|
try {
|
|
252
300
|
return submitVnextSpecify(context, { projectRoot, workId: work.work_id, resultFile: candidate, outcome: input.outcome });
|
|
253
301
|
}
|
|
@@ -260,11 +308,7 @@ function readIntake(input) {
|
|
|
260
308
|
return { markdown: input.intakeMarkdown };
|
|
261
309
|
if (!input.intakeFile)
|
|
262
310
|
throw new AppError("usage", "SPECIFY intake is required", 2);
|
|
263
|
-
|
|
264
|
-
if (!fs.existsSync(intakeFile) || !fs.statSync(intakeFile).isFile()) {
|
|
265
|
-
throw new AppError("not_found", "--intake-file must point to an existing file", 1, { intake_file: input.intakeFile });
|
|
266
|
-
}
|
|
267
|
-
return { markdown: fs.readFileSync(intakeFile, "utf8") };
|
|
311
|
+
return { markdown: prepareTextFile(input.intakeFile, "SPECIFY intake").text };
|
|
268
312
|
}
|
|
269
313
|
function requireRun(context, projectRoot, runId) {
|
|
270
314
|
const project = requireProjectByRoot(context, projectRoot);
|
|
@@ -299,28 +343,6 @@ function requireWorkSession(context, workSessionId) {
|
|
|
299
343
|
function latestWorkSession(context, workId) {
|
|
300
344
|
return context.db.get("SELECT * FROM work_sessions WHERE work_id = ? ORDER BY created_at DESC, id DESC LIMIT 1", [workId]) ?? null;
|
|
301
345
|
}
|
|
302
|
-
function writablePreflight(projectRoot, stageRoot) {
|
|
303
|
-
const memoryBank = preflightMemoryPermissions({
|
|
304
|
-
root: projectRoot,
|
|
305
|
-
memoryBank: ".memory-bank",
|
|
306
|
-
flow: "custom",
|
|
307
|
-
mode: "write",
|
|
308
|
-
targets: [{ path: ".memory-bank", mode: "read" }]
|
|
309
|
-
});
|
|
310
|
-
const probe = path.join(stageRoot, `.dd-flow-probe-${crypto.randomUUID()}`);
|
|
311
|
-
try {
|
|
312
|
-
fs.writeFileSync(probe, "");
|
|
313
|
-
fs.rmSync(probe);
|
|
314
|
-
return { ...memoryBank, ok: memoryBank.ok === true, stage_workspace: { path: stageRoot, status: "passed" } };
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
try {
|
|
318
|
-
fs.rmSync(probe, { force: true });
|
|
319
|
-
}
|
|
320
|
-
catch { /* best effort */ }
|
|
321
|
-
return { ...memoryBank, ok: false, stage_workspace: { path: stageRoot, status: "failed", message: String(error) } };
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
346
|
function projectGrounding(projectRoot) {
|
|
325
347
|
return [
|
|
326
348
|
".memory-bank/index.md",
|
|
@@ -368,6 +390,13 @@ function renderPrompt(input) {
|
|
|
368
390
|
"```",
|
|
369
391
|
"</trusted_runtime_context>",
|
|
370
392
|
"",
|
|
393
|
+
"<workspace_contract>",
|
|
394
|
+
`- authoritative SPECIFY artifact directory: ${input.stageRoot}`,
|
|
395
|
+
`- result file to write: ${input.resultPath}`,
|
|
396
|
+
`- project root is read-only grounding for this Work: ${input.projectRoot}`,
|
|
397
|
+
"Write only the named result and stage-local artifacts. Do not switch cwd, create a worktree, or write another RUN/native home.",
|
|
398
|
+
"</workspace_contract>",
|
|
399
|
+
"",
|
|
371
400
|
"<eval_input_boundary>",
|
|
372
401
|
"Use only this prompt, the current project root, this stage workspace and project files reached through the supplied grounding. Do not read or search another RUN, transcript, prior result, reviewer material or other project under ~/.dd-flow or ~/.codex.",
|
|
373
402
|
"If stage finish rejects the result, correct this same JSON result only from the returned validation error. Do not seek another schema or example outside this stage workspace.",
|
|
@@ -392,13 +421,14 @@ function renderPrompt(input) {
|
|
|
392
421
|
"```",
|
|
393
422
|
"The JSON must preserve the problem-space contract for a fresh PROTOCOLIZE worker: user intent, scope, acceptance and verification, settled defaults, relevant project facts, gap-method outcomes, task assessment, delivery shape and handoff. Use stable R-001... identifiers in requirements and AC-001... identifiers in acceptance_criteria. Do not describe implementation design.",
|
|
394
423
|
"Do not finish while a material user question remains. Use stage pause instead of adding a question to specify.json.",
|
|
395
|
-
"If a user answer is required,
|
|
424
|
+
"If a user answer is required, use the file and standalone lifecycle command below instead of finish. Write only the concise user-facing question packet to the named text file; do not combine file creation and dd-flow in one shell command:",
|
|
396
425
|
"```sh",
|
|
397
426
|
input.pauseCommandTemplate,
|
|
398
427
|
"```",
|
|
399
428
|
"The response tells you exactly what to ask and how to resume this same stage. Ask its user_message and stop the Turn.",
|
|
400
|
-
`Finish exactly once only after all questions are resolved. Run this as one standalone Bash command:\n\`${input.
|
|
401
|
-
"Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators.
|
|
429
|
+
`Finish exactly once only after all questions are resolved. Run this as one standalone Bash command:\n\`${input.finishCommand}\`.`,
|
|
430
|
+
"Do not combine the lifecycle command with cat, skill reads, Git commands, pipes or shell operators. After success, follow only its returned next directive; a focused eval controller may explicitly stop at this stage boundary.",
|
|
431
|
+
stageLifecycleInstruction,
|
|
402
432
|
"</output_contract>",
|
|
403
433
|
""
|
|
404
434
|
].join("\n");
|
|
@@ -562,12 +592,20 @@ export function readVnextFlowDefinition(projectRoot) {
|
|
|
562
592
|
}
|
|
563
593
|
return null;
|
|
564
594
|
}
|
|
565
|
-
function executionSnapshot(context, runId) {
|
|
566
|
-
const row = context.db.get("SELECT index_json FROM runs WHERE id = ?", [runId]);
|
|
595
|
+
function executionSnapshot(context, projectId, runId, projectRoot, workspaceRoot) {
|
|
596
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
567
597
|
const snapshot = row ? JSON.parse(row.index_json) : null;
|
|
568
|
-
|
|
598
|
+
return executionSnapshotFromProfile(context, snapshot?.execution_profile, projectRoot, workspaceRoot, runId);
|
|
599
|
+
}
|
|
600
|
+
function executionSnapshotFromProfile(context, profile, projectRoot, workspaceRoot, runId) {
|
|
601
|
+
let handoff = profile?.settings.stage_session_mode;
|
|
569
602
|
if (handoff !== "same_session" && handoff !== "new_session")
|
|
570
603
|
throw new AppError("execution_profile_invalid", "RUN has no frozen stage session mode", 1, { run_id: runId });
|
|
604
|
+
if (profile?.settings.execution) {
|
|
605
|
+
if (!profile.agent_profiles)
|
|
606
|
+
throw new AppError("execution_profile_not_frozen", "Stage handoff requires frozen RUN profiles", 1);
|
|
607
|
+
handoff = resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot, workspaceRoot, fromStage: "specify", stage: "protocolize", stageSessionMode: handoff, mergeMode: profile.settings.merge_mode }).session_mode;
|
|
608
|
+
}
|
|
571
609
|
return { stage_handoff: { effective: handoff, source: "run_override" } };
|
|
572
610
|
}
|
|
573
611
|
function executionSnapshotFromPath(contextPath, workId) {
|