@deksden-com/dd-flow-cli 0.9.0-beta.26 → 0.9.0-beta.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +0 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +0 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +0 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +0 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +0 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +0 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +0 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +27 -1
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +10 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +2 -2
- package/dist/services/run-recovery.js +13 -7
- package/package.json +12 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.28
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Accept an authoritative terminal Antigravity root result when the provider omits a root Stop hook, while retaining fail-closed behavior for an observed non-idle tree.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.27
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Resume retained native sessions from their active coordinator WorkSession, preserving the root launch record and rejecting lifecycle receipts from retired daemons.
|
|
14
|
+
- Propagate Antigravity lifecycle hooks to direct native children while preserving workspace hooks, and settle confirmed Codex system-error turns without losing provider failure evidence.
|
|
15
|
+
|
|
3
16
|
## 0.9.0-beta.26
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.28",
|
|
4
|
+
"cli_commit": "575fa0e034bf7564748628ae4d7d70ffa04be1c9",
|
|
5
|
+
"built_at": "2026-09-07T08:09:01.885Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.0.6",
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "c6fc50cb3b5526ea0162ee4454d1ee36f17be2da",
|
|
9
9
|
"flow_contract": "dd-flow-canonical-2026-08",
|
|
10
10
|
"repo_root": "/Users/deksden/Documents/_Projects/dd-memorybank",
|
|
11
11
|
"memorybank_root": "/Users/deksden/Documents/_Projects/dd-memorybank/.memory-bank",
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -45,8 +45,30 @@ async function prepare(paths, config) {
|
|
|
45
45
|
PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: command.replace("__EVENT__", "PostToolUse"), timeout: 30 }] }],
|
|
46
46
|
Stop: [{ type: "command", command: command.replace("__EVENT__", "Stop"), timeout: 30 }]
|
|
47
47
|
} };
|
|
48
|
+
// Antigravity's process-level configuration is not inherited by native
|
|
49
|
+
// subagents. Put the same handlers in the isolated workspace discovery
|
|
50
|
+
// root before the first Session is created, preserving unrelated hooks.
|
|
51
|
+
const workspaceConfig = path.join(config.projectRoot, ".agents");
|
|
52
|
+
await mkdir(workspaceConfig, { recursive: true, mode: 0o700 });
|
|
53
|
+
const workspaceHooks = path.join(workspaceConfig, "hooks.json");
|
|
54
|
+
let existing = {};
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(await readFile(workspaceHooks, "utf8"));
|
|
57
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error("not an object");
|
|
58
|
+
existing = parsed;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code !== "ENOENT") throw new DaemonError("workspace_hooks_invalid", `cannot install Antigravity lifecycle hooks: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
const workspace = { ...existing };
|
|
63
|
+
const isManagedHandler = handler => typeof handler?.command === "string" && handler.command.includes(config.entryPath) && handler.command.includes("hook handle");
|
|
64
|
+
for (const [event, handlers] of Object.entries(hooks["dd-flow"])) {
|
|
65
|
+
const retained = Array.isArray(existing[event]) ? existing[event].filter(item => Array.isArray(item?.hooks) ? !item.hooks.some(isManagedHandler) : !isManagedHandler(item)) : [];
|
|
66
|
+
workspace[event] = [...retained, ...handlers];
|
|
67
|
+
}
|
|
68
|
+
await writeJson(workspaceHooks, workspace);
|
|
48
69
|
await writeJson(path.join(paths.config, "hooks.json"), hooks);
|
|
49
70
|
config.hooksSha256 = createHash("sha256").update(JSON.stringify(hooks)).digest("hex");
|
|
71
|
+
config.workspaceHooksPath = workspaceHooks;
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
@@ -202,7 +224,11 @@ export class Runtime {
|
|
|
202
224
|
const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
|
|
203
225
|
const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
|
|
204
226
|
const treeSettled = [...this.descendants.values()].every(child => ["completed", "failed", "cancelled"].includes(child.status));
|
|
205
|
-
|
|
227
|
+
// A terminal root result is the provider's authoritative end-of-turn
|
|
228
|
+
// observation when AGY does not emit a root Stop hook. A received
|
|
229
|
+
// non-idle Stop still wins, and every discovered child must be terminal.
|
|
230
|
+
const rootSettled = observation?.stop?.fullyIdle !== false;
|
|
231
|
+
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.active && result && result.status !== "RUNNING" && treeSettled && rootSettled) };
|
|
206
232
|
}
|
|
207
233
|
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
208
234
|
async prompt(text, assertDispatch = captureDispatchGuard(this)) { if (!text) throw new DaemonError("prompt_required", "prompt is required"); await this.start(); if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running"); if (this.lastResult && !this.receipt().settled) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled"); const root = this.init?.conversation_id; const prior = this.sessionObservations.get(root) ?? {}; this.sessionObservations.set(root, { ...prior, execution_floor: prior.stop?.executionNum ?? prior.execution_floor ?? null, stop: null }); this.lastProviderEvent = null; await this.persist({ active_tree: true }); return await new Promise((resolve, reject) => { assertDispatch(); this.active = { resolve, reject }; this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => { if (error) { this.active = null; reject(error); } }); }); }
|
|
@@ -20,6 +20,14 @@ async function readState(file) { try { return JSON.parse(await readFile(file, "u
|
|
|
20
20
|
async function writeState(file, value) { const temp = `${file}.${process.pid}.tmp`; await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 }); await rename(temp, file); }
|
|
21
21
|
async function removeSocket(socket) { try { const info = await lstat(socket); if (!info.isSocket()) throw Object.assign(new Error("refusing to remove non-socket daemon path"), { code: "unsafe_socket" }); await unlink(socket); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
22
22
|
const errorPayload = errorRecord;
|
|
23
|
+
const terminalTurn = turn => ["completed", "interrupted", "failed", "cancelled"].includes(turn?.status);
|
|
24
|
+
export function settledCodexSession(status, latestTurn, active) {
|
|
25
|
+
if (status === "idle") return true;
|
|
26
|
+
if (status === "notLoaded") return !latestTurn || terminalTurn(latestTurn);
|
|
27
|
+
// `systemError` alone is not evidence that the provider stopped. A current
|
|
28
|
+
// terminal Turn plus no daemon operation is the narrow settled case.
|
|
29
|
+
return status === "systemError" && !active && terminalTurn(latestTurn);
|
|
30
|
+
}
|
|
23
31
|
|
|
24
32
|
export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
|
|
25
33
|
const target = locations(stateDir); const request = { schema_id: requestSchema, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
|
|
@@ -69,9 +77,8 @@ export async function serveDaemon(stateDir) {
|
|
|
69
77
|
const observed = await inspectSessionWithBridge(bridge, { ...state.config, sessionId });
|
|
70
78
|
for (const child of observed.descendants ?? []) sessions.add(child.provider_session_id);
|
|
71
79
|
const status = observed.status?.type ?? observed.status;
|
|
72
|
-
const stored =
|
|
73
|
-
|
|
74
|
-
if (status !== "idle" && !terminalUnloaded) unsettled.push(sessionId);
|
|
80
|
+
const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
|
|
81
|
+
if (!settledCodexSession(status, stored?.data?.[0] ?? null, active)) unsettled.push(sessionId);
|
|
75
82
|
}
|
|
76
83
|
return { sessions: unsettled.reverse(), active: Boolean(active), active_operation: active };
|
|
77
84
|
},
|
|
@@ -9,7 +9,7 @@ import { observeModel } from "./model-observations.mjs";
|
|
|
9
9
|
// Minimal JSON-RPC wrapper around the public Codex app-server protocol. It is
|
|
10
10
|
// intentionally one-shot: the provider owns durable Threads, while the caller
|
|
11
11
|
// owns the runner journal and can safely resume after a process restart.
|
|
12
|
-
function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
|
|
12
|
+
function fail(message, code, details) { const error = new Error(message); if (code) error.code = code; if (details) error.details = details; throw error; }
|
|
13
13
|
function required(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
|
|
14
14
|
function absolute(value, label) { const resolved = path.resolve(required(value, label)); if (!path.isAbsolute(resolved)) fail(`${label} must be absolute`); return resolved; }
|
|
15
15
|
function now() { return new Date().toISOString(); }
|
|
@@ -305,7 +305,7 @@ function terminalTurn(value, turnId, bridge) {
|
|
|
305
305
|
const observed = bridge.turns.get(turnId);
|
|
306
306
|
if (observed?.status === "completed") value = observed.value;
|
|
307
307
|
const turn = value?.turn ?? value;
|
|
308
|
-
if (turn?.status && turn.status !== "completed") fail(`Codex turn ${turnId ?? "unknown"} ended as ${turn.status}`, "turn_interrupted");
|
|
308
|
+
if (turn?.status && turn.status !== "completed") fail(`Codex turn ${turnId ?? "unknown"} ended as ${turn.status}`, "turn_interrupted", { turn_id: turn.id ?? turnId ?? null, terminal_status: turn.status, provider_error: turn.error ?? turn.codexErrorInfo ?? null });
|
|
309
309
|
return value;
|
|
310
310
|
}
|
|
311
311
|
|
|
@@ -68,7 +68,10 @@ export function assertRecoveryHookCurrent(context, projectId, runId, identity) {
|
|
|
68
68
|
const generation = assertRunMutationAllowed(context, projectId, runId);
|
|
69
69
|
if (!generation)
|
|
70
70
|
return;
|
|
71
|
-
const
|
|
71
|
+
const current = identity.daemonId && context.db.get("SELECT b.recovery_id 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.status = 'recovered' AND b.status = 'accepted' AND b.daemon_id = ? LIMIT 1", [projectId, runId, identity.daemonId]);
|
|
72
|
+
if (current)
|
|
73
|
+
return;
|
|
74
|
+
const retired = identity.daemonId && (context.db.get(`SELECT ws.id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN hook_events he ON he.project_id = w.project_id AND he.event_key = ws.hook_event_id WHERE w.project_id = ? AND w.run_id = ? AND he.daemon_id = ? LIMIT 1`, [projectId, runId, identity.daemonId]) || context.db.get("SELECT b.recovery_id 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 b.daemon_id = ? AND b.status = 'abandoned' LIMIT 1", [projectId, runId, identity.daemonId]));
|
|
72
75
|
if (!identity.daemonId || retired)
|
|
73
76
|
throw new AppError("recovery_daemon_stale", "Lifecycle receipt belongs to an unidentified or retired recovery daemon", 1, { run_id: runId, generation, daemon_id: identity.daemonId });
|
|
74
77
|
}
|
|
@@ -155,9 +158,12 @@ export function resumeRunRecovery(context, input) {
|
|
|
155
158
|
}
|
|
156
159
|
if (guard.status !== "sealed" || !guard.capture_path)
|
|
157
160
|
throw new AppError("recovery_capture_required", "Recovery must be sealed and captured before it can resume", 1, { recovery_id: guard.recovery_id, status: guard.status });
|
|
158
|
-
// The
|
|
159
|
-
// prompt from the eval runner.
|
|
160
|
-
//
|
|
161
|
+
// The coordinator keeps its same native Session and receives a recovery
|
|
162
|
+
// prompt from the eval runner. A root Work may have recorded its launch as
|
|
163
|
+
// complete while its currently active Stage Work owns that Session, so bind
|
|
164
|
+
// the active WorkSession rather than assuming the root row is still running.
|
|
165
|
+
// Only delegated Work is returned to the ready graph and therefore gets a
|
|
166
|
+
// fresh, addressable recovery packet.
|
|
161
167
|
verifyRecoverySnapshot(guard.capture_path, { runId: run.id, recoveryId: guard.recovery_id, projectRoot: run.project_root, workspaceRoot: run.workspace_root });
|
|
162
168
|
const now = context.now();
|
|
163
169
|
let running = [];
|
|
@@ -168,14 +174,14 @@ export function resumeRunRecovery(context, input) {
|
|
|
168
174
|
throw new AppError("recovery_state_changed", "Recovery state changed before its resume transaction", 1, { recovery_id: guard.recovery_id });
|
|
169
175
|
if (!input.daemonId?.trim() || !input.harness?.trim() || !input.nativeSessionId?.trim())
|
|
170
176
|
throw new AppError("recovery_identity_required", "Recovery requires the replacement daemon and retained native Session identity", 1);
|
|
171
|
-
const owner = context.db.get(`SELECT w.work_id, ws.id, ws.session_id FROM works w JOIN work_sessions ws ON ws.work_id = w.work_id JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND w.
|
|
177
|
+
const owner = context.db.get(`SELECT w.work_id, ws.id, ws.session_id FROM works w JOIN work_sessions ws ON ws.work_id = w.work_id JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND w.status IN ('running', 'paused') AND ws.status = 'running' AND s.harness = ? AND s.provider_session_id = ? ORDER BY ws.created_at DESC LIMIT 1`, [project.id, run.id, input.harness, input.nativeSessionId]);
|
|
172
178
|
if (!owner)
|
|
173
|
-
throw new AppError("recovery_owner_missing", "Recovery must retain
|
|
179
|
+
throw new AppError("recovery_owner_missing", "Recovery must retain an active coordinator WorkSession in its native Session", 1, { run_id: run.id });
|
|
174
180
|
if (context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND daemon_id = ? LIMIT 1", [project.id, input.daemonId]) || context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND b.daemon_id = ? LIMIT 1", [project.id, input.daemonId]))
|
|
175
181
|
throw new AppError("recovery_daemon_stale", "Replacement daemon already has historical lifecycle authority in this project", 1);
|
|
176
182
|
const packet = rootRecoveryPacket(context, run, guard, owner);
|
|
177
183
|
context.db.run("INSERT INTO run_recovery_bindings (recovery_id, work_id, prior_work_session_id, session_id, harness, provider_session_id, daemon_id, status, created_at, packet_json, packet_sha256) VALUES (?, ?, ?, ?, ?, ?, ?, 'prepared', ?, ?, ?)", [guard.recovery_id, owner.work_id, owner.id, owner.session_id, input.harness, input.nativeSessionId, input.daemonId, now, packet, crypto.createHash("sha256").update(packet).digest("hex")]);
|
|
178
|
-
running = context.db.all("SELECT work_id, started_at, status FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NOT NULL AND (status = 'running' OR (status = 'created' AND EXISTS (SELECT 1 FROM work_recovery_segments s WHERE s.work_id = works.work_id AND s.status = 'prepared'))) ORDER BY work_id", [project.id, run.id]);
|
|
184
|
+
running = context.db.all("SELECT work_id, started_at, status FROM works WHERE project_id = ? AND run_id = ? AND work_id <> ? AND parent_work_id IS NOT NULL AND (status = 'running' OR (status = 'created' AND EXISTS (SELECT 1 FROM work_recovery_segments s WHERE s.work_id = works.work_id AND s.status = 'prepared'))) ORDER BY work_id", [project.id, run.id, owner.work_id]);
|
|
179
185
|
for (const work of running)
|
|
180
186
|
context.db.run("UPDATE work_recovery_segments SET status = 'superseded', updated_at = ? WHERE work_id = ? AND status = 'prepared'", [now, work.work_id]);
|
|
181
187
|
for (const work of running) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"version": "0.9.0-beta.
|
|
3
|
+
"version": "0.9.0-beta.28",
|
|
4
4
|
"description": "Mechanical runtime CLI for dd-flow workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,16 +22,6 @@
|
|
|
22
22
|
"node": ">=26.0.0",
|
|
23
23
|
"pnpm": ">=10.0.0"
|
|
24
24
|
},
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
|
|
27
|
-
"typecheck": "tsc --noEmit",
|
|
28
|
-
"lint": "eslint . --max-warnings=0",
|
|
29
|
-
"test": "vitest run --pool=forks --no-file-parallelism",
|
|
30
|
-
"changeset": "changeset",
|
|
31
|
-
"version-packages": "changeset version",
|
|
32
|
-
"release": "pnpm run build && changeset publish",
|
|
33
|
-
"prepublishOnly": "node scripts/verify-release-build.mjs"
|
|
34
|
-
},
|
|
35
25
|
"devDependencies": {
|
|
36
26
|
"@changesets/cli": "^2.31.0",
|
|
37
27
|
"@eslint/js": "^9.39.1",
|
|
@@ -50,5 +40,14 @@
|
|
|
50
40
|
"publishConfig": {
|
|
51
41
|
"access": "public"
|
|
52
42
|
},
|
|
53
|
-
"license": "MIT"
|
|
54
|
-
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json && node scripts/generate-build-info.mjs && node scripts/copy-assets.mjs",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"lint": "eslint . --max-warnings=0",
|
|
48
|
+
"test": "vitest run --pool=forks --no-file-parallelism",
|
|
49
|
+
"changeset": "changeset",
|
|
50
|
+
"version-packages": "changeset version",
|
|
51
|
+
"release": "pnpm run build && changeset publish"
|
|
52
|
+
}
|
|
53
|
+
}
|