@deksden-com/dd-flow-cli 0.9.0-beta.1 → 0.9.0-beta.100
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 +561 -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 +91 -15
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +116 -0
- package/dist/cli/run-cli.js +1506 -334
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/DD-ZCODE.md +99 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +39 -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 +449 -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 +468 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +300 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +217 -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 +941 -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 +267 -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/harness-runtime/lib/zcode-usage.mjs +54 -0
- package/dist/runtime/context.js +3 -1
- 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/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-decision.schema.json +1 -1
- 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 +75 -21
- package/dist/services/cli-operation-classifier.js +22 -24
- package/dist/services/code-checks.js +676 -79
- 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 +134 -0
- package/dist/services/dashboard.js +103 -31
- package/dist/services/engines.js +125 -80
- package/dist/services/eval-snapshots.js +838 -74
- 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 +68 -0
- package/dist/services/hooks.js +609 -310
- package/dist/services/lanes.js +62 -53
- package/dist/services/lifecycle-command.js +103 -17
- package/dist/services/lifecycle-contract.js +30 -0
- package/dist/services/lifecycle-invocations.js +1128 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +425 -0
- package/dist/services/merge-queue.js +179 -104
- package/dist/services/merge-server.js +78 -27
- 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/portable-refs.js +57 -0
- package/dist/services/projects.js +18 -5
- package/dist/services/prompts.js +27 -18
- 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/repair-intents.js +40 -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 +136 -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 +803 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +138 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-projection.js +10 -8
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +343 -0
- package/dist/services/runs.js +437 -81
- package/dist/services/runtime-budget.js +439 -0
- package/dist/services/runtime-command.js +43 -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 +21 -19
- package/dist/services/session-identity.js +19 -0
- package/dist/services/sessions.js +61 -80
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +44 -18
- package/dist/services/stage-lifecycle.js +112 -108
- package/dist/services/stage-pause.js +117 -73
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/usage.js +108 -132
- package/dist/services/vnext-code-review.js +245 -98
- package/dist/services/vnext-code.js +355 -257
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +166 -26
- package/dist/services/vnext-merge.js +413 -147
- package/dist/services/vnext-plan-review.js +182 -109
- package/dist/services/vnext-plan.js +199 -82
- package/dist/services/vnext-protocolize.js +133 -60
- package/dist/services/vnext-specify.js +94 -71
- package/dist/services/work-registry.js +813 -160
- 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 +569 -16
- 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 +24 -13
- package/tools/audit-runtime-fix-boundaries.mjs +96 -0
- 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,61 +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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
}
|
|
56
|
-
const intakeDir = path.join(started.run.run_home_path ?? path.dirname(started.run.id), "intake");
|
|
45
|
+
registerProtocol(context, { handshakeId: bootstrap.protocolId, projectRoot });
|
|
46
|
+
const started = startFlowRun(context, bootstrap.startInput, bootstrap.start);
|
|
47
|
+
if (intakeMarkdown !== undefined) {
|
|
48
|
+
if (!started.run.run_root)
|
|
49
|
+
throw new AppError("runtime_missing", "New RUN has no artifact root", 1, { run_id: started.run.id });
|
|
50
|
+
const intakeDir = path.join(started.run.run_root, "intake");
|
|
57
51
|
fs.mkdirSync(intakeDir, { recursive: true });
|
|
58
|
-
fs.
|
|
52
|
+
fs.writeFileSync(path.join(intakeDir, "user-request.md"), intakeMarkdown);
|
|
59
53
|
}
|
|
60
54
|
return started.run.id;
|
|
61
55
|
}
|
|
62
|
-
function stagePreflight(projectRoot
|
|
56
|
+
function stagePreflight(projectRoot) {
|
|
63
57
|
const memoryBank = preflightMemoryPermissions({
|
|
64
58
|
root: projectRoot,
|
|
65
59
|
memoryBank: ".memory-bank",
|
|
66
60
|
flow: "custom",
|
|
67
|
-
mode: "
|
|
61
|
+
mode: "read",
|
|
68
62
|
targets: [{ path: ".memory-bank", mode: "read" }]
|
|
69
63
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fs.renameSync(probe, `${probe}.renamed`);
|
|
74
|
-
fs.rmSync(`${probe}.renamed`);
|
|
75
|
-
return { ...memoryBank, ok: memoryBank.ok === true, stage_workspace: { path: stageRoot, status: "passed" } };
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
for (const candidate of [probe, `${probe}.renamed`]) {
|
|
79
|
-
try {
|
|
80
|
-
fs.rmSync(candidate, { force: true });
|
|
81
|
-
}
|
|
82
|
-
catch { /* best effort */ }
|
|
83
|
-
}
|
|
84
|
-
return { ...memoryBank, ok: false, stage_workspace: { path: stageRoot, status: "failed", message: String(error) } };
|
|
85
|
-
}
|
|
64
|
+
if (memoryBank.ok !== true)
|
|
65
|
+
throw new AppError("permission_preflight_failed", "Stage project context is not readable", 1, { preflight: memoryBank });
|
|
66
|
+
return memoryBank;
|
|
86
67
|
}
|
|
87
68
|
function flowKindForStage(stage) {
|
|
88
69
|
return ["code", "implementation", "readiness", "merge"].includes(stage) ? "implementation" : "planning";
|
|
@@ -101,23 +82,57 @@ function runtimeFacts(view, projectRoot) {
|
|
|
101
82
|
git: view.index.execution?.git ?? { branch: null, head: null, status: "unavailable" }
|
|
102
83
|
};
|
|
103
84
|
}
|
|
104
|
-
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)) {
|
|
105
119
|
if (input.requireSessionBinding && !input.hookEventId) {
|
|
106
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);
|
|
107
121
|
}
|
|
108
|
-
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) });
|
|
109
125
|
let requiredSessionId = null;
|
|
110
126
|
let requiredIdentity = null;
|
|
111
127
|
if (input.requireSessionBinding) {
|
|
112
|
-
registerProject(context, { root: projectRoot });
|
|
113
128
|
requiredIdentity = hookSessionIdentity(context, requireProjectByRoot(context, projectRoot).id, input.hookEventId);
|
|
114
129
|
requiredSessionId = requiredIdentity.sessionId;
|
|
115
130
|
}
|
|
116
|
-
const runId =
|
|
131
|
+
const runId = prepared.bootstrap ? bootstrapStageRun(context, projectRoot, prepared.bootstrap, prepared.intakeMarkdown) : input.runId;
|
|
117
132
|
if (!runId)
|
|
118
133
|
throw new AppError("usage", "stage start requires a RUN id or --bootstrap", 2);
|
|
119
|
-
const before = runView(getFlowRunStatus(context, { projectRoot, runId }));
|
|
120
|
-
|
|
134
|
+
const before = retained ?? runView(getFlowRunStatus(context, { projectRoot, runId }));
|
|
135
|
+
assertRunMutationAllowed(context, before.run.project_id, before.run.id);
|
|
121
136
|
const attached = runView(attachFlowRunStage(context, {
|
|
122
137
|
projectRoot,
|
|
123
138
|
runId,
|
|
@@ -130,9 +145,6 @@ export function startStage(context, input) {
|
|
|
130
145
|
const stageRoot = path.join(runHome, dir);
|
|
131
146
|
ensureWithin(runHome, stageRoot, "stage root");
|
|
132
147
|
fs.mkdirSync(stageRoot, { recursive: true });
|
|
133
|
-
const preflight = stagePreflight(projectRoot, stageRoot);
|
|
134
|
-
if (preflight.ok !== true)
|
|
135
|
-
throw new AppError("permission_preflight_failed", "Stage workspace is not writable", 1, { preflight });
|
|
136
148
|
if (attached.run.subject.type === "protocol") {
|
|
137
149
|
syncProtocolLifecycle(context, attached.run.project_id, attached.run.subject.id, input.stage, "running");
|
|
138
150
|
}
|
|
@@ -140,7 +152,7 @@ export function startStage(context, input) {
|
|
|
140
152
|
if (sessionId) {
|
|
141
153
|
const identity = requiredIdentity ?? (input.hookEventId ? hookSessionIdentity(context, attached.run.project_id, input.hookEventId) : null);
|
|
142
154
|
registerFlowSession(context, {
|
|
143
|
-
|
|
155
|
+
hookEventId: input.hookEventId,
|
|
144
156
|
payloadJson: JSON.stringify({
|
|
145
157
|
project_root: projectRoot,
|
|
146
158
|
harness: identity?.harness ?? "codex-desktop",
|
|
@@ -165,7 +177,7 @@ export function startStage(context, input) {
|
|
|
165
177
|
});
|
|
166
178
|
}
|
|
167
179
|
const promptPath = path.join(stageRoot, "stage-prompt.md");
|
|
168
|
-
const prompt = composeStagePrompt(context, projectRoot, before, attached, input.stage, dir, preflight);
|
|
180
|
+
const prompt = composeStagePrompt(context, projectRoot, before, attached, input.stage, dir, preflight, sources);
|
|
169
181
|
atomicWrite(promptPath, prompt);
|
|
170
182
|
const semanticInputPath = path.join(stageRoot, "stage-input.json");
|
|
171
183
|
if (!fs.existsSync(semanticInputPath)) {
|
|
@@ -176,7 +188,6 @@ export function startStage(context, input) {
|
|
|
176
188
|
const aspectMapPath = path.join(stageRoot, "aspect-map.json");
|
|
177
189
|
const attempt = attached.index.stage_runs?.find((stage) => stage.stage === input.stage)?.attempt ?? "try-001";
|
|
178
190
|
const attemptNumber = Number(attempt.replace("try-", "")) || 1;
|
|
179
|
-
const sources = stageInstructionSources(context, projectRoot, attached.run.flow_kind, input.stage);
|
|
180
191
|
atomicWrite(promptDataPath, {
|
|
181
192
|
schema_id: "dd-flow/stage-prompt@2",
|
|
182
193
|
run_id: attached.run.id,
|
|
@@ -216,7 +227,7 @@ export function startStage(context, input) {
|
|
|
216
227
|
})),
|
|
217
228
|
worker_prompt_markdown: prompt
|
|
218
229
|
});
|
|
219
|
-
validateSchema({ schemaName: "stage-prompt", file: promptDataPath, projectRoot, runId: attached.run.id, ddFlowHome: context.ddFlowHome });
|
|
230
|
+
validateSchema({ schemaName: "stage-prompt", file: promptDataPath, projectRoot, runId: attached.run.id, runRoot: runHome, ddFlowHome: context.ddFlowHome });
|
|
220
231
|
atomicWrite(path.join(stageRoot, "stage-start.json"), {
|
|
221
232
|
schema_id: "dd-flow/stage-start@2",
|
|
222
233
|
run_id: attached.run.id,
|
|
@@ -255,12 +266,13 @@ export function startStage(context, input) {
|
|
|
255
266
|
lifecycle: "stage start -> semantic work -> stage finish"
|
|
256
267
|
};
|
|
257
268
|
}
|
|
258
|
-
export function
|
|
269
|
+
export function prepareStageFinish(context, input) {
|
|
259
270
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
260
271
|
if (!input.runId)
|
|
261
272
|
throw new AppError("usage", "stage finish requires a RUN id", 2);
|
|
262
273
|
const runId = input.runId;
|
|
263
274
|
const view = runView(getFlowRunStatus(context, { projectRoot, runId }));
|
|
275
|
+
assertRunMutationAllowed(context, view.run.project_id, view.run.id);
|
|
264
276
|
const existing = view.index.stage_runs?.find((stage) => stage.stage === input.stage);
|
|
265
277
|
if (!existing) {
|
|
266
278
|
throw new AppError("not_found", `Run stage is not attached: ${input.stage}`, 1, { run_id: view.run.id, stage: input.stage });
|
|
@@ -269,31 +281,38 @@ export function finishStage(context, input) {
|
|
|
269
281
|
const runHome = runHomePath(view.run);
|
|
270
282
|
const stageRoot = path.join(runHome, dir);
|
|
271
283
|
ensureWithin(runHome, stageRoot, "stage root");
|
|
272
|
-
fs.mkdirSync(stageRoot, { recursive: true });
|
|
273
284
|
const semanticFile = resolveStageFile(input.semanticFile ?? "@stage/stage-input.json", stageRoot, runHome);
|
|
274
|
-
|
|
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);
|
|
275
301
|
try {
|
|
276
|
-
validateSchema({ schemaName: "stage-finish-input", file: semanticFile, projectRoot, runId, ddFlowHome: context.ddFlowHome });
|
|
277
|
-
const semantic = readSemanticFile(semanticFile, runHome);
|
|
278
|
-
const status = stringValue(semantic.status) ?? "done";
|
|
279
|
-
if (!["done", "waiting_for_user", "blocked", "failed"].includes(status)) {
|
|
280
|
-
throw new AppError("validation", "Stage finish status must be done, waiting_for_user, blocked, or failed", 2, { status });
|
|
281
|
-
}
|
|
282
|
-
validateStageOutcome(input.stage, status, semantic);
|
|
283
302
|
const coverage = workerCoverage(context, view.run.project_id, view.run.id);
|
|
284
303
|
if (coverage.jobs.missing?.length) {
|
|
285
304
|
throw new AppError("worker_jobs_incomplete", "Stage finish requires all declared worker jobs", 1, { coverage });
|
|
286
305
|
}
|
|
287
306
|
const lint = runTargetedMemoryBankLint(projectRoot);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
291
310
|
const report = buildStageReport(context, view, input.stage, status, semantic, stageRoot, lint, planFinish);
|
|
292
311
|
const dataPath = path.join(stageRoot, "stage-report.json");
|
|
293
312
|
const reportPath = path.join(stageRoot, "stage-report.md");
|
|
294
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 });
|
|
295
315
|
atomicWrite(dataPath, report);
|
|
296
|
-
validateSchema({ schemaName: stageReportSchemaName(input.stage, planFinish), file: dataPath, projectRoot, runId, ddFlowHome: context.ddFlowHome });
|
|
297
316
|
atomicWrite(reportPath, renderMarkdown(report));
|
|
298
317
|
atomicWrite(htmlPath, renderHtml(projectRoot, report));
|
|
299
318
|
if (view.run.subject.type === "protocol") {
|
|
@@ -308,7 +327,8 @@ export function finishStage(context, input) {
|
|
|
308
327
|
stageReport: htmlPath,
|
|
309
328
|
data: dataPath,
|
|
310
329
|
dataSchemaId: String(report.schema_id),
|
|
311
|
-
report: reportPath
|
|
330
|
+
report: reportPath,
|
|
331
|
+
usage: stageUsage
|
|
312
332
|
});
|
|
313
333
|
finishReceipt(receipt, { outcome: "accepted", result: { status, report: dataPath } });
|
|
314
334
|
appendAudit(context, {
|
|
@@ -410,7 +430,12 @@ function nextActionForProtocolStage(stage) {
|
|
|
410
430
|
return `continue_${stage}`;
|
|
411
431
|
}
|
|
412
432
|
function runHomePath(run) {
|
|
413
|
-
|
|
433
|
+
if (!run.run_root)
|
|
434
|
+
throw new AppError("runtime_missing", "RUN has no artifact root", 1, { run_id: run.id });
|
|
435
|
+
return run.run_root;
|
|
436
|
+
}
|
|
437
|
+
function runStatePath(run) {
|
|
438
|
+
return path.join(runHomePath(run), "run.json");
|
|
414
439
|
}
|
|
415
440
|
export function defaultStageDir(stage, flowKind) {
|
|
416
441
|
const normalized = stage === "implementation" ? "code" : stage;
|
|
@@ -473,12 +498,10 @@ function semanticInputTemplate(stage) {
|
|
|
473
498
|
next_action: waiting ? "waiting_for_user: obtain the clarification packet" : "Continue to the next approved stage."
|
|
474
499
|
};
|
|
475
500
|
}
|
|
476
|
-
function composeStagePrompt(context, projectRoot, before, current, stage, dir, preflight) {
|
|
501
|
+
function composeStagePrompt(context, projectRoot, before, current, stage, dir, preflight, sources) {
|
|
477
502
|
const project = requireProjectByRoot(context, projectRoot);
|
|
478
503
|
const runHome = runHomePath(current.run);
|
|
479
|
-
const instructions =
|
|
480
|
-
return fs.existsSync(absolute) ? `### ${label}\n\n${fs.readFileSync(absolute, "utf8").trim()}` : `### ${label}\n\n(unavailable)`;
|
|
481
|
-
}).join("\n\n");
|
|
504
|
+
const instructions = sources.map(({ label, text }) => `### ${label}\n\n${text}`).join("\n\n");
|
|
482
505
|
const protocol = safeProtocolState(context, current.run.project_id, current.run.subject.id);
|
|
483
506
|
return [
|
|
484
507
|
"<stage_identity>",
|
|
@@ -501,7 +524,7 @@ function composeStagePrompt(context, projectRoot, before, current, stage, dir, p
|
|
|
501
524
|
"</authoritative_runtime_facts>",
|
|
502
525
|
"",
|
|
503
526
|
"<preflight>",
|
|
504
|
-
`-
|
|
527
|
+
`- Read-only permission check: ${String(preflight.ok === true ? "passed" : "failed")}.`,
|
|
505
528
|
"- CLI compatibility was checked before this state-changing command.",
|
|
506
529
|
"</preflight>",
|
|
507
530
|
"",
|
|
@@ -574,16 +597,6 @@ function safeProtocolState(context, projectId, protocolId) {
|
|
|
574
597
|
return undefined;
|
|
575
598
|
}
|
|
576
599
|
}
|
|
577
|
-
function readSemanticFile(file, runHome) {
|
|
578
|
-
const resolved = path.resolve(file);
|
|
579
|
-
ensureWithin(runHome, resolved, "semantic file");
|
|
580
|
-
if (!fs.existsSync(resolved))
|
|
581
|
-
throw new AppError("not_found", "Semantic input file is missing", 1, { path: file });
|
|
582
|
-
const value = JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
583
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
584
|
-
throw new AppError("validation", "Semantic input must be a JSON object", 2);
|
|
585
|
-
return value;
|
|
586
|
-
}
|
|
587
600
|
function validateStageOutcome(stage, status, semantic) {
|
|
588
601
|
if (stage !== "specify")
|
|
589
602
|
return;
|
|
@@ -595,17 +608,12 @@ function validateStageOutcome(stage, status, semantic) {
|
|
|
595
608
|
throw new AppError("semantic_status_inconsistent", "SPECIFY done cannot contain unresolved structured questions", 2);
|
|
596
609
|
}
|
|
597
610
|
}
|
|
598
|
-
function beginFinishReceipt(context, stageRoot, semanticFile, runId, stage) {
|
|
611
|
+
function beginFinishReceipt(context, stageRoot, semanticFile, runId, stage, raw) {
|
|
599
612
|
const receiptDir = path.join(stageRoot, "finish-receipts");
|
|
600
613
|
fs.mkdirSync(receiptDir, { recursive: true });
|
|
601
614
|
const number = fs.readdirSync(receiptDir).filter((file) => /^\d{3}\.json$/u.test(file)).length + 1;
|
|
602
615
|
const receiptPath = path.join(receiptDir, `${String(number).padStart(3, "0")}.json`);
|
|
603
|
-
|
|
604
|
-
try {
|
|
605
|
-
raw = fs.readFileSync(semanticFile, "utf8");
|
|
606
|
-
}
|
|
607
|
-
catch { /* receipt records the missing input below */ }
|
|
608
|
-
const semanticSha = raw === null ? null : crypto.createHash("sha256").update(raw).digest("hex");
|
|
616
|
+
const semanticSha = crypto.createHash("sha256").update(raw).digest("hex");
|
|
609
617
|
atomicWrite(receiptPath, {
|
|
610
618
|
schema_id: "dd-flow/stage-finish-receipt@1",
|
|
611
619
|
run_id: runId,
|
|
@@ -702,7 +710,7 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
702
710
|
if (stage === "code" || stage === "implementation") {
|
|
703
711
|
return {
|
|
704
712
|
schema_id: "dd-flow/code-stage-report@2",
|
|
705
|
-
run: { run_id: view.run.id, run_state: view.run
|
|
713
|
+
run: { run_id: view.run.id, run_state: runStatePath(view.run) },
|
|
706
714
|
stage: { name: stage, dir: path.basename(stageRoot), status },
|
|
707
715
|
project: { id: view.run.project_id, title: path.basename(view.run.project_root) },
|
|
708
716
|
subject: { id: view.run.subject.id, title: view.run.subject.id },
|
|
@@ -712,7 +720,7 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
712
720
|
summary: result,
|
|
713
721
|
next_action: stringValue(semantic.next_action) ?? "Proceed to readiness."
|
|
714
722
|
},
|
|
715
|
-
breadcrumbs: [{ label: "RUN", href: view.run
|
|
723
|
+
breadcrumbs: [{ label: "RUN", href: runStatePath(view.run), status: "available" }],
|
|
716
724
|
implemented_goals: [{ title: `Stage ${stage}`, summary: result }],
|
|
717
725
|
acceptance_scenarios: acceptance.map((summary, index) => ({
|
|
718
726
|
id: `SCN-${stage}-${index + 1}`,
|
|
@@ -763,22 +771,18 @@ function buildStageReport(context, view, stage, status, semantic, stageRoot, lin
|
|
|
763
771
|
memory_bank_scope: "changed_files_and_links_only",
|
|
764
772
|
status: "passed"
|
|
765
773
|
},
|
|
766
|
-
breadcrumbs: [{ label: "RUN", href: view.run
|
|
774
|
+
breadcrumbs: [{ label: "RUN", href: runStatePath(view.run), status: "available" }]
|
|
767
775
|
};
|
|
768
776
|
}
|
|
769
777
|
function preparePlanFinish(context, projectRoot, view, stageRoot) {
|
|
770
778
|
const planPath = planJsonPath(projectRoot, view.run.subject.id);
|
|
771
|
-
const canonical =
|
|
779
|
+
const canonical = prepareCanonicalPlan(context, {
|
|
772
780
|
projectId: view.run.project_id,
|
|
773
781
|
protocolId: view.run.subject.id,
|
|
774
782
|
planPath
|
|
775
783
|
});
|
|
776
784
|
const aspectMapPath = path.join(stageRoot, "aspect-map.json");
|
|
777
|
-
|
|
778
|
-
throw new AppError("not_found", "PLAN aspect-map.json is missing", 1, { path: aspectMapPath });
|
|
779
|
-
}
|
|
780
|
-
const raw = fs.readFileSync(aspectMapPath, "utf8");
|
|
781
|
-
const value = JSON.parse(raw);
|
|
785
|
+
const { text: raw, value } = prepareJsonFile(aspectMapPath, "PLAN aspect map");
|
|
782
786
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
783
787
|
throw new AppError("validation", "PLAN aspect-map.json must be a JSON object", 2, { path: aspectMapPath });
|
|
784
788
|
}
|