@deksden-com/dd-flow-cli 0.9.0-beta.52 → 0.9.0-beta.53
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 +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/help.js +13 -3
- package/dist/cli/run-cli.js +74 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -4
- package/dist/harness-runtime/bin/dd-zcode.mjs +13 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -19
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +20 -4
- package/dist/harness-runtime/lib/dd-zcode.mjs +85 -9
- package/dist/harness-runtime/lib/native-hook-command.mjs +33 -0
- package/dist/services/cli-operation-classifier.js +2 -0
- package/dist/services/eval-snapshots.js +1 -1
- package/dist/services/external-work-launch.js +5 -2
- package/dist/services/hooks.js +158 -113
- package/dist/services/lifecycle-command.js +63 -5
- package/dist/services/lifecycle-invocations.js +306 -0
- package/dist/services/merge-server.js +6 -4
- package/dist/services/recovery-snapshot-database.js +5 -1
- package/dist/services/run-controller-recovery.js +4 -1
- package/dist/services/run-controller.js +11 -3
- package/dist/services/run-recovery.js +11 -1
- package/dist/services/stage-pause.js +9 -5
- package/dist/services/vnext-code-review.js +5 -4
- package/dist/services/vnext-code.js +3 -2
- package/dist/services/vnext-merge.js +5 -4
- package/dist/services/vnext-plan-review.js +8 -7
- package/dist/services/vnext-plan.js +3 -2
- package/dist/services/vnext-protocolize.js +5 -4
- package/dist/services/vnext-specify.js +5 -4
- package/dist/services/work-registry.js +6 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.53
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Admit managed ZCode lifecycle commands through durable invocation receipts, with exact native identity, bounded waits, retained outcomes and explicit validated retries. Preserve recovery generations, cooperative settlement, immutable recovery evidence and scoped snapshots. Keep Grok native hook responses and stable parent identity, and qualify the installed ZCode native event contract without requiring child hooks. Use Node 26 for the guarded release workflow.
|
|
8
|
+
|
|
3
9
|
## 0.9.0-beta.52
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.53",
|
|
4
|
+
"cli_commit": "db14065f43f320bc69dacbec425cd04eac8236ae",
|
|
5
|
+
"built_at": "2026-09-13T02:52:44.434Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.0",
|
|
8
8
|
"commit": "ef349bf47cba1c987468e51d73a0dbadbd48dc1f",
|
package/dist/cli/help.js
CHANGED
|
@@ -583,17 +583,27 @@ handler resolves the registered project from cwd. The option is only a manual
|
|
|
583
583
|
override.
|
|
584
584
|
|
|
585
585
|
Managed homes isolate dd-flow hooks while sharing selected Codex state from the source home.`
|
|
586
|
+
],
|
|
587
|
+
[
|
|
588
|
+
"zcode hook",
|
|
589
|
+
`dd-flow zcode hook - handle a synchronous native ZCode PreToolUse callback
|
|
590
|
+
|
|
591
|
+
Usage:
|
|
592
|
+
dd-flow zcode hook handle --project-root <root> --json
|
|
593
|
+
|
|
594
|
+
The managed ZCode native hook supplies the callback on stdin. It records the
|
|
595
|
+
receipt before returning a rewritten tool input with --hook-event-id.`
|
|
586
596
|
],
|
|
587
597
|
[
|
|
588
598
|
"zcode event",
|
|
589
|
-
`dd-flow zcode event - ingest a zcode-acp session/update notification
|
|
599
|
+
`dd-flow zcode event - ingest a zcode-acp session/update notification as evidence
|
|
590
600
|
|
|
591
601
|
Usage:
|
|
592
602
|
dd-flow zcode event handle --project-root <root> --json
|
|
593
603
|
dd-flow zcode usage ingest --project-root <root> --json
|
|
594
604
|
|
|
595
|
-
Both payloads are read from stdin.
|
|
596
|
-
|
|
605
|
+
Both payloads are read from stdin. Native hooks own lifecycle admission; ACP
|
|
606
|
+
events and usage retain secondary evidence for RUN accounting.`
|
|
597
607
|
],
|
|
598
608
|
[
|
|
599
609
|
"droid event",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { quote } from "shell-quote";
|
|
4
|
+
import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, issueLifecycleInvocation, isRetryableLifecycleRejection, lifecycleInvocationScope, retryLifecycleInvocationCommand, settleLifecycleInvocation } from "../services/lifecycle-invocations.js";
|
|
3
5
|
import { createContext, createRouterContext } from "../runtime/context.js";
|
|
4
6
|
import { migrateStoreWriter } from "../storage/writer-migration.js";
|
|
5
7
|
import { helpForArgs } from "./help.js";
|
|
@@ -11,7 +13,7 @@ import { blockPlanItem, completePlanItem, getPlanStatus, skipPlanItem, startPlan
|
|
|
11
13
|
import { cancelMergeQueueJob, claimNextMergeJob, claimMergeJob, claimMergeBundle, completeMergeBundle, completeMergeJob, failMergeBundle, failMergeJob, getMergeBundleStatus, getMergeQueueStatus, noteMergeJob, waitNextMergeJob } from "../services/merge-queue.js";
|
|
12
14
|
import { getMergeWorkerStatus, oneShotMergeClaim, startMergeWorker, stopProjectMergeWorker } from "../services/merge-worker.js";
|
|
13
15
|
import { cleanupApply, cleanupScan } from "../services/cleanup.js";
|
|
14
|
-
import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleDroidEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
|
|
16
|
+
import { getCodexHomeStatus, getCodexHooksStatus, handleCodexHook, handleAgyEvent, handleDroidEvent, handleGrokEvent, handleOpenCodeEvent, handleZcodeHook, handleZcodeEvent, isolatedDdFlowHomeFromHookStdin, claimBootstrapHookEvent, findRecentMatchingHookEvent, stageStartMatchKey, initCodexHome, installCodexHooks, planCodexHome, printCodexHomeEnv, printCodexHooks, removeCodexHome, removeCodexHooks } from "../services/hooks.js";
|
|
15
17
|
import { bootstrapWorktree, closeWorktree, createWorktreeRecord, getWorktreeStatus, planWorktree } from "../services/worktrees.js";
|
|
16
18
|
import { acquireLaneLock, cancelLaneWaiter, checkLaneWorkspace, getLaneWaiters, getLaneStatus, heartbeatLaneLock, releaseLaneLock, setLaneWorkspace, waitAcquireLaneLock, waitForLaneLock } from "../services/lanes.js";
|
|
17
19
|
import { getProjectConfigStatus, setProjectConfigValue } from "../services/config.js";
|
|
@@ -73,6 +75,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
73
75
|
const output = parseOutputOptions(args);
|
|
74
76
|
const progress = progressForCommand(output.args);
|
|
75
77
|
let heartbeat;
|
|
78
|
+
let invocationSettlement;
|
|
76
79
|
try {
|
|
77
80
|
if (output.args.length === 1 && output.args[0] === "--version") {
|
|
78
81
|
const payload = getCliVersionReport();
|
|
@@ -115,9 +118,37 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
115
118
|
}
|
|
116
119
|
const dispatchContext = routed?.inProcess
|
|
117
120
|
? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(output.args, classification))
|
|
118
|
-
: createContext(env, databaseModeForCommand(output.args, classification));
|
|
121
|
+
: createContext({ ...env }, databaseModeForCommand(output.args, classification));
|
|
119
122
|
preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
|
|
120
|
-
|
|
123
|
+
dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
|
|
124
|
+
let dispatchArgs = output.args;
|
|
125
|
+
let retainedOutcome;
|
|
126
|
+
const invocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
|
|
127
|
+
if (invocationId) {
|
|
128
|
+
const command = quote(["dd-flow", ...output.args]);
|
|
129
|
+
const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
|
|
130
|
+
assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
|
|
131
|
+
if (admission.replay)
|
|
132
|
+
retainedOutcome = admission.outcome;
|
|
133
|
+
else {
|
|
134
|
+
invocationSettlement = { context: dispatchContext, id: invocationId };
|
|
135
|
+
dispatchArgs = [...output.args, "--hook-event-id", admission.eventKey];
|
|
136
|
+
dispatchContext.env.DD_FLOW_INVOCATION_SCOPE = JSON.stringify(lifecycleInvocationScope(dispatchContext, invocationId));
|
|
137
|
+
dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (retainedOutcome?.error) {
|
|
141
|
+
const error = retainedOutcome.error;
|
|
142
|
+
throw new AppError(error.code, error.message, error.exitCode, error.details);
|
|
143
|
+
}
|
|
144
|
+
const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output);
|
|
145
|
+
if (invocationSettlement) {
|
|
146
|
+
const completed = invocationSettlement;
|
|
147
|
+
invocationSettlement = undefined;
|
|
148
|
+
// A failure to persist an already completed result is an unknown reply,
|
|
149
|
+
// not a failed operation eligible for a new productive retry.
|
|
150
|
+
settleLifecycleInvocation(completed.context, completed.id, { result });
|
|
151
|
+
}
|
|
121
152
|
const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
|
|
122
153
|
const resultWithWarnings = addWarnings(result, refreshWarnings);
|
|
123
154
|
if (heartbeat)
|
|
@@ -136,6 +167,27 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
136
167
|
catch (error) {
|
|
137
168
|
if (heartbeat)
|
|
138
169
|
clearInterval(heartbeat);
|
|
170
|
+
if (invocationSettlement) {
|
|
171
|
+
// Optional retry preparation must never prevent recording the primary
|
|
172
|
+
// operation rejection. Otherwise a known failure becomes an unknown run.
|
|
173
|
+
if (isAppError(error) && isRetryableLifecycleRejection(error.code)) {
|
|
174
|
+
try {
|
|
175
|
+
error.details.retry_command = retryLifecycleInvocationCommand(invocationSettlement.context, invocationSettlement.id);
|
|
176
|
+
error.details.retry_instruction = "Correct only the evidenced defect, then use retry_command with the required result input. Reissuing the old command only reads its retained outcome.";
|
|
177
|
+
}
|
|
178
|
+
catch (retryError) {
|
|
179
|
+
error.details.retry_preparation_error = String(retryError);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: isAppError(error)
|
|
184
|
+
? { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details }
|
|
185
|
+
: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
|
|
186
|
+
}
|
|
187
|
+
catch (persistenceError) {
|
|
188
|
+
io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
139
191
|
const payload = isAppError(error)
|
|
140
192
|
? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
|
|
141
193
|
: { ok: false, error: { code: "unexpected", message: String(error) } };
|
|
@@ -520,6 +572,13 @@ function shortSha(value) {
|
|
|
520
572
|
async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
|
|
521
573
|
const [family, command, ...rest] = args;
|
|
522
574
|
const parsed = parseArgs(rest);
|
|
575
|
+
if (family === "zcode" && command === "invocation" && parsed.positional[0] === "prepare") {
|
|
576
|
+
const payload = JSON.parse(await readStdin(io.stdin));
|
|
577
|
+
if (typeof payload.command !== "string" || !payload.scope || typeof payload.scope !== "object")
|
|
578
|
+
throw new AppError("usage", "Expected command and managed scope on stdin", 2);
|
|
579
|
+
assertLifecycleInvocationCurrent(context, payload.scope, payload.command);
|
|
580
|
+
return { ok: true, ...issueLifecycleInvocation(context, payload.command, payload.scope) };
|
|
581
|
+
}
|
|
523
582
|
if (family === "storage" && command === "migrate-writer") {
|
|
524
583
|
if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
525
584
|
throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
|
|
@@ -1146,6 +1205,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1146
1205
|
if (family === "codex" && command === "hook") {
|
|
1147
1206
|
return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
|
|
1148
1207
|
}
|
|
1208
|
+
if (family === "zcode" && command === "hook") {
|
|
1209
|
+
return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
|
|
1210
|
+
}
|
|
1149
1211
|
if (family === "zcode" && command === "event") {
|
|
1150
1212
|
return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
|
|
1151
1213
|
}
|
|
@@ -2249,6 +2311,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
|
|
|
2249
2311
|
}
|
|
2250
2312
|
throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
|
|
2251
2313
|
}
|
|
2314
|
+
function dispatchZcodeHook(context, parsed, stdin) {
|
|
2315
|
+
if (requiredPosition(parsed, 0, "zcode hook action") !== "handle")
|
|
2316
|
+
throw new AppError("usage", "Unknown zcode hook action", 2);
|
|
2317
|
+
const result = handleZcodeHook(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
|
|
2318
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
|
|
2319
|
+
return { hookSpecificOutput: result.hookSpecificOutput };
|
|
2320
|
+
}
|
|
2321
|
+
return {};
|
|
2322
|
+
}
|
|
2252
2323
|
function dispatchGrokEvent(context, parsed, stdin) {
|
|
2253
2324
|
const action = requiredPosition(parsed, 0, "grok event action");
|
|
2254
2325
|
if (action === "handle") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { invokeNativeHook } from "../lib/native-hook-command.mjs";
|
|
4
4
|
import { cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-grok.mjs";
|
|
5
5
|
import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-grok-daemon.mjs";
|
|
6
6
|
|
|
@@ -9,9 +9,8 @@ function common(options) { return { bin: options["grok-bin"], journal: options.j
|
|
|
9
9
|
async function readStdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
|
|
10
10
|
async function hook(options) {
|
|
11
11
|
const stdin = await readStdin(); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id });
|
|
12
|
-
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
|
-
|
|
14
|
-
return { decision: "allow" };
|
|
12
|
+
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 });
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { durableDaemonStart, inspectDaemonOperation } from "../lib/daemon-operations.mjs";
|
|
3
|
+
import { invokeNativeHook } from "../lib/native-hook-command.mjs";
|
|
3
4
|
import { cancelChild, cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-zcode.mjs";
|
|
4
5
|
import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-zcode-daemon.mjs";
|
|
5
6
|
|
|
@@ -29,6 +30,15 @@ function common(options) {
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
async function readStdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
|
|
34
|
+
async function hook(options) {
|
|
35
|
+
const payload = JSON.parse((await readStdin()) || "{}");
|
|
36
|
+
const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId });
|
|
37
|
+
const command = options["dd-flow-bin"]; const args = ["zcode", "hook", "handle", "--project-root", options["project-root"], "--json"];
|
|
38
|
+
|
|
39
|
+
return invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } });
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
try {
|
|
33
43
|
const { positional, options } = parse(process.argv.slice(2)); const [family, command] = positional; let result;
|
|
34
44
|
const answers = options["answers-file"] ? JSON.parse(await promptFromFile(options["answers-file"])) : undefined;
|
|
@@ -39,6 +49,7 @@ try {
|
|
|
39
49
|
else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
|
|
40
50
|
else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
|
|
41
51
|
else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
|
|
52
|
+
else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
|
|
42
53
|
else if (family === "session") {
|
|
43
54
|
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") } : {}) };
|
|
44
55
|
// promptSessionWithBridge owns the meaningful deadline: it fails when its
|
|
@@ -56,8 +67,9 @@ try {
|
|
|
56
67
|
else if (command === "cancel-child") result = await cancelChild(params);
|
|
57
68
|
else if (command === "fork") result = await forkSession(params);
|
|
58
69
|
else throw new Error(`unknown session command: ${command}`);
|
|
59
|
-
} else throw new Error("usage: dd-zcode doctor | daemon start|status|stop | session create|prompt|inspect|status|resume|cancel|cancel-child|fork [options] --json");
|
|
70
|
+
} 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");
|
|
60
71
|
process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
|
|
61
72
|
} catch (error) {
|
|
73
|
+
if (process.argv[2] === "hook") { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); }
|
|
62
74
|
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);
|
|
63
75
|
}
|
|
@@ -73,25 +73,10 @@ async function writeHook(paths, config, entryPath) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
async function observeAcpToolCall(config, identity, message) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (update?.sessionUpdate !== "tool_call" || !sessionId || typeof command !== "string" || !command.includes("dd-flow")) return;
|
|
81
|
-
const eventId = update?._meta?.eventId ?? `${sessionId}:${update.toolCallId ?? createHash("sha256").update(command).digest("hex")}`;
|
|
82
|
-
const payload = {
|
|
83
|
-
hook_event_name: "PreToolUse",
|
|
84
|
-
tool_name: update?._meta?.["x.ai/tool"]?.name ?? "run_terminal_command",
|
|
85
|
-
tool_input: { command }, session_id: sessionId, event_id: eventId,
|
|
86
|
-
_meta: { ddGrok: identity }
|
|
87
|
-
};
|
|
88
|
-
const target = executable(config.ddFlowBin, ["grok", "event", "handle", "--project-root", config.projectRoot, "--json"]);
|
|
89
|
-
await new Promise((resolve, reject) => {
|
|
90
|
-
const child = spawn(target.command, target.args, { env: { ...process.env, DD_FLOW_HOME: config.ddFlowHome }, stdio: ["pipe", "ignore", "pipe"] }); let stderr = "";
|
|
91
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk)); child.on("error", reject);
|
|
92
|
-
child.on("close", (code) => code === 0 ? resolve() : reject(new DaemonError("grok_hook_failed", stderr || "dd-flow rejected Grok ACP tool event")));
|
|
93
|
-
child.stdin.end(`${JSON.stringify(payload)}\n`);
|
|
94
|
-
});
|
|
76
|
+
// Grok's synchronous native hook owns admission. This ACP mirror is
|
|
77
|
+
// observational only; forwarding it used to create a second receipt after
|
|
78
|
+
// the command had already run.
|
|
79
|
+
void config; void identity; void message;
|
|
95
80
|
}
|
|
96
81
|
|
|
97
82
|
async function copyAuth(paths, source) {
|
|
@@ -151,6 +151,9 @@ export async function startDaemon(options) {
|
|
|
151
151
|
}
|
|
152
152
|
await removeSocket(paths.socket, paths.dir);
|
|
153
153
|
const versions = await doctor({ bin: config.bin, zcodePath: config.zcodePath });
|
|
154
|
+
if (hasFlowContext && versions.compatible !== true) {
|
|
155
|
+
throw new DaemonError("zcode_lifecycle_unqualified", "ZCode native event identity is not qualified for managed CLI admission; inspect lifecycle_qualification before starting E2E.", false, { observed_runtime: versions.observed_runtime, lifecycle_qualification: versions.lifecycle_qualification });
|
|
156
|
+
}
|
|
154
157
|
const resourceProcess = await registerDaemonProcess(config, {
|
|
155
158
|
kind: "zcode-daemon", owner: `zcode:${path.basename(paths.dir)}`, operation: `zcode-daemon:${path.basename(paths.dir)}`,
|
|
156
159
|
stdout: paths.log, stderr: paths.log,
|
|
@@ -229,11 +232,18 @@ export class DaemonRuntime {
|
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
track(result, topology = result?.evidence?.subagents) {
|
|
232
|
-
if (result?.provider_session_id) { const existing = this.sessions.get(result.provider_session_id);
|
|
235
|
+
if (result?.provider_session_id) { const existing = this.sessions.get(result.provider_session_id); const suppliedParent = result.parent_provider_session_id;
|
|
236
|
+
if (existing && suppliedParent !== undefined && suppliedParent !== existing.parent_provider_session_id) {
|
|
237
|
+
throw new DaemonError("session_ancestry_conflict", "ZCode Session parent changed after it was recorded", false, { session_id: result.provider_session_id, expected_parent: existing.parent_provider_session_id, actual_parent: suppliedParent });
|
|
238
|
+
}
|
|
239
|
+
const parent = suppliedParent ?? existing?.parent_provider_session_id ?? null;
|
|
240
|
+
const parentRoot = parent ? this.sessions.get(parent)?.root_provider_session_id ?? null : null;
|
|
241
|
+
this.sessions.set(result.provider_session_id, {
|
|
233
242
|
provider_session_id: result.provider_session_id,
|
|
234
|
-
adapter_session_id: result.adapter_session_id ?? result.provider_session_id,
|
|
235
|
-
parent_provider_session_id:
|
|
236
|
-
|
|
243
|
+
adapter_session_id: result.adapter_session_id ?? existing?.adapter_session_id ?? result.provider_session_id,
|
|
244
|
+
parent_provider_session_id: parent,
|
|
245
|
+
root_provider_session_id: existing?.root_provider_session_id ?? (parent ? parentRoot : result.provider_session_id),
|
|
246
|
+
topology: topology ?? existing?.topology ?? null,
|
|
237
247
|
completed_turns: existing?.completed_turns ?? 0,
|
|
238
248
|
}); }
|
|
239
249
|
if (result?.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
|
|
@@ -289,6 +299,12 @@ export class DaemonRuntime {
|
|
|
289
299
|
active_tree: this.state.active_tree, active_operation: this.activeProductive,
|
|
290
300
|
sessions: [...this.sessions.values()], config: this.state.config,
|
|
291
301
|
};
|
|
302
|
+
if (operation === "hook.resolve") {
|
|
303
|
+
const sessionId = String(params.sessionId ?? "");
|
|
304
|
+
const session = this.sessions.get(sessionId);
|
|
305
|
+
if (!session?.root_provider_session_id) throw new DaemonError("hook_identity_unknown", "ZCode hook Session is not a tracked controlled Session", false, { session_id: sessionId });
|
|
306
|
+
return { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} };
|
|
307
|
+
}
|
|
292
308
|
if (operation === "session.create") return await this.productive(operation, async () => await createSessionWithBridge(this.bridge, this.options(params), this.initialized));
|
|
293
309
|
if (operation === "session.prompt") {
|
|
294
310
|
const result = await this.productive(operation, async () => { await this.requireSettled(params); return await promptSessionWithBridge(this.bridge, this.options(params)); });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { observedTimeout } from "./observation-clock.mjs";
|
|
2
|
+
import { invokeNativeHook } from "./native-hook-command.mjs";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
3
5
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import readline from "node:readline";
|
|
@@ -53,6 +55,7 @@ class Journal {
|
|
|
53
55
|
if (!this.file) return;
|
|
54
56
|
const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload });
|
|
55
57
|
this.pending = this.pending.then(() => appendFile(this.file, `${line}\n`));
|
|
58
|
+
return this.pending;
|
|
56
59
|
}
|
|
57
60
|
async flush() { await this.pending; }
|
|
58
61
|
}
|
|
@@ -64,6 +67,7 @@ export class AcpBridge {
|
|
|
64
67
|
this.pending = new Map();
|
|
65
68
|
this.sessionActivity = new Map();
|
|
66
69
|
this.notifications = Promise.resolve();
|
|
70
|
+
this.lifecycleNotifications = new Set();
|
|
67
71
|
this.notificationError = null;
|
|
68
72
|
this.bridgeStderr = "";
|
|
69
73
|
this.toolCalls = new Map();
|
|
@@ -127,6 +131,16 @@ export class AcpBridge {
|
|
|
127
131
|
this.assistantText.set(sessionId, `${this.assistantText.get(sessionId) ?? ""}${message.params.update.content.text ?? ""}`);
|
|
128
132
|
}
|
|
129
133
|
this.observeToolCall(message);
|
|
134
|
+
if (message.method === "session/update" && this.options.onLifecycleNotification) {
|
|
135
|
+
const observer = this.options.onLifecycleNotification;
|
|
136
|
+
const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
|
|
137
|
+
this.notificationError ??= error;
|
|
138
|
+
await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message,
|
|
139
|
+
session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" });
|
|
140
|
+
});
|
|
141
|
+
this.lifecycleNotifications.add(pending);
|
|
142
|
+
void pending.then(() => this.lifecycleNotifications.delete(pending), () => this.lifecycleNotifications.delete(pending));
|
|
143
|
+
}
|
|
130
144
|
if (message.id !== undefined && message.method) { void this.answer(message).catch((error) => this.rejectAll(error)); return; }
|
|
131
145
|
if (message.id !== undefined) {
|
|
132
146
|
const pending = this.pending.get(message.id);
|
|
@@ -148,7 +162,14 @@ export class AcpBridge {
|
|
|
148
162
|
}
|
|
149
163
|
if (message.method) this.notifications = this.notifications
|
|
150
164
|
.then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message); })
|
|
151
|
-
.catch((error) => {
|
|
165
|
+
.catch(async (error) => {
|
|
166
|
+
const failure = { code: error.code ?? "notification_observation_failed", message: error.message, session_id: message.params?.sessionId ?? null, tool_call_id: message.params?.update?.toolCallId ?? null, event_time: now(), delivery_stage: "acp_evidence", completeness: "incomplete" };
|
|
167
|
+
try { await this.journal.write("notification_failure", failure); }
|
|
168
|
+
catch (persistenceError) { process.stderr.write(`${JSON.stringify({ ...failure, persistence_error: persistenceError.message })}\n`); }
|
|
169
|
+
// Evidence loss does not retroactively revoke a committed native hook.
|
|
170
|
+
// Permission/profile integrity is a separate, still-fatal contract.
|
|
171
|
+
if (error.code === "profile_integrity_violation") { this.notificationError ??= error; this.rejectAll(error); }
|
|
172
|
+
});
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
observeToolCall(message) {
|
|
@@ -249,7 +270,7 @@ export class AcpBridge {
|
|
|
249
270
|
notify(method, params = {}) { this.send({ jsonrpc: "2.0", method, params }); }
|
|
250
271
|
send(message) { this.journal.write("outbound", message); this.child.stdin.write(`${JSON.stringify(message)}\n`); }
|
|
251
272
|
rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error); } this.pending.clear(); }
|
|
252
|
-
async flush() { await this.notifications; await this.journal.flush(); if (this.notificationError) throw this.notificationError; }
|
|
273
|
+
async flush() { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); if (this.notificationError) throw this.notificationError; }
|
|
253
274
|
async close() {
|
|
254
275
|
let flushError = null;
|
|
255
276
|
try { await this.flush(); } catch (error) { flushError = error; }
|
|
@@ -437,8 +458,6 @@ async function withBridge(options, operation) {
|
|
|
437
458
|
}
|
|
438
459
|
|
|
439
460
|
function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
440
|
-
if (!options.ddFlowBin) return undefined;
|
|
441
|
-
const projectRoot = absolute(options.projectRoot, "--project-root");
|
|
442
461
|
return async (notification) => {
|
|
443
462
|
if (notification.method !== "session/update") return;
|
|
444
463
|
const update = notification.params?.update;
|
|
@@ -447,10 +466,52 @@ function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
|
447
466
|
const childId = notification.params?.update?._meta?.zcodeRuntime?.childSessionId;
|
|
448
467
|
const observed = bridge?.modelProfiles?.get(childId ?? rootProviderSessionId) ?? {};
|
|
449
468
|
if (childId && !observed.model) await observeModel({ journal: options.journal, harness: "zcode-acp", sessionId: childId, parentSessionId: rootProviderSessionId, requested: profile(options), observed, source: "zcode.child_tool_event", evidence: "unavailable" });
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
469
|
+
// Native PreToolUse has already admitted (or denied) the command. ACP is
|
|
470
|
+
// intentionally only journal/model evidence: its serialized delivery can
|
|
471
|
+
// lag behind execution and must never mint a second lifecycle receipt.
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Preserve native tool ownership independently of slow secondary evidence.
|
|
476
|
+
* Rehydration uses this same session's retained inbound journal, not model text. */
|
|
477
|
+
export async function zcodeInvocationObserver(options, rootProviderSessionId, send = invokeNativeHook) {
|
|
478
|
+
if (!options.ddFlowBin) return async () => {};
|
|
479
|
+
const owners = new Map();
|
|
480
|
+
const remember = message => {
|
|
481
|
+
const update = message.params?.update;
|
|
482
|
+
if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
|
|
483
|
+
if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
|
|
484
|
+
const native = update._meta?.zcodeRuntime ?? {};
|
|
485
|
+
if (native.source === "subagent" && !native.childSessionId) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
|
|
486
|
+
const session = native.childSessionId ?? rootProviderSessionId;
|
|
487
|
+
const key = update.toolCallId;
|
|
488
|
+
if (!key || typeof session !== "string") return null;
|
|
489
|
+
if (owners.has(key) && owners.get(key) !== session) throw Object.assign(new Error("Native tool id has conflicting Session ownership"), { code: "zcode_tool_identity_conflict" });
|
|
490
|
+
owners.set(key, session);
|
|
491
|
+
return { update, native, session };
|
|
492
|
+
};
|
|
493
|
+
if (options.journal) {
|
|
494
|
+
try {
|
|
495
|
+
const contents = await readFile(options.journal, "utf8");
|
|
496
|
+
for (const line of contents.split("\n").filter(Boolean)) {
|
|
497
|
+
const event = JSON.parse(line);
|
|
498
|
+
if (event.kind === "inbound") remember(event.payload);
|
|
499
|
+
}
|
|
500
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
501
|
+
}
|
|
502
|
+
return async message => {
|
|
503
|
+
const facts = remember(message);
|
|
504
|
+
if (!facts || !options.ddFlowBin) return;
|
|
505
|
+
const { update, native, session } = facts;
|
|
506
|
+
const command = update.rawInput?.command ?? update.rawInput?.cmd;
|
|
507
|
+
const toolName = update._meta?.claudeCode?.toolName;
|
|
508
|
+
if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string" || !/(?:^|\s)--invocation-id\s/.test(command)) return;
|
|
509
|
+
const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
|
|
510
|
+
if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
|
|
511
|
+
await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
|
|
512
|
+
args: ["zcode", "event", "handle", "--project-root", options.projectRoot ?? options.cwd, "--json"],
|
|
513
|
+
payload: { ...message, _meta: { ddZcode: { daemonId: options.daemonId, rootProviderSessionId, parentProviderSessionId: parent } } },
|
|
514
|
+
});
|
|
454
515
|
};
|
|
455
516
|
}
|
|
456
517
|
|
|
@@ -480,13 +541,26 @@ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
|
|
|
480
541
|
await runWithStdin(options.ddFlowBin, ["zcode", "usage", "ingest", "--project-root", projectRoot, "--json"], `${JSON.stringify(payload)}\n`, options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") } : {});
|
|
481
542
|
}
|
|
482
543
|
|
|
544
|
+
export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContract) {
|
|
545
|
+
// Behavioural evidence: dd-eval production-invocations probe, 2026-09-13.
|
|
546
|
+
// Pin both providers of native event identity, not a string in the bundle.
|
|
547
|
+
const qualified = sha256 === "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8"
|
|
548
|
+
&& bridgeCommit === "43f654bccdbb1aa4f4fb4617f7315c4336dbcde0" && harnessContract === ZCODE_HARNESS_CONTRACT;
|
|
549
|
+
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
550
|
+
capabilities: qualified ? ["root", "concurrent_children", "child_continuation"] : [],
|
|
551
|
+
native_nested_children: false, reason: qualified ? null : "native_event_contract_unqualified" };
|
|
552
|
+
}
|
|
553
|
+
|
|
483
554
|
export async function doctor(options = {}) {
|
|
484
555
|
const zcodeAcp = await commandOutput(options.bin, ["--version"]);
|
|
485
556
|
const zcodeAcpCommit = await commandOutput(options.bin, ["--dd-harness-commit"]);
|
|
486
557
|
const ddHarness = await commandOutput(options.bin, ["--dd-harness-version"]);
|
|
487
558
|
const zcodePath = options.zcodePath ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
|
|
488
559
|
const zcode = await commandOutput(zcodePath, ["--version"]);
|
|
489
|
-
|
|
560
|
+
const native = await readFile(zcodePath);
|
|
561
|
+
const sha256 = createHash("sha256").update(native).digest("hex");
|
|
562
|
+
const qualification = zcodeLifecycleQualification(sha256, zcodeAcpCommit, ddHarness);
|
|
563
|
+
return { compatible: qualification.status === "qualified", lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
|
|
490
564
|
}
|
|
491
565
|
|
|
492
566
|
export async function createSession(options) {
|
|
@@ -527,6 +601,8 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
527
601
|
let forward;
|
|
528
602
|
bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
|
|
529
603
|
const identity = await controlledIdentity(bridge, { ...options, cwd }); forward = flowForwarder(options, identity.providerSessionId, bridge);
|
|
604
|
+
await bridge.journal.flush();
|
|
605
|
+
bridge.configure({ onLifecycleNotification: await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId) });
|
|
530
606
|
const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
|
|
531
607
|
options.assertDispatch?.();
|
|
532
608
|
await forwardUsage(options, identity.providerSessionId, await bridge.request("zcode/session/usage", { sessionId: identity.adapterSessionId }), bridge.toolSummary(identity.adapterSessionId));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** Preserve the native hook protocol across the CLI process boundary. */
|
|
4
|
+
export async function invokeNativeHook({ bin, args, payload, home, timeoutMs = 15_000 }) {
|
|
5
|
+
const script = /\.[cm]?js$/.test(bin);
|
|
6
|
+
const output = await new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(script ? process.execPath : bin, [...(script ? [bin] : []), ...args], {
|
|
8
|
+
env: { ...process.env, DD_FLOW_HOME: home }, stdio: ["pipe", "pipe", "pipe"]
|
|
9
|
+
});
|
|
10
|
+
let stdout = "", stderr = "", failure;
|
|
11
|
+
const stop = error => { failure ??= error; child.kill("SIGKILL"); };
|
|
12
|
+
const timer = setTimeout(() => stop(Object.assign(new Error("Native hook admission timed out"), { code: "native_hook_timeout" })), timeoutMs);
|
|
13
|
+
child.stdout.setEncoding("utf8").on("data", chunk => { stdout += chunk; if (stdout.length > 1_048_576) stop(new Error("Native hook response exceeds protocol limit")); });
|
|
14
|
+
child.stderr.setEncoding("utf8").on("data", chunk => { stderr = (stderr + chunk).slice(-65_536); });
|
|
15
|
+
child.stdin.on("error", error => { failure ??= error; });
|
|
16
|
+
child.on("error", error => { clearTimeout(timer); reject(error); });
|
|
17
|
+
child.on("close", code => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
if (failure) return reject(failure);
|
|
20
|
+
if (code !== 0) {
|
|
21
|
+
let record;
|
|
22
|
+
try { record = JSON.parse(stderr.trim()).error; } catch { /* Non-JSON stderr remains diagnostic evidence. */ }
|
|
23
|
+
return reject(Object.assign(new Error(record?.message ?? (stderr.trim() || "Native hook admission failed")), { code: record?.code ?? "native_hook_failed", details: record?.details }));
|
|
24
|
+
}
|
|
25
|
+
resolve(stdout);
|
|
26
|
+
});
|
|
27
|
+
child.stdin.end(`${JSON.stringify(payload)}\n`);
|
|
28
|
+
});
|
|
29
|
+
let result;
|
|
30
|
+
try { result = JSON.parse(output); } catch { throw Object.assign(new Error("Invalid native hook JSON response"), { code: "native_hook_response_invalid" }); }
|
|
31
|
+
if (!result || typeof result !== "object" || Array.isArray(result) || result.ok === false) throw Object.assign(new Error("Native hook did not return a successful protocol object"), { code: "native_hook_response_invalid" });
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -104,6 +104,8 @@ function isRouterNative(args) {
|
|
|
104
104
|
if (first === "storage" && args[1] === "migrate-writer")
|
|
105
105
|
return true;
|
|
106
106
|
return (first === "codex" && args[1] === "hook" && args[2] === "handle")
|
|
107
|
+
|| (first === "zcode" && args[1] === "hook" && args[2] === "handle")
|
|
108
|
+
|| (first === "zcode" && args[1] === "invocation" && args[2] === "prepare")
|
|
107
109
|
|| (first === "zcode" && args[1] === "event" && args[2] === "handle")
|
|
108
110
|
|| (first === "zcode" && args[1] === "usage" && args[2] === "ingest")
|
|
109
111
|
|| (first === "grok" && args[1] === "event" && args[2] === "handle")
|
|
@@ -391,7 +391,7 @@ function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
|
|
|
391
391
|
throw new AppError("snapshot_control_requires_recovery", "A fenced RUN requires explicit recovery, not a stage-entry fork", 1);
|
|
392
392
|
if (context.db.get("SELECT 1 FROM merge_requests WHERE status IN ('dispatching','active','waiting_user','action_required','recovery_required') LIMIT 1"))
|
|
393
393
|
throw new AppError("snapshot_execution_active", "A stage-entry fork cannot discard an unresolved MERGE execution", 1);
|
|
394
|
-
const detachedTables = ["run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
|
|
394
|
+
const detachedTables = ["lifecycle_invocations", "run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
|
|
395
395
|
const runTables = Object.fromEntries([...detachedTables, "runs", "merge_requests", "sessions", "work_sessions"].map(table => [table, context.db.all(`SELECT * FROM ${table}`)]));
|
|
396
396
|
const resourceFile = path.join(context.ddFlowHome, "runtime.sqlite");
|
|
397
397
|
const resources = fs.existsSync(resourceFile) ? new DatabaseSync(resourceFile) : null;
|
|
@@ -10,6 +10,7 @@ import { resolveExecutionPolicy, runtimeHarness, validateExecutionOverride } fro
|
|
|
10
10
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
11
11
|
import { assertWorkLaunchReady, workStartCommand } from "./work-registry.js";
|
|
12
12
|
import { initCodexHome } from "./hooks.js";
|
|
13
|
+
import { managedInvocationContext } from "./lifecycle-invocations.js";
|
|
13
14
|
/**
|
|
14
15
|
* Creates an external provider root for one already-created Work. The launch
|
|
15
16
|
* is not a Work claim: only the worker's trusted `work start` hook binds it.
|
|
@@ -48,7 +49,7 @@ export async function launchExternalWork(context, input) {
|
|
|
48
49
|
fs.mkdirSync(root, { recursive: true });
|
|
49
50
|
const now = context.now();
|
|
50
51
|
const flowExecutable = prepareHarnessFlowExecutable(context, root);
|
|
51
|
-
const env = { ...context.env, DD_FLOW_BIN: flowExecutable };
|
|
52
|
+
const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined };
|
|
52
53
|
// An external Codex root is a fresh thread, so it does not inherit the
|
|
53
54
|
// coordinator's managed PreToolUse hook. Its first `work start` must emit
|
|
54
55
|
// that hook event to bind the Work to the physical session.
|
|
@@ -57,7 +58,7 @@ export async function launchExternalWork(context, input) {
|
|
|
57
58
|
initCodexHome(context, { projectRoot: run.project_root, profile: `external-${operationId}`, targetHome: codexHome, ...(context.env.CODEX_HOME ? { sourceHome: context.env.CODEX_HOME } : {}) });
|
|
58
59
|
env.CODEX_HOME = codexHome;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
let runtimeContext = { ...context, env };
|
|
61
62
|
const runtimeOwner = { schema_id: "dd-flow/runtime-owner@1", owner_id: `run:${work.run_id}:work:${work.work_id}`, role: "worker", project_id: work.project_id, run_id: work.run_id, work_id: work.work_id, operation_id: operationId, state_dir: root, dd_flow_home: context.ddFlowHome, dd_flow_bin: flowExecutable, resource_home: context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, budget: runtimeBudgetForRun(context, work.project_id, work.run_id) };
|
|
62
63
|
const executable = resolveHarnessCommand(context.ddFlowHome, runtimeHarness(resolved.worker), "adapter");
|
|
63
64
|
context.db.run("INSERT INTO work_launches (operation_id, work_id, profile_json, profile_hash, status, state_dir, journal_path, provider_session_id, adapter_receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'launching', ?, ?, NULL, NULL, ?, ?)", [operationId, work.work_id, JSON.stringify({ ...resolved, runtime_adapter: { executable, owner: runtimeOwner } }), resolved.profile_hash, root, journal, now, now]);
|
|
@@ -73,6 +74,8 @@ export async function launchExternalWork(context, input) {
|
|
|
73
74
|
throw new AppError("harness_adapter_invalid", "Harness adapter did not return a typed Session id", 1, { work_id: work.work_id, harness: runtimeHarness(resolved.worker) });
|
|
74
75
|
// Retain the physical root before any subsequent guard or prompt can fail.
|
|
75
76
|
context.db.run("UPDATE work_launches SET provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [sessionId, JSON.stringify({ create: created }), context.now(), operationId]);
|
|
77
|
+
runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(resolved.worker), stateDir: root, sessionId,
|
|
78
|
+
projectRoot: run.project_root, runId: work.run_id, generation });
|
|
76
79
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
77
80
|
assertWorkLaunchReady(context, work.work_id);
|
|
78
81
|
const promptFile = path.join(root, "launch.md");
|