@deksden-com/dd-flow-cli 0.9.0-beta.100 → 0.9.0-beta.103
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/dist/build-info.json +3 -3
- package/dist/cli/hook-ingress.js +5 -10
- package/dist/harness-runtime/bin/dd-agy.mjs +20 -7
- package/dist/harness-runtime/bin/dd-droid.mjs +2 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +4 -4
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- 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 +13 -2
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +89 -36
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -4
- package/dist/harness-runtime/lib/dd-codex.mjs +1 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +24 -17
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +107 -20
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +5 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +6 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +63 -26
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +6 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +32 -3
- package/dist/harness-runtime/lib/native-hook-command.mjs +87 -13
- package/dist/harness-runtime/lib/process-json.mjs +3 -2
- package/dist/services/code-checks.js +53 -16
- package/dist/services/controller-fanout.js +37 -6
- package/dist/services/eval-snapshots.js +148 -38
- package/dist/services/external-work-launch.js +60 -5
- package/dist/services/prompts.js +2 -0
- package/dist/services/review-copy.js +136 -0
- package/dist/services/run-control-worker.js +16 -11
- package/dist/services/run-controller-adapter.js +9 -0
- package/dist/services/runs.js +3 -0
- package/dist/services/runtime-scope-stop.js +66 -6
- package/dist/services/vnext-code-review.js +6 -2
- package/dist/services/vnext-code.js +1 -0
- package/dist/services/vnext-fanout.js +9 -1
- package/dist/services/vnext-merge.js +8 -4
- package/dist/services/vnext-plan-review.js +4 -15
- package/dist/services/vnext-plan.js +4 -0
- package/dist/services/vnext-protocolize.js +4 -2
- package/dist/services/vnext-specify.js +7 -0
- package/dist/services/work-registry.js +208 -22
- package/dist/services/workspace-files.js +83 -0
- package/dist/services/worktrees.js +3 -1
- package/package.json +1 -1
|
@@ -80,40 +80,45 @@ async function launchControlWorker(context, input) {
|
|
|
80
80
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
81
81
|
const log = path.join(directory, "control.log");
|
|
82
82
|
const output = fs.openSync(log, "a", 0o600);
|
|
83
|
+
let launched = null;
|
|
83
84
|
try {
|
|
84
85
|
const env = { ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_RESOURCE_HOME: home, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_CONTROL_TOKEN: token };
|
|
85
86
|
delete env.DD_FLOW_CONTROLLER_TOKEN;
|
|
86
87
|
delete env.DD_FLOW_SCOPE_CONTROL_TOKEN;
|
|
87
88
|
const child = spawn(process.execPath, [entry, "run", "control", "serve", "--run", input.runId, "--project-root", project.root, "--control-id", input.controlId, "--json"], { cwd: run.workspace_root, detached: true, stdio: ["ignore", output, output], env });
|
|
88
89
|
await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
|
|
89
|
-
|
|
90
|
+
launched = { pid: child.pid, pid_started_at: child.pid ? processStartedAt(child.pid) : null };
|
|
91
|
+
context.db.run("UPDATE run_control_operations SET receipt_json = json_set(receipt_json, '$.child', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(launched), context.now(), launchOperationId(input.controlId, token)]);
|
|
90
92
|
child.unref();
|
|
91
93
|
}
|
|
92
94
|
finally {
|
|
93
95
|
fs.closeSync(output);
|
|
94
96
|
}
|
|
95
97
|
// The detached worker is the durable owner of the stop. Do not let the
|
|
96
|
-
// caller
|
|
97
|
-
//
|
|
98
|
-
await waitForControlWorkerAdmission(context, input.controlId, token);
|
|
98
|
+
// caller treat an unadmitted but physically live child as a failed owner.
|
|
99
|
+
// Its durable intent/launch identity retains the RUN while it registers.
|
|
100
|
+
await waitForControlWorkerAdmission(context, input.controlId, token, 5_000, launched);
|
|
99
101
|
}
|
|
100
102
|
catch (error) {
|
|
101
103
|
context.db.run("UPDATE run_control_workers SET status = 'failed', error_json = ?, updated_at = ? WHERE control_id = ? AND owner_token = ? AND status = 'starting' AND owner_pid IS NULL", [JSON.stringify(failure(error)), context.now(), input.controlId, token]);
|
|
102
104
|
// The durable intent remains accepted even if its worker could not start.
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
|
-
async function waitForControlWorkerAdmission(context, controlId, ownerToken, timeoutMs = 5_000) {
|
|
107
|
+
export async function waitForControlWorkerAdmission(context, controlId, ownerToken, timeoutMs = 5_000, launched = null) {
|
|
106
108
|
const deadline = Date.now() + timeoutMs;
|
|
107
109
|
for (;;) {
|
|
108
110
|
const worker = context.db.get("SELECT status, process_id, owner_token FROM run_control_workers WHERE control_id = ?", [controlId]);
|
|
109
111
|
if (!worker || worker.owner_token !== ownerToken)
|
|
110
|
-
return;
|
|
111
|
-
// A running worker without a resource lease is still in the
|
|
112
|
-
//
|
|
112
|
+
return "admitted";
|
|
113
|
+
// A running worker without a resource lease is still in the registration
|
|
114
|
+
// gap; only a physically live retained child can remain pending at expiry.
|
|
113
115
|
if (worker.process_id || ["failed", "completed", "superseded"].includes(worker.status))
|
|
114
|
-
return;
|
|
115
|
-
if (Date.now() >= deadline)
|
|
116
|
-
|
|
116
|
+
return "admitted";
|
|
117
|
+
if (Date.now() >= deadline) {
|
|
118
|
+
if (launched?.pid_started_at && processIsAlive(launched))
|
|
119
|
+
return "pending";
|
|
120
|
+
throw new AppError("run_control_worker_start_timeout", "Control worker did not publish its managed-process lease and is not alive", 1, { control_id: controlId, timeout_ms: timeoutMs });
|
|
121
|
+
}
|
|
117
122
|
await delay(25);
|
|
118
123
|
}
|
|
119
124
|
}
|
|
@@ -119,6 +119,15 @@ async function recoverCompletedOperation(context, manifest, session, executable,
|
|
|
119
119
|
}
|
|
120
120
|
const target = args.includes("--session-id") ? args[args.indexOf("--session-id") + 1] : null;
|
|
121
121
|
const result = observed.result;
|
|
122
|
+
const primary = observed.primary_error;
|
|
123
|
+
if (observed.operation_id === operationId && observed.operation === expectedMethod(args)
|
|
124
|
+
&& observed.state !== "completed" && (!target || observed.session_id === target)
|
|
125
|
+
&& primary && typeof primary === "object" && !Array.isArray(primary)) {
|
|
126
|
+
const failure = primary;
|
|
127
|
+
if (typeof failure.code === "string" && typeof failure.message === "string") {
|
|
128
|
+
throw new AppError(failure.code, failure.message, 1, { ...(failure.details && typeof failure.details === "object" && !Array.isArray(failure.details) ? failure.details : {}), operation_id: operationId, effect: "unknown" });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
122
131
|
if (observed.operation_id !== operationId || observed.operation !== expectedMethod(args) || observed.state !== "completed"
|
|
123
132
|
|| !result || typeof result !== "object" || Array.isArray(result)
|
|
124
133
|
|| target && (observed.session_id !== target || adapterSessionId(result) !== target))
|
package/dist/services/runs.js
CHANGED
|
@@ -576,6 +576,7 @@ export function attachFlowRunStage(context, input) {
|
|
|
576
576
|
stage,
|
|
577
577
|
dir,
|
|
578
578
|
status,
|
|
579
|
+
...(input.inputSha256 ? { input_sha256: input.inputSha256 } : {}),
|
|
579
580
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : existing?.data_schema_id ? { data_schema_id: existing.data_schema_id } : {}),
|
|
580
581
|
updated_at: now
|
|
581
582
|
};
|
|
@@ -638,11 +639,13 @@ export function prepareFlowRunStageCompletion(context, input) {
|
|
|
638
639
|
export function completeFlowRunStage(context, input) {
|
|
639
640
|
const { project, run, index, stage, status, existing } = prepareFlowRunStageCompletion(context, input);
|
|
640
641
|
const now = context.now();
|
|
642
|
+
const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
|
|
641
643
|
const stageRun = {
|
|
642
644
|
...existing,
|
|
643
645
|
status,
|
|
644
646
|
...(input.stageReport ? { stage_report: input.stageReport } : {}),
|
|
645
647
|
...(input.data ? { data: input.data } : {}),
|
|
648
|
+
...(dataPath && fs.existsSync(dataPath) ? { data_sha256: crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") } : {}),
|
|
646
649
|
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
|
|
647
650
|
...(input.report ? { report: input.report } : {}),
|
|
648
651
|
...(input.usage ? { usage: input.usage } : {}),
|
|
@@ -42,6 +42,10 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
42
42
|
const receipt = await runHarnessAdapter({ executable: metadata.adapter_executable,
|
|
43
43
|
args: prior ? ["daemon", "operation", "--operation-id", operationId, "--state-dir", metadata.state_dir, "--json"] : ["daemon", "stop", "--cancel-tree", "--state-dir", metadata.state_dir, "--json"],
|
|
44
44
|
env, operationId: prior ? `${operationId}:observe` : operationId, timeoutMs: 30_000 });
|
|
45
|
+
if (prior && receipt.state === "failed" && receipt.operation_id === operationId && receipt.daemon_id === state.daemon_id) {
|
|
46
|
+
const failure = receipt.error;
|
|
47
|
+
throw new AppError(failure?.code ?? "runtime_scope_control_failed", failure?.message ?? "Native close operation failed", 1, failure?.details);
|
|
48
|
+
}
|
|
45
49
|
if (prior && (receipt.operation_id !== operationId || receipt.daemon_id !== state.daemon_id || receipt.operation !== "daemon.stop" || receipt.state !== "completed"))
|
|
46
50
|
throw new AppError("runtime_scope_outcome_unknown", "Native close operation has not completed", 1);
|
|
47
51
|
const result = (prior ? receipt.result : receipt);
|
|
@@ -52,10 +56,17 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
52
56
|
if (result?.stopped !== true || result.clean !== true || after.daemon_id !== state.daemon_id || after.shutdown_state !== "clean" || after.active_tree !== false || processTreeIsAlive(finalOwner))
|
|
53
57
|
throw new AppError("runtime_scope_not_settled", "Native close has not proved whole-tree settlement", 1);
|
|
54
58
|
const processes = managedProcessStatus(context).filter(item => managedProviderBelongsToDaemon(item, finalOwner, after) && item.operation_id?.startsWith(`${owner.operation_id}:`) && !processTreeIsAlive(item)).map(item => ({ process_id: item.id, native_settled: true, binding: managedProcessBinding(item, resourceHome(context)) }));
|
|
55
|
-
return { process_id: record.id, state_dir: metadata.state_dir, settled: true, operation_id: operationId, owner_operation_id: owner.operation_id, processes };
|
|
59
|
+
return { process_id: record.id, state_dir: metadata.state_dir, settled: true, native_settled: true, physical_stopped: true, operation_id: operationId, owner_operation_id: owner.operation_id, processes };
|
|
56
60
|
}
|
|
57
61
|
catch (error) {
|
|
58
|
-
|
|
62
|
+
const failure = error;
|
|
63
|
+
const native = failure.details?.cause && typeof failure.details.cause === "object" ? failure.details.cause : null;
|
|
64
|
+
const physical = await stopVerifiedNativeProcesses(context, scopeId, record);
|
|
65
|
+
const stateDir = JSON.parse(record.metadata_json).state_dir;
|
|
66
|
+
return { process_id: record.id, ...(stateDir ? { state_dir: stateDir } : {}), settled: false, native_settled: false, physical_stopped: physical.stopped,
|
|
67
|
+
reason: native?.code ?? failure.code ?? "runtime_scope_control_failed", message: native?.message ?? failure.message ?? String(error),
|
|
68
|
+
...(native ? { cause: { code: native.code ?? null, message: native.message ?? null, details: native.details ?? null } } : {}), ...(physical.error ? { cleanup_error: physical.error } : {}),
|
|
69
|
+
...(physical.provider ? { processes: [{ process_id: physical.provider.id, native_settled: false, binding: managedProcessBinding(physical.provider, resourceHome(context)) }] } : {}) };
|
|
59
70
|
}
|
|
60
71
|
}));
|
|
61
72
|
const covered = new Set(nodes.map(node => node.process_id));
|
|
@@ -90,10 +101,59 @@ export async function stopRuntimeScope(context, scopeId, requestId) {
|
|
|
90
101
|
const binding = JSON.stringify(managedProcessBinding(record, resourceHome(context)));
|
|
91
102
|
const parent = nodes.find(node => node.settled && node.state_dir === metadata.state_dir && "processes" in node && node.processes?.some(item => item.process_id === record.id && item.native_settled && JSON.stringify(item.binding) === binding));
|
|
92
103
|
if (parent && !processTreeIsAlive(record)) {
|
|
93
|
-
nodes.push({ process_id: record.id, state_dir: metadata.state_dir, settled: true, operation_id: parent.operation_id, owner_operation_id: parent.owner_operation_id });
|
|
104
|
+
nodes.push({ process_id: record.id, state_dir: metadata.state_dir, settled: true, physical_stopped: true, operation_id: parent.operation_id, owner_operation_id: parent.owner_operation_id });
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const retired = nodes.find(node => node.physical_stopped === true && node.state_dir === metadata.state_dir && node.processes?.some(item => item.process_id === record.id && JSON.stringify(item.binding) === binding));
|
|
108
|
+
if (retired && !processTreeIsAlive(record))
|
|
109
|
+
nodes.push({ process_id: record.id, ...(metadata.state_dir ? { state_dir: metadata.state_dir } : {}), settled: false, native_settled: false, physical_stopped: true, ...(retired.reason ? { reason: retired.reason } : {}) });
|
|
110
|
+
else
|
|
111
|
+
nodes.push({ process_id: record.id, settled: false, reason: "runtime_scope_resource_unsettled", message: "Resource needs its own settlement proof" });
|
|
94
112
|
}
|
|
95
|
-
else
|
|
96
|
-
nodes.push({ process_id: record.id, settled: false, reason: "runtime_scope_resource_unsettled", message: "Resource needs its own settlement proof" });
|
|
97
113
|
}
|
|
98
|
-
return { scope_id: scopeId, fence, settled: nodes.every(node => node.settled),
|
|
114
|
+
return { scope_id: scopeId, fence, settled: nodes.every(node => node.settled),
|
|
115
|
+
physical_stopped: nodes.every(node => node.physical_stopped === true || node.settled && node.physical_stopped !== null)
|
|
116
|
+
? true : nodes.some(node => node.physical_stopped === null) ? null : false, nodes };
|
|
117
|
+
}
|
|
118
|
+
async function stopVerifiedNativeProcesses(context, scopeId, daemon) {
|
|
119
|
+
const metadata = JSON.parse(daemon.metadata_json);
|
|
120
|
+
if (!metadata.state_dir || !path.isAbsolute(metadata.state_dir))
|
|
121
|
+
return { stopped: null };
|
|
122
|
+
let state;
|
|
123
|
+
try {
|
|
124
|
+
state = JSON.parse(fs.readFileSync(path.join(metadata.state_dir, "daemon.json"), "utf8"));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { stopped: null };
|
|
128
|
+
}
|
|
129
|
+
const owner = state.config?.runtime_owner;
|
|
130
|
+
const current = managedProcessStatus(context).find(item => item.id === daemon.id);
|
|
131
|
+
if (!owner || !current || current.lease_token !== daemon.lease_token || current.pid !== daemon.pid || current.pid_started_at !== daemon.pid_started_at
|
|
132
|
+
|| !daemon.pid || !daemon.pid_started_at || owner.budget?.scope_id !== scopeId || owner.owner_id !== daemon.owner_id
|
|
133
|
+
|| owner.state_dir !== metadata.state_dir || owner.role !== metadata.role || owner.adapter_executable !== metadata.adapter_executable
|
|
134
|
+
|| owner.dd_flow_home !== metadata.dd_flow_home || owner.resource_home !== resourceHome(context)
|
|
135
|
+
|| state.resource_process?.id !== daemon.id || state.resource_process.lease_token !== daemon.lease_token
|
|
136
|
+
|| (state.pid !== daemon.pid && !(state.pid == null && state.shutdown_state === "starting")))
|
|
137
|
+
return { stopped: null };
|
|
138
|
+
const registered = managedProcessStatus(context);
|
|
139
|
+
const providers = registered.filter(item => managedProviderBelongsToDaemon(item, daemon, state));
|
|
140
|
+
const unpublished = !state.provider_process && !state.provider_pid ? registered.filter(item => {
|
|
141
|
+
const child = JSON.parse(item.metadata_json);
|
|
142
|
+
return item.kind === daemon.kind.replace(/-daemon$/, "-provider") && item.owner_id === daemon.owner_id && item.project_id === daemon.project_id && item.run_id === daemon.run_id && item.work_id === daemon.work_id
|
|
143
|
+
&& item.operation_id?.startsWith(`${owner.operation_id}:`) && item.pid && item.pid_started_at && child.owner_pid === daemon.pid && child.owner_pid_started_at === daemon.pid_started_at
|
|
144
|
+
&& child.state_dir === metadata.state_dir && child.role === metadata.role && child.adapter_executable === metadata.adapter_executable && child.dd_flow_home === metadata.dd_flow_home
|
|
145
|
+
&& child.dispatch_barrier === "run-generation@1" && child.budget?.scope_id === scopeId;
|
|
146
|
+
}) : [];
|
|
147
|
+
const provider = providers.length === 1 ? providers[0] : unpublished.length === 1 ? unpublished[0] : undefined;
|
|
148
|
+
if (providers.length > 1 || unpublished.length > 1 || state.provider_pid && !provider || !provider && state.shutdown_state !== "starting")
|
|
149
|
+
return { stopped: null };
|
|
150
|
+
try {
|
|
151
|
+
if (provider)
|
|
152
|
+
await stopManagedProcess(context, { id: provider.id, leaseToken: provider.lease_token, force: false, graceMs: 500, reason: `scope-stop:${scopeId}` });
|
|
153
|
+
await stopManagedProcess(context, { id: daemon.id, leaseToken: daemon.lease_token, force: false, graceMs: 500, reason: `scope-stop:${scopeId}` });
|
|
154
|
+
return { stopped: !provider ? null : !processTreeIsAlive(daemon) && !processTreeIsAlive(provider), ...(provider ? { provider } : {}) };
|
|
155
|
+
}
|
|
156
|
+
catch (cleanup) {
|
|
157
|
+
return { stopped: false, ...(provider ? { provider } : {}), error: { code: cleanup.code ?? "physical_stop_failed", message: String(cleanup) } };
|
|
158
|
+
}
|
|
99
159
|
}
|
|
@@ -75,10 +75,14 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
75
75
|
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
76
76
|
}
|
|
77
77
|
const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
|
|
78
|
+
const aspectMapChecksums = mode.mode === "off" ? [] : fs.readdirSync(path.join(run.run_root, "03-plan"), { withFileTypes: true })
|
|
79
|
+
.filter(entry => entry.isDirectory() && fs.existsSync(path.join(run.run_root, "03-plan", entry.name, "aspect-map.json")))
|
|
80
|
+
.map(entry => ({ path: `03-plan/${entry.name}/aspect-map.json`, sha256: sha256File(path.join(run.run_root, "03-plan", entry.name, "aspect-map.json")) }))
|
|
81
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
78
82
|
freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
|
|
79
83
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
80
84
|
const reviewContextPath = path.join(root, "review-context.json");
|
|
81
|
-
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id } } : {}), code_report_sha256: codeReportSha256 }, null, 2)}\n`);
|
|
85
|
+
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
|
|
82
86
|
if (mode.mode === "off") {
|
|
83
87
|
fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
|
|
84
88
|
const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n`;
|
|
@@ -96,7 +100,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
96
100
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
97
101
|
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
|
|
98
102
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
99
|
-
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
103
|
+
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1", inputSha256: sha256Value(JSON.stringify({ batch: sha256File(batchFile), aspect_maps: aspectMapChecksums })) });
|
|
100
104
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
101
105
|
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
102
106
|
}
|
|
@@ -502,6 +502,7 @@ function coordinatorPrompt(context, input) {
|
|
|
502
502
|
`- RUN: ${input.run.id}`,
|
|
503
503
|
`- root Work: ${input.rootWork.work_id}`,
|
|
504
504
|
"- stage: code",
|
|
505
|
+
"Before any project edit, change into the immutable write workspace shown below with cd --. Write only that workspace and the named CODE verification artifacts under the stage workspace; never another RUN/native home or Git/worktree control data.",
|
|
505
506
|
"</stage_identity>",
|
|
506
507
|
"",
|
|
507
508
|
"<trusted_runtime_context>",
|
|
@@ -124,6 +124,10 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
124
124
|
const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
|
|
125
125
|
const issues = [], reconciled = [];
|
|
126
126
|
for (const child of value.children) {
|
|
127
|
+
if (child.provenance === "parent_mismatch" || child.parent_session_id !== undefined && child.parent_session_id !== value.parent_session_id) {
|
|
128
|
+
issues.push({ code: "native_child_parent_mismatch", session_id: child.session_id, observed_parent_session_id: child.parent_session_id ?? null, expected_parent_session_id: value.parent_session_id, outcome: child.status });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
127
131
|
if (child.status === "running" || child.status === "unknown")
|
|
128
132
|
continue;
|
|
129
133
|
const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
|
|
@@ -182,7 +186,7 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
182
186
|
export function prepareNativeObservations(observations) {
|
|
183
187
|
const value = observations;
|
|
184
188
|
if (!value || typeof value.harness_id !== "string" || !value.harness_id.trim() || typeof value.parent_session_id !== "string" || !value.parent_session_id.trim() || !Array.isArray(value.children)
|
|
185
|
-
|| value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled", "settled_by_root"].includes(child.status)))
|
|
189
|
+
|| value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled", "settled_by_root"].includes(child.status) || child.parent_session_id !== undefined && typeof child.parent_session_id !== "string" || child.provenance !== undefined && !["root_child", "parent_mismatch"].includes(String(child.provenance))))
|
|
186
190
|
throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
|
|
187
191
|
if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
|
|
188
192
|
throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
|
|
@@ -194,6 +198,10 @@ export function observeVnextNativeChildren(context, input) {
|
|
|
194
198
|
const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
|
|
195
199
|
const issues = [], history = [], pending = [];
|
|
196
200
|
for (const child of value.children) {
|
|
201
|
+
if (child.provenance === "parent_mismatch" || child.parent_session_id !== undefined && child.parent_session_id !== value.parent_session_id) {
|
|
202
|
+
issues.push({ code: "native_child_parent_mismatch", session_id: child.session_id, observed_parent_session_id: child.parent_session_id ?? null, expected_parent_session_id: value.parent_session_id, outcome: child.status, detected_at_stage: input.stage });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
197
205
|
const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
|
|
198
206
|
const links = context.db.all(`SELECT ws.id, ws.work_id, ws.status, w.status AS work_status, w.parent_work_id, s.current_stage
|
|
199
207
|
FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id
|
|
@@ -16,7 +16,7 @@ import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime
|
|
|
16
16
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
17
17
|
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
18
18
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
19
|
-
import { checkReceiptEvidenceValid, checkReceipts,
|
|
19
|
+
import { checkReceiptEvidenceValid, checkReceipts, codeCheckProfileHash, effectiveCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
|
|
20
20
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
21
|
import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
22
22
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
@@ -67,7 +67,7 @@ export function prepareVnextMergeRequest(context, input) {
|
|
|
67
67
|
const freezeFile = path.join(freezeRoot, "source-freeze.json");
|
|
68
68
|
const gateFile = path.join(freezeRoot, "merge-gate.json");
|
|
69
69
|
fs.mkdirSync(freezeRoot, { recursive: true });
|
|
70
|
-
freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash:
|
|
70
|
+
freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash: codeCheckProfileHash(targetWorkspace, effective), now: context.now() });
|
|
71
71
|
let sourceCommit;
|
|
72
72
|
let sourcePaths;
|
|
73
73
|
if (fs.existsSync(freezeFile)) {
|
|
@@ -105,7 +105,7 @@ export function prepareVnextMergeRequest(context, input) {
|
|
|
105
105
|
return requestView(context, raced);
|
|
106
106
|
const child = createChildWork(context, { parentWorkId: root.work_id, stage: "merge", slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
|
|
107
107
|
const requestId = nextMergeRequestId(context);
|
|
108
|
-
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route,
|
|
108
|
+
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, codeCheckProfileHash(targetWorkspace, effective), replacementOf(run), now, now]);
|
|
109
109
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_request_queued", merge_request_id: requestId, work_id: child.work_id, route, source_commit: sourceCommit, target_branch: targetBranch });
|
|
110
110
|
return requestView(context, requireRequest(context, requestId));
|
|
111
111
|
});
|
|
@@ -532,7 +532,11 @@ function commitPendingSource(cwd, runId, ignored) { const paths = meaningfulStat
|
|
|
532
532
|
function commitIntegration(cwd, runId, ignored) { const mergeHead = fs.existsSync(path.join(gitDirectory(cwd), "MERGE_HEAD")); const paths = meaningfulStatus(cwd, ignored); if (!mergeHead && !paths.length)
|
|
533
533
|
return; if (paths.length)
|
|
534
534
|
git(cwd, ["add", "-A", "--", ...paths]); git(cwd, ["commit", "-m", `merge(${runId.toLowerCase()}): integrate accepted source`]); }
|
|
535
|
-
function ignoredGitPaths(context, projectId) {
|
|
535
|
+
function ignoredGitPaths(context, projectId) {
|
|
536
|
+
// Shared workspace classification omits only the exact untracked qualified
|
|
537
|
+
// hook. Tracked or mixed user hooks remain ordinary product changes.
|
|
538
|
+
return [readProjectConfig(context, projectId).dashboard.markdown_path.replaceAll("\\", "/")];
|
|
539
|
+
}
|
|
536
540
|
function meaningfulStatus(cwd, ignored) { return workspaceChangedPaths(cwd).filter((item) => !ignored.includes(item)); }
|
|
537
541
|
function unmerged(cwd) { const value = gitValue(cwd, ["diff", "--name-only", "--diff-filter=U"], true); return value.split(/\r?\n/).filter(Boolean); }
|
|
538
542
|
function gitDirectory(cwd) { const value = gitValue(cwd, ["rev-parse", "--git-dir"]); return path.resolve(cwd, value); }
|
|
@@ -146,7 +146,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
146
146
|
const workSessionId = String(binding.work_session_id);
|
|
147
147
|
const sessionId = String(binding.session_id);
|
|
148
148
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
149
|
-
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
149
|
+
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
150
150
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
151
151
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
152
152
|
return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
@@ -242,11 +242,11 @@ function finishPlanReviewOwned(context, input) {
|
|
|
242
242
|
if (decision.outcome === "blocked") {
|
|
243
243
|
throw new AppError("stage_pause_required", "A user or decision blocker must pause the current PLAN-REVIEW Work with `dd-flow stage pause`; do not finish the stage as blocked", 2, { run_id: run.id, stage, work_id: parent.work_id });
|
|
244
244
|
}
|
|
245
|
+
if (children.some((child) => !["completed", "failed", "cancelled"].includes(child.status)))
|
|
246
|
+
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
245
247
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
246
|
-
return withStageSettlement(context, () => { preserveDecisionReceipt(root, decision, input.decisionText); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id,
|
|
248
|
+
return withStageSettlement(context, () => { preserveDecisionReceipt(root, decision, input.decisionText); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, contextFile, decision }); });
|
|
247
249
|
}
|
|
248
|
-
if (children.some((child) => child.status === "created" || child.status === "running"))
|
|
249
|
-
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
250
250
|
const latestChildren = latestReviewChildren(children);
|
|
251
251
|
const evidenceFailures = reviewerEvidenceFailures(context, { projectId: project.id, parentWorkId: parent.work_id, children: latestChildren, groups, planRevision: reviewedPlanRevision, workspaceRoot: run.workspace_root, runHome: home, runId: run.id });
|
|
252
252
|
if (evidenceFailures.length)
|
|
@@ -508,10 +508,6 @@ function settleReviewParent(context, input) {
|
|
|
508
508
|
function finishTerminalReview(context, input) {
|
|
509
509
|
const now = context.now();
|
|
510
510
|
const childStatus = input.decision.outcome === "cancelled" ? "cancelled" : "failed";
|
|
511
|
-
// A terminal decision is structured cancellation of the review subtree. It
|
|
512
|
-
// must settle semantic reviewers before its parent Work can settle; otherwise
|
|
513
|
-
// the RUN contains abandoned active Work/Session links.
|
|
514
|
-
settleReviewChildren(context, { projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, reason: `PLAN-REVIEW terminal decision: ${input.decision.summary}`, now });
|
|
515
511
|
settleReviewParent(context, { projectRoot: input.projectRoot, projectId: input.projectId, runId: input.run.id, parentWorkId: input.parentWorkId, status: childStatus, result: input.decision.summary, reason: `plan_review_${input.decision.outcome}`, resultPath: path.join(input.root, "stage-report.json") });
|
|
516
512
|
const rootWork = rootWorkId(context, input.projectId, input.run.id);
|
|
517
513
|
context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [childStatus, input.decision.summary, now, now, rootWork]);
|
|
@@ -526,13 +522,6 @@ function finishTerminalReview(context, input) {
|
|
|
526
522
|
appendFlowRunTimelineEvent(context, input.projectId, input.run.id, { type: "plan_review_terminal", outcome: input.decision.outcome, work_id: input.parentWorkId });
|
|
527
523
|
return { ok: true, run_id: input.run.id, stage, outcome: input.decision.outcome, next_action: "resolve_plan_review_blocker" };
|
|
528
524
|
}
|
|
529
|
-
function settleReviewChildren(context, input) {
|
|
530
|
-
const active = context.db.all("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ? AND status IN ('created', 'running')", [input.projectId, input.runId, input.parentWorkId]);
|
|
531
|
-
for (const child of active) {
|
|
532
|
-
context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [input.status, input.reason, input.now, input.now, child.work_id]);
|
|
533
|
-
closeRunningWorkSession(context, child.work_id, input.status, input.now);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
525
|
function closeRunningWorkSession(context, workId, status, now) {
|
|
537
526
|
context.db.run("UPDATE work_sessions SET status = ?, completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [status, now, now, workId]);
|
|
538
527
|
}
|
|
@@ -168,6 +168,10 @@ function finishVnextPlanOwned(context, input) {
|
|
|
168
168
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
169
169
|
const reviewCommand = managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage plan-review --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
170
170
|
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage: "plan", generated_at: now, verdict: "done", semantic: { result: `Accepted ${protocols.length} executable PLAN artifact${protocols.length === 1 ? "" : "s"}.`, acceptance: protocols, changed_files: [...planFiles.map((file) => path.relative(projectRoot, file)), ...mapFiles.map((file) => runRef(run.id, home, file)), runRef(run.id, home, batch)], checks: ["protocol-plan schema", "aspect-map schema", "cross-artifact references", "generated CODE batch"], evidence: [runRef(run.id, home, path.join(root, "stage-report.json"))], next_action: "start_plan_review", plans: planFiles.map((file) => path.relative(projectRoot, file)), aspect_maps: mapFiles.map((file) => runRef(run.id, home, file)), code_work_batch: runRef(run.id, home, batch), batch_checksum: batchChecksum }, mechanical: { started_at: stageStartedAt(home, now), finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(stageStartedAt(home, now))), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, next_command: reviewCommand }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" }, validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" } };
|
|
171
|
+
Object.assign(report.semantic, {
|
|
172
|
+
plan_checksums: planFiles.map((file) => ({ path: path.relative(run.workspace_root, file).split(path.sep).join("/"), sha256: checksum(file) })),
|
|
173
|
+
aspect_map_checksums: mapFiles.map((file) => ({ path: path.relative(home, file).split(path.sep).join("/"), sha256: checksum(file) }))
|
|
174
|
+
});
|
|
171
175
|
const reportJson = writeStageReport(root, report).json;
|
|
172
176
|
validateSchema({ schemaName: "stage-report", file: reportJson, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
173
177
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
@@ -76,10 +76,12 @@ export function prepareVnextProtocolize(context, input, prepared = prepareVnextP
|
|
|
76
76
|
writeJson(resultPath, resultTemplate(obligations));
|
|
77
77
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId: work.work_id, projectRoot });
|
|
78
78
|
const prompt = renderPrompt({ projectRoot, runId: run.id, workId: work.work_id, specifyPath, obligations, resultPath, handoff: input.handoff, catalog: projectCatalog, policy, template, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), finishCommand: managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --result-file ${JSON.stringify(resultPath)} --json`) });
|
|
79
|
-
|
|
79
|
+
const workspaceContract = ["<workspace_contract>", `- stage artifact directory: ${stageRoot}`, `- project workspace: ${policy.worktree_path ?? projectRoot}`, `- project root (read-only identity): ${projectRoot}`, `Before editing the result, change into the project workspace: cd -- ${JSON.stringify(policy.worktree_path ?? projectRoot)}. Write only the named result file in the stage artifact directory; do not write project files, another RUN, another native home, branches, worktrees or Git commits.`, "The CLI performs all protocol document materialization after the result is accepted.", "</workspace_contract>"].join("\n");
|
|
80
|
+
const promptWithWorkspaceContract = prompt.replace("</work_context>", `</work_context>\n\n${workspaceContract}`);
|
|
81
|
+
fs.writeFileSync(promptPath, promptWithWorkspaceContract);
|
|
80
82
|
// Packet publication does not itself bind a Session or open the Stage.
|
|
81
83
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_prepared", work_id: work.work_id, stage, handoff_mode: input.handoff.stage_handoff.effective });
|
|
82
|
-
const directive = input.handoff.stage_handoff.effective === "same_session" ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown:
|
|
84
|
+
const directive = input.handoff.stage_handoff.effective === "same_session" ? { kind: "continue_current_session", stage, prompt_path: promptPath, worker_prompt_markdown: promptWithWorkspaceContract } : { kind: "await_new_session", stage, work_id: work.work_id, prompt_path: promptPath, resume_command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage protocolize --project-root ${JSON.stringify(projectRoot)} --json`) };
|
|
83
85
|
return { directive, prompt_path: promptPath };
|
|
84
86
|
}
|
|
85
87
|
export function prepareVnextProtocolizeStart(context, input) {
|
|
@@ -383,6 +383,13 @@ function renderPrompt(input) {
|
|
|
383
383
|
"```",
|
|
384
384
|
"</trusted_runtime_context>",
|
|
385
385
|
"",
|
|
386
|
+
"<workspace_contract>",
|
|
387
|
+
`- authoritative SPECIFY artifact directory: ${input.stageRoot}`,
|
|
388
|
+
`- result file to write: ${input.resultPath}`,
|
|
389
|
+
`- project root is read-only grounding for this Work: ${input.projectRoot}`,
|
|
390
|
+
"Write only the named result and stage-local artifacts. Do not switch cwd, create a worktree, or write another RUN/native home.",
|
|
391
|
+
"</workspace_contract>",
|
|
392
|
+
"",
|
|
386
393
|
"<eval_input_boundary>",
|
|
387
394
|
"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.",
|
|
388
395
|
"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.",
|