@deksden-com/dd-flow-cli 0.9.0-beta.82 → 0.9.0-beta.86
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 +28 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +28 -5
- package/dist/services/controller-fanout.js +2 -2
- package/dist/services/hooks.js +2 -2
- package/dist/services/lifecycle-command.js +4 -0
- package/dist/services/lifecycle-invocations.js +129 -35
- package/dist/services/run-controller.js +1 -1
- package/dist/services/vnext-merge.js +4 -3
- package/dist/services/work-registry.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.86
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8abe471: Settle invalid managed lifecycle arguments atomically before dispatch and return one safe corrected command.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.85
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fe8f493: Restore runtime-owned response publication options before dispatching a managed lifecycle command, and update the controller fixture to validate the public command contract.
|
|
14
|
+
|
|
15
|
+
## 0.9.0-beta.84
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 644d181: Keep RUN identity, project paths, context files, response destinations and
|
|
20
|
+
integrity hashes inside retained lifecycle authority instead of asking agents
|
|
21
|
+
to copy them. Restore those inputs deterministically when the observed command
|
|
22
|
+
executes, including MERGE apply and repair commands.
|
|
23
|
+
|
|
24
|
+
## 0.9.0-beta.83
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- bf69c56: Accept shell-escaped lifecycle path aliases deterministically and render model-facing aliases with stable quoting.
|
|
29
|
+
|
|
3
30
|
## 0.9.0-beta.82
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
|
@@ -1230,6 +1257,7 @@ record` command with the bounded 15-probe contract.
|
|
|
1230
1257
|
- 16f68a3: Add router-native engine snapshot commands and routed project-command dispatch.
|
|
1231
1258
|
|
|
1232
1259
|
Also include the runtime/dashboard compatibility wave needed by the current Memory Bank canon:
|
|
1260
|
+
|
|
1233
1261
|
- target-based dashboard `open`/`refresh` commands and related-command help;
|
|
1234
1262
|
- persisted FIFO lane waiters and merge wait-acquire specialization;
|
|
1235
1263
|
- `migration plan/report/verify` evidence helpers for controlled `mb-upgrade` runtime migrations;
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.86",
|
|
4
|
+
"cli_commit": "979a6151c910228ed3da8daa015bcf89098b04f1",
|
|
5
|
+
"built_at": "2026-09-20T19:12:05.808Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
8
|
"commit": "97f811d33c212ae3497020178b1ed825c7c3ebac",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { quote } from "shell-quote";
|
|
5
5
|
import { observeLifecycleCommand } from "../services/lifecycle-invocations.js";
|
|
6
6
|
import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
|
|
7
|
-
import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleInvocation, settleLifecycleInvocation, settleLifecycleRejection } from "../services/lifecycle-invocations.js";
|
|
7
|
+
import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleInvocation, resolveObservedLifecycleArgs, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
|
|
8
8
|
import { createContext, createRouterContext } from "../runtime/context.js";
|
|
9
9
|
import { migrateStoreWriter } from "../storage/writer-migration.js";
|
|
10
10
|
import { helpForArgs } from "./help.js";
|
|
@@ -130,21 +130,44 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
130
130
|
const routerContext = createRouterContext(env);
|
|
131
131
|
const publicCommand = quote(["dd-flow", ...output.args]);
|
|
132
132
|
const lifecycleCommand = parseLifecycleCommand(publicCommand);
|
|
133
|
+
if (lifecycleCommand.kind === "standalone")
|
|
134
|
+
validateLifecyclePathAliases(output.args, lifecycleCommand.invocation.operation);
|
|
133
135
|
const observedInvocation = observedLifecycleInvocation(routerContext, publicCommand);
|
|
134
136
|
if (!observedInvocation && !suppliedInvocationId && env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone") {
|
|
135
137
|
throw new AppError("invocation_receipt_missing", "Managed lifecycle command has no committed native receipt", 1, { effect: "no_effect", recoverable: false });
|
|
136
138
|
}
|
|
137
139
|
const invocationId = observedInvocation?.id ?? suppliedInvocationId;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
let boundInvocationArgs;
|
|
141
|
+
try {
|
|
142
|
+
boundInvocationArgs = observedInvocation
|
|
143
|
+
? resolveObservedLifecycleArgs(routerContext, observedInvocation.id, output.args)
|
|
144
|
+
: invocationId && invocationId !== suppliedInvocationId
|
|
145
|
+
? [...output.args.filter((_, index, values) => values[index - 1] !== "--invocation-id" && values[index] !== "--invocation-id"), "--invocation-id", invocationId]
|
|
146
|
+
: output.args;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!(observedInvocation && error instanceof AppError))
|
|
150
|
+
throw error;
|
|
151
|
+
const rejectionContext = createContext({ ...env }, "hook");
|
|
152
|
+
try {
|
|
153
|
+
Object.assign(error.details, settleLifecyclePreparationRejection(rejectionContext, observedInvocation.id, error));
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
rejectionContext.db.close?.();
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
141
160
|
const observedOperation = lifecycleCommand.kind === "standalone" ? lifecycleCommand.invocation.operation : null;
|
|
142
161
|
if (observedInvocation && !observedOperation)
|
|
143
162
|
throw new AppError("invocation_command_invalid", "Observed lifecycle command could not be parsed", 1);
|
|
144
163
|
const invocationScope = invocationId ? lifecycleInvocationScope({ ...routerContext, env }, invocationId) : undefined;
|
|
145
|
-
const
|
|
164
|
+
const expandedInvocationArgs = invocationScope && observedOperation
|
|
146
165
|
? expandLifecycleInvocationArgs(routerContext, boundInvocationArgs, invocationScope, observedOperation)
|
|
147
166
|
: boundInvocationArgs;
|
|
167
|
+
const retainedOutput = observedInvocation ? parseOutputOptions(expandedInvocationArgs) : null;
|
|
168
|
+
if (retainedOutput?.responseFile)
|
|
169
|
+
output.responseFile = retainedOutput.responseFile;
|
|
170
|
+
const invocationArgs = retainedOutput?.args ?? expandedInvocationArgs;
|
|
148
171
|
emitHumanProgress(io, output, progress, "start");
|
|
149
172
|
if (output.progressJsonl) {
|
|
150
173
|
writeProgressJsonl(io, "start", `Starting ${output.args.slice(0, 3).join(" ")}`);
|
|
@@ -5,11 +5,11 @@ import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
|
5
5
|
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
6
6
|
import { renderDelegationInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
7
7
|
/** Stage entry acknowledges the packet before the controller issues child commands. */
|
|
8
|
-
export function controllerStageEntryPrompt(stage, command
|
|
8
|
+
export function controllerStageEntryPrompt(stage, command) {
|
|
9
9
|
return [
|
|
10
10
|
`Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
|
|
11
11
|
command,
|
|
12
|
-
|
|
12
|
+
"Read the complete authoritative packet from the command's response_file result, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.",
|
|
13
13
|
"If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
|
|
14
14
|
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
15
15
|
"Execute each runtime-issued lifecycle operation and target as one standalone call. Internal invocation IDs are intentionally absent; never add, copy or invent one. For a no-effect input rejection, correct the input and use its returned retry_command. If a required command is missing or no safe retry is returned, report the exact error and end the Turn so the controller can handle it."
|
package/dist/services/hooks.js
CHANGED
|
@@ -352,7 +352,7 @@ export function handleCodexHook(context, input) {
|
|
|
352
352
|
?? (bindingOwnerId ? sessionBinding(context, project.id, bindingOwnerId)?.transcript_path ?? locateCodexTranscript(bindingOwnerId, context.env.CODEX_HOME) : null);
|
|
353
353
|
const persist = (scope) => {
|
|
354
354
|
if (scope)
|
|
355
|
-
matchKey = lifecycleMatchKey(
|
|
355
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), project.root, scope);
|
|
356
356
|
const binding = bindingOwnerId ? upsertSessionBindingFromPayload(context, project, bindingOwnerId, payload, transcriptPath) : undefined;
|
|
357
357
|
const effectiveSessionId = storageId;
|
|
358
358
|
const protocolId = binding?.protocol_id ?? null;
|
|
@@ -549,7 +549,7 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
549
549
|
const eventNameForReceipt = invocationId ? "ToolCallObserved" : "PreToolUse";
|
|
550
550
|
const persist = (scope) => {
|
|
551
551
|
if (scope)
|
|
552
|
-
matchKey = lifecycleMatchKey(
|
|
552
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
553
553
|
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: expectedRoot });
|
|
554
554
|
const payload = { ...hook, session_id: providerSessionId, cwd: expectedRoot, tool_name: toolName ?? "Bash", tool_input: rawInput,
|
|
555
555
|
command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode) };
|
|
@@ -198,6 +198,10 @@ function lifecycleOperation(argv) {
|
|
|
198
198
|
return "work_finish";
|
|
199
199
|
if (key === "work fail")
|
|
200
200
|
return "work_fail";
|
|
201
|
+
if (key === "merge apply")
|
|
202
|
+
return "merge_apply";
|
|
203
|
+
if (key === "merge repair")
|
|
204
|
+
return "merge_repair";
|
|
201
205
|
return null;
|
|
202
206
|
}
|
|
203
207
|
export function parseCommandArgs(argv) {
|
|
@@ -58,12 +58,53 @@ function invocation(command) {
|
|
|
58
58
|
// Input data may be supplied later (stdin/file contents); authority is bound to
|
|
59
59
|
// the exact operation and literal argv. Existing lifecycle validation owns data.
|
|
60
60
|
function fingerprint(parsed) {
|
|
61
|
-
const
|
|
61
|
+
const projected = publicInvocationArgs(parsed);
|
|
62
|
+
const publicParsed = invocation(`${quote([parsed.executable])} ${quoteModelFacingArgs(projected)}`);
|
|
63
|
+
const options = [...publicParsed.args.options].filter(([key]) => !["invocation-id", "hook-event-id", "json", "progress-jsonl"].includes(key))
|
|
62
64
|
.map(([key, values]) => [key, key === "reason" ? ["<semantic-reason>"] : values.map(value => normalizedInvocationValue(key, value))])
|
|
63
65
|
.sort(([a], [b]) => a.localeCompare(b));
|
|
64
|
-
return crypto.createHash("sha256").update(JSON.stringify([parsed.operation,
|
|
66
|
+
return crypto.createHash("sha256").update(JSON.stringify([parsed.operation, publicParsed.args.positional.map(value => normalizedInvocationValue("positional", value)), options])).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
const runtimeOwnedOptions = {
|
|
69
|
+
stage_start: new Set(["project-root", "context-file", "context-sha256", "response-file", "require-session-binding"]),
|
|
70
|
+
stage_finish: new Set(["project-root", "result-file", "decision-file", "verification-file"]),
|
|
71
|
+
stage_pause: new Set(["project-root"]), stage_resume: new Set(["project-root"]),
|
|
72
|
+
work_start: new Set(["project-root", "recovery-id"]),
|
|
73
|
+
work_finish: new Set(["project-root"]), work_fail: new Set(["project-root"]),
|
|
74
|
+
merge_apply: new Set(["project-root"]), merge_repair: new Set(["project-root"]),
|
|
75
|
+
recovery_accept: new Set(["project-root"])
|
|
76
|
+
};
|
|
77
|
+
function publicInvocationArgs(parsed) {
|
|
78
|
+
const hidden = runtimeOwnedOptions[parsed.operation] ?? new Set();
|
|
79
|
+
const args = [];
|
|
80
|
+
for (let index = 0; index < parsed.argv.length; index += 1) {
|
|
81
|
+
const value = parsed.argv[index];
|
|
82
|
+
if (!value.startsWith("--")) {
|
|
83
|
+
args.push(value);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const key = value.slice(2).split("=", 1)[0];
|
|
87
|
+
const inline = value.includes("=");
|
|
88
|
+
const next = parsed.argv[index + 1];
|
|
89
|
+
if (hidden.has(key) || key === "invocation-id" || key === "hook-event-id") {
|
|
90
|
+
if (!inline && next && !next.startsWith("--"))
|
|
91
|
+
index += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
args.push(value);
|
|
95
|
+
if (!inline && next && !next.startsWith("--"))
|
|
96
|
+
args.push(parsed.argv[++index]);
|
|
97
|
+
}
|
|
98
|
+
// A managed coordinator Session is already bound to its RUN. Work IDs stay
|
|
99
|
+
// public because sibling Work assignments can coexist under one native root.
|
|
100
|
+
const operationOffset = parsed.operation === "recovery_accept" ? 3 : 2;
|
|
101
|
+
if (["stage_start", "stage_finish", "recovery_accept"].includes(parsed.operation)
|
|
102
|
+
&& !parsed.args.options.has("bootstrap") && args[operationOffset] && !args[operationOffset].startsWith("--"))
|
|
103
|
+
args.splice(operationOffset, 1);
|
|
104
|
+
return args;
|
|
65
105
|
}
|
|
66
106
|
function normalizedInvocationValue(key, value) {
|
|
107
|
+
value = normalizedPathAlias(value);
|
|
67
108
|
if (key === "project-root" && (path.isAbsolute(value) || value === "@project"))
|
|
68
109
|
return "@project";
|
|
69
110
|
if (["result-file", "decision-file", "verification-file"].includes(key))
|
|
@@ -79,12 +120,19 @@ function normalizedInvocationValue(key, value) {
|
|
|
79
120
|
return shortEntityReference(value);
|
|
80
121
|
return key === "retry-check" ? /(?:^|\/)(RCP-\d{3,})$/.exec(value)?.[1] ?? value : value;
|
|
81
122
|
}
|
|
123
|
+
/** Shell/JSON builders sometimes preserve the shell escape before an alias.
|
|
124
|
+
* Accept it only for the small declared alias vocabulary. */
|
|
125
|
+
function normalizedPathAlias(value) {
|
|
126
|
+
return value.replace(/^\\+(?=@(?:project|workspace|run)(?:\/|$))/, "");
|
|
127
|
+
}
|
|
128
|
+
function quoteModelFacingArgs(argv) {
|
|
129
|
+
return argv.map(value => /^@(project|workspace|run)(?:\/.*)?$/.test(value)
|
|
130
|
+
? `'${value.replaceAll("'", `'\\''`)}'`
|
|
131
|
+
: quote([value])).join(" ");
|
|
132
|
+
}
|
|
82
133
|
function withoutInvocationId(command, aliases) {
|
|
83
134
|
const parsed = invocation(command);
|
|
84
|
-
const argv =
|
|
85
|
-
const at = argv.indexOf("--invocation-id");
|
|
86
|
-
if (at >= 0)
|
|
87
|
-
argv.splice(at, 2);
|
|
135
|
+
const argv = publicInvocationArgs(parsed);
|
|
88
136
|
for (let index = 0; index < argv.length; index += 1) {
|
|
89
137
|
const option = argv[index - 1]?.replace(/^--/, "");
|
|
90
138
|
if (option === "project-root")
|
|
@@ -109,14 +157,23 @@ function withoutInvocationId(command, aliases) {
|
|
|
109
157
|
const executable = parsed.env.DD_FLOW_BIN || parsed.executable === "$DD_FLOW_BIN" ? '"$DD_FLOW_BIN"' : quote([parsed.executable]);
|
|
110
158
|
const prefix = Object.entries(env).map(([key, value]) => `${key}=${quote([value])}`).join(" ");
|
|
111
159
|
const suffix = command.slice(shellSuffixOffset(command)).trimStart();
|
|
112
|
-
return `${prefix ? `${prefix} ` : ""}${executable}${argv.length ? ` ${
|
|
160
|
+
return `${prefix ? `${prefix} ` : ""}${executable}${argv.length ? ` ${quoteModelFacingArgs(argv)}` : ""}${suffix ? ` ${suffix}` : ""}`.trim();
|
|
113
161
|
}
|
|
114
162
|
function publicInvocationCommand(context, row, command) {
|
|
163
|
+
if (!commandOption(invocation(command), "project-root"))
|
|
164
|
+
command = retainedCommandWithoutInvocationId(row);
|
|
115
165
|
const rendered = renderInvocationCommand(row, command);
|
|
116
166
|
const scope = JSON.parse(row.scope_json);
|
|
117
167
|
const run = scope.runId ? context.db.get("SELECT workspace_root, run_root FROM runs WHERE id = ?", [scope.runId]) : undefined;
|
|
118
168
|
return withoutInvocationId(rendered, { project: scope.projectRoot, ...(run?.workspace_root ? { workspace: run.workspace_root } : {}), ...(run?.run_root ? { run: run.run_root } : {}) });
|
|
119
169
|
}
|
|
170
|
+
function retainedCommandWithoutInvocationId(row) {
|
|
171
|
+
const marker = ` --invocation-id ${row.id}`;
|
|
172
|
+
const at = row.command.indexOf(marker);
|
|
173
|
+
if (at < 0 || row.command.indexOf(marker, at + marker.length) >= 0)
|
|
174
|
+
throw new AppError("invocation_command_invalid", "Retained issued command has an ambiguous invocation marker", 1);
|
|
175
|
+
return `${row.command.slice(0, at)}${row.command.slice(at + marker.length)}`;
|
|
176
|
+
}
|
|
120
177
|
/** Resolve the small, declared alias vocabulary used by model-facing lifecycle commands.
|
|
121
178
|
* Both native-hook admission and CLI execution call this function so they cannot
|
|
122
179
|
* disagree about the command that an alias denotes. */
|
|
@@ -128,7 +185,10 @@ export function expandLifecycleInvocationArgs(context, args, scope, operation) {
|
|
|
128
185
|
if (run && !args[index - 1]?.startsWith("--") && [run.short_id, normalizedInvocationValue("positional", run.id)].includes(value) && !operation.startsWith("work_"))
|
|
129
186
|
return run.id;
|
|
130
187
|
const option = args[index - 1]?.replace(/^--/, "");
|
|
131
|
-
if (!option || !pathOptions.has(option)
|
|
188
|
+
if (!option || !pathOptions.has(option))
|
|
189
|
+
return value;
|
|
190
|
+
value = normalizedPathAlias(value);
|
|
191
|
+
if (!value.startsWith("@"))
|
|
132
192
|
return value;
|
|
133
193
|
const match = /^@(project|workspace|run)(?:\/(.*))?$/.exec(value);
|
|
134
194
|
if (!match)
|
|
@@ -139,6 +199,22 @@ export function expandLifecycleInvocationArgs(context, args, scope, operation) {
|
|
|
139
199
|
return assertPathWithin(root, path.join(root, match[2] ?? ""), `${match[1]}_alias`);
|
|
140
200
|
});
|
|
141
201
|
}
|
|
202
|
+
/** Validate alias spelling before native-receipt lookup, so a correctable
|
|
203
|
+
* command typo is never reported as missing infrastructure evidence. */
|
|
204
|
+
export function validateLifecyclePathAliases(args, operation) {
|
|
205
|
+
const pathOptions = contextualPathOptionsForLifecycle(operation);
|
|
206
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
207
|
+
const option = args[index - 1]?.replace(/^--/, "");
|
|
208
|
+
if (!option || !pathOptions.has(option))
|
|
209
|
+
continue;
|
|
210
|
+
const original = args[index];
|
|
211
|
+
const value = normalizedPathAlias(original);
|
|
212
|
+
if ((original.startsWith("\\") && original.replace(/^\\+/, "").startsWith("@") && value === original)
|
|
213
|
+
|| (value.startsWith("@") && !/^@(project|workspace|run)(?:\/.*)?$/.test(value))) {
|
|
214
|
+
throw new AppError("usage", `Unknown path alias: ${original}`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter: option });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
142
218
|
function canonicalManagedCommand(context, command, scope) {
|
|
143
219
|
const parsed = invocation(command);
|
|
144
220
|
const argv = expandLifecycleInvocationArgs(context, parsed.argv, scope, parsed.operation);
|
|
@@ -424,11 +500,7 @@ function successorLifecycleInvocationCommand(context, prior) {
|
|
|
424
500
|
// The parser proves the marker belongs to argv. Remove that one rendered
|
|
425
501
|
// token from the retained text so response-file/stdin/heredoc presentation
|
|
426
502
|
// remains byte-for-byte stable across a retry.
|
|
427
|
-
const
|
|
428
|
-
const markerAt = prior.command.indexOf(marker);
|
|
429
|
-
if (markerAt < 0 || prior.command.indexOf(marker, markerAt + marker.length) >= 0)
|
|
430
|
-
throw new AppError("invocation_command_invalid", "Retained issued command has an ambiguous invocation marker", 1);
|
|
431
|
-
const command = `${prior.command.slice(0, markerAt)}${prior.command.slice(markerAt + marker.length)}`;
|
|
503
|
+
const command = retainedCommandWithoutInvocationId(prior);
|
|
432
504
|
return managedLifecycleCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: prior.scope_json, DD_FLOW_CURRENT_INVOCATION: prior.id } }, command);
|
|
433
505
|
}
|
|
434
506
|
// Caller holds the write transaction and has checked authority. Issued (or
|
|
@@ -538,6 +610,11 @@ export function assertLifecycleInvocationCurrent(context, scope, command) {
|
|
|
538
610
|
throw new AppError("invocation_scope_mismatch", "Work does not belong unambiguously to the issued RUN scope", 1);
|
|
539
611
|
}
|
|
540
612
|
}
|
|
613
|
+
else if (parsed.operation.startsWith("merge_")) {
|
|
614
|
+
const request = context.db.get("SELECT project_id, run_id FROM merge_requests WHERE merge_request_id = ?", [parsed.args.positional[0] ?? ""]);
|
|
615
|
+
if (!request || request.project_id !== project.id || request.run_id !== scope.runId)
|
|
616
|
+
throw new AppError("invocation_scope_mismatch", "MERGE request does not belong to the issued RUN scope", 1);
|
|
617
|
+
}
|
|
541
618
|
else if (parsed.operation !== "session_register" && !parsed.args.options.has("bootstrap")) {
|
|
542
619
|
const target = parsed.args.positional[0];
|
|
543
620
|
const run = context.db.get("SELECT id FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, target ?? null, target ?? null]);
|
|
@@ -683,7 +760,7 @@ export function observeLifecycleInvocation(context, input) {
|
|
|
683
760
|
context.db.exec("COMMIT");
|
|
684
761
|
return { eventKey: row.event_key, duplicate: true, ...resolved };
|
|
685
762
|
}
|
|
686
|
-
const eventKey = input.recordReceipt(scope);
|
|
763
|
+
const eventKey = input.recordReceipt({ projectRoot: scope.projectRoot, runId: scope.runId, command: initial.command });
|
|
687
764
|
// A native hook may execute from a provider worktree while the issued
|
|
688
765
|
// lifecycle scope is anchored to the stable project root. Project
|
|
689
766
|
// identity is the canonical boundary; comparing raw cwd would reject a
|
|
@@ -733,6 +810,43 @@ export function observedLifecycleInvocation(context, command) {
|
|
|
733
810
|
throw new AppError("invocation_ambiguous", "More than one observed lifecycle attempt matches this command", 1, { effect: "no_effect", matches: matches.map(row => row.id) });
|
|
734
811
|
return { id: matches[0].id, scope: JSON.parse(matches[0].scope_json) };
|
|
735
812
|
}
|
|
813
|
+
/** Replace presentation argv with the exact retained machine assignment.
|
|
814
|
+
* Public semantic arguments are already covered by the projected fingerprint;
|
|
815
|
+
* any explicitly repeated private value must agree instead of being ignored. */
|
|
816
|
+
export function resolveObservedLifecycleArgs(context, id, actualArgs) {
|
|
817
|
+
const row = context.db.get("SELECT * FROM lifecycle_invocations WHERE id = ?", [id]);
|
|
818
|
+
if (!row)
|
|
819
|
+
throw new AppError("invocation_unknown", "Lifecycle invocation is not registered", 1, { invocation_id: id });
|
|
820
|
+
const retained = invocation(row.command), actual = invocation(`${quote([retained.executable])} ${quoteModelFacingArgs(actualArgs)}`);
|
|
821
|
+
if (fingerprint(actual) !== row.fingerprint)
|
|
822
|
+
throw new AppError("invocation_command_mismatch", "Lifecycle command does not match its issued assignment", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
|
|
823
|
+
for (const key of runtimeOwnedOptions[retained.operation] ?? []) {
|
|
824
|
+
const supplied = commandOption(actual, key), expected = commandOption(retained, key);
|
|
825
|
+
if (supplied !== undefined && normalizedInvocationValue(key, supplied) !== normalizedInvocationValue(key, expected ?? "")) {
|
|
826
|
+
throw new AppError("invocation_argument_mismatch", `--${key} is owned by the managed assignment and does not match it`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter: key });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return [...retained.argv];
|
|
830
|
+
}
|
|
831
|
+
export function settleLifecyclePreparationRejection(context, id, error) {
|
|
832
|
+
return context.db.writeTransaction(() => {
|
|
833
|
+
const row = load(context, id);
|
|
834
|
+
if (row.status === "settled") {
|
|
835
|
+
const details = row.outcome_json ? JSON.parse(row.outcome_json)?.error?.details : undefined;
|
|
836
|
+
return details?.effect === "no_effect" && details.retry_command ? details : { effect: "unknown", recoverable: false };
|
|
837
|
+
}
|
|
838
|
+
if (!["issued", "observed"].includes(row.status))
|
|
839
|
+
return { effect: "unknown", recoverable: false };
|
|
840
|
+
const scope = JSON.parse(row.scope_json);
|
|
841
|
+
if (scope.runId)
|
|
842
|
+
assertLifecycleInvocationCurrent(context, scope, row.command);
|
|
843
|
+
const details = { ...error.details, effect: "no_effect", recoverable: true,
|
|
844
|
+
retry_command: successorLifecycleInvocationCommand(context, row),
|
|
845
|
+
retry_instruction: "Execute retry_command verbatim in this same Session. The rejected call changed no lifecycle state." };
|
|
846
|
+
context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ?", [JSON.stringify({ error: { ...errorRecord(error), details } }), context.now(), row.id]);
|
|
847
|
+
return details;
|
|
848
|
+
});
|
|
849
|
+
}
|
|
736
850
|
/** One caller owns execution; a lost process leaves an explicit unknown
|
|
737
851
|
* outcome, never an automatic replay. Existing recovery decides the next step. */
|
|
738
852
|
export async function awaitLifecycleInvocation(context, input) {
|
|
@@ -752,27 +866,7 @@ export async function awaitLifecycleInvocation(context, input) {
|
|
|
752
866
|
catch (error) {
|
|
753
867
|
if (!(error instanceof AppError) || !(["usage", "invocation_command_mismatch", "invocation_command_invalid"].includes(error.code) || error.details.phase === "prepare"))
|
|
754
868
|
throw error;
|
|
755
|
-
|
|
756
|
-
const row = load(context, input.id);
|
|
757
|
-
if (row.status === "settled") {
|
|
758
|
-
const details = row.outcome_json ? JSON.parse(row.outcome_json)?.error?.details : undefined;
|
|
759
|
-
return details?.effect === "no_effect" && details.retry_command ? details : { effect: "unknown", recoverable: false };
|
|
760
|
-
}
|
|
761
|
-
if (!["issued", "observed"].includes(row.status)) {
|
|
762
|
-
return { effect: "unknown", recoverable: false };
|
|
763
|
-
}
|
|
764
|
-
const scope = JSON.parse(row.scope_json);
|
|
765
|
-
if (scope.runId)
|
|
766
|
-
assertLifecycleInvocationCurrent(context, scope, row.command);
|
|
767
|
-
// No executor has claimed this attempt. Atomically retain the rejected
|
|
768
|
-
// call and issue one corrected command; old IDs only replay the outcome.
|
|
769
|
-
const details = { ...error.details, effect: "no_effect", recoverable: true,
|
|
770
|
-
retry_command: successorLifecycleInvocationCommand(context, row),
|
|
771
|
-
retry_instruction: "Execute retry_command verbatim in this same Session. The rejected call changed no lifecycle state." };
|
|
772
|
-
context.db.run("UPDATE lifecycle_invocations SET status = 'settled', outcome_json = ?, updated_at = ? WHERE id = ?", [JSON.stringify({ error: { ...errorRecord(error), details } }), context.now(), row.id]);
|
|
773
|
-
return details;
|
|
774
|
-
});
|
|
775
|
-
Object.assign(error.details, rejectionDetails);
|
|
869
|
+
Object.assign(error.details, settleLifecyclePreparationRejection(context, input.id, error));
|
|
776
870
|
throw error;
|
|
777
871
|
}
|
|
778
872
|
const timeoutMs = input.timeoutMs ?? 30000;
|
|
@@ -693,7 +693,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
693
693
|
const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
|
|
694
694
|
const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage start ${run.id} --stage ${next.stage} --project-root ${JSON.stringify(run.project_root)} --context-file ${JSON.stringify(stageContext.file)} --context-sha256 ${stageContext.sha256} --require-session-binding --response-file ${JSON.stringify(responseFile)} --json --progress-jsonl`);
|
|
695
695
|
appendEvent(context, row.controller_id, "stage_entered", { stage: next.stage, session_id: session.id, context_sha256: stageContext.sha256 });
|
|
696
|
-
await prompt(session, controllerStageEntryPrompt(next.stage, command
|
|
696
|
+
await prompt(session, controllerStageEntryPrompt(next.stage, command));
|
|
697
697
|
continue;
|
|
698
698
|
}
|
|
699
699
|
if (state.current_session === null)
|
|
@@ -290,7 +290,7 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
|
|
|
290
290
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
291
291
|
if (failed.length) {
|
|
292
292
|
context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
|
|
293
|
-
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command:
|
|
293
|
+
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot), retry_command: finishCommand(context, run.id, request, projectRoot, failed[0].id) });
|
|
294
294
|
}
|
|
295
295
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
296
296
|
const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
|
|
@@ -429,7 +429,7 @@ export async function repairVnextMerge(context, input) {
|
|
|
429
429
|
appendFlowRunTimelineEvent(context, project.id, request.run_id, { type: "merge_source_repair_created", merge_request_id: request.merge_request_id, repair_work_id: repair.repair_work_id, cycle: prepared.cycle, failed_receipt_ids: failed });
|
|
430
430
|
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${request.run_id} --stage code --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } };
|
|
431
431
|
}
|
|
432
|
-
function mergePrompt(context, input, checks, settings) { const pause = managedLifecycleCommand(context, `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`); const repair =
|
|
432
|
+
function mergePrompt(context, input, checks, settings) { const pause = managedLifecycleCommand(context, `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`); const repair = repairCommand(context, input.request, input.projectRoot); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...checks.map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and use the returned finish command with --retry-check <receipt-id> --reason "<what was restored>". The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work with the exact heredoc below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
|
|
433
433
|
function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
|
|
434
434
|
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
435
435
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
@@ -588,5 +588,6 @@ function findRootWork(context, projectId, runId) { const work = context.db.get("
|
|
|
588
588
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
|
|
589
589
|
function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
|
|
590
590
|
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
|
|
591
|
-
function applyCommand(context, request, projectRoot) { return `${flowCommand(context)} merge apply ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl
|
|
591
|
+
function applyCommand(context, request, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} merge apply ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`); }
|
|
592
|
+
function repairCommand(context, request, projectRoot) { return managedLifecycleCommand(context, `${flowCommand(context)} merge repair ${request.merge_request_id} --project-root ${JSON.stringify(projectRoot)} --json`); }
|
|
592
593
|
export function finishCommand(context, runId, request, projectRoot, retryCheckId) { const retry = retryCheckId ? ` --retry-check ${retryCheckId} --reason "<environment recovery evidence>"` : ""; return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage merge --request ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`); }
|
|
@@ -988,7 +988,7 @@ function renderWorkerPrompt(context, work, run, dependencies) {
|
|
|
988
988
|
codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
|
|
989
989
|
if (packet)
|
|
990
990
|
codeContext.push("<temporary_services>", "Prefer the declared check launcher: it already owns check resources. If the planned scenario genuinely requires an interactive HTTP service, use the managed supervisor below. This is a template: replace the project service command, port names and readiness path from the plan/project instructions; do not invent a fixed port.", `${command} runtime process start --run ${run.id} --project-root ${JSON.stringify(run.project_root)} --command '<project-service-command>' --ports api --ready-port api --ready-path /health --json --progress-jsonl`, "The service receives DD_FLOW_PORT_API (and equivalent variables for all declared names). The command stays running as its supervisor. Retain its tool handle; wait for the service ready event and read its service.json receipt. Pass those exact ports and the same project environment to reset/seed, API and browser operations.", "A ready receipt proves service readiness only. Record the scenario outcome and real evidence separately. After the scenario, execute the exact stop_command from that receipt, then wait for the supervisor to exit. Never use pkill/killall or stop a sibling's process. If cleanup fails, retain the process id and report the failure; do not claim the resource is free.", "</temporary_services>", "");
|
|
991
|
-
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<cli_context>", `- Work commands use the short ID ${shortWorkId(work.work_id)}; dd-flow binds it to this RUN.`, `- @project = ${run.project_root}`, `- @workspace = ${run.workspace_root}`, `- @run = ${requireRunHome(run)}`, "- @ aliases are accepted only by declared dd-flow path parameters. Shell tools such as cat and rg require ordinary paths relative to cwd or absolute paths.", "- Lifecycle invocation IDs are internal runtime authority and are intentionally omitted from commands.", "</cli_context>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", packet?.repair?.verification_check_refs?.length ? "Work finish runs its normal work-scoped checks plus the listed causal repair checks. Their original run_at remains an aggregate obligation; this is the additional proof required before accepting this repair." : "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed.", "A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command with a quoted heredoc (replace the example JSON with your result):\n${finishWorkCommand} <<'DD_FLOW_RESULT'\n{}\nDD_FLOW_RESULT`, `Fail only for an evidenced external or semantic-contract blocker: ${failWorkCommand}`, "</completion>", ""].join("\n");
|
|
991
|
+
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<cli_context>", `- Work commands use the short ID ${shortWorkId(work.work_id)}; dd-flow binds it to this RUN.`, `- @project = ${run.project_root}`, `- @workspace = ${run.workspace_root}`, `- @run = ${requireRunHome(run)}`, "- @ aliases are accepted only by declared dd-flow path parameters. Copy the quoted alias literally; do not add backslashes. Shell tools such as cat and rg require ordinary paths relative to cwd or absolute paths.", "- Lifecycle invocation IDs are internal runtime authority and are intentionally omitted from commands.", "</cli_context>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", packet?.repair?.verification_check_refs?.length ? "Work finish runs its normal work-scoped checks plus the listed causal repair checks. Their original run_at remains an aggregate obligation; this is the additional proof required before accepting this repair." : "Work finish runs only declared run_at=work checks. Stage finish owns readiness/code/merge gates; successful Work completion does not mean those gates have passed.", "A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command with a quoted heredoc (replace the example JSON with your result):\n${finishWorkCommand} <<'DD_FLOW_RESULT'\n{}\nDD_FLOW_RESULT`, `Fail only for an evidenced external or semantic-contract blocker: ${failWorkCommand}`, "</completion>", ""].join("\n");
|
|
992
992
|
}
|
|
993
993
|
export function resultSchemaGuidance(work, runId) {
|
|
994
994
|
const schema = work.result_schema;
|