@deksden-com/dd-flow-cli 0.9.0-beta.22 → 0.9.0-beta.23

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 (42) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/help.js +1 -0
  4. package/dist/cli/run-cli.js +3 -0
  5. package/dist/harness-runtime/bin/dd-agy.mjs +34 -0
  6. package/dist/harness-runtime/bin/dd-codex.mjs +22 -0
  7. package/dist/harness-runtime/bin/dd-droid.mjs +30 -0
  8. package/dist/harness-runtime/bin/dd-grok.mjs +28 -0
  9. package/dist/harness-runtime/bin/dd-opencode.mjs +18 -0
  10. package/dist/harness-runtime/bin/dd-zcode.mjs +62 -0
  11. package/dist/harness-runtime/lib/daemon-operations.mjs +78 -0
  12. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +249 -0
  13. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  14. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +90 -0
  15. package/dist/harness-runtime/lib/dd-codex.mjs +382 -0
  16. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +123 -0
  17. package/dist/harness-runtime/lib/dd-droid.mjs +434 -0
  18. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +279 -0
  19. package/dist/harness-runtime/lib/dd-grok.mjs +167 -0
  20. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +153 -0
  21. package/dist/harness-runtime/lib/dd-opencode.mjs +80 -0
  22. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +420 -0
  23. package/dist/harness-runtime/lib/dd-zcode.mjs +593 -0
  24. package/dist/harness-runtime/lib/driver-recovery.mjs +119 -0
  25. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  26. package/dist/harness-runtime/lib/managed-daemon.mjs +119 -0
  27. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  28. package/dist/harness-runtime/lib/observation-clock.mjs +33 -0
  29. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  30. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  31. package/dist/harness-runtime/lib/process-json.mjs +61 -0
  32. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  33. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  34. package/dist/harness-runtime/lib/session-settlement.mjs +24 -0
  35. package/dist/services/execution-policy.js +61 -0
  36. package/dist/services/external-work-launch.js +76 -0
  37. package/dist/services/harness-adapter.js +31 -14
  38. package/dist/services/runs.js +2 -1
  39. package/dist/services/stage-context.js +2 -2
  40. package/dist/services/vnext-execution-profile.js +38 -3
  41. package/dist/storage/database.js +21 -0
  42. package/package.json +8 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.23
4
+
5
+ ### Minor Changes
6
+
7
+ - bcfd8cb: Ship the six managed provider adapters with the CLI runtime, including their
8
+ daemon primitives and public adapter entrypoints. Add frozen mixed-execution
9
+ policy resolution and durable explicit external Work launch without changing a
10
+ same-profile external request into native delegation.
11
+
3
12
  ## 0.9.0-beta.22
4
13
 
5
14
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.22",
4
- "cli_commit": "7a08ac0550694b06cec5438d5ed938d19f772418",
5
- "built_at": "2026-09-07T02:03:13.276Z",
3
+ "cli_version": "0.9.0-beta.23",
4
+ "cli_commit": "907047591797ed4db97ade9e94df42c3be7340d2",
5
+ "built_at": "2026-09-07T05:55:58.460Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.0.6",
8
8
  "commit": "2aafb30d23e3f646b736b7ead25438cab97b16bf",
package/dist/cli/help.js CHANGED
@@ -207,6 +207,7 @@ Usage:
207
207
  dd-flow work ls (--run <RUN-ID>|--parent <WORK-ID>) [--ready] --json
208
208
  dd-flow work show <WORK-ID> --json
209
209
  dd-flow work start <WORK-ID> --project-root <root> --json
210
+ dd-flow work launch <WORK-ID> [--stage <name>] --json
210
211
  dd-flow work finish <WORK-ID> (--result-file <path>|--result-stdin) --project-root <root> --json
211
212
  dd-flow work repair add --run <RUN-ID> (--from-check <CHECK-ID>|--from-unresolved <gap> --verification-file <file>) --origin-work <WORK-ID>... --task-stdin --project-root <root> --json
212
213
 
@@ -48,6 +48,7 @@ import { classifyCliOperation, requiresExplicitUpgradeAuthorization } from "../s
48
48
  import { renderWorkerPrompt } from "../services/prompts.js";
49
49
  import { finishStage, startStage } from "../services/stage-lifecycle.js";
50
50
  import { addWorkBatch, assertStageLifecycleOwner, assertWorkLifecycleOwner, cancelWork, deleteWork, failWork, finishWork, listWorks, mutateWorkDeps, retryWork, showWork, startWork } from "../services/work-registry.js";
51
+ import { launchExternalWork } from "../services/external-work-launch.js";
51
52
  import { createEvalBootstrapSnapshot, createEvalRunSnapshot, prepareVnextSpecifyRun, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
52
53
  import { acceptRunRecovery, beginRunRecovery, inspectRunRecovery, recordRecoveryCapture, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
53
54
  import { loadExternalStageContext } from "../services/stage-context.js";
@@ -761,6 +762,8 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
761
762
  }
762
763
  if (family === "work" && command === "delete")
763
764
  return deleteWork(context, requiredPosition(parsed, 0, "work-id"));
765
+ if (family === "work" && command === "launch")
766
+ return await launchExternalWork(context, { workId: requiredPosition(parsed, 0, "work-id"), ...(optionalOption(parsed, "stage") ? { stage: optionalOption(parsed, "stage") } : {}), progress: commandProgress(io, output) });
764
767
  if (family === "work" && command === "start") {
765
768
  const hookEventId = optionalOption(parsed, "hook-event-id");
766
769
  const recoveryId = optionalOption(parsed, "recovery-id");
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ import { doctor } from "../lib/dd-agy.mjs";
5
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-agy-daemon.mjs";
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","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 }; }
8
+ const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
9
+ async function stdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
10
+ async function hook(options) {
11
+ const payload = JSON.parse(await stdin() || "{}"), event = options.event;
12
+ const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload });
13
+ if (event === "PreToolUse" || event === "PostToolUse") {
14
+ const phase = event === "PreToolUse" ? "before" : "after", args = payload.toolCall?.args ?? {};
15
+ const normalized = { schema_id: "dd-flow/agy-tool-event@1", event_id: identity.event_id, phase, daemon_id: identity.daemon_id, conversation_id: payload.conversationId, parent_conversation_id: identity.parent_conversation_id, step_index: payload.stepIdx, tool: payload.toolCall?.name, input: args, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath, model: payload.modelName, profile: identity.profile, ...(phase === "after" ? { error: payload.error ?? "" } : {}) };
16
+ const command = options["dd-flow-bin"] ?? identity.dd_flow_bin, target = /\.[cm]?js$/.test(command) ? { command: process.execPath, args: [command] } : { command, args: [] };
17
+ await new Promise((resolve, reject) => { const child = spawn(target.command, [...target.args, "agy", "event", "handle", "--project-root", options["project-root"] ?? identity.project_root, "--json"], { env: { ...process.env, DD_FLOW_HOME: options["dd-flow-home"] ?? identity.dd_flow_home }, stdio: ["pipe", "ignore", "pipe"] }); let error = ""; child.stderr.setEncoding("utf8").on("data", chunk => error += chunk); child.on("error", reject); child.on("close", code => code === 0 ? resolve() : reject(new Error(error || "dd-flow rejected Antigravity hook"))); child.stdin.end(`${JSON.stringify(normalized)}\n`); });
18
+ }
19
+ return event === "PreToolUse" ? { decision: "allow" } : {};
20
+ }
21
+
22
+ try {
23
+ const { positional, options } = parse(process.argv.slice(2)), [family, command] = positional; const common = { 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;
24
+ if (family === "doctor") result = await doctor(common);
25
+ else if (family === "daemon" && command === "start") result = await startDaemon({ ...common, sessionId: options["session-id"], entryPath: process.argv[1] });
26
+ else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
27
+ else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
28
+ else if (family === "daemon" && command === "operation") result = await callDaemon(common.stateDir, "operation.inspect", { operationId: options["operation-id"] });
29
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
30
+ else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
31
+ 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); }
32
+ else throw new Error("usage: dd-agy doctor | daemon start|status|stop | session create|prompt|inspect|resume|cancel|fork [options] --json");
33
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
34
+ } 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); }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { cancelSession, createSession, doctor, inspectSession, promptFromFile, promptSession, startSession } from "../lib/dd-codex.mjs";
3
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-codex-daemon.mjs";
4
+
5
+ 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"].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 }; }
6
+ 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"], timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
7
+ try {
8
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command] = positional; const shared = common(options); let result;
9
+ if (family === "doctor") result = await doctor(shared);
10
+ else if (family === "daemon" && command === "start") result = await startDaemon({ ...shared, entryPath: process.argv[1] });
11
+ else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
12
+ else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
13
+ else if (family === "daemon" && command === "operation") result = await callDaemon(shared.stateDir, "operation.inspect", { operationId: options["operation-id"] });
14
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ ...shared, cancelTree: options["cancel-tree"] });
15
+ else if (family === "session" && command === "create") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.create", shared) : await createSession(shared);
16
+ else if (family === "session" && command === "prompt") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.prompt", { ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt }, shared.timeoutMs ?? 1_800_000) : await promptSession({ ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt });
17
+ else if (family === "session" && command === "start") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.start", { ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt }, shared.timeoutMs ?? 30_000) : await startSession({ ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt });
18
+ else if (family === "session" && ["inspect", "status", "resume"].includes(command)) result = shared.stateDir ? await callDaemon(shared.stateDir, "session.inspect", shared) : await inspectSession(shared);
19
+ else if (family === "session" && command === "cancel") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.cancel", shared) : await cancelSession(shared);
20
+ else throw new Error("usage: dd-codex doctor | daemon start|status|stop | session create|start|prompt|inspect|status|resume|cancel [--json]");
21
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
22
+ } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code } : {}) })}\n`); process.exit(1); }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { randomUUID } from "node:crypto";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { DROID_CONTRACT, DROID_PROTOCOL, DroidRuntime, droidVersion } from "../lib/dd-droid.mjs";
7
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-droid-daemon.mjs";
8
+ import { errorRecord } from "../lib/operation-errors.mjs";
9
+
10
+ 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 }; }
11
+ async function input() { let text = ""; for await (const chunk of process.stdin) text += chunk; return text; }
12
+ try {
13
+ const { positional: [family, command], options } = parse(process.argv.slice(2));
14
+ 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 };
15
+ let result;
16
+ if (family === "doctor") {
17
+ const dir = await mkdtemp(path.join(os.tmpdir(), "dd-droid-doctor-"));
18
+ const runtime = new DroidRuntime({ ...config, stateDir: dir, home: path.join(dir, "home"), cwd: config.cwd ?? process.cwd(), journal: path.join(dir, "events.jsonl"), daemonId: randomUUID(), ddFlowHome: null, ddFlowBin: null }, {}, async () => {});
19
+ try { await runtime.start(); result = { harness: "droid-cli", observed_runtime: { droid: await droidVersion(config.bin), factory_protocol: runtime.protocol, dd_harness_contract: DROID_CONTRACT }, models: runtime.models.map(model => model.id ?? model.modelId), capabilities: { native_children: true, tree_cancel: true, selective_child_cancel: false, native_fork: false }, protocol_expected: DROID_PROTOCOL }; }
20
+ finally { await runtime.closeTree(true); await rm(dir, { recursive: true, force: true }); }
21
+ } else if (family === "daemon" && command === "start") result = await startDaemon(config);
22
+ else if (family === "daemon" && command === "serve") { await serveDaemon(config.stateDir); process.exit(0); }
23
+ else if (family === "daemon" && command === "status") result = await callDaemon(config.stateDir, "daemon.status");
24
+ else if (family === "daemon" && command === "operation") result = await callDaemon(config.stateDir, "operation.inspect", { operationId: options["operation-id"] });
25
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: config.stateDir, cancelTree: options["cancel-tree"] });
26
+ else if (family === "hook" && command === "handle") { const payload = JSON.parse(await input()); const eventId = randomUUID(); result = await callDaemon(config.stateDir, "hook.observe", { payload, eventId }, 25_000, eventId); }
27
+ 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"], ...(prompt ? { prompt } : {}) }); }
28
+ else throw new Error("usage: dd-droid doctor | daemon start|status|operation|stop | session create|resume|prompt|inspect|cancel");
29
+ 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`);
30
+ } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: errorRecord(error) })}\n`); process.exitCode = process.argv[2] === "hook" ? 2 : 1; }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-grok.mjs";
4
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-grok-daemon.mjs";
5
+
6
+ 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 }; }
7
+ 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 }; }
8
+ async function readStdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
9
+ async function hook(options) {
10
+ const stdin = await readStdin(); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id });
11
+ 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"]; const target = /\.[cm]?js$/.test(command) ? { command: process.execPath, args: [command, ...args] } : { command, args };
12
+ await new Promise((resolve, reject) => { const child = spawn(target.command, target.args, { env: { ...process.env, DD_FLOW_HOME: options["dd-flow-home"] }, stdio: ["pipe", "ignore", "pipe"] }); let stderr = ""; child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk)); child.on("error", reject); child.on("close", (code) => code === 0 ? resolve() : reject(new Error(stderr || "dd-flow rejected Grok hook"))); child.stdin.end(`${JSON.stringify(enriched)}\n`); });
13
+ return { decision: "allow" };
14
+ }
15
+
16
+ try {
17
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command] = positional; const shared = common(options); let result;
18
+ if (family === "doctor") result = await doctor(shared);
19
+ else if (family === "daemon" && command === "start") result = await startDaemon({ ...shared, entryPath: process.argv[1] });
20
+ else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
21
+ else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
22
+ else if (family === "daemon" && command === "operation") result = await callDaemon(shared.stateDir, "operation.inspect", { operationId: options["operation-id"] });
23
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
24
+ else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
25
+ 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.${["status", "resume"].includes(command) ? "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}`); }
26
+ else throw new Error("usage: dd-grok doctor | daemon start|status|stop | session create|prompt|inspect|status|resume|cancel|fork|archive [options] --json");
27
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
28
+ } 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); }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-opencode-daemon.mjs";
4
+
5
+ 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 }; }
6
+ const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
7
+ try {
8
+ const { positional, options } = parse(process.argv.slice(2)), [family, command] = positional; const common = { 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;
9
+ if (family === "daemon" && command === "start") result = await startDaemon({ ...common, entryPath: process.argv[1] });
10
+ else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
11
+ else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
12
+ else if (family === "daemon" && command === "operation") result = await callDaemon(common.stateDir, "operation.inspect", { operationId: options["operation-id"] });
13
+ else if (family === "doctor") result = await callDaemon(common.stateDir, "daemon.doctor");
14
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
15
+ else if (family === "session") { const operation = ["status","resume"].includes(command) ? "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); }
16
+ else throw new Error("usage: dd-opencode daemon start|status|stop | session create|prompt|inspect|fork|cancel|archive [options] --json");
17
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
18
+ } 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); }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { cancelChild, cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-zcode.mjs";
3
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-zcode-daemon.mjs";
4
+
5
+ function parse(argv) {
6
+ const positional = []; const options = {};
7
+ for (let index = 0; index < argv.length; index += 1) {
8
+ const token = argv[index];
9
+ if (!token.startsWith("--")) { positional.push(token); continue; }
10
+ const key = token.slice(2); if (["json", "cancel-tree"].includes(key)) { options[key] = true; continue; }
11
+ const value = argv[++index]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value;
12
+ }
13
+ return { positional, options };
14
+ }
15
+
16
+ function common(options) {
17
+ return {
18
+ bin: options["zcode-acp-bin"], zcodePath: options["zcode-path"], journal: options.journal,
19
+ stateDir: options["state-dir"],
20
+ cwd: options.cwd, sessionId: options["session-id"], adapterSessionId: options["adapter-session-id"], provider: options.provider,
21
+ childSessionId: options["child-session-id"],
22
+ model: options.model, reasoning: options.reasoning, mode: options.mode,
23
+ permission: options.permission ?? "deny", ddFlowBin: options["dd-flow-bin"],
24
+ ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"],
25
+ env: options["dd-flow-home"] ? { DD_FLOW_HOME: options["dd-flow-home"] } : undefined,
26
+ timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined,
27
+ livenessTimeoutMs: options["liveness-timeout"] ? Number(options["liveness-timeout"]) * 1000 : undefined,
28
+ };
29
+ }
30
+
31
+ try {
32
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command] = positional; let result;
33
+ const answers = options["answers-file"] ? JSON.parse(await promptFromFile(options["answers-file"])) : undefined;
34
+ const shared = common(options);
35
+ if (family === "doctor") result = await doctor(shared);
36
+ else if (family === "daemon" && command === "start") result = await startDaemon({ ...shared, entryPath: process.argv[1] });
37
+ else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
38
+ else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
39
+ else if (family === "daemon" && command === "operation") result = await callDaemon(shared.stateDir, "operation.inspect", { operationId: options["operation-id"] });
40
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
41
+ else if (family === "session") {
42
+ const params = { ...shared, answers, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}) };
43
+ // promptSessionWithBridge owns the meaningful deadline: it fails when its
44
+ // ACP Session has been idle for the configured liveness window. A fixed
45
+ // RPC deadline would wrongly terminate a long but active Turn.
46
+ const operation = `session.${["status", "resume"].includes(command) ? "inspect" : command}`;
47
+ const rpcTimeout = command === "prompt" && shared.timeoutMs === undefined
48
+ ? null
49
+ : (shared.timeoutMs ?? 1_800_000);
50
+ if (shared.stateDir) result = await callDaemon(shared.stateDir, operation, params, rpcTimeout);
51
+ else if (command === "create") result = await createSession(params);
52
+ else if (command === "prompt") result = await promptSession(params);
53
+ else if (["inspect", "status", "resume"].includes(command)) result = await inspectSession(params);
54
+ else if (command === "cancel") result = await cancelSession(params);
55
+ else if (command === "cancel-child") result = await cancelChild(params);
56
+ else if (command === "fork") result = await forkSession(params);
57
+ else throw new Error(`unknown session command: ${command}`);
58
+ } else throw new Error("usage: dd-zcode doctor | daemon start|status|stop | session create|prompt|inspect|status|resume|cancel|cancel-child|fork [options] --json");
59
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
60
+ } catch (error) {
61
+ 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);
62
+ }
@@ -0,0 +1,78 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, link, unlink } from "node:fs/promises";
3
+ import { randomUUID } from "node:crypto";
4
+ import path from "node:path";
5
+ import { hashJson, writeJsonAtomic } from "./runner-events.mjs";
6
+ import { errorRecord, isObservationLoss, reportedError } from "./operation-errors.mjs";
7
+ import { assertDaemonOwnership } from "./managed-daemon.mjs";
8
+ import { readModelObservations, modelAttribution } from "./model-observations.mjs";
9
+
10
+ const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork"]);
11
+ const controls = new Map();
12
+
13
+ async function controlDispatch(root, request, action) {
14
+ const key = request.operation === "daemon.stop" ? `stop:${request.params?.cancelTree === true}` : `cancel:${request.params?.sessionId ?? ""}`;
15
+ let pending = controls.get(root);
16
+ if (!pending) { pending = new Map(); controls.set(root, pending); }
17
+ if (pending.has(key)) return await pending.get(key);
18
+ // Preserve the adapter's identity check for every distinct cancel target.
19
+ // A stop arriving after cancel waits for its attempts before verification.
20
+ const predecessors = key.startsWith("stop:") ? [...pending.values()] : [];
21
+ const promise = (async () => { await Promise.allSettled(predecessors); return await action(); })();
22
+ pending.set(key, promise);
23
+ try { return await promise; }
24
+ finally { pending.delete(key); if (!pending.size) controls.delete(root); }
25
+ }
26
+ function location(root, id) {
27
+ if (typeof id !== "string" || !id) throw Object.assign(new Error("operation id is required"), { code: "operation_id_required" });
28
+ return path.join(root, "operations", createHash("sha256").update(id).digest("hex"));
29
+ }
30
+ async function read(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
31
+
32
+ export async function inspectDaemonOperation(root, id) {
33
+ const directory = location(root, id);
34
+ const requested = await read(path.join(directory, "requested.json"));
35
+ if (!requested) throw Object.assign(new Error("daemon operation is not recorded"), { code: "operation_not_found", details: { operation_id: id } });
36
+ return { ...requested, ...(await read(path.join(directory, "result.json")) ?? await read(path.join(directory, "observation-lost.json")) ?? { state: "running" }) };
37
+ }
38
+
39
+ /** At-most-once dispatch and a durable terminal reply, including when the socket
40
+ * observer disconnects. Recovery reads this receipt; it never resends a prompt. */
41
+ export async function durableDaemonDispatch(root, request, action) {
42
+ if (request.operation === "operation.inspect") return await inspectDaemonOperation(root, request.params?.operationId);
43
+ if (request.operation === "daemon.status") {
44
+ const result = await action();
45
+ if (!result?.config?.journal) return result;
46
+ try { return { ...result, model_attribution: modelAttribution(await readModelObservations(result.config.journal)) }; }
47
+ catch (error) { return { ...result, model_observation_error: errorRecord(error) }; }
48
+ }
49
+ if (["daemon.stop", "session.cancel"].includes(request.operation)) return await controlDispatch(root, request, action);
50
+ if (!productive.has(request.operation)) return await action();
51
+ assertDaemonOwnership();
52
+ const directory = location(root, request.id), fingerprint = hashJson({ operation: request.operation, params: request.params ?? {} });
53
+ await mkdir(directory, { recursive: true, mode: 0o700 });
54
+ // Publish a complete request atomically and exclusively. Writing directly with
55
+ // wx exposes a partially written JSON document to a concurrent observer.
56
+ const temporary = path.join(directory, `requested-${randomUUID()}.json`);
57
+ await writeJsonAtomic(temporary, { schema_id: "dd-flow/daemon-operation@1", operation_id: request.id, operation: request.operation, session_id: request.params?.sessionId ?? null, params_sha256: fingerprint, requested_at: new Date().toISOString() });
58
+ try {
59
+ await link(temporary, path.join(directory, "requested.json"));
60
+ } catch (error) {
61
+ if (error.code !== "EEXIST") throw error;
62
+ const saved = await inspectDaemonOperation(root, request.id);
63
+ if (saved.params_sha256 !== fingerprint) throw Object.assign(new Error("operation id was reused with different parameters"), { code: "operation_conflict", details: { operation_id: request.id } });
64
+ if (saved.state === "completed") return saved.result;
65
+ if (saved.state === "failed") throw reportedError(saved.error, "provider operation failed");
66
+ throw Object.assign(new Error("provider operation has no confirmed terminal result; inspect the existing operation instead of resending it"), { code: "operation_observation_lost", details: { operation_id: request.id, receipt_directory: directory } });
67
+ } finally { await unlink(temporary); }
68
+ let result;
69
+ try { result = await action(); }
70
+ catch (error) {
71
+ const unknown = isObservationLoss(error);
72
+ await writeJsonAtomic(path.join(directory, unknown ? "observation-lost.json" : "result.json"), { state: unknown ? "awaiting_provider" : "failed", observed_at: new Date().toISOString(), error: errorRecord(error) });
73
+ throw error;
74
+ }
75
+ try { await writeJsonAtomic(path.join(directory, "result.json"), { state: "completed", finished_at: new Date().toISOString(), result }); }
76
+ catch (cause) { throw Object.assign(new Error("provider returned but the durable reply could not be saved; do not resend the operation"), { code: "operation_observation_lost", cause, details: { operation_id: request.id, receipt_directory: directory } }); }
77
+ return result;
78
+ }