@deksden-com/dd-flow-cli 0.9.0-beta.124 → 0.9.0-beta.127
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 +16 -0
- package/README.md +10 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/vnext-code-review.js +10 -9
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +34 -4
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -4,7 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { AppError, errorRecord } from "../shared/errors.js";
|
|
6
6
|
import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
|
|
7
|
-
import { recoveryRpcTimeout } from "./recovery-observation-budget.js";
|
|
7
|
+
import { recoveryObservationBudget, recoveryRpcTimeout, withRecoveryObservation } from "./recovery-observation-budget.js";
|
|
8
|
+
import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
9
|
+
import { canonicalNativeOperation, nativeOperationWait } from "../harness-runtime/lib/adapter-timeouts.mjs";
|
|
10
|
+
import { readOperationProgress, readOperationRecord, readOperationWaiting, suspendOperationClock } from "../harness-runtime/lib/operation-progress.mjs";
|
|
11
|
+
import { recoverDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
12
|
+
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
8
13
|
const maxAdapterOutputBytes = 1_000_000;
|
|
9
14
|
/** Native hooks need a real executable, not a PATH-dependent shell expression. */
|
|
10
15
|
export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
@@ -37,11 +42,50 @@ export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
|
37
42
|
}
|
|
38
43
|
return executable;
|
|
39
44
|
}
|
|
40
|
-
/** Runs one harness command with the lifecycle guarantees shared by agent services. */
|
|
41
45
|
export function runHarnessAdapter(input) {
|
|
46
|
+
const index = input.args.indexOf("--state-dir"), stateDir = index >= 0 ? input.args[index + 1] : undefined;
|
|
47
|
+
const operation = canonicalNativeOperation(input.harness ?? "", `${input.args[0]}.${input.args[1]}`);
|
|
48
|
+
const boundOperation = ["session.create", "session.fork", "session.start", "session.resume"].includes(operation);
|
|
49
|
+
const binding = { operation, ...(input.runtimeOwner ? { owner_id: input.runtimeOwner.owner_id } : {}) };
|
|
50
|
+
let retainedOutcome;
|
|
51
|
+
const onOutcome = (value) => {
|
|
52
|
+
const fingerprint = JSON.stringify(value);
|
|
53
|
+
if (fingerprint !== retainedOutcome) {
|
|
54
|
+
input.onNativeOutcome?.(value);
|
|
55
|
+
retainedOutcome = fingerprint;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const dispatched = runAdapterTransport(input, stateDir, binding);
|
|
59
|
+
return dispatched.catch(async (error) => {
|
|
60
|
+
if (!stateDir || !boundOperation || input.signal?.aborted || !(isObservationLoss(error) || isObservationLoss(error?.details?.cause)))
|
|
61
|
+
throw error;
|
|
62
|
+
try {
|
|
63
|
+
return await recoverDaemonReply(stateDir, input.operationId, { timeoutMs: recoveryRpcTimeout(30_000), ...(input.signal ? { signal: input.signal } : {}), ...(error?.observationClock ? { observationClock: error.observationClock } : {}), binding, onOutcome,
|
|
64
|
+
// Reconcile only original bookkeeping. Work/MERGE share this path and
|
|
65
|
+
// must not replay creation merely because its release receipt is pending.
|
|
66
|
+
onPendingSettlement: async (retained) => {
|
|
67
|
+
input.signal?.throwIfAborted();
|
|
68
|
+
await withRecoveryObservation(recoveryObservationBudget(retained), () => runAdapterTransport({ ...input, args: ["daemon", "operation", "settle", "--state-dir", stateDir, "--operation-id", input.operationId, "--json"], operationId: `${input.operationId}:settlement`, timeoutMs: recoveryRpcTimeout(30_000) }, stateDir, {}), true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (recovery) {
|
|
73
|
+
if (recovery && typeof recovery === "object")
|
|
74
|
+
Object.assign(recovery, { cause: error });
|
|
75
|
+
throw recovery;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function runAdapterTransport(input, stateDir, binding = {}) {
|
|
42
80
|
// `null` is deliberate: a productive Turn has no adapter-wall-clock limit.
|
|
43
81
|
// Preserve it instead of treating it like an omitted short control request.
|
|
44
|
-
|
|
82
|
+
if (input.timeoutMs != null)
|
|
83
|
+
validateObservationDuration(input.timeoutMs);
|
|
84
|
+
const operation = canonicalNativeOperation(input.harness ?? "", `${input.args[0]}.${input.args[1]}`);
|
|
85
|
+
const productive = input.harness !== undefined && nativeOperationWait(input.harness, operation) === "native-work";
|
|
86
|
+
const timeoutMs = recoveryRpcTimeout(productive ? null : input.timeoutMs === undefined ? 120_000 : input.timeoutMs);
|
|
87
|
+
if (timeoutMs !== null)
|
|
88
|
+
validateObservationDuration(timeoutMs);
|
|
45
89
|
if (!input.operationId.trim())
|
|
46
90
|
throw new AppError("operation_id_required", "Harness calls require a scoped operation id", 2);
|
|
47
91
|
// The current adapter transport uses this name; its identity is runtime-owned,
|
|
@@ -66,13 +110,20 @@ export function runHarnessAdapter(input) {
|
|
|
66
110
|
let timedOut = false;
|
|
67
111
|
let aborted = false;
|
|
68
112
|
let cleanup;
|
|
113
|
+
let pendingDiagnostic = "", droppingDiagnostic = false, progressCursor = 0;
|
|
114
|
+
let prettyDiagnostic = "", retainedError;
|
|
115
|
+
const phases = new Set();
|
|
116
|
+
let operationSequence = 0;
|
|
117
|
+
let diagnosticReads = Promise.resolve();
|
|
118
|
+
const clock = timeoutMs === null ? null : new ObservationClock({ timeoutMs });
|
|
119
|
+
clock?.sample(progressCursor);
|
|
69
120
|
let settled = false;
|
|
70
121
|
let callbackError;
|
|
71
122
|
let spawnError;
|
|
72
123
|
const stopTransport = () => {
|
|
73
124
|
cleanup ??= stopProcessGroup(child, 5_000, { detached }).catch(error => {
|
|
74
125
|
if (timer)
|
|
75
|
-
|
|
126
|
+
clearInterval(timer);
|
|
76
127
|
if (heartbeat)
|
|
77
128
|
clearInterval(heartbeat);
|
|
78
129
|
input.signal?.removeEventListener("abort", abort);
|
|
@@ -100,7 +151,43 @@ export function runHarnessAdapter(input) {
|
|
|
100
151
|
}
|
|
101
152
|
};
|
|
102
153
|
const heartbeat = input.progress && input.progressMessage ? setInterval(() => deliver(() => input.progress?.(`${input.progressMessage}; waiting for adapter completion`)), 15_000) : undefined;
|
|
103
|
-
|
|
154
|
+
let observing = false;
|
|
155
|
+
const timer = timeoutMs === null ? undefined : setInterval(async () => {
|
|
156
|
+
if (settled || cleanup)
|
|
157
|
+
return;
|
|
158
|
+
if (observing)
|
|
159
|
+
return;
|
|
160
|
+
observing = true;
|
|
161
|
+
// Helper phase progress must never replenish its owner's independent
|
|
162
|
+
// physical-settlement episode. ALS retains that existing budget here.
|
|
163
|
+
try {
|
|
164
|
+
recoveryRpcTimeout(timeoutMs);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
callbackError ??= error;
|
|
168
|
+
stopTransport();
|
|
169
|
+
observing = false;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const waiting = stateDir && await readOperationWaiting(stateDir, input.operationId, binding);
|
|
174
|
+
if (settled || cleanup)
|
|
175
|
+
return;
|
|
176
|
+
if (waiting)
|
|
177
|
+
suspendOperationClock(clock);
|
|
178
|
+
else if (clock.sample(progressCursor)) {
|
|
179
|
+
timedOut = true;
|
|
180
|
+
stopTransport();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
callbackError ??= error;
|
|
185
|
+
stopTransport();
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
observing = false;
|
|
189
|
+
}
|
|
190
|
+
}, Math.max(1, Math.min(1000, timeoutMs / 4)));
|
|
104
191
|
const abort = () => { aborted = true; stopTransport(); };
|
|
105
192
|
if (input.signal?.aborted)
|
|
106
193
|
abort();
|
|
@@ -115,19 +202,96 @@ export function runHarnessAdapter(input) {
|
|
|
115
202
|
}
|
|
116
203
|
return current + chunk;
|
|
117
204
|
};
|
|
205
|
+
const observeDiagnostic = (line) => {
|
|
206
|
+
if (prettyDiagnostic === null) {
|
|
207
|
+
if (line.trimEnd() === "}")
|
|
208
|
+
prettyDiagnostic = "";
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const candidate = prettyDiagnostic ? `${prettyDiagnostic}\n${line}` : line;
|
|
212
|
+
if (candidate.length > 64_000) {
|
|
213
|
+
prettyDiagnostic = null;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
let value;
|
|
217
|
+
try {
|
|
218
|
+
value = JSON.parse(candidate);
|
|
219
|
+
prettyDiagnostic = "";
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
prettyDiagnostic = prettyDiagnostic || line.trimStart().startsWith("{") ? candidate : "";
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
226
|
+
return;
|
|
227
|
+
if (value.kind === "operation_progress" && stateDir && value.operation_id === input.operationId) {
|
|
228
|
+
// The diagnostic is only a wake-up: durable, bound evidence owns liveness.
|
|
229
|
+
diagnosticReads = diagnosticReads.then(async () => {
|
|
230
|
+
const record = await readOperationProgress(stateDir, input.operationId, binding);
|
|
231
|
+
if (!record || record.sequence <= operationSequence)
|
|
232
|
+
return;
|
|
233
|
+
const requested = await readOperationRecord(stateDir, input.operationId, "requested.json");
|
|
234
|
+
if (!requested)
|
|
235
|
+
throw new AppError("operation_progress_invalid", "Operation request disappeared while observing progress", 1);
|
|
236
|
+
for (const key of ["daemon_id", "params_sha256", "generation"])
|
|
237
|
+
binding[key] ??= requested[key] ?? null;
|
|
238
|
+
operationSequence = record.sequence;
|
|
239
|
+
clock?.sample(++progressCursor, Date.parse(record.observed_at));
|
|
240
|
+
}).catch(error => { callbackError ??= error; if (!settled)
|
|
241
|
+
stopTransport(); });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const cause = adapterCause(candidate);
|
|
245
|
+
if (typeof cause.code === "string")
|
|
246
|
+
retainedError = cause;
|
|
247
|
+
if (clock && !(stateDir && ["session.create", "session.fork", "session.start", "session.resume", "session.prompt"].includes(operation))
|
|
248
|
+
&& ["native_hook_phase", "process_maintenance_phase"].includes(String(value.kind))
|
|
249
|
+
&& typeof value.phase === "string" && /^[a-z_]{1,48}$/.test(value.phase)
|
|
250
|
+
&& !["heartbeat", "poll", "elapsed", "wait", "waiting", "status"].includes(value.phase)) {
|
|
251
|
+
const marker = JSON.stringify([value.kind, value.phase, value.operation_id ?? value.process_id]);
|
|
252
|
+
if (!phases.has(marker)) {
|
|
253
|
+
phases.add(marker);
|
|
254
|
+
clock.sample(++progressCursor);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
118
258
|
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
119
|
-
|
|
259
|
+
// Diagnostics have no lifetime quota. Stream to the existing evidence sink
|
|
260
|
+
// and carry only a bounded tail in the disposable RPC receipt.
|
|
261
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
262
|
+
stderr = (stderr + chunk).slice(-64_000);
|
|
263
|
+
for (const piece of chunk.split(/(?<=\n)/)) {
|
|
264
|
+
if (!droppingDiagnostic)
|
|
265
|
+
pendingDiagnostic += piece;
|
|
266
|
+
if (pendingDiagnostic.length > 64_000) {
|
|
267
|
+
prettyDiagnostic = prettyDiagnostic || pendingDiagnostic.trimStart().startsWith("{") ? null : "";
|
|
268
|
+
pendingDiagnostic = "";
|
|
269
|
+
droppingDiagnostic = true;
|
|
270
|
+
}
|
|
271
|
+
if (!piece.endsWith("\n"))
|
|
272
|
+
continue;
|
|
273
|
+
if (!droppingDiagnostic)
|
|
274
|
+
observeDiagnostic(pendingDiagnostic);
|
|
275
|
+
pendingDiagnostic = "";
|
|
276
|
+
droppingDiagnostic = false;
|
|
277
|
+
}
|
|
278
|
+
if (input.onEvidence)
|
|
279
|
+
deliver(() => evidence({ type: "adapter_diagnostic", stderr: chunk }));
|
|
280
|
+
});
|
|
120
281
|
child.once("error", (error) => { spawnError = error; });
|
|
121
282
|
child.once("close", async (status, signal) => {
|
|
122
283
|
if (settled)
|
|
123
284
|
return;
|
|
124
285
|
settled = true;
|
|
125
286
|
if (timer)
|
|
126
|
-
|
|
287
|
+
clearInterval(timer);
|
|
127
288
|
input.signal?.removeEventListener("abort", abort);
|
|
128
289
|
if (heartbeat)
|
|
129
290
|
clearInterval(heartbeat);
|
|
130
291
|
await cleanup;
|
|
292
|
+
if (!droppingDiagnostic && pendingDiagnostic)
|
|
293
|
+
observeDiagnostic(pendingDiagnostic);
|
|
294
|
+
await diagnosticReads;
|
|
131
295
|
const progressFailure = callbackError;
|
|
132
296
|
deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
|
|
133
297
|
if (spawnError)
|
|
@@ -135,11 +299,11 @@ export function runHarnessAdapter(input) {
|
|
|
135
299
|
if (outputTooLarge)
|
|
136
300
|
return reject(new AppError("harness_adapter_output_limit", "Harness adapter exceeded the bounded reply limit", 1, { executable: input.executable, args: input.args.slice(0, 3), max_output_bytes: maxAdapterOutputBytes }));
|
|
137
301
|
if (timedOut)
|
|
138
|
-
return reject(new AppError("harness_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }));
|
|
302
|
+
return reject(Object.assign(new AppError("harness_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }), { observationClock: clock }));
|
|
139
303
|
if (aborted)
|
|
140
304
|
return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled", 1, { executable: input.executable, args: input.args.slice(0, 3) }));
|
|
141
305
|
if (status !== 0)
|
|
142
|
-
return reject(progressFailure ?? new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), status, signal, stderr, cause: adapterCause(stdout, stderr), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
|
|
306
|
+
return reject(progressFailure ?? new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { executable: input.executable, args: input.args.slice(0, 3), status, signal, stderr, cause: adapterCause(stdout, stderr, retainedError ? JSON.stringify(retainedError) : ""), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
|
|
143
307
|
if (callbackError)
|
|
144
308
|
return reject(callbackError);
|
|
145
309
|
try {
|
|
@@ -164,9 +328,9 @@ function adapterCause(...outputs) {
|
|
|
164
328
|
const value = JSON.parse(line);
|
|
165
329
|
const nested = value.error && typeof value.error === "object" && !Array.isArray(value.error) ? value.error : null;
|
|
166
330
|
const code = typeof nested?.code === "string" ? nested.code : typeof value.code === "string" ? value.code : null;
|
|
167
|
-
const message = typeof nested?.message === "string" ? nested.message : typeof value.error === "string" ? value.error : code;
|
|
331
|
+
const message = typeof nested?.message === "string" ? nested.message : typeof value.error === "string" ? value.error : typeof value.message === "string" ? value.message : code;
|
|
168
332
|
if (code)
|
|
169
|
-
return {
|
|
333
|
+
return errorRecord({ ...(nested ?? value), code, message });
|
|
170
334
|
}
|
|
171
335
|
catch { /* Adapter diagnostics need not be JSON. */ }
|
|
172
336
|
}
|
package/dist/services/hooks.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appendAudit } from "./audit.js";
|
|
|
12
12
|
import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
|
|
13
13
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
14
14
|
import { assertRecoveryHookCurrent } from "./run-recovery.js";
|
|
15
|
-
import { observeLifecycleInvocation, observedLifecycleInvocation } from "./lifecycle-invocations.js";
|
|
15
|
+
import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope } from "./lifecycle-invocations.js";
|
|
16
16
|
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
|
|
17
17
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
18
18
|
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./sessions.js";
|
|
@@ -398,6 +398,8 @@ export function handleCodexHook(context, input) {
|
|
|
398
398
|
protocol_id: null
|
|
399
399
|
};
|
|
400
400
|
if (!lifecycle) {
|
|
401
|
+
if (providerSessionId)
|
|
402
|
+
assignedLifecycleCwd(context, project.root, null, providerSessionId, command, stringValue(payload.cwd));
|
|
401
403
|
const persisted = context.db.writeTransaction(() => persist());
|
|
402
404
|
return { ...common, observed: persisted.inserted, duplicate: !persisted.inserted };
|
|
403
405
|
}
|
|
@@ -406,10 +408,13 @@ export function handleCodexHook(context, input) {
|
|
|
406
408
|
const toolCallId = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
|
|
407
409
|
if (!daemonId || !rootProviderSessionId || !providerSessionId || !toolCallId)
|
|
408
410
|
throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
|
|
411
|
+
const executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
|
|
409
412
|
const receipt = observeLifecycleInvocation(context, {
|
|
410
413
|
...(invocationId ? { id: invocationId } : {}),
|
|
411
414
|
command,
|
|
412
415
|
harness: "codex-desktop",
|
|
416
|
+
projectId: project.id,
|
|
417
|
+
...(executionCwd ? { executionCwd } : {}),
|
|
413
418
|
identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId },
|
|
414
419
|
recordReceipt: scope => { persist(scope); return eventKey; }
|
|
415
420
|
});
|
|
@@ -493,17 +498,17 @@ export function handleZcodeEvent(context, input) {
|
|
|
493
498
|
if (toolName !== "Bash" && toolName !== "bash" && toolName !== "execute")
|
|
494
499
|
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
495
500
|
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd);
|
|
496
|
-
const parsed = command ? parseLifecycleCommand(command) : { kind: "none" };
|
|
497
|
-
if (parsed.
|
|
501
|
+
const parsed = command ? parseLifecycleCommand(command) : { kind: "none", participation: "none" };
|
|
502
|
+
if (parsed.participation === "none" || parsed.participation === "unproven")
|
|
498
503
|
return { ok: true, observed: false, reason: "acp_evidence_only" };
|
|
499
|
-
if (parsed.kind === "none")
|
|
500
|
-
throw new AppError("lifecycle_shell_syntax_invalid", "
|
|
504
|
+
if (parsed.kind === "none" && !continuationCommand(command))
|
|
505
|
+
throw new AppError("lifecycle_shell_syntax_invalid", "Known lifecycle shell execution is unsupported", 1, { reason: parsed.reason, effect: "unknown", recoverable: false });
|
|
501
506
|
const controlled = objectRecord(objectRecord(notification._meta).ddZcode);
|
|
502
507
|
const providerSessionId = stringValue(native.childSessionId) ?? stringValue(controlled.rootProviderSessionId);
|
|
503
508
|
return recordZcodeLifecycle(context, { projectRoot: input.projectRoot, stdin: JSON.stringify({
|
|
504
509
|
session_id: providerSessionId, tool_use_id: update.toolCallId, tool_name: toolName, tool_input: rawInput,
|
|
505
510
|
agent_id: native.agentId, _meta: { ddZcode: controlled }
|
|
506
|
-
}) }, commandOption(parsed.invocation, "invocation-id"));
|
|
511
|
+
}) }, parsed.kind !== "none" ? commandOption(parsed.invocation, "invocation-id") : undefined);
|
|
507
512
|
}
|
|
508
513
|
/**
|
|
509
514
|
* Accept the synchronous native ZCode PreToolUse callback. Invocation-ID
|
|
@@ -576,12 +581,17 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
|
|
|
576
581
|
const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, harness: "zcode-acp", session: nativeSessionIdentity("zcode-acp", providerSessionId), ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", parentProviderSessionId) } : {}), daemon_id: daemonId };
|
|
577
582
|
return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: rewritten } } };
|
|
578
583
|
};
|
|
579
|
-
if (!lifecycle)
|
|
584
|
+
if (!lifecycle) {
|
|
585
|
+
assignedLifecycleCwd(context, expectedRoot, null, providerSessionId, command, observedCwd ?? undefined);
|
|
580
586
|
return context.db.writeTransaction(() => persist());
|
|
587
|
+
}
|
|
581
588
|
let persistedResult;
|
|
589
|
+
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
|
|
582
590
|
const receipt = observeLifecycleInvocation(context, {
|
|
583
591
|
...(invocationId ? { id: invocationId } : {}), command,
|
|
584
592
|
harness: "zcode-acp",
|
|
593
|
+
projectId: project.id,
|
|
594
|
+
...(executionCwd ? { executionCwd } : {}),
|
|
585
595
|
identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId },
|
|
586
596
|
recordReceipt: scope => { persistedResult = persist(scope); return eventKey; }
|
|
587
597
|
});
|
|
@@ -668,11 +678,15 @@ export function handleGrokEvent(context, input) {
|
|
|
668
678
|
};
|
|
669
679
|
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
|
|
670
680
|
...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null });
|
|
671
|
-
if (!lifecycle)
|
|
681
|
+
if (!lifecycle) {
|
|
682
|
+
assignedLifecycleCwd(context, expectedRoot, null, providerSessionId, command, observedCwd ?? undefined);
|
|
672
683
|
return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
684
|
+
}
|
|
673
685
|
let inserted = false;
|
|
686
|
+
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
|
|
674
687
|
const receipt = observeLifecycleInvocation(context, {
|
|
675
688
|
command, harness: "grok-acp", projectId: project.id,
|
|
689
|
+
...(executionCwd ? { executionCwd } : {}),
|
|
676
690
|
identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
|
|
677
691
|
recordReceipt: scope => { inserted = persist(scope); return eventKey; }
|
|
678
692
|
});
|
|
@@ -695,12 +709,35 @@ export function handleDroidEvent(context, input) {
|
|
|
695
709
|
return handleControlledToolEvent(context, input, event, "droid-cli");
|
|
696
710
|
}
|
|
697
711
|
/** Stable project identity is not the native Session's execution workspace. */
|
|
712
|
+
function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness) {
|
|
713
|
+
const auxiliary = continuationCommand(command);
|
|
714
|
+
if ((!facts && !auxiliary) || facts?.analysis.supported === false)
|
|
715
|
+
return undefined;
|
|
716
|
+
const proven = identity && harness ? managedNativeInvocationScope(context, projectRoot, identity, harness) : null;
|
|
717
|
+
const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
|
|
718
|
+
const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
|
|
719
|
+
if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
|
|
720
|
+
throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
721
|
+
const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId);
|
|
722
|
+
// Unknown CLI targets have no assigned workspace. Recording their native
|
|
723
|
+
// observation must not turn the stable identity root into cwd authority.
|
|
724
|
+
if (!expected)
|
|
725
|
+
return undefined;
|
|
726
|
+
const route = facts?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
|
|
727
|
+
if (route && resolveProjectRoot(route) !== expected)
|
|
728
|
+
throw new AppError("invocation_directory_mismatch", "Literal lifecycle route differs from the assigned workspace", 1, { expected, actual: route, actual_provenance: "shell_literal_route", phase: "prepare", effect: "no_effect", recoverable: false });
|
|
729
|
+
if (!route && observedCwd && resolveProjectRoot(observedCwd) !== expected)
|
|
730
|
+
throw new AppError("invocation_directory_mismatch", "Native lifecycle directory differs from the assigned workspace", 1, { expected, actual: observedCwd, actual_provenance: "native_tool_cwd", phase: "prepare", effect: "no_effect", recoverable: false });
|
|
731
|
+
// ACP without native cwd still binds assigned authority; CLI confirms the
|
|
732
|
+
// physical process directory, rather than inventing native observation.
|
|
733
|
+
return expected;
|
|
734
|
+
}
|
|
698
735
|
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId) {
|
|
699
736
|
if (facts?.bootstrapStageStart)
|
|
700
737
|
return projectRoot;
|
|
701
738
|
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
702
739
|
if (!project)
|
|
703
|
-
return
|
|
740
|
+
return undefined;
|
|
704
741
|
if (facts?.invocation.operation === "merge_apply" || facts?.invocation.operation === "merge_repair") {
|
|
705
742
|
const requestId = commandPosition(facts.invocation, 0);
|
|
706
743
|
const request = requestId && context.db.get("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND merge_request_id = ?", [project.id, requestId]);
|
|
@@ -716,13 +753,15 @@ function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId,
|
|
|
716
753
|
if (!(error instanceof AppError) || !["not_found", "ambiguous_work_alias"].includes(error.code))
|
|
717
754
|
throw error;
|
|
718
755
|
}
|
|
756
|
+
if (!work)
|
|
757
|
+
return undefined;
|
|
719
758
|
}
|
|
720
759
|
const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2] ?? issuedRunId;
|
|
721
760
|
if (!reference)
|
|
722
|
-
return
|
|
761
|
+
return undefined;
|
|
723
762
|
const run = context.db.get("SELECT id, run_root, workspace_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
|
|
724
763
|
if (!run)
|
|
725
|
-
return
|
|
764
|
+
return undefined; // Hook ingress does not validate a mistyped CLI target.
|
|
726
765
|
if (work) {
|
|
727
766
|
// PROTOCOLIZE provisions the feature checkout after its coordinator
|
|
728
767
|
// Session has launched in the stable project root. Its transition Work
|
|
@@ -809,13 +848,17 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
809
848
|
// Syntax refusal precedes assignment/cwd lookup: an unsupported AST cannot
|
|
810
849
|
// supply a route or execution authority, including a short public Stage call.
|
|
811
850
|
const shellRefusal = lifecycle?.analysis.supported === false;
|
|
851
|
+
const nativeIdentity = { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId };
|
|
852
|
+
const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
|
|
812
853
|
const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
|
|
813
|
-
? observedLifecycleInvocation(context, command, daemonId)?.scope.runId : null;
|
|
854
|
+
? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
|
|
814
855
|
if (!shellRefusal && lifecycle && lifecycle.invocation.operation.startsWith("stage_") && !lifecycle.bootstrapStageStart && !lifecycle.runId && !issuedRunId) {
|
|
815
856
|
throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { effect: "no_effect", recoverable: false });
|
|
816
857
|
}
|
|
817
|
-
|
|
818
|
-
|
|
858
|
+
// These controlled platforms additionally qualify their native launch
|
|
859
|
+
// route against the stable project root when no CLI target is assigned.
|
|
860
|
+
const expectedCwd = shellRefusal ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId) ?? expectedRoot;
|
|
861
|
+
const executionRoute = shellRefusal ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
|
|
819
862
|
if (executionRoute && resolveProjectRoot(executionRoute) !== expectedCwd) {
|
|
820
863
|
throw new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
|
|
821
864
|
}
|
|
@@ -854,13 +897,14 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
854
897
|
return inserted;
|
|
855
898
|
};
|
|
856
899
|
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId });
|
|
857
|
-
if (
|
|
900
|
+
if (lifecycle) {
|
|
858
901
|
let inserted = false;
|
|
859
902
|
const receipt = observeLifecycleInvocation(context, {
|
|
860
|
-
command, harness, projectId: project.id, ...(!shellRefusal
|
|
903
|
+
command, harness, projectId: project.id, ...(!shellRefusal ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
|
|
861
904
|
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
862
905
|
});
|
|
863
|
-
|
|
906
|
+
const admitted = { ...result(inserted), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
907
|
+
return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
|
|
864
908
|
}
|
|
865
909
|
const inserted = context.db.writeTransaction(() => persist());
|
|
866
910
|
return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
|
|
@@ -968,8 +1012,8 @@ export function analyzeLifecycleShellCommand(command, includeSessionRegister = f
|
|
|
968
1012
|
}
|
|
969
1013
|
function lifecycleFacts(command) {
|
|
970
1014
|
const analysis = parseLifecycleCommand(command);
|
|
971
|
-
if (analysis.kind === "none" && analysis.unresolved)
|
|
972
|
-
throw new AppError("lifecycle_shell_syntax_invalid", "
|
|
1015
|
+
if (analysis.kind === "none" && analysis.unresolved && ["lifecycle", "auxiliary"].includes(analysis.participation))
|
|
1016
|
+
throw new AppError("lifecycle_shell_syntax_invalid", "Known lifecycle execution is unsupported; use a literal issued command", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.reason });
|
|
973
1017
|
if (analysis.kind === "none")
|
|
974
1018
|
return null;
|
|
975
1019
|
const invocation = analysis.invocation;
|
|
@@ -1431,8 +1475,8 @@ function commandHook(command, statusMessage) {
|
|
|
1431
1475
|
}
|
|
1432
1476
|
function hookCommand(event) {
|
|
1433
1477
|
const args = `codex hook handle --event ${event} --json`;
|
|
1434
|
-
const handler = `if [ -n "\${DD_FLOW_BIN:-}" ]; then "$DD_FLOW_BIN" ${args}; else
|
|
1435
|
-
return `/bin/bash -c 'log_dir="\${DD_FLOW_HOME:-\${HOME}/.dd-flow}/logs"; mkdir -p "$log_dir"; log="$log_dir/codex-hook.log"; printf "%s event=${event} phase=start pid=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" >>"$log"; ${handler} 2> >(tee -a "$log" >&2); status=$?; printf "%s event=${event} phase=finish pid=%s exit=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" "$status" >>"$log"; exit "$status"'`;
|
|
1478
|
+
const handler = `if [ -n "\${DD_FLOW_BIN:-}" ]; then "$DD_FLOW_BIN" ${args}; else dd-flow ${args}; fi`;
|
|
1479
|
+
return `/bin/bash -c 'export PATH="\${DD_FLOW_HOME:+\${DD_FLOW_HOME}/bin:}\${PATH}:\${HOME}/Library/pnpm:/opt/homebrew/bin:/usr/local/bin"; log_dir="\${DD_FLOW_HOME:-\${HOME}/.dd-flow}/logs"; mkdir -p "$log_dir"; log="$log_dir/codex-hook.log"; printf "%s event=${event} phase=start pid=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" >>"$log"; ${handler} 2> >(tee -a "$log" >&2); status=$?; printf "%s event=${event} phase=finish pid=%s exit=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" "$status" >>"$log"; exit "$status"'`;
|
|
1436
1480
|
}
|
|
1437
1481
|
function resolveHookTarget(target) {
|
|
1438
1482
|
if (!target || target === "isolated") {
|