harnery 0.42.0 → 0.44.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/README.md +3 -3
- package/dist/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +95 -52
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +16 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- package/dist/commands/workflow.js +24 -30
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +19 -17
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +64 -43
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/agents/state/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +120 -23
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- package/dist/core/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +9 -1
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +47 -27
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/servers/index.d.ts +24 -13
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +132 -92
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +3 -9
- package/dist/core/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- package/dist/core/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +9 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/run-state.d.ts +3 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +31 -27
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +270 -6
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +21 -13
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- package/dist/lib/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/gate.js +13 -6
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +8 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +151 -74
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +16 -4
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/commands/workflow.ts +30 -30
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +35 -20
- package/src/core/agents/render/session-context.ts +92 -48
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +57 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +139 -22
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- package/src/core/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +71 -24
- package/src/core/resources/status.ts +57 -8
- package/src/core/servers/index.ts +177 -91
- package/src/core/servers/tunnels.ts +3 -11
- package/src/core/storage/process-log.ts +17 -2
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +88 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/run-state.ts +31 -29
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +315 -5
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +58 -0
- package/src/core/workflow/workspaces/recovery.ts +25 -13
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
- package/src/lib/zip/index.ts +483 -0
package/src/core/hooks/cli.ts
CHANGED
|
@@ -57,7 +57,13 @@ import {
|
|
|
57
57
|
} from "../agents/state/heartbeat-writer.ts";
|
|
58
58
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
59
59
|
import { ensureLiveCoordinationHeartbeat } from "../agents/state/live-coordination-writer.ts";
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
assignName,
|
|
62
|
+
readForkParent,
|
|
63
|
+
readForkState,
|
|
64
|
+
recordForkDetection,
|
|
65
|
+
resolveName,
|
|
66
|
+
} from "../agents/state/names.ts";
|
|
61
67
|
import { writePidmapRow } from "../agents/state/pidmap.ts";
|
|
62
68
|
import {
|
|
63
69
|
agentsRequireGitFinalization,
|
|
@@ -115,6 +121,7 @@ import {
|
|
|
115
121
|
soundForEvent,
|
|
116
122
|
} from "./effects/index.ts";
|
|
117
123
|
import { toolInputHash, toolTargetHash } from "./events/input-hash.ts";
|
|
124
|
+
import { detectClaudeCodeFork } from "./fork-lineage.ts";
|
|
118
125
|
import { canonicalize } from "./guard-path.ts";
|
|
119
126
|
import {
|
|
120
127
|
beginHookHealth,
|
|
@@ -135,6 +142,7 @@ import {
|
|
|
135
142
|
} from "./resolve/transcript.ts";
|
|
136
143
|
import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.ts";
|
|
137
144
|
import { sessionNamePresence } from "./session-name-presence.ts";
|
|
145
|
+
import { shellRmReason } from "./shell-rm.ts";
|
|
138
146
|
import { shellWaiterReason } from "./shell-waiter.ts";
|
|
139
147
|
|
|
140
148
|
interface Argv {
|
|
@@ -250,12 +258,14 @@ function assignNameInProcess(
|
|
|
250
258
|
coordRoot: string,
|
|
251
259
|
instanceId: string,
|
|
252
260
|
kind: "session" | "subagent" | "transient",
|
|
253
|
-
forkedFrom?: string,
|
|
261
|
+
fork?: { forkedFrom?: string; fork?: boolean; forkChecked?: boolean },
|
|
254
262
|
): { name: string; kind: string } | null {
|
|
255
263
|
try {
|
|
256
264
|
const name = assignName(coordRoot, instanceId, kind, {
|
|
257
265
|
freshnessSecs: coordFreshnessSeconds(coordRoot),
|
|
258
|
-
...(forkedFrom ? { forkedFrom } : {}),
|
|
266
|
+
...(fork?.forkedFrom ? { forkedFrom: fork.forkedFrom } : {}),
|
|
267
|
+
...(fork?.fork ? { fork: true } : {}),
|
|
268
|
+
...(fork?.forkChecked ? { forkChecked: true } : {}),
|
|
259
269
|
});
|
|
260
270
|
return { name, kind };
|
|
261
271
|
} catch {
|
|
@@ -263,6 +273,66 @@ function assignNameInProcess(
|
|
|
263
273
|
}
|
|
264
274
|
}
|
|
265
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Name a main session, recording fork lineage when the instance is new. Every
|
|
278
|
+
* main-session naming path goes through here: whichever hook first sees a new
|
|
279
|
+
* instance writes its history row, and lineage can only ride that first row
|
|
280
|
+
* (later detection appends a `fork.detected` row instead).
|
|
281
|
+
*/
|
|
282
|
+
function assignSessionNameInProcess(
|
|
283
|
+
coordRoot: string,
|
|
284
|
+
instanceId: string,
|
|
285
|
+
payload: ParsedPayload | null,
|
|
286
|
+
adapter: Adapter,
|
|
287
|
+
): { name: string; kind: string } | null {
|
|
288
|
+
let fork: { forkedFrom?: string; fork?: boolean; forkChecked?: boolean } | undefined;
|
|
289
|
+
try {
|
|
290
|
+
if (
|
|
291
|
+
adapter === "claude-code" &&
|
|
292
|
+
payload?.session_id === instanceId &&
|
|
293
|
+
!resolveName(coordRoot, instanceId)
|
|
294
|
+
) {
|
|
295
|
+
const detected = detectClaudeCodeFork({
|
|
296
|
+
coordRoot,
|
|
297
|
+
transcriptPath: payload.transcript_path,
|
|
298
|
+
sessionId: instanceId,
|
|
299
|
+
source: payload.source,
|
|
300
|
+
});
|
|
301
|
+
fork = {
|
|
302
|
+
...(detected.forkedFrom ? { forkedFrom: detected.forkedFrom } : {}),
|
|
303
|
+
fork: detected.fork,
|
|
304
|
+
forkChecked: detected.checked,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
fork = undefined;
|
|
309
|
+
}
|
|
310
|
+
return assignNameInProcess(coordRoot, instanceId, "session", fork);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Finish fork detection that session start could not: a CLI fork starts before
|
|
315
|
+
* its transcript exists, and a mid-flight instance may never see SessionStart.
|
|
316
|
+
* Runs once per instance; the appended `fork_checked` row stops later calls.
|
|
317
|
+
*/
|
|
318
|
+
function healForkLineage(
|
|
319
|
+
coordRoot: string,
|
|
320
|
+
instanceId: string,
|
|
321
|
+
payload: ParsedPayload | null,
|
|
322
|
+
): void {
|
|
323
|
+
if (!payload?.transcript_path || payload.session_id !== instanceId) return;
|
|
324
|
+
const state = readForkState(coordRoot, instanceId);
|
|
325
|
+
if (!state || state.checked) return;
|
|
326
|
+
const detected = detectClaudeCodeFork({
|
|
327
|
+
coordRoot,
|
|
328
|
+
transcriptPath: payload.transcript_path,
|
|
329
|
+
sessionId: instanceId,
|
|
330
|
+
knownFork: state.fork,
|
|
331
|
+
});
|
|
332
|
+
if (!detected.checked) return;
|
|
333
|
+
recordForkDetection(coordRoot, instanceId, detected);
|
|
334
|
+
}
|
|
335
|
+
|
|
266
336
|
/** Mid-flight sessions can emit tools before SessionStart assigns a pool name.
|
|
267
337
|
* Stamp the disposable cache so Codec does not render the 8-character id. */
|
|
268
338
|
function ensureSessionDisplayName(
|
|
@@ -270,13 +340,14 @@ function ensureSessionDisplayName(
|
|
|
270
340
|
instanceId: string,
|
|
271
341
|
sessionId: string,
|
|
272
342
|
adapter: Adapter,
|
|
343
|
+
payload: ParsedPayload | null,
|
|
273
344
|
): void {
|
|
274
345
|
if (coordEnv("WORKFLOW_CHILD") === "1") return;
|
|
275
346
|
const row =
|
|
276
347
|
readLiveCoordinationRow(coordRoot, instanceId) ?? readHeartbeat(coordRoot, instanceId);
|
|
277
348
|
if (row?.kind === "subagent" || row?.kind === "transient" || row?.workflow_run_id) return;
|
|
278
349
|
if (row?.name?.trim()) return;
|
|
279
|
-
const assigned =
|
|
350
|
+
const assigned = assignSessionNameInProcess(coordRoot, instanceId, payload, adapter);
|
|
280
351
|
if (!assigned?.name) return;
|
|
281
352
|
if (!readHeartbeat(coordRoot, instanceId)) {
|
|
282
353
|
try {
|
|
@@ -345,22 +416,22 @@ function buildEventData(
|
|
|
345
416
|
// The platform label is the adapter id itself; enumerating and defaulting
|
|
346
417
|
// to codex silently mislabels any adapter not in the list.
|
|
347
418
|
const adapterPlatform = ctx.adapter;
|
|
348
|
-
// Recorded fork lineage is NOT detected here. On claude-code a fork
|
|
349
|
-
// never fires its own session.started — SessionStart fires under the
|
|
350
|
-
// PARENT's session id (source=resume) before the fork id is minted
|
|
351
|
-
// (verified 2026-08-05) — so detection at this point can only mislabel
|
|
352
|
-
// the resumed parent. The fork's new instance is caught by the
|
|
353
|
-
// tool.requested heal path instead. The forkedFrom plumbing below stays
|
|
354
|
-
// for adapters that DO report a parent at session start.
|
|
355
|
-
const forkedFrom: string | undefined = undefined;
|
|
356
419
|
// Assign (or recover) name + kind in-process. Idempotent: resume
|
|
357
|
-
// returns the original name; new owner consumes a counter slot
|
|
420
|
+
// returns the original name; new owner consumes a counter slot and,
|
|
421
|
+
// on claude-code, runs fork detection (fork-lineage.ts). Since Claude
|
|
422
|
+
// Code 2.1.283 both fork flows fire SessionStart under the FORK's own
|
|
423
|
+
// id (verified 2026-10-07), superseding the 2026-08-05 finding that a
|
|
424
|
+
// fork never started on its own id. A resumed parent keeps its existing
|
|
425
|
+
// history row, so it is never relabeled here.
|
|
358
426
|
// Workflow children stay unnamed, as on the tool path: a pool name
|
|
359
427
|
// would make them the addressee of mail held for that name.
|
|
360
428
|
const assigned =
|
|
361
429
|
coordEnv("WORKFLOW_CHILD") === "1"
|
|
362
430
|
? undefined
|
|
363
|
-
:
|
|
431
|
+
: assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
|
|
432
|
+
const forkedFrom = assigned
|
|
433
|
+
? readForkParent(ctx.coordRoot, ctx.instanceId)?.instance_id
|
|
434
|
+
: undefined;
|
|
364
435
|
// Write the adapter pid-map row so `harn agents whoami` ppid-walks find
|
|
365
436
|
// this owner. Prefer the payload pid (the actual claude binary), then the
|
|
366
437
|
// anchor walk (the `node` ancestor for Cursor, which has no payload pid),
|
|
@@ -429,7 +500,7 @@ function buildEventData(
|
|
|
429
500
|
coordEnv("WORKFLOW_CHILD") !== "1" &&
|
|
430
501
|
!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name
|
|
431
502
|
) {
|
|
432
|
-
|
|
503
|
+
assignSessionNameInProcess(ctx.coordRoot, ctx.instanceId, p, ctx.adapter);
|
|
433
504
|
}
|
|
434
505
|
const prompt = p?.prompt ?? "";
|
|
435
506
|
const { value, truncated } = clampString(prompt, 4000);
|
|
@@ -851,7 +922,7 @@ async function main(): Promise<number> {
|
|
|
851
922
|
!priorCoordination?.workflow_run_id &&
|
|
852
923
|
!priorCoordination?.name?.trim()
|
|
853
924
|
) {
|
|
854
|
-
|
|
925
|
+
assignSessionNameInProcess(coordRoot, owner.instance_id, payload, adapter);
|
|
855
926
|
}
|
|
856
927
|
|
|
857
928
|
const data = buildEventData(norm.event_type, {
|
|
@@ -1063,11 +1134,18 @@ function recordSignal(run: HookRun): void {
|
|
|
1063
1134
|
}
|
|
1064
1135
|
if (norm.event_type !== "session.ended") {
|
|
1065
1136
|
try {
|
|
1066
|
-
ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter);
|
|
1137
|
+
ensureSessionDisplayName(coordRoot, owner.instance_id, sessionId, adapter, payload);
|
|
1067
1138
|
} catch (error) {
|
|
1068
1139
|
logError(coordRoot, error, { phase: "session-display-name" });
|
|
1069
1140
|
}
|
|
1070
1141
|
}
|
|
1142
|
+
if (norm.event_type === "tool.requested" && adapter === "claude-code" && run.topLevelSession) {
|
|
1143
|
+
try {
|
|
1144
|
+
healForkLineage(coordRoot, owner.instance_id, payload);
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
logError(coordRoot, error, { phase: "fork-lineage-heal" });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1071
1149
|
const v3EventId =
|
|
1072
1150
|
v3Result && "event" in v3Result
|
|
1073
1151
|
? v3Result.event.event_id
|
|
@@ -1559,6 +1637,17 @@ async function handleToolRequested(run: HookRun): Promise<number | undefined> {
|
|
|
1559
1637
|
return 0;
|
|
1560
1638
|
}
|
|
1561
1639
|
|
|
1640
|
+
const rmReason = shellRmReason(
|
|
1641
|
+
payload?.tool_name,
|
|
1642
|
+
payload?.tool_input,
|
|
1643
|
+
resolveBinName(coordRoot),
|
|
1644
|
+
);
|
|
1645
|
+
if (rmReason) {
|
|
1646
|
+
const { emitDeny } = await import("./adapter/output.ts");
|
|
1647
|
+
emitDeny(adapter, rmReason);
|
|
1648
|
+
return 0;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1562
1651
|
// G-guard for ALL adapters. Claude Code previously ran this via a
|
|
1563
1652
|
// pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
|
|
1564
1653
|
// that adapter is now deleted, so agent-hook owns the deny for every adapter.
|
|
@@ -2209,12 +2298,16 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
2209
2298
|
if (coordEnv("WORKFLOW_CHILD") === "1") return false;
|
|
2210
2299
|
const behavior = adapterBehavior(adapter);
|
|
2211
2300
|
const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
|
|
2301
|
+
let peerMessageIds: readonly string[] = [];
|
|
2212
2302
|
let additionalContext = renderPromptContext({
|
|
2213
2303
|
coordRoot,
|
|
2214
2304
|
instanceId,
|
|
2215
2305
|
sessionId,
|
|
2216
2306
|
sessionNameNudge: true,
|
|
2217
2307
|
...behavior.promptContextNudges,
|
|
2308
|
+
onInboxPrepared: (ids) => {
|
|
2309
|
+
peerMessageIds = ids;
|
|
2310
|
+
},
|
|
2218
2311
|
}).trim();
|
|
2219
2312
|
const fileLinkContext = behavior.fileLinkContext?.(coordRoot, workspaceCwd) ?? "";
|
|
2220
2313
|
if (fileLinkContext) {
|
|
@@ -2229,7 +2322,12 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
2229
2322
|
if (!additionalContext) return false;
|
|
2230
2323
|
|
|
2231
2324
|
const { emitContext } = await import("./adapter/output.ts");
|
|
2232
|
-
|
|
2325
|
+
const emitted = emitContext(adapter, "UserPromptSubmit", additionalContext);
|
|
2326
|
+
if (emitted) {
|
|
2327
|
+
const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
|
|
2328
|
+
await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
|
|
2329
|
+
}
|
|
2330
|
+
return emitted;
|
|
2233
2331
|
}
|
|
2234
2332
|
|
|
2235
2333
|
async function emitSubagentStartContext(
|
|
@@ -2253,19 +2351,26 @@ async function emitSubagentStartContext(
|
|
|
2253
2351
|
// is around. Reuse prompt-context (which dedups against the per-owner hash);
|
|
2254
2352
|
// first call will always emit.
|
|
2255
2353
|
let combined = message;
|
|
2354
|
+
let peerMessageIds: readonly string[] = [];
|
|
2256
2355
|
const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
|
|
2257
2356
|
const context = renderPromptContext({
|
|
2258
2357
|
coordRoot,
|
|
2259
2358
|
instanceId,
|
|
2260
2359
|
sessionId,
|
|
2261
2360
|
agentName: subagentName,
|
|
2361
|
+
onInboxPrepared: (ids) => {
|
|
2362
|
+
peerMessageIds = ids;
|
|
2363
|
+
},
|
|
2262
2364
|
}).trim();
|
|
2263
2365
|
if (context) combined = `${message}\n\n${context}`;
|
|
2264
2366
|
|
|
2265
2367
|
// Use SubagentStart event-name in CC's hookSpecificOutput shape; cursor's
|
|
2266
2368
|
// flat `additional_context` works the same way.
|
|
2267
2369
|
const { emitContext } = await import("./adapter/output.ts");
|
|
2268
|
-
emitContext(adapter, "SubagentStart", combined)
|
|
2370
|
+
if (emitContext(adapter, "SubagentStart", combined)) {
|
|
2371
|
+
const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
|
|
2372
|
+
await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
|
|
2373
|
+
}
|
|
2269
2374
|
}
|
|
2270
2375
|
|
|
2271
2376
|
function adapterPlatform(adapter: Adapter): string {
|
|
@@ -2284,6 +2389,7 @@ async function emitSessionStartSystemMessage(
|
|
|
2284
2389
|
const behavior = adapterBehavior(adapter);
|
|
2285
2390
|
const workflowChild = coordEnv("WORKFLOW_CHILD") === "1";
|
|
2286
2391
|
let additionalContext = "";
|
|
2392
|
+
let peerMessageIds: readonly string[] = [];
|
|
2287
2393
|
// Opportunistic reconciliation makes normal session starts a failsafe for
|
|
2288
2394
|
// archive, idle, cascade, and host lifecycle observations. It also runs the
|
|
2289
2395
|
// stale cache sweep through the shared ADR 0077 composition.
|
|
@@ -2303,6 +2409,9 @@ async function emitSessionStartSystemMessage(
|
|
|
2303
2409
|
sessionId,
|
|
2304
2410
|
agentName: agentName || undefined,
|
|
2305
2411
|
platformLabel: behavior.sessionStartPlatformLabel,
|
|
2412
|
+
onInboxPrepared: (ids) => {
|
|
2413
|
+
peerMessageIds = ids;
|
|
2414
|
+
},
|
|
2306
2415
|
}).trim();
|
|
2307
2416
|
}
|
|
2308
2417
|
|
|
@@ -2331,7 +2440,12 @@ async function emitSessionStartSystemMessage(
|
|
|
2331
2440
|
}
|
|
2332
2441
|
}
|
|
2333
2442
|
const { emitContext } = await import("./adapter/output.ts");
|
|
2334
|
-
|
|
2443
|
+
const emitted = emitContext(adapter, "SessionStart", additionalContext);
|
|
2444
|
+
if (emitted) {
|
|
2445
|
+
const { acknowledgeEmittedPeerInbox } = await import("../inbox/peer-messages.ts");
|
|
2446
|
+
await acknowledgeEmittedPeerInbox(coordRoot, instanceId, peerMessageIds);
|
|
2447
|
+
}
|
|
2448
|
+
return emitted;
|
|
2335
2449
|
}
|
|
2336
2450
|
|
|
2337
2451
|
function completeRecoveryInjection(
|
|
@@ -2340,8 +2454,11 @@ function completeRecoveryInjection(
|
|
|
2340
2454
|
sessionId: string,
|
|
2341
2455
|
recovery: PreparedContextRecovery,
|
|
2342
2456
|
): void {
|
|
2343
|
-
completeContextRecovery(coordRoot, {
|
|
2344
|
-
|
|
2457
|
+
completeContextRecovery(coordRoot, {
|
|
2458
|
+
sessionId,
|
|
2459
|
+
instanceId,
|
|
2460
|
+
checkpoint: recovery.checkpoint,
|
|
2461
|
+
});
|
|
2345
2462
|
}
|
|
2346
2463
|
|
|
2347
2464
|
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code fork detection: decide whether a new main-session instance is a
|
|
3
|
+
* branch of another session, and which one.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes exist as of Claude Code 2.1.283 (verified 2026-10-07):
|
|
6
|
+
*
|
|
7
|
+
* - CLI `--resume <id> --fork-session` fires SessionStart under the FORK's id
|
|
8
|
+
* with `source: "fork"`, before its transcript exists, and names no parent.
|
|
9
|
+
* The transcript later holds the parent's message uuids with every row's
|
|
10
|
+
* `sessionId` rewritten, so the parent is found by uuid containment
|
|
11
|
+
* (`detectForkParent`) once the transcript is on disk.
|
|
12
|
+
* - The desktop app's "Fork from here" copies the parent's rows into the
|
|
13
|
+
* fork's transcript with their original `sessionId`, so inherited rows
|
|
14
|
+
* name the parent directly (`detectInheritedSessionParent`).
|
|
15
|
+
*
|
|
16
|
+
* Inherited rows alone do not prove a fork: a continuation of a finished
|
|
17
|
+
* session can carry them too. The parent's liveness separates the two. A
|
|
18
|
+
* parent with an unended generation is still running (or resumable) and owns
|
|
19
|
+
* its name, task, and claims, so the new instance is a fork. A finished
|
|
20
|
+
* parent makes it a continuation, which keeps the generic notice.
|
|
21
|
+
*
|
|
22
|
+
* Everything here fails open: an unreadable transcript or an unknown layout
|
|
23
|
+
* yields "not a fork", never an error.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
28
|
+
import { detectForkParent, detectInheritedSessionParent } from "./resolve/transcript.ts";
|
|
29
|
+
|
|
30
|
+
export interface ForkDetection {
|
|
31
|
+
fork: boolean;
|
|
32
|
+
forkedFrom?: string;
|
|
33
|
+
/** Detection had the evidence it needs; the tool-path heal can skip it. */
|
|
34
|
+
checked: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Sources after which a brand-new instance cannot hold inherited history. */
|
|
38
|
+
const FRESH_SOURCES = new Set(["startup", "clear"]);
|
|
39
|
+
|
|
40
|
+
export function detectClaudeCodeFork(input: {
|
|
41
|
+
coordRoot: string;
|
|
42
|
+
transcriptPath: string | undefined;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
source?: string;
|
|
45
|
+
/** The instance was already recorded as a fork without a parent. */
|
|
46
|
+
knownFork?: boolean;
|
|
47
|
+
isLive?: (instanceId: string) => boolean;
|
|
48
|
+
}): ForkDetection {
|
|
49
|
+
try {
|
|
50
|
+
const declared = input.source === "fork" || input.knownFork === true;
|
|
51
|
+
const transcriptReady = !!input.transcriptPath && existsSync(input.transcriptPath);
|
|
52
|
+
const isLive =
|
|
53
|
+
input.isLive ?? ((id: string) => readLiveCoordinationRow(input.coordRoot, id) !== null);
|
|
54
|
+
|
|
55
|
+
const inherited = transcriptReady
|
|
56
|
+
? detectInheritedSessionParent(input.transcriptPath, input.sessionId)
|
|
57
|
+
: undefined;
|
|
58
|
+
if (inherited && inherited !== input.sessionId) {
|
|
59
|
+
if (declared || isLive(inherited)) {
|
|
60
|
+
return { fork: true, forkedFrom: inherited, checked: true };
|
|
61
|
+
}
|
|
62
|
+
return { fork: false, checked: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (declared) {
|
|
66
|
+
if (!transcriptReady) return { fork: true, checked: false };
|
|
67
|
+
const parent = detectForkParent(input.transcriptPath, input.sessionId);
|
|
68
|
+
return {
|
|
69
|
+
fork: true,
|
|
70
|
+
...(parent && parent !== input.sessionId ? { forkedFrom: parent } : {}),
|
|
71
|
+
checked: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
fork: false,
|
|
77
|
+
checked: transcriptReady || FRESH_SOURCES.has(input.source ?? ""),
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return { fork: false, checked: true };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -356,6 +356,94 @@ const FORK_SCAN_MAX_BYTES = 128 * 1024 * 1024;
|
|
|
356
356
|
/** How many message uuids to sample across the fork's copied prefix. */
|
|
357
357
|
const FORK_SAMPLE_SIZE = 24;
|
|
358
358
|
|
|
359
|
+
/** Tail window scanned for inherited rows; a fork's own rows are few when the
|
|
360
|
+
* scan runs (session start or the first tool call), so the copied history ends
|
|
361
|
+
* well inside it. */
|
|
362
|
+
const INHERITED_TAIL_BYTES = 4 * 1024 * 1024;
|
|
363
|
+
const INHERITED_HEAD_BYTES = 64 * 1024;
|
|
364
|
+
const SESSION_ID_FIELD_RE =
|
|
365
|
+
/"sessionId":"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/gi;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Detect inherited history from row session ids: the session whose rows were
|
|
369
|
+
* copied into this transcript, or undefined when every row is this session's.
|
|
370
|
+
*
|
|
371
|
+
* Empirical basis (verified 2026-10-07, Claude Code 2.1.283): the desktop
|
|
372
|
+
* app's "Fork from here" copies the parent's rows into the fork's transcript
|
|
373
|
+
* WITHOUT rewriting their `sessionId`, so the fork's file opens with thousands
|
|
374
|
+
* of rows stamped with the parent's id. The CLI `--fork-session` path rewrites
|
|
375
|
+
* the ids instead and is covered by {@link detectForkParent}.
|
|
376
|
+
*
|
|
377
|
+
* When the history is itself several generations deep, the LAST foreign row is
|
|
378
|
+
* the nearest ancestor, so the tail is scanned first. Whether the foreign
|
|
379
|
+
* session is a fork parent or a dead predecessor (a continuation) is the
|
|
380
|
+
* caller's call. Fail-soft: never throws, undefined on any read problem.
|
|
381
|
+
*/
|
|
382
|
+
export function detectInheritedSessionParent(
|
|
383
|
+
transcriptPath: string | undefined,
|
|
384
|
+
sessionId: string,
|
|
385
|
+
): string | undefined {
|
|
386
|
+
try {
|
|
387
|
+
const readablePath = resolveTranscriptPath(transcriptPath);
|
|
388
|
+
if (!readablePath || !sessionId) return undefined;
|
|
389
|
+
const size = statSync(readablePath).size;
|
|
390
|
+
const fromTail = lastForeignSessionId(
|
|
391
|
+
readRange(readablePath, Math.max(0, size - INHERITED_TAIL_BYTES), size),
|
|
392
|
+
sessionId,
|
|
393
|
+
);
|
|
394
|
+
if (fromTail || size <= INHERITED_TAIL_BYTES) return fromTail;
|
|
395
|
+
return lastForeignSessionId(
|
|
396
|
+
readRange(readablePath, 0, Math.min(size, INHERITED_HEAD_BYTES)),
|
|
397
|
+
sessionId,
|
|
398
|
+
);
|
|
399
|
+
} catch {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function lastForeignSessionId(text: string | undefined, sessionId: string): string | undefined {
|
|
405
|
+
if (!text) return undefined;
|
|
406
|
+
const own = sessionId.toLowerCase();
|
|
407
|
+
const lines = text.split("\n");
|
|
408
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
409
|
+
const line = lines[i]!;
|
|
410
|
+
if (!line.includes('"sessionId":"')) continue;
|
|
411
|
+
let foreign = false;
|
|
412
|
+
for (const m of line.matchAll(SESSION_ID_FIELD_RE)) {
|
|
413
|
+
if (m[1]!.toLowerCase() !== own) foreign = true;
|
|
414
|
+
}
|
|
415
|
+
if (!foreign) continue;
|
|
416
|
+
// Confirm on the parsed row: a nested object can carry another session's
|
|
417
|
+
// id, but only the row's own top-level field says whose row it is. A
|
|
418
|
+
// partial line at a window edge fails to parse and is skipped.
|
|
419
|
+
try {
|
|
420
|
+
const row = JSON.parse(line) as { sessionId?: unknown };
|
|
421
|
+
if (typeof row.sessionId === "string" && row.sessionId.toLowerCase() !== own) {
|
|
422
|
+
return row.sessionId;
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
/* torn line */
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function readRange(path: string, start: number, end: number): string | undefined {
|
|
432
|
+
try {
|
|
433
|
+
const fs = require("node:fs") as typeof import("node:fs");
|
|
434
|
+
const fd = fs.openSync(path, "r");
|
|
435
|
+
try {
|
|
436
|
+
const buf = Buffer.alloc(Math.max(0, end - start));
|
|
437
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
438
|
+
return buf.toString("utf8");
|
|
439
|
+
} finally {
|
|
440
|
+
fs.closeSync(fd);
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
359
447
|
/**
|
|
360
448
|
* Detect the parent session of a forked CC conversation from its transcript.
|
|
361
449
|
*
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { basename, shellCommandFromTool, tokenize } from "./shell-tokens.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deny a shell `rm` and name the guarded replacement. The instructions have
|
|
5
|
+
* said "use <bin> rm instead of shell rm" since the command shipped, and
|
|
6
|
+
* agents still reached for bare rm several times a day, often on unquoted
|
|
7
|
+
* variable paths that Claude Code then stops on for a human prompt. A
|
|
8
|
+
* repeated, machine-detectable failure belongs in a gate, not more prose.
|
|
9
|
+
*
|
|
10
|
+
* This is a guardrail against the reflex, not a security boundary: it reads
|
|
11
|
+
* command positions only (so `git rm`, `docker rm`, and quoted examples pass),
|
|
12
|
+
* plus the `xargs rm`, `find -exec rm`, and `bash -c '...rm...'` shapes.
|
|
13
|
+
* HARNERY_ALLOW_RM=1 on the command is the deliberate override.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const OVERRIDE = "HARNERY_ALLOW_RM=1";
|
|
17
|
+
const SEPARATORS = new Set([";", "|", "||", "&", "&&", "(", ")", "{", "}"]);
|
|
18
|
+
const KEYWORDS = new Set(["do", "if", "then", "elif", "else", "!", "while", "until", "time"]);
|
|
19
|
+
const WRAPPERS = new Set(["command", "exec", "builtin", "nohup", "sudo", "doas", "env", "nice"]);
|
|
20
|
+
// xargs options that consume the following token as their value.
|
|
21
|
+
const XARGS_VALUE_FLAGS = new Set(["-n", "-I", "-L", "-P", "-d", "-E", "-s", "-a"]);
|
|
22
|
+
|
|
23
|
+
function isRm(value: string, quoted: boolean): boolean {
|
|
24
|
+
return !quoted && basename(value) === "rm";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function invokesRm(source: string, depth = 0): boolean {
|
|
28
|
+
if (depth > 8) return false;
|
|
29
|
+
const tokens = tokenize(source);
|
|
30
|
+
if (tokens.some((t) => !t.quoted && t.value === OVERRIDE)) return false;
|
|
31
|
+
let start = true;
|
|
32
|
+
let afterWrapper = false;
|
|
33
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
34
|
+
const token = tokens[i]!;
|
|
35
|
+
if (token.operator) {
|
|
36
|
+
if (SEPARATORS.has(token.value)) {
|
|
37
|
+
start = true;
|
|
38
|
+
afterWrapper = false;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!start) continue;
|
|
43
|
+
if (!token.quoted) {
|
|
44
|
+
if (KEYWORDS.has(token.value)) continue;
|
|
45
|
+
if (/^[A-Za-z_][A-Za-z_0-9]*=/.test(token.value)) continue;
|
|
46
|
+
if (afterWrapper && token.value.startsWith("-")) continue;
|
|
47
|
+
}
|
|
48
|
+
const name = basename(token.value);
|
|
49
|
+
if (WRAPPERS.has(name)) {
|
|
50
|
+
afterWrapper = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
start = false;
|
|
54
|
+
afterWrapper = false;
|
|
55
|
+
if (isRm(token.value, token.quoted)) return true;
|
|
56
|
+
if (name === "xargs") {
|
|
57
|
+
for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
|
|
58
|
+
const arg = tokens[j]!;
|
|
59
|
+
if (arg.value.startsWith("-")) {
|
|
60
|
+
if (XARGS_VALUE_FLAGS.has(arg.value)) j++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (isRm(arg.value, arg.quoted)) return true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (name === "find") {
|
|
68
|
+
for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
|
|
69
|
+
if (!/^-(?:exec|execdir|ok|okdir)$/.test(tokens[j]!.value)) continue;
|
|
70
|
+
const next = tokens[j + 1];
|
|
71
|
+
if (next && !next.operator && isRm(next.value, next.quoted)) return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (["bash", "sh", "dash", "zsh", "ksh"].includes(name)) {
|
|
75
|
+
for (let j = i + 1; j < tokens.length && !tokens[j]!.operator; j++) {
|
|
76
|
+
if (/^-[a-z]*c[a-z]*$/.test(tokens[j]!.value)) {
|
|
77
|
+
const script = tokens[j + 1];
|
|
78
|
+
if (script && !script.operator && invokesRm(script.value, depth + 1)) return true;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function shellRmReason(
|
|
88
|
+
toolName: unknown,
|
|
89
|
+
toolInput: unknown,
|
|
90
|
+
binName: string,
|
|
91
|
+
): string | null {
|
|
92
|
+
const command = shellCommandFromTool(toolName, toolInput);
|
|
93
|
+
if (command === null || !invokesRm(command)) return null;
|
|
94
|
+
return (
|
|
95
|
+
`Shell rm blocked: it skips the resolved-path, Git-ownership, and peer-claim checks, and an ` +
|
|
96
|
+
`unquoted variable path can expand to the wrong target. Use the guarded paths instead: ` +
|
|
97
|
+
`\`${binName} rm --root <directory> <paths...>\` previews removal of untracked files (add ` +
|
|
98
|
+
`--yes to delete, -r for a directory); \`git rm\` removes tracked source; ` +
|
|
99
|
+
`\`${binName} artifacts discard <id>\` retires managed evidence. To replace a file, overwrite ` +
|
|
100
|
+
`it in place or \`mv\` the new file over the old one, so nothing needs deleting. If none of ` +
|
|
101
|
+
`these fit, prefix the command with ${OVERRIDE}.`
|
|
102
|
+
);
|
|
103
|
+
}
|