harnery 0.31.6 → 0.32.0
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/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +2 -0
- package/dist/commands/agents.d.ts +0 -9
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +70 -95
- package/dist/commands/browse-session.d.ts +21 -0
- package/dist/commands/browse-session.d.ts.map +1 -0
- package/dist/commands/browse-session.js +157 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +205 -37
- package/dist/commands/checkpoint.js +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +4 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +40 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +100 -17
- package/dist/commands/init.d.ts +1 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -5
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -3
- package/dist/core/agents/canonical-emit.d.ts +1 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +1 -2
- package/dist/core/agents/cli.js +123 -88
- package/dist/core/agents/coord-client.d.ts +7 -3
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +5 -4
- package/dist/core/agents/finalization.d.ts +68 -0
- package/dist/core/agents/finalization.d.ts.map +1 -0
- package/dist/core/agents/finalization.js +443 -0
- package/dist/core/agents/git-hook.d.ts +51 -0
- package/dist/core/agents/git-hook.d.ts.map +1 -0
- package/dist/core/agents/git-hook.js +118 -0
- package/dist/core/agents/render/prompt-context.d.ts +6 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +26 -13
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +15 -2
- package/dist/core/agents/rules/claim-conflict.js +3 -3
- package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +21 -5
- package/dist/core/agents/rules/stop-hook.d.ts +3 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +59 -23
- package/dist/core/agents/session-events.d.ts +8 -16
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -26
- package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +44 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +54 -17
- package/dist/core/agents/state/names.d.ts +22 -1
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +40 -2
- package/dist/core/config.d.ts +30 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +74 -11
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +12 -13
- package/dist/core/hooks/adapter/detect.d.ts +2 -4
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +4 -14
- package/dist/core/hooks/adapter/output.d.ts +1 -1
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +5 -4
- package/dist/core/hooks/adapter/wiring.d.ts +23 -0
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +32 -0
- package/dist/core/hooks/cli.d.ts +1 -2
- package/dist/core/hooks/cli.d.ts.map +1 -1
- package/dist/core/hooks/cli.js +169 -27
- package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
- package/dist/core/hooks/codex-wsl-bridge.js +137 -0
- package/dist/core/hooks/effects/index.d.ts +1 -1
- package/dist/core/hooks/effects/index.js +3 -3
- package/dist/core/hooks/events/schema.d.ts +27 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.d.ts +7 -1
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +15 -0
- package/dist/core/hooks/resolve/transcript.d.ts +44 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +176 -1
- package/dist/core/hooks/session-name-presence.d.ts +30 -0
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
- package/dist/core/hooks/session-name-presence.js +43 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
- package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
- package/dist/core/hooks/unsafe-cross-shell.js +151 -0
- package/dist/core/work/state.d.ts +4 -5
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +5 -8
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/proof.d.ts +2 -2
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +2 -2
- package/dist/core/workflow/run-state.d.ts +2 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +2 -2
- package/dist/core/workflow/types.d.ts +3 -3
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.d.ts +2 -2
- package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/execution.js +5 -0
- package/dist/core/workflow/workspaces/index.d.ts +1 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +1 -2
- package/dist/core/workflow/workspaces/paths.d.ts +2 -2
- package/dist/core/workflow/workspaces/types.d.ts +0 -6
- package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.d.ts +0 -2
- package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/validate.js +0 -2
- package/dist/lib/browser/client.d.ts +118 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +435 -6
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +180 -37
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +5 -1
- package/dist/lib/browser/netscape-cookies.d.ts +6 -0
- package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
- package/dist/lib/browser/netscape-cookies.js +31 -0
- package/dist/lib/browser/proxy.d.ts +24 -0
- package/dist/lib/browser/proxy.d.ts.map +1 -0
- package/dist/lib/browser/proxy.js +84 -0
- package/dist/lib/browser/runts.d.ts +6 -0
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +21 -4
- package/dist/lib/browser/session-control.d.ts +112 -0
- package/dist/lib/browser/session-control.d.ts.map +1 -0
- package/dist/lib/browser/session-control.js +670 -0
- package/dist/lib/docs-links.d.ts +108 -0
- package/dist/lib/docs-links.d.ts.map +1 -0
- package/dist/lib/docs-links.js +555 -0
- package/dist/lib/exec.d.ts +1 -1
- package/dist/lib/exec.js +1 -1
- package/dist/lib/identities/assume.d.ts +4 -2
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +15 -2
- package/dist/lib/instructions/git-hooks.d.ts +75 -0
- package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
- package/dist/lib/instructions/git-hooks.js +238 -0
- package/dist/lib/instructions/splice.d.ts +10 -4
- package/dist/lib/instructions/splice.d.ts.map +1 -1
- package/dist/lib/instructions/splice.js +19 -11
- package/package.json +1 -1
- package/schemas/config.schema.json +32 -0
- package/src/commander.ts +2 -0
- package/src/commands/agents.ts +96 -115
- package/src/commands/browse-session.ts +245 -0
- package/src/commands/browse.ts +287 -44
- package/src/commands/checkpoint.ts +1 -1
- package/src/commands/deinit.ts +7 -1
- package/src/commands/docs.ts +56 -0
- package/src/commands/doctor.ts +100 -18
- package/src/commands/init.ts +17 -9
- package/src/commands/tunnel.ts +2 -5
- package/src/core/agents/canonical-emit.ts +1 -2
- package/src/core/agents/cli.ts +137 -88
- package/src/core/agents/coord-client.ts +10 -5
- package/src/core/agents/finalization.ts +595 -0
- package/src/core/agents/git-hook.ts +126 -0
- package/src/core/agents/render/prompt-context.ts +28 -13
- package/src/core/agents/render/session-context.ts +15 -2
- package/src/core/agents/rules/claim-conflict.ts +3 -3
- package/src/core/agents/rules/commit-conflict.ts +35 -8
- package/src/core/agents/rules/stop-hook.ts +71 -23
- package/src/core/agents/session-events.ts +14 -41
- package/src/core/agents/state/heartbeat-projector.ts +45 -1
- package/src/core/agents/state/heartbeat-writer.ts +78 -16
- package/src/core/agents/state/names.ts +54 -2
- package/src/core/config.ts +92 -12
- package/src/core/governor/planning.ts +17 -12
- package/src/core/hooks/adapter/detect.ts +4 -13
- package/src/core/hooks/adapter/output.ts +9 -4
- package/src/core/hooks/adapter/wiring.ts +43 -0
- package/src/core/hooks/cli.ts +201 -23
- package/src/core/hooks/codex-wsl-bridge.ts +188 -0
- package/src/core/hooks/effects/index.ts +5 -5
- package/src/core/hooks/events/schema.ts +27 -0
- package/src/core/hooks/resolve/owner.ts +22 -1
- package/src/core/hooks/resolve/transcript.ts +164 -1
- package/src/core/hooks/session-name-presence.ts +52 -0
- package/src/core/hooks/unsafe-cross-shell.ts +160 -0
- package/src/core/work/state.ts +8 -12
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/proof.ts +4 -4
- package/src/core/workflow/run-state.ts +5 -5
- package/src/core/workflow/types.ts +3 -3
- package/src/core/workflow/workspaces/execution.ts +11 -4
- package/src/core/workflow/workspaces/index.ts +0 -3
- package/src/core/workflow/workspaces/local-git.ts +1 -2
- package/src/core/workflow/workspaces/paths.ts +2 -2
- package/src/core/workflow/workspaces/types.ts +0 -9
- package/src/core/workflow/workspaces/validate.ts +1 -5
- package/src/lib/browser/client.ts +528 -6
- package/src/lib/browser/geometry.ts +197 -37
- package/src/lib/browser/index.ts +39 -0
- package/src/lib/browser/netscape-cookies.ts +39 -0
- package/src/lib/browser/proxy.ts +105 -0
- package/src/lib/browser/runts.ts +27 -3
- package/src/lib/browser/session-control.ts +892 -0
- package/src/lib/docs-links.ts +674 -0
- package/src/lib/exec.ts +1 -1
- package/src/lib/identities/assume.ts +22 -1
- package/src/lib/instructions/git-hooks.ts +259 -0
- package/src/lib/instructions/splice.ts +41 -11
package/dist/core/hooks/cli.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agent-hook` CLI entry point
|
|
3
|
-
* alongside the legacy stream.
|
|
2
|
+
* `agent-hook` CLI entry point for canonical event emission.
|
|
4
3
|
*
|
|
5
4
|
* Flow:
|
|
6
5
|
* 1. Parse argv → event-name + adapter.
|
|
@@ -24,14 +23,17 @@ import { coordEnv } from "../../lib/env.js";
|
|
|
24
23
|
import { replayCodexJsonl } from "../agents/codex-replay.js";
|
|
25
24
|
import { coordBinPath } from "../agents/coord-bin.js";
|
|
26
25
|
import { consumeSince, writeCursor } from "../agents/events/consume.js";
|
|
26
|
+
import { classifyWriteClaimFinalization, formatWriteClaimFinalizationDenial, } from "../agents/finalization.js";
|
|
27
27
|
import { evaluateStopHook } from "../agents/rules/stop-hook.js";
|
|
28
28
|
import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
|
|
29
29
|
import { writePidmapRow } from "../agents/state/pidmap.js";
|
|
30
30
|
import { shellMutationPaths } from "../agents/state/shell-mutation.js";
|
|
31
|
+
import { agentsRequireGitFinalization, resolveBinName } from "../config.js";
|
|
31
32
|
import { checkpointContext, completeContextRecovery, extractContextSample, markContextCompactionCompleted, prepareContextRecovery, readContextState, recordContextSample, } from "../context/index.js";
|
|
32
33
|
import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.js";
|
|
33
34
|
import { detectAdapter } from "./adapter/detect.js";
|
|
34
35
|
import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./adapter/parse.js";
|
|
36
|
+
import { codexWslFileLinkTelemetry, inspectCodexWslBridge, isWslUncPath, renderCodexWslFileLinkContext, } from "./codex-wsl-bridge.js";
|
|
35
37
|
import { captureImages, detectPresence, imageJanitor, journalArchive, journalJanitor, journalRecoveryCue, playSound, resetSoundCounters, runSessionSyncExtension, runTurnSummary, soundForEvent, } from "./effects/index.js";
|
|
36
38
|
import { emit } from "./events/emit.js";
|
|
37
39
|
import { canonicalize } from "./guard-path.js";
|
|
@@ -39,16 +41,18 @@ import { adapterPidFromEnv, parsePsChainLine, selectAnchorPid } from "./resolve/
|
|
|
39
41
|
import { findCoordRoot } from "./resolve/coord-root.js";
|
|
40
42
|
import { extractIntentComment, resolveIntent } from "./resolve/intent.js";
|
|
41
43
|
import { resolveOwner } from "./resolve/owner.js";
|
|
42
|
-
import { scanStatusBoxPresent, scanTranscriptModel } from "./resolve/transcript.js";
|
|
44
|
+
import { detectForkParent, scanAssistantTextIncludes, scanStatusBoxPresent, scanTranscriptModel, } from "./resolve/transcript.js";
|
|
45
|
+
import { sessionNamePresence } from "./session-name-presence.js";
|
|
46
|
+
import { unsafeCrossShellReason } from "./unsafe-cross-shell.js";
|
|
43
47
|
function parseArgv(argv) {
|
|
44
48
|
const out = { eventName: null, extra: [] };
|
|
45
49
|
for (let i = 0; i < argv.length; i++) {
|
|
46
50
|
const arg = argv[i];
|
|
47
|
-
if (arg === "--adapter"
|
|
51
|
+
if (arg === "--adapter") {
|
|
48
52
|
i++; // detectAdapter will re-parse; just consume the value here.
|
|
49
53
|
continue;
|
|
50
54
|
}
|
|
51
|
-
if (arg.startsWith("--adapter=")
|
|
55
|
+
if (arg.startsWith("--adapter="))
|
|
52
56
|
continue;
|
|
53
57
|
if (!out.eventName && !arg.startsWith("--")) {
|
|
54
58
|
out.eventName = arg;
|
|
@@ -118,12 +122,15 @@ function logError(coordRoot, err, context) {
|
|
|
118
122
|
* Lives at agent-hooks side (not agent-coord) to keep emitter/consumer
|
|
119
123
|
* separation: we spawn rather than import.
|
|
120
124
|
*/
|
|
121
|
-
function assignNameViaAgentCoord(coordRoot, instanceId, kind) {
|
|
125
|
+
function assignNameViaAgentCoord(coordRoot, instanceId, kind, forkedFrom) {
|
|
122
126
|
const binary = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
123
127
|
if (!existsSync(binary))
|
|
124
128
|
return null;
|
|
125
129
|
try {
|
|
126
|
-
const
|
|
130
|
+
const args = ["assign-name", instanceId, kind];
|
|
131
|
+
if (forkedFrom)
|
|
132
|
+
args.push("--forked-from", forkedFrom);
|
|
133
|
+
const result = spawnSync(binary, args, {
|
|
127
134
|
encoding: "utf8",
|
|
128
135
|
timeout: 2000,
|
|
129
136
|
env: childEnv(coordRoot),
|
|
@@ -178,13 +185,21 @@ function buildEventData(eventType, ctx) {
|
|
|
178
185
|
switch (eventType) {
|
|
179
186
|
case "session.start": {
|
|
180
187
|
const adapterPlatform = ctx.adapter === "claude-code"
|
|
181
|
-
? "
|
|
188
|
+
? "claude-code"
|
|
182
189
|
: ctx.adapter === "cursor"
|
|
183
190
|
? "cursor"
|
|
184
191
|
: "codex";
|
|
192
|
+
// Recorded fork lineage is NOT detected here. On claude-code a fork
|
|
193
|
+
// never fires its own session.start — SessionStart fires under the
|
|
194
|
+
// PARENT's session id (source=resume) before the fork id is minted
|
|
195
|
+
// (verified 2026-08-05) — so detection at this point can only mislabel
|
|
196
|
+
// the resumed parent. The fork's new instance is caught by the
|
|
197
|
+
// tool.pre_use heal path instead. The forkedFrom plumbing below stays
|
|
198
|
+
// for adapters that DO report a parent at session start.
|
|
199
|
+
const forkedFrom = undefined;
|
|
185
200
|
// Assign (or recover) name + kind via agent-coord. Idempotent: resume
|
|
186
201
|
// returns the original name; new owner consumes a counter slot.
|
|
187
|
-
const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session");
|
|
202
|
+
const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
|
|
188
203
|
// Write the adapter pid-map row so `harn agents whoami` ppid-walks find
|
|
189
204
|
// this owner. Prefer the payload pid (the actual claude binary), then the
|
|
190
205
|
// anchor walk (the `node` ancestor for Cursor, which has no payload pid),
|
|
@@ -208,6 +223,10 @@ function buildEventData(eventType, ctx) {
|
|
|
208
223
|
name: assigned?.name,
|
|
209
224
|
kind: "session",
|
|
210
225
|
agent_id: ctx.instanceId,
|
|
226
|
+
// Recorded fork lineage rides the canonical event too, so full replay
|
|
227
|
+
// and derived readers converge on the same parent linkage that
|
|
228
|
+
// .name-history carries (the ADR 0017 dual-write pattern).
|
|
229
|
+
...(forkedFrom ? { forked_from: forkedFrom } : {}),
|
|
211
230
|
// Workflow-child linkage: `workflow run` children carry the run id in
|
|
212
231
|
// env (spawn adapters set it via buildChildEnv), so their sessions and
|
|
213
232
|
// heartbeats join back to the run transcript + /workflows web view.
|
|
@@ -235,6 +254,10 @@ function buildEventData(eventType, ctx) {
|
|
|
235
254
|
return { prompt_text: value, ...(truncated ? { truncated: true } : {}) };
|
|
236
255
|
}
|
|
237
256
|
case "turn.stop": {
|
|
257
|
+
const lastAssistantMessage = p?.raw.last_assistant_message ?? "";
|
|
258
|
+
const fileLinkTelemetry = ctx.adapter === "codex"
|
|
259
|
+
? codexWslFileLinkTelemetry(ctx.coordRoot, p?.cwd, lastAssistantMessage)
|
|
260
|
+
: null;
|
|
238
261
|
return {
|
|
239
262
|
// Backfill the model for adapters that omit it at session.start
|
|
240
263
|
// (Claude Code). The transcript is populated with assistant turns by
|
|
@@ -252,8 +275,24 @@ function buildEventData(eventType, ctx) {
|
|
|
252
275
|
// text-only stop (box in last_assistant_message, no transcript), which
|
|
253
276
|
// the verdict now sees because agent-hook emits this turn.stop itself
|
|
254
277
|
// (the previous path passed those via the no-history fail-open).
|
|
255
|
-
status_box_present: scanStatusBoxPresent(p?.transcript_path) ||
|
|
256
|
-
|
|
278
|
+
status_box_present: scanStatusBoxPresent(p?.transcript_path) || lastAssistantMessage.includes("┌─ agent-"),
|
|
279
|
+
// Shadow measurement for the rule-2/3 detector: the loose scan above
|
|
280
|
+
// matches the box prefix ANYWHERE in the transcript tail, including
|
|
281
|
+
// the tool_result row the status command itself writes, so it can
|
|
282
|
+
// report a paste that never happened. The strict variant counts
|
|
283
|
+
// assistant text only (same window, same scanner as the session-name
|
|
284
|
+
// detection). Telemetry-only: the Stop verdict still reads
|
|
285
|
+
// status_box_present. Once the divergence rate between the two fields
|
|
286
|
+
// is known, the gate either flips to strict or the rule gets
|
|
287
|
+
// reconsidered; either way this field decides it with data.
|
|
288
|
+
status_box_present_strict: scanAssistantTextIncludes(p?.transcript_path, "┌─ agent-") ||
|
|
289
|
+
lastAssistantMessage.includes("┌─ agent-"),
|
|
290
|
+
// Whether the session's suggested name is satisfied as of this turn,
|
|
291
|
+
// and which name that refers to. Reported on every stop while a name
|
|
292
|
+
// exists (the stop-hook.session_name verdict reads it per turn); the
|
|
293
|
+
// transcript scan itself stops once the name has been sighted.
|
|
294
|
+
...sessionNamePresence(ctx.coordRoot, ctx.instanceId, lastAssistantMessage, (name) => scanAssistantTextIncludes(p?.transcript_path, name)),
|
|
295
|
+
...(fileLinkTelemetry ?? {}),
|
|
257
296
|
stop_hook_active: p?.stop_hook_active,
|
|
258
297
|
};
|
|
259
298
|
}
|
|
@@ -694,7 +733,7 @@ async function main() {
|
|
|
694
733
|
}
|
|
695
734
|
}
|
|
696
735
|
try {
|
|
697
|
-
const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, recovery?.briefing ?? "");
|
|
736
|
+
const injected = await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, adapter, payload?.cwd, recovery?.briefing ?? "");
|
|
698
737
|
if (injected && recovery) {
|
|
699
738
|
completeRecoveryInjection(coordRoot, owner.instance_id, sessionId, adapter, recovery, "UserPromptSubmit");
|
|
700
739
|
}
|
|
@@ -778,7 +817,7 @@ async function main() {
|
|
|
778
817
|
// re-prompts only via a `followup_message` it auto-submits. emitStopBlock
|
|
779
818
|
// writes the right shape and returns the exit code to use.
|
|
780
819
|
const { emitStopBlock } = await import("./adapter/output.js");
|
|
781
|
-
return emitStopBlock(adapter, verdict);
|
|
820
|
+
return emitStopBlock(adapter, verdict, coordRoot);
|
|
782
821
|
}
|
|
783
822
|
}
|
|
784
823
|
// Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
|
|
@@ -792,7 +831,19 @@ async function main() {
|
|
|
792
831
|
// call was restored here afterward.
|
|
793
832
|
if (norm.event_type === "tool.pre_use") {
|
|
794
833
|
try {
|
|
795
|
-
|
|
834
|
+
// Recorded fork lineage, heal-path flavor. A forked CC conversation
|
|
835
|
+
// never fires its own session.start (SessionStart fires under the
|
|
836
|
+
// PARENT's session id with source=resume, before the fork id is
|
|
837
|
+
// minted; verified 2026-08-05), so the fork's new instance first
|
|
838
|
+
// materializes right here. Gate detection on "no heartbeat yet" so the
|
|
839
|
+
// transcript scan runs once per instance lifetime, not per tool call.
|
|
840
|
+
let forkedFrom;
|
|
841
|
+
if (adapter === "claude-code" &&
|
|
842
|
+
payload?.transcript_path &&
|
|
843
|
+
!existsSync(join(coordRoot, ".harnery", "active", `${owner.instance_id}.json`))) {
|
|
844
|
+
forkedFrom = detectForkParent(payload.transcript_path, owner.instance_id);
|
|
845
|
+
}
|
|
846
|
+
healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, adapter, forkedFrom);
|
|
796
847
|
refreshPidmap(coordRoot, owner.instance_id, adapter, payload?.pid);
|
|
797
848
|
}
|
|
798
849
|
catch (err) {
|
|
@@ -813,6 +864,28 @@ async function main() {
|
|
|
813
864
|
catch (err) {
|
|
814
865
|
logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
|
|
815
866
|
}
|
|
867
|
+
// Windows-native Codex + WSL UNC only: block the one cross-shell shape
|
|
868
|
+
// proven to corrupt argument boundaries. Normal WSL argv calls, literal
|
|
869
|
+
// bash -s scripts, native Linux/macOS sessions, and every other adapter
|
|
870
|
+
// pass through. The host instructions name its concrete safe bridge.
|
|
871
|
+
const unsafeShellReason = unsafeCrossShellReason({
|
|
872
|
+
adapter,
|
|
873
|
+
cwd: payload?.cwd,
|
|
874
|
+
toolName: payload?.tool_name,
|
|
875
|
+
toolInput: payload?.tool_input,
|
|
876
|
+
});
|
|
877
|
+
if (unsafeShellReason) {
|
|
878
|
+
emit(coordRoot, {
|
|
879
|
+
event_type: "decision.block",
|
|
880
|
+
instance_id: owner.instance_id,
|
|
881
|
+
session_id: sessionId,
|
|
882
|
+
adapter,
|
|
883
|
+
data: { rule: "unsafe_cross_shell", reason: unsafeShellReason },
|
|
884
|
+
});
|
|
885
|
+
const { emitDeny } = await import("./adapter/output.js");
|
|
886
|
+
emitDeny(adapter, unsafeShellReason);
|
|
887
|
+
return 0;
|
|
888
|
+
}
|
|
816
889
|
// G-guard for ALL adapters. Claude Code previously ran this via a
|
|
817
890
|
// pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
|
|
818
891
|
// that adapter is now deleted, so agent-hook owns the deny for every adapter.
|
|
@@ -903,14 +976,40 @@ async function main() {
|
|
|
903
976
|
}
|
|
904
977
|
async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapter) {
|
|
905
978
|
const toolName = data.tool_name ?? "";
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
979
|
+
const rawTargets = collectGuardTargets(toolName, data);
|
|
980
|
+
if (rawTargets.length === 0)
|
|
981
|
+
return;
|
|
982
|
+
let targets;
|
|
983
|
+
if (agentsRequireGitFinalization(coordRoot)) {
|
|
984
|
+
const decisions = rawTargets.map((path) => classifyWriteClaimFinalization(coordRoot, path));
|
|
985
|
+
const denied = decisions.find((decision) => !decision.allow);
|
|
986
|
+
if (denied && !denied.allow) {
|
|
987
|
+
const { emitDeny } = await import("./adapter/output.js");
|
|
988
|
+
emitDeny(adapter, formatWriteClaimFinalizationDenial(denied, resolveBinName(coordRoot)));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const allowed = decisions.filter((decision) => decision.allow);
|
|
992
|
+
targets = allowed.map((decision) => ({
|
|
993
|
+
path: decision.path,
|
|
994
|
+
finalization: decision.descriptor,
|
|
995
|
+
}));
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
targets = rawTargets
|
|
999
|
+
.map((path) => canonicalize(coordRoot, path))
|
|
1000
|
+
.filter((path) => path !== null)
|
|
1001
|
+
.map((path) => ({ path }));
|
|
1002
|
+
}
|
|
909
1003
|
if (targets.length === 0)
|
|
910
1004
|
return;
|
|
911
1005
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
912
|
-
if (!existsSync(agentCoordBin))
|
|
1006
|
+
if (!existsSync(agentCoordBin)) {
|
|
1007
|
+
if (agentsRequireGitFinalization(coordRoot)) {
|
|
1008
|
+
const { emitDeny } = await import("./adapter/output.js");
|
|
1009
|
+
emitDeny(adapter, "Harnery denied the write before mutation because the claim coordinator is unavailable; repair the project's Harnery installation and retry.");
|
|
1010
|
+
}
|
|
913
1011
|
return;
|
|
1012
|
+
}
|
|
914
1013
|
// For apply_patch (multi-file), collect siblings so the deny reason names
|
|
915
1014
|
// them. For single-file tools the array has one entry.
|
|
916
1015
|
for (const target of targets) {
|
|
@@ -918,7 +1017,7 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
|
|
|
918
1017
|
rule: "claim",
|
|
919
1018
|
instance_id: instanceId,
|
|
920
1019
|
session_id: sessionId,
|
|
921
|
-
path: target,
|
|
1020
|
+
path: target.path,
|
|
922
1021
|
});
|
|
923
1022
|
const result = spawnSync(agentCoordBin, ["verdict"], {
|
|
924
1023
|
input: verdictReq,
|
|
@@ -936,10 +1035,11 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
|
|
|
936
1035
|
continue;
|
|
937
1036
|
}
|
|
938
1037
|
if (parsed.allow === false) {
|
|
939
|
-
let reason = parsed.reason ?? `Path ${target} is currently being edited by another agent.`;
|
|
1038
|
+
let reason = parsed.reason ?? `Path ${target.path} is currently being edited by another agent.`;
|
|
940
1039
|
if (targets.length > 1) {
|
|
941
1040
|
const siblings = targets
|
|
942
|
-
.filter((
|
|
1041
|
+
.filter((candidate) => candidate !== target)
|
|
1042
|
+
.map((candidate) => candidate.path)
|
|
943
1043
|
.slice(0, 3)
|
|
944
1044
|
.join(", ");
|
|
945
1045
|
if (siblings) {
|
|
@@ -950,6 +1050,23 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, adapte
|
|
|
950
1050
|
emitDeny(adapter, reason);
|
|
951
1051
|
return;
|
|
952
1052
|
}
|
|
1053
|
+
// The verdict mutates the live heartbeat immediately, but that file is a
|
|
1054
|
+
// projection and can be rebuilt at any later hook. Persist the acquisition
|
|
1055
|
+
// in the canonical stream too. This matters most for Codex apply_patch:
|
|
1056
|
+
// one tool call can claim several files, and tool.pre_use's clamped payload
|
|
1057
|
+
// is not a durable ownership representation for those targets.
|
|
1058
|
+
emit(coordRoot, {
|
|
1059
|
+
event_type: "claim.acquire",
|
|
1060
|
+
instance_id: instanceId,
|
|
1061
|
+
session_id: sessionId,
|
|
1062
|
+
adapter,
|
|
1063
|
+
source: "agent-hooks",
|
|
1064
|
+
data: {
|
|
1065
|
+
path: target.path,
|
|
1066
|
+
mode: "write",
|
|
1067
|
+
...(target.finalization ? { finalization: target.finalization } : {}),
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
953
1070
|
}
|
|
954
1071
|
}
|
|
955
1072
|
/** Canonicalize a path to monorepo-relative form. Absolute paths under
|
|
@@ -1008,15 +1125,19 @@ function parseApplyPatchPaths(data) {
|
|
|
1008
1125
|
}
|
|
1009
1126
|
return out;
|
|
1010
1127
|
}
|
|
1011
|
-
function healHeartbeatViaCli(coordRoot, instanceId, sessionId, adapter) {
|
|
1128
|
+
function healHeartbeatViaCli(coordRoot, instanceId, sessionId, adapter, forkedFrom) {
|
|
1012
1129
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
1013
1130
|
if (!existsSync(agentCoordBin))
|
|
1014
1131
|
return;
|
|
1015
1132
|
// Pass the detected adapter so a pruned Cursor/Codex heartbeat is recreated
|
|
1016
1133
|
// with the correct platform; without it, healHeartbeat defaults to
|
|
1017
|
-
//
|
|
1018
|
-
// heartbeat-writer.healHeartbeat.
|
|
1019
|
-
|
|
1134
|
+
// claude-code and the dashboard mislabels the agent. See
|
|
1135
|
+
// heartbeat-writer.healHeartbeat. --forked-from carries detected fork
|
|
1136
|
+
// lineage for an instance that never session.started (the CC fork flow).
|
|
1137
|
+
const args = ["heal-heartbeat", instanceId, sessionId, `--adapter=${adapter}`];
|
|
1138
|
+
if (forkedFrom)
|
|
1139
|
+
args.push(`--forked-from=${forkedFrom}`);
|
|
1140
|
+
spawnSync(agentCoordBin, args, {
|
|
1020
1141
|
encoding: "utf8",
|
|
1021
1142
|
timeout: 2000,
|
|
1022
1143
|
env: childEnv(coordRoot),
|
|
@@ -1238,7 +1359,7 @@ function cleanupSessionEnd(coordRoot, instanceId, reason) {
|
|
|
1238
1359
|
spawnSync(agentCoordBin, ["log", `SESSION_END reason=${reason}`, "--instance", instanceId], { encoding: "utf8", timeout: 2000 });
|
|
1239
1360
|
}
|
|
1240
1361
|
}
|
|
1241
|
-
async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, recoveryBriefing = "") {
|
|
1362
|
+
async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, adapter, workspaceCwd, recoveryBriefing = "") {
|
|
1242
1363
|
const agentCoordBin = coordBinPath("agent-coord", coordRoot) ?? "";
|
|
1243
1364
|
let additionalContext = "";
|
|
1244
1365
|
if (existsSync(agentCoordBin)) {
|
|
@@ -1272,6 +1393,12 @@ async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionI
|
|
|
1272
1393
|
if (result.status === 0 && result.stdout)
|
|
1273
1394
|
additionalContext = result.stdout.trim();
|
|
1274
1395
|
}
|
|
1396
|
+
if (adapter === "codex") {
|
|
1397
|
+
const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, workspaceCwd);
|
|
1398
|
+
if (fileLinkContext) {
|
|
1399
|
+
additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1275
1402
|
if (recoveryBriefing) {
|
|
1276
1403
|
additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
|
|
1277
1404
|
}
|
|
@@ -1310,7 +1437,7 @@ function emitSubagentStartContext(coordRoot, instanceId, sessionId, data, adapte
|
|
|
1310
1437
|
}
|
|
1311
1438
|
function adapterPlatform(adapter) {
|
|
1312
1439
|
if (adapter === "claude-code")
|
|
1313
|
-
return "
|
|
1440
|
+
return "claude-code";
|
|
1314
1441
|
return adapter;
|
|
1315
1442
|
}
|
|
1316
1443
|
async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, emittedData, adapter, recoveryBriefing = "") {
|
|
@@ -1359,6 +1486,21 @@ async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, e
|
|
|
1359
1486
|
if (recoveryBriefing) {
|
|
1360
1487
|
additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
|
|
1361
1488
|
}
|
|
1489
|
+
if (adapter === "codex" && isWslUncPath(emittedData.cwd)) {
|
|
1490
|
+
const fileLinkContext = renderCodexWslFileLinkContext(coordRoot, emittedData.cwd);
|
|
1491
|
+
if (fileLinkContext) {
|
|
1492
|
+
additionalContext = [additionalContext, fileLinkContext].filter(Boolean).join("\n\n");
|
|
1493
|
+
}
|
|
1494
|
+
const bridge = inspectCodexWslBridge(process.env, { expected: true });
|
|
1495
|
+
if (bridge && !bridge.ok) {
|
|
1496
|
+
additionalContext = [
|
|
1497
|
+
additionalContext,
|
|
1498
|
+
`Harnery hybrid warning: ${bridge.detail}. Run \`${resolveBinName(coordRoot)} doctor\` for the repair hint.`,
|
|
1499
|
+
]
|
|
1500
|
+
.filter(Boolean)
|
|
1501
|
+
.join("\n\n");
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1362
1504
|
if (!additionalContext)
|
|
1363
1505
|
return false;
|
|
1364
1506
|
const { emitContext } = await import("./adapter/output.js");
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface CodexWslBridgeStatus {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
detail: string;
|
|
4
|
+
threadIdPresent: boolean;
|
|
5
|
+
wslenvForwardsThreadId: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface CodexWslBridgeOptions {
|
|
8
|
+
/** The caller already knows this is a Codex adapter process. */
|
|
9
|
+
expected?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface CodexWslWorkspaceLinkMapping {
|
|
12
|
+
/** Absolute path seen by Linux-side tools. */
|
|
13
|
+
linuxRoot: string;
|
|
14
|
+
/** Forward-slash UNC path that Codex Desktop can use as a Markdown destination. */
|
|
15
|
+
hostRoot: string;
|
|
16
|
+
distro: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CodexWslFileLinkTelemetry {
|
|
19
|
+
/** Markdown destinations that still use the Linux root in a Windows-hosted WSL task. */
|
|
20
|
+
wsl_linux_file_link_count: number;
|
|
21
|
+
/** Bounded examples for local diagnosis; omitted when the count is zero. */
|
|
22
|
+
wsl_linux_file_link_examples?: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Whether an adapter-reported workspace is a Linux path exposed through WSL UNC. */
|
|
25
|
+
export declare function isWslUncPath(value: unknown): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Derive the Linux-to-host path pair from the actual adapter workspace. The
|
|
28
|
+
* roots must overlap so an unrelated WSL UNC path cannot authorize a mapping
|
|
29
|
+
* for some other checkout.
|
|
30
|
+
*/
|
|
31
|
+
export declare function codexWslWorkspaceLinkMapping(coordRootRaw: string, workspaceCwd: unknown): CodexWslWorkspaceLinkMapping | null;
|
|
32
|
+
/** Fresh adapter context for the model. Shell paths stay Linux-native; only
|
|
33
|
+
* Markdown destinations use the host-visible root. */
|
|
34
|
+
export declare function renderCodexWslFileLinkContext(coordRoot: string, workspaceCwd: unknown): string;
|
|
35
|
+
/**
|
|
36
|
+
* Count Markdown links that point at the Linux workspace root even though the
|
|
37
|
+
* adapter workspace is Windows-visible UNC. This is telemetry only: the Stop
|
|
38
|
+
* hook must not replace or retry an otherwise complete Codex reply.
|
|
39
|
+
*/
|
|
40
|
+
export declare function codexWslFileLinkTelemetry(coordRoot: string, workspaceCwd: unknown, assistantMessage: string): CodexWslFileLinkTelemetry | null;
|
|
41
|
+
/**
|
|
42
|
+
* Inspect the environment contract used when Windows-native Codex launches a
|
|
43
|
+
* Harnery hook or shell inside WSL. Returns null outside that hybrid mode.
|
|
44
|
+
*/
|
|
45
|
+
export declare function inspectCodexWslBridge(env?: NodeJS.ProcessEnv, options?: CodexWslBridgeOptions): CodexWslBridgeStatus | null;
|
|
46
|
+
//# sourceMappingURL=codex-wsl-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-wsl-bridge.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/codex-wsl-bridge.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;IACzB,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B;IAC3C,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC,wFAAwF;IACxF,yBAAyB,EAAE,MAAM,CAAC;IAClC,4EAA4E;IAC5E,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;CACzC;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMpD;AA2BD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,OAAO,GACpB,4BAA4B,GAAG,IAAI,CAarC;AAED;sDACsD;AACtD,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,CAS9F;AAmCD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,OAAO,EACrB,gBAAgB,EAAE,MAAM,GACvB,yBAAyB,GAAG,IAAI,CAYlC;AAOD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,GAAG,IAAI,CAsB7B"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Whether an adapter-reported workspace is a Linux path exposed through WSL UNC. */
|
|
2
|
+
export function isWslUncPath(value) {
|
|
3
|
+
if (typeof value !== "string")
|
|
4
|
+
return false;
|
|
5
|
+
return (/^\\\\(?:wsl\.localhost|wsl\$)\\/i.test(value) ||
|
|
6
|
+
/^\\\\\?\\unc\\(?:wsl\.localhost|wsl\$)\\/i.test(value));
|
|
7
|
+
}
|
|
8
|
+
function parseWslUncPath(value) {
|
|
9
|
+
if (typeof value !== "string")
|
|
10
|
+
return null;
|
|
11
|
+
const ordinary = value.replace(/^\\\\\?\\UNC\\/i, "\\\\");
|
|
12
|
+
const match = ordinary.match(/^\\\\(wsl\.localhost|wsl\$)\\([^\\]+)(?:\\(.*))?$/i);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
const host = match[1];
|
|
16
|
+
const distro = match[2];
|
|
17
|
+
const tail = (match[3] ?? "").split("\\").filter(Boolean).join("/");
|
|
18
|
+
return { host, distro, linuxPath: tail ? `/${tail}` : "/" };
|
|
19
|
+
}
|
|
20
|
+
function trimTrailingSlash(path) {
|
|
21
|
+
return path === "/" ? path : path.replace(/\/+$/, "");
|
|
22
|
+
}
|
|
23
|
+
function isWithinOrEqual(path, root) {
|
|
24
|
+
return path === root || path.startsWith(`${root}/`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Derive the Linux-to-host path pair from the actual adapter workspace. The
|
|
28
|
+
* roots must overlap so an unrelated WSL UNC path cannot authorize a mapping
|
|
29
|
+
* for some other checkout.
|
|
30
|
+
*/
|
|
31
|
+
export function codexWslWorkspaceLinkMapping(coordRootRaw, workspaceCwd) {
|
|
32
|
+
const parsed = parseWslUncPath(workspaceCwd);
|
|
33
|
+
const linuxRoot = trimTrailingSlash(coordRootRaw.replace(/\\/g, "/"));
|
|
34
|
+
if (!parsed || !linuxRoot.startsWith("/"))
|
|
35
|
+
return null;
|
|
36
|
+
const linuxCwd = trimTrailingSlash(parsed.linuxPath);
|
|
37
|
+
if (!isWithinOrEqual(linuxCwd, linuxRoot) && !isWithinOrEqual(linuxRoot, linuxCwd)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
linuxRoot,
|
|
42
|
+
hostRoot: `//${parsed.host}/${parsed.distro}${linuxRoot}`,
|
|
43
|
+
distro: parsed.distro,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Fresh adapter context for the model. Shell paths stay Linux-native; only
|
|
47
|
+
* Markdown destinations use the host-visible root. */
|
|
48
|
+
export function renderCodexWslFileLinkContext(coordRoot, workspaceCwd) {
|
|
49
|
+
const mapping = codexWslWorkspaceLinkMapping(coordRoot, workspaceCwd);
|
|
50
|
+
if (!mapping)
|
|
51
|
+
return "";
|
|
52
|
+
return ("Codex WSL file links: this task is hosted by Windows while tools run inside WSL. " +
|
|
53
|
+
`For clickable local-file Markdown links, replace the Linux workspace root \`${mapping.linuxRoot}\` with the host-visible root \`${mapping.hostRoot}\`. ` +
|
|
54
|
+
`Example: \`[label](<${mapping.hostRoot}/path/to/file>)\`. ` +
|
|
55
|
+
"Keep Linux paths unchanged in shell commands and tool inputs.");
|
|
56
|
+
}
|
|
57
|
+
function withoutMarkdownCode(text) {
|
|
58
|
+
const output = [];
|
|
59
|
+
let fence = null;
|
|
60
|
+
for (const line of text.split(/\r?\n/)) {
|
|
61
|
+
const candidate = line.match(/^\s{0,3}(`{3,}|~{3,})/);
|
|
62
|
+
if (fence) {
|
|
63
|
+
if (candidate && candidate[1][0] === fence.marker && candidate[1].length >= fence.length) {
|
|
64
|
+
fence = null;
|
|
65
|
+
}
|
|
66
|
+
output.push("");
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (candidate) {
|
|
70
|
+
fence = { marker: candidate[1][0], length: candidate[1].length };
|
|
71
|
+
output.push("");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
output.push(line.replace(/(`+)([^`\n]*?)\1/g, ""));
|
|
75
|
+
}
|
|
76
|
+
return output.join("\n");
|
|
77
|
+
}
|
|
78
|
+
function markdownDestinations(text) {
|
|
79
|
+
const out = [];
|
|
80
|
+
const visible = withoutMarkdownCode(text);
|
|
81
|
+
const re = /!?\[[^\]\n]*\]\(\s*(?:<([^>\n]+)>|([^\s)\n]+))/g;
|
|
82
|
+
for (const match of visible.matchAll(re)) {
|
|
83
|
+
const target = match[1] ?? match[2];
|
|
84
|
+
if (target)
|
|
85
|
+
out.push(target);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Count Markdown links that point at the Linux workspace root even though the
|
|
91
|
+
* adapter workspace is Windows-visible UNC. This is telemetry only: the Stop
|
|
92
|
+
* hook must not replace or retry an otherwise complete Codex reply.
|
|
93
|
+
*/
|
|
94
|
+
export function codexWslFileLinkTelemetry(coordRoot, workspaceCwd, assistantMessage) {
|
|
95
|
+
const mapping = codexWslWorkspaceLinkMapping(coordRoot, workspaceCwd);
|
|
96
|
+
if (!mapping)
|
|
97
|
+
return null;
|
|
98
|
+
const mismatches = markdownDestinations(assistantMessage).filter((target) => isWithinOrEqual(target, mapping.linuxRoot));
|
|
99
|
+
return {
|
|
100
|
+
wsl_linux_file_link_count: mismatches.length,
|
|
101
|
+
...(mismatches.length > 0
|
|
102
|
+
? { wsl_linux_file_link_examples: mismatches.slice(0, 3).map((path) => path.slice(0, 240)) }
|
|
103
|
+
: {}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function wslenvForwards(wslenv, variable) {
|
|
107
|
+
if (!wslenv)
|
|
108
|
+
return false;
|
|
109
|
+
return wslenv.split(":").some((entry) => entry.split("/", 1)[0] === variable);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Inspect the environment contract used when Windows-native Codex launches a
|
|
113
|
+
* Harnery hook or shell inside WSL. Returns null outside that hybrid mode.
|
|
114
|
+
*/
|
|
115
|
+
export function inspectCodexWslBridge(env = process.env, options = {}) {
|
|
116
|
+
const inWsl = Boolean(env.WSL_DISTRO_NAME?.trim() || env.WSL_INTEROP?.trim());
|
|
117
|
+
if (!inWsl)
|
|
118
|
+
return null;
|
|
119
|
+
const threadIdPresent = Boolean(env.CODEX_THREAD_ID?.trim());
|
|
120
|
+
const wslenvForwardsThreadId = wslenvForwards(env.WSLENV, "CODEX_THREAD_ID");
|
|
121
|
+
if (!options.expected && !threadIdPresent && !wslenvForwardsThreadId)
|
|
122
|
+
return null;
|
|
123
|
+
const problems = [];
|
|
124
|
+
if (!threadIdPresent)
|
|
125
|
+
problems.push("CODEX_THREAD_ID did not reach WSL");
|
|
126
|
+
if (!wslenvForwardsThreadId)
|
|
127
|
+
problems.push("WSLENV does not forward CODEX_THREAD_ID");
|
|
128
|
+
const distro = env.WSL_DISTRO_NAME?.trim();
|
|
129
|
+
return {
|
|
130
|
+
ok: problems.length === 0,
|
|
131
|
+
detail: problems.length === 0
|
|
132
|
+
? `thread identity forwarded through WSLENV${distro ? ` (${distro})` : ""}`
|
|
133
|
+
: problems.join("; "),
|
|
134
|
+
threadIdPresent,
|
|
135
|
+
wslenvForwardsThreadId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -40,7 +40,7 @@ export declare function journalArchive(repoRoot: string, owner: string): void;
|
|
|
40
40
|
* Fire the optional host session-sync extension on turn stop / session end.
|
|
41
41
|
* harnery core has no session-telemetry sink of its own; a host that wants one
|
|
42
42
|
* drops an executable at
|
|
43
|
-
* `scripts/hooks/
|
|
43
|
+
* `scripts/hooks/harness/claude-code/extensions/session-sync.sh` under the coord
|
|
44
44
|
* root, and core runs it detached + unref'd so a slow sink never blocks the
|
|
45
45
|
* hook. `force` arrives as argv $1 ("1" on session end, "0" on turn stop) so the
|
|
46
46
|
* host can rate-limit the stop path and force-flush on end. No-op when the
|
|
@@ -113,7 +113,7 @@ export function journalArchive(repoRoot, owner) {
|
|
|
113
113
|
* Fire the optional host session-sync extension on turn stop / session end.
|
|
114
114
|
* harnery core has no session-telemetry sink of its own; a host that wants one
|
|
115
115
|
* drops an executable at
|
|
116
|
-
* `scripts/hooks/
|
|
116
|
+
* `scripts/hooks/harness/claude-code/extensions/session-sync.sh` under the coord
|
|
117
117
|
* root, and core runs it detached + unref'd so a slow sink never blocks the
|
|
118
118
|
* hook. `force` arrives as argv $1 ("1" on session end, "0" on turn stop) so the
|
|
119
119
|
* host can rate-limit the stop path and force-flush on end. No-op when the
|
|
@@ -123,7 +123,7 @@ export function journalArchive(repoRoot, owner) {
|
|
|
123
123
|
*/
|
|
124
124
|
export function runSessionSyncExtension(repoRoot, force) {
|
|
125
125
|
try {
|
|
126
|
-
const script = join(repoRoot, "scripts", "hooks", "
|
|
126
|
+
const script = join(repoRoot, "scripts", "hooks", "harness", "claude-code", "extensions", "session-sync.sh");
|
|
127
127
|
if (!existsSync(script))
|
|
128
128
|
return;
|
|
129
129
|
const child = spawn("bash", [script, force ? "1" : "0"], {
|
|
@@ -147,7 +147,7 @@ export function runTurnSummary(repoRoot, owner, sessionId, transcriptPath) {
|
|
|
147
147
|
try {
|
|
148
148
|
if (!transcriptPath || !existsSync(transcriptPath))
|
|
149
149
|
return;
|
|
150
|
-
const script = join(repoRoot, "scripts", "hooks", "
|
|
150
|
+
const script = join(repoRoot, "scripts", "hooks", "harness", "claude-code", "extensions", "turn-summary.sh");
|
|
151
151
|
if (!existsSync(script))
|
|
152
152
|
return;
|
|
153
153
|
const child = spawn("bash", [script, owner, sessionId, transcriptPath], {
|
|
@@ -69,6 +69,24 @@ export type TurnStop = EventEnvelope<"turn.stop", {
|
|
|
69
69
|
tool_call_count: number;
|
|
70
70
|
text_length: number;
|
|
71
71
|
status_box_present: boolean;
|
|
72
|
+
/** Strict variant of status_box_present: assistant text blocks only, so a
|
|
73
|
+
* tool_result carrying the box (the status command's own output) does not
|
|
74
|
+
* count. Shadow telemetry while the rule-2/3 detector decision is open;
|
|
75
|
+
* the Stop verdict does not read it yet. */
|
|
76
|
+
status_box_present_strict?: boolean;
|
|
77
|
+
/** Whether the session's suggested name is satisfied as of this turn:
|
|
78
|
+
* either this reply showed it, or an earlier reply did. Present whenever a
|
|
79
|
+
* suggested name exists, since the Stop verdict reads it per turn. */
|
|
80
|
+
session_name_present?: boolean;
|
|
81
|
+
/** Which suggested name `session_name_present` refers to. A projector
|
|
82
|
+
* rebuild attributes the sighting to this name instead of to whichever
|
|
83
|
+
* name is current during the replay. */
|
|
84
|
+
session_name_present_for?: string;
|
|
85
|
+
/** Shadow telemetry for Windows-hosted Codex workspaces: Markdown link
|
|
86
|
+
* destinations that still use the Linux coordination root. */
|
|
87
|
+
wsl_linux_file_link_count?: number;
|
|
88
|
+
/** Up to three bounded examples for diagnosing the mismatches locally. */
|
|
89
|
+
wsl_linux_file_link_examples?: string[];
|
|
72
90
|
}>;
|
|
73
91
|
export type CommandStart = EventEnvelope<"command.start", {
|
|
74
92
|
cmd_id: string;
|
|
@@ -164,6 +182,10 @@ export type ContextRecoveryInjected = EventEnvelope<"context.recovery.injected",
|
|
|
164
182
|
export type ClaimAcquire = EventEnvelope<"claim.acquire", {
|
|
165
183
|
path: string;
|
|
166
184
|
mode: "read" | "write";
|
|
185
|
+
finalization?: {
|
|
186
|
+
disposition: "git" | "output";
|
|
187
|
+
root: string;
|
|
188
|
+
};
|
|
167
189
|
}>;
|
|
168
190
|
export type ClaimRelease = EventEnvelope<"claim.release", {
|
|
169
191
|
path: string;
|
|
@@ -177,6 +199,11 @@ export type ClaimConflict = EventEnvelope<"claim.conflict", {
|
|
|
177
199
|
export type StateTaskSet = EventEnvelope<"state.task_set", {
|
|
178
200
|
task: string;
|
|
179
201
|
cleared: boolean;
|
|
202
|
+
/** True exactly when this call produced the session name (first non-empty
|
|
203
|
+
* declaration of a human-facing session); never true with a null name. */
|
|
204
|
+
first_of_session?: boolean;
|
|
205
|
+
/** The name produced by this call, for the Stop-hook naming rule. */
|
|
206
|
+
suggested_session_name?: string | null;
|
|
180
207
|
}>;
|
|
181
208
|
export type StateStatusChecked = EventEnvelope<"state.status_checked", {
|
|
182
209
|
format: "box" | "json" | "table";
|