@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -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/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. package/package.json +7 -4
@@ -0,0 +1,51 @@
1
+ import { isRuntimeMaintenance, runtimeMaintenanceDiagnostics, withMaintenanceDiagnosticScope } from "./runtime-maintenance-diagnostics.js";
2
+ import { errorRecord, isAppError } from "../shared/errors.js";
3
+ import { writeJson } from "../shared/json.js";
4
+ /** Only the already-selected engine's internal JSON transport takes this path.
5
+ * Operator calls and richer output formats retain the ordinary engine router. */
6
+ export function isRuntimeMaintenanceIngress(args, env) {
7
+ return env.DD_FLOW_ENGINE_MODE === "1" && Number.isFinite(Number(env.DD_FLOW_HOOK_DEADLINE_AT))
8
+ && Number(env.DD_FLOW_HOOK_DEADLINE_AT) > 0 && args.includes("--json")
9
+ && isRuntimeMaintenance(args)
10
+ && !args.some(arg => ["--help", "-h", "--progress-jsonl", "--response-file"].includes(arg) || arg.startsWith("--response-file="));
11
+ }
12
+ export async function runRuntimeMaintenanceIngress(args, env = process.env, io = { stdout: process.stdout, stderr: process.stderr }) {
13
+ return withMaintenanceDiagnosticScope(async () => {
14
+ const deadline = Number(env.DD_FLOW_HOOK_DEADLINE_AT);
15
+ // Publish evidence before loading even the maintenance service graph.
16
+ const diagnostics = runtimeMaintenanceDiagnostics(args, deadline, line => io.stderr.write(line));
17
+ let context;
18
+ try {
19
+ const [{ createContext }, { prepareCommandRoute }, { resolveCommandProjectRoot, resolveCommandPaths }, processCommands] = await Promise.all([
20
+ import("../runtime/context.js"), import("./command-inputs.js"), import("../services/command-context.js"), import("./runtime-process.js")
21
+ ]);
22
+ const invocation = args.filter(arg => arg !== "--json");
23
+ const options = { databaseDeadlineAt: deadline, resourceDatabaseMode: args[2] === "register" ? "initialize" : args[2] === "status" ? "read_existing" : "prepared_existing" };
24
+ context = createContext({ ...env, DD_FLOW_INVOCATION_READONLY: args[2] === "status" ? "1" : undefined }, "read_existing", undefined, options);
25
+ const root = resolveCommandProjectRoot(context, invocation, null);
26
+ const routedArgs = resolveCommandPaths(context, invocation, root);
27
+ const route = prepareCommandRoute(routedArgs);
28
+ const prepared = processCommands.prepareRuntimeProcess(context, route.key, route.parsed);
29
+ // Preserve the common compatibility gate; this is not a second dispatch
30
+ // implementation or permission granted by the ingress predicate.
31
+ const { preflightCliCompatibility } = await import("../services/compatibility-preflight.js");
32
+ preflightCliCompatibility(context, routedArgs, env, root);
33
+ const result = await processCommands.dispatchRuntimeProcess(context, args[2], route.parsed, prepared);
34
+ const receipt = result;
35
+ const expiry = receipt?.lease_expires_at ?? receipt?.process?.lease_expires_at;
36
+ diagnostics.phase("ack", { ...(expiry && Number.isFinite(Date.parse(expiry)) ? { lease_expires_at: expiry } : {}) });
37
+ writeJson(io.stdout, result);
38
+ return 0;
39
+ }
40
+ catch (error) {
41
+ diagnostics.failure(error);
42
+ const record = errorRecord(error);
43
+ writeJson(io.stderr, { ok: false, error: { ...record, details: { ...(!isAppError(error) ? { effect: "unknown" } : {}), ...record.details } } });
44
+ return record.exitCode;
45
+ }
46
+ finally {
47
+ context?.db.close?.();
48
+ diagnostics.close();
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,128 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { requiredOption, optionalOption, optionalNumber, optionalPositiveNumber } from "./argument-values.js";
3
+ import { prepareError } from "./input-preparation.js";
4
+ import { recoveryGuard, assertRunMutationAllowed } from "../services/run-recovery-guard.js";
5
+ import { recoveryRuntimeAdmission } from "../services/run-recovery-guard.js";
6
+ import { prepareManagedProcessRegistration, prepareManagedProcessConfirmation, prepareManagedProcessHeartbeat, prepareManagedProcessFinish, prepareManagedProcessStopOperation, prepareManagedProcessReconcile, stopManagedProcess, confirmManagedProcess, finishManagedProcess, renewManagedProcess, assertManagedProcessAdmission, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
7
+ import { admitRuntimeCheck, parseRuntimeBudget, reserveProviderTurn, releaseProviderTurns, prepareProviderTurnRelease } from "../services/runtime-budget.js";
8
+ function runtimeProcessStopInput(parsed) {
9
+ const graceMs = optionalNumber(parsed, "grace-ms");
10
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
11
+ }
12
+ function runtimeProcessRegistrationInput(context, parsed) {
13
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
14
+ const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
15
+ const kind = requiredOption(parsed, "kind");
16
+ const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
17
+ const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
18
+ const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
19
+ return {
20
+ ...(optionalOption(parsed, "id") ? { id: optionalOption(parsed, "id") } : {}), kind, ownerId: requiredOption(parsed, "owner"), uniqueActiveOperation: kind === "eval-observer",
21
+ ...(kind === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
22
+ ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), projectId, runId,
23
+ workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
24
+ operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
25
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs !== undefined ? { leaseMs } : {}),
26
+ metadata: { ...(budget ? { budget, harness: optionalOption(parsed, "harness") ?? null } : {}), ...(projectId && runId ? { run_generation: guard?.generation ?? 0, run_admitted: Boolean(run) && !["draining", "sealed", "resuming"].includes(guard?.status ?? "") } : {}), ...(optionalOption(parsed, "dispatch-barrier") ? { dispatch_barrier: optionalOption(parsed, "dispatch-barrier") } : {}), ...(optionalOption(parsed, "role") ? { role: optionalOption(parsed, "role") } : {}), ...(optionalOption(parsed, "state-dir") ? { state_dir: optionalOption(parsed, "state-dir") } : {}), ...(optionalOption(parsed, "adapter-executable") ? { adapter_executable: optionalOption(parsed, "adapter-executable") } : {}) }
27
+ };
28
+ }
29
+ function runtimeProcessConfirmationInput(parsed) {
30
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true), processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
31
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId !== undefined ? { processGroupId } : {}), ...(leaseMs !== undefined ? { leaseMs } : {}) };
32
+ }
33
+ function runtimeProcessHeartbeatInput(parsed) {
34
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
35
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs !== undefined ? { leaseMs } : {}) };
36
+ }
37
+ function runtimeProcessFinishInput(parsed) {
38
+ const state = requiredOption(parsed, "state");
39
+ if (state !== "stopped" && state !== "failed")
40
+ throw new AppError("validation", "--state must be stopped or failed", 2);
41
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state, reason: optionalOption(parsed, "reason") ?? null };
42
+ }
43
+ export function prepareRuntimeProcess(context, key, parsed) {
44
+ try {
45
+ return prepareRuntimeProcessInput(context, key, parsed);
46
+ }
47
+ catch (error) {
48
+ return prepareError(error);
49
+ }
50
+ }
51
+ function prepareRuntimeProcessInput(context, key, parsed) {
52
+ const prepared = {};
53
+ if (key === "runtime process register") {
54
+ prepareManagedProcessRegistration(context, runtimeProcessRegistrationInput(context, parsed));
55
+ }
56
+ if (key === "runtime process heartbeat")
57
+ prepareManagedProcessHeartbeat(context, runtimeProcessHeartbeatInput(parsed));
58
+ if (key === "runtime process finish")
59
+ prepareManagedProcessFinish(context, runtimeProcessFinishInput(parsed));
60
+ if (key === "runtime process confirm")
61
+ prepareManagedProcessConfirmation(context, runtimeProcessConfirmationInput(parsed));
62
+ if (key === "runtime process stop")
63
+ prepareManagedProcessStopOperation(context, runtimeProcessStopInput(parsed));
64
+ if (key === "runtime process reconcile")
65
+ prepareManagedProcessReconcile(requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms"));
66
+ if (key === "runtime process release-turn") {
67
+ prepared.providerTurnRelease = prepareProviderTurnRelease({ daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
68
+ }
69
+ return prepared;
70
+ }
71
+ export async function dispatchRuntimeProcess(context, action, parsed, prepared = {}) {
72
+ if (action === "check-admission")
73
+ return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
74
+ if (action === "stop")
75
+ return { ok: true, process: await stopManagedProcess(context, runtimeProcessStopInput(parsed)) };
76
+ if (action === "register") {
77
+ const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
78
+ return { ok: true, process };
79
+ }
80
+ if (action === "confirm")
81
+ return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
82
+ if (action === "heartbeat") {
83
+ const receipt = renewManagedProcess(context, runtimeProcessHeartbeatInput(parsed));
84
+ return { ...receipt, ok: receipt.owned };
85
+ }
86
+ if (action === "admission") {
87
+ const [{ controllerHitlAdmission }, { assertCapacityOwnerAdmission }] = await Promise.all([import("../services/run-controller-adapter.js"), import("../services/codex-capacity.js")]);
88
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
89
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || record.state !== "running" || !record.pid || !record.pid_started_at || !processIsAlive(record))
90
+ throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
91
+ assertManagedProcessAdmission(context, record);
92
+ const metadata = JSON.parse(record.metadata_json);
93
+ if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
94
+ throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
95
+ if (record.project_id && record.run_id) {
96
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
97
+ throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
98
+ const recovering = recoveryRuntimeAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), daemonId: optionalOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
99
+ if (!recovering) {
100
+ if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
101
+ throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
102
+ assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
103
+ }
104
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
105
+ const retainedHitlPrompt = recovering && ["paused", "waiting_for_user"].includes(run?.status ?? "") && ["session.prompt", "session.resume"].includes(optionalOption(parsed, "operation") ?? "") && context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND b.daemon_id = ? AND b.provider_session_id = ?", [record.project_id, record.run_id, metadata.run_generation, optionalOption(parsed, "daemon-id"), optionalOption(parsed, "native-session-id")]);
106
+ const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
107
+ if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
108
+ throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
109
+ if (!recovering)
110
+ assertCapacityOwnerAdmission(context, record, optionalOption(parsed, "operation-id"), { nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
111
+ }
112
+ const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
113
+ return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
114
+ }
115
+ if (action === "release-turn") {
116
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
117
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
118
+ throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
119
+ return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") }, prepared.providerTurnRelease);
120
+ }
121
+ if (action === "finish")
122
+ return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
123
+ if (action === "status")
124
+ return { ok: true, processes: managedProcessStatus(context) };
125
+ if (action === "reconcile")
126
+ return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
127
+ throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
128
+ }
package/dist/cli.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { isNativeHookIngress } from "./cli/native-hook-ingress.js";
3
+ import { isRuntimeMaintenanceIngress, runRuntimeMaintenanceIngress } from "./cli/runtime-maintenance-ingress.js";
3
4
  const args = process.argv.slice(2);
4
5
  const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle" && !args.includes("--help") && !args.includes("-h");
5
- const exitCode = managedHook
6
- ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
7
- : isNativeHookIngress(args) && !args.includes("--help") && !args.includes("-h")
8
- ? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
9
- : await (await import("./cli/run-cli.js")).runCli(args);
6
+ const exitCode = isRuntimeMaintenanceIngress(args, process.env)
7
+ ? await runRuntimeMaintenanceIngress(args)
8
+ : managedHook
9
+ ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
10
+ : isNativeHookIngress(args) && !args.includes("--help") && !args.includes("-h")
11
+ ? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
12
+ : await (await import("./cli/run-cli.js")).runCli(args);
10
13
  process.exitCode = exitCode;
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { randomUUID } from "node:crypto";
@@ -38,7 +39,7 @@ async function hook(options) {
38
39
  }
39
40
 
40
41
  try {
41
- const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, bin: options["agy-bin"], provider: options.provider, model: options.model, reasoning: options.reasoning, mode: options.mode, projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; let result;
42
+ const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, bin: options["agy-bin"], provider: options.provider, model: options.model, reasoning: options.reasoning, mode: options.mode, projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, family === "session" ? command : null) }; let result;
42
43
  if (family === "doctor") result = await doctor(common);
43
44
  else if (family === "hooks" && ["install", "check"].includes(command)) result = await qualifyAgyHooks(common.projectRoot, { stateDir: common.stateDir, install: command === "install" });
44
45
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, sessionId: options["session-id"], entryPath: process.argv[1] }, startDaemon);
@@ -49,7 +50,7 @@ try {
49
50
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
50
51
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
51
52
  else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
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); }
53
+ 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"], livenessTimeoutMs: common.livenessTimeoutMs, ...(command === "create" || command === "prompt" ? { prompt: await prompt(options) } : {}), tree: options.tree === true }, common.timeoutMs ?? 1_800_000); }
53
54
  else throw new Error("usage: dd-agy doctor | hooks install|check | daemon start|status|stop | session create|prompt|inspect|resume|cancel|fork [options] --json");
54
55
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
55
56
  } 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); }
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { cancelSession, createSession, doctor, inspectSession, promptFromFile, promptSession, startSession } from "../lib/dd-codex.mjs";
4
5
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-codex-daemon.mjs";
5
6
  import { errorRecord } from "../lib/operation-errors.mjs";
6
7
 
7
8
  function parse(argv) { const positional = []; const options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; } const value = argv[++i]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
8
- function common(options) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
9
+ function common(options, operation) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, operation) }; }
9
10
  try {
10
- const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
11
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options, family === "session" ? command : null); let result;
11
12
  if (family === "doctor") result = await doctor(shared);
12
13
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
13
14
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { mkdtemp, readFile, rm } from "node:fs/promises";
4
5
  import { randomUUID } from "node:crypto";
@@ -12,7 +13,7 @@ import { hookBudget, readNativeHookInput } from "../lib/native-hook-command.mjs"
12
13
  function parse(argv) { const positional = [], options = {}; for (let i = 0; i < argv.length; i++) { const word = argv[i]; if (!word.startsWith("--")) { positional.push(word); continue; } const key = word.slice(2); if (["json", "cancel-tree", "tree", "no-flow"].includes(key)) options[key] = true; else { if (argv[i + 1] === undefined) throw new Error(`${word} requires a value`); options[key] = argv[++i]; } } return { positional, options }; }
13
14
  try {
14
15
  const { positional: [family, command, modifier], options } = parse(process.argv.slice(2));
15
- const config = { stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, projectRoot: options["project-root"], bin: options["droid-bin"] ?? "droid", entryPath: process.argv[1], journal: options.journal, authHome: options["auth-home"], model: options.model, provider: options.provider, reasoning: options.reasoning, mode: options.mode, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1_000 : undefined };
16
+ const config = { stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, projectRoot: options["project-root"], bin: options["droid-bin"] ?? "droid", entryPath: process.argv[1], journal: options.journal, authHome: options["auth-home"], model: options.model, provider: options.provider, reasoning: options.reasoning, mode: options.mode, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, family === "session" ? command : null, "droid-cli") };
16
17
  let result;
17
18
  if (family === "doctor") {
18
19
  const dir = await mkdtemp(path.join(os.tmpdir(), "dd-droid-doctor-"));
@@ -27,7 +28,7 @@ try {
27
28
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(config.stateDir, options["operation-id"]);
28
29
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: config.stateDir, cancelTree: options["cancel-tree"] });
29
30
  else if (family === "hook" && command === "handle") { const budget = hookBudget(); const payload = JSON.parse(await readNativeHookInput(process.stdin, budget)); const eventId = randomUUID(); result = await callDaemon(config.stateDir, "hook.observe", { payload, eventId, deadlineAt: budget.deadlineAt }, budget.remaining("observation"), eventId); }
30
- else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], ...(config.noFlow ? { noFlow: true } : {}), ...(prompt ? { prompt } : {}) }); }
31
+ else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], livenessTimeoutMs: config.livenessTimeoutMs, ...(config.noFlow ? { noFlow: true } : {}), ...(prompt ? { prompt } : {}) }); }
31
32
  else throw new Error("usage: dd-droid doctor | daemon start|status|operation|stop | session create|resume|prompt|inspect|cancel");
32
33
  process.stdout.write(`${JSON.stringify(family === "hook" ? { ...(result.hookSpecificOutput ? { hookSpecificOutput: result.hookSpecificOutput } : {}), ...(typeof result.continue === "boolean" ? { continue: result.continue } : {}), ...(result.stopReason ? { stopReason: result.stopReason } : {}) } : { ok: true, ...result })}\n`);
33
34
  } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: errorRecord(error) })}\n`); process.exitCode = process.argv[2] === "hook" ? 2 : 1; }
@@ -1,20 +1,30 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
- import { hookBudget, invokeNativeHook, readNativeHookInput } from "../lib/native-hook-command.mjs";
4
+ import { hookBudget, invokeNativeHook, readNativeHookInput, writeNativeHookFailure } from "../lib/native-hook-command.mjs";
4
5
  import { cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-grok.mjs";
5
6
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-grok-daemon.mjs";
7
+ import { errorRecord } from "../lib/operation-errors.mjs";
6
8
 
7
9
  function parse(argv) { const positional = []; const options = {}; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; } const value = argv[++index]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
8
- function common(options) { return { bin: options["grok-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], model: options.model, reasoning: options.reasoning, mode: options.mode ?? "bypassPermissions", authPath: options["auth-path"], authMethodId: options["auth-method-id"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], sessionArchive: options["session-archive"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
10
+ function common(options, operation) { return { bin: options["grok-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], model: options.model, reasoning: options.reasoning, mode: options.mode ?? "bypassPermissions", authPath: options["auth-path"], authMethodId: options["auth-method-id"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], sessionArchive: options["session-archive"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, operation) }; }
9
11
  async function hook(options) {
10
12
  const budget = hookBudget();
11
- const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id }, budget.remaining("identity"));
13
+ const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id, payload }, budget.remaining("identity"));
12
14
  const enriched = { ...payload, _meta: { ...(payload._meta ?? {}), ddGrok: identity } }; const args = ["grok", "event", "handle", "--project-root", options["project-root"], "--json"]; const command = options["dd-flow-bin"];
13
- return invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: enriched, timeoutMs: budget.remaining("admission", 15_000) });
15
+ try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: enriched, timeoutMs: budget.remaining("admission", 15_000) }); }
16
+ catch (error) {
17
+ if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
18
+ try {
19
+ await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error, hookBudget(1_000, null));
20
+ if (identity.nativeHookBinding) await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id }, 1_000);
21
+ } catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
22
+ throw error;
23
+ }
14
24
  }
15
25
 
16
26
  try {
17
- const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
27
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options, family === "session" ? command : null); let result;
18
28
  if (family === "doctor") result = await doctor(shared);
19
29
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
20
30
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
@@ -27,4 +37,4 @@ try {
27
37
  else if (family === "session") { const params = { ...shared, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}), ...(command === "archive" ? { archiveDir: options["archive-dir"] } : {}) }; if (shared.stateDir) result = await callDaemon(shared.stateDir, `session.${command === "status" ? "inspect" : command}`, params, shared.timeoutMs ?? 1_800_000); else if (command === "create") result = await createSession(params); else if (command === "prompt") result = await promptSession(params); else if (["inspect", "status", "resume"].includes(command)) result = await inspectSession(params); else if (command === "cancel") result = await cancelSession(params); else if (command === "fork") result = await forkSession(params); else throw new Error(`unknown session command: ${command}`); }
28
38
  else throw new Error("usage: dd-grok doctor | daemon start|status|stop | session create|prompt|inspect|status|resume|cancel|fork|archive [options] --json");
29
39
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
30
- } catch (error) { if (process.argv[2] === "hook") { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); } 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); }
40
+ } catch (error) { if (process.argv[2] === "hook") { process.stderr.write(`${JSON.stringify({ native_hook_failure: errorRecord(error), bound: Boolean(error.details?.native_hook_binding) })}\n`); process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); } const failure = errorRecord(error); process.stderr.write(`${JSON.stringify({ ok: false, error: failure.message, ...failure })}\n`); process.exit(1); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-opencode-daemon.mjs";
@@ -6,7 +7,7 @@ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-open
6
7
  function parse(argv) { const positional = [], options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json","cancel-tree","no-flow"].includes(key)) options[key] = true; else { if (argv[i + 1] === undefined) throw new Error(`--${key} requires a value`); options[key] = argv[++i]; } } return { positional, options }; }
7
8
  const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
8
9
  try {
9
- const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, bin: options["opencode-bin"], provider: options.provider, model: options.model, variant: options.variant, agent: options.agent, authPath: options["auth-path"], projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, sessionArchive: options["session-archive"], timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; let result;
10
+ const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, bin: options["opencode-bin"], provider: options.provider, model: options.model, variant: options.variant, agent: options.agent, authPath: options["auth-path"], projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, sessionArchive: options["session-archive"], ...adapterTimeoutOptions(options, family === "session" ? command : null) }; let result;
10
11
  if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, entryPath: process.argv[1] }, startDaemon);
11
12
  else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
12
13
  else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
@@ -15,7 +16,7 @@ try {
15
16
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
16
17
  else if (family === "doctor") result = await callDaemon(common.stateDir, "daemon.doctor");
17
18
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
18
- else if (family === "session") { const operation = command === "status" ? "inspect" : command; const params = { sessionId: options["session-id"], title: options.title, messageId: options["message-id"], archiveFile: options["archive-file"], ...(command === "prompt" ? { prompt: await prompt(options) } : {}) }; result = await callDaemon(common.stateDir, `session.${operation}`, params, common.timeoutMs ?? 1_800_000); }
19
+ else if (family === "session") { const operation = command === "status" ? "inspect" : command; const params = { livenessTimeoutMs: common.livenessTimeoutMs, sessionId: options["session-id"], title: options.title, messageId: options["message-id"], archiveFile: options["archive-file"], ...(command === "prompt" ? { prompt: await prompt(options) } : {}) }; result = await callDaemon(common.stateDir, `session.${operation}`, params, common.timeoutMs ?? 1_800_000); }
19
20
  else throw new Error("usage: dd-opencode daemon start|status|stop | session create|prompt|inspect|fork|cancel|archive [options] --json");
20
21
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
21
22
  } catch (error) { 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); }
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
- import { hookBudget, invokeNativeHook, readNativeHookInput } from "../lib/native-hook-command.mjs";
4
+ import { hookBudget, invokeNativeHook, readNativeHookInput, writeNativeHookFailure } from "../lib/native-hook-command.mjs";
4
5
  import { cancelChild, cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-zcode.mjs";
5
6
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-zcode-daemon.mjs";
7
+ import { errorRecord } from "../lib/operation-errors.mjs";
6
8
 
7
9
  function parse(argv) {
8
10
  const positional = []; const options = {};
@@ -15,13 +17,7 @@ function parse(argv) {
15
17
  return { positional, options };
16
18
  }
17
19
 
18
- function common(options) {
19
- const seconds = (name) => {
20
- if (options[name] === undefined) return undefined;
21
- const value = Number(options[name]);
22
- if (!Number.isFinite(value) || value <= 0) throw new Error(`--${name} must be a positive finite number of seconds`);
23
- return value * 1000;
24
- };
20
+ function common(options, operation) {
25
21
  return {
26
22
  bin: options["zcode-acp-bin"], zcodePath: options["zcode-path"], journal: options.journal,
27
23
  stateDir: options["state-dir"],
@@ -31,25 +27,33 @@ function common(options) {
31
27
  permission: options.permission ?? "deny", ddFlowBin: options["dd-flow-bin"],
32
28
  ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true,
33
29
  env: options["dd-flow-home"] ? { DD_FLOW_HOME: options["dd-flow-home"] } : undefined,
34
- timeoutMs: seconds("timeout"),
35
- livenessTimeoutMs: seconds("liveness-timeout"),
36
- requestTimeoutMs: seconds("request-timeout"),
30
+ ...adapterTimeoutOptions(options, operation),
37
31
  };
38
32
  }
39
33
 
40
34
  async function hook(options) {
41
35
  const budget = hookBudget();
42
36
  const payload = JSON.parse((await readNativeHookInput(process.stdin, budget)) || "{}");
43
- const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId }, budget.remaining("identity"));
37
+ const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId, payload }, budget.remaining("identity"));
44
38
  const command = options["dd-flow-bin"]; const args = ["zcode", "hook", "handle", "--project-root", options["project-root"], "--json"];
45
39
 
46
- return invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], timeoutMs: budget.remaining("admission", 15_000), payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } });
40
+ try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], timeoutMs: budget.remaining("admission", 15_000), payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } }); }
41
+ catch (error) {
42
+ if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
43
+ try {
44
+ // The 25s work budget leaves this bounded 2s evidence tail inside the
45
+ // qualified 30s native hook limit, even after the work deadline expired.
46
+ await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error, hookBudget(1_000, null));
47
+ if (identity.nativeHookBinding) await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id }, 1_000);
48
+ } catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
49
+ throw error;
50
+ }
47
51
  }
48
52
 
49
53
  try {
50
54
  const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; let result;
51
55
  const answers = options["answers-file"] ? JSON.parse(await promptFromFile(options["answers-file"])) : undefined;
52
- const shared = common(options);
56
+ const shared = common(options, family === "session" ? command : null);
53
57
  if (family === "doctor") result = await doctor(shared);
54
58
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
55
59
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
@@ -78,6 +82,6 @@ try {
78
82
  } else throw new Error("usage: dd-zcode doctor | daemon start|status|stop | hook handle | session create|prompt|inspect|status|resume|cancel|cancel-child|fork [options] --json");
79
83
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
80
84
  } catch (error) {
81
- if (process.argv[2] === "hook") { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); }
82
- 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);
85
+ if (process.argv[2] === "hook") { process.stderr.write(`${JSON.stringify({ native_hook_failure: errorRecord(error), bound: Boolean(error.details?.native_hook_binding) })}\n`); process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); }
86
+ const failure = errorRecord(error); process.stderr.write(`${JSON.stringify({ ok: false, error: failure.message, ...failure })}\n`); process.exit(1);
83
87
  }
@@ -0,0 +1,5 @@
1
+ export const NATIVE_OPERATION_WAIT_CONTRACT: "native-operation-wait@1";
2
+ export function canonicalNativeOperation(harness: string, operation: string): string;
3
+ export function nativeOperationWait(harness: string, operation: string): "native-work" | "control";
4
+ export function validateDaemonWait(harness: string, operation: string, duration: number | null): void;
5
+ export function adapterTimeoutOptions(options: Record<string, unknown>, operation?: string | null, harness?: string): { timeoutMs?: number; livenessTimeoutMs?: number; requestTimeoutMs?: number };
@@ -0,0 +1,34 @@
1
+ // Public work flags express silence, never the lifetime of a native Turn.
2
+ import { validateObservationDuration } from "./observation-clock.mjs";
3
+ export const NATIVE_OPERATION_WAIT_CONTRACT = "native-operation-wait@1";
4
+ // Grok's resume only inspects its retained native Session. Other adapters
5
+ // materialize that Session, so their original operation remains productive.
6
+ export function canonicalNativeOperation(harness, operation) {
7
+ return ["grok", "grok-acp"].includes(harness) && operation === "session.resume" ? "session.inspect" : operation;
8
+ }
9
+ // Only Droid's start waits for the full Turn. Codex start returns acceptance;
10
+ // the other bundled adapters do not expose start as a productive operation.
11
+ export function nativeOperationWait(harness, operation) {
12
+ operation = canonicalNativeOperation(harness, operation);
13
+ return operation === "session.prompt" || ["droid", "droid-cli"].includes(harness) && operation === "session.start" ? "native-work" : "control";
14
+ }
15
+ export function validateDaemonWait(harness, operation, duration) {
16
+ if (duration === null && nativeOperationWait(harness, operation) === "native-work") return;
17
+ validateObservationDuration(duration, "daemon control duration");
18
+ }
19
+
20
+ export function adapterTimeoutOptions(options, operation, harness) {
21
+ const duration = name => {
22
+ if (options[name] === undefined) return undefined;
23
+ const ms = Number(options[name]) * 1_000;
24
+ if (!Number.isInteger(ms) || ms < 1 || ms > 2_147_483_647) throw Object.assign(new Error(`--${name} requires positive finite seconds within the timer range`), { code: "invalid_timeout" });
25
+ return ms;
26
+ };
27
+ const timeoutMs = duration("timeout"), livenessTimeoutMs = duration("liveness-timeout"), requestTimeoutMs = duration("request-timeout");
28
+ if (operation === "prompt" || nativeOperationWait(harness, `session.${operation}`) === "native-work") {
29
+ if (timeoutMs !== undefined && livenessTimeoutMs !== undefined && timeoutMs !== livenessTimeoutMs) throw Object.assign(new Error("--timeout and --liveness-timeout must agree for session.prompt"), { code: "invalid_timeout" });
30
+ if (requestTimeoutMs !== undefined) throw Object.assign(new Error("--request-timeout cannot limit a productive prompt"), { code: "invalid_timeout" });
31
+ return { timeoutMs: undefined, livenessTimeoutMs: livenessTimeoutMs ?? timeoutMs };
32
+ }
33
+ return { timeoutMs, livenessTimeoutMs, requestTimeoutMs };
34
+ }
@@ -0,0 +1,2 @@
1
+ export function recoverDaemonReply(root: string, id: string, options?: { timeoutMs?: number | null; signal?: AbortSignal; binding?: Record<string, unknown>; onProgress?: (value: Record<string, unknown>) => void; onOutcome?: (value: Record<string, unknown>) => void; onPendingSettlement?: (value: Record<string, unknown>) => Promise<void>; observationClock?: import("./observation-clock.mjs").ObservationClock }): Promise<Record<string, unknown>>;
2
+ export function retainedDaemonReply(root: string, id: string, binding?: Record<string, unknown>): Promise<{ ready: boolean; outcome: unknown; value?: unknown; settlement?: Record<string, unknown> | null; reason?: string } | null>;