@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127
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 +26 -0
- package/README.md +33 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +43 -6
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -7,11 +7,21 @@ import { reportedError } from "./operation-errors.mjs";
|
|
|
7
7
|
import { ObservationClock } from "./observation-clock.mjs";
|
|
8
8
|
import { runtimeProcess } from "./managed-daemon.mjs";
|
|
9
9
|
|
|
10
|
+
function cleanShutdownConfirmed(state) {
|
|
11
|
+
const proof = state.shutdown;
|
|
12
|
+
return state.shutdown_state === "clean" && state.active_tree === false
|
|
13
|
+
&& proof?.schema_id === "dd-flow/daemon-shutdown@1" && proof.daemon_id === state.daemon_id
|
|
14
|
+
&& proof.result?.clean === true && Array.isArray(proof.required_phases)
|
|
15
|
+
&& proof.required_phases.length >= 3 && proof.required_phases.includes("tree")
|
|
16
|
+
&& proof.required_phases.includes("daemon_resource")
|
|
17
|
+
&& proof.required_phases.every(phase => typeof phase === "string" && proof.phases?.[phase] === true);
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/** Explicit same-Session restart retains native stores and the old daemon
|
|
11
21
|
* receipt; it does not turn a terminal directory into a new execution. */
|
|
12
22
|
export async function authorizeRetainedDaemonResume(root, previous, sessionId, config) {
|
|
13
23
|
const sessions = [...(previous.sessions ?? []), ...(previous.root_session_id ? [previous.root_session_id] : [])];
|
|
14
|
-
if (previous
|
|
24
|
+
if (!cleanShutdownConfirmed(previous) || typeof sessionId !== "string" || !sessions.some(session => (typeof session === "string" ? session : session.provider_session_id) === sessionId)) throw Object.assign(new Error("terminal daemon restart requires a retained, settled Session identity and complete shutdown proof"), { code: "daemon_state_terminal" });
|
|
15
25
|
for (const key of ["cwd", "projectRoot", "ddFlowHome", "ddFlowBin", "provider", "model", "reasoning", "mode", "variant", "agent", "bin", "noFlow"]) {
|
|
16
26
|
if ((previous.config?.[key] ?? null) !== (config[key] ?? null)) throw Object.assign(new Error(`retained daemon configuration changed: ${key}`), { code: "daemon_config_mismatch" });
|
|
17
27
|
}
|
|
@@ -43,7 +53,7 @@ export async function recoverySettlement(driversRoot) {
|
|
|
43
53
|
}
|
|
44
54
|
else if (entry.name === "daemon.json") {
|
|
45
55
|
const state = JSON.parse(await readFile(file, "utf8"));
|
|
46
|
-
if (state
|
|
56
|
+
if (!cleanShutdownConfirmed(state)) throw Object.assign(new Error("provider tree has no complete clean shutdown receipt"), { code: "recovery_settlement_unconfirmed" });
|
|
47
57
|
await assertDaemonReplaceable(directory);
|
|
48
58
|
await reconcileDriverReplies(directory);
|
|
49
59
|
if (state.config?.resourceHome && state.resource_process?.id) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const RENEWAL_POLICY: Readonly<{ budgetMs: number; attemptMs: number }>;
|
|
2
|
+
export type RenewalState = { uncertainSince: number | null; attempt: number; exhausted: boolean };
|
|
3
|
+
export function createRenewalState(): RenewalState;
|
|
4
|
+
export function renewalRemaining(state: RenewalState, now?: number): number;
|
|
5
|
+
export function renewalFailure(state: RenewalState, now?: number): number;
|
|
6
|
+
export function renewalDelay(state: RenewalState): number;
|
|
7
|
+
export function renewalConfirmed(state: RenewalState, now?: number): boolean;
|
|
8
|
+
export function maintenanceRetryable(error: unknown): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Shared policy only; transports and owner-specific fences stay at callsites. */
|
|
2
|
+
export const RENEWAL_POLICY = Object.freeze({ budgetMs: 30_000, attemptMs: 5_000 });
|
|
3
|
+
export function createRenewalState() { return { uncertainSince: null, attempt: 0, exhausted: false }; }
|
|
4
|
+
export function renewalRemaining(state, now = performance.now()) {
|
|
5
|
+
const remaining = state.exhausted ? 0 : state.uncertainSince === null ? RENEWAL_POLICY.budgetMs : Math.max(0, RENEWAL_POLICY.budgetMs - (now - state.uncertainSince));
|
|
6
|
+
if (remaining === 0) state.exhausted = true;
|
|
7
|
+
return remaining;
|
|
8
|
+
}
|
|
9
|
+
export function renewalFailure(state, now = performance.now()) { state.uncertainSince ??= now; state.attempt++; return renewalRemaining(state, now); }
|
|
10
|
+
export function renewalDelay(state) { return Math.min(2_000, 250 * 2 ** Math.min(3, Math.max(0, state.attempt - 1))); }
|
|
11
|
+
export function renewalConfirmed(state, now = performance.now()) { if (!renewalRemaining(state, now)) return false; state.uncertainSince = null; state.attempt = 0; return true; }
|
|
12
|
+
export function maintenanceRetryable(error) {
|
|
13
|
+
let retryable = false;
|
|
14
|
+
// Inspect only bounded typed causes from our maintenance transport, never text.
|
|
15
|
+
for (let depth = 0; error && depth < 8; depth++, error = error.cause ?? error.details?.cause) {
|
|
16
|
+
if (error.details?.cleanup_unconfirmed === true) return false;
|
|
17
|
+
if (["process_lease_lost", "controller_lease_lost", "process_maintenance_receipt_invalid", "runtime_owner_mismatch"].includes(error.code)) return false;
|
|
18
|
+
if (error.code === "process_ownership_unknown" && error.details?.reason === "physical_identity_unavailable" && ["process_confirmation", "observer_registration"].includes(error.details.phase) && error.details.effect === "no_effect" && error.details.native_dispatch_started === false) retryable = true;
|
|
19
|
+
if (["process_heartbeat_timeout", "process_maintenance_timeout", "hook_deadline_exceeded", "native_hook_timeout", "SQLITE_BUSY", "SQLITE_LOCKED"].includes(error.code)) retryable = true;
|
|
20
|
+
if (error.code === "ERR_SQLITE_ERROR" && [5, 6].includes((error.details?.sqlite_extended_code ?? error.sqlite_extended_code ?? error.errcode) & 255)) retryable = true;
|
|
21
|
+
}
|
|
22
|
+
return retryable;
|
|
23
|
+
}
|
|
@@ -5,13 +5,13 @@ export interface LifecycleInvocation {
|
|
|
5
5
|
operation: LifecycleOperation; executable: string; launcher?: string; argv: string[]; env: Record<string, string>; args: ParsedCommandArgs; command: string;
|
|
6
6
|
rewriteSafe: boolean; wrapped: boolean; heredoc: boolean; sourceRange?: { start: number; end: number }; contexts?: string[]; expectedCwd?: string;
|
|
7
7
|
}
|
|
8
|
-
export type LifecycleAnalysis =
|
|
8
|
+
export type LifecycleAnalysis = ({ participation: "none" | "lifecycle" | "auxiliary" | "unproven"; auxiliary?: { operation: string; args: ParsedCommandArgs; expectedCwd?: string } } & (
|
|
9
9
|
| { kind: "none"; unresolved?: true; reason?: string }
|
|
10
10
|
| { kind: "standalone"; invocation: LifecycleInvocation; supported?: boolean; invocations?: LifecycleInvocation[] }
|
|
11
|
-
| { kind: "compound"; invocation: LifecycleInvocation; reason: string; supported?: boolean; invocations?: LifecycleInvocation[] };
|
|
11
|
+
| { kind: "compound"; invocation: LifecycleInvocation; reason: string; supported?: boolean; invocations?: LifecycleInvocation[] }));
|
|
12
12
|
export function lifecycleCommandFromPayload(payload: { command?: unknown; tool_input?: unknown }): string | undefined;
|
|
13
13
|
export function parseLifecycleCommand(command: string): LifecycleAnalysis;
|
|
14
|
-
export function continuationCommand(command: string): { operation: string; args: ParsedCommandArgs } | null;
|
|
14
|
+
export function continuationCommand(command: string): { operation: string; args: ParsedCommandArgs; expectedCwd?: string } | null;
|
|
15
15
|
export function unwrapShellCommand(command: string): string;
|
|
16
16
|
export function parseCommandArgs(argv: string[]): ParsedCommandArgs;
|
|
17
17
|
export function commandOption(invocation: LifecycleInvocation, name: string): string | undefined;
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
import Bash from "tree-sitter-bash";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
4
3
|
import { parse, quote } from "shell-quote";
|
|
5
4
|
import { lifecycleOperationForArgv } from "./lifecycle-contract.mjs";
|
|
6
5
|
|
|
7
6
|
// One grammar owns structure. shell-quote decodes identified command words;
|
|
8
7
|
// neither parser grants authority or executes expansions.
|
|
9
|
-
const
|
|
10
|
-
parser
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
let parser;
|
|
10
|
+
function shellParser() {
|
|
11
|
+
if (!parser) {
|
|
12
|
+
const Parser = require("tree-sitter"), Bash = require("tree-sitter-bash");
|
|
13
|
+
const initialized = new Parser();
|
|
14
|
+
initialized.setLanguage(Bash);
|
|
15
|
+
parser = initialized;
|
|
16
|
+
}
|
|
17
|
+
return parser;
|
|
18
|
+
}
|
|
11
19
|
const shells = new Set(["bash", "sh", "zsh"]);
|
|
12
20
|
const repeatedContexts = new Set(["for_statement", "while_statement", "function_definition", "c_style_for_statement"]);
|
|
13
21
|
const dynamicTypes = ["command_substitution", "process_substitution", "arithmetic_expansion", "brace_expression", "extglob_pattern"];
|
|
@@ -97,15 +105,48 @@ function invocationFromWords(values, node, wrapped, env) {
|
|
|
97
105
|
return { operation, executable, ...(launcher ? { launcher } : {}), argv, env, args: parseCommandArgs(argv.slice(operation === "recovery_accept" ? 3 : 2)),
|
|
98
106
|
command: quote(values), rewriteSafe: !wrapped, wrapped, heredoc, contexts, ...(!wrapped ? { sourceRange: { start: node.startIndex, end: node.endIndex } } : {}) };
|
|
99
107
|
}
|
|
100
|
-
export function parseLifecycleCommand(command) {
|
|
108
|
+
export function parseLifecycleCommand(command) {
|
|
109
|
+
const result = analyze(command, false, 0);
|
|
110
|
+
return { ...result, participation: result.kind !== "none" ? "lifecycle" : result.participation ?? (result.auxiliary ? "auxiliary" : result.unresolved ? "unproven" : "none") };
|
|
111
|
+
}
|
|
112
|
+
function auxiliaryFromWords(values) {
|
|
113
|
+
const env = {}, index = launcherIndex(values, env);
|
|
114
|
+
let start = index;
|
|
115
|
+
if (path.basename(values[start] ?? "") === "node" && (path.basename(values[start + 1] ?? "") === "cli.js" || values[start + 1] === "$DD_FLOW_BIN")) start++;
|
|
116
|
+
if (!(path.basename(values[start] ?? "") === "dd-flow" || values[start] === "$DD_FLOW_BIN" || start !== index && path.basename(values[start] ?? "") === "cli.js")) return null;
|
|
117
|
+
const args = parseCommandArgs(values.slice(start + 1)), route = args.positional.slice(0, 3).join(" ");
|
|
118
|
+
const operation = route === "work repair add" ? "work_repair_add" : args.positional[0] === "stage" && args.positional[1] === "block" ? "stage_block" : null;
|
|
119
|
+
return operation ? { operation, args } : null;
|
|
120
|
+
}
|
|
121
|
+
// Recognition is not admission: recovered commands never provide argv authority.
|
|
122
|
+
function recognizedParticipation(root, depth) {
|
|
123
|
+
for (const node of root.descendantsOfType("command")) {
|
|
124
|
+
const name = node.childForFieldName("name");
|
|
125
|
+
if (!name || name.type === "ERROR" || dynamicWord(name)) continue;
|
|
126
|
+
const values = words(node.text);
|
|
127
|
+
if (!values) continue;
|
|
128
|
+
if (invocationFromWords(values, node, true, {})) return "lifecycle";
|
|
129
|
+
if (auxiliaryFromWords(values)) return "auxiliary";
|
|
130
|
+
const index = launcherIndex(values, {});
|
|
131
|
+
if (values[index] === "eval" && depth < 8) {
|
|
132
|
+
const payload = node.namedChildren[index + 1];
|
|
133
|
+
if (payload && !dynamicWord(payload)) {
|
|
134
|
+
const nested = analyze(values.slice(index + 1).join(" "), true, depth + 1);
|
|
135
|
+
if (nested.kind !== "none" || nested.participation === "lifecycle") return "lifecycle";
|
|
136
|
+
if (nested.auxiliary || nested.participation === "auxiliary") return "auxiliary";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return "unproven";
|
|
141
|
+
}
|
|
101
142
|
function analyze(command, wrapped, depth) {
|
|
102
143
|
if (depth > 8 || command.length > 1_000_000) return { kind: "none", unresolved: true, reason: "shell_input_limit" };
|
|
103
|
-
const tree =
|
|
144
|
+
const tree = shellParser().parse(command);
|
|
104
145
|
if (!tree) return { kind: "none", unresolved: true, reason: "shell_parse_error" };
|
|
105
146
|
try {
|
|
106
147
|
const root = tree.rootNode;
|
|
107
|
-
if (root.hasError) return { kind: "none", unresolved: true, reason: "shell_parse_error" };
|
|
108
|
-
const nodes = root.descendantsOfType("command"), found = [];
|
|
148
|
+
if (root.hasError) return { kind: "none", unresolved: true, participation: recognizedParticipation(root, depth), reason: "shell_parse_error" };
|
|
149
|
+
const nodes = root.descendantsOfType("command"), found = [], auxiliaries = [];
|
|
109
150
|
let unsupported;
|
|
110
151
|
for (const node of nodes) {
|
|
111
152
|
const name = node.childForFieldName("name"), values = words(node.text);
|
|
@@ -122,6 +163,13 @@ function analyze(command, wrapped, depth) {
|
|
|
122
163
|
}
|
|
123
164
|
}
|
|
124
165
|
const direct = invocationFromWords(values, node, wrapped, inherited);
|
|
166
|
+
const auxiliary = auxiliaryFromWords(values);
|
|
167
|
+
if (auxiliary) {
|
|
168
|
+
const route = executionCwd(node, nodes);
|
|
169
|
+
if (dynamicWord(node) || backgroundContext(node) || ancestors(node).some(type => repeatedContexts.has(type) || ["command_substitution", "process_substitution"].includes(type)) || route.unresolved) unsupported ??= "unsupported_auxiliary_execution";
|
|
170
|
+
auxiliaries.push({ ...auxiliary, ...(route.cwd ? { expectedCwd: route.cwd } : {}) });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
125
173
|
if (direct) {
|
|
126
174
|
if (backgroundContext(node)) unsupported ??= "background_lifecycle_execution";
|
|
127
175
|
if (node.namedChildren.some(dynamicWord)) unsupported ??= "unresolved_lifecycle_word";
|
|
@@ -140,6 +188,7 @@ function analyze(command, wrapped, depth) {
|
|
|
140
188
|
if (values[nestedIndex] === "--") nestedIndex++;
|
|
141
189
|
if (wrapper === "timeout" && /^\d+(?:\.\d+)?[smhd]?$/.test(values[nestedIndex] ?? "")) nestedIndex++;
|
|
142
190
|
const nested = analyze(quote(values.slice(nestedIndex)), true, depth + 1);
|
|
191
|
+
if (nested.auxiliary || ["lifecycle", "auxiliary"].includes(nested.participation)) return { kind: "none", participation: nested.auxiliary ? "auxiliary" : nested.participation, unresolved: true, reason: "opaque_execution_wrapper" };
|
|
143
192
|
if (nested.kind !== "none") { found.push(...(nested.invocations ?? [nested.invocation])); unsupported ??= "opaque_execution_wrapper"; continue; }
|
|
144
193
|
if (nested.unresolved) unsupported ??= nested.reason;
|
|
145
194
|
}
|
|
@@ -148,6 +197,12 @@ function analyze(command, wrapped, depth) {
|
|
|
148
197
|
if (!payloadNode || dynamicWord(payloadNode)) { unsupported ??= "dynamic_shell_payload"; continue; }
|
|
149
198
|
if (node.namedChildren.some(child => child !== payloadNode && dynamicWord(child))) unsupported ??= "unresolved_shell_environment";
|
|
150
199
|
const nested = analyze(values[shellIndex + 2], true, depth + 1);
|
|
200
|
+
if (nested.auxiliary) {
|
|
201
|
+
const route = executionCwd(node, nodes);
|
|
202
|
+
if (route.unresolved || backgroundContext(node) || ancestors(node).some(type => repeatedContexts.has(type) || ["command_substitution", "process_substitution"].includes(type))) unsupported ??= "unsupported_auxiliary_execution";
|
|
203
|
+
auxiliaries.push({ ...nested.auxiliary, ...(route.cwd && !nested.auxiliary.expectedCwd ? { expectedCwd: route.cwd } : {}) });
|
|
204
|
+
}
|
|
205
|
+
if (nested.participation && ["lifecycle", "auxiliary"].includes(nested.participation) && nested.unresolved) return { kind: "none", unresolved: true, participation: nested.participation, reason: nested.reason };
|
|
151
206
|
if (nested.kind !== "none") {
|
|
152
207
|
const route = executionCwd(node, nodes);
|
|
153
208
|
if (route.unresolved) unsupported ??= "unresolved_execution_cwd";
|
|
@@ -162,10 +217,18 @@ function analyze(command, wrapped, depth) {
|
|
|
162
217
|
if (nested.supported === false) unsupported ??= nested.reason ?? "unsupported_wrapper";
|
|
163
218
|
}
|
|
164
219
|
else if (nested.unresolved) unsupported ??= nested.reason;
|
|
165
|
-
} else if (values[shellIndex] === "eval")
|
|
220
|
+
} else if (values[shellIndex] === "eval") {
|
|
221
|
+
const participation = recognizedParticipation(root, depth);
|
|
222
|
+
if (participation !== "unproven") return { kind: "none", unresolved: true, participation, reason: "dynamic_eval" };
|
|
223
|
+
unsupported ??= "dynamic_eval";
|
|
224
|
+
}
|
|
166
225
|
else if (values[shellIndex] === "coproc") unsupported ??= "background_lifecycle_execution";
|
|
167
226
|
else if (dynamicWord(name)) unsupported ??= "dynamic_executable";
|
|
168
227
|
}
|
|
228
|
+
if (auxiliaries.length) {
|
|
229
|
+
if (found.length || auxiliaries.length !== 1) unsupported ??= "multiple_lifecycle_invocations";
|
|
230
|
+
if (!found.length) return { kind: "none", participation: "auxiliary", ...(unsupported ? { unresolved: true, reason: unsupported } : { auxiliary: auxiliaries[0] }) };
|
|
231
|
+
}
|
|
169
232
|
if (!found.length) return unsupported ? { kind: "none", unresolved: true, reason: unsupported } : { kind: "none" };
|
|
170
233
|
if (found.length > 1) unsupported ??= "multiple_lifecycle_invocations";
|
|
171
234
|
const invocation = found[0];
|
|
@@ -180,20 +243,11 @@ export function lifecycleCommandFromPayload(payload) {
|
|
|
180
243
|
return typeof command === "string" ? unwrapShellCommand(command) : undefined;
|
|
181
244
|
}
|
|
182
245
|
export function continuationCommand(command) {
|
|
183
|
-
|
|
184
|
-
const start = values.findIndex(word => ["stage", "work"].includes(word)); if (start < 1) return null;
|
|
185
|
-
const launcher = values.slice(0, start);
|
|
186
|
-
while (launcher[0] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(launcher[0])) launcher.shift();
|
|
187
|
-
const direct = launcher.length === 1 && (path.basename(launcher[0]) === "dd-flow" || launcher[0] === "$DD_FLOW_BIN");
|
|
188
|
-
const node = launcher.length === 2 && path.basename(launcher[0]) === "node" && (path.basename(launcher[1]) === "cli.js" || launcher[1] === "$DD_FLOW_BIN");
|
|
189
|
-
if (!direct && !node) return null;
|
|
190
|
-
const args = parseCommandArgs(values.slice(start)), route = args.positional.slice(0, 3).join(" ");
|
|
191
|
-
const operation = route === "work repair add" ? "work_repair_add" : args.positional[0] === "stage" && args.positional[1] === "block" ? "stage_block" : null;
|
|
192
|
-
return operation ? { operation, args } : null;
|
|
246
|
+
return parseLifecycleCommand(command).auxiliary ?? null;
|
|
193
247
|
}
|
|
194
248
|
export function unwrapShellCommand(command) {
|
|
195
249
|
const values = words(command); if (!values || values.length !== 3 || !shells.has(path.basename(values[0])) || !/^-(?:l?c|cl)$/.test(values[1])) return command;
|
|
196
|
-
const tree =
|
|
250
|
+
const tree = shellParser().parse(command);
|
|
197
251
|
try { const node = tree?.rootNode.namedChildren[0]?.namedChildren[2]; return node && !dynamicWord(node) ? values[2] : command; }
|
|
198
252
|
finally { /* Native Tree lifetime is managed by the Node binding. */ }
|
|
199
253
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
type RuntimeConfig = Record<string, unknown>;
|
|
3
|
+
type ProcessBinding = { id: string; lease_token: string; kind?: string; lease_expires_at?: string; registration_sha256?: string };
|
|
4
|
+
export function runtimeProcess(config: RuntimeConfig, action: string, options?: Record<string, unknown>, transport?: { timeoutMs?: number; signal?: AbortSignal }): Promise<Record<string, unknown> | null>;
|
|
5
|
+
export function registerDaemonProcess(config: RuntimeConfig, input: { kind: string; [key: string]: unknown }): Promise<ProcessBinding | null>;
|
|
6
|
+
export function confirmDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, child: Pick<ChildProcess, "pid"> | null | undefined): Promise<void>;
|
|
7
|
+
export function assertDaemonOwnership(stateDir: string, dispatch?: Record<string, unknown>, assertCurrent?: () => void): Promise<void>;
|
|
8
|
+
export function heartbeatDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined): Promise<void>;
|
|
9
|
+
export function finishDaemonProcess(config: RuntimeConfig, record: ProcessBinding | null | undefined, state?: string, reason?: string): Promise<void>;
|
|
10
|
+
export function completeBudgetOperation(stateDir: string, request: { id: string; operation: string; params?: { sessionId?: string } }, result: unknown, observe?: () => Promise<Record<string, unknown>>): Promise<Record<string, unknown>>;
|
|
11
|
+
export function stopProcessGroup(child: ChildProcess | null | undefined, graceMs?: number, options?: { detached?: boolean; settlementMs?: number }): Promise<void>;
|