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,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image-feed capture: a non-coordination side effect fired from the normalized
|
|
3
|
+
* `agent-hook` tool handlers (see effects/index.ts for the family rationale).
|
|
4
|
+
*
|
|
5
|
+
* When an agent VIEWS an image (Read tool on a `.png`/`.jpg`/…) or PRODUCES one
|
|
6
|
+
* (a Bash command writes an image file via `harn browse`, `harn image`, `--diff`, …),
|
|
7
|
+
* we content-address the bytes into `.harnery/images/<sha256>.<ext>` (dedup:
|
|
8
|
+
* identical bytes collapse to one blob) and emit an `image.captured` event into
|
|
9
|
+
* the canonical stream. The web image feed (`/images`) groups those events by
|
|
10
|
+
* hash and streams them live over the existing SSE infra.
|
|
11
|
+
*
|
|
12
|
+
* Why this lives at the hook layer: it's the single harness-agnostic chokepoint
|
|
13
|
+
* that sees every tool call across Claude Code / Cursor / Codex with the full
|
|
14
|
+
* `tool_input` (file paths) and `tool_response`. No per-command code needed.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is best-effort and MUST NOT throw; callers wrap it in
|
|
17
|
+
* try/catch + logError, matching every other effect.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import {
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
renameSync,
|
|
27
|
+
rmSync,
|
|
28
|
+
statSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from "node:fs";
|
|
31
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
32
|
+
import { emit } from "../events/emit.ts";
|
|
33
|
+
import type { Harness } from "../events/schema.ts";
|
|
34
|
+
import type { ParsedPayload } from "../harness/parse.ts";
|
|
35
|
+
|
|
36
|
+
/** Raster + vector image extensions the feed accepts. PDF is intentionally
|
|
37
|
+
* excluded: book renders aren't an "image feed" and don't thumbnail inline. */
|
|
38
|
+
const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"]);
|
|
39
|
+
|
|
40
|
+
/** Skip hashing files larger than this; screenshots are tens of KB; a huge
|
|
41
|
+
* file mentioned in passing isn't worth the read. */
|
|
42
|
+
const MAX_CAPTURE_BYTES = 25 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
/** A produced image must have been written within this window of the command
|
|
45
|
+
* finishing, otherwise a path merely *mentioned* in output (an old baseline,
|
|
46
|
+
* a doc reference) would be captured. Viewed images skip this gate. */
|
|
47
|
+
const PRODUCED_MTIME_WINDOW_MS = 120_000;
|
|
48
|
+
|
|
49
|
+
/** Matches image-ish paths in a command string or its output. Deliberately
|
|
50
|
+
* permissive on the path body (`~`, `@`, `.`, `-`, `/`); existence + ext + the
|
|
51
|
+
* mtime gate do the real filtering. */
|
|
52
|
+
const IMAGE_PATH_RE = /[\w./~@+-]+\.(?:png|jpe?g|gif|webp|bmp|svg)\b/gi;
|
|
53
|
+
|
|
54
|
+
export interface CaptureContext {
|
|
55
|
+
eventType: "tool.pre_use" | "tool.post_use";
|
|
56
|
+
/** The built event data (tool_name, tool_input string, intent, tool_use_id). */
|
|
57
|
+
data: Record<string, unknown>;
|
|
58
|
+
/** The full parsed payload, used for the un-clamped command + tool_response. */
|
|
59
|
+
payload: ParsedPayload | null;
|
|
60
|
+
instanceId: string;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
harness: Harness;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Candidate {
|
|
66
|
+
path: string; // resolved absolute path
|
|
67
|
+
role: "viewed" | "produced";
|
|
68
|
+
intent?: string;
|
|
69
|
+
commandHead?: string;
|
|
70
|
+
requireRecentMtime: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Inspect one tool event for image references and capture any that resolve to
|
|
75
|
+
* a real image on disk. Emits zero or more `image.captured` events.
|
|
76
|
+
*/
|
|
77
|
+
export function captureImages(coordRoot: string, ctx: CaptureContext): void {
|
|
78
|
+
const imagesDir = join(coordRoot, ".harnery", "images");
|
|
79
|
+
const cwd = resolveCwd(ctx.payload);
|
|
80
|
+
const toolName = String(ctx.data.tool_name ?? "");
|
|
81
|
+
|
|
82
|
+
const candidates =
|
|
83
|
+
ctx.eventType === "tool.pre_use"
|
|
84
|
+
? collectViewed(toolName, ctx.data, cwd)
|
|
85
|
+
: collectProduced(toolName, ctx.payload, cwd);
|
|
86
|
+
|
|
87
|
+
if (candidates.length === 0) return;
|
|
88
|
+
|
|
89
|
+
for (const cand of candidates) {
|
|
90
|
+
// Never re-capture our own blob store (would loop on Reads of the gallery).
|
|
91
|
+
if (cand.path.startsWith(`${imagesDir}/`)) continue;
|
|
92
|
+
const captured = captureOne(imagesDir, cand);
|
|
93
|
+
if (!captured) continue;
|
|
94
|
+
emit(coordRoot, {
|
|
95
|
+
event_type: "image.captured",
|
|
96
|
+
instance_id: ctx.instanceId,
|
|
97
|
+
session_id: ctx.sessionId,
|
|
98
|
+
harness: ctx.harness,
|
|
99
|
+
data: {
|
|
100
|
+
hash: captured.hash,
|
|
101
|
+
ext: captured.ext,
|
|
102
|
+
bytes: captured.bytes,
|
|
103
|
+
role: cand.role,
|
|
104
|
+
source_path: canonicalize(coordRoot, cand.path),
|
|
105
|
+
tool_name: toolName,
|
|
106
|
+
tool_use_id: ctx.data.tool_use_id as string | undefined,
|
|
107
|
+
...(cand.intent ? { intent: cand.intent } : {}),
|
|
108
|
+
...(cand.commandHead ? { command_head: cand.commandHead } : {}),
|
|
109
|
+
},
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
111
|
+
} as Parameters<typeof emit>[1]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Read tool → the file_path it's about to show (the "agent saw this" signal). */
|
|
116
|
+
function collectViewed(toolName: string, data: Record<string, unknown>, cwd: string): Candidate[] {
|
|
117
|
+
if (toolName !== "Read") return [];
|
|
118
|
+
const input = parseToolInput(data.tool_input);
|
|
119
|
+
const filePath = input?.file_path;
|
|
120
|
+
if (typeof filePath !== "string" || !filePath) return [];
|
|
121
|
+
if (!hasImageExt(filePath)) return [];
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
path: toAbsolute(filePath, cwd),
|
|
125
|
+
role: "viewed",
|
|
126
|
+
intent: typeof data.intent === "string" ? data.intent : undefined,
|
|
127
|
+
requireRecentMtime: false,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Bash tool → scan the command + its output for freshly-written image files. */
|
|
133
|
+
function collectProduced(
|
|
134
|
+
toolName: string,
|
|
135
|
+
payload: ParsedPayload | null,
|
|
136
|
+
cwd: string,
|
|
137
|
+
): Candidate[] {
|
|
138
|
+
if (toolName !== "Bash") return [];
|
|
139
|
+
const command = bashCommand(payload);
|
|
140
|
+
const responseText = stringifyResponse(payload?.tool_response);
|
|
141
|
+
const commandHead = command ? command.slice(0, 120) : undefined;
|
|
142
|
+
|
|
143
|
+
const seen = new Set<string>();
|
|
144
|
+
const out: Candidate[] = [];
|
|
145
|
+
for (const text of [command, responseText]) {
|
|
146
|
+
if (!text) continue;
|
|
147
|
+
for (const m of text.matchAll(IMAGE_PATH_RE)) {
|
|
148
|
+
const raw = m[0];
|
|
149
|
+
const abs = toAbsolute(raw, cwd);
|
|
150
|
+
if (seen.has(abs)) continue;
|
|
151
|
+
seen.add(abs);
|
|
152
|
+
out.push({ path: abs, role: "produced", commandHead, requireRecentMtime: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface CapturedBlob {
|
|
159
|
+
hash: string;
|
|
160
|
+
ext: string;
|
|
161
|
+
bytes: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate the candidate on disk, hash it, and copy into the content-addressed
|
|
166
|
+
* store (skipping the copy when the blob already exists). Returns null when the
|
|
167
|
+
* candidate doesn't qualify.
|
|
168
|
+
*/
|
|
169
|
+
function captureOne(imagesDir: string, cand: Candidate): CapturedBlob | null {
|
|
170
|
+
let st: ReturnType<typeof statSync>;
|
|
171
|
+
try {
|
|
172
|
+
st = statSync(cand.path);
|
|
173
|
+
} catch {
|
|
174
|
+
return null; // path doesn't exist (common for "produced" false-positives)
|
|
175
|
+
}
|
|
176
|
+
if (!st.isFile() || st.size === 0 || st.size > MAX_CAPTURE_BYTES) return null;
|
|
177
|
+
if (cand.requireRecentMtime && Date.now() - st.mtimeMs > PRODUCED_MTIME_WINDOW_MS) {
|
|
178
|
+
return null; // mentioned but not freshly produced by this command
|
|
179
|
+
}
|
|
180
|
+
const ext = extOf(cand.path);
|
|
181
|
+
if (!ext || !IMAGE_EXTS.has(ext)) return null;
|
|
182
|
+
|
|
183
|
+
let bytes: Buffer;
|
|
184
|
+
try {
|
|
185
|
+
bytes = readFileSync(cand.path);
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
190
|
+
const dest = join(imagesDir, `${hash}.${ext}`);
|
|
191
|
+
if (!existsSync(dest)) {
|
|
192
|
+
try {
|
|
193
|
+
mkdirSync(imagesDir, { recursive: true });
|
|
194
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
195
|
+
writeFileSync(tmp, bytes);
|
|
196
|
+
renameSync(tmp, dest);
|
|
197
|
+
} catch {
|
|
198
|
+
return null; // couldn't store, don't emit a dangling event
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { hash, ext, bytes: st.size };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Prune `.harnery/images/` past a size cap (default 2 GB) and an age cap
|
|
206
|
+
* (default 30 days), oldest-mtime-first. Fired on session.start next to
|
|
207
|
+
* scratchJanitor. Pure-fs, fail-soft. Orphaned `image.captured` events whose
|
|
208
|
+
* blob was pruned render as an "expired" placeholder in the gallery.
|
|
209
|
+
*/
|
|
210
|
+
export function imageJanitor(coordRoot: string): void {
|
|
211
|
+
try {
|
|
212
|
+
const dir = join(coordRoot, ".harnery", "images");
|
|
213
|
+
if (!existsSync(dir)) return;
|
|
214
|
+
const maxBytes = envInt("HARNERY_IMAGES_MAX_BYTES", 2 * 1024 * 1024 * 1024);
|
|
215
|
+
const maxAgeMs = envInt("HARNERY_IMAGES_MAX_AGE_DAYS", 30) * 24 * 60 * 60 * 1000;
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
|
|
218
|
+
type Entry = { path: string; size: number; mtimeMs: number };
|
|
219
|
+
const entries: Entry[] = [];
|
|
220
|
+
for (const name of readdirSync(dir)) {
|
|
221
|
+
if (name.endsWith(".tmp") || name.includes(".tmp.")) {
|
|
222
|
+
// Orphaned temp from a crashed copy; sweep it.
|
|
223
|
+
rmSync(join(dir, name), { force: true });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const full = join(dir, name);
|
|
227
|
+
let st: ReturnType<typeof statSync>;
|
|
228
|
+
try {
|
|
229
|
+
st = statSync(full);
|
|
230
|
+
} catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!st.isFile()) continue;
|
|
234
|
+
entries.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Age cap.
|
|
238
|
+
let total = 0;
|
|
239
|
+
const survivors: Entry[] = [];
|
|
240
|
+
for (const e of entries) {
|
|
241
|
+
if (now - e.mtimeMs > maxAgeMs) {
|
|
242
|
+
rmSync(e.path, { force: true });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
total += e.size;
|
|
246
|
+
survivors.push(e);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Size cap: drop oldest first until under.
|
|
250
|
+
if (total > maxBytes) {
|
|
251
|
+
survivors.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
252
|
+
for (const e of survivors) {
|
|
253
|
+
if (total <= maxBytes) break;
|
|
254
|
+
rmSync(e.path, { force: true });
|
|
255
|
+
total -= e.size;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// best-effort
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
264
|
+
|
|
265
|
+
function envInt(name: string, fallback: number): number {
|
|
266
|
+
const v = process.env[name];
|
|
267
|
+
if (!v) return fallback;
|
|
268
|
+
const n = Number(v);
|
|
269
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseToolInput(raw: unknown): Record<string, unknown> | null {
|
|
273
|
+
if (typeof raw !== "string") return null;
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
276
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The un-clamped Bash command from the raw payload (falls back to clamped). */
|
|
283
|
+
function bashCommand(payload: ParsedPayload | null): string {
|
|
284
|
+
const ti = payload?.raw?.tool_input as Record<string, unknown> | undefined;
|
|
285
|
+
const cmd = ti?.command;
|
|
286
|
+
return typeof cmd === "string" ? cmd : "";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function stringifyResponse(value: unknown): string {
|
|
290
|
+
if (value == null) return "";
|
|
291
|
+
if (typeof value === "string") return value;
|
|
292
|
+
try {
|
|
293
|
+
return JSON.stringify(value);
|
|
294
|
+
} catch {
|
|
295
|
+
return "";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function resolveCwd(payload: ParsedPayload | null): string {
|
|
300
|
+
const raw = payload?.raw as Record<string, unknown> | undefined;
|
|
301
|
+
const cwd = raw?.cwd;
|
|
302
|
+
return typeof cwd === "string" && cwd ? cwd : process.cwd();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function hasImageExt(p: string): boolean {
|
|
306
|
+
const ext = extOf(p);
|
|
307
|
+
return !!ext && IMAGE_EXTS.has(ext);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function extOf(p: string): string {
|
|
311
|
+
const clean = p.split(/[?#]/)[0] ?? p; // drop any query/fragment
|
|
312
|
+
const dot = clean.lastIndexOf(".");
|
|
313
|
+
if (dot < 0) return "";
|
|
314
|
+
return clean.slice(dot + 1).toLowerCase();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function toAbsolute(p: string, cwd: string): string {
|
|
318
|
+
let path = p;
|
|
319
|
+
if (path.startsWith("~/")) {
|
|
320
|
+
const home = process.env.HOME;
|
|
321
|
+
if (home) path = join(home, path.slice(2));
|
|
322
|
+
}
|
|
323
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Strip the coordRoot prefix so the feed shows repo-relative paths. */
|
|
327
|
+
function canonicalize(coordRoot: string, p: string): string {
|
|
328
|
+
if (p.startsWith(`${coordRoot}/`)) return p.slice(coordRoot.length + 1);
|
|
329
|
+
return p;
|
|
330
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-coordination side effects fired from the normalized `agent-hook` handlers.
|
|
3
|
+
*
|
|
4
|
+
* These are DELIBERATELY outside the agent-coord path: sounds, scratch
|
|
5
|
+
* lifecycle, session telemetry, presence detection. They used to live in
|
|
6
|
+
* per-harness bash adapters. Per the directive ("use the normalized hooks; if
|
|
7
|
+
* they aren't coordination, implement them outside of coordination") they move
|
|
8
|
+
* here so the harness configs reference only `agent-hook`, while staying a
|
|
9
|
+
* distinct concern from the coordination logic in cli.ts / agent-coord.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is best-effort: it never throws and never blocks the hook on a
|
|
12
|
+
* slow dependency (telemetry runs detached).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { applyDetection } from "../../../lib/presence.ts";
|
|
20
|
+
|
|
21
|
+
export type { CaptureContext } from "./image-capture.ts";
|
|
22
|
+
export { captureImages, imageJanitor } from "./image-capture.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Play a notification sound via the cross-platform utility
|
|
26
|
+
* (afplay on macOS, powershell.exe on WSL). The
|
|
27
|
+
* utility backgrounds the actual player, so this returns fast. Rate-limiting
|
|
28
|
+
* lives in the utility, keyed on CLAUDE_SOUND_SESSION_ID + the per-event count.
|
|
29
|
+
* Claude-Code-only today (Cursor has no sounds; Codex's never worked).
|
|
30
|
+
*/
|
|
31
|
+
export function playSound(
|
|
32
|
+
repoRoot: string,
|
|
33
|
+
soundEvent: string,
|
|
34
|
+
sessionId: string,
|
|
35
|
+
maxPlays = 0,
|
|
36
|
+
): void {
|
|
37
|
+
try {
|
|
38
|
+
const player = join(repoRoot, "scripts", "hooks", "play-sound.sh");
|
|
39
|
+
if (!existsSync(player)) return;
|
|
40
|
+
spawnSync("bash", [player, soundEvent, String(maxPlays)], {
|
|
41
|
+
env: { ...process.env, CLAUDE_SOUND_SESSION_ID: sessionId },
|
|
42
|
+
timeout: 4000,
|
|
43
|
+
stdio: "ignore",
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
// best-effort
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Map an agent-hook CLI event name → sound, or null if the event has no sound. */
|
|
51
|
+
export function soundForEvent(eventName: string): { sound: string; maxPlays: number } | null {
|
|
52
|
+
switch (eventName) {
|
|
53
|
+
case "stop":
|
|
54
|
+
return { sound: "stop", maxPlays: 0 };
|
|
55
|
+
case "stop-failure":
|
|
56
|
+
return { sound: "error", maxPlays: 0 };
|
|
57
|
+
case "sub-agent-start":
|
|
58
|
+
return { sound: "subagent-start", maxPlays: 3 };
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function harnBin(repoRoot: string): string | null {
|
|
65
|
+
const bin = join(repoRoot, "harnery", "bin", "harn");
|
|
66
|
+
return existsSync(bin) ? bin : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Prune stale scratch archives + sweep orphans (global, fast). Fire-and-forget. */
|
|
70
|
+
export function scratchJanitor(repoRoot: string): void {
|
|
71
|
+
try {
|
|
72
|
+
const bin = harnBin(repoRoot);
|
|
73
|
+
if (!bin) return;
|
|
74
|
+
spawnSync("bash", [bin, "scratch", "janitor", "--quiet"], {
|
|
75
|
+
env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
|
|
76
|
+
timeout: 5000,
|
|
77
|
+
stdio: "ignore",
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
// best-effort
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Return the one-line scratch recovery cue for SessionStart, or "" if none.
|
|
86
|
+
* The caller merges it into the session-start additionalContext (it used to be
|
|
87
|
+
* a standalone additionalContext emission from the previous scratch-on-start adapter).
|
|
88
|
+
*/
|
|
89
|
+
export function scratchRecoveryCue(repoRoot: string): string {
|
|
90
|
+
try {
|
|
91
|
+
const bin = harnBin(repoRoot);
|
|
92
|
+
if (!bin) return "";
|
|
93
|
+
const r = spawnSync("bash", [bin, "scratch", "recovery-cue"], {
|
|
94
|
+
env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
|
|
95
|
+
timeout: 5000,
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
});
|
|
98
|
+
return (r.stdout ?? "").trim();
|
|
99
|
+
} catch {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Archive the ending agent's scratchpad. Fire-and-forget. */
|
|
105
|
+
export function scratchArchive(repoRoot: string, owner: string): void {
|
|
106
|
+
try {
|
|
107
|
+
const bin = harnBin(repoRoot);
|
|
108
|
+
if (!bin || !owner) return;
|
|
109
|
+
spawnSync("bash", [bin, "scratch", "archive", "--owner", owner], {
|
|
110
|
+
env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
|
|
111
|
+
timeout: 5000,
|
|
112
|
+
stdio: "ignore",
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
// best-effort
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sync Claude Code session JSONL → BigQuery (`harn claude-sessions sync`). Lives
|
|
121
|
+
* in the host CLI (parses Claude Code's own ~/.claude/projects/**\/*.jsonl), so harnery
|
|
122
|
+
* shells out to the harn binary rather than importing it. Detached + unref'd so a
|
|
123
|
+
* slow BigQuery round-trip never blocks the hook. Stop-path syncs are rate-
|
|
124
|
+
* limited inside `harn claude-sessions sync`; SessionEnd forces via env. Caller
|
|
125
|
+
* gates to the claude-code harness.
|
|
126
|
+
*/
|
|
127
|
+
export function syncClaudeSessions(repoRoot: string, force: boolean): void {
|
|
128
|
+
try {
|
|
129
|
+
const bin = join(repoRoot, "bin", "bp");
|
|
130
|
+
if (!existsSync(bin)) return;
|
|
131
|
+
const env: Record<string, string | undefined> = {
|
|
132
|
+
...process.env,
|
|
133
|
+
HARNERY_OUTPUT_SESSION_TEE: "0",
|
|
134
|
+
};
|
|
135
|
+
if (force) env.HARNERY_CLAUDE_SESSIONS_FORCE = "1";
|
|
136
|
+
const child = spawn("bash", [bin, "claude-sessions", "sync", "--quiet"], {
|
|
137
|
+
env,
|
|
138
|
+
detached: true,
|
|
139
|
+
stdio: "ignore",
|
|
140
|
+
});
|
|
141
|
+
child.unref();
|
|
142
|
+
} catch {
|
|
143
|
+
// best-effort
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fire the turn-summary extension (Haiku auto-summary of the turn → heartbeat
|
|
149
|
+
* `turn_summary`). Detached + unref'd; it makes an Anthropic API call and must
|
|
150
|
+
* never block the Stop hook. Claude-Code-only. The
|
|
151
|
+
* script self-guards on ANTHROPIC_API_KEY / curl / jq / matching session.
|
|
152
|
+
*/
|
|
153
|
+
export function runTurnSummary(
|
|
154
|
+
repoRoot: string,
|
|
155
|
+
owner: string,
|
|
156
|
+
sessionId: string,
|
|
157
|
+
transcriptPath: string | undefined,
|
|
158
|
+
): void {
|
|
159
|
+
try {
|
|
160
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return;
|
|
161
|
+
const script = join(
|
|
162
|
+
repoRoot,
|
|
163
|
+
"scripts",
|
|
164
|
+
"hooks",
|
|
165
|
+
"harness",
|
|
166
|
+
"claude_code",
|
|
167
|
+
"extensions",
|
|
168
|
+
"turn-summary.sh",
|
|
169
|
+
);
|
|
170
|
+
if (!existsSync(script)) return;
|
|
171
|
+
const child = spawn("bash", [script, owner, sessionId, transcriptPath], {
|
|
172
|
+
detached: true,
|
|
173
|
+
stdio: "ignore",
|
|
174
|
+
});
|
|
175
|
+
child.unref();
|
|
176
|
+
} catch {
|
|
177
|
+
// best-effort
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Reset per-turn sound rate-limit counters at the start of a new turn. */
|
|
182
|
+
export function resetSoundCounters(sessionId: string): void {
|
|
183
|
+
try {
|
|
184
|
+
if (!sessionId) return;
|
|
185
|
+
const dir = os.tmpdir();
|
|
186
|
+
const prefix = `claude-sounds-${sessionId}-`;
|
|
187
|
+
for (const f of readdirSync(dir)) {
|
|
188
|
+
if (f.startsWith(prefix) && f.endsWith(".count")) {
|
|
189
|
+
rmSync(join(dir, f), { force: true });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// best-effort
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Update the mobile-vs-office presence file from the user's prompt shape, using
|
|
199
|
+
* harnery's own presence library in-process: no external script, no host-path
|
|
200
|
+
* dependency. Claude-Code-only (the heuristic is tuned to CC's user-prompt
|
|
201
|
+
* payload; Cursor/Codex don't surface comparable prompt text). Fire-and-forget.
|
|
202
|
+
*/
|
|
203
|
+
export function detectPresence(prompt: string): void {
|
|
204
|
+
try {
|
|
205
|
+
if (!prompt) return;
|
|
206
|
+
applyDetection(prompt);
|
|
207
|
+
} catch {
|
|
208
|
+
// best-effort
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type EventEnvelope,
|
|
5
|
+
type EventType,
|
|
6
|
+
type Harness,
|
|
7
|
+
SCHEMA_VERSION,
|
|
8
|
+
type Source,
|
|
9
|
+
} from "./schema.ts";
|
|
10
|
+
import { ulid } from "./ulid.ts";
|
|
11
|
+
|
|
12
|
+
const LOCK_FILE = ".harnery/events.ndjson.lock";
|
|
13
|
+
const STREAM_FILE = ".harnery/events.ndjson";
|
|
14
|
+
|
|
15
|
+
const MAX_LINE_BYTES = 64 * 1024;
|
|
16
|
+
|
|
17
|
+
export interface EmitInput<TType extends EventType, TData> {
|
|
18
|
+
event_type: TType;
|
|
19
|
+
instance_id: string;
|
|
20
|
+
session_id: string;
|
|
21
|
+
harness: Harness;
|
|
22
|
+
source?: Source;
|
|
23
|
+
parent_session_id?: string;
|
|
24
|
+
turn_id?: string;
|
|
25
|
+
parent_turn_id?: string;
|
|
26
|
+
ts?: string;
|
|
27
|
+
data: TData;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a canonical envelope around the caller's `data` payload. Pure;
|
|
32
|
+
* doesn't touch the filesystem. Useful for tests + replay fixtures.
|
|
33
|
+
*/
|
|
34
|
+
export function buildEnvelope<TType extends EventType, TData>(
|
|
35
|
+
input: EmitInput<TType, TData>,
|
|
36
|
+
): EventEnvelope<TType, TData> {
|
|
37
|
+
return {
|
|
38
|
+
schema_version: SCHEMA_VERSION,
|
|
39
|
+
event_id: ulid(),
|
|
40
|
+
event_type: input.event_type,
|
|
41
|
+
ts: input.ts ?? new Date().toISOString(),
|
|
42
|
+
instance_id: input.instance_id,
|
|
43
|
+
session_id: input.session_id,
|
|
44
|
+
parent_session_id: input.parent_session_id,
|
|
45
|
+
turn_id: input.turn_id,
|
|
46
|
+
parent_turn_id: input.parent_turn_id,
|
|
47
|
+
harness: input.harness,
|
|
48
|
+
source: input.source ?? "agent-hooks",
|
|
49
|
+
data: input.data,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Append one canonical event to `.harnery/events.ndjson` under a file lock.
|
|
55
|
+
* Append-only, one JSON object per line, lock-serialized.
|
|
56
|
+
*
|
|
57
|
+
* Synchronous because hooks are short-lived processes: we want the append to
|
|
58
|
+
* land before the binary exits, and async/await across a tiny write is just
|
|
59
|
+
* latency for no benefit.
|
|
60
|
+
*
|
|
61
|
+
* Returns the envelope so callers can chain off it (debug logs, post-emit
|
|
62
|
+
* verdict requests).
|
|
63
|
+
*/
|
|
64
|
+
export function emit<TType extends EventType, TData>(
|
|
65
|
+
coordRoot: string,
|
|
66
|
+
input: EmitInput<TType, TData>,
|
|
67
|
+
): EventEnvelope<TType, TData> {
|
|
68
|
+
const envelope = buildEnvelope(input);
|
|
69
|
+
const streamPath = join(coordRoot, STREAM_FILE);
|
|
70
|
+
const lockPath = join(coordRoot, LOCK_FILE);
|
|
71
|
+
|
|
72
|
+
ensureDir(dirname(streamPath));
|
|
73
|
+
ensureFile(lockPath);
|
|
74
|
+
|
|
75
|
+
let line = `${JSON.stringify(envelope)}\n`;
|
|
76
|
+
if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) {
|
|
77
|
+
// lines >64KB are a schema bug. Phase 2 hard-truncates the `data`
|
|
78
|
+
// payload + sets `truncated: true` so we keep audit visibility without
|
|
79
|
+
// breaking downstream consumers.
|
|
80
|
+
const truncated = `${JSON.stringify({
|
|
81
|
+
...envelope,
|
|
82
|
+
data: { __over_size_limit: true, original_bytes: Buffer.byteLength(line, "utf8") },
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
84
|
+
})}\n`;
|
|
85
|
+
line = truncated;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const lockFd = openSync(lockPath, "r+");
|
|
89
|
+
try {
|
|
90
|
+
// flock isn't directly exposed in node fs; use fcntl-like via shelling out
|
|
91
|
+
// would be slower than POSIX flock(2) inside the kernel. Bun exposes
|
|
92
|
+
// `Bun.flock` in recent builds, but a simple fs-level append is atomic
|
|
93
|
+
// for lines <PIPE_BUF (4KB on Linux) anyway. For larger lines we rely on
|
|
94
|
+
// append-mode flag (O_APPEND) which kernels serialize per-fd.
|
|
95
|
+
//
|
|
96
|
+
// The `events.ndjson.lock` file is kept around for forward
|
|
97
|
+
// compatibility: Phase 4+ helpers acquire it before multi-write batches.
|
|
98
|
+
appendFileSync(streamPath, line, { encoding: "utf8", flag: "a" });
|
|
99
|
+
} finally {
|
|
100
|
+
closeSync(lockFd);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return envelope;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ensureDir(path: string): void {
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(path, { recursive: true });
|
|
109
|
+
} catch {
|
|
110
|
+
/* swallow */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensureFile(path: string): void {
|
|
115
|
+
try {
|
|
116
|
+
closeSync(openSync(path, "a"));
|
|
117
|
+
} catch {
|
|
118
|
+
/* swallow */
|
|
119
|
+
}
|
|
120
|
+
}
|