@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81
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 +402 -0
- package/README.md +65 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +81 -9
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1344 -376
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -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/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -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 +146 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -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 +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -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 +70 -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/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- 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 +57 -13
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +543 -96
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +116 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +121 -76
- package/dist/services/eval-snapshots.js +750 -73
- package/dist/services/execution-policy.js +170 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +2 -0
- package/dist/services/hooks.js +575 -293
- package/dist/services/lanes.js +61 -53
- package/dist/services/lifecycle-command.js +87 -7
- package/dist/services/lifecycle-invocations.js +835 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +283 -27
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +73 -28
- package/dist/services/migrations.js +14 -9
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +64 -20
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +222 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +36 -0
- package/dist/services/run-controller.js +779 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +368 -74
- package/dist/services/runtime-budget.js +434 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +565 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +243 -0
- package/dist/services/runtime-service.js +97 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +99 -102
- package/dist/services/stage-pause.js +69 -28
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +189 -85
- package/dist/services/vnext-code.js +257 -243
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +134 -19
- package/dist/services/vnext-merge.js +285 -102
- package/dist/services/vnext-plan-review.js +148 -94
- package/dist/services/vnext-plan.js +141 -72
- package/dist/services/vnext-protocolize.js +129 -56
- package/dist/services/vnext-specify.js +87 -64
- package/dist/services/work-registry.js +701 -164
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/services/worktrees.js +85 -35
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +444 -32
- package/dist/storage/paths.js +17 -4
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +11 -4
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -5,19 +5,31 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { requireProjectByRoot } from "./projects.js";
|
|
6
6
|
import { appendAudit } from "./audit.js";
|
|
7
7
|
import { persistProtocolState, requireProtocol, readProtocolRuntimeState } from "./protocols.js";
|
|
8
|
-
import { attachFlowRunStage, completeFlowRunStage, getFlowRunStatus, startFlowRun } from "./runs.js";
|
|
8
|
+
import { attachFlowRunStage, prepareFlowRunStart, prepareFlowRunStageAttachment, prepareStageDirectory, completeFlowRunStage, getFlowRunStatus, startFlowRun } from "./runs.js";
|
|
9
9
|
import { validateSchema } from "./schema-validation.js";
|
|
10
10
|
import { reconcileSessionCoverage, registerFlowSession } from "./sessions.js";
|
|
11
|
-
import { usageForRun } from "./usage.js";
|
|
12
|
-
import { boundCanonicalPlan } from "./plan-runtime.js";
|
|
11
|
+
import { recalculateRunUsage, usageForRun } from "./usage.js";
|
|
12
|
+
import { boundCanonicalPlan, prepareCanonicalPlan } from "./plan-runtime.js";
|
|
13
|
+
import { prepareJsonFile, prepareJsonSchema, prepareOutputDirectory, prepareOutputFile, prepareTextFile } from "../cli/input-preparation.js";
|
|
13
14
|
import { AppError } from "../shared/errors.js";
|
|
14
|
-
import { planJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
15
|
+
import { planJsonPath, projectHome, resolveProjectRoot } from "../storage/paths.js";
|
|
15
16
|
import { resolveCanonRoot } from "./canon.js";
|
|
16
17
|
import { preflightMemoryPermissions } from "./memory-permissions.js";
|
|
17
18
|
import { registerProject } from "./projects.js";
|
|
18
19
|
import { registerProtocol } from "./protocols.js";
|
|
19
20
|
import { previewNextEntityId } from "./ids.js";
|
|
20
21
|
import { hookSessionIdentity, sessionIdForHookEvent } from "./hooks.js";
|
|
22
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
23
|
+
function stageUsageProjection(context, view, stage, startedAt) {
|
|
24
|
+
if (!startedAt)
|
|
25
|
+
return { status: "unavailable", input_tokens: null, cached_input_tokens: null, output_tokens: null, total_tokens: null, session_count: 0, observed_at: context.now() };
|
|
26
|
+
const value = recalculateRunUsage(context, { projectId: view.run.project_id, runId: view.run.id, stage: { name: stage, startedAt, completedAt: context.now() } });
|
|
27
|
+
const totals = value.totals, coverage = value.coverage;
|
|
28
|
+
const count = (name) => typeof coverage?.[name] === "number" ? coverage[name] : 0;
|
|
29
|
+
const measured = count("measured"), other = Object.values(coverage ?? {}).reduce((total, item) => total + (typeof item === "number" ? item : 0), 0) - measured;
|
|
30
|
+
const tokens = (name) => typeof totals?.[name] === "number" ? totals[name] : null;
|
|
31
|
+
return { status: measured === 0 ? "unavailable" : other > 0 || ["input_tokens", "cache_read_input_tokens", "output_tokens", "total_tokens"].some(name => tokens(name) === null) ? "partial" : "measured", input_tokens: tokens("input_tokens"), cached_input_tokens: tokens("cache_read_input_tokens"), output_tokens: tokens("output_tokens"), total_tokens: tokens("total_tokens"), session_count: Array.isArray(value.sessions) ? value.sessions.length : 0, observed_at: typeof value.observed_at === "string" ? value.observed_at : context.now() };
|
|
32
|
+
}
|
|
21
33
|
const stagePromptSections = [
|
|
22
34
|
"stage_identity",
|
|
23
35
|
"authoritative_runtime_facts",
|
|
@@ -28,63 +40,30 @@ const stagePromptSections = [
|
|
|
28
40
|
"work_contract",
|
|
29
41
|
"completion_contract"
|
|
30
42
|
];
|
|
31
|
-
function bootstrapStageRun(context, projectRoot,
|
|
32
|
-
if (input.stage !== "specify") {
|
|
33
|
-
throw new AppError("validation", "--bootstrap currently starts the initial specify stage only", 2, { stage: input.stage });
|
|
34
|
-
}
|
|
35
|
-
const subject = input.bootstrap?.subject.trim();
|
|
36
|
-
if (!subject)
|
|
37
|
-
throw new AppError("usage", "--bootstrap requires --subject", 2);
|
|
43
|
+
function bootstrapStageRun(context, projectRoot, bootstrap, intakeMarkdown) {
|
|
38
44
|
registerProject(context, { root: projectRoot });
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const started = startFlowRun(context, {
|
|
43
|
-
projectRoot,
|
|
44
|
-
flowKind: "mb_sdlc",
|
|
45
|
-
subjectType: "protocol",
|
|
46
|
-
subjectId: protocolId,
|
|
47
|
-
slug: subject,
|
|
48
|
-
nextAction: "run_specify"
|
|
49
|
-
});
|
|
50
|
-
const intakeFile = input.bootstrap?.intakeFile;
|
|
51
|
-
if (intakeFile) {
|
|
52
|
-
const source = path.resolve(intakeFile);
|
|
53
|
-
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
|
|
54
|
-
throw new AppError("not_found", "--intake-file must point to an existing file", 1, { intake_file: intakeFile });
|
|
55
|
-
}
|
|
45
|
+
registerProtocol(context, { handshakeId: bootstrap.protocolId, projectRoot });
|
|
46
|
+
const started = startFlowRun(context, bootstrap.startInput, bootstrap.start);
|
|
47
|
+
if (intakeMarkdown !== undefined) {
|
|
56
48
|
if (!started.run.run_root)
|
|
57
49
|
throw new AppError("runtime_missing", "New RUN has no artifact root", 1, { run_id: started.run.id });
|
|
58
50
|
const intakeDir = path.join(started.run.run_root, "intake");
|
|
59
51
|
fs.mkdirSync(intakeDir, { recursive: true });
|
|
60
|
-
fs.
|
|
52
|
+
fs.writeFileSync(path.join(intakeDir, "user-request.md"), intakeMarkdown);
|
|
61
53
|
}
|
|
62
54
|
return started.run.id;
|
|
63
55
|
}
|
|
64
|
-
function stagePreflight(projectRoot
|
|
56
|
+
function stagePreflight(projectRoot) {
|
|
65
57
|
const memoryBank = preflightMemoryPermissions({
|
|
66
58
|
root: projectRoot,
|
|
67
59
|
memoryBank: ".memory-bank",
|
|
68
60
|
flow: "custom",
|
|
69
|
-
mode: "
|
|
61
|
+
mode: "read",
|
|
70
62
|
targets: [{ path: ".memory-bank", mode: "read" }]
|
|
71
63
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
fs.renameSync(probe, `${probe}.renamed`);
|
|
76
|
-
fs.rmSync(`${probe}.renamed`);
|
|
77
|
-
return { ...memoryBank, ok: memoryBank.ok === true, stage_workspace: { path: stageRoot, status: "passed" } };
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
for (const candidate of [probe, `${probe}.renamed`]) {
|
|
81
|
-
try {
|
|
82
|
-
fs.rmSync(candidate, { force: true });
|
|
83
|
-
}
|
|
84
|
-
catch { /* best effort */ }
|
|
85
|
-
}
|
|
86
|
-
return { ...memoryBank, ok: false, stage_workspace: { path: stageRoot, status: "failed", message: String(error) } };
|
|
87
|
-
}
|
|
64
|
+
if (memoryBank.ok !== true)
|
|
65
|
+
throw new AppError("permission_preflight_failed", "Stage project context is not readable", 1, { preflight: memoryBank });
|
|
66
|
+
return memoryBank;
|
|
88
67
|
}
|
|
89
68
|
function flowKindForStage(stage) {
|
|
90
69
|
return ["code", "implementation", "readiness", "merge"].includes(stage) ? "implementation" : "planning";
|
|
@@ -103,23 +82,57 @@ function runtimeFacts(view, projectRoot) {
|
|
|
103
82
|
git: view.index.execution?.git ?? { branch: null, head: null, status: "unavailable" }
|
|
104
83
|
};
|
|
105
84
|
}
|
|
106
|
-
export function
|
|
85
|
+
export function prepareLegacyStageStart(context, input, intakeMarkdown) {
|
|
86
|
+
if (input.bootstrap && input.stage !== "specify")
|
|
87
|
+
throw new AppError("validation", "--bootstrap currently starts the initial specify stage only", 2);
|
|
88
|
+
if (input.bootstrap && !input.bootstrap.subject?.trim())
|
|
89
|
+
throw new AppError("usage", "--bootstrap requires --subject", 2);
|
|
90
|
+
if (!input.bootstrap && !input.runId)
|
|
91
|
+
throw new AppError("usage", "stage start requires a RUN id or --bootstrap", 2);
|
|
92
|
+
if (input.dir !== undefined)
|
|
93
|
+
prepareStageDirectory(input.dir);
|
|
94
|
+
const intake = intakeMarkdown ?? (input.bootstrap?.intakeFile ? prepareTextFile(input.bootstrap.intakeFile, "SPECIFY intake").text : undefined);
|
|
95
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
96
|
+
const retained = !input.bootstrap && input.runId ? runView(getFlowRunStatus(context, { projectRoot, runId: input.runId })) : null;
|
|
97
|
+
const dir = input.dir ?? defaultStageDir(input.stage, retained?.run.flow_kind ?? "mb_sdlc");
|
|
98
|
+
if (retained) {
|
|
99
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: retained.run.id, stage: input.stage, dir, status: "running", dataSchemaId: dataSchemaForStage(input.stage) });
|
|
100
|
+
const root = path.join(runHomePath(retained.run), dir);
|
|
101
|
+
for (const name of ["stage-prompt.md", "stage-prompt.json", "stage-start.json", "stage-input.json"])
|
|
102
|
+
prepareOutputFile(path.join(root, name));
|
|
103
|
+
if (retained.run.subject.type === "protocol")
|
|
104
|
+
readProtocolRuntimeState(context, requireProtocol(context, retained.run.subject.id, retained.run.project_id));
|
|
105
|
+
}
|
|
106
|
+
const preflight = stagePreflight(projectRoot);
|
|
107
|
+
const sources = stageInstructionSources(context, projectRoot, retained?.run.flow_kind ?? "mb_sdlc", input.stage).map(source => ({ ...source, text: fs.existsSync(source.absolute) ? fs.readFileSync(source.absolute, "utf8").trim() : "(unavailable)" }));
|
|
108
|
+
let bootstrap;
|
|
109
|
+
if (input.bootstrap) {
|
|
110
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
111
|
+
prepareOutputDirectory(project ? projectHome(context.ddFlowHome, project.id) : path.join(context.ddFlowHome, "projects"));
|
|
112
|
+
const preview = previewNextEntityId(context, { projectRoot, type: "protocol", slug: input.bootstrap.subject });
|
|
113
|
+
const startInput = { projectRoot, flowKind: "mb_sdlc", subjectType: "protocol", subjectId: preview.entity.id, slug: input.bootstrap.subject, nextAction: "run_specify" };
|
|
114
|
+
bootstrap = { protocolId: preview.entity.id, startInput, start: prepareFlowRunStart(context, startInput) };
|
|
115
|
+
}
|
|
116
|
+
return { intakeMarkdown: intake, projectRoot, retained, dir, preflight, sources, bootstrap };
|
|
117
|
+
}
|
|
118
|
+
export function startStage(context, input, prepared = prepareLegacyStageStart(context, input)) {
|
|
107
119
|
if (input.requireSessionBinding && !input.hookEventId) {
|
|
108
120
|
throw new AppError("trusted_session_binding_required", "This stage requires a trusted harness lifecycle binding; run it from a controlled Session with the dd-flow adapter enabled", 1);
|
|
109
121
|
}
|
|
110
|
-
const projectRoot =
|
|
122
|
+
const { projectRoot, retained, dir, preflight, sources } = prepared;
|
|
123
|
+
if (retained)
|
|
124
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: retained.run.id, stage: input.stage, dir: input.dir ?? defaultStageDir(input.stage, retained.run.flow_kind), status: "running", dataSchemaId: dataSchemaForStage(input.stage) });
|
|
111
125
|
let requiredSessionId = null;
|
|
112
126
|
let requiredIdentity = null;
|
|
113
127
|
if (input.requireSessionBinding) {
|
|
114
|
-
registerProject(context, { root: projectRoot });
|
|
115
128
|
requiredIdentity = hookSessionIdentity(context, requireProjectByRoot(context, projectRoot).id, input.hookEventId);
|
|
116
129
|
requiredSessionId = requiredIdentity.sessionId;
|
|
117
130
|
}
|
|
118
|
-
const runId =
|
|
131
|
+
const runId = prepared.bootstrap ? bootstrapStageRun(context, projectRoot, prepared.bootstrap, prepared.intakeMarkdown) : input.runId;
|
|
119
132
|
if (!runId)
|
|
120
133
|
throw new AppError("usage", "stage start requires a RUN id or --bootstrap", 2);
|
|
121
|
-
const before = runView(getFlowRunStatus(context, { projectRoot, runId }));
|
|
122
|
-
|
|
134
|
+
const before = retained ?? runView(getFlowRunStatus(context, { projectRoot, runId }));
|
|
135
|
+
assertRunMutationAllowed(context, before.run.project_id, before.run.id);
|
|
123
136
|
const attached = runView(attachFlowRunStage(context, {
|
|
124
137
|
projectRoot,
|
|
125
138
|
runId,
|
|
@@ -132,9 +145,6 @@ export function startStage(context, input) {
|
|
|
132
145
|
const stageRoot = path.join(runHome, dir);
|
|
133
146
|
ensureWithin(runHome, stageRoot, "stage root");
|
|
134
147
|
fs.mkdirSync(stageRoot, { recursive: true });
|
|
135
|
-
const preflight = stagePreflight(projectRoot, stageRoot);
|
|
136
|
-
if (preflight.ok !== true)
|
|
137
|
-
throw new AppError("permission_preflight_failed", "Stage workspace is not writable", 1, { preflight });
|
|
138
148
|
if (attached.run.subject.type === "protocol") {
|
|
139
149
|
syncProtocolLifecycle(context, attached.run.project_id, attached.run.subject.id, input.stage, "running");
|
|
140
150
|
}
|
|
@@ -142,7 +152,7 @@ export function startStage(context, input) {
|
|
|
142
152
|
if (sessionId) {
|
|
143
153
|
const identity = requiredIdentity ?? (input.hookEventId ? hookSessionIdentity(context, attached.run.project_id, input.hookEventId) : null);
|
|
144
154
|
registerFlowSession(context, {
|
|
145
|
-
|
|
155
|
+
hookEventId: input.hookEventId,
|
|
146
156
|
payloadJson: JSON.stringify({
|
|
147
157
|
project_root: projectRoot,
|
|
148
158
|
harness: identity?.harness ?? "codex-desktop",
|
|
@@ -167,7 +177,7 @@ export function startStage(context, input) {
|
|
|
167
177
|
});
|
|
168
178
|
}
|
|
169
179
|
const promptPath = path.join(stageRoot, "stage-prompt.md");
|
|
170
|
-
const prompt = composeStagePrompt(context, projectRoot, before, attached, input.stage, dir, preflight);
|
|
180
|
+
const prompt = composeStagePrompt(context, projectRoot, before, attached, input.stage, dir, preflight, sources);
|
|
171
181
|
atomicWrite(promptPath, prompt);
|
|
172
182
|
const semanticInputPath = path.join(stageRoot, "stage-input.json");
|
|
173
183
|
if (!fs.existsSync(semanticInputPath)) {
|
|
@@ -178,7 +188,6 @@ export function startStage(context, input) {
|
|
|
178
188
|
const aspectMapPath = path.join(stageRoot, "aspect-map.json");
|
|
179
189
|
const attempt = attached.index.stage_runs?.find((stage) => stage.stage === input.stage)?.attempt ?? "try-001";
|
|
180
190
|
const attemptNumber = Number(attempt.replace("try-", "")) || 1;
|
|
181
|
-
const sources = stageInstructionSources(context, projectRoot, attached.run.flow_kind, input.stage);
|
|
182
191
|
atomicWrite(promptDataPath, {
|
|
183
192
|
schema_id: "dd-flow/stage-prompt@2",
|
|
184
193
|
run_id: attached.run.id,
|
|
@@ -257,12 +266,13 @@ export function startStage(context, input) {
|
|
|
257
266
|
lifecycle: "stage start -> semantic work -> stage finish"
|
|
258
267
|
};
|
|
259
268
|
}
|
|
260
|
-
export function
|
|
269
|
+
export function prepareStageFinish(context, input) {
|
|
261
270
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
262
271
|
if (!input.runId)
|
|
263
272
|
throw new AppError("usage", "stage finish requires a RUN id", 2);
|
|
264
273
|
const runId = input.runId;
|
|
265
274
|
const view = runView(getFlowRunStatus(context, { projectRoot, runId }));
|
|
275
|
+
assertRunMutationAllowed(context, view.run.project_id, view.run.id);
|
|
266
276
|
const existing = view.index.stage_runs?.find((stage) => stage.stage === input.stage);
|
|
267
277
|
if (!existing) {
|
|
268
278
|
throw new AppError("not_found", `Run stage is not attached: ${input.stage}`, 1, { run_id: view.run.id, stage: input.stage });
|
|
@@ -271,31 +281,38 @@ export function finishStage(context, input) {
|
|
|
271
281
|
const runHome = runHomePath(view.run);
|
|
272
282
|
const stageRoot = path.join(runHome, dir);
|
|
273
283
|
ensureWithin(runHome, stageRoot, "stage root");
|
|
274
|
-
fs.mkdirSync(stageRoot, { recursive: true });
|
|
275
284
|
const semanticFile = resolveStageFile(input.semanticFile ?? "@stage/stage-input.json", stageRoot, runHome);
|
|
276
|
-
|
|
285
|
+
ensureWithin(runHome, semanticFile, "semantic file");
|
|
286
|
+
const source = prepareJsonSchema(semanticFile, "Stage finish input", { schemaName: "stage-finish-input", projectRoot, runId, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
287
|
+
const semantic = source.value;
|
|
288
|
+
const status = stringValue(semantic.status) ?? "done";
|
|
289
|
+
if (!["done", "waiting_for_user", "blocked", "failed"].includes(status)) {
|
|
290
|
+
throw new AppError("validation", "Stage finish status must be done, waiting_for_user, blocked, or failed", 2, { status });
|
|
291
|
+
}
|
|
292
|
+
validateStageOutcome(input.stage, status, semantic);
|
|
293
|
+
const planFinish = input.stage === "plan" ? preparePlanFinish(context, projectRoot, view, stageRoot) : undefined;
|
|
294
|
+
return { projectRoot, runId, view, existing, runHome, stageRoot, semanticFile, semantic, status, source, planFinish };
|
|
295
|
+
}
|
|
296
|
+
export function finishStage(context, input, prepared = prepareStageFinish(context, input)) {
|
|
297
|
+
const { projectRoot, runId, view, existing, runHome, stageRoot, semanticFile, semantic, status, source, planFinish } = prepared;
|
|
298
|
+
assertRunMutationAllowed(context, view.run.project_id, view.run.id);
|
|
299
|
+
fs.mkdirSync(stageRoot, { recursive: true });
|
|
300
|
+
const receipt = beginFinishReceipt(context, stageRoot, semanticFile, view.run.id, input.stage, source.text);
|
|
277
301
|
try {
|
|
278
|
-
validateSchema({ schemaName: "stage-finish-input", file: semanticFile, projectRoot, runId, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
279
|
-
const semantic = readSemanticFile(semanticFile, runHome);
|
|
280
|
-
const status = stringValue(semantic.status) ?? "done";
|
|
281
|
-
if (!["done", "waiting_for_user", "blocked", "failed"].includes(status)) {
|
|
282
|
-
throw new AppError("validation", "Stage finish status must be done, waiting_for_user, blocked, or failed", 2, { status });
|
|
283
|
-
}
|
|
284
|
-
validateStageOutcome(input.stage, status, semantic);
|
|
285
302
|
const coverage = workerCoverage(context, view.run.project_id, view.run.id);
|
|
286
303
|
if (coverage.jobs.missing?.length) {
|
|
287
304
|
throw new AppError("worker_jobs_incomplete", "Stage finish requires all declared worker jobs", 1, { coverage });
|
|
288
305
|
}
|
|
289
306
|
const lint = runTargetedMemoryBankLint(projectRoot);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
307
|
+
if (planFinish)
|
|
308
|
+
boundCanonicalPlan(context, { projectId: view.run.project_id, protocolId: view.run.subject.id, planPath: planFinish.plan_path }, planFinish.canonical);
|
|
309
|
+
const stageUsage = stageUsageProjection(context, view, input.stage, existing.started_at);
|
|
293
310
|
const report = buildStageReport(context, view, input.stage, status, semantic, stageRoot, lint, planFinish);
|
|
294
311
|
const dataPath = path.join(stageRoot, "stage-report.json");
|
|
295
312
|
const reportPath = path.join(stageRoot, "stage-report.md");
|
|
296
313
|
const htmlPath = path.join(stageRoot, "stage-report.html");
|
|
314
|
+
validateSchema({ schemaName: stageReportSchemaName(input.stage, planFinish), file: dataPath, data: report, projectRoot, runId, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
297
315
|
atomicWrite(dataPath, report);
|
|
298
|
-
validateSchema({ schemaName: stageReportSchemaName(input.stage, planFinish), file: dataPath, projectRoot, runId, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
299
316
|
atomicWrite(reportPath, renderMarkdown(report));
|
|
300
317
|
atomicWrite(htmlPath, renderHtml(projectRoot, report));
|
|
301
318
|
if (view.run.subject.type === "protocol") {
|
|
@@ -310,7 +327,8 @@ export function finishStage(context, input) {
|
|
|
310
327
|
stageReport: htmlPath,
|
|
311
328
|
data: dataPath,
|
|
312
329
|
dataSchemaId: String(report.schema_id),
|
|
313
|
-
report: reportPath
|
|
330
|
+
report: reportPath,
|
|
331
|
+
usage: stageUsage
|
|
314
332
|
});
|
|
315
333
|
finishReceipt(receipt, { outcome: "accepted", result: { status, report: dataPath } });
|
|
316
334
|
appendAudit(context, {
|
|
@@ -480,12 +498,10 @@ function semanticInputTemplate(stage) {
|
|
|
480
498
|
next_action: waiting ? "waiting_for_user: obtain the clarification packet" : "Continue to the next approved stage."
|
|
481
499
|
};
|
|
482
500
|
}
|
|
483
|
-
function composeStagePrompt(context, projectRoot, before, current, stage, dir, preflight) {
|
|
501
|
+
function composeStagePrompt(context, projectRoot, before, current, stage, dir, preflight, sources) {
|
|
484
502
|
const project = requireProjectByRoot(context, projectRoot);
|
|
485
503
|
const runHome = runHomePath(current.run);
|
|
486
|
-
const instructions =
|
|
487
|
-
return fs.existsSync(absolute) ? `### ${label}\n\n${fs.readFileSync(absolute, "utf8").trim()}` : `### ${label}\n\n(unavailable)`;
|
|
488
|
-
}).join("\n\n");
|
|
504
|
+
const instructions = sources.map(({ label, text }) => `### ${label}\n\n${text}`).join("\n\n");
|
|
489
505
|
const protocol = safeProtocolState(context, current.run.project_id, current.run.subject.id);
|
|
490
506
|
return [
|
|
491
507
|
"<stage_identity>",
|
|
@@ -508,7 +524,7 @@ function composeStagePrompt(context, projectRoot, before, current, stage, dir, p
|
|
|
508
524
|
"</authoritative_runtime_facts>",
|
|
509
525
|
"",
|
|
510
526
|
"<preflight>",
|
|
511
|
-
`-
|
|
527
|
+
`- Read-only permission check: ${String(preflight.ok === true ? "passed" : "failed")}.`,
|
|
512
528
|
"- CLI compatibility was checked before this state-changing command.",
|
|
513
529
|
"</preflight>",
|
|
514
530
|
"",
|
|
@@ -581,16 +597,6 @@ function safeProtocolState(context, projectId, protocolId) {
|
|
|
581
597
|
return undefined;
|
|
582
598
|
}
|
|
583
599
|
}
|
|
584
|
-
function readSemanticFile(file, runHome) {
|
|
585
|
-
const resolved = path.resolve(file);
|
|
586
|
-
ensureWithin(runHome, resolved, "semantic file");
|
|
587
|
-
if (!fs.existsSync(resolved))
|
|
588
|
-
throw new AppError("not_found", "Semantic input file is missing", 1, { path: file });
|
|
589
|
-
const value = JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
590
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
591
|
-
throw new AppError("validation", "Semantic input must be a JSON object", 2);
|
|
592
|
-
return value;
|
|
593
|
-
}
|
|
594
600
|
function validateStageOutcome(stage, status, semantic) {
|
|
595
601
|
if (stage !== "specify")
|
|
596
602
|
return;
|
|
@@ -602,17 +608,12 @@ function validateStageOutcome(stage, status, semantic) {
|
|
|
602
608
|
throw new AppError("semantic_status_inconsistent", "SPECIFY done cannot contain unresolved structured questions", 2);
|
|
603
609
|
}
|
|
604
610
|
}
|
|
605
|
-
function beginFinishReceipt(context, stageRoot, semanticFile, runId, stage) {
|
|
611
|
+
function beginFinishReceipt(context, stageRoot, semanticFile, runId, stage, raw) {
|
|
606
612
|
const receiptDir = path.join(stageRoot, "finish-receipts");
|
|
607
613
|
fs.mkdirSync(receiptDir, { recursive: true });
|
|
608
614
|
const number = fs.readdirSync(receiptDir).filter((file) => /^\d{3}\.json$/u.test(file)).length + 1;
|
|
609
615
|
const receiptPath = path.join(receiptDir, `${String(number).padStart(3, "0")}.json`);
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
raw = fs.readFileSync(semanticFile, "utf8");
|
|
613
|
-
}
|
|
614
|
-
catch { /* receipt records the missing input below */ }
|
|
615
|
-
const semanticSha = raw === null ? null : crypto.createHash("sha256").update(raw).digest("hex");
|
|
616
|
+
const semanticSha = crypto.createHash("sha256").update(raw).digest("hex");
|
|
616
617
|
atomicWrite(receiptPath, {
|
|
617
618
|
schema_id: "dd-flow/stage-finish-receipt@1",
|
|
618
619
|
run_id: runId,
|
|
@@ -775,17 +776,13 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
775
776
|
}
|
|
776
777
|
function preparePlanFinish(context, projectRoot, view, stageRoot) {
|
|
777
778
|
const planPath = planJsonPath(projectRoot, view.run.subject.id);
|
|
778
|
-
const canonical =
|
|
779
|
+
const canonical = prepareCanonicalPlan(context, {
|
|
779
780
|
projectId: view.run.project_id,
|
|
780
781
|
protocolId: view.run.subject.id,
|
|
781
782
|
planPath
|
|
782
783
|
});
|
|
783
784
|
const aspectMapPath = path.join(stageRoot, "aspect-map.json");
|
|
784
|
-
|
|
785
|
-
throw new AppError("not_found", "PLAN aspect-map.json is missing", 1, { path: aspectMapPath });
|
|
786
|
-
}
|
|
787
|
-
const raw = fs.readFileSync(aspectMapPath, "utf8");
|
|
788
|
-
const value = JSON.parse(raw);
|
|
785
|
+
const { text: raw, value } = prepareJsonFile(aspectMapPath, "PLAN aspect map");
|
|
789
786
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
790
787
|
throw new AppError("validation", "PLAN aspect-map.json must be a JSON object", 2, { path: aspectMapPath });
|
|
791
788
|
}
|
|
@@ -2,30 +2,41 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { lifecycleRetryCommands, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
5
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { entityReferenceVariants, shortEntityReference } from "../shared/entity-references.js";
|
|
6
8
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
9
|
+
import { resolveWorkReference } from "../storage/work-references.js";
|
|
7
10
|
import { findRecentMatchingHookEvent, claimStageResumeHookEvent, stageResumeMatchKey } from "./hooks.js";
|
|
8
11
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
12
|
import { appendFlowRunTimelineEvent, pauseFlowRunStage, resumeFlowRunStage } from "./runs.js";
|
|
10
13
|
import { bindSessionForResume, refreshRunWorkProjection } from "./work-registry.js";
|
|
11
|
-
|
|
14
|
+
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
15
|
+
export function prepareStagePause(context, input) {
|
|
16
|
+
if (typeof input.question !== "string" || !input.question.trim())
|
|
17
|
+
throw new AppError("validation", "stage pause requires a non-empty user question", 2);
|
|
18
|
+
safeStage(input.stage);
|
|
12
19
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
13
20
|
const project = requireProjectByRoot(context, projectRoot);
|
|
14
21
|
const run = requireRun(context, project.id, input.runId);
|
|
22
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
15
23
|
const work = requireWork(context, project.id, run.id, input.workId);
|
|
16
24
|
if (work.status !== "running")
|
|
17
25
|
throw new AppError("invalid_work_state", "Only a running Work can pause", 1, { work_id: work.work_id, status: work.status });
|
|
18
26
|
const question = input.question;
|
|
19
|
-
if (!question.trim())
|
|
20
|
-
throw new AppError("validation", "stage pause requires a non-empty user question", 2);
|
|
21
27
|
const stage = requireStage(run, input.stage, "running");
|
|
28
|
+
requireRunHome(run);
|
|
29
|
+
return { projectRoot, project, run, work, question, stage };
|
|
30
|
+
}
|
|
31
|
+
export function pauseStageForUser(context, input) {
|
|
32
|
+
const { projectRoot, project, run, work, question, stage } = prepareStagePause(context, input);
|
|
22
33
|
const pauseId = nextPauseId(requireRunHome(run));
|
|
23
34
|
const pauseRoot = path.join(requireRunHome(run), "intake", "hitl", `${pauseId}-${safeStage(input.stage)}`);
|
|
24
35
|
const questionPath = path.join(pauseRoot, "question.md");
|
|
25
36
|
fs.mkdirSync(pauseRoot, { recursive: true });
|
|
26
37
|
fs.writeFileSync(questionPath, question);
|
|
27
38
|
const now = context.now();
|
|
28
|
-
context.db.
|
|
39
|
+
context.db.beginWriteTransaction();
|
|
29
40
|
try {
|
|
30
41
|
const paused = context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
|
|
31
42
|
if (paused.changes !== 1)
|
|
@@ -53,34 +64,57 @@ export function pauseStageForUser(context, input) {
|
|
|
53
64
|
resume_command_template: resumeTemplate
|
|
54
65
|
};
|
|
55
66
|
}
|
|
56
|
-
export function
|
|
67
|
+
export function prepareStageResume(context, input) {
|
|
68
|
+
if (typeof input.answer !== "string" || !input.answer.trim())
|
|
69
|
+
throw new AppError("validation", "stage resume requires a non-empty user answer", 2);
|
|
70
|
+
safeStage(input.stage);
|
|
57
71
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
58
72
|
const project = requireProjectByRoot(context, projectRoot);
|
|
59
73
|
const run = requireRun(context, project.id, input.runId);
|
|
74
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
60
75
|
const work = requireWork(context, project.id, run.id, input.workId);
|
|
61
76
|
if (work.status !== "paused")
|
|
62
77
|
throw new AppError("invalid_work_state", "stage resume requires its paused Work", 1, { work_id: work.work_id, status: work.status });
|
|
63
78
|
const stage = requireStage(run, input.stage, "paused");
|
|
64
79
|
if (!stage.pause || stage.pause.work_id !== work.work_id || stage.pause.answer_path)
|
|
65
80
|
throw new AppError("invalid_stage_state", "Stage has no unanswered pause for this Work", 1, { run_id: run.id, stage: stage.stage, work_id: work.work_id });
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
const workSession = context.db.get("SELECT id, session_id, prompt_path, result_path, status FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [work.work_id]);
|
|
82
|
+
if (!workSession)
|
|
83
|
+
throw new AppError("runtime_missing", "Paused Work has no open Session link", 1, { work_id: work.work_id });
|
|
84
|
+
let evidence = input.evidence;
|
|
85
|
+
if (evidence && (evidence.pauseId !== stage.pause.id || evidence.promptPath !== workSession.prompt_path || evidence.questionPath !== stage.pause.question_path))
|
|
86
|
+
throw new AppError("runtime_state_changed", "Paused Work changed after resume preparation", 1, { work_id: work.work_id });
|
|
87
|
+
if (!evidence) {
|
|
88
|
+
try {
|
|
89
|
+
evidence = { pauseId: stage.pause.id, promptPath: workSession.prompt_path, questionPath: stage.pause.question_path, prompt: fs.readFileSync(workSession.prompt_path, "utf8"), question: fs.readFileSync(stage.pause.question_path, "utf8") };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new AppError("runtime_missing", "Paused Work continuation artifacts cannot be read", 1, { work_id: work.work_id, cause: String(error) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { projectRoot, project, run, work, stage, pause: stage.pause, workSession, evidence };
|
|
96
|
+
}
|
|
97
|
+
export function resumeStageAfterUser(context, input) {
|
|
98
|
+
const { projectRoot, project, run, work, stage, pause, workSession, evidence } = prepareStageResume(context, input);
|
|
68
99
|
const hookEventId = input.hookEventId ?? findRecentMatchingHookEvent(context, {
|
|
69
100
|
projectId: project.id,
|
|
70
|
-
matchKey:
|
|
101
|
+
matchKey: entityReferenceVariants(run.id).flatMap((runReference) => entityReferenceVariants(work.work_id).map((workReference) => stageResumeMatchKey(runReference, stage.stage, workReference, projectRoot))),
|
|
71
102
|
errorCode: "trusted_stage_resume_required",
|
|
72
103
|
operation: "stage resume"
|
|
73
104
|
}).eventKey;
|
|
74
|
-
const answerPath = path.join(path.dirname(stage.pause.question_path), "answer.md");
|
|
75
|
-
fs.writeFileSync(answerPath, input.answer);
|
|
76
105
|
const now = context.now();
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
throw new AppError("runtime_missing", "Paused Work has no open Session link", 1, { work_id: work.work_id });
|
|
80
|
-
context.db.exec("BEGIN IMMEDIATE");
|
|
106
|
+
const answerPath = path.join(path.dirname(pause.question_path), `answer-${crypto.createHash("sha256").update(`${pause.id}\0${input.answer}`).digest("hex")}.md`);
|
|
107
|
+
context.db.beginWriteTransaction();
|
|
81
108
|
let identity;
|
|
82
109
|
try {
|
|
83
110
|
identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
|
|
111
|
+
try {
|
|
112
|
+
fs.writeFileSync(answerPath, input.answer, { flag: "wx", mode: 0o600 });
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code !== "EEXIST" || fs.readFileSync(answerPath, "utf8") !== input.answer)
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
84
118
|
if (workSession.session_id !== identity.sessionId) {
|
|
85
119
|
bindSessionForResume(context, work.work_id, identity, now);
|
|
86
120
|
context.db.run("UPDATE work_sessions SET status = 'completed', updated_at = ?, completed_at = ? WHERE id = ?", [now, now, workSession.id]);
|
|
@@ -98,18 +132,20 @@ export function resumeStageAfterUser(context, input) {
|
|
|
98
132
|
}
|
|
99
133
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
100
134
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
|
|
101
|
-
const prompt =
|
|
135
|
+
const prompt = evidence.prompt;
|
|
136
|
+
const retryCommands = lifecycleRetryCommands(context, stage.stage);
|
|
102
137
|
const continuation = [
|
|
103
138
|
prompt.trimEnd(),
|
|
104
139
|
"",
|
|
105
140
|
"<hitl_resume>",
|
|
106
|
-
`- Pause: ${
|
|
107
|
-
`- Question source: ${
|
|
141
|
+
`- Pause: ${pause.id}`,
|
|
142
|
+
`- Question source: ${pause.question_path}`,
|
|
108
143
|
`- Answer source: ${answerPath}`,
|
|
109
144
|
"- Continue the same stage, Work and attempt. Do not call stage start or repeat completed preparation.",
|
|
145
|
+
...(retryCommands.length ? ["- A prior no-effect lifecycle rejection now has these authoritative replacement commands; correct the rejected input and use only these replacements:", ...retryCommands.map(command => ` ${command}`)] : []),
|
|
110
146
|
"",
|
|
111
147
|
"<user_question>",
|
|
112
|
-
|
|
148
|
+
evidence.question,
|
|
113
149
|
"</user_question>",
|
|
114
150
|
"",
|
|
115
151
|
"<user_answer>",
|
|
@@ -118,10 +154,10 @@ export function resumeStageAfterUser(context, input) {
|
|
|
118
154
|
"</hitl_resume>",
|
|
119
155
|
""
|
|
120
156
|
].join("\n");
|
|
121
|
-
return { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id:
|
|
157
|
+
return { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
|
|
122
158
|
}
|
|
123
159
|
export function stagePauseCommand(context, input) {
|
|
124
|
-
return `${flowCommand(context)} stage pause ${input.runId} --stage ${input.stage} --work ${input.workId} --question-stdin --project-root ${JSON.stringify(input.projectRoot)} --json
|
|
160
|
+
return managedLifecycleCommand(context, `${flowCommand(context)} stage pause ${shortEntityReference(input.runId)} --stage ${input.stage} --work ${shortEntityReference(input.workId)} --question-stdin --project-root ${JSON.stringify(input.projectRoot)} --json`);
|
|
125
161
|
}
|
|
126
162
|
/**
|
|
127
163
|
* The only shell form allowed for an agent-owned HITL pause. Supplying a
|
|
@@ -147,7 +183,7 @@ Effect on scope or acceptance:
|
|
|
147
183
|
USER_QUESTION`;
|
|
148
184
|
}
|
|
149
185
|
function stageResumeCommand(context, input) {
|
|
150
|
-
return `${flowCommand(context)} stage resume ${input.runId} --stage ${input.stage} --work ${input.workId} --answer-stdin --project-root ${JSON.stringify(input.projectRoot)} --json
|
|
186
|
+
return managedLifecycleCommand(context, `${flowCommand(context)} stage resume ${shortEntityReference(input.runId)} --stage ${input.stage} --work ${shortEntityReference(input.workId)} --answer-stdin --project-root ${JSON.stringify(input.projectRoot)} --json`);
|
|
151
187
|
}
|
|
152
188
|
export function flowCommand(context) {
|
|
153
189
|
const defaultHome = path.resolve(path.join(os.homedir(), ".dd-flow"));
|
|
@@ -158,7 +194,7 @@ export function flowCommand(context) {
|
|
|
158
194
|
// make every generated lifecycle command self-contained.
|
|
159
195
|
const executable = context.env.DD_FLOW_BIN;
|
|
160
196
|
if (executable && path.isAbsolute(executable)) {
|
|
161
|
-
return `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} $
|
|
197
|
+
return `DD_FLOW_HOME=${JSON.stringify(context.ddFlowHome)} "$DD_FLOW_BIN"`;
|
|
162
198
|
}
|
|
163
199
|
const isolatedExecutable = path.join(context.ddFlowHome, "bin", "dd-flow");
|
|
164
200
|
if (context.ddFlowHome !== defaultHome && fs.existsSync(isolatedExecutable)) {
|
|
@@ -173,14 +209,19 @@ function requireRun(context, projectId, id) {
|
|
|
173
209
|
return rows[0];
|
|
174
210
|
}
|
|
175
211
|
function requireWork(context, projectId, runId, workId) {
|
|
176
|
-
|
|
177
|
-
if (!work)
|
|
178
|
-
throw new AppError("not_found", "Work does not belong to this RUN", 1, { run_id: runId, work_id: workId });
|
|
179
|
-
return work;
|
|
212
|
+
return resolveWorkReference(context, workId, { projectId, runId });
|
|
180
213
|
}
|
|
181
214
|
function requireStage(run, stage, status) {
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
let index;
|
|
216
|
+
try {
|
|
217
|
+
index = JSON.parse(run.index_json);
|
|
218
|
+
if (!index || !Array.isArray(index.stage_runs))
|
|
219
|
+
throw new Error("RUN index must contain stage_runs");
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
223
|
+
}
|
|
224
|
+
const found = index.stage_runs.find((item) => item.stage === stage);
|
|
184
225
|
if (!found || found.status !== status)
|
|
185
226
|
throw new AppError("invalid_stage_state", `Stage must be ${status}`, 1, { run_id: run.id, stage, status: found?.status ?? "missing" });
|
|
186
227
|
return found;
|