@deksden-com/dd-flow-cli 0.9.0-beta.52 → 0.9.0-beta.54
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 +12 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/help.js +13 -3
- package/dist/cli/run-cli.js +92 -5
- 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 +101 -10
- package/dist/harness-runtime/lib/native-hook-command.mjs +33 -0
- package/dist/services/cli-operation-classifier.js +7 -1
- 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 +371 -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 +19 -7
- 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 +8 -5
- package/dist/services/work-registry.js +6 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.54
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Preserve current lifecycle command input, atomically settle retry authority, select current resume commands, and stop ZCode productive admission immediately on fatal identity errors. Reject ambiguous result inputs and keep diagnostic commands read-only.
|
|
8
|
+
|
|
9
|
+
## 0.9.0-beta.53
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 0.9.0-beta.52
|
|
4
16
|
|
|
5
17
|
### 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.54",
|
|
4
|
+
"cli_commit": "eb2cb9746a9ee386d40f63791f5dbe0847bf5017",
|
|
5
|
+
"built_at": "2026-09-13T12:29:37.211Z",
|
|
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, lifecycleInvocationScope, settleLifecycleInvocation, settleLifecycleRejection } 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";
|
|
@@ -70,9 +72,18 @@ const defaultIo = {
|
|
|
70
72
|
};
|
|
71
73
|
const flagValue = "__dd_flow_flag__";
|
|
72
74
|
export async function runCli(args, io = defaultIo, env = process.env) {
|
|
73
|
-
|
|
75
|
+
let output;
|
|
76
|
+
try {
|
|
77
|
+
output = parseOutputOptions(args);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
writeJson(io.stderr, { ok: false, error: { code: "usage", message } });
|
|
82
|
+
return 2;
|
|
83
|
+
}
|
|
74
84
|
const progress = progressForCommand(output.args);
|
|
75
85
|
let heartbeat;
|
|
86
|
+
let invocationSettlement;
|
|
76
87
|
try {
|
|
77
88
|
if (output.args.length === 1 && output.args[0] === "--version") {
|
|
78
89
|
const payload = getCliVersionReport();
|
|
@@ -87,6 +98,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
87
98
|
io.stdout.write(`${help}\n`);
|
|
88
99
|
return 0;
|
|
89
100
|
}
|
|
101
|
+
assertSingletonOptions(output.args);
|
|
90
102
|
if (io.stdin?.isTTY && ["work", "stage"].includes(output.args[0] ?? "") && output.args[1] === "finish"
|
|
91
103
|
&& hasOption(parseArgs(output.args.slice(2)), "result-stdin")) {
|
|
92
104
|
throw new AppError("usage", "--result-stdin requires non-interactive input. Pipe or redirect the complete JSON into this command, or use --result-file; interactive terminals can truncate long JSON and wait indefinitely for EOF.", 2);
|
|
@@ -110,14 +122,44 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
110
122
|
if (routed?.routed && !routed.inProcess) {
|
|
111
123
|
if (heartbeat)
|
|
112
124
|
clearInterval(heartbeat);
|
|
113
|
-
emitHumanProgress(io, output, progress, "done");
|
|
125
|
+
emitHumanProgress(io, output, progress, routed.exitCode === 0 ? "done" : "failed");
|
|
126
|
+
if (output.progressJsonl && routed.exitCode !== 0)
|
|
127
|
+
writeProgressJsonl(io, "failed", "Routed engine failed", { exit_code: routed.exitCode });
|
|
114
128
|
return routed.exitCode;
|
|
115
129
|
}
|
|
116
130
|
const dispatchContext = routed?.inProcess
|
|
117
131
|
? 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));
|
|
132
|
+
: createContext({ ...env }, databaseModeForCommand(output.args, classification));
|
|
119
133
|
preflightCliCompatibility(dispatchContext, output.args, env, scopeProjectRoot);
|
|
120
|
-
|
|
134
|
+
dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
|
|
135
|
+
let dispatchArgs = output.args;
|
|
136
|
+
let retainedOutcome;
|
|
137
|
+
const invocationId = optionalOption(parseArgs(output.args.slice(2)), "invocation-id");
|
|
138
|
+
if (invocationId) {
|
|
139
|
+
const command = quote(["dd-flow", ...output.args]);
|
|
140
|
+
const admission = await awaitLifecycleInvocation(dispatchContext, { id: invocationId, command,
|
|
141
|
+
assertCurrent: scope => assertLifecycleInvocationCurrent(dispatchContext, scope, command) });
|
|
142
|
+
if (admission.replay)
|
|
143
|
+
retainedOutcome = admission.outcome;
|
|
144
|
+
else {
|
|
145
|
+
invocationSettlement = { context: dispatchContext, id: invocationId };
|
|
146
|
+
dispatchArgs = [...output.args, "--hook-event-id", admission.eventKey];
|
|
147
|
+
dispatchContext.env.DD_FLOW_INVOCATION_SCOPE = JSON.stringify(lifecycleInvocationScope(dispatchContext, invocationId));
|
|
148
|
+
dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (retainedOutcome?.error) {
|
|
152
|
+
const error = retainedOutcome.error;
|
|
153
|
+
throw new AppError(error.code, error.message, error.exitCode, error.details);
|
|
154
|
+
}
|
|
155
|
+
const result = retainedOutcome ? retainedOutcome.result : await dispatch(dispatchArgs, dispatchContext, io, scopeProjectRoot, classification, output);
|
|
156
|
+
if (invocationSettlement) {
|
|
157
|
+
const completed = invocationSettlement;
|
|
158
|
+
invocationSettlement = undefined;
|
|
159
|
+
// A failure to persist an already completed result is an unknown reply,
|
|
160
|
+
// not a failed operation eligible for a new productive retry.
|
|
161
|
+
settleLifecycleInvocation(completed.context, completed.id, { result });
|
|
162
|
+
}
|
|
121
163
|
const refreshWarnings = refreshDashboardsAfterMutation(dispatchContext, output.args, result, scopeProjectRoot);
|
|
122
164
|
const resultWithWarnings = addWarnings(result, refreshWarnings);
|
|
123
165
|
if (heartbeat)
|
|
@@ -136,6 +178,17 @@ export async function runCli(args, io = defaultIo, env = process.env) {
|
|
|
136
178
|
catch (error) {
|
|
137
179
|
if (heartbeat)
|
|
138
180
|
clearInterval(heartbeat);
|
|
181
|
+
if (invocationSettlement) {
|
|
182
|
+
try {
|
|
183
|
+
if (isAppError(error))
|
|
184
|
+
settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
|
|
185
|
+
else
|
|
186
|
+
settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
|
|
187
|
+
}
|
|
188
|
+
catch (persistenceError) {
|
|
189
|
+
io.stderr.write(`Invocation settlement persistence failed: ${String(persistenceError)}\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
139
192
|
const payload = isAppError(error)
|
|
140
193
|
? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
|
|
141
194
|
: { ok: false, error: { code: "unexpected", message: String(error) } };
|
|
@@ -520,6 +573,13 @@ function shortSha(value) {
|
|
|
520
573
|
async function dispatch(args, context, io, scopeProjectRoot = null, classification = classifyCliOperation(args, context.env), output = parseOutputOptions(args)) {
|
|
521
574
|
const [family, command, ...rest] = args;
|
|
522
575
|
const parsed = parseArgs(rest);
|
|
576
|
+
if (family === "zcode" && command === "invocation" && parsed.positional[0] === "prepare") {
|
|
577
|
+
const payload = JSON.parse(await readStdin(io.stdin));
|
|
578
|
+
if (typeof payload.command !== "string" || !payload.scope || typeof payload.scope !== "object")
|
|
579
|
+
throw new AppError("usage", "Expected command and managed scope on stdin", 2);
|
|
580
|
+
assertLifecycleInvocationCurrent(context, payload.scope, payload.command);
|
|
581
|
+
return { ok: true, ...issueLifecycleInvocation(context, payload.command, payload.scope) };
|
|
582
|
+
}
|
|
523
583
|
if (family === "storage" && command === "migrate-writer") {
|
|
524
584
|
if (parsed.positional.length || [...parsed.options.keys()].some(key => !["database", "backup", "offline"].includes(key)) || [...parsed.options.values()].some(values => values.length !== 1))
|
|
525
585
|
throw new AppError("usage", "Usage: dd-flow storage migrate-writer --database ABSOLUTE_DB --backup NEW_ABSOLUTE_FILE --offline", 2);
|
|
@@ -1146,6 +1206,9 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
|
|
|
1146
1206
|
if (family === "codex" && command === "hook") {
|
|
1147
1207
|
return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
|
|
1148
1208
|
}
|
|
1209
|
+
if (family === "zcode" && command === "hook") {
|
|
1210
|
+
return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
|
|
1211
|
+
}
|
|
1149
1212
|
if (family === "zcode" && command === "event") {
|
|
1150
1213
|
return dispatchZcodeEvent(context, parsed, await readStdin(io.stdin));
|
|
1151
1214
|
}
|
|
@@ -2249,6 +2312,15 @@ function dispatchZcodeEvent(context, parsed, stdin) {
|
|
|
2249
2312
|
}
|
|
2250
2313
|
throw new AppError("usage", `Unknown zcode event action: ${action}`, 2);
|
|
2251
2314
|
}
|
|
2315
|
+
function dispatchZcodeHook(context, parsed, stdin) {
|
|
2316
|
+
if (requiredPosition(parsed, 0, "zcode hook action") !== "handle")
|
|
2317
|
+
throw new AppError("usage", "Unknown zcode hook action", 2);
|
|
2318
|
+
const result = handleZcodeHook(context, { projectRoot: requiredOption(parsed, "project-root"), stdin });
|
|
2319
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
|
|
2320
|
+
return { hookSpecificOutput: result.hookSpecificOutput };
|
|
2321
|
+
}
|
|
2322
|
+
return {};
|
|
2323
|
+
}
|
|
2252
2324
|
function dispatchGrokEvent(context, parsed, stdin) {
|
|
2253
2325
|
const action = requiredPosition(parsed, 0, "grok event action");
|
|
2254
2326
|
if (action === "handle") {
|
|
@@ -2371,6 +2443,21 @@ function parseFlowFlagOverrides(values) {
|
|
|
2371
2443
|
function parseArgs(args) {
|
|
2372
2444
|
return parseCommandArgs(args);
|
|
2373
2445
|
}
|
|
2446
|
+
/** Most CLI options name one authority, path or target. Silently taking the
|
|
2447
|
+
* last duplicate can widen a mutation after an operator typo. Keep only the
|
|
2448
|
+
* options consumed as intentional arrays repeatable. */
|
|
2449
|
+
function assertSingletonOptions(args) {
|
|
2450
|
+
const repeatable = new Set(["alias", "flag", "hide", "check-ref", "evidence", "from-check", "from-finding", "from-unresolved", "on", "origin-work"]);
|
|
2451
|
+
const parsed = parseCommandArgs(args);
|
|
2452
|
+
if (args[0] === "stage" && args[1] === "finish"
|
|
2453
|
+
&& ["result-file", "semantic-file", "data", "result-stdin"].filter(name => parsed.options.has(name)).length > 1) {
|
|
2454
|
+
throw new AppError("usage", "Stage finish requires exactly one result input; result-file, semantic-file, data and result-stdin are mutually exclusive", 2);
|
|
2455
|
+
}
|
|
2456
|
+
for (const [name, values] of parsed.options) {
|
|
2457
|
+
if (!repeatable.has(name) && values.length > 1)
|
|
2458
|
+
throw new AppError("usage", `--${name} may be supplied only once`, 2);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2374
2461
|
function requiredPosition(parsed, index, label) {
|
|
2375
2462
|
const value = parsed.positional[index];
|
|
2376
2463
|
if (!value) {
|
|
@@ -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";
|
|
@@ -9,6 +11,9 @@ import { checkObservedProfile, observeModel, observedFields } from "./model-obse
|
|
|
9
11
|
const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
|
|
10
12
|
|
|
11
13
|
function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
|
|
14
|
+
function fatalLifecycleError(error) {
|
|
15
|
+
return ["invocation_unknown", "invocation_identity_mismatch", "invocation_identity_conflict", "invocation_receipt_missing", "invocation_command_mismatch", "invocation_scope_mismatch", "invocation_generation_stale", "invocation_expired", "zcode_parent_identity_missing", "zcode_tool_identity_conflict"].includes(error?.code);
|
|
16
|
+
}
|
|
12
17
|
function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
|
|
13
18
|
function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
|
|
14
19
|
function now() { return new Date().toISOString(); }
|
|
@@ -53,6 +58,7 @@ class Journal {
|
|
|
53
58
|
if (!this.file) return;
|
|
54
59
|
const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload });
|
|
55
60
|
this.pending = this.pending.then(() => appendFile(this.file, `${line}\n`));
|
|
61
|
+
return this.pending;
|
|
56
62
|
}
|
|
57
63
|
async flush() { await this.pending; }
|
|
58
64
|
}
|
|
@@ -64,6 +70,7 @@ export class AcpBridge {
|
|
|
64
70
|
this.pending = new Map();
|
|
65
71
|
this.sessionActivity = new Map();
|
|
66
72
|
this.notifications = Promise.resolve();
|
|
73
|
+
this.lifecycleNotifications = new Set();
|
|
67
74
|
this.notificationError = null;
|
|
68
75
|
this.bridgeStderr = "";
|
|
69
76
|
this.toolCalls = new Map();
|
|
@@ -127,6 +134,24 @@ export class AcpBridge {
|
|
|
127
134
|
this.assistantText.set(sessionId, `${this.assistantText.get(sessionId) ?? ""}${message.params.update.content.text ?? ""}`);
|
|
128
135
|
}
|
|
129
136
|
this.observeToolCall(message);
|
|
137
|
+
if (message.method === "session/update" && this.options.onLifecycleNotification) {
|
|
138
|
+
const observer = this.options.onLifecycleNotification;
|
|
139
|
+
const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
|
|
140
|
+
if (!this.notificationError || (fatalLifecycleError(error) && !fatalLifecycleError(this.notificationError))) this.notificationError = error;
|
|
141
|
+
// A missing/foreign authority is not a recoverable CLI validation
|
|
142
|
+
// result. Stop admitting productive requests immediately; the managed
|
|
143
|
+
// controller still owns physical stop/drain and evidence capture.
|
|
144
|
+
if (fatalLifecycleError(error)) this.rejectAll(error);
|
|
145
|
+
try {
|
|
146
|
+
await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message,
|
|
147
|
+
session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" });
|
|
148
|
+
} catch (persistenceError) {
|
|
149
|
+
process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
this.lifecycleNotifications.add(pending);
|
|
153
|
+
void pending.then(() => this.lifecycleNotifications.delete(pending), () => this.lifecycleNotifications.delete(pending));
|
|
154
|
+
}
|
|
130
155
|
if (message.id !== undefined && message.method) { void this.answer(message).catch((error) => this.rejectAll(error)); return; }
|
|
131
156
|
if (message.id !== undefined) {
|
|
132
157
|
const pending = this.pending.get(message.id);
|
|
@@ -148,7 +173,14 @@ export class AcpBridge {
|
|
|
148
173
|
}
|
|
149
174
|
if (message.method) this.notifications = this.notifications
|
|
150
175
|
.then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message); })
|
|
151
|
-
.catch((error) => {
|
|
176
|
+
.catch(async (error) => {
|
|
177
|
+
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" };
|
|
178
|
+
try { await this.journal.write("notification_failure", failure); }
|
|
179
|
+
catch (persistenceError) { process.stderr.write(`${JSON.stringify({ ...failure, persistence_error: persistenceError.message })}\n`); }
|
|
180
|
+
// Evidence loss does not retroactively revoke a committed native hook.
|
|
181
|
+
// Permission/profile integrity is a separate, still-fatal contract.
|
|
182
|
+
if (error.code === "profile_integrity_violation") { this.notificationError ??= error; this.rejectAll(error); }
|
|
183
|
+
});
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
observeToolCall(message) {
|
|
@@ -203,7 +235,7 @@ export class AcpBridge {
|
|
|
203
235
|
}
|
|
204
236
|
|
|
205
237
|
request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null } = {}) {
|
|
206
|
-
if (this.notificationError?.code === "profile_integrity_violation" && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.notificationError);
|
|
238
|
+
if ((this.notificationError?.code === "profile_integrity_violation" || fatalLifecycleError(this.notificationError)) && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.notificationError);
|
|
207
239
|
const id = this.nextId++;
|
|
208
240
|
return new Promise((resolve, reject) => {
|
|
209
241
|
const finish = (error) => {
|
|
@@ -249,7 +281,11 @@ export class AcpBridge {
|
|
|
249
281
|
notify(method, params = {}) { this.send({ jsonrpc: "2.0", method, params }); }
|
|
250
282
|
send(message) { this.journal.write("outbound", message); this.child.stdin.write(`${JSON.stringify(message)}\n`); }
|
|
251
283
|
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() {
|
|
284
|
+
async flush() {
|
|
285
|
+
try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
|
|
286
|
+
catch (error) { throw this.notificationError ?? error; }
|
|
287
|
+
if (this.notificationError) throw this.notificationError;
|
|
288
|
+
}
|
|
253
289
|
async close() {
|
|
254
290
|
let flushError = null;
|
|
255
291
|
try { await this.flush(); } catch (error) { flushError = error; }
|
|
@@ -437,8 +473,6 @@ async function withBridge(options, operation) {
|
|
|
437
473
|
}
|
|
438
474
|
|
|
439
475
|
function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
440
|
-
if (!options.ddFlowBin) return undefined;
|
|
441
|
-
const projectRoot = absolute(options.projectRoot, "--project-root");
|
|
442
476
|
return async (notification) => {
|
|
443
477
|
if (notification.method !== "session/update") return;
|
|
444
478
|
const update = notification.params?.update;
|
|
@@ -447,10 +481,52 @@ function flowForwarder(options, rootProviderSessionId, bridge) {
|
|
|
447
481
|
const childId = notification.params?.update?._meta?.zcodeRuntime?.childSessionId;
|
|
448
482
|
const observed = bridge?.modelProfiles?.get(childId ?? rootProviderSessionId) ?? {};
|
|
449
483
|
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
|
-
|
|
484
|
+
// Native PreToolUse has already admitted (or denied) the command. ACP is
|
|
485
|
+
// intentionally only journal/model evidence: its serialized delivery can
|
|
486
|
+
// lag behind execution and must never mint a second lifecycle receipt.
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Preserve native tool ownership independently of slow secondary evidence.
|
|
491
|
+
* Rehydration uses this same session's retained inbound journal, not model text. */
|
|
492
|
+
export async function zcodeInvocationObserver(options, rootProviderSessionId, send = invokeNativeHook) {
|
|
493
|
+
if (!options.ddFlowBin) return async () => {};
|
|
494
|
+
const owners = new Map();
|
|
495
|
+
const remember = message => {
|
|
496
|
+
const update = message.params?.update;
|
|
497
|
+
if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
|
|
498
|
+
if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
|
|
499
|
+
const native = update._meta?.zcodeRuntime ?? {};
|
|
500
|
+
if (native.source === "subagent" && !native.childSessionId) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
|
|
501
|
+
const session = native.childSessionId ?? rootProviderSessionId;
|
|
502
|
+
const key = update.toolCallId;
|
|
503
|
+
if (!key || typeof session !== "string") return null;
|
|
504
|
+
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" });
|
|
505
|
+
owners.set(key, session);
|
|
506
|
+
return { update, native, session };
|
|
507
|
+
};
|
|
508
|
+
if (options.journal) {
|
|
509
|
+
try {
|
|
510
|
+
const contents = await readFile(options.journal, "utf8");
|
|
511
|
+
for (const line of contents.split("\n").filter(Boolean)) {
|
|
512
|
+
const event = JSON.parse(line);
|
|
513
|
+
if (event.kind === "inbound") remember(event.payload);
|
|
514
|
+
}
|
|
515
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
516
|
+
}
|
|
517
|
+
return async message => {
|
|
518
|
+
const facts = remember(message);
|
|
519
|
+
if (!facts || !options.ddFlowBin) return;
|
|
520
|
+
const { update, native, session } = facts;
|
|
521
|
+
const command = update.rawInput?.command ?? update.rawInput?.cmd;
|
|
522
|
+
const toolName = update._meta?.claudeCode?.toolName;
|
|
523
|
+
if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string" || !/(?:^|\s)--invocation-id\s/.test(command)) return;
|
|
524
|
+
const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
|
|
525
|
+
if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
|
|
526
|
+
await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
|
|
527
|
+
args: ["zcode", "event", "handle", "--project-root", options.projectRoot ?? options.cwd, "--json"],
|
|
528
|
+
payload: { ...message, _meta: { ddZcode: { daemonId: options.daemonId, rootProviderSessionId, parentProviderSessionId: parent } } },
|
|
529
|
+
});
|
|
454
530
|
};
|
|
455
531
|
}
|
|
456
532
|
|
|
@@ -480,13 +556,26 @@ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
|
|
|
480
556
|
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
557
|
}
|
|
482
558
|
|
|
559
|
+
export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContract) {
|
|
560
|
+
// Behavioural evidence: dd-eval production-invocations probe, 2026-09-13.
|
|
561
|
+
// Pin both providers of native event identity, not a string in the bundle.
|
|
562
|
+
const qualified = sha256 === "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8"
|
|
563
|
+
&& bridgeCommit === "43f654bccdbb1aa4f4fb4617f7315c4336dbcde0" && harnessContract === ZCODE_HARNESS_CONTRACT;
|
|
564
|
+
return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
|
|
565
|
+
capabilities: qualified ? ["root", "concurrent_children", "child_continuation"] : [],
|
|
566
|
+
native_nested_children: false, reason: qualified ? null : "native_event_contract_unqualified" };
|
|
567
|
+
}
|
|
568
|
+
|
|
483
569
|
export async function doctor(options = {}) {
|
|
484
570
|
const zcodeAcp = await commandOutput(options.bin, ["--version"]);
|
|
485
571
|
const zcodeAcpCommit = await commandOutput(options.bin, ["--dd-harness-commit"]);
|
|
486
572
|
const ddHarness = await commandOutput(options.bin, ["--dd-harness-version"]);
|
|
487
573
|
const zcodePath = options.zcodePath ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
|
|
488
574
|
const zcode = await commandOutput(zcodePath, ["--version"]);
|
|
489
|
-
|
|
575
|
+
const native = await readFile(zcodePath);
|
|
576
|
+
const sha256 = createHash("sha256").update(native).digest("hex");
|
|
577
|
+
const qualification = zcodeLifecycleQualification(sha256, zcodeAcpCommit, ddHarness);
|
|
578
|
+
return { compatible: qualification.status === "qualified", lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
|
|
490
579
|
}
|
|
491
580
|
|
|
492
581
|
export async function createSession(options) {
|
|
@@ -527,6 +616,8 @@ export async function promptSessionWithBridge(bridge, options) {
|
|
|
527
616
|
let forward;
|
|
528
617
|
bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
|
|
529
618
|
const identity = await controlledIdentity(bridge, { ...options, cwd }); forward = flowForwarder(options, identity.providerSessionId, bridge);
|
|
619
|
+
await bridge.journal.flush();
|
|
620
|
+
bridge.configure({ onLifecycleNotification: await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId) });
|
|
530
621
|
const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
|
|
531
622
|
options.assertDispatch?.();
|
|
532
623
|
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
|
+
}
|