@deksden-com/dd-flow-cli 0.9.0-beta.123 → 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 +26 -0
- package/README.md +33 -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/domain/stage-catalog.js +1 -1
- 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/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -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 +159 -15
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
- package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
- package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- 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 +11 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
- 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/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +93 -27
- package/dist/services/codex-capacity.js +251 -74
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -75
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +21 -9
- package/dist/services/harness-adapter.js +195 -25
- 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 +117 -53
- package/dist/services/merge-server.js +66 -23
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/review-deferral.js +23 -0
- 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 +177 -51
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +207 -107
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +21 -30
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +40 -7
- 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/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +26 -13
- 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 +30 -11
- package/dist/services/vnext-plan-review.js +102 -35
- 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 +43 -6
- 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
|
@@ -93,10 +93,16 @@ export async function launchExternalWork(context, input) {
|
|
|
93
93
|
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
|
|
94
94
|
const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
|
|
95
95
|
try {
|
|
96
|
-
const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence
|
|
96
|
+
const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, harness: runtimeHarness(resolved.worker), env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence, onNativeOutcome: value => {
|
|
97
|
+
if (args[0] !== "session" || args[1] !== "create")
|
|
98
|
+
return;
|
|
99
|
+
const nativeId = adapterSessionId(value);
|
|
100
|
+
if (nativeId)
|
|
101
|
+
context.db.run("UPDATE work_launches SET provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [nativeId, JSON.stringify({ create: value, settlement_pending: true }), context.now(), operationId]);
|
|
102
|
+
} });
|
|
97
103
|
await call(["daemon", "start", ...common]);
|
|
98
104
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
99
|
-
const created = await call(["session", "create", ...common]
|
|
105
|
+
const created = await call(["session", "create", ...common]);
|
|
100
106
|
const sessionId = adapterSessionId(created);
|
|
101
107
|
if (!sessionId)
|
|
102
108
|
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) });
|
|
@@ -111,20 +117,28 @@ export async function launchExternalWork(context, input) {
|
|
|
111
117
|
const promptFile = path.join(root, "launch.md");
|
|
112
118
|
fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n\n${lifecycleRetryInstruction}${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
|
|
113
119
|
const promptOperationId = `${operationId}:session.prompt`;
|
|
114
|
-
|
|
120
|
+
context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, progress_policy: "native-inactivity@1" }), context.now(), operationId]);
|
|
115
121
|
let prompted;
|
|
116
122
|
try {
|
|
117
|
-
prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common],
|
|
123
|
+
prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null);
|
|
118
124
|
}
|
|
119
125
|
catch (error) {
|
|
120
126
|
if (runtimeHarness(resolved.worker) !== "codex-desktop")
|
|
121
127
|
throw error;
|
|
122
128
|
const retained = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
|
|
123
129
|
const capacityIntents = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_intents ?? [] : [];
|
|
130
|
+
const capacityFailures = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_failures ?? [] : [];
|
|
131
|
+
const retainCapacity = () => {
|
|
132
|
+
const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, progress_policy: "native-inactivity@1", capacity_intents: capacityIntents, capacity_failures: capacityFailures }), context.now(), operationId]);
|
|
133
|
+
if (changed.changes !== 1)
|
|
134
|
+
throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
|
|
135
|
+
};
|
|
124
136
|
const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId, allowSettledDescendants: true,
|
|
125
|
-
...(input.signal ? { signal: input.signal } : {}),
|
|
137
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
126
138
|
continuationText: `Continue only Work ${work.work_id} in RUN ${work.run_id}, Stage ${stage ?? "current"}, in the same Session. Inspect its current durable state and the retained Work packet. Never repeat Work start or a completed tool, accepted answer, check or lifecycle command. Use only the current exact issued successor and finish at the original Work boundary.`,
|
|
127
139
|
retainedIntents: () => capacityIntents,
|
|
140
|
+
retainedFailures: () => capacityFailures,
|
|
141
|
+
retainFailure: (_id, refusal) => { capacityFailures.push(refusal); retainCapacity(); },
|
|
128
142
|
inspectOperation: id => call(["daemon", "operation", "--state-dir", root, "--operation-id", id, "--json"], 30_000, `${id}:capacity-observe`),
|
|
129
143
|
inspectSession: turnId => call(["session", "inspect", "--session-id", sessionId, ...(turnId ? ["--turn-id", turnId] : []), ...common], 30_000, `${operationId}:capacity-inspect`),
|
|
130
144
|
assertCurrent: () => {
|
|
@@ -141,9 +155,7 @@ export async function launchExternalWork(context, input) {
|
|
|
141
155
|
},
|
|
142
156
|
retainIntent: intent => {
|
|
143
157
|
capacityIntents.push(intent);
|
|
144
|
-
|
|
145
|
-
if (changed.changes !== 1)
|
|
146
|
-
throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
|
|
158
|
+
retainCapacity();
|
|
147
159
|
},
|
|
148
160
|
prompt: (id, text, ordinal) => {
|
|
149
161
|
const file = path.join(root, `capacity-${ordinal}.md`);
|
|
@@ -153,7 +165,7 @@ export async function launchExternalWork(context, input) {
|
|
|
153
165
|
}
|
|
154
166
|
else
|
|
155
167
|
fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
|
|
156
|
-
return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common],
|
|
168
|
+
return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], null, id);
|
|
157
169
|
} });
|
|
158
170
|
if (!resumed)
|
|
159
171
|
throw error;
|
|
@@ -2,8 +2,14 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
import { AppError } from "../shared/errors.js";
|
|
6
|
-
import {
|
|
5
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
6
|
+
import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
|
|
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";
|
|
7
13
|
const maxAdapterOutputBytes = 1_000_000;
|
|
8
14
|
/** Native hooks need a real executable, not a PATH-dependent shell expression. */
|
|
9
15
|
export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
@@ -36,11 +42,50 @@ export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
|
36
42
|
}
|
|
37
43
|
return executable;
|
|
38
44
|
}
|
|
39
|
-
/** Runs one harness command with the lifecycle guarantees shared by agent services. */
|
|
40
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 = {}) {
|
|
41
80
|
// `null` is deliberate: a productive Turn has no adapter-wall-clock limit.
|
|
42
81
|
// Preserve it instead of treating it like an omitted short control request.
|
|
43
|
-
|
|
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);
|
|
44
89
|
if (!input.operationId.trim())
|
|
45
90
|
throw new AppError("operation_id_required", "Harness calls require a scoped operation id", 2);
|
|
46
91
|
// The current adapter transport uses this name; its identity is runtime-owned,
|
|
@@ -64,18 +109,33 @@ export function runHarnessAdapter(input) {
|
|
|
64
109
|
let outputTooLarge = false;
|
|
65
110
|
let timedOut = false;
|
|
66
111
|
let aborted = false;
|
|
67
|
-
let
|
|
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);
|
|
68
120
|
let settled = false;
|
|
69
121
|
let callbackError;
|
|
70
122
|
let spawnError;
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
123
|
+
const stopTransport = () => {
|
|
124
|
+
cleanup ??= stopProcessGroup(child, 5_000, { detached }).catch(error => {
|
|
125
|
+
if (timer)
|
|
126
|
+
clearInterval(timer);
|
|
127
|
+
if (heartbeat)
|
|
128
|
+
clearInterval(heartbeat);
|
|
129
|
+
input.signal?.removeEventListener("abort", abort);
|
|
130
|
+
const details = { cleanup_error: errorRecord(error) };
|
|
131
|
+
if (callbackError instanceof Error) {
|
|
132
|
+
Object.assign(callbackError, { cleanup_error: details.cleanup_error, details: { ...callbackError.details, ...details } });
|
|
133
|
+
reject(callbackError);
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
reject(new AppError(outputTooLarge ? "harness_adapter_output_limit" : timedOut ? "harness_adapter_timeout" : aborted ? "harness_adapter_aborted" : "harness_adapter_cleanup_unconfirmed", "Adapter transport cleanup could not be confirmed", 1, details));
|
|
137
|
+
});
|
|
138
|
+
};
|
|
79
139
|
const deliver = (callback) => {
|
|
80
140
|
try {
|
|
81
141
|
const result = callback();
|
|
@@ -91,7 +151,43 @@ export function runHarnessAdapter(input) {
|
|
|
91
151
|
}
|
|
92
152
|
};
|
|
93
153
|
const heartbeat = input.progress && input.progressMessage ? setInterval(() => deliver(() => input.progress?.(`${input.progressMessage}; waiting for adapter completion`)), 15_000) : undefined;
|
|
94
|
-
|
|
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)));
|
|
95
191
|
const abort = () => { aborted = true; stopTransport(); };
|
|
96
192
|
if (input.signal?.aborted)
|
|
97
193
|
abort();
|
|
@@ -106,22 +202,96 @@ export function runHarnessAdapter(input) {
|
|
|
106
202
|
}
|
|
107
203
|
return current + chunk;
|
|
108
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
|
+
};
|
|
109
258
|
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout = append(stdout, chunk); });
|
|
110
|
-
|
|
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
|
+
});
|
|
111
281
|
child.once("error", (error) => { spawnError = error; });
|
|
112
|
-
child.once("close", (status, signal) => {
|
|
282
|
+
child.once("close", async (status, signal) => {
|
|
113
283
|
if (settled)
|
|
114
284
|
return;
|
|
115
285
|
settled = true;
|
|
116
286
|
if (timer)
|
|
117
|
-
|
|
287
|
+
clearInterval(timer);
|
|
118
288
|
input.signal?.removeEventListener("abort", abort);
|
|
119
289
|
if (heartbeat)
|
|
120
290
|
clearInterval(heartbeat);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
291
|
+
await cleanup;
|
|
292
|
+
if (!droppingDiagnostic && pendingDiagnostic)
|
|
293
|
+
observeDiagnostic(pendingDiagnostic);
|
|
294
|
+
await diagnosticReads;
|
|
125
295
|
const progressFailure = callbackError;
|
|
126
296
|
deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
|
|
127
297
|
if (spawnError)
|
|
@@ -129,11 +299,11 @@ export function runHarnessAdapter(input) {
|
|
|
129
299
|
if (outputTooLarge)
|
|
130
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 }));
|
|
131
301
|
if (timedOut)
|
|
132
|
-
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 }));
|
|
133
303
|
if (aborted)
|
|
134
304
|
return reject(new AppError("harness_adapter_aborted", "Harness adapter call was cancelled", 1, { executable: input.executable, args: input.args.slice(0, 3) }));
|
|
135
305
|
if (status !== 0)
|
|
136
|
-
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) } : {}) }));
|
|
137
307
|
if (callbackError)
|
|
138
308
|
return reject(callbackError);
|
|
139
309
|
try {
|
|
@@ -158,9 +328,9 @@ function adapterCause(...outputs) {
|
|
|
158
328
|
const value = JSON.parse(line);
|
|
159
329
|
const nested = value.error && typeof value.error === "object" && !Array.isArray(value.error) ? value.error : null;
|
|
160
330
|
const code = typeof nested?.code === "string" ? nested.code : typeof value.code === "string" ? value.code : null;
|
|
161
|
-
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;
|
|
162
332
|
if (code)
|
|
163
|
-
return {
|
|
333
|
+
return errorRecord({ ...(nested ?? value), code, message });
|
|
164
334
|
}
|
|
165
335
|
catch { /* Adapter diagnostics need not be JSON. */ }
|
|
166
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") {
|