@deksden-com/dd-flow-cli 0.9.0-beta.88 → 0.9.0-beta.89
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 +6 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/command-inputs.js +13 -10
- package/dist/cli/help.js +2 -0
- package/dist/cli/hook-ingress.js +2 -2
- package/dist/cli/run-cli.js +27 -12
- package/dist/services/codex-hook-delivery.js +3 -3
- package/dist/services/command-context.js +80 -0
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +4 -2
- package/dist/services/hooks.js +21 -15
- package/dist/services/lifecycle-command.js +27 -0
- package/dist/services/lifecycle-invocations.js +65 -31
- package/dist/services/recovery-snapshot-database.js +1 -1
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/run-controller.js +4 -1
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/stage-pause.js +7 -21
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/vnext-code-review.js +13 -11
- package/dist/services/vnext-code.js +39 -21
- package/dist/services/vnext-fanout.js +27 -6
- package/dist/services/vnext-merge.js +13 -10
- package/dist/services/vnext-plan-review.js +6 -5
- package/dist/services/work-registry.js +18 -9
- package/dist/storage/database.js +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.89
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Register aggregate CODE and CODE-REVIEW repairs atomically from all failed receipts, with stable replay identity and stage/attempt/cycle membership. Resolve declared contextual file paths independently of lifecycle authority, retain exact native command rejection causes, and emit shell-safe commands with explicit JSON recovery inputs. Keep historical Work out of current dispatch and block ambiguous legacy fanout execution.
|
|
8
|
+
|
|
3
9
|
## 0.9.0-beta.88
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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.89",
|
|
4
|
+
"cli_commit": "e36aabd9160805831809ca27575600fb6db47511",
|
|
5
|
+
"built_at": "2026-09-21T08:53:05.446Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "d1a6081ab15ab92ac917ff5d037121a40c709db1",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank/.memory-bank",
|
|
@@ -11,6 +11,9 @@ const scoped = "project-root"; // Optional when the router resolves a unique ret
|
|
|
11
11
|
const lifecycle = "invocation-id hook-event-id";
|
|
12
12
|
const worker = "worker-id";
|
|
13
13
|
const control = "project-root run";
|
|
14
|
+
/** Path-shaped arguments default to physical paths unless the route explicitly
|
|
15
|
+
* opts into contextualPaths. This inventory does not expand arbitrary strings. */
|
|
16
|
+
export const physicalPathOptions = new Set(words("root project-root workspace-root workspace-path path dir file schema-dir output result-file semantic-file data decision-file verification-file reason-file task-file intake-file context-file observations-file observation-file summary-file question-file answer-file payload-file json-file task-profile-file execution-routing-file protocol-override-file manifest-file routing-file config-file input-file report-file snapshot-file output-dir state-dir capture-root capture-path destination source archive snapshot plan-file backup-path database backup source-home target-home stdout stderr adapter-executable memory-bank flow stage-report dashboard report"));
|
|
14
17
|
export const commandInputs = {
|
|
15
18
|
"version": route(0),
|
|
16
19
|
"status": route(0, "", "root project-root", "check-registry"),
|
|
@@ -40,19 +43,19 @@ export const commandInputs = {
|
|
|
40
43
|
"plan item block": route(2, "reason user-required", scoped),
|
|
41
44
|
"plan item skip": route(2, "reason", scoped),
|
|
42
45
|
"plan-review dispatch": route(1, "", scoped),
|
|
43
|
-
"prompt render": route(0, `${project} run stage profile`, "workspace-root task-file plan-item"),
|
|
46
|
+
"prompt render": { ...route(0, `${project} run stage profile`, "workspace-root task-file plan-item"), contextualPaths: words("project-root workspace-root task-file") },
|
|
44
47
|
"stage start": { ...route([0, 1], `${project} stage`, `${lifecycle} dir subject intake-file context-file context-sha256`, "bootstrap intake-stdin require-session-binding"), contextualPaths: words("project-root intake-file context-file") },
|
|
45
|
-
"stage finish": { ...route(1, `${project} stage`, `${lifecycle} dir semantic-file data result-file outcome decision-file verification-file retry-check reason request work`, "result-stdin"), contextualPaths: words("project-root semantic-file result-file decision-file verification-file") },
|
|
48
|
+
"stage finish": { ...route(1, `${project} stage`, `${lifecycle} dir semantic-file data result-file outcome decision-file verification-file retry-check reason reason-file request work`, "result-stdin"), contextualPaths: words("project-root semantic-file result-file decision-file verification-file reason-file") },
|
|
46
49
|
"stage pause": { ...route(1, `${project} stage work`, `${lifecycle} question-file`, "question-stdin"), contextualPaths: words("project-root question-file") },
|
|
47
50
|
"stage resume": { ...route(1, `${project} stage work`, `${lifecycle} answer-file`, "answer-stdin"), contextualPaths: words("project-root answer-file") },
|
|
48
51
|
"stage block": { ...route(1, `${project} stage work kind code`, "summary-file", "summary-stdin retryable", "", { kind: ["engine", "harness", "environment"] }), contextualPaths: words("project-root summary-file") },
|
|
49
52
|
"stage unblock": route(1, `${project} stage work`),
|
|
50
53
|
"stage fanout status": route(1, "stage", scoped),
|
|
51
54
|
"stage fanout dispatch": route(1, "stage", scoped),
|
|
52
|
-
"stage fanout reconcile": route(1, "stage observations-file", scoped),
|
|
53
|
-
"stage native observe": route(1, "stage observations-file", scoped),
|
|
55
|
+
"stage fanout reconcile": { ...route(1, "stage observations-file", scoped), contextualPaths: words("project-root observations-file") },
|
|
56
|
+
"stage native observe": { ...route(1, "stage observations-file", scoped), contextualPaths: words("project-root observations-file") },
|
|
54
57
|
"work status": route(1, project),
|
|
55
|
-
"work add-batch": route(0, "parent file", scoped),
|
|
58
|
+
"work add-batch": { ...route(0, "parent file", scoped), contextualPaths: words("project-root file") },
|
|
56
59
|
"work ls": route(0, "", `${scoped} run parent status limit`, "ready include-results", "", { status: ["created", "running", "paused", "completed", "failed", "cancelled"] }),
|
|
57
60
|
"work show": route(1, "", scoped),
|
|
58
61
|
"work repair add": { ...route(0, "run", `${scoped} origin-work from-check from-finding from-unresolved check-ref verification-file task-file`, "task-stdin", "origin-work from-finding from-unresolved check-ref"), contextualPaths: words("project-root verification-file task-file") },
|
|
@@ -63,7 +66,7 @@ export const commandInputs = {
|
|
|
63
66
|
"work delete": route(1, "", scoped),
|
|
64
67
|
"work launch": route(1, "", `${scoped} stage`),
|
|
65
68
|
"work start": { ...route(1, "", `${scoped} ${lifecycle} recovery-id`), contextualPaths: ["project-root"] },
|
|
66
|
-
"work finish": { ...route(1, "", `${scoped} ${lifecycle} result-file retry-check reason`, "result-stdin"), contextualPaths: words("project-root result-file") },
|
|
69
|
+
"work finish": { ...route(1, "", `${scoped} ${lifecycle} result-file retry-check reason reason-file`, "result-stdin"), contextualPaths: words("project-root result-file reason-file") },
|
|
67
70
|
"work fail": { ...route(1, "reason", `${scoped} ${lifecycle}`), contextualPaths: ["project-root"] },
|
|
68
71
|
"work cancel": route(1, "reason", scoped),
|
|
69
72
|
"work retry": route(1, "reason", scoped),
|
|
@@ -218,7 +221,7 @@ export const commandInputs = {
|
|
|
218
221
|
"dashboard open": route(0, "", "project project-root protocol viewer format"),
|
|
219
222
|
"dashboard refresh": route(0, "", "project project-root protocol open format output", "all"),
|
|
220
223
|
"dashboard refresh-global": route(0, "", "output format"),
|
|
221
|
-
"schema validate": route(0, "schema file", "schema-dir project-root run"),
|
|
224
|
+
"schema validate": { ...route(0, "schema file", "schema-dir project-root run"), contextualPaths: words("project-root file") },
|
|
222
225
|
"memory permissions preflight": route(0, "root memory-bank flow mode", "tasks")
|
|
223
226
|
};
|
|
224
227
|
export function contextualPathOptionsForLifecycle(operation) {
|
|
@@ -286,14 +289,14 @@ export function prepareCommandRoute(args) {
|
|
|
286
289
|
["payload-file", "payload-json", "payload-base64"], ["semantic-file", "data", "result-file", "result-stdin"],
|
|
287
290
|
["stage-report", "dashboard"], ["task-file", "plan-item"], ["project", "project-root"],
|
|
288
291
|
["intake-file", "intake-stdin", "context-file"], ["question-file", "question-stdin"], ["answer-file", "answer-stdin"],
|
|
289
|
-
["summary-file", "summary-stdin"], ["task-file", "task-stdin"]
|
|
292
|
+
["summary-file", "summary-stdin"], ["task-file", "task-stdin"], ["reason", "reason-file"]
|
|
290
293
|
]) {
|
|
291
294
|
const present = aliases.filter(name => parsed.options.has(name));
|
|
292
295
|
if (present.length > 1)
|
|
293
296
|
reject(`Use only one of ${present.map(name => `--${name}`).join(", ")}`);
|
|
294
297
|
}
|
|
295
|
-
if (contract.values.includes("retry-check") && parsed.options.has("retry-check") !== parsed.options.has("reason"))
|
|
296
|
-
reject("--retry-check
|
|
298
|
+
if (contract.values.includes("retry-check") && parsed.options.has("retry-check") !== (parsed.options.has("reason") || parsed.options.has("reason-file")))
|
|
299
|
+
reject("--retry-check requires exactly one of --reason or --reason-file");
|
|
297
300
|
// Retain the dispatcher's historic offsets for nested action positions.
|
|
298
301
|
const family = parsed.positional[0];
|
|
299
302
|
const singleWord = ["status", "version", "transition"].includes(family);
|
package/dist/cli/help.js
CHANGED
|
@@ -235,6 +235,8 @@ Usage:
|
|
|
235
235
|
dd-flow stage resume <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --work <WORK-ID> (--answer-file <path>|--answer-stdin) --json
|
|
236
236
|
|
|
237
237
|
Managed resume commands restore the accepted answer file from runtime state. Execute the returned command without adding an answer path or recreating the answer. The explicit answer flags above are for unmanaged calls.
|
|
238
|
+
Path aliases @project, @workspace and @run are supported only in declared CLI path parameters and require a uniquely bound context. File-writing tools use physical paths. Both --option value and --option=value are accepted; quoted or escaped aliases are normalized identically. Pre-RUN bootstrap paths and external schema directories require physical paths.
|
|
239
|
+
An aggregate CODE/CODE-REVIEW failure may return continuation.kind=repair_required: its repair Work is already registered. End the coordinator turn and let the controller dispatch the fresh child; do not recreate the repair. A recovery retry uses --retry-check <receipt> --reason-file <recovery.json>, containing only {"reason":"actual environment recovery evidence"}. Legacy --reason remains accepted, but cannot be combined with --reason-file. A needs_input continuation is not yet executable: write its requested file first, then use command_after_input.
|
|
238
240
|
dd-flow stage block <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --work <WORK-ID> --kind engine|harness|environment --code <code> (--summary-file <path>|--summary-stdin) [--retryable] --json
|
|
239
241
|
dd-flow stage unblock <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --work <WORK-ID> --json
|
|
240
242
|
dd-flow stage fanout status <RUN-ID|RUN-short-id> --project-root <root> --stage <name> --json
|
package/dist/cli/hook-ingress.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
|
-
import { parseCommandArgs, parseLifecycleCommand, lifecycleCommandFromPayload } from "../services/lifecycle-command.js";
|
|
3
|
+
import { parseCommandArgs, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "../services/lifecycle-command.js";
|
|
4
4
|
import { errorRecord, AppError } from "../shared/errors.js";
|
|
5
5
|
import { parseJsonObject } from "../shared/json.js";
|
|
6
6
|
const workerFile = fileURLToPath(import.meta.url);
|
|
@@ -28,7 +28,7 @@ export async function runHookIngress(args) {
|
|
|
28
28
|
}
|
|
29
29
|
const payload = parseJsonObject(stdin || "{}", "native hook stdin");
|
|
30
30
|
const command = lifecycleCommandFromPayload(payload);
|
|
31
|
-
if (event !== "PreToolUse" || !command || parseLifecycleCommand(command).kind === "none") {
|
|
31
|
+
if (event !== "PreToolUse" || !command || (parseLifecycleCommand(command).kind === "none" && !continuationCommand(command))) {
|
|
32
32
|
process.stdout.write("{}\n");
|
|
33
33
|
return 0;
|
|
34
34
|
}
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { resolveCommandPaths, resolveCommandProjectRoot } from "../services/command-context.js";
|
|
3
|
+
import { assertAuxiliaryCommandScope } from "../services/lifecycle-invocations.js";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { randomUUID } from "node:crypto";
|
|
4
6
|
import { quote } from "shell-quote";
|
|
@@ -171,7 +173,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
171
173
|
const retainedOutput = observedInvocation ? parseOutputOptions(expandedInvocationArgs) : null;
|
|
172
174
|
if (retainedOutput?.responseFile)
|
|
173
175
|
output.responseFile = retainedOutput.responseFile;
|
|
174
|
-
|
|
176
|
+
let invocationArgs = retainedOutput?.args ?? expandedInvocationArgs;
|
|
175
177
|
emitHumanProgress(io, output, progress, "start");
|
|
176
178
|
if (output.progressJsonl) {
|
|
177
179
|
writeProgressJsonl(io, "start", `Starting ${output.args.slice(0, 3).join(" ")}`);
|
|
@@ -179,7 +181,9 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
179
181
|
}
|
|
180
182
|
// Route by retained authority, so a mistyped project path reaches the CLI
|
|
181
183
|
// validator instead of selecting a different engine or failing preflight.
|
|
182
|
-
const scopeProjectRoot = invocationScope?.projectRoot ?? resolveProjectRootBeforeRouting(output.args, env);
|
|
184
|
+
const scopeProjectRoot = invocationScope?.projectRoot ?? resolveCommandProjectRoot(routerContext, output.args, resolveProjectRootBeforeRouting(output.args, env));
|
|
185
|
+
assertAuxiliaryCommandScope({ ...routerContext, env }, publicCommand, scopeProjectRoot);
|
|
186
|
+
invocationArgs = resolveCommandPaths(routerContext, invocationArgs, scopeProjectRoot);
|
|
183
187
|
const routedArgs = output.responseFile ? [...invocationArgs, "--response-file", output.responseFile] : invocationArgs;
|
|
184
188
|
const routed = await routeArgsThroughEngine(routerContext, output.json ? [...routedArgs, "--json", ...(output.progressJsonl ? ["--progress-jsonl"] : [])] : routedArgs, io, io.stdin, env, scopeProjectRoot);
|
|
185
189
|
if (routed?.routed && !routed.inProcess) {
|
|
@@ -237,8 +241,11 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
237
241
|
preparedInput = await prepareCliInput(invocationArgs, dispatchContext, io, scopeProjectRoot, output.responseFile);
|
|
238
242
|
if (!retainedOutcome)
|
|
239
243
|
preflightCliCompatibility(dispatchContext, invocationArgs, env, scopeProjectRoot);
|
|
240
|
-
if (!retainedOutcome)
|
|
241
|
-
|
|
244
|
+
if (!retainedOutcome) {
|
|
245
|
+
const command = lifecycleCommand.kind === "none" ? publicCommand : quote(["dd-flow", ...dispatchArgs]);
|
|
246
|
+
const event = lifecycleCommand.kind === "none" && env.DD_FLOW_DAEMON_ID ? observedLifecycleNativeEvent(dispatchContext, command, env.DD_FLOW_DAEMON_ID) : null;
|
|
247
|
+
settleHook = observeLifecycleCommand(dispatchContext, command, event ?? undefined);
|
|
248
|
+
}
|
|
242
249
|
const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output, preparedInput);
|
|
243
250
|
const completedHook = settleHook;
|
|
244
251
|
settleHook = undefined;
|
|
@@ -764,6 +771,14 @@ async function prepareCliInput(...args) {
|
|
|
764
771
|
async function prepareCliInputValue(args, context, io, scopeProjectRoot, responseFile = null, route = prepareCommandRoute(args)) {
|
|
765
772
|
const { family, command, parsed } = route;
|
|
766
773
|
const prepared = { route };
|
|
774
|
+
if (optionalOption(parsed, "reason-file")) {
|
|
775
|
+
const value = prepareJsonFile(requiredOption(parsed, "reason-file"), "Check recovery evidence").value;
|
|
776
|
+
if (!value || typeof value.reason !== "string" || !value.reason.trim() || Object.keys(value).some(key => key !== "reason"))
|
|
777
|
+
throw new AppError("validation", "Recovery evidence must be a JSON object with one nonempty reason", 2);
|
|
778
|
+
prepared.retryReason = value.reason;
|
|
779
|
+
}
|
|
780
|
+
else if (optionalOption(parsed, "retry-check"))
|
|
781
|
+
prepared.retryReason = optionalOption(parsed, "reason");
|
|
767
782
|
if (route.key === "project register")
|
|
768
783
|
resolveProjectRoot(requiredOption(parsed, "root"));
|
|
769
784
|
if (route.key === "project archive")
|
|
@@ -1200,7 +1215,7 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
|
|
|
1200
1215
|
}
|
|
1201
1216
|
}
|
|
1202
1217
|
if (family === "work" && command === "finish") {
|
|
1203
|
-
const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason =
|
|
1218
|
+
const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = prepared.retryReason;
|
|
1204
1219
|
if (Boolean(retryReceiptId) !== Boolean(retryReason))
|
|
1205
1220
|
throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
|
|
1206
1221
|
const resultFile = optionalOption(parsed, "result-file");
|
|
@@ -1225,8 +1240,8 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
|
|
|
1225
1240
|
throw new AppError("usage", "Stage status belongs in the semantic input file", 2);
|
|
1226
1241
|
if (["result-file", "semantic-file", "data", "result-stdin"].filter(key => hasOption(parsed, key)).length > 1)
|
|
1227
1242
|
throw new AppError("usage", "Use exactly one result input: --result-file, --semantic-file, --data or --result-stdin", 2);
|
|
1228
|
-
if (Boolean(optionalOption(parsed, "retry-check")) !== Boolean(
|
|
1229
|
-
throw new AppError("usage", "--retry-check
|
|
1243
|
+
if (Boolean(optionalOption(parsed, "retry-check")) !== Boolean(prepared.retryReason))
|
|
1244
|
+
throw new AppError("usage", "--retry-check requires recovery evidence", 2);
|
|
1230
1245
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
1231
1246
|
const semanticFile = optionalOption(parsed, "semantic-file") ?? optionalOption(parsed, "data");
|
|
1232
1247
|
const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
|
|
@@ -1244,8 +1259,8 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
|
|
|
1244
1259
|
};
|
|
1245
1260
|
const acceptStageInputs = (allowed) => {
|
|
1246
1261
|
const options = allowed.split(" ");
|
|
1247
|
-
for (const name of ["dir", "semantic-file", "data", "result-file", "result-stdin", "outcome", "decision-file", "verification-file", "retry-check", "reason", "request", "work"]) {
|
|
1248
|
-
if (hasOption(parsed, name) && !options.includes(name))
|
|
1262
|
+
for (const name of ["dir", "semantic-file", "data", "result-file", "result-stdin", "outcome", "decision-file", "verification-file", "retry-check", "reason", "reason-file", "request", "work"]) {
|
|
1263
|
+
if (hasOption(parsed, name) && !options.includes(name === "reason-file" ? "reason" : name))
|
|
1249
1264
|
throw new AppError("usage", `stage finish ${stage} does not accept --${name} for this flow`, 2, { parameter: name });
|
|
1250
1265
|
}
|
|
1251
1266
|
};
|
|
@@ -1302,7 +1317,7 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
|
|
|
1302
1317
|
requiredOption(parsed, "request");
|
|
1303
1318
|
requiredOption(parsed, "work");
|
|
1304
1319
|
try {
|
|
1305
|
-
prepared.mergeFinish = prepareVnextMergeFinish(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(optionalOption(parsed, "retry-check") ? { retryCheckId: optionalOption(parsed, "retry-check"), retryReason:
|
|
1320
|
+
prepared.mergeFinish = prepareVnextMergeFinish(context, { projectRoot, runId, requestId: requiredOption(parsed, "request"), workId: requiredOption(parsed, "work"), ...(optionalOption(parsed, "retry-check") ? { retryCheckId: optionalOption(parsed, "retry-check"), retryReason: prepared.retryReason } : {}) });
|
|
1306
1321
|
}
|
|
1307
1322
|
catch (error) {
|
|
1308
1323
|
return prepareError(error, { parameter: "MERGE finish" });
|
|
@@ -1574,7 +1589,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1574
1589
|
const workId = requiredPosition(parsed, 0, "work-id");
|
|
1575
1590
|
const projectRoot = requiredScopeProjectRoot(scopeProjectRoot, "work finish");
|
|
1576
1591
|
assertWorkLifecycleOwner(context, { workId, projectRoot, operation: "work_finish", ...(optionalOption(parsed, "hook-event-id") ? { hookEventId: optionalOption(parsed, "hook-event-id") } : {}) });
|
|
1577
|
-
const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason =
|
|
1592
|
+
const retryReceiptId = optionalOption(parsed, "retry-check"), retryReason = prepared.retryReason;
|
|
1578
1593
|
if (Boolean(retryReceiptId) !== Boolean(retryReason))
|
|
1579
1594
|
throw new AppError("usage", "--retry-check and --reason must be supplied together", 2);
|
|
1580
1595
|
const result = prepared.workFinish?.result ?? (resultFile ? prepareTextFile(resultFile, "Work result").text : await readStdin(io.stdin));
|
|
@@ -1698,7 +1713,7 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1698
1713
|
const resultFile = optionalOption(parsed, "result-file") ?? semanticFile;
|
|
1699
1714
|
const resultStdin = hasOption(parsed, "result-stdin");
|
|
1700
1715
|
const retryCheckId = optionalOption(parsed, "retry-check");
|
|
1701
|
-
const retryReason =
|
|
1716
|
+
const retryReason = prepared.retryReason;
|
|
1702
1717
|
const hookEventId = optionalOption(parsed, "hook-event-id");
|
|
1703
1718
|
if (retryCheckId && !retryReason?.trim())
|
|
1704
1719
|
throw new AppError("usage", "--retry-check requires --reason explaining the recovered environment", 2);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { callDaemon } from "../harness-runtime/lib/dd-codex-daemon.mjs";
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
|
-
import { commandOption, lifecycleCommandFromPayload, parseLifecycleCommand } from "./lifecycle-command.js";
|
|
4
|
+
import { commandOption, lifecycleCommandFromPayload, parseLifecycleCommand, continuationCommand } from "./lifecycle-command.js";
|
|
5
5
|
/** The hook asks the existing daemon about native evidence; it cannot mint it. */
|
|
6
6
|
export async function verifyCodexHookDelivery(env, payload) {
|
|
7
7
|
const command = lifecycleCommandFromPayload(payload);
|
|
8
8
|
const parsed = command ? parseLifecycleCommand(command) : { kind: "none" };
|
|
9
|
-
if (parsed.kind === "none")
|
|
9
|
+
if (parsed.kind === "none" && !(command && continuationCommand(command)))
|
|
10
10
|
return undefined;
|
|
11
|
-
const managed = commandOption(parsed.invocation, "invocation-id") || env.DD_FLOW_DAEMON_ID;
|
|
11
|
+
const managed = (parsed.kind !== "none" && commandOption(parsed.invocation, "invocation-id")) || env.DD_FLOW_DAEMON_ID;
|
|
12
12
|
if (!managed)
|
|
13
13
|
return undefined; // Explicit legacy/unmanaged transport.
|
|
14
14
|
if (!env.DD_FLOW_CODEX_STATE_DIR || !env.DD_FLOW_DAEMON_ID)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { commandInputs, physicalPathOptions } from "../cli/command-inputs.js";
|
|
3
|
+
import { parseCommandArgs } from "./lifecycle-command.js";
|
|
4
|
+
import { assertPathWithin, canonicalPath } from "../storage/paths.js";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { resolveWorkReference } from "../storage/work-references.js";
|
|
7
|
+
export function normalizedPathAlias(value) {
|
|
8
|
+
return value.replace(/^\\+(?=@(?:project|workspace|run)(?:\/|$))/, "");
|
|
9
|
+
}
|
|
10
|
+
export function resolveContextualPaths(args, options, roots) {
|
|
11
|
+
const resolve = (option, original) => {
|
|
12
|
+
if (!options.has(option))
|
|
13
|
+
return original;
|
|
14
|
+
const value = normalizedPathAlias(original);
|
|
15
|
+
if (!value.startsWith("@"))
|
|
16
|
+
return value;
|
|
17
|
+
const match = /^@(project|workspace|run)(?:\/(.*))?$/.exec(value);
|
|
18
|
+
if (!match || !roots[match[1]])
|
|
19
|
+
throw new AppError("usage", `Unknown or unavailable path alias: ${value}`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter: option });
|
|
20
|
+
const root = roots[match[1]];
|
|
21
|
+
return assertPathWithin(root, path.join(root, match[2] ?? ""), `${match[1]}_alias`);
|
|
22
|
+
};
|
|
23
|
+
return args.map((value, index) => {
|
|
24
|
+
const equal = /^--([^=]+)=(.*)$/s.exec(value);
|
|
25
|
+
if (equal)
|
|
26
|
+
return `--${equal[1]}=${resolve(equal[1], equal[2])}`;
|
|
27
|
+
const option = args[index - 1]?.match(/^--([^=]+)$/)?.[1];
|
|
28
|
+
return option ? resolve(option, value) : value;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Contextual paths are route semantics, not lifecycle invocation authority. */
|
|
32
|
+
export function resolveCommandProjectRoot(context, args, explicit) {
|
|
33
|
+
if (explicit && !normalizedPathAlias(explicit).startsWith("@"))
|
|
34
|
+
return canonicalPath(explicit, false);
|
|
35
|
+
if (!context.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'runs'"))
|
|
36
|
+
return null;
|
|
37
|
+
const parsed = parseCommandArgs(args);
|
|
38
|
+
const key = Object.keys(commandInputs).filter(key => key.split(" ").every((part, i) => parsed.positional[i] === part)).sort((a, b) => b.length - a.length)[0];
|
|
39
|
+
const reference = parsed.options.get("run")?.[0] ?? (key?.startsWith("stage ") ? parsed.positional[key.split(" ").length] : undefined);
|
|
40
|
+
if (reference) {
|
|
41
|
+
const rows = context.db.all("SELECT project_root FROM runs WHERE id = ? OR short_id = ?", [reference, reference]);
|
|
42
|
+
if (rows.length === 1)
|
|
43
|
+
return rows[0].project_root;
|
|
44
|
+
if (rows.length > 1)
|
|
45
|
+
throw new AppError("usage", "RUN reference requires an explicit project scope", 2, { phase: "prepare", effect: "no_effect" });
|
|
46
|
+
}
|
|
47
|
+
const work = parsed.options.get("parent")?.[0] ?? (key?.startsWith("work ") && !key.startsWith("work repair") ? parsed.positional[key.split(" ").length] : undefined);
|
|
48
|
+
if (work) {
|
|
49
|
+
const resolved = resolveWorkReference(context, work);
|
|
50
|
+
return context.db.get("SELECT root FROM projects WHERE id = ?", [resolved.project_id])?.root ?? null;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function resolveCommandPaths(context, args, projectRoot) {
|
|
55
|
+
const parsed = parseCommandArgs(args);
|
|
56
|
+
const key = Object.keys(commandInputs).filter(key => key.split(" ").every((part, i) => parsed.positional[i] === part)).sort((a, b) => b.length - a.length)[0];
|
|
57
|
+
const options = new Set(key ? commandInputs[key]?.contextualPaths ?? [] : []);
|
|
58
|
+
if (!args.some(value => /(?:^|=)\\*@/.test(value)))
|
|
59
|
+
return args;
|
|
60
|
+
for (const [option, values] of parsed.options) {
|
|
61
|
+
if (physicalPathOptions.has(option) && !options.has(option) && values.some(value => normalizedPathAlias(value).startsWith("@")))
|
|
62
|
+
throw new AppError("usage", `--${option} requires a physical path on this route`, 2, { phase: "prepare", effect: "no_effect", recoverable: true, parameter: option });
|
|
63
|
+
}
|
|
64
|
+
const project = projectRoot ? context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]) : undefined;
|
|
65
|
+
const reference = parsed.options.get("run")?.[0] ?? (key?.startsWith("stage ") ? parsed.positional[key.split(" ").length] : undefined);
|
|
66
|
+
let run;
|
|
67
|
+
if (reference && project) {
|
|
68
|
+
const rows = context.db.all("SELECT id, workspace_root, run_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
|
|
69
|
+
if (rows.length !== 1)
|
|
70
|
+
throw new AppError("usage", "Path aliases require one RUN in the selected project", 2, { phase: "prepare", effect: "no_effect" });
|
|
71
|
+
run = rows[0];
|
|
72
|
+
}
|
|
73
|
+
const work = parsed.options.get("parent")?.[0] ?? (key?.startsWith("work ") && !key.startsWith("work repair") ? parsed.positional[key.split(" ").length] : undefined);
|
|
74
|
+
if (work && project) {
|
|
75
|
+
const resolved = resolveWorkReference(context, work, { projectId: project.id, ...(run ? { runId: run.id } : {}) });
|
|
76
|
+
if (!run)
|
|
77
|
+
run = context.db.get("SELECT id, workspace_root, run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, resolved.run_id]);
|
|
78
|
+
}
|
|
79
|
+
return resolveContextualPaths(args, options, { project: projectRoot, workspace: run?.workspace_root, run: run?.run_root });
|
|
80
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { isRegisteredRepair } from "./repair-intents.js";
|
|
3
|
+
import { stageAttempt } from "./stage-work-graph.js";
|
|
4
|
+
/** Only the settled coordinator turn can explain a repeated continuation.
|
|
5
|
+
* A stale error from another stage/turn or an error followed by success is not
|
|
6
|
+
* authority. Never inspect the assistant's prose for this decision. */
|
|
7
|
+
export function assertNoCorrelatedRejection(context, scope) {
|
|
8
|
+
const rows = context.db.all(`SELECT outcome_json FROM hook_events
|
|
9
|
+
WHERE project_id = ? AND json_extract(outcome_json, '$.run_id') = ?
|
|
10
|
+
AND json_extract(outcome_json, '$.generation') = ? AND json_extract(outcome_json, '$.stage') = ?
|
|
11
|
+
AND json_extract(outcome_json, '$.controller_turn') = ? AND json_extract(outcome_json, '$.attempt') = ? ORDER BY id`, [scope.projectId, scope.runId, scope.generation, scope.stage, scope.turn, stageAttempt(context, scope.projectId, scope.runId, scope.stage)]);
|
|
12
|
+
const latest = new Map();
|
|
13
|
+
for (const row of rows) {
|
|
14
|
+
const value = JSON.parse(row.outcome_json);
|
|
15
|
+
latest.set(value.operation, value);
|
|
16
|
+
}
|
|
17
|
+
for (const value of latest.values())
|
|
18
|
+
if (value.error && !isRegisteredRepair(context, value.error.details.continuation, scope))
|
|
19
|
+
throw new AppError(value.error.code, value.error.message, 1, { ...value.error.details, operation: value.operation, hook_event_id: value.hook_event_id, continuation_scope: scope });
|
|
20
|
+
}
|
|
@@ -28,6 +28,8 @@ export async function nextControllerFanout(context, input) {
|
|
|
28
28
|
let fanout = getVnextFanoutStatus(context, input).orchestration;
|
|
29
29
|
if (!fanout)
|
|
30
30
|
return { kind: "none" };
|
|
31
|
+
if (fanout.state === "compatibility_required")
|
|
32
|
+
throw new AppError("stage_graph_compatibility_required", "The retained graph cannot be dispatched by this controller; use the RUN's pinned engine", 1, { stage: input.stage, run_id: input.runId, graph: fanout });
|
|
31
33
|
if (children.length) {
|
|
32
34
|
const reconciled = await reconcileVnextFanout(context, { ...input, observations });
|
|
33
35
|
const issues = reconciled.issues.filter(issue => issue.code !== "child_check_in_progress");
|
|
@@ -43,7 +45,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
43
45
|
if (fanout.state === "dispatch_required" || fanout.state === "capacity_required")
|
|
44
46
|
return { kind: "wait", reason: "qualified_capacity_required" };
|
|
45
47
|
}
|
|
46
|
-
const prompt = (text) => ({ kind: "prompt", prompt: text, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id,
|
|
48
|
+
const prompt = (text) => ({ kind: "prompt", prompt: text, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, state: fanout.state, graph: fanout.graph_revision }) });
|
|
47
49
|
if (fanout.state === "coordinator_required")
|
|
48
50
|
return prompt(`Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
|
|
49
51
|
if (fanout.works.failed || fanout.works.cancelled)
|
|
@@ -73,7 +75,7 @@ export async function nextControllerFanout(context, input) {
|
|
|
73
75
|
return { kind: "wait", reason: "work_receipts_pending" };
|
|
74
76
|
if (fanout.works.created)
|
|
75
77
|
throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
|
|
76
|
-
return prompt(`All declared ${input.stage} child Work has settled (${fanout.works.completed} completed). Read the accepted result receipts and continue this Stage's semantic decision under its original packet. A rejected stage finish
|
|
78
|
+
return prompt(`All declared ${input.stage} child Work has settled (${fanout.works.completed} completed). Read the accepted result receipts and continue this Stage's semantic decision under its original packet. A rejected stage finish with repair_required has already registered its repair Work. End this Turn immediately so the controller can launch a fresh child; do not create another repair or invoke its start_command in this coordinator Session. Do not start a successor Stage.`);
|
|
77
79
|
}
|
|
78
80
|
/** Normalize provider-observed topology only; never parse assistant prose. */
|
|
79
81
|
export function controllerNativeChildren(receipt, rootSessionId) {
|
package/dist/services/hooks.js
CHANGED
|
@@ -12,7 +12,7 @@ import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-pre
|
|
|
12
12
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
13
13
|
import { assertRecoveryHookCurrent } from "./run-recovery.js";
|
|
14
14
|
import { observeLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
15
|
-
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload } from "./lifecycle-command.js";
|
|
15
|
+
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
|
|
16
16
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
17
17
|
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
|
|
18
18
|
const defaultProfileName = "default";
|
|
@@ -309,13 +309,13 @@ export function handleCodexHook(context, input) {
|
|
|
309
309
|
if (!command)
|
|
310
310
|
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
311
311
|
const lifecycle = lifecycleFacts(command);
|
|
312
|
-
if (!lifecycle)
|
|
312
|
+
if (!lifecycle && !continuationCommand(command))
|
|
313
313
|
return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
|
|
314
314
|
const parentProviderSessionId = rawChildProviderSessionId ? codexNativeParentId(payload, rawChildProviderSessionId) : null;
|
|
315
315
|
// Resume legitimately receives an answer through stdin. It is matched by
|
|
316
316
|
// immutable lifecycle arguments below; never reject or rewrite that pipe.
|
|
317
|
-
const
|
|
318
|
-
const invocationId = commandOption(lifecycle.invocation, "invocation-id");
|
|
317
|
+
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
318
|
+
const invocationId = lifecycle ? commandOption(lifecycle.invocation, "invocation-id") : undefined;
|
|
319
319
|
if (invocationId || context.env.DD_FLOW_DAEMON_ID) {
|
|
320
320
|
const proof = context.codexHookProof;
|
|
321
321
|
if (!proof || proof.daemonId !== context.env.DD_FLOW_DAEMON_ID || proof.rootSessionId !== rootProviderSessionId
|
|
@@ -337,7 +337,7 @@ export function handleCodexHook(context, input) {
|
|
|
337
337
|
throw new AppError("project_not_registered", "Managed lifecycle invocation has no registered project", 1, { project_root: nativeProjectRoot ?? null });
|
|
338
338
|
return { ok: true, observed: false, reason: "unrelated_cwd" };
|
|
339
339
|
}
|
|
340
|
-
let matchKey = lifecycleMatchKey(lifecycle, project.root);
|
|
340
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, project.root) : null;
|
|
341
341
|
// Codex retains the root session_id for a thread-spawned child, while the
|
|
342
342
|
// payload transcript belongs to the current agent_id. Keep lineage separate
|
|
343
343
|
// from ownership: otherwise a child overwrites the root transcript binding.
|
|
@@ -396,6 +396,10 @@ export function handleCodexHook(context, input) {
|
|
|
396
396
|
...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("codex-desktop", parentProviderSessionId) } : {}),
|
|
397
397
|
protocol_id: null
|
|
398
398
|
};
|
|
399
|
+
if (!lifecycle) {
|
|
400
|
+
const persisted = context.db.writeTransaction(() => persist());
|
|
401
|
+
return { ...common, observed: persisted.inserted, duplicate: !persisted.inserted };
|
|
402
|
+
}
|
|
399
403
|
if (invocationId || context.env.DD_FLOW_DAEMON_ID) {
|
|
400
404
|
const daemonId = stringValue(context.env.DD_FLOW_DAEMON_ID);
|
|
401
405
|
const toolCallId = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
|
|
@@ -517,9 +521,9 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
517
521
|
if (!command || (toolName && toolName !== "Bash" && toolName !== "bash" && toolName !== "execute"))
|
|
518
522
|
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
519
523
|
const lifecycle = lifecycleFacts(command);
|
|
520
|
-
if (!lifecycle)
|
|
524
|
+
if (!lifecycle && !continuationCommand(command))
|
|
521
525
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
522
|
-
if (!invocationId && commandOption(lifecycle.invocation, "invocation-id"))
|
|
526
|
+
if (!invocationId && lifecycle && commandOption(lifecycle.invocation, "invocation-id"))
|
|
523
527
|
return { ok: true, observed: false, reason: "cli_invocation_owns_admission" };
|
|
524
528
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
525
529
|
const meta = objectRecord(hook._meta);
|
|
@@ -539,13 +543,13 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
539
543
|
throw new AppError("zcode_hook_event_missing", "ZCode native hook has no tool-use ID", 1);
|
|
540
544
|
const eventKey = crypto.createHash("sha256").update(JSON.stringify(["zcode-acp", daemonId, providerSessionId, toolUseId])).digest("hex");
|
|
541
545
|
const rewritten = invocationId ? command : commandWithHookEvent(command, eventKey);
|
|
542
|
-
if (lifecycle
|
|
546
|
+
if (lifecycle?.bootstrapStageStart)
|
|
543
547
|
registerProject(context, { root: expectedRoot });
|
|
544
548
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
545
549
|
const sessionId = storageSessionId(nativeSessionIdentity("zcode-acp", providerSessionId));
|
|
546
550
|
const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("zcode-acp", parentProviderSessionId)) : null;
|
|
547
551
|
const observedProfile = objectRecord(ddZcode.observedProfile);
|
|
548
|
-
let matchKey = lifecycleMatchKey(lifecycle, expectedRoot);
|
|
552
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
549
553
|
const eventNameForReceipt = invocationId ? "ToolCallObserved" : "PreToolUse";
|
|
550
554
|
const persist = (scope) => {
|
|
551
555
|
if (scope)
|
|
@@ -563,6 +567,8 @@ function recordZcodeLifecycle(context, input, invocationId) {
|
|
|
563
567
|
const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, harness: "zcode-acp", session: nativeSessionIdentity("zcode-acp", providerSessionId), ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", parentProviderSessionId) } : {}), daemon_id: daemonId };
|
|
564
568
|
return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: rewritten } } };
|
|
565
569
|
};
|
|
570
|
+
if (!lifecycle)
|
|
571
|
+
return context.db.writeTransaction(() => persist());
|
|
566
572
|
let persistedResult;
|
|
567
573
|
const receipt = observeLifecycleInvocation(context, {
|
|
568
574
|
...(invocationId ? { id: invocationId } : {}), command,
|
|
@@ -592,11 +598,11 @@ export function handleGrokEvent(context, input) {
|
|
|
592
598
|
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
593
599
|
}
|
|
594
600
|
const lifecycle = lifecycleFacts(command);
|
|
595
|
-
if (!lifecycle)
|
|
601
|
+
if (!lifecycle && !continuationCommand(command))
|
|
596
602
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
597
|
-
const
|
|
603
|
+
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
598
604
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
599
|
-
const matchKey = lifecycleMatchKey(lifecycle, expectedRoot);
|
|
605
|
+
const matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
600
606
|
if (bootstrapStageStart)
|
|
601
607
|
registerProject(context, { root: expectedRoot });
|
|
602
608
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
@@ -692,10 +698,10 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
692
698
|
return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
|
|
693
699
|
}
|
|
694
700
|
const lifecycle = lifecycleFacts(command);
|
|
695
|
-
if (!lifecycle)
|
|
701
|
+
if (!lifecycle && !continuationCommand(command))
|
|
696
702
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
697
|
-
const
|
|
698
|
-
const matchKey = lifecycleMatchKey(lifecycle, expectedRoot);
|
|
703
|
+
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
704
|
+
const matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
699
705
|
if (bootstrapStageStart)
|
|
700
706
|
registerProject(context, { root: expectedRoot });
|
|
701
707
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
@@ -12,6 +12,33 @@ const redirectionOperators = new Set(["<", ">", ">>", ">|", "<&", ">&", "<<<"]);
|
|
|
12
12
|
export function parseLifecycleCommand(command) {
|
|
13
13
|
return parseLifecycleCommandInner(command, false);
|
|
14
14
|
}
|
|
15
|
+
/** Exact diagnostic routes; these deliberately confer no lifecycle authority. */
|
|
16
|
+
export function continuationCommand(command) {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = parse(command, name => `$${name}`);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (!entries.every(entry => typeof entry === "string"))
|
|
25
|
+
return null;
|
|
26
|
+
const words = entries;
|
|
27
|
+
const start = words.findIndex(word => ["stage", "work"].includes(word));
|
|
28
|
+
if (start < 1)
|
|
29
|
+
return null;
|
|
30
|
+
const launcher = words.slice(0, start);
|
|
31
|
+
while (launcher[0] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(launcher[0]))
|
|
32
|
+
launcher.shift();
|
|
33
|
+
const direct = launcher.length === 1 && (path.basename(launcher[0]) === "dd-flow" || launcher[0] === "$DD_FLOW_BIN");
|
|
34
|
+
const node = launcher.length === 2 && path.basename(launcher[0]) === "node" && (path.basename(launcher[1]) === "cli.js" || launcher[1] === "$DD_FLOW_BIN");
|
|
35
|
+
if (!direct && !node)
|
|
36
|
+
return null;
|
|
37
|
+
const args = parseCommandArgs(words.slice(start));
|
|
38
|
+
const route = args.positional.slice(0, 3).join(" ");
|
|
39
|
+
const operation = route === "work repair add" ? "work_repair_add" : args.positional[0] === "stage" && args.positional[1] === "block" ? "stage_block" : null;
|
|
40
|
+
return operation ? { operation, args } : null;
|
|
41
|
+
}
|
|
15
42
|
/** Preserve the historical Desktop contract: execute the payload, not its one-command shell wrapper. */
|
|
16
43
|
export function unwrapShellCommand(command) {
|
|
17
44
|
let entries;
|