@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.
Files changed (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. 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.shutdown_state !== "clean" || previous.active_tree === true || 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"), { code: "daemon_state_terminal" });
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.shutdown_state !== "clean" || state.active_tree === true) throw Object.assign(new Error("provider tree has no clean shutdown receipt"), { code: "recovery_settlement_unconfirmed" });
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 Parser from "tree-sitter";
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 parser = new Parser();
10
- parser.setLanguage(Bash);
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) { return analyze(command, false, 0); }
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 = parser.parse(command);
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") unsupported ??= "dynamic_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
- const values = words(command); if (!values) return null;
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 = parser.parse(command);
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>;