@deksden-com/dd-flow-cli 0.9.0-beta.26 → 0.9.0-beta.27
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 +7 -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 +22 -0
- 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,12 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.27
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Resume retained native sessions from their active coordinator WorkSession, preserving the root launch record and rejecting lifecycle receipts from retired daemons.
|
|
8
|
+
- Propagate Antigravity lifecycle hooks to direct native children while preserving workspace hooks, and settle confirmed Codex system-error turns without losing provider failure evidence.
|
|
9
|
+
|
|
3
10
|
## 0.9.0-beta.26
|
|
4
11
|
|
|
5
12
|
### 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.27",
|
|
4
|
+
"cli_commit": "f619ffc1750da72ea36703933265005be43a5401",
|
|
5
|
+
"built_at": "2026-09-07T07:04:13.513Z",
|
|
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) {
|
|
@@ -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.27",
|
|
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
|
+
}
|