harnery 0.0.1 → 0.2.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/LICENSE +21 -0
- package/README.md +84 -2
- package/bin/agent-coord +42 -0
- package/bin/agent-hook +44 -0
- package/bin/harn +40 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/commander.d.ts +128 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +126 -0
- package/dist/commands/agents.d.ts +18 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +3946 -0
- package/dist/commands/backup.d.ts +22 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +262 -0
- package/dist/commands/browse-ai.d.ts +4 -0
- package/dist/commands/browse-ai.d.ts.map +1 -0
- package/dist/commands/browse-ai.js +156 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.d.ts.map +1 -0
- package/dist/commands/browse.js +590 -0
- package/dist/commands/callers.d.ts +4 -0
- package/dist/commands/callers.d.ts.map +1 -0
- package/dist/commands/callers.js +276 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +158 -0
- package/dist/commands/config-get.d.ts +4 -0
- package/dist/commands/config-get.d.ts.map +1 -0
- package/dist/commands/config-get.js +131 -0
- package/dist/commands/context.d.ts +11 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +185 -0
- package/dist/commands/cookies.d.ts +4 -0
- package/dist/commands/cookies.d.ts.map +1 -0
- package/dist/commands/cookies.js +140 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +137 -0
- package/dist/commands/doctor.d.ts +25 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +200 -0
- package/dist/commands/edit-batch.d.ts +18 -0
- package/dist/commands/edit-batch.d.ts.map +1 -0
- package/dist/commands/edit-batch.js +172 -0
- package/dist/commands/eml.d.ts +4 -0
- package/dist/commands/eml.d.ts.map +1 -0
- package/dist/commands/eml.js +428 -0
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +201 -0
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +99 -0
- package/dist/commands/file-history.d.ts +4 -0
- package/dist/commands/file-history.d.ts.map +1 -0
- package/dist/commands/file-history.js +152 -0
- package/dist/commands/grep.d.ts +4 -0
- package/dist/commands/grep.d.ts.map +1 -0
- package/dist/commands/grep.js +317 -0
- package/dist/commands/init.d.ts +82 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +288 -0
- package/dist/commands/outline.d.ts +4 -0
- package/dist/commands/outline.d.ts.map +1 -0
- package/dist/commands/outline.js +494 -0
- package/dist/commands/presence.d.ts +12 -0
- package/dist/commands/presence.d.ts.map +1 -0
- package/dist/commands/presence.js +123 -0
- package/dist/commands/read.d.ts +7 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +46 -0
- package/dist/commands/scratch.d.ts +4 -0
- package/dist/commands/scratch.d.ts.map +1 -0
- package/dist/commands/scratch.js +426 -0
- package/dist/commands/session.d.ts +4 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +162 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +275 -0
- package/dist/commands/toc.d.ts +5 -0
- package/dist/commands/toc.d.ts.map +1 -0
- package/dist/commands/toc.js +153 -0
- package/dist/commands/tokens.d.ts +4 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +48 -0
- package/dist/commands/tunnel.d.ts +4 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +513 -0
- package/dist/commands/uninstall.d.ts +22 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +126 -0
- package/dist/commands/web.d.ts +4 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +165 -0
- package/dist/core/agents/canonical-emit.d.ts +27 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -0
- package/dist/core/agents/canonical-emit.js +72 -0
- package/dist/core/agents/cli-emit.d.ts +27 -0
- package/dist/core/agents/cli-emit.d.ts.map +1 -0
- package/dist/core/agents/cli-emit.js +57 -0
- package/dist/core/agents/cli.d.ts +10 -0
- package/dist/core/agents/cli.d.ts.map +1 -0
- package/dist/core/agents/cli.js +757 -0
- package/dist/core/agents/codex-replay.d.ts +29 -0
- package/dist/core/agents/codex-replay.d.ts.map +1 -0
- package/dist/core/agents/codex-replay.js +138 -0
- package/dist/core/agents/coord-client.d.ts +98 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -0
- package/dist/core/agents/coord-client.js +212 -0
- package/dist/core/agents/events/consume.d.ts +59 -0
- package/dist/core/agents/events/consume.d.ts.map +1 -0
- package/dist/core/agents/events/consume.js +147 -0
- package/dist/core/agents/events/emit.d.ts +42 -0
- package/dist/core/agents/events/emit.d.ts.map +1 -0
- package/dist/core/agents/events/emit.js +70 -0
- package/dist/core/agents/events/ulid.d.ts +11 -0
- package/dist/core/agents/events/ulid.d.ts.map +1 -0
- package/dist/core/agents/events/ulid.js +47 -0
- package/dist/core/agents/index.d.ts +14 -0
- package/dist/core/agents/index.d.ts.map +1 -0
- package/dist/core/agents/index.js +13 -0
- package/dist/core/agents/paths.d.ts +6 -0
- package/dist/core/agents/paths.d.ts.map +1 -0
- package/dist/core/agents/paths.js +17 -0
- package/dist/core/agents/render/prompt-context.d.ts +43 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
- package/dist/core/agents/render/prompt-context.js +335 -0
- package/dist/core/agents/render/session-context.d.ts +39 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -0
- package/dist/core/agents/render/session-context.js +283 -0
- package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/claim-conflict.js +244 -0
- package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/commit-conflict.js +244 -0
- package/dist/core/agents/rules/stop-hook.d.ts +44 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
- package/dist/core/agents/rules/stop-hook.js +161 -0
- package/dist/core/agents/session-events.d.ts +41 -0
- package/dist/core/agents/session-events.d.ts.map +1 -0
- package/dist/core/agents/session-events.js +205 -0
- package/dist/core/agents/state/activity-log.d.ts +18 -0
- package/dist/core/agents/state/activity-log.d.ts.map +1 -0
- package/dist/core/agents/state/activity-log.js +34 -0
- package/dist/core/agents/state/council.d.ts +39 -0
- package/dist/core/agents/state/council.d.ts.map +1 -0
- package/dist/core/agents/state/council.js +216 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-projector.js +436 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-writer.js +271 -0
- package/dist/core/agents/state/names.d.ts +35 -0
- package/dist/core/agents/state/names.d.ts.map +1 -0
- package/dist/core/agents/state/names.js +376 -0
- package/dist/core/agents/state/pidmap.d.ts +11 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -0
- package/dist/core/agents/state/pidmap.js +32 -0
- package/dist/core/agents/state/scratch.d.ts +27 -0
- package/dist/core/agents/state/scratch.d.ts.map +1 -0
- package/dist/core/agents/state/scratch.js +90 -0
- package/dist/core/agents/state/shell-mutation.d.ts +17 -0
- package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
- package/dist/core/agents/state/shell-mutation.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts +16 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
- package/dist/core/agents/state/stale-sweep.js +166 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +108 -0
- package/dist/core/hooks/cli.d.ts +21 -0
- package/dist/core/hooks/cli.d.ts.map +1 -0
- package/dist/core/hooks/cli.js +1123 -0
- package/dist/core/hooks/effects/image-capture.d.ts +43 -0
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
- package/dist/core/hooks/effects/image-capture.js +288 -0
- package/dist/core/hooks/effects/index.d.ts +64 -0
- package/dist/core/hooks/effects/index.d.ts.map +1 -0
- package/dist/core/hooks/effects/index.js +197 -0
- package/dist/core/hooks/events/emit.d.ts +31 -0
- package/dist/core/hooks/events/emit.d.ts.map +1 -0
- package/dist/core/hooks/events/emit.js +89 -0
- package/dist/core/hooks/events/schema.d.ts +235 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -0
- package/dist/core/hooks/events/schema.js +12 -0
- package/dist/core/hooks/events/ulid.d.ts +10 -0
- package/dist/core/hooks/events/ulid.d.ts.map +1 -0
- package/dist/core/hooks/events/ulid.js +47 -0
- package/dist/core/hooks/harness/detect.d.ts +9 -0
- package/dist/core/hooks/harness/detect.d.ts.map +1 -0
- package/dist/core/hooks/harness/detect.js +29 -0
- package/dist/core/hooks/harness/events.d.ts +45 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -0
- package/dist/core/hooks/harness/events.js +71 -0
- package/dist/core/hooks/harness/output.d.ts +46 -0
- package/dist/core/hooks/harness/output.d.ts.map +1 -0
- package/dist/core/hooks/harness/output.js +87 -0
- package/dist/core/hooks/harness/parse.d.ts +67 -0
- package/dist/core/hooks/harness/parse.d.ts.map +1 -0
- package/dist/core/hooks/harness/parse.js +132 -0
- package/dist/core/hooks/index.d.ts +8 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +7 -0
- package/dist/core/hooks/resolve/anchor.d.ts +37 -0
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
- package/dist/core/hooks/resolve/anchor.js +48 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
- package/dist/core/hooks/resolve/coord-root.js +27 -0
- package/dist/core/hooks/resolve/intent.d.ts +33 -0
- package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
- package/dist/core/hooks/resolve/intent.js +79 -0
- package/dist/core/hooks/resolve/owner.d.ts +42 -0
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
- package/dist/core/hooks/resolve/owner.js +140 -0
- package/dist/core/hooks/resolve/transcript.d.ts +26 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
- package/dist/core/hooks/resolve/transcript.js +73 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/lib/agent-browser/client.d.ts +99 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -0
- package/dist/lib/agent-browser/client.js +177 -0
- package/dist/lib/agent-browser/index.d.ts +2 -0
- package/dist/lib/agent-browser/index.d.ts.map +1 -0
- package/dist/lib/agent-browser/index.js +1 -0
- package/dist/lib/browser/client.d.ts +193 -0
- package/dist/lib/browser/client.d.ts.map +1 -0
- package/dist/lib/browser/client.js +325 -0
- package/dist/lib/browser/dev-overlay.d.ts +23 -0
- package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
- package/dist/lib/browser/dev-overlay.js +153 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.d.ts.map +1 -0
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/layout.d.ts +79 -0
- package/dist/lib/browser/layout.d.ts.map +1 -0
- package/dist/lib/browser/layout.js +220 -0
- package/dist/lib/browser/visibility.d.ts +86 -0
- package/dist/lib/browser/visibility.d.ts.map +1 -0
- package/dist/lib/browser/visibility.js +333 -0
- package/dist/lib/browser/visual-diff.d.ts +38 -0
- package/dist/lib/browser/visual-diff.d.ts.map +1 -0
- package/dist/lib/browser/visual-diff.js +107 -0
- package/dist/lib/completion/bash.d.ts +25 -0
- package/dist/lib/completion/bash.d.ts.map +1 -0
- package/dist/lib/completion/bash.js +284 -0
- package/dist/lib/completion/fish.d.ts +16 -0
- package/dist/lib/completion/fish.d.ts.map +1 -0
- package/dist/lib/completion/fish.js +118 -0
- package/dist/lib/completion/index.d.ts +5 -0
- package/dist/lib/completion/index.d.ts.map +1 -0
- package/dist/lib/completion/index.js +4 -0
- package/dist/lib/completion/walk.d.ts +68 -0
- package/dist/lib/completion/walk.d.ts.map +1 -0
- package/dist/lib/completion/walk.js +102 -0
- package/dist/lib/completion/zsh.d.ts +13 -0
- package/dist/lib/completion/zsh.d.ts.map +1 -0
- package/dist/lib/completion/zsh.js +249 -0
- package/dist/lib/context/index.d.ts +107 -0
- package/dist/lib/context/index.d.ts.map +1 -0
- package/dist/lib/context/index.js +275 -0
- package/dist/lib/cookies/client.d.ts +131 -0
- package/dist/lib/cookies/client.d.ts.map +1 -0
- package/dist/lib/cookies/client.js +239 -0
- package/dist/lib/cookies/index.d.ts +2 -0
- package/dist/lib/cookies/index.d.ts.map +1 -0
- package/dist/lib/cookies/index.js +1 -0
- package/dist/lib/council/index.d.ts +266 -0
- package/dist/lib/council/index.d.ts.map +1 -0
- package/dist/lib/council/index.js +674 -0
- package/dist/lib/docs-index.d.ts +28 -0
- package/dist/lib/docs-index.d.ts.map +1 -0
- package/dist/lib/docs-index.js +169 -0
- package/dist/lib/docs-lint.d.ts +26 -0
- package/dist/lib/docs-lint.d.ts.map +1 -0
- package/dist/lib/docs-lint.js +378 -0
- package/dist/lib/docs-sweep.d.ts +34 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -0
- package/dist/lib/docs-sweep.js +304 -0
- package/dist/lib/docs.d.ts +27 -0
- package/dist/lib/docs.d.ts.map +1 -0
- package/dist/lib/docs.js +142 -0
- package/dist/lib/env.d.ts +11 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +12 -0
- package/dist/lib/exec.d.ts +32 -0
- package/dist/lib/exec.d.ts.map +1 -0
- package/dist/lib/exec.js +54 -0
- package/dist/lib/format.d.ts +29 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +139 -0
- package/dist/lib/http/client.d.ts +56 -0
- package/dist/lib/http/client.d.ts.map +1 -0
- package/dist/lib/http/client.js +160 -0
- package/dist/lib/http/index.d.ts +2 -0
- package/dist/lib/http/index.d.ts.map +1 -0
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/identities/index.d.ts +77 -0
- package/dist/lib/identities/index.d.ts.map +1 -0
- package/dist/lib/identities/index.js +190 -0
- package/dist/lib/machine.d.ts +19 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/machine.js +61 -0
- package/dist/lib/presence.d.ts +48 -0
- package/dist/lib/presence.d.ts.map +1 -0
- package/dist/lib/presence.js +123 -0
- package/dist/lib/readability/client.d.ts +32 -0
- package/dist/lib/readability/client.d.ts.map +1 -0
- package/dist/lib/readability/client.js +119 -0
- package/dist/lib/readability/index.d.ts +2 -0
- package/dist/lib/readability/index.d.ts.map +1 -0
- package/dist/lib/readability/index.js +1 -0
- package/dist/lib/scratch/index.d.ts +74 -0
- package/dist/lib/scratch/index.d.ts.map +1 -0
- package/dist/lib/scratch/index.js +393 -0
- package/dist/lib/tunnel/gate.d.ts +12 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -0
- package/dist/lib/tunnel/gate.js +101 -0
- package/dist/lib/tunnel/state.d.ts +34 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -0
- package/dist/lib/tunnel/state.js +132 -0
- package/package.json +160 -8
- package/schemas/.gitkeep +0 -0
- package/schemas/config.schema.json +109 -0
- package/src/cli.ts +22 -0
- package/src/commander.ts +242 -0
- package/src/commands/.gitkeep +0 -0
- package/src/commands/agents.ts +4567 -0
- package/src/commands/backup.ts +305 -0
- package/src/commands/browse-ai.ts +198 -0
- package/src/commands/browse.ts +849 -0
- package/src/commands/callers.ts +363 -0
- package/src/commands/completion.ts +193 -0
- package/src/commands/config-get.ts +161 -0
- package/src/commands/context.ts +209 -0
- package/src/commands/cookies.ts +198 -0
- package/src/commands/docs.ts +174 -0
- package/src/commands/doctor.ts +231 -0
- package/src/commands/edit-batch.ts +233 -0
- package/src/commands/eml.ts +519 -0
- package/src/commands/env.ts +254 -0
- package/src/commands/fetch.ts +136 -0
- package/src/commands/file-history.ts +202 -0
- package/src/commands/grep.ts +371 -0
- package/src/commands/init.ts +335 -0
- package/src/commands/outline.ts +564 -0
- package/src/commands/presence.ts +152 -0
- package/src/commands/read.ts +64 -0
- package/src/commands/scratch.ts +445 -0
- package/src/commands/session.ts +187 -0
- package/src/commands/sync.ts +306 -0
- package/src/commands/toc.ts +218 -0
- package/src/commands/tokens.ts +79 -0
- package/src/commands/tunnel.ts +633 -0
- package/src/commands/uninstall.ts +144 -0
- package/src/commands/web.ts +193 -0
- package/src/core/agents/canonical-emit.ts +77 -0
- package/src/core/agents/cli-emit.ts +64 -0
- package/src/core/agents/cli.ts +838 -0
- package/src/core/agents/codex-replay.ts +163 -0
- package/src/core/agents/coord-client.ts +249 -0
- package/src/core/agents/events/consume.ts +196 -0
- package/src/core/agents/events/emit.ts +108 -0
- package/src/core/agents/events/ulid.ts +51 -0
- package/src/core/agents/index.ts +14 -0
- package/src/core/agents/paths.ts +16 -0
- package/src/core/agents/render/prompt-context.ts +401 -0
- package/src/core/agents/render/session-context.ts +341 -0
- package/src/core/agents/rules/claim-conflict.ts +282 -0
- package/src/core/agents/rules/commit-conflict.ts +303 -0
- package/src/core/agents/rules/stop-hook.ts +229 -0
- package/src/core/agents/session-events.ts +228 -0
- package/src/core/agents/state/activity-log.ts +33 -0
- package/src/core/agents/state/council.ts +265 -0
- package/src/core/agents/state/heartbeat-projector.ts +488 -0
- package/src/core/agents/state/heartbeat-writer.ts +333 -0
- package/src/core/agents/state/names.ts +399 -0
- package/src/core/agents/state/pidmap.ts +38 -0
- package/src/core/agents/state/scratch.ts +121 -0
- package/src/core/agents/state/shell-mutation.ts +44 -0
- package/src/core/agents/state/stale-sweep.ts +190 -0
- package/src/core/config.ts +111 -0
- package/src/core/hooks/cli.ts +1247 -0
- package/src/core/hooks/effects/image-capture.ts +330 -0
- package/src/core/hooks/effects/index.ts +210 -0
- package/src/core/hooks/events/emit.ts +120 -0
- package/src/core/hooks/events/schema.ts +430 -0
- package/src/core/hooks/events/ulid.ts +51 -0
- package/src/core/hooks/harness/detect.ts +30 -0
- package/src/core/hooks/harness/events.ts +102 -0
- package/src/core/hooks/harness/output.ts +100 -0
- package/src/core/hooks/harness/parse.ts +180 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/resolve/anchor.ts +51 -0
- package/src/core/hooks/resolve/coord-root.ts +25 -0
- package/src/core/hooks/resolve/intent.ts +89 -0
- package/src/core/hooks/resolve/owner.ts +140 -0
- package/src/core/hooks/resolve/transcript.ts +72 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/index.ts +15 -0
- package/src/lib/agent-browser/client.ts +239 -0
- package/src/lib/agent-browser/index.ts +1 -0
- package/src/lib/browser/client.ts +449 -0
- package/src/lib/browser/dev-overlay.ts +207 -0
- package/src/lib/browser/index.ts +24 -0
- package/src/lib/browser/layout.ts +288 -0
- package/src/lib/browser/visibility.ts +419 -0
- package/src/lib/browser/visual-diff.ts +150 -0
- package/src/lib/completion/bash.ts +291 -0
- package/src/lib/completion/fish.ts +134 -0
- package/src/lib/completion/index.ts +10 -0
- package/src/lib/completion/walk.ts +184 -0
- package/src/lib/completion/zsh.ts +262 -0
- package/src/lib/context/index.ts +386 -0
- package/src/lib/cookies/client.ts +301 -0
- package/src/lib/cookies/index.ts +13 -0
- package/src/lib/council/index.ts +803 -0
- package/src/lib/docs-index.ts +216 -0
- package/src/lib/docs-lint.ts +413 -0
- package/src/lib/docs-sweep.ts +348 -0
- package/src/lib/docs.ts +199 -0
- package/src/lib/env.ts +12 -0
- package/src/lib/exec.ts +74 -0
- package/src/lib/format.ts +147 -0
- package/src/lib/http/client.ts +211 -0
- package/src/lib/http/index.ts +1 -0
- package/src/lib/identities/index.ts +210 -0
- package/src/lib/machine.ts +61 -0
- package/src/lib/presence.ts +154 -0
- package/src/lib/readability/client.ts +156 -0
- package/src/lib/readability/index.ts +5 -0
- package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
- package/src/lib/scratch/index.ts +470 -0
- package/src/lib/tunnel/gate.ts +113 -0
- package/src/lib/tunnel/state.ts +167 -0
- package/src/web/.gitkeep +0 -0
- package/index.js +0 -1
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-hook` CLI entry point. Phase 2: real canonical-event emission
|
|
3
|
+
* alongside the legacy stream.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. Parse argv → event-name + harness.
|
|
7
|
+
* 2. Read stdin → harness payload (JSON or empty).
|
|
8
|
+
* 3. Find coord root (walk up for .harnery/).
|
|
9
|
+
* 4. Resolve instance_id (env → payload → pid-map walk).
|
|
10
|
+
* 5. Map event-name → canonical event_type.
|
|
11
|
+
* 6. Build event data from payload + resolvers (intent, transcript scan).
|
|
12
|
+
* 7. Append envelope to .harnery/events.ndjson via emit() under flock.
|
|
13
|
+
* 8. (Still also writes a debug breadcrumb to .harnery/debug/ for visibility.)
|
|
14
|
+
*
|
|
15
|
+
* Phase 2 ship criterion: confirms parser correctness across thousands of
|
|
16
|
+
* real events without affecting behavior. Always exits 0. Failures land in
|
|
17
|
+
* `.harnery/debug/agent-hook.errors.ndjson` for audit but never break the
|
|
18
|
+
* harness flow.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { coordEnv } from "../../lib/env.js";
|
|
24
|
+
import { replayCodexJsonl } from "../agents/codex-replay.js";
|
|
25
|
+
import { consumeSince, writeCursor } from "../agents/events/consume.js";
|
|
26
|
+
import { evaluateStopHook } from "../agents/rules/stop-hook.js";
|
|
27
|
+
import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
|
|
28
|
+
import { shellMutationPaths } from "../agents/state/shell-mutation.js";
|
|
29
|
+
import { captureImages, detectPresence, imageJanitor, playSound, resetSoundCounters, runTurnSummary, scratchArchive, scratchJanitor, scratchRecoveryCue, soundForEvent, syncClaudeSessions, } from "./effects/index.js";
|
|
30
|
+
import { emit } from "./events/emit.js";
|
|
31
|
+
import { detectHarness } from "./harness/detect.js";
|
|
32
|
+
import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./harness/parse.js";
|
|
33
|
+
import { selectAnchorPid } from "./resolve/anchor.js";
|
|
34
|
+
import { findCoordRoot } from "./resolve/coord-root.js";
|
|
35
|
+
import { extractIntentComment, resolveIntent } from "./resolve/intent.js";
|
|
36
|
+
import { resolveOwner } from "./resolve/owner.js";
|
|
37
|
+
import { scanStatusBoxPresent, scanTranscriptModel } from "./resolve/transcript.js";
|
|
38
|
+
function parseArgv(argv) {
|
|
39
|
+
const out = { eventName: null, extra: [] };
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const arg = argv[i];
|
|
42
|
+
if (arg === "--harness") {
|
|
43
|
+
i++; // detectHarness will re-parse; just consume the value here.
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg.startsWith("--harness="))
|
|
47
|
+
continue;
|
|
48
|
+
if (!out.eventName && !arg.startsWith("--")) {
|
|
49
|
+
out.eventName = arg;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
out.extra.push(arg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
async function readStdin() {
|
|
58
|
+
if (process.stdin.isTTY)
|
|
59
|
+
return "";
|
|
60
|
+
try {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
for await (const chunk of process.stdin) {
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
}
|
|
65
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function appendDebug(coordRoot, entry) {
|
|
72
|
+
const path = join(coordRoot, ".harnery", "debug", "agent-hook.ndjson");
|
|
73
|
+
try {
|
|
74
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
75
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* swallow */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function logError(coordRoot, err, context) {
|
|
82
|
+
if (!coordRoot)
|
|
83
|
+
return;
|
|
84
|
+
const path = join(coordRoot, ".harnery", "debug", "agent-hook.errors.ndjson");
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
87
|
+
appendFileSync(path, `${JSON.stringify({
|
|
88
|
+
ts: new Date().toISOString(),
|
|
89
|
+
error: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
|
|
90
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
91
|
+
...context,
|
|
92
|
+
})}\n`, "utf8");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* swallow */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Spawn `agent-coord assign-name <owner> <kind>` to mint or recover the
|
|
100
|
+
* hurricane-style name for this owner. Returns null on any failure so
|
|
101
|
+
* session.start emission never breaks the harness flow.
|
|
102
|
+
*
|
|
103
|
+
* Lives at agent-hooks side (not agent-coord) to keep emitter/consumer
|
|
104
|
+
* separation: we spawn rather than import.
|
|
105
|
+
*/
|
|
106
|
+
function assignNameViaAgentCoord(coordRoot, instanceId, kind) {
|
|
107
|
+
const binary = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
108
|
+
if (!existsSync(binary))
|
|
109
|
+
return null;
|
|
110
|
+
try {
|
|
111
|
+
const result = spawnSync(binary, ["assign-name", instanceId, kind], {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
timeout: 2000,
|
|
114
|
+
});
|
|
115
|
+
if (result.status !== 0 || !result.stdout)
|
|
116
|
+
return null;
|
|
117
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
118
|
+
if (parsed.name && parsed.kind) {
|
|
119
|
+
return { name: parsed.name, kind: parsed.kind };
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Direct (in-process) pidmap write, avoids the spawn overhead of going via
|
|
129
|
+
* the agent-coord CLI for every session.start / subagent.start. Pid-map
|
|
130
|
+
* rows are essential for `harn agents whoami` ppid resolution.
|
|
131
|
+
*/
|
|
132
|
+
function writePidmapViaAgentCoord(coordRoot, pid, instanceId, platform) {
|
|
133
|
+
try {
|
|
134
|
+
// Inline write: same atomic temp+rename pattern as
|
|
135
|
+
// agent-coord/src/state/pidmap.ts but skips importing across module
|
|
136
|
+
// boundaries to keep agent-hooks's deps explicit.
|
|
137
|
+
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
138
|
+
const path = join(dir, String(pid));
|
|
139
|
+
const row = `${instanceId}\t${platform}`;
|
|
140
|
+
if (existsSync(path)) {
|
|
141
|
+
try {
|
|
142
|
+
const current = require("node:fs").readFileSync(path, "utf8");
|
|
143
|
+
if (current === row)
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* fall through */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
mkdirSync(dir, { recursive: true });
|
|
151
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
152
|
+
require("node:fs").writeFileSync(tmp, row, "utf8");
|
|
153
|
+
require("node:fs").renameSync(tmp, path);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
/* never break the harness flow */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function clampString(s, max) {
|
|
160
|
+
if (s.length <= max)
|
|
161
|
+
return { value: s, truncated: false };
|
|
162
|
+
return { value: s.slice(0, max), truncated: true };
|
|
163
|
+
}
|
|
164
|
+
function summarizeOutput(value, headTail = 500) {
|
|
165
|
+
const str = typeof value === "string" ? value : JSON.stringify(value ?? "");
|
|
166
|
+
if (str.length <= headTail * 2)
|
|
167
|
+
return { summary: str, truncated: false };
|
|
168
|
+
return {
|
|
169
|
+
summary: `${str.slice(0, headTail)}\n…[truncated]…\n${str.slice(-headTail)}`,
|
|
170
|
+
truncated: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function buildEventData(eventType, ctx) {
|
|
174
|
+
const p = ctx.payload;
|
|
175
|
+
switch (eventType) {
|
|
176
|
+
case "session.start": {
|
|
177
|
+
const harnessPlatform = ctx.harness === "claude-code"
|
|
178
|
+
? "claude_code"
|
|
179
|
+
: ctx.harness === "cursor"
|
|
180
|
+
? "cursor"
|
|
181
|
+
: "codex";
|
|
182
|
+
// Assign (or recover) name + kind via agent-coord. Idempotent: resume
|
|
183
|
+
// returns the original name; new owner consumes a counter slot.
|
|
184
|
+
const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "session");
|
|
185
|
+
// Write the harness pid-map row so `harn agents whoami` ppid-walks find
|
|
186
|
+
// this owner. Prefer the payload pid (the actual claude binary), then the
|
|
187
|
+
// anchor walk (the `node` ancestor for Cursor, which has no payload pid),
|
|
188
|
+
// then our own process.ppid. Without the anchor, Cursor anchored on the
|
|
189
|
+
// ephemeral hook bash parent, a PID that dies before the agent's next
|
|
190
|
+
// shell tool call, so the ppid walk found nothing (no_pidmap_entry).
|
|
191
|
+
const harnessPid = p?.pid ?? findHarnessAnchorPid(ctx.harness) ?? process.ppid;
|
|
192
|
+
if (harnessPid) {
|
|
193
|
+
writePidmapViaAgentCoord(ctx.coordRoot, harnessPid, ctx.instanceId, harnessPlatform);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
started_at: new Date().toISOString(),
|
|
197
|
+
cwd: p?.cwd ?? process.cwd(),
|
|
198
|
+
// Claude Code's SessionStart payload omits `model` (Codex + Cursor
|
|
199
|
+
// supply it). Fall back to the transcript, populated on `resume`, and
|
|
200
|
+
// backfilled later by `turn.stop` for a fresh `startup` session.
|
|
201
|
+
model: p?.model ?? scanTranscriptModel(p?.transcript_path),
|
|
202
|
+
pid: harnessPid,
|
|
203
|
+
source: p?.source,
|
|
204
|
+
platform: harnessPlatform,
|
|
205
|
+
name: assigned?.name,
|
|
206
|
+
kind: "session",
|
|
207
|
+
agent_id: ctx.instanceId,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
case "session.end":
|
|
211
|
+
return {
|
|
212
|
+
ended_at: new Date().toISOString(),
|
|
213
|
+
clean_exit: p?.clean_exit ?? true,
|
|
214
|
+
};
|
|
215
|
+
case "user_prompt.submit": {
|
|
216
|
+
const prompt = p?.prompt ?? "";
|
|
217
|
+
const { value, truncated } = clampString(prompt, 4000);
|
|
218
|
+
return { prompt_text: value, ...(truncated ? { truncated: true } : {}) };
|
|
219
|
+
}
|
|
220
|
+
case "turn.stop": {
|
|
221
|
+
return {
|
|
222
|
+
// Backfill the model for harnesses that omit it at session.start
|
|
223
|
+
// (Claude Code). The transcript is populated with assistant turns by
|
|
224
|
+
// Stop-hook time, so this resolves even for fresh `startup` sessions.
|
|
225
|
+
model: p?.model ?? scanTranscriptModel(p?.transcript_path),
|
|
226
|
+
// Phase 2: tool_call_count + text_length aren't cheaply available
|
|
227
|
+
// from the Stop payload alone (they'd require a transcript scan that
|
|
228
|
+
// races with the JSONL flush). Emit `-1` / `0` sentinels and let
|
|
229
|
+
// Phase 5 (the verdict path) recompute these from the event stream
|
|
230
|
+
// itself rather than re-scanning the transcript.
|
|
231
|
+
tool_call_count: -1,
|
|
232
|
+
text_length: 0,
|
|
233
|
+
// Box present if the transcript scan finds it OR the final assistant
|
|
234
|
+
// message carries the `┌─ agent-` prefix. The latter covers codex's
|
|
235
|
+
// text-only stop (box in last_assistant_message, no transcript), which
|
|
236
|
+
// the verdict now sees because agent-hook emits this turn.stop itself
|
|
237
|
+
// (the previous path passed those via the no-history fail-open).
|
|
238
|
+
status_box_present: scanStatusBoxPresent(p?.transcript_path) ||
|
|
239
|
+
(p?.raw.last_assistant_message ?? "").includes("┌─ agent-"),
|
|
240
|
+
stop_hook_active: p?.stop_hook_active,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
case "subagent.start": {
|
|
244
|
+
const subagentCallId = p?.raw.subagent_id ?? p?.raw.agent_id;
|
|
245
|
+
// Subagents inherit parent's name via the resolve-name session_id path
|
|
246
|
+
// (agent-coord/state/names.ts → kind=transient). Use the call ID as the
|
|
247
|
+
// instance_id input; assignName falls through to transient.
|
|
248
|
+
const assigned = assignNameViaAgentCoord(ctx.coordRoot, ctx.instanceId, "subagent");
|
|
249
|
+
return {
|
|
250
|
+
agent_type: p?.raw.agent_type ??
|
|
251
|
+
p?.raw.subagent_type ??
|
|
252
|
+
"unknown",
|
|
253
|
+
prompt_summary: p?.raw.prompt_summary,
|
|
254
|
+
name: assigned?.name,
|
|
255
|
+
kind: "subagent",
|
|
256
|
+
agent_id: ctx.instanceId,
|
|
257
|
+
subagent_call_id: subagentCallId,
|
|
258
|
+
parent_session_id: p?.parent_session_id,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
case "subagent.stop": {
|
|
262
|
+
const status = p?.exit_status;
|
|
263
|
+
const normalized = status === "error" || status === "interrupted" ? status : "ok";
|
|
264
|
+
return { exit_status: normalized, reason: p?.reason };
|
|
265
|
+
}
|
|
266
|
+
case "tool.pre_use": {
|
|
267
|
+
const toolName = p?.tool_name ?? "unknown";
|
|
268
|
+
const command = extractBashCommand(toolName, p?.tool_input);
|
|
269
|
+
const description = extractToolDescription(p?.tool_input);
|
|
270
|
+
const { intent, source } = resolveIntent({
|
|
271
|
+
coordRoot: ctx.coordRoot,
|
|
272
|
+
instanceId: ctx.instanceId,
|
|
273
|
+
commandIntentComment: extractIntentComment(command),
|
|
274
|
+
description,
|
|
275
|
+
});
|
|
276
|
+
const toolInputStr = JSON.stringify(p?.tool_input ?? null);
|
|
277
|
+
const clamped = clampString(toolInputStr, 8000);
|
|
278
|
+
return {
|
|
279
|
+
tool_name: toolName,
|
|
280
|
+
tool_input: clamped.value,
|
|
281
|
+
intent,
|
|
282
|
+
intent_source: source,
|
|
283
|
+
tool_use_id: p?.tool_use_id,
|
|
284
|
+
...(clamped.truncated ? { truncated: true } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
case "tool.post_use": {
|
|
288
|
+
const toolName = p?.tool_name ?? "unknown";
|
|
289
|
+
const summary = summarizeOutput(p?.tool_response);
|
|
290
|
+
return {
|
|
291
|
+
tool_name: toolName,
|
|
292
|
+
output_summary: summary.summary,
|
|
293
|
+
exit_status: "ok",
|
|
294
|
+
duration_ms: 0, // Phase 3 pairs pre/post via tool_use_id
|
|
295
|
+
tool_use_id: p?.tool_use_id,
|
|
296
|
+
...(summary.truncated ? { truncated: true } : {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
case "tool.post_use_failure": {
|
|
300
|
+
const toolName = p?.tool_name ?? "unknown";
|
|
301
|
+
const summary = summarizeOutput(p?.tool_response);
|
|
302
|
+
return {
|
|
303
|
+
tool_name: toolName,
|
|
304
|
+
error: summary.summary,
|
|
305
|
+
duration_ms: 0,
|
|
306
|
+
tool_use_id: p?.tool_use_id,
|
|
307
|
+
...(summary.truncated ? { truncated: true } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function main() {
|
|
313
|
+
const { eventName, extra } = parseArgv(process.argv.slice(2));
|
|
314
|
+
const harness = detectHarness(process.argv.slice(2));
|
|
315
|
+
const raw = await readStdin();
|
|
316
|
+
// Kill-switch-INDEPENDENT effects: notification sounds fire BEFORE the
|
|
317
|
+
// HARNERY_AGENT_COORD_OFF gate so audible feedback survives incident-triage
|
|
318
|
+
// bypass: sound playback happens before the kill-switch bailout.
|
|
319
|
+
// Claude-Code-only; stop-failure → error, sub-agent-start → subagent-start.
|
|
320
|
+
if (harness === "claude-code" && eventName) {
|
|
321
|
+
const s = soundForEvent(eventName);
|
|
322
|
+
if (s) {
|
|
323
|
+
const repoRoot = findCoordRoot(process.cwd());
|
|
324
|
+
if (repoRoot) {
|
|
325
|
+
let sid = "";
|
|
326
|
+
try {
|
|
327
|
+
const j = JSON.parse(raw);
|
|
328
|
+
sid = j.session_id ?? j.conversation_id ?? "";
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// non-JSON payload: play unkeyed (rate-limit just won't dedup)
|
|
332
|
+
}
|
|
333
|
+
playSound(repoRoot, s.sound, sid, s.maxPlays);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Kill switch. Disables every effect of
|
|
338
|
+
// agent-hook + agent-coord: no event emit, no projection, no systemMessage,
|
|
339
|
+
// no G-guard verdict. Used for the cross-client `HARNERY_AGENT_COORD_OFF=1`
|
|
340
|
+
// bypass during incident triage.
|
|
341
|
+
if (coordEnv("AGENT_COORD_OFF") === "1")
|
|
342
|
+
return 0;
|
|
343
|
+
const coordRoot = findCoordRoot(process.cwd());
|
|
344
|
+
if (!coordRoot)
|
|
345
|
+
return 0;
|
|
346
|
+
// Always log a breadcrumb, useful when an event_type maps to null or owner
|
|
347
|
+
// resolution fails. Stays cheap (one append) and self-prunes via repo log
|
|
348
|
+
// rotation policy.
|
|
349
|
+
const debugBase = {
|
|
350
|
+
ts: new Date().toISOString(),
|
|
351
|
+
event_name: eventName,
|
|
352
|
+
harness,
|
|
353
|
+
extra_argv: extra,
|
|
354
|
+
payload_bytes: raw.length,
|
|
355
|
+
cwd: process.cwd(),
|
|
356
|
+
pid: process.pid,
|
|
357
|
+
ppid: process.ppid,
|
|
358
|
+
};
|
|
359
|
+
if (!eventName || !harness) {
|
|
360
|
+
appendDebug(coordRoot, { ...debugBase, skipped: "missing-event-or-harness" });
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
const norm = normalizeEventName(eventName);
|
|
364
|
+
if (!norm) {
|
|
365
|
+
appendDebug(coordRoot, { ...debugBase, skipped: "non-canonical-event" });
|
|
366
|
+
return 0;
|
|
367
|
+
}
|
|
368
|
+
const payload = parsePayload(raw, harness);
|
|
369
|
+
const owner = resolveOwner({ payload: payload?.raw ?? null, coordRoot });
|
|
370
|
+
if (!owner) {
|
|
371
|
+
appendDebug(coordRoot, {
|
|
372
|
+
...debugBase,
|
|
373
|
+
skipped: "no-owner-resolved",
|
|
374
|
+
event_type: norm.event_type,
|
|
375
|
+
});
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
const sessionId = payload?.session_id ?? payload?.conversation_id ?? owner.instance_id;
|
|
379
|
+
const data = buildEventData(norm.event_type, {
|
|
380
|
+
coordRoot,
|
|
381
|
+
payload,
|
|
382
|
+
raw,
|
|
383
|
+
harness,
|
|
384
|
+
instanceId: owner.instance_id,
|
|
385
|
+
});
|
|
386
|
+
const envelope = emit(coordRoot, {
|
|
387
|
+
event_type: norm.event_type,
|
|
388
|
+
instance_id: owner.instance_id,
|
|
389
|
+
session_id: sessionId,
|
|
390
|
+
parent_session_id: payload?.parent_session_id,
|
|
391
|
+
turn_id: payload?.turn_id,
|
|
392
|
+
parent_turn_id: payload?.parent_turn_id,
|
|
393
|
+
harness,
|
|
394
|
+
data,
|
|
395
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
396
|
+
});
|
|
397
|
+
appendDebug(coordRoot, {
|
|
398
|
+
...debugBase,
|
|
399
|
+
event_type: norm.event_type,
|
|
400
|
+
owner_source: owner.source,
|
|
401
|
+
event_id: envelope.event_id,
|
|
402
|
+
});
|
|
403
|
+
// Phase 8: SessionStart post-emit: project the event so the heartbeat
|
|
404
|
+
// lands synchronously, run stale-sweep, and emit the harness-shaped
|
|
405
|
+
// systemMessage JSON (peer table + wiring check + council invites).
|
|
406
|
+
// Harness-agnostic since v0.5.0; replaces the previous bash UX layer
|
|
407
|
+
// and the equivalent per-harness bash session_start handlers.
|
|
408
|
+
if (norm.event_type === "session.start") {
|
|
409
|
+
// Effect (claude-code): prune stale scratch archives + sweep orphans.
|
|
410
|
+
// The recovery-cue is merged into the
|
|
411
|
+
// session-start additionalContext inside emitSessionStartSystemMessage.
|
|
412
|
+
if (harness === "claude-code")
|
|
413
|
+
scratchJanitor(coordRoot);
|
|
414
|
+
// Image-feed retention sweep (size + age cap on .harnery/images/). Harness-
|
|
415
|
+
// agnostic, cheap (one readdir), fail-soft. Paired with scratchJanitor as a
|
|
416
|
+
// session-start "tidy the coord layer" step.
|
|
417
|
+
try {
|
|
418
|
+
imageJanitor(coordRoot);
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
logError(coordRoot, err, { phase: "session-start-image-janitor" });
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
await emitSessionStartSystemMessage(coordRoot, owner.instance_id, sessionId, data, harness);
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
logError(coordRoot, err, { phase: "session-start-systemMessage" });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Harness-
|
|
431
|
+
// agnostic since v0.5.0.
|
|
432
|
+
if (norm.event_type === "session.end") {
|
|
433
|
+
try {
|
|
434
|
+
cleanupSessionEnd(coordRoot, owner.instance_id, data.reason ?? "unknown");
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
logError(coordRoot, err, { phase: "session-end-cleanup" });
|
|
438
|
+
}
|
|
439
|
+
// Effects (claude-code): archive the ending agent's scratchpad + force a
|
|
440
|
+
// session-telemetry sync (via HARNERY_CLAUDE_SESSIONS_FORCE=1).
|
|
441
|
+
if (harness === "claude-code") {
|
|
442
|
+
scratchArchive(coordRoot, owner.instance_id);
|
|
443
|
+
syncClaudeSessions(coordRoot, true);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Phase 8: SubagentStart: sync-project to create the subagent heartbeat,
|
|
447
|
+
// log the lifecycle event, and emit a context message announcing the
|
|
448
|
+
// subagent (claude-code + cursor; codex doesn't fan out subagents today).
|
|
449
|
+
if (norm.event_type === "subagent.start") {
|
|
450
|
+
try {
|
|
451
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
452
|
+
if (existsSync(agentCoordBin)) {
|
|
453
|
+
spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
|
|
454
|
+
spawnSync(agentCoordBin, [
|
|
455
|
+
"log",
|
|
456
|
+
`SUBAGENT_START agent_type=${data.agent_type ?? "unknown"} agent_id=${owner.instance_id.slice(0, 8)} platform=${harnessPlatform(harness)}`,
|
|
457
|
+
"--instance",
|
|
458
|
+
owner.instance_id,
|
|
459
|
+
], { encoding: "utf8", timeout: 2000 });
|
|
460
|
+
}
|
|
461
|
+
emitSubagentStartContext(coordRoot, owner.instance_id, sessionId, data, harness);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
logError(coordRoot, err, { phase: "subagent-start-project" });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Phase 8: SubagentStop: delete subagent heartbeat + log.
|
|
468
|
+
if (norm.event_type === "subagent.stop") {
|
|
469
|
+
try {
|
|
470
|
+
cleanupSessionEnd(coordRoot, owner.instance_id, data.reason ?? "unknown");
|
|
471
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
472
|
+
if (existsSync(agentCoordBin)) {
|
|
473
|
+
spawnSync(agentCoordBin, [
|
|
474
|
+
"log",
|
|
475
|
+
`SUBAGENT_STOP agent_id=${owner.instance_id.slice(0, 8)} platform=${harnessPlatform(harness)}`,
|
|
476
|
+
"--instance",
|
|
477
|
+
owner.instance_id,
|
|
478
|
+
], { encoding: "utf8", timeout: 2000 });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
logError(coordRoot, err, { phase: "subagent-stop-cleanup" });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Phase 8: UserPromptSubmit: render dedup'd peer table + council pending
|
|
486
|
+
// and emit the harness-shaped systemMessage JSON. Harness-agnostic since v0.5.0.
|
|
487
|
+
if (norm.event_type === "user_prompt.submit") {
|
|
488
|
+
// Effects (claude-code): reset per-turn sound rate-limit counters + run
|
|
489
|
+
// presence detection on the prompt.
|
|
490
|
+
if (harness === "claude-code") {
|
|
491
|
+
resetSoundCounters(sessionId);
|
|
492
|
+
const prompt = payload?.raw?.prompt ?? "";
|
|
493
|
+
if (prompt)
|
|
494
|
+
detectPresence(prompt);
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
await emitUserPromptSubmitSystemMessage(coordRoot, owner.instance_id, sessionId, harness);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// turn.stop: telemetry + turn-summary effects, then the stop verdict. The
|
|
504
|
+
// verdict + codex-replay previously lived in the per-harness shell adapters;
|
|
505
|
+
// agent-hook owns them now. Runs on the normal "stop" event only;
|
|
506
|
+
// "stop-failure" (API error) gets no gate, matching the previous
|
|
507
|
+
// stop vs stop-failure split.
|
|
508
|
+
if (norm.event_type === "turn.stop" && eventName === "stop") {
|
|
509
|
+
// Codex: replay the JSONL transcript → canonical events so the verdict has
|
|
510
|
+
// the status_checked / task_set / status_box_present evidence (codex
|
|
511
|
+
// doesn't emit those live; this re-emits turn.stop after agent-hook's own,
|
|
512
|
+
// so the verdict reads the replay's box signal as the latest).
|
|
513
|
+
if (harness === "codex" && payload?.transcript_path && existsSync(payload.transcript_path)) {
|
|
514
|
+
try {
|
|
515
|
+
replayCodexJsonl({
|
|
516
|
+
coordRoot,
|
|
517
|
+
jsonlPath: payload.transcript_path,
|
|
518
|
+
sessionId,
|
|
519
|
+
instanceId: owner.instance_id,
|
|
520
|
+
lastAssistantMessage: payload.raw.last_assistant_message ?? "",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
logError(coordRoot, err, { phase: "codex-replay" });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// CC effects: rate-limited session-telemetry sync + turn-summary Haiku
|
|
528
|
+
// auto-summary.
|
|
529
|
+
if (harness === "claude-code") {
|
|
530
|
+
syncClaudeSessions(coordRoot, false);
|
|
531
|
+
runTurnSummary(coordRoot, owner.instance_id, sessionId, payload?.transcript_path);
|
|
532
|
+
}
|
|
533
|
+
// Master-state heartbeat projection. Drains events.ndjson since the last
|
|
534
|
+
// cursor → per-owner heartbeats. Was a SECOND binary (`agent-coord project`)
|
|
535
|
+
// pinned to Claude Code Stop only; folded in here so it (a) is one
|
|
536
|
+
// entry per event like everything else and (b) fires on EVERY harness's stop,
|
|
537
|
+
// not just CC. Runs unconditionally before the verdict's possible exit-2 return
|
|
538
|
+
// (the events are real regardless of whether the agent gets nagged), and after
|
|
539
|
+
// codex-replay above so codex's replayed events are included in the drain.
|
|
540
|
+
// Not an emitter (consumes + writes heartbeats), so no emitter/consumer conflict.
|
|
541
|
+
try {
|
|
542
|
+
const result = consumeSince(coordRoot);
|
|
543
|
+
projectHeartbeats(coordRoot, result.events);
|
|
544
|
+
if (result.lastEventId)
|
|
545
|
+
writeCursor(coordRoot, result.lastEventId);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
logError(coordRoot, err, { phase: "stop-projection" });
|
|
549
|
+
}
|
|
550
|
+
// Stop verdict (status-box + set-task gate). Direct in-process call: the
|
|
551
|
+
// rule lives in harnery. agent-hook already emitted this turn.stop (with
|
|
552
|
+
// status_box_present) above, so the evidence is in the stream.
|
|
553
|
+
const verdict = evaluateStopHook(coordRoot, {
|
|
554
|
+
rule: "stop-hook",
|
|
555
|
+
instance_id: owner.instance_id,
|
|
556
|
+
session_id: sessionId,
|
|
557
|
+
harness,
|
|
558
|
+
bypass: coordEnv("AGENT_COORD_BYPASS_STOP") === "1",
|
|
559
|
+
});
|
|
560
|
+
if (!verdict.allow) {
|
|
561
|
+
// Harness-aware enforcement channel: Claude Code / Codex honor exit-2 +
|
|
562
|
+
// stderr as a turn block; Cursor ignores exit codes (fail-open) and
|
|
563
|
+
// re-prompts only via a `followup_message` it auto-submits. emitStopBlock
|
|
564
|
+
// writes the right shape and returns the exit code to use.
|
|
565
|
+
const { emitStopBlock } = await import("./harness/output.js");
|
|
566
|
+
return emitStopBlock(harness, verdict);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Phase 7: PreToolUse: heartbeat + pid-map self-heal on every tool call.
|
|
570
|
+
// Harness-agnostic: both writes have the same shape regardless of who fired.
|
|
571
|
+
// Cursor/Codex bash dispatchers still fire their own G-guard logic, but the
|
|
572
|
+
// heals here keep the agent-coord layer's view of liveness fresh.
|
|
573
|
+
//
|
|
574
|
+
// The heartbeat + pid-map heals are paired by design; they were wired
|
|
575
|
+
// side-by-side in the previous pre-tool-use adapter. The Phase 4-6 refactor
|
|
576
|
+
// preserved the heartbeat half but dropped the pid-map half; the pid-map
|
|
577
|
+
// call was restored here afterward.
|
|
578
|
+
if (norm.event_type === "tool.pre_use") {
|
|
579
|
+
try {
|
|
580
|
+
healHeartbeatViaCli(coordRoot, owner.instance_id, sessionId, harness);
|
|
581
|
+
refreshPidmap(coordRoot, owner.instance_id, harness, payload?.pid);
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
logError(coordRoot, err, { phase: "pre-tool-use-heal" });
|
|
585
|
+
}
|
|
586
|
+
// Image feed: a Read on an image file is the "agent viewed this" signal.
|
|
587
|
+
// Capture the bytes (content-addressed, dedup'd) + emit image.captured.
|
|
588
|
+
try {
|
|
589
|
+
captureImages(coordRoot, {
|
|
590
|
+
eventType: "tool.pre_use",
|
|
591
|
+
data,
|
|
592
|
+
payload,
|
|
593
|
+
instanceId: owner.instance_id,
|
|
594
|
+
sessionId,
|
|
595
|
+
harness,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
logError(coordRoot, err, { phase: "pre-tool-use-image-capture" });
|
|
600
|
+
}
|
|
601
|
+
// G-guard for ALL harnesses. Claude Code previously ran this via a
|
|
602
|
+
// pre-tool-use bash adapter (which called `agent-coord verdict --rule=claim`);
|
|
603
|
+
// that adapter is now deleted, so agent-hook owns the deny for every harness.
|
|
604
|
+
// emitDeny() inside emits the harness-shaped permission JSON (claude-code +
|
|
605
|
+
// codex use hookSpecificOutput.permissionDecision; cursor uses .permission).
|
|
606
|
+
// apply_patch (codex) parses paths from the patch body and runs verdict
|
|
607
|
+
// per-path; Edit/Write/NotebookEdit resolve a single target. Non-write tools
|
|
608
|
+
// (incl. Agent) yield no targets and pass through with no deny.
|
|
609
|
+
try {
|
|
610
|
+
await runPreToolUseGuard(coordRoot, owner.instance_id, sessionId, data, harness);
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
logError(coordRoot, err, { phase: "pre-tool-use-guard" });
|
|
614
|
+
}
|
|
615
|
+
// Shell-mutation warn (warn-only, never blocks). Was the cursor
|
|
616
|
+
// beforeShellExecution + codex preToolUse-Bash shell-mutation-claim-log in
|
|
617
|
+
// the per-harness shell adapters. Cursor sends the command at payload.command;
|
|
618
|
+
// codex Bash at tool_input.command. Emits a decision.warn per candidate-mutated
|
|
619
|
+
// path so a peer sees the write in events.ndjson. (CC never did this,
|
|
620
|
+
// preserved; it emits with its own hooks-side emitter per the
|
|
621
|
+
// independent-emitter rule.)
|
|
622
|
+
const shellCmd = eventName === "before-shell-execution"
|
|
623
|
+
? (payload?.raw.command ?? "")
|
|
624
|
+
: harness === "codex" && data.tool_name === "Bash"
|
|
625
|
+
? (payload?.raw.tool_input?.command ?? "")
|
|
626
|
+
: "";
|
|
627
|
+
if (shellCmd) {
|
|
628
|
+
try {
|
|
629
|
+
const paths = shellMutationPaths(shellCmd, coordRoot);
|
|
630
|
+
const truncated = shellCmd.length > 80 ? shellCmd.slice(0, 80) : shellCmd;
|
|
631
|
+
const platform = harnessPlatform(harness);
|
|
632
|
+
for (const p of paths) {
|
|
633
|
+
emit(coordRoot, {
|
|
634
|
+
event_type: "decision.warn",
|
|
635
|
+
instance_id: owner.instance_id,
|
|
636
|
+
session_id: sessionId,
|
|
637
|
+
harness,
|
|
638
|
+
data: {
|
|
639
|
+
rule: "shell_mutation_candidate",
|
|
640
|
+
reason: `path=${p} cmd=${truncated} platform=${platform}`,
|
|
641
|
+
},
|
|
642
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
logError(coordRoot, err, { phase: "shell-mutation-warn" });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Phase 7: PostToolUse: stamp last_tool + last_tool_target on heartbeat.
|
|
652
|
+
// Harness-agnostic for the same reason as tool.pre_use above.
|
|
653
|
+
if (norm.event_type === "tool.post_use") {
|
|
654
|
+
try {
|
|
655
|
+
stampToolActivity(coordRoot, owner.instance_id, data);
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
logError(coordRoot, err, { phase: "post-tool-use-stamp" });
|
|
659
|
+
}
|
|
660
|
+
// Image feed: a Bash command that wrote an image (harn browse, harn image,
|
|
661
|
+
// --diff, …) is the "agent produced this" signal. Scan the command + its
|
|
662
|
+
// output for freshly-written image paths and capture them.
|
|
663
|
+
try {
|
|
664
|
+
captureImages(coordRoot, {
|
|
665
|
+
eventType: "tool.post_use",
|
|
666
|
+
data,
|
|
667
|
+
payload,
|
|
668
|
+
instanceId: owner.instance_id,
|
|
669
|
+
sessionId,
|
|
670
|
+
harness,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
logError(coordRoot, err, { phase: "post-tool-use-image-capture" });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// Phase 7: PostToolUseFailure: release claim on failed Edit (the file
|
|
678
|
+
// never landed; the claim is stale). Harness-agnostic.
|
|
679
|
+
if (norm.event_type === "tool.post_use_failure") {
|
|
680
|
+
try {
|
|
681
|
+
releaseClaimOnFailure(coordRoot, owner.instance_id, data, payload?.raw);
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
logError(coordRoot, err, { phase: "post-tool-use-failure-release" });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return 0;
|
|
688
|
+
}
|
|
689
|
+
async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, harness) {
|
|
690
|
+
const toolName = data.tool_name ?? "";
|
|
691
|
+
const targets = collectGuardTargets(toolName, data).map((p) => canonicalize(coordRoot, p));
|
|
692
|
+
if (targets.length === 0)
|
|
693
|
+
return;
|
|
694
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
695
|
+
if (!existsSync(agentCoordBin))
|
|
696
|
+
return;
|
|
697
|
+
// For apply_patch (multi-file), collect siblings so the deny reason names
|
|
698
|
+
// them. For single-file tools the array has one entry.
|
|
699
|
+
for (const target of targets) {
|
|
700
|
+
const verdictReq = JSON.stringify({
|
|
701
|
+
rule: "claim",
|
|
702
|
+
instance_id: instanceId,
|
|
703
|
+
session_id: sessionId,
|
|
704
|
+
path: target,
|
|
705
|
+
});
|
|
706
|
+
const result = spawnSync(agentCoordBin, ["verdict"], {
|
|
707
|
+
input: verdictReq,
|
|
708
|
+
encoding: "utf8",
|
|
709
|
+
timeout: 3000,
|
|
710
|
+
});
|
|
711
|
+
if (result.status !== 0 || !result.stdout)
|
|
712
|
+
continue;
|
|
713
|
+
let parsed = {};
|
|
714
|
+
try {
|
|
715
|
+
parsed = JSON.parse(result.stdout.trim());
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (parsed.allow === false) {
|
|
721
|
+
let reason = parsed.reason ?? `Path ${target} is currently being edited by another agent.`;
|
|
722
|
+
if (targets.length > 1) {
|
|
723
|
+
const siblings = targets
|
|
724
|
+
.filter((p) => p !== target)
|
|
725
|
+
.slice(0, 3)
|
|
726
|
+
.join(", ");
|
|
727
|
+
if (siblings) {
|
|
728
|
+
reason += ` The patch also touched: ${siblings}: pick a different file or wait.`;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const { emitDeny } = await import("./harness/output.js");
|
|
732
|
+
emitDeny(harness, reason);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** Canonicalize a path to monorepo-relative form. Absolute paths under
|
|
738
|
+
* coordRoot get the prefix stripped; relative paths pass through (assumed
|
|
739
|
+
* already canonical). */
|
|
740
|
+
function canonicalize(coordRoot, p) {
|
|
741
|
+
if (!p)
|
|
742
|
+
return p;
|
|
743
|
+
if (p.startsWith(`${coordRoot}/`))
|
|
744
|
+
return p.slice(coordRoot.length + 1);
|
|
745
|
+
if (p === coordRoot)
|
|
746
|
+
return ".";
|
|
747
|
+
return p;
|
|
748
|
+
}
|
|
749
|
+
/** Pull the candidate path(s) out of a write-tool payload. Empty array when
|
|
750
|
+
* the tool isn't a write or no path could be derived. */
|
|
751
|
+
function collectGuardTargets(toolName, data) {
|
|
752
|
+
const writeTools = new Set(["Edit", "Write", "NotebookEdit", "StrReplace"]);
|
|
753
|
+
if (writeTools.has(toolName)) {
|
|
754
|
+
const target = extractFilePathFromData(data);
|
|
755
|
+
return target ? [target] : [];
|
|
756
|
+
}
|
|
757
|
+
if (toolName === "apply_patch") {
|
|
758
|
+
return parseApplyPatchPaths(data);
|
|
759
|
+
}
|
|
760
|
+
return [];
|
|
761
|
+
}
|
|
762
|
+
function extractFilePathFromData(data) {
|
|
763
|
+
const raw = data.tool_input;
|
|
764
|
+
if (typeof raw !== "string")
|
|
765
|
+
return undefined;
|
|
766
|
+
try {
|
|
767
|
+
const parsed = JSON.parse(raw);
|
|
768
|
+
return (parsed.file_path ??
|
|
769
|
+
parsed.path ??
|
|
770
|
+
parsed.notebook_path ??
|
|
771
|
+
undefined);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/** Parse Codex's `apply_patch` body for `*** Add|Update|Delete File: <path>`
|
|
778
|
+
* directives. Extracts apply_patch target paths for Codex. */
|
|
779
|
+
function parseApplyPatchPaths(data) {
|
|
780
|
+
const raw = data.tool_input;
|
|
781
|
+
if (typeof raw !== "string")
|
|
782
|
+
return [];
|
|
783
|
+
let body = "";
|
|
784
|
+
try {
|
|
785
|
+
const parsed = JSON.parse(raw);
|
|
786
|
+
body = parsed.command ?? "";
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
if (!body)
|
|
792
|
+
return [];
|
|
793
|
+
const out = [];
|
|
794
|
+
const re = /^\s*\*\*\* (Add|Update|Delete) File:\s*(.+)$/gm;
|
|
795
|
+
let m = re.exec(body);
|
|
796
|
+
while (m !== null) {
|
|
797
|
+
out.push(m[2].trim());
|
|
798
|
+
m = re.exec(body);
|
|
799
|
+
}
|
|
800
|
+
return out;
|
|
801
|
+
}
|
|
802
|
+
function healHeartbeatViaCli(coordRoot, instanceId, sessionId, harness) {
|
|
803
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
804
|
+
if (!existsSync(agentCoordBin))
|
|
805
|
+
return;
|
|
806
|
+
// Pass the detected harness so a pruned Cursor/Codex heartbeat is recreated
|
|
807
|
+
// with the correct platform; without it, healHeartbeat defaults to
|
|
808
|
+
// claude_code and the dashboard mislabels the agent. See
|
|
809
|
+
// heartbeat-writer.healHeartbeat.
|
|
810
|
+
spawnSync(agentCoordBin, ["heal-heartbeat", instanceId, sessionId, `--harness=${harness}`], {
|
|
811
|
+
encoding: "utf8",
|
|
812
|
+
timeout: 2000,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Walk up the ppid chain on Linux/WSL looking for the harness anchor PID,
|
|
817
|
+
* the PID of the claude / cursor / codex binary. Finds the agent PID. Used by
|
|
818
|
+
* `tool.pre_use`'s pid-map self-heal so a re-parented harness binary (the
|
|
819
|
+
* VS Code 2.1.x sibling-claude spawn case) gets its pid-map row rewritten on
|
|
820
|
+
* the next tool call rather than going invisible until SessionStart fires
|
|
821
|
+
* again, which it may never do.
|
|
822
|
+
*
|
|
823
|
+
* Returns undefined on macOS (no /proc) or when no anchor is found; callers
|
|
824
|
+
* fall back to `process.ppid` (the bash wrapper's parent, which is usually
|
|
825
|
+
* the harness binary itself). `HARNERY_AGENT_COORD_TEST_ANCHOR_PID` overrides
|
|
826
|
+
* everything so the test sandbox can pin a deterministic PID.
|
|
827
|
+
*/
|
|
828
|
+
function findHarnessAnchorPid(harness) {
|
|
829
|
+
const override = coordEnv("AGENT_COORD_TEST_ANCHOR_PID");
|
|
830
|
+
if (override) {
|
|
831
|
+
const n = Number(override);
|
|
832
|
+
if (Number.isFinite(n) && n > 0)
|
|
833
|
+
return n;
|
|
834
|
+
}
|
|
835
|
+
// Build the ppid chain (nearest → root, up to 20 hops) from /proc, then hand
|
|
836
|
+
// it to the pure selector. Splitting the /proc walk (untestable off a live
|
|
837
|
+
// box) from the comm-matching (unit-tested against the real Phase 0 chains in
|
|
838
|
+
// resolve/anchor.ts) keeps the cursor `node`-fallback logic verifiable.
|
|
839
|
+
const chain = [];
|
|
840
|
+
let pid = process.pid;
|
|
841
|
+
for (let hops = 0; hops < 20; hops++) {
|
|
842
|
+
let comm;
|
|
843
|
+
let status;
|
|
844
|
+
try {
|
|
845
|
+
comm = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
|
|
846
|
+
status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
chain.push({ pid, comm });
|
|
852
|
+
const m = status.match(/^PPid:\s+(\d+)/m);
|
|
853
|
+
if (!m)
|
|
854
|
+
break;
|
|
855
|
+
const ppid = Number(m[1]);
|
|
856
|
+
if (!Number.isFinite(ppid) || ppid === 0 || ppid === 1)
|
|
857
|
+
break;
|
|
858
|
+
pid = ppid;
|
|
859
|
+
}
|
|
860
|
+
return selectAnchorPid(chain, harness);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Pid-map self-heal for `tool.pre_use`. Symmetric counterpart to
|
|
864
|
+
* `healHeartbeatViaCli`; the two were paired before the Phase 6 refactor split
|
|
865
|
+
* them apart, then restored together afterward.
|
|
866
|
+
*
|
|
867
|
+
* The pid argument prefers the payload's `pid` (CC populates it on
|
|
868
|
+
* SessionStart and may also send it on PreToolUse), then
|
|
869
|
+
* `findHarnessAnchorPid`, then `process.ppid`. Writes go through the same
|
|
870
|
+
* idempotent `writePidmapViaAgentCoord` helper that SessionStart uses: no
|
|
871
|
+
* disk I/O on no-op heals (when the row already points at us).
|
|
872
|
+
*
|
|
873
|
+
* Follow-up: emit `PIDMAP_HEAL` telemetry on actual writes to keep
|
|
874
|
+
* `harn agents heal-events` pidmap counts meaningful. The inline helper does
|
|
875
|
+
* not yet.
|
|
876
|
+
*/
|
|
877
|
+
function refreshPidmap(coordRoot, instanceId, harness, payloadPid) {
|
|
878
|
+
const pid = payloadPid ?? findHarnessAnchorPid(harness) ?? process.ppid;
|
|
879
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
880
|
+
return;
|
|
881
|
+
writePidmapViaAgentCoord(coordRoot, pid, instanceId, harnessPlatform(harness));
|
|
882
|
+
}
|
|
883
|
+
function stampToolActivity(coordRoot, instanceId, data) {
|
|
884
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
885
|
+
if (!existsSync(agentCoordBin))
|
|
886
|
+
return;
|
|
887
|
+
const toolName = data.tool_name ?? "";
|
|
888
|
+
// Extract a 1-line target from the tool_input blob (file path / command head).
|
|
889
|
+
const toolInputRaw = data.tool_input;
|
|
890
|
+
let target = "";
|
|
891
|
+
if (typeof toolInputRaw === "string") {
|
|
892
|
+
try {
|
|
893
|
+
const parsed = JSON.parse(toolInputRaw);
|
|
894
|
+
target =
|
|
895
|
+
parsed.file_path ??
|
|
896
|
+
parsed.path ??
|
|
897
|
+
parsed.notebook_path ??
|
|
898
|
+
parsed.command ??
|
|
899
|
+
parsed.url ??
|
|
900
|
+
parsed.pattern ??
|
|
901
|
+
"";
|
|
902
|
+
}
|
|
903
|
+
catch {
|
|
904
|
+
/* skip */
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (target.length > 200)
|
|
908
|
+
target = target.slice(0, 200);
|
|
909
|
+
spawnSync(agentCoordBin, ["stamp-tool-activity", instanceId, toolName, target], {
|
|
910
|
+
encoding: "utf8",
|
|
911
|
+
timeout: 2000,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
function releaseClaimOnFailure(coordRoot, instanceId, data, rawPayload) {
|
|
915
|
+
const toolName = data.tool_name ?? "";
|
|
916
|
+
if (toolName !== "Edit" && toolName !== "Write" && toolName !== "NotebookEdit")
|
|
917
|
+
return;
|
|
918
|
+
// Path is in tool_input parsed from payload; try data first, fall back to raw.
|
|
919
|
+
const toolInputRaw = data.tool_input;
|
|
920
|
+
let filePath = "";
|
|
921
|
+
if (typeof toolInputRaw === "string") {
|
|
922
|
+
try {
|
|
923
|
+
const parsed = JSON.parse(toolInputRaw);
|
|
924
|
+
filePath =
|
|
925
|
+
parsed.file_path ??
|
|
926
|
+
parsed.path ??
|
|
927
|
+
parsed.notebook_path ??
|
|
928
|
+
"";
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
/* skip */
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (!filePath && rawPayload) {
|
|
935
|
+
const ti = rawPayload.tool_input;
|
|
936
|
+
if (ti) {
|
|
937
|
+
filePath =
|
|
938
|
+
ti.file_path ??
|
|
939
|
+
ti.path ??
|
|
940
|
+
ti.notebook_path ??
|
|
941
|
+
"";
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (!filePath)
|
|
945
|
+
return;
|
|
946
|
+
// Canonicalize path relative to coordRoot
|
|
947
|
+
let canonical = filePath;
|
|
948
|
+
if (filePath.startsWith("/")) {
|
|
949
|
+
canonical = filePath.startsWith(`${coordRoot}/`)
|
|
950
|
+
? filePath.slice(coordRoot.length + 1)
|
|
951
|
+
: filePath;
|
|
952
|
+
}
|
|
953
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
954
|
+
if (!existsSync(agentCoordBin))
|
|
955
|
+
return;
|
|
956
|
+
spawnSync(agentCoordBin, ["release-claim", instanceId, canonical], {
|
|
957
|
+
encoding: "utf8",
|
|
958
|
+
timeout: 2000,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
function cleanupSessionEnd(coordRoot, instanceId, reason) {
|
|
962
|
+
// Remove heartbeat from the canonical .harnery/active/ dir.
|
|
963
|
+
const path = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
|
|
964
|
+
try {
|
|
965
|
+
if (existsSync(path)) {
|
|
966
|
+
require("node:fs").unlinkSync(path);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
/* swallow */
|
|
971
|
+
}
|
|
972
|
+
// Sweep pid-map entries pointing to this instance
|
|
973
|
+
const pidmapDir = join(coordRoot, ".harnery", "pid-map");
|
|
974
|
+
if (existsSync(pidmapDir)) {
|
|
975
|
+
try {
|
|
976
|
+
const fs = require("node:fs");
|
|
977
|
+
for (const f of fs.readdirSync(pidmapDir)) {
|
|
978
|
+
const rowPath = join(pidmapDir, f);
|
|
979
|
+
try {
|
|
980
|
+
const row = fs.readFileSync(rowPath, "utf8").trim();
|
|
981
|
+
const ownerCol = row.split("\t")[0]?.trim() ?? "";
|
|
982
|
+
if (ownerCol === instanceId) {
|
|
983
|
+
fs.unlinkSync(rowPath);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
/* swallow */
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
/* swallow */
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// Activity log
|
|
996
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
997
|
+
if (existsSync(agentCoordBin)) {
|
|
998
|
+
spawnSync(agentCoordBin, ["log", `SESSION_END reason=${reason}`, "--instance", instanceId], { encoding: "utf8", timeout: 2000 });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
async function emitUserPromptSubmitSystemMessage(coordRoot, instanceId, sessionId, harness) {
|
|
1002
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
1003
|
+
if (!existsSync(agentCoordBin))
|
|
1004
|
+
return;
|
|
1005
|
+
// Look up the agent's name from its heartbeat (for council pending rendering).
|
|
1006
|
+
let agentName = "";
|
|
1007
|
+
try {
|
|
1008
|
+
const fs = require("node:fs");
|
|
1009
|
+
const hbPath = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
|
|
1010
|
+
if (fs.existsSync(hbPath)) {
|
|
1011
|
+
const hb = JSON.parse(fs.readFileSync(hbPath, "utf8"));
|
|
1012
|
+
agentName = hb.name ?? "";
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
/* fall through with empty name; peer table still renders */
|
|
1017
|
+
}
|
|
1018
|
+
const args = ["prompt-context", "--instance", instanceId, "--session", sessionId];
|
|
1019
|
+
if (agentName)
|
|
1020
|
+
args.push("--name", agentName);
|
|
1021
|
+
// Cursor + Codex sessions get the set-task staleness nudge. CC enforces it
|
|
1022
|
+
// via the Stop-hook transcript scan; the nudge replaces that for harnesses
|
|
1023
|
+
// that don't reliably expose a transcript_path during stop.
|
|
1024
|
+
if (harness === "cursor" || harness === "codex")
|
|
1025
|
+
args.push("--task-nudge");
|
|
1026
|
+
const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
|
|
1027
|
+
if (result.status !== 0 || !result.stdout)
|
|
1028
|
+
return;
|
|
1029
|
+
const additionalContext = result.stdout.trim();
|
|
1030
|
+
if (!additionalContext)
|
|
1031
|
+
return;
|
|
1032
|
+
const { emitContext } = await import("./harness/output.js");
|
|
1033
|
+
emitContext(harness, "UserPromptSubmit", additionalContext);
|
|
1034
|
+
}
|
|
1035
|
+
function emitSubagentStartContext(coordRoot, instanceId, sessionId, data, harness) {
|
|
1036
|
+
// Look up the subagent's assigned name (just-written by agent-coord assignName
|
|
1037
|
+
// in session.start data) + the parent's short id for the "you are a subagent
|
|
1038
|
+
// of X" framing.
|
|
1039
|
+
const subagentName = data.name ?? "";
|
|
1040
|
+
if (!subagentName)
|
|
1041
|
+
return;
|
|
1042
|
+
const platformLabel = harnessPlatform(harness);
|
|
1043
|
+
const parentShort = sessionId && sessionId !== instanceId ? `agent-${sessionId.slice(0, 8)}` : "the parent session";
|
|
1044
|
+
const message = `You are agent-${subagentName} (${platformLabel} subagent). You're a subagent of ${parentShort}.`;
|
|
1045
|
+
// Render peer table inline since the subagent might want to know who else
|
|
1046
|
+
// is around. Reuse prompt-context (which dedups against the per-owner hash);
|
|
1047
|
+
// first call will always emit.
|
|
1048
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
1049
|
+
let combined = message;
|
|
1050
|
+
if (existsSync(agentCoordBin)) {
|
|
1051
|
+
const result = spawnSync(agentCoordBin, ["prompt-context", "--instance", instanceId, "--session", sessionId, "--name", subagentName], { encoding: "utf8", timeout: 3000 });
|
|
1052
|
+
const ctx = (result.stdout ?? "").trim();
|
|
1053
|
+
if (ctx)
|
|
1054
|
+
combined = `${message}\n\n${ctx}`;
|
|
1055
|
+
}
|
|
1056
|
+
// Use SubagentStart event-name in CC's hookSpecificOutput shape; cursor's
|
|
1057
|
+
// flat `additional_context` works the same way.
|
|
1058
|
+
void import("./harness/output.js").then(({ emitContext }) => {
|
|
1059
|
+
emitContext(harness, "SubagentStart", combined);
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
function harnessPlatform(harness) {
|
|
1063
|
+
if (harness === "claude-code")
|
|
1064
|
+
return "claude_code";
|
|
1065
|
+
return harness;
|
|
1066
|
+
}
|
|
1067
|
+
async function emitSessionStartSystemMessage(coordRoot, instanceId, sessionId, emittedData, harness) {
|
|
1068
|
+
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
1069
|
+
if (!existsSync(agentCoordBin))
|
|
1070
|
+
return;
|
|
1071
|
+
// Sync-project so the heartbeat exists for downstream readers (peer table,
|
|
1072
|
+
// wiring check, council invites).
|
|
1073
|
+
spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
|
|
1074
|
+
// Stale-sweep dead peers before rendering peer table.
|
|
1075
|
+
spawnSync(agentCoordBin, ["stale-sweep"], { encoding: "utf8", timeout: 3000 });
|
|
1076
|
+
// SESSION_START activity log line, fired across all harnesses.
|
|
1077
|
+
const model = emittedData.model ?? "unknown";
|
|
1078
|
+
const source = emittedData.source ?? "startup";
|
|
1079
|
+
const platform = harnessPlatform(harness);
|
|
1080
|
+
spawnSync(agentCoordBin, [
|
|
1081
|
+
"log",
|
|
1082
|
+
`SESSION_START model=${model} source=${source} platform=${platform}`,
|
|
1083
|
+
"--instance",
|
|
1084
|
+
instanceId,
|
|
1085
|
+
], { encoding: "utf8", timeout: 2000 });
|
|
1086
|
+
// Render the systemMessage via agent-coord.
|
|
1087
|
+
const agentName = emittedData.name ?? "";
|
|
1088
|
+
const args = ["session-context", "--instance", instanceId, "--session", sessionId];
|
|
1089
|
+
if (agentName)
|
|
1090
|
+
args.push("--name", agentName);
|
|
1091
|
+
// The "You are agent-X." prefix in session-context renders unqualified by
|
|
1092
|
+
// default (claude-code-style). For cursor/codex the bash dispatchers add
|
|
1093
|
+
// a "(Cursor)" / "(Codex)" suffix; pass it through as --platform-label.
|
|
1094
|
+
if (harness !== "claude-code") {
|
|
1095
|
+
args.push("--platform-label", platform === "cursor" ? "Cursor" : "Codex");
|
|
1096
|
+
}
|
|
1097
|
+
const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
|
|
1098
|
+
if (result.status !== 0 || !result.stdout)
|
|
1099
|
+
return;
|
|
1100
|
+
let additionalContext = result.stdout.trim();
|
|
1101
|
+
if (!additionalContext)
|
|
1102
|
+
return;
|
|
1103
|
+
// Effect (claude-code): merge the scratch recovery cue into the session-start
|
|
1104
|
+
// context. Was a standalone additionalContext emission from the previous
|
|
1105
|
+
// scratch-on-start adapter; now that agent-hook is the single SessionStart
|
|
1106
|
+
// entry, it folds in here.
|
|
1107
|
+
if (harness === "claude-code") {
|
|
1108
|
+
const cue = scratchRecoveryCue(coordRoot);
|
|
1109
|
+
if (cue)
|
|
1110
|
+
additionalContext = `${additionalContext}\n\n${cue}`;
|
|
1111
|
+
}
|
|
1112
|
+
const { emitContext } = await import("./harness/output.js");
|
|
1113
|
+
emitContext(harness, "SessionStart", additionalContext);
|
|
1114
|
+
}
|
|
1115
|
+
main()
|
|
1116
|
+
.then((code) => process.exit(code))
|
|
1117
|
+
.catch((err) => {
|
|
1118
|
+
logError(findCoordRoot(process.cwd()), err, {
|
|
1119
|
+
argv: process.argv.slice(2),
|
|
1120
|
+
pid: process.pid,
|
|
1121
|
+
});
|
|
1122
|
+
process.exit(0);
|
|
1123
|
+
});
|