@deksden-com/dd-flow-cli 0.9.0-beta.121 → 0.9.0-beta.122
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 +9 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/hook-ingress.js +4 -3
- package/dist/cli/native-hook-ingress.js +34 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +16 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
- package/dist/harness-runtime/lib/dd-zcode.mjs +25 -12
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/services/codex-hook-delivery.js +6 -2
- package/dist/services/hooks.js +51 -59
- package/dist/services/lifecycle-command.js +1 -383
- package/dist/services/lifecycle-contract.js +1 -30
- package/dist/services/lifecycle-invocations.js +34 -13
- package/dist/services/runs.js +60 -47
- package/dist/services/stage-report-renderer.js +19 -0
- package/dist/services/verification-matrix.js +325 -0
- package/dist/services/vnext-code-review.js +25 -8
- package/dist/services/vnext-code.js +31 -10
- package/dist/services/vnext-merge.js +30 -7
- package/dist/services/vnext-plan-review.js +22 -6
- package/dist/services/vnext-plan.js +16 -3
- package/dist/services/work-registry.js +13 -1
- package/dist/storage/database.js +43 -9
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.122
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Generate a deterministic RUN-local verification matrix from accepted requirements, PLAN and exact check receipts; publish retained input/output packets and show their paths in shared Stage/Work instructions.
|
|
8
|
+
- Recognize one unambiguous lifecycle command through the shared Bash grammar instead of rejecting all shell composition; retain scoped authority, execution-directory and foreground checks.
|
|
9
|
+
- Share native hook deadlines through subprocess and SQLite admission, retain bounded phase diagnostics and the first ZCode failure, and prepare Stage archive/Git work outside the writer.
|
|
10
|
+
- Admit the tested ZCode ACP 0.52 bridge and retain explicitly configured native storage routes across daemon lifecycle calls.
|
|
11
|
+
|
|
3
12
|
## 0.9.0-beta.120
|
|
4
13
|
|
|
5
14
|
### 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.122",
|
|
4
|
+
"cli_commit": "b03c699ac77fa596e684408c8e46e8710edbe9f6",
|
|
5
|
+
"built_at": "2026-09-30T01:56:09.576Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version": "4.1.
|
|
8
|
-
"commit": "
|
|
7
|
+
"version": "4.1.2",
|
|
8
|
+
"commit": "0cf38231406614cfeb0a96001ba0201de2316e47",
|
|
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",
|
package/dist/cli/hook-ingress.js
CHANGED
|
@@ -21,11 +21,12 @@ export async function runHookIngress(args) {
|
|
|
21
21
|
throw new AppError("usage", "--project-root requires a value", 2);
|
|
22
22
|
if (parsed.options.has("json") && parsed.options.get("json")?.[0] !== "__dd_flow_flag__")
|
|
23
23
|
throw new AppError("usage", "--json does not take a value", 2);
|
|
24
|
-
const budget = hookBudget(ingressBudgetMs);
|
|
24
|
+
const budget = hookBudget(ingressBudgetMs, process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null);
|
|
25
25
|
const stdin = await readNativeHookInput(process.stdin, budget);
|
|
26
26
|
const payload = parseJsonObject(stdin || "{}", "native hook stdin");
|
|
27
27
|
const command = lifecycleCommandFromPayload(payload);
|
|
28
|
-
|
|
28
|
+
const analysis = command ? parseLifecycleCommand(command) : { kind: "none", unresolved: false };
|
|
29
|
+
if (event !== "PreToolUse" || !command || (analysis.kind === "none" && !analysis.unresolved && !continuationCommand(command))) {
|
|
29
30
|
process.stdout.write("{}\n");
|
|
30
31
|
return 0;
|
|
31
32
|
}
|
|
@@ -46,7 +47,7 @@ export async function runHookIngress(args) {
|
|
|
46
47
|
// A bounded CLI subprocess, not a detached writer: its successful reply follows COMMIT.
|
|
47
48
|
if (process.argv[1] === workerFile) {
|
|
48
49
|
try {
|
|
49
|
-
const stdin = await readNativeHookInput(process.stdin, hookBudget(ingressBudgetMs));
|
|
50
|
+
const stdin = await readNativeHookInput(process.stdin, hookBudget(ingressBudgetMs, process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null));
|
|
50
51
|
const payload = parseJsonObject(stdin, "native hook stdin");
|
|
51
52
|
const started = performance.now();
|
|
52
53
|
const { verifyCodexHookDelivery } = await import("../services/codex-hook-delivery.js");
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
2
2
|
import { parseCommandArgs } from "../services/lifecycle-command.js";
|
|
3
|
+
import { channel } from "node:diagnostics_channel";
|
|
3
4
|
/** These ingress commands consume a prepared store; they never prepare it. */
|
|
4
5
|
export function isNativeHookIngress(args) {
|
|
5
6
|
return args[2] === "handle" && ((args[1] === "event" && ["agy", "grok", "droid", "opencode", "zcode"].includes(args[0] ?? ""))
|
|
6
7
|
|| args[0] === "zcode" && args[1] === "hook");
|
|
7
8
|
}
|
|
8
9
|
export async function dispatchNativeHook(args, stdin, env) {
|
|
10
|
+
const started = performance.now();
|
|
11
|
+
const deadline = env.DD_FLOW_HOOK_DEADLINE_AT === undefined ? undefined : Number(env.DD_FLOW_HOOK_DEADLINE_AT);
|
|
12
|
+
if (deadline !== undefined && !Number.isSafeInteger(deadline))
|
|
13
|
+
throw new AppError("hook_deadline_invalid", "Native hook deadline must be an absolute millisecond timestamp", 1, { effect: "unknown" });
|
|
14
|
+
const phase = (name) => {
|
|
15
|
+
if (deadline !== undefined && deadline <= Date.now())
|
|
16
|
+
throw new AppError("native_hook_timeout", "Native hook deadline expired", 1, { phase: name, effect: "unknown" });
|
|
17
|
+
process.stderr.write(`${JSON.stringify({ kind: "native_hook_phase", phase: name, elapsed_ms: Math.round(performance.now() - started), ...(deadline !== undefined ? { remaining_ms: Math.max(0, deadline - Date.now()) } : {}) })}\n`);
|
|
18
|
+
};
|
|
19
|
+
phase("import");
|
|
9
20
|
const parsed = parseCommandArgs(args.slice(3));
|
|
10
21
|
const projectRoot = parsed.options.get("project-root")?.[0];
|
|
11
22
|
if (!isNativeHookIngress(args) || parsed.positional.length || !projectRoot || projectRoot === "__dd_flow_flag__")
|
|
@@ -16,27 +27,48 @@ export async function dispatchNativeHook(args, stdin, env) {
|
|
|
16
27
|
const [hooks, { createContext }, { resolveDdFlowHome }] = await Promise.all([
|
|
17
28
|
import("../services/hooks.js"), import("../runtime/context.js"), import("../storage/paths.js")
|
|
18
29
|
]);
|
|
30
|
+
phase("imported");
|
|
19
31
|
let opened;
|
|
20
32
|
// Reuse the actual envelope validators and shell parser. They return for
|
|
21
33
|
// unrelated tools before their first DB access, without duplicating policy.
|
|
22
34
|
const context = {
|
|
23
35
|
env, ddFlowHome: resolveDdFlowHome(env), now: () => new Date().toISOString(),
|
|
24
|
-
get db() {
|
|
36
|
+
get db() {
|
|
37
|
+
if (!opened) {
|
|
38
|
+
phase("store_open");
|
|
39
|
+
opened = createContext(env, "hook");
|
|
40
|
+
phase("store_opened");
|
|
41
|
+
}
|
|
42
|
+
return opened.db;
|
|
43
|
+
}
|
|
25
44
|
};
|
|
26
45
|
const handler = args[0] === "agy" ? hooks.handleAgyEvent : args[0] === "grok" ? hooks.handleGrokEvent
|
|
27
46
|
: args[0] === "droid" ? hooks.handleDroidEvent : args[0] === "opencode" ? hooks.handleOpenCodeEvent
|
|
28
47
|
: args[1] === "hook" ? hooks.handleZcodeHook : hooks.handleZcodeEvent;
|
|
48
|
+
const timing = channel("dd-flow.sqlite.ingress");
|
|
49
|
+
// This command owns one synchronous store; there is no concurrent handler here.
|
|
50
|
+
let previousPhase;
|
|
51
|
+
const log = (value) => {
|
|
52
|
+
const record = value;
|
|
53
|
+
if (record.phase === previousPhase)
|
|
54
|
+
return;
|
|
55
|
+
previousPhase = record.phase;
|
|
56
|
+
process.stderr.write(`${JSON.stringify({ kind: "native_hook_phase", phase: record.phase, elapsed_ms: Math.round(performance.now() - started), ...(record.remaining_ms !== undefined ? { remaining_ms: record.remaining_ms } : {}) })}\n`);
|
|
57
|
+
};
|
|
58
|
+
timing.subscribe(log);
|
|
29
59
|
try {
|
|
60
|
+
phase("receipt");
|
|
30
61
|
return await handler(context, { projectRoot, stdin });
|
|
31
62
|
}
|
|
32
63
|
finally {
|
|
64
|
+
timing.unsubscribe(log);
|
|
33
65
|
opened?.db.close?.();
|
|
34
66
|
}
|
|
35
67
|
}
|
|
36
68
|
export async function runNativeHookIngress(args) {
|
|
37
69
|
try {
|
|
38
70
|
const { readNativeHookInput, hookBudget } = await import("../harness-runtime/lib/native-hook-command.mjs");
|
|
39
|
-
const stdin = await readNativeHookInput(process.stdin, hookBudget());
|
|
71
|
+
const stdin = await readNativeHookInput(process.stdin, hookBudget(25_000, process.env.DD_FLOW_HOOK_DEADLINE_AT ? Number(process.env.DD_FLOW_HOOK_DEADLINE_AT) : null));
|
|
40
72
|
process.stdout.write(`${JSON.stringify(await dispatchNativeHook(args, stdin, process.env))}\n`);
|
|
41
73
|
return 0;
|
|
42
74
|
}
|
|
@@ -15,7 +15,7 @@ async function hook(options) {
|
|
|
15
15
|
const payload = JSON.parse(await readNativeHookInput(process.stdin, budget) || "{}"), event = options.event;
|
|
16
16
|
try {
|
|
17
17
|
const observationMs = budget.remaining("observation");
|
|
18
|
-
const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload, daemonId: options["daemon-id"], deadlineAt:
|
|
18
|
+
const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload, daemonId: options["daemon-id"], deadlineAt: budget.deadlineAt }, observationMs, requestId);
|
|
19
19
|
if (identity.stale) return event === "PreToolUse" ? { decision: "deny", reason: "agy_stale_hook" } : {};
|
|
20
20
|
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
21
21
|
const phase = event === "PreToolUse" ? "before" : "after", args = payload.toolCall?.args ?? {};
|
|
@@ -28,7 +28,10 @@ async function hook(options) {
|
|
|
28
28
|
} catch (error) {
|
|
29
29
|
// A failed socket cannot be used to report its own failure. The daemon
|
|
30
30
|
// reads only receipts matching request IDs it actually accepted this turn.
|
|
31
|
-
|
|
31
|
+
const correctable = event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid" && error.details?.effect === "no_effect"
|
|
32
|
+
&& error.details?.recoverable === true && error.details?.disposition === "pre_cli_shell_refusal";
|
|
33
|
+
try { await writeJsonAtomic(path.join(options["state-dir"], "hook-failures", `${requestId}.json`), { request_id: requestId, observed_at: new Date().toISOString(), code: error.code ?? "agy_hook_rejected", message: error.message,
|
|
34
|
+
details: { ...error.details, effect: correctable ? "no_effect" : "unknown", ...(correctable ? { native_pre_cli_refusal: true } : {}) } }); }
|
|
32
35
|
catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
|
|
33
36
|
throw error;
|
|
34
37
|
}
|
|
@@ -49,4 +52,4 @@ try {
|
|
|
49
52
|
else if (family === "session") { if (command === "fork") throw Object.assign(new Error("Antigravity native headless fork is unsupported; use deterministic_replay"), { code: "agy_headless_fork_unsupported" }); const operation = command === "status" ? "inspect" : command; result = await callDaemon(common.stateDir, `session.${operation}`, { sessionId: options["session-id"], ...(command === "create" || command === "prompt" ? { prompt: await prompt(options) } : {}), tree: options.tree === true }, common.timeoutMs ?? 1_800_000); }
|
|
50
53
|
else throw new Error("usage: dd-agy doctor | hooks install|check | daemon start|status|stop | session create|prompt|inspect|resume|cancel|fork [options] --json");
|
|
51
54
|
process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
|
|
52
|
-
} catch (error) { if (process.argv.includes("hook") && process.argv.includes("handle")) { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.code ?? "agy_hook_rejected" })}\n`); process.exit(0); } process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
|
|
55
|
+
} catch (error) { if (process.argv.includes("hook") && process.argv.includes("handle")) { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: `${error.code ?? "agy_hook_rejected"}: ${error.message ?? "Native hook refused this tool call"}` })}\n`); process.exit(0); } process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
|
|
@@ -404,6 +404,21 @@ export class Runtime {
|
|
|
404
404
|
}
|
|
405
405
|
async rejectHook(error, event, payload, current = this.active) {
|
|
406
406
|
if (current && this.active !== current) return;
|
|
407
|
+
const correctable = current && event === "PreToolUse" && error.code === "lifecycle_shell_syntax_invalid"
|
|
408
|
+
&& error.details?.effect === "no_effect" && error.details?.recoverable === true
|
|
409
|
+
&& error.details?.disposition === "pre_cli_shell_refusal" && error.details?.native_pre_cli_refusal === true;
|
|
410
|
+
if (correctable && current.hookCorrectionGeneration !== current.generation) {
|
|
411
|
+
// The request was correlated with this still-active Turn by
|
|
412
|
+
// observeHookFailures. Persist before allowing native same-Turn recovery.
|
|
413
|
+
const correction = { code: error.code, message: error.message, details: error.details, event, conversation_id: payload.conversationId ?? null,
|
|
414
|
+
daemon_id: this.config.daemonId, operation_id: current.operationId, turn_generation: current.generation };
|
|
415
|
+
try {
|
|
416
|
+
await this.journal("hook_correction", { correction });
|
|
417
|
+
await this.persist({ last_hook_correction: correction });
|
|
418
|
+
current.hookCorrectionGeneration = current.generation;
|
|
419
|
+
return;
|
|
420
|
+
} catch (storage) { error = Object.assign(new Error("Could not persist native hook correction"), { code: "native_hook_receipt_invalid", details: { effect: "unknown", primary_error: correction, receipt_error: storage.message } }); }
|
|
421
|
+
}
|
|
407
422
|
const rejection = this.state.last_hook_rejection && this.state.last_hook_rejection.turn_generation === current?.generation ? this.state.last_hook_rejection : { code: error.code ?? "agy_hook_rejected", message: error.message ?? String(error), details: error.details, event, conversation_id: payload.conversationId ?? null, tool: payload.toolCall?.name ?? null, daemon_id: this.config.daemonId, operation_id: current?.operationId ?? null, turn_generation: current?.generation ?? null };
|
|
408
423
|
this.state.last_hook_rejection = rejection;
|
|
409
424
|
if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
|
|
@@ -417,6 +432,7 @@ export class Runtime {
|
|
|
417
432
|
try { failure = JSON.parse(await readFile(path.join(this.paths.dir, "hook-failures", `${requestId}.json`), "utf8")); }
|
|
418
433
|
catch (error) { if (error.code === "ENOENT") continue; failure = { code: "native_hook_receipt_invalid", message: error.message }; }
|
|
419
434
|
if (failure.request_id !== undefined && failure.request_id !== requestId) continue;
|
|
435
|
+
this.hookRequests.delete(requestId);
|
|
420
436
|
await this.rejectHook(Object.assign(new Error(failure.message ?? "Native hook failed"), { code: failure.code, details: failure.details }), binding.event, binding.payload, binding.current);
|
|
421
437
|
}
|
|
422
438
|
}
|
|
@@ -124,8 +124,19 @@ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 3
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
const ZCODE_ROUTING_ENV = ["ZCODE_HOME", "ZCODE_STORAGE_DIR", "ZCODE_SESSION_DB_PATH"];
|
|
128
|
+
|
|
129
|
+
function nativeRoutingEnv() {
|
|
130
|
+
return Object.fromEntries(ZCODE_ROUTING_ENV.filter(key => process.env[key] !== undefined)
|
|
131
|
+
.map(key => [key, absolute(process.env[key], key)]));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function sameNativeRouting(previous, config) {
|
|
135
|
+
return ZCODE_ROUTING_ENV.every(key => (previous?.env?.[key] ?? null) === (config.env?.[key] ?? null));
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
function sameConfig(state, config) {
|
|
128
|
-
return ["cwd", "journal", "bin", "zcodePath", "ddFlowBin", "ddFlowHome", "projectRoot", "resourceHome", "livenessTimeoutMs", "requestTimeoutMs"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null));
|
|
139
|
+
return sameNativeRouting(state.config, config) && ["cwd", "journal", "bin", "zcodePath", "ddFlowBin", "ddFlowHome", "projectRoot", "resourceHome", "livenessTimeoutMs", "requestTimeoutMs"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null));
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
export async function startDaemon(options) {
|
|
@@ -145,7 +156,7 @@ export async function startDaemon(options) {
|
|
|
145
156
|
ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null,
|
|
146
157
|
projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
|
|
147
158
|
resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
|
|
148
|
-
env: options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {},
|
|
159
|
+
env: { ...nativeRoutingEnv(), ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {}) },
|
|
149
160
|
livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
|
|
150
161
|
requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
|
|
151
162
|
};
|
|
@@ -159,7 +170,10 @@ export async function startDaemon(options) {
|
|
|
159
170
|
} catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
|
|
160
171
|
|
|
161
172
|
const previous = await readState(paths.dir);
|
|
162
|
-
if (previous?.shutdown_state === "clean")
|
|
173
|
+
if (previous?.shutdown_state === "clean") {
|
|
174
|
+
if (!sameNativeRouting(previous.config, config)) throw new DaemonError("daemon_config_mismatch", "retained ZCode native storage routing changed");
|
|
175
|
+
await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
|
|
176
|
+
}
|
|
163
177
|
if (previous?.shutdown_state === "running" && previous.active_tree) {
|
|
164
178
|
throw new DaemonError("invalid_harness_crash", "the previous daemon died with an active or unproven Session tree", false, { daemon_id: previous.daemon_id });
|
|
165
179
|
}
|
|
@@ -237,6 +251,7 @@ export class DaemonRuntime {
|
|
|
237
251
|
const generation = this.activeGeneration ?? this.controlGeneration ?? 0;
|
|
238
252
|
return {
|
|
239
253
|
...this.state.config, ...params, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
|
|
254
|
+
neverPromptedRootProof: this.neverPromptedRootProof(params.sessionId),
|
|
240
255
|
daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter),
|
|
241
256
|
assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); },
|
|
242
257
|
// The bridge owns the quiet timer; the daemon owns the durable status.
|
|
@@ -304,6 +319,12 @@ export class DaemonRuntime {
|
|
|
304
319
|
// A late identity is evidence, not permission to resurrect a completed
|
|
305
320
|
// request or overwrite another operation's ownership.
|
|
306
321
|
this.track(identity);
|
|
322
|
+
const allocated = this.sessions.get(identity.provider_session_id);
|
|
323
|
+
if (allocated && this.activeProductive === "session.create" && current?.allocation?.adapter_session_id === identity.adapter_session_id
|
|
324
|
+
&& allocated.prompt_dispatched === undefined) {
|
|
325
|
+
allocated.prompt_dispatched = false;
|
|
326
|
+
allocated.created_by_daemon_id = this.state.daemon_id;
|
|
327
|
+
}
|
|
307
328
|
if (!current || current.allocation?.adapter_session_id !== identity.adapter_session_id) {
|
|
308
329
|
await this.persist({});
|
|
309
330
|
return;
|
|
@@ -344,6 +365,8 @@ export class DaemonRuntime {
|
|
|
344
365
|
topology: topology ?? existing?.topology ?? null,
|
|
345
366
|
known_child_session_ids: [...new Set([...(existing?.known_child_session_ids ?? zcodeChildSessionIds(existing?.topology)), ...zcodeChildSessionIds(topology)])],
|
|
346
367
|
completed_turns: existing?.completed_turns ?? 0,
|
|
368
|
+
prompt_dispatched: existing?.prompt_dispatched,
|
|
369
|
+
created_by_daemon_id: existing?.created_by_daemon_id,
|
|
347
370
|
close_receipt: result.close?.closed === true ? result : existing?.close_receipt ?? null,
|
|
348
371
|
}); }
|
|
349
372
|
if (result?.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
|
|
@@ -397,10 +420,9 @@ export class DaemonRuntime {
|
|
|
397
420
|
|
|
398
421
|
async requireSettled(params) {
|
|
399
422
|
if (this.sessions.get(params.sessionId)?.close_receipt) throw new DaemonError("session_closed", "Closed Session requires explicit recovery");
|
|
400
|
-
const known = this.sessions.get(params.sessionId);
|
|
401
423
|
// A freshly created ZCode Session has no prior Turn and is not yet exposed
|
|
402
424
|
// by the provider's subagent index. Its first prompt is therefore safe.
|
|
403
|
-
if (
|
|
425
|
+
if (this.neverPromptedRootProof(params.sessionId)) return null;
|
|
404
426
|
const observed = await inspectSessionWithBridge(this.bridge, this.options(params));
|
|
405
427
|
const runningChildren = observed.subagents?.running ?? [];
|
|
406
428
|
if (this.treeRunning(observed)) {
|
|
@@ -410,6 +432,21 @@ export class DaemonRuntime {
|
|
|
410
432
|
return observed;
|
|
411
433
|
}
|
|
412
434
|
|
|
435
|
+
neverPromptedRootProof(sessionId) {
|
|
436
|
+
const known = this.sessions.get(sessionId);
|
|
437
|
+
return known?.prompt_dispatched === false && known.created_by_daemon_id === this.state.daemon_id
|
|
438
|
+
&& known.parent_provider_session_id === null && known.root_provider_session_id === sessionId
|
|
439
|
+
&& Array.isArray(known.known_child_session_ids) && known.known_child_session_ids.length === 0
|
|
440
|
+
? { source: "owned_daemon_create_without_prompt", daemon_id: this.state.daemon_id, provider_session_id: sessionId }
|
|
441
|
+
: null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async markPromptDispatched(sessionId) {
|
|
445
|
+
const known = this.sessions.get(sessionId);
|
|
446
|
+
if (known) known.prompt_dispatched = true;
|
|
447
|
+
await this.persist();
|
|
448
|
+
}
|
|
449
|
+
|
|
413
450
|
async dispatch(operation, params) {
|
|
414
451
|
if (operation === "daemon.status") return {
|
|
415
452
|
daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket,
|
|
@@ -447,11 +484,11 @@ export class DaemonRuntime {
|
|
|
447
484
|
dispatched_at: new Date().toISOString(),
|
|
448
485
|
request_timeout_ms: this.state.config.requestTimeoutMs,
|
|
449
486
|
};
|
|
450
|
-
const result = await this.productive(operation, async () => { await this.requireSettled(params); return await promptSessionWithBridge(this.bridge, { ...this.options(params), timeoutMs: request.request_timeout_ms }); }, request);
|
|
487
|
+
const result = await this.productive(operation, async () => { await this.requireSettled(params); await this.markPromptDispatched(params.sessionId); return await promptSessionWithBridge(this.bridge, { ...this.options(params), timeoutMs: request.request_timeout_ms }); }, request);
|
|
451
488
|
const session = this.sessions.get(result.provider_session_id); if (session) session.completed_turns = (session.completed_turns ?? 0) + 1;
|
|
452
489
|
await this.persist(); return result;
|
|
453
490
|
}
|
|
454
|
-
if (operation === "session.fork") return await this.productive(operation, async () => { await this.requireSettled(params); return await forkSessionWithBridge(this.bridge, this.options(params)); });
|
|
491
|
+
if (operation === "session.fork") return await this.productive(operation, async () => { await this.requireSettled(params); await this.markPromptDispatched(params.sessionId); return await forkSessionWithBridge(this.bridge, this.options(params)); });
|
|
455
492
|
if (operation === "session.inspect" || operation === "session.resume") {
|
|
456
493
|
const closed = this.sessions.get(params.sessionId)?.close_receipt;
|
|
457
494
|
if (closed) {
|
|
@@ -467,7 +504,8 @@ export class DaemonRuntime {
|
|
|
467
504
|
&& Array.isArray(receipt.topology.child_session_ids)) return receipt;
|
|
468
505
|
}
|
|
469
506
|
const known = [...new Set([...topology.child_session_ids, ...(previous?.topology?.child_session_ids ?? [])])];
|
|
470
|
-
const
|
|
507
|
+
const emptyRoot = this.neverPromptedRootProof(params.sessionId);
|
|
508
|
+
const observation = { ...closed, ...await observeClosedZcodeTree(this.bridge, params.sessionId, known, emptyRoot && closed.close?.closed === true ? {...emptyRoot,close_confirmed:true} : null), observed_at: new Date().toISOString() };
|
|
471
509
|
// Observations are not ownership inventory: their timestamps must not
|
|
472
510
|
// invalidate a drain fence on every read.
|
|
473
511
|
this.state.settlement_observations ??= {};
|
|
@@ -525,6 +563,20 @@ export class DaemonRuntime {
|
|
|
525
563
|
}
|
|
526
564
|
const options = this.options({ sessionId: session.provider_session_id, adapterSessionId: session.adapter_session_id });
|
|
527
565
|
let observed;
|
|
566
|
+
// A daemon-owned allocation with no productive dispatch cannot have
|
|
567
|
+
// children, and native ZCode has no durable read index for it yet. Close
|
|
568
|
+
// that resident directly; do not send cancellation for default stop.
|
|
569
|
+
const emptyRoot = !cancel && !this.activeProductive ? this.neverPromptedRootProof(session.provider_session_id) : null;
|
|
570
|
+
if (emptyRoot) {
|
|
571
|
+
const close = await this.bridge.request("zcode/session/close", { sessionId: session.adapter_session_id });
|
|
572
|
+
const observation = close?.closed === true
|
|
573
|
+
? await observeClosedZcodeTree(this.bridge, session.provider_session_id, [], { ...emptyRoot, close_confirmed: true })
|
|
574
|
+
: { settled: false, topology: { root_session_id: session.provider_session_id, child_session_ids: [], complete: false } };
|
|
575
|
+
this.track({ provider_session_id: session.provider_session_id, adapter_session_id: session.adapter_session_id, cancellation_requested: false, close_requested: true, close, ...observation });
|
|
576
|
+
await this.persist({ active_tree: observation.settled !== true });
|
|
577
|
+
if (observation.settled !== true) unsettled.push(session.provider_session_id);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
528
580
|
if (!cancel) observed = await inspectSessionWithBridge(this.bridge, options);
|
|
529
581
|
if (cancel || !observed || this.treeRunning(observed)) {
|
|
530
582
|
if (!cancel) unsettled.push(session.provider_session_id);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
2
2
|
import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
|
|
3
3
|
import { invokeNativeHook } from "./native-hook-command.mjs";
|
|
4
|
+
import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
7
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -19,7 +20,7 @@ const LATE_RESPONSE_GRACE_MS = 5_000;
|
|
|
19
20
|
|
|
20
21
|
function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
|
|
21
22
|
function fatalLifecycleError(error) {
|
|
22
|
-
return ["invocation_unknown", "invocation_identity_mismatch", "invocation_identity_conflict", "invocation_receipt_missing", "invocation_command_mismatch", "invocation_scope_mismatch", "invocation_generation_stale", "invocation_expired", "zcode_parent_identity_missing", "zcode_tool_identity_conflict"].includes(error?.code);
|
|
23
|
+
return ["invocation_unknown", "invocation_identity_mismatch", "invocation_identity_conflict", "invocation_receipt_missing", "invocation_command_mismatch", "invocation_scope_mismatch", "invocation_generation_stale", "invocation_expired", "zcode_parent_identity_missing", "zcode_tool_identity_conflict", "native_hook_timeout", "native_hook_failed", "native_hook_response_invalid", "hook_storage_unprepared", "storage_write_failed"].includes(error?.code);
|
|
23
24
|
}
|
|
24
25
|
function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
|
|
25
26
|
function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
|
|
@@ -233,13 +234,14 @@ export class AcpBridge {
|
|
|
233
234
|
// A validated no-effect rejection is returned through the invocation
|
|
234
235
|
// ledger. It is evidence, not a poisoned ACP Session.
|
|
235
236
|
const fatal = fatalLifecycleError(error);
|
|
236
|
-
|
|
237
|
+
const current = this.options.onLifecycleNotification === observer;
|
|
238
|
+
if (current && (!this.notificationError || fatal && !fatalLifecycleError(this.notificationError) && this.notificationError.code !== "profile_integrity_violation")) this.notificationError = error;
|
|
237
239
|
// A missing/foreign authority is not a recoverable CLI validation
|
|
238
240
|
// result. Stop admitting productive requests immediately; the managed
|
|
239
241
|
// controller still owns physical stop/drain and evidence capture.
|
|
240
|
-
if (fatal) this.rejectAll(error);
|
|
242
|
+
if (fatal && current) this.rejectAll(this.notificationError ?? error);
|
|
241
243
|
try {
|
|
242
|
-
await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message,
|
|
244
|
+
await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message, ...(error.details ? { details: error.details } : {}),
|
|
243
245
|
session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" });
|
|
244
246
|
} catch (persistenceError) {
|
|
245
247
|
process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
|
|
@@ -281,7 +283,7 @@ export class AcpBridge {
|
|
|
281
283
|
if (message.method) this.notifications = this.notifications
|
|
282
284
|
.then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message); })
|
|
283
285
|
.catch(async (error) => {
|
|
284
|
-
const failure = { code: error.code ?? "notification_observation_failed", message: error.message, session_id: message.params?.sessionId ?? null, tool_call_id: message.params?.update?.toolCallId ?? null, event_time: now(), delivery_stage: "acp_evidence", completeness: "incomplete" };
|
|
286
|
+
const failure = { code: error.code ?? "notification_observation_failed", message: error.message, ...(error.details ? { details: error.details } : {}), session_id: message.params?.sessionId ?? null, tool_call_id: message.params?.update?.toolCallId ?? null, event_time: now(), delivery_stage: "acp_evidence", completeness: "incomplete" };
|
|
285
287
|
try { await this.journal.write("notification_failure", failure); }
|
|
286
288
|
catch (persistenceError) { process.stderr.write(`${JSON.stringify({ ...failure, persistence_error: persistenceError.message })}\n`); }
|
|
287
289
|
// Evidence loss does not retroactively revoke a committed native hook.
|
|
@@ -551,7 +553,7 @@ export function zcodeChildSessionIds(snapshot) {
|
|
|
551
553
|
// Read residency BEFORE each durable topology snapshot. An inactive node cannot
|
|
552
554
|
// add descendants; a resident node never proves settlement, even when idle.
|
|
553
555
|
// No resolve/read/subagents extension here: those may implicitly resume.
|
|
554
|
-
export async function observeClosedZcodeTree(bridge, root, retainedChildren = []) {
|
|
556
|
+
export async function observeClosedZcodeTree(bridge, root, retainedChildren = [], neverPromptedRootProof = null) {
|
|
555
557
|
const pending = new Set([root, ...retainedChildren]), residents = [], snapshots = [];
|
|
556
558
|
const deadline = performance.now() + 30_000;
|
|
557
559
|
try {
|
|
@@ -561,6 +563,15 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
|
|
|
561
563
|
const resident = await bridge.request("zcode/session/resident", { sessionId }, timeout());
|
|
562
564
|
if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
|
|
563
565
|
residents.push(resident);
|
|
566
|
+
// A newly allocated root is not durable in ZCode until its first prompt.
|
|
567
|
+
// This is an explicit owned-daemon ledger fact, never error→empty inference.
|
|
568
|
+
if (sessionId === root && resident.resident === false && pending.size === 1
|
|
569
|
+
&& neverPromptedRootProof?.source === "owned_daemon_create_without_prompt"
|
|
570
|
+
&& neverPromptedRootProof.close_confirmed === true
|
|
571
|
+
&& neverPromptedRootProof.provider_session_id === root && typeof neverPromptedRootProof.daemon_id === "string") {
|
|
572
|
+
snapshots.push({ sessionId, childSessionIds: [], running: [], ended: {total:0,items:[]}, provenance: neverPromptedRootProof });
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
564
575
|
const tree = await readZcodeSubagents(bridge, sessionId, deadline - performance.now(), true);
|
|
565
576
|
if (!Array.isArray(tree?.childSessionIds)
|
|
566
577
|
|| tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
|
|
@@ -577,7 +588,7 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
|
|
|
577
588
|
}
|
|
578
589
|
}
|
|
579
590
|
|
|
580
|
-
async function cancelTree(bridge, sessionId, before, providerSessionId, retainedChildren = []) {
|
|
591
|
+
async function cancelTree(bridge, sessionId, before, providerSessionId, retainedChildren = [], neverPromptedRootProof = null) {
|
|
581
592
|
const knownChildren = new Set(retainedChildren);
|
|
582
593
|
const remember = snapshot => {
|
|
583
594
|
for (const id of zcodeChildSessionIds(snapshot)) knownChildren.add(id);
|
|
@@ -616,7 +627,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
|
|
|
616
627
|
let close = null;
|
|
617
628
|
try { close = await bridge.request("zcode/session/close", { sessionId }); }
|
|
618
629
|
catch (error) { close = { closed: false, error: { code: error.code ?? "close_failed", message: error.message } }; }
|
|
619
|
-
const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren])
|
|
630
|
+
const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren], neverPromptedRootProof ? {...neverPromptedRootProof,close_confirmed:true} : null)
|
|
620
631
|
: { topology: { root_session_id: providerSessionId, child_session_ids: [...knownChildren], complete: false }, settled: false };
|
|
621
632
|
return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, ...observation };
|
|
622
633
|
}
|
|
@@ -713,9 +724,8 @@ function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
|
713
724
|
const childId = notification.params?.update?._meta?.zcodeRuntime?.childSessionId;
|
|
714
725
|
const observed = bridge?.modelProfiles?.get(childId ?? rootProviderSessionId) ?? {};
|
|
715
726
|
if (childId && !observed.model) await observeModel({ journal: options.journal, harness: "zcode-acp", sessionId: childId, parentSessionId: rootProviderSessionId, requested: profile(options), observed, source: "zcode.child_tool_event", evidence: "unavailable" });
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
// lag behind execution and must never mint a second lifecycle receipt.
|
|
727
|
+
// This serialized path is model evidence only. The independent observer
|
|
728
|
+
// below owns qualified ACP admission; do not mint a second receipt here.
|
|
719
729
|
};
|
|
720
730
|
}
|
|
721
731
|
|
|
@@ -753,6 +763,8 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
|
|
|
753
763
|
const command = update.rawInput?.command ?? update.rawInput?.cmd;
|
|
754
764
|
const toolName = update._meta?.claudeCode?.toolName;
|
|
755
765
|
if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string") return;
|
|
766
|
+
const analysis = parseLifecycleCommand(command);
|
|
767
|
+
if (analysis.kind === "none" && !analysis.unresolved && !continuationCommand(command)) return;
|
|
756
768
|
const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
|
|
757
769
|
if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
|
|
758
770
|
await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
|
|
@@ -808,6 +820,7 @@ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContrac
|
|
|
808
820
|
["91a1470650f5fd6105f9863fd31d1cad7b90a109", "dd-zcode-harness@2"],
|
|
809
821
|
["b9e0f5106dddb439b76186278bd9be5f45dbf3ef", "dd-zcode-harness@2"],
|
|
810
822
|
["636b14181d5c9a95476055079836fd6b58169f58", "dd-zcode-harness@2"],
|
|
823
|
+
["4621fabd3f517ecdd349dfebebae9077c01d2f2b", "dd-zcode-harness@2"],
|
|
811
824
|
]);
|
|
812
825
|
const qualified = qualifiedBridgeCommits.get(bridgeCommit) === harnessContract;
|
|
813
826
|
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
@@ -965,7 +978,7 @@ export async function cancelSessionWithBridge(bridge, options) {
|
|
|
965
978
|
requireJournal(options);
|
|
966
979
|
const identity = await controlledIdentity(bridge, options);
|
|
967
980
|
const before = await readZcodeSubagents(bridge, identity.adapterSessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
|
|
968
|
-
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...await cancelTree(bridge, identity.adapterSessionId, before, identity.providerSessionId, options.knownChildSessionIds) });
|
|
981
|
+
return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...await cancelTree(bridge, identity.adapterSessionId, before, identity.providerSessionId, options.knownChildSessionIds, options.neverPromptedRootProof) });
|
|
969
982
|
}
|
|
970
983
|
|
|
971
984
|
export async function forkSession(options) {
|
|
@@ -56,7 +56,7 @@ export const lifecycleRetryInstruction = `${lifecycleCommandCompletionInstructio
|
|
|
56
56
|
export const stageLifecycleInstruction = [
|
|
57
57
|
"The project root identifies this RUN; use the Stage packet's declared execution workspace and absolute artifact paths. Do not change a native Session's working directory to a workspace provisioned for a later Stage.",
|
|
58
58
|
"In a controller-managed fanout, the coordinator ends the entry Turn after the Work graph is returned; the controller dispatches children. A completed child or Work is not Stage success: settle the declared graph and Stage gate before finishing.",
|
|
59
|
-
"Execute each runtime-issued lifecycle operation
|
|
59
|
+
"Execute each runtime-issued lifecycle operation and its arguments exactly. One literal lifecycle call may be composed with ordinary shell commands; preserve conditions, stdin and redirects, and never replay the surrounding shell chain after failure. Invocation IDs are internal authority; never add, copy or invent one.",
|
|
60
60
|
lifecycleRetryInstruction
|
|
61
61
|
].join(" ");
|
|
62
62
|
|
|
@@ -68,13 +68,13 @@ export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
|
|
|
68
68
|
return [
|
|
69
69
|
`Complete one already-declared Work: ${workId}.`,
|
|
70
70
|
context,
|
|
71
|
-
"Your first technical action must be this exact
|
|
71
|
+
"Your first technical action must be this exact lifecycle command:",
|
|
72
72
|
startCommand,
|
|
73
73
|
"Execute the issued operation and target exactly. Lifecycle invocation IDs are internal runtime authority and are intentionally omitted; never add or invent them. Preserve any explicit native receipt handle that the command does contain.",
|
|
74
74
|
"Use the native shell's inherited working directory. Do not set or override a workdir/cwd field for this lifecycle command.",
|
|
75
75
|
"Continue only if it succeeds and returns the authoritative Work packet.",
|
|
76
76
|
lifecycleRetryInstruction,
|
|
77
|
-
"After a successful start, complete the assigned Work, write its required result, invoke its exact
|
|
77
|
+
"After a successful start, complete the assigned Work, write its required result, invoke its exact work finish command, then stop.",
|
|
78
78
|
"Do not start another Work, create a child, change dependencies, ask the user, or treat a quiet sibling as failed. You cannot ask the user or pause the parent Stage. Missing facts go through this Work's declared result/failure contract; only the coordinator handles Stage HITL."
|
|
79
79
|
].join("\n");
|
|
80
80
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LifecycleOperation } from "./lifecycle-contract.mjs";
|
|
2
|
+
export type { LifecycleOperation } from "./lifecycle-contract.mjs";
|
|
3
|
+
export interface ParsedCommandArgs { positional: string[]; options: Map<string, string[]> }
|
|
4
|
+
export interface LifecycleInvocation {
|
|
5
|
+
operation: LifecycleOperation; executable: string; launcher?: string; argv: string[]; env: Record<string, string>; args: ParsedCommandArgs; command: string;
|
|
6
|
+
rewriteSafe: boolean; wrapped: boolean; heredoc: boolean; sourceRange?: { start: number; end: number }; contexts?: string[]; expectedCwd?: string;
|
|
7
|
+
}
|
|
8
|
+
export type LifecycleAnalysis =
|
|
9
|
+
| { kind: "none"; unresolved?: true; reason?: string }
|
|
10
|
+
| { kind: "standalone"; invocation: LifecycleInvocation; supported?: boolean; invocations?: LifecycleInvocation[] }
|
|
11
|
+
| { kind: "compound"; invocation: LifecycleInvocation; reason: string; supported?: boolean; invocations?: LifecycleInvocation[] };
|
|
12
|
+
export function lifecycleCommandFromPayload(payload: { command?: unknown; tool_input?: unknown }): string | undefined;
|
|
13
|
+
export function parseLifecycleCommand(command: string): LifecycleAnalysis;
|
|
14
|
+
export function continuationCommand(command: string): { operation: string; args: ParsedCommandArgs } | null;
|
|
15
|
+
export function unwrapShellCommand(command: string): string;
|
|
16
|
+
export function parseCommandArgs(argv: string[]): ParsedCommandArgs;
|
|
17
|
+
export function commandOption(invocation: LifecycleInvocation, name: string): string | undefined;
|
|
18
|
+
export function commandHasOption(invocation: LifecycleInvocation, name: string): boolean;
|
|
19
|
+
export function commandPosition(invocation: LifecycleInvocation, index: number): string | undefined;
|
|
20
|
+
export function shellSuffixOffset(command: string): number;
|