harnery 0.0.1 → 0.2.1
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 +509 -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 +39 -0
- package/dist/lib/readability/client.d.ts.map +1 -0
- package/dist/lib/readability/client.js +121 -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 +583 -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 +169 -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,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-claim verdict.
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 cutover: PreToolUse routes Edit/Write/NotebookEdit through here.
|
|
5
|
+
*
|
|
6
|
+
* Two checks:
|
|
7
|
+
* 1. Conflict: is the path already claimed by a fresh peer? Block with
|
|
8
|
+
* the peer's name in the reason.
|
|
9
|
+
* 2. Ordering: if we already hold a claim on path A and want path
|
|
10
|
+
* B, B must sort > A lexicographically. Otherwise emit
|
|
11
|
+
* claim.conflict (ordering_violation) and block.
|
|
12
|
+
*
|
|
13
|
+
* Reads from `.harnery/active/<id>.json`, the single canonical heartbeat
|
|
14
|
+
* location after Phase 8 collapsed the v1/v2 dual-write.
|
|
15
|
+
*/
|
|
16
|
+
export type VerdictResult = {
|
|
17
|
+
allow: boolean;
|
|
18
|
+
exit_code: 0 | 2;
|
|
19
|
+
rule: string;
|
|
20
|
+
reason?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface ClaimRequest {
|
|
23
|
+
rule: "claim";
|
|
24
|
+
instance_id: string;
|
|
25
|
+
session_id?: string;
|
|
26
|
+
path: string;
|
|
27
|
+
mode?: "read" | "write";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Evaluate a file-claim. Returns deny when another fresh peer claims the
|
|
31
|
+
* path, OR when this owner already holds a lower-sorted claim and the new
|
|
32
|
+
* one would create a circular wait.
|
|
33
|
+
*/
|
|
34
|
+
export declare function evaluateClaim(coordRoot: string, req: ClaimRequest): VerdictResult;
|
|
35
|
+
//# sourceMappingURL=claim-conflict.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/claim-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAeF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,aAAa,CAmEjF"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-claim verdict.
|
|
3
|
+
*
|
|
4
|
+
* Phase 5 cutover: PreToolUse routes Edit/Write/NotebookEdit through here.
|
|
5
|
+
*
|
|
6
|
+
* Two checks:
|
|
7
|
+
* 1. Conflict: is the path already claimed by a fresh peer? Block with
|
|
8
|
+
* the peer's name in the reason.
|
|
9
|
+
* 2. Ordering: if we already hold a claim on path A and want path
|
|
10
|
+
* B, B must sort > A lexicographically. Otherwise emit
|
|
11
|
+
* claim.conflict (ordering_violation) and block.
|
|
12
|
+
*
|
|
13
|
+
* Reads from `.harnery/active/<id>.json`, the single canonical heartbeat
|
|
14
|
+
* location after Phase 8 collapsed the v1/v2 dual-write.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
const FRESHNESS_SECS = 600;
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate a file-claim. Returns deny when another fresh peer claims the
|
|
22
|
+
* path, OR when this owner already holds a lower-sorted claim and the new
|
|
23
|
+
* one would create a circular wait.
|
|
24
|
+
*/
|
|
25
|
+
export function evaluateClaim(coordRoot, req) {
|
|
26
|
+
const peers = readPeers(coordRoot);
|
|
27
|
+
const myPeer = peers.find((p) => p.instance_id === req.instance_id);
|
|
28
|
+
// Group-ownership exclusion ("Option B" semantics): the parent
|
|
29
|
+
// session and all subagents under it share a group; claims within the
|
|
30
|
+
// group don't block each other. Compute the group root for the calling
|
|
31
|
+
// instance_id, then exclude any peer in the same group from the conflict
|
|
32
|
+
// scan. session_id from the payload is the parent_owner for subagents.
|
|
33
|
+
const groupRoot = computeGroupRoot(peers, req.instance_id, req.session_id);
|
|
34
|
+
const inMyGroup = (p) => p.instance_id === req.instance_id ||
|
|
35
|
+
p.instance_id === groupRoot ||
|
|
36
|
+
p.parent_instance_id === groupRoot ||
|
|
37
|
+
(p.parent_instance_id !== undefined && p.parent_instance_id === req.instance_id);
|
|
38
|
+
const otherPeers = peers.filter((p) => !inMyGroup(p));
|
|
39
|
+
// Conflict scan: any other fresh peer claiming this exact path?
|
|
40
|
+
const conflict = otherPeers.find((p) => isFresh(p.last_heartbeat) && p.files_touched.includes(req.path));
|
|
41
|
+
if (conflict) {
|
|
42
|
+
// Self-heal probe: if the file is committed-clean (exists in the repo AND has no uncommitted
|
|
43
|
+
// changes), the peer's claim is stale (worker crashed without releasing) →
|
|
44
|
+
// prune the claim from their heartbeat and allow the edit. Skips when the
|
|
45
|
+
// file doesn't exist (intent-to-create claim); that surface IS load-bearing.
|
|
46
|
+
if (isFileCommittedClean(coordRoot, req.path)) {
|
|
47
|
+
pruneClaimFromPeer(coordRoot, conflict.instance_id, req.path);
|
|
48
|
+
// Fall through to ordering check + allow.
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return {
|
|
52
|
+
allow: false,
|
|
53
|
+
exit_code: 2,
|
|
54
|
+
rule: "claim.conflict",
|
|
55
|
+
reason: `File ${req.path} is currently being edited by agent-${conflict.name ?? conflict.instance_id.slice(0, 8)}. Wait for them to finish or pick a different file. Set HARNERY_AGENT_COORD_OFF=1 to bypass (not recommended).`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Ordering check: if we hold any claim with path < req.path, fine.
|
|
60
|
+
// Otherwise the new claim would create a backward-edge in the dependency
|
|
61
|
+
// graph and risk deadlock. Only applies when there are OTHER fresh peers:
|
|
62
|
+
// single-agent flow can't deadlock with itself, and the rule otherwise
|
|
63
|
+
// forces release-and-reacquire cycles on every reverse-order edit pair.
|
|
64
|
+
const hasFreshPeers = otherPeers.some((p) => isFresh(p.last_heartbeat) && p.files_touched.length > 0);
|
|
65
|
+
if (hasFreshPeers && myPeer && myPeer.files_touched.length > 0) {
|
|
66
|
+
const highest = [...myPeer.files_touched].sort().at(-1);
|
|
67
|
+
if (req.path < highest) {
|
|
68
|
+
return {
|
|
69
|
+
allow: false,
|
|
70
|
+
exit_code: 2,
|
|
71
|
+
rule: "claim.ordering_violation",
|
|
72
|
+
reason: `Cannot acquire ${req.path} while holding ${highest} (claim ordering rule: paths must be acquired in sorted order). Release the higher claim first.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Acquire the claim: atomic check-and-set. Adds req.path to my
|
|
77
|
+
// files_touched if not already present.
|
|
78
|
+
if (req.mode !== "read") {
|
|
79
|
+
addClaimToOwner(coordRoot, req.instance_id, req.path);
|
|
80
|
+
}
|
|
81
|
+
return { allow: true, exit_code: 0, rule: "claim.pass" };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Add `relPath` to the owner's heartbeat `files_touched` array (idempotent;
|
|
85
|
+
* no-op if already present). Atomic temp + rename. If the owner has no
|
|
86
|
+
* heartbeat yet (subagent that hasn't been initialized), creates a minimal one.
|
|
87
|
+
*/
|
|
88
|
+
function addClaimToOwner(coordRoot, instanceId, relPath) {
|
|
89
|
+
const activeDir = join(coordRoot, ".harnery", "active");
|
|
90
|
+
if (!existsSync(activeDir))
|
|
91
|
+
return;
|
|
92
|
+
const path = join(activeDir, `${instanceId}.json`);
|
|
93
|
+
if (!existsSync(path)) {
|
|
94
|
+
const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
95
|
+
const minimal = {
|
|
96
|
+
schema_version: 1,
|
|
97
|
+
instance_id: instanceId,
|
|
98
|
+
session_id: instanceId,
|
|
99
|
+
files_touched: [relPath],
|
|
100
|
+
started_at: now,
|
|
101
|
+
last_heartbeat: now,
|
|
102
|
+
};
|
|
103
|
+
try {
|
|
104
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
105
|
+
writeFileSync(tmp, JSON.stringify(minimal, null, 2), "utf8");
|
|
106
|
+
renameSync(tmp, path);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* silent */
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const body = JSON.parse(readFileSync(path, "utf8"));
|
|
115
|
+
const files = body.files_touched ?? [];
|
|
116
|
+
if (files.includes(relPath))
|
|
117
|
+
return;
|
|
118
|
+
body.files_touched = [...files, relPath];
|
|
119
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
120
|
+
writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
121
|
+
renameSync(tmp, path);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* silent */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function readPeers(coordRoot) {
|
|
128
|
+
const out = [];
|
|
129
|
+
const path = join(coordRoot, ".harnery", "active");
|
|
130
|
+
if (!existsSync(path))
|
|
131
|
+
return out;
|
|
132
|
+
for (const f of readdirSync(path)) {
|
|
133
|
+
if (!f.endsWith(".json"))
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
const hb = JSON.parse(readFileSync(join(path, f), "utf8"));
|
|
137
|
+
if (!hb.instance_id)
|
|
138
|
+
continue;
|
|
139
|
+
// Derive parent_instance_id: explicit field first, then infer from
|
|
140
|
+
// session_id-differs-from-instance_id (the subagent shape:
|
|
141
|
+
// instance_id is the agent_id, session_id is the parent session).
|
|
142
|
+
const inferredParent = hb.parent_instance_id ??
|
|
143
|
+
hb.parent_session_id ??
|
|
144
|
+
(hb.session_id && hb.session_id !== hb.instance_id ? hb.session_id : undefined);
|
|
145
|
+
out.push({
|
|
146
|
+
instance_id: hb.instance_id,
|
|
147
|
+
name: hb.name,
|
|
148
|
+
session_id: hb.session_id ?? hb.instance_id,
|
|
149
|
+
files_touched: hb.files_touched ?? [],
|
|
150
|
+
last_heartbeat: hb.last_heartbeat ?? "",
|
|
151
|
+
parent_instance_id: inferredParent,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* skip */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Compute the group root for `instanceId`. For a parent session it's instanceId
|
|
162
|
+
* itself. For a subagent it's the parent_instance_id from the heartbeat
|
|
163
|
+
* (stamped on subagent heartbeats). When session_id is
|
|
164
|
+
* supplied and differs from instanceId (Claude Code's `session_id != agent_id`
|
|
165
|
+
* shape for subagents), that's the group root too.
|
|
166
|
+
*/
|
|
167
|
+
function computeGroupRoot(peers, instanceId, sessionId) {
|
|
168
|
+
// If we have a peer entry, its parent_instance_id is authoritative.
|
|
169
|
+
const myPeer = peers.find((p) => p.instance_id === instanceId);
|
|
170
|
+
if (myPeer?.parent_instance_id)
|
|
171
|
+
return myPeer.parent_instance_id;
|
|
172
|
+
// Fallback to session_id (parent's instance_id for subagents).
|
|
173
|
+
if (sessionId && sessionId !== instanceId)
|
|
174
|
+
return sessionId;
|
|
175
|
+
// I am the group root.
|
|
176
|
+
return instanceId;
|
|
177
|
+
}
|
|
178
|
+
function isFresh(lastHeartbeat) {
|
|
179
|
+
if (!lastHeartbeat)
|
|
180
|
+
return false;
|
|
181
|
+
const ts = Date.parse(lastHeartbeat);
|
|
182
|
+
if (!Number.isFinite(ts))
|
|
183
|
+
return false;
|
|
184
|
+
return (Date.now() - ts) / 1000 <= FRESHNESS_SECS;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if a monorepo-relative path is committed-clean: file exists in the
|
|
188
|
+
* repo, is tracked, AND `git diff HEAD -- path` shows no uncommitted
|
|
189
|
+
* modifications. Untracked files are NOT committed-clean: `git diff HEAD`
|
|
190
|
+
* exits 0 on an untracked path (because git ignores it), so we have to
|
|
191
|
+
* positively confirm tracking via `git ls-files` before trusting the diff.
|
|
192
|
+
*
|
|
193
|
+
* Returns false when:
|
|
194
|
+
* - file doesn't exist (intent-to-create, preserve the claim)
|
|
195
|
+
* - file is untracked (peer wrote it, hasn't staged yet, preserve the claim)
|
|
196
|
+
* - any git op fails (treat as dirty, fail-safe)
|
|
197
|
+
* - diff shows non-empty output (genuinely dirty)
|
|
198
|
+
*/
|
|
199
|
+
function isFileCommittedClean(coordRoot, relPath) {
|
|
200
|
+
const abs = join(coordRoot, relPath);
|
|
201
|
+
if (!existsSync(abs))
|
|
202
|
+
return false;
|
|
203
|
+
try {
|
|
204
|
+
const tracked = spawnSync("git", ["ls-files", "--error-unmatch", "--", relPath], {
|
|
205
|
+
cwd: coordRoot,
|
|
206
|
+
encoding: "utf8",
|
|
207
|
+
timeout: 2000,
|
|
208
|
+
});
|
|
209
|
+
if (tracked.status !== 0)
|
|
210
|
+
return false;
|
|
211
|
+
const result = spawnSync("git", ["diff", "--quiet", "HEAD", "--", relPath], {
|
|
212
|
+
cwd: coordRoot,
|
|
213
|
+
encoding: "utf8",
|
|
214
|
+
timeout: 2000,
|
|
215
|
+
});
|
|
216
|
+
return result.status === 0;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Remove a stale claim from a peer's heartbeat. Atomic temp + rename. Silent
|
|
224
|
+
* on failure.
|
|
225
|
+
*/
|
|
226
|
+
function pruneClaimFromPeer(coordRoot, instanceId, relPath) {
|
|
227
|
+
const path = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
|
|
228
|
+
if (!existsSync(path))
|
|
229
|
+
return;
|
|
230
|
+
try {
|
|
231
|
+
const body = JSON.parse(readFileSync(path, "utf8"));
|
|
232
|
+
const files = body.files_touched ?? [];
|
|
233
|
+
const next = files.filter((p) => p !== relPath);
|
|
234
|
+
if (next.length === files.length)
|
|
235
|
+
return;
|
|
236
|
+
body.files_touched = next;
|
|
237
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
238
|
+
writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
239
|
+
renameSync(tmp, path);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
/* silent */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-commit E-guard verdict. The caller (typically a bash pre-commit hook)
|
|
3
|
+
* sends a JSON request with the staged paths, resolves its own instance_id
|
|
4
|
+
* via pid-map, and prints the verdict messages + exits with
|
|
5
|
+
* verdict.exit_code.
|
|
6
|
+
*
|
|
7
|
+
* Three outcomes:
|
|
8
|
+
* 1. allow (no conflicts): exit 0.
|
|
9
|
+
* 2. allow + suppressed (self-attribution heuristic: holder's files ⊆
|
|
10
|
+
* staged set AND no live foreign pid anchors them): exit 0, prints
|
|
11
|
+
* "treating as self under transient identity".
|
|
12
|
+
* 3. block: exit 1, prints "Commit blocked by multi-agent coordination".
|
|
13
|
+
*
|
|
14
|
+
* `bypass: true` flips conflict → allow + warning lines (the
|
|
15
|
+
* `HARNERY_AGENT_COORD_BYPASS=1` escape hatch).
|
|
16
|
+
*
|
|
17
|
+
* Gitlink discrimination: a parent-repo staged submodule path is a pointer
|
|
18
|
+
* bump, NOT a claim on the submodule's contents. The caller supplies
|
|
19
|
+
* `staged_gitlinks[]` (cheap to compute via `git ls-files --stage`); paths
|
|
20
|
+
* in that set are matched with the staged-is-gitlink rule.
|
|
21
|
+
*/
|
|
22
|
+
interface Conflict {
|
|
23
|
+
/** Staged path that triggered the match. */
|
|
24
|
+
staged_path: string;
|
|
25
|
+
/** The peer's claimed path (may be a parent/child of staged_path). */
|
|
26
|
+
claimed_path: string;
|
|
27
|
+
/** Peer's instance_id. */
|
|
28
|
+
instance_id: string;
|
|
29
|
+
/** Peer's display name (or first 8 chars of instance_id). */
|
|
30
|
+
short_name: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CommitVerdictRequest {
|
|
33
|
+
instance_id: string;
|
|
34
|
+
/** Group key. Subagents inherit parent's session_id. */
|
|
35
|
+
session_id: string;
|
|
36
|
+
/** Canonical monorepo-relative paths. */
|
|
37
|
+
staged_paths: string[];
|
|
38
|
+
/** Paths in `staged_paths` that resolve to submodule gitlinks (mode 160000)
|
|
39
|
+
* in the index. Used for gitlink-discrimination prefix matching. */
|
|
40
|
+
staged_gitlinks?: string[];
|
|
41
|
+
/** `HARNERY_AGENT_COORD_BYPASS=1` was set. Conflicts become warnings, not blocks. */
|
|
42
|
+
bypass?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface CommitVerdictResult {
|
|
45
|
+
allow: boolean;
|
|
46
|
+
exit_code: number;
|
|
47
|
+
rule: string;
|
|
48
|
+
/** Conflict details (for the caller to print). */
|
|
49
|
+
conflicts: Conflict[];
|
|
50
|
+
/** Path-specific gates that fired (for `coord_log` lines). */
|
|
51
|
+
log_lines: string[];
|
|
52
|
+
/** Human-readable header for printing. */
|
|
53
|
+
message: string;
|
|
54
|
+
/** When true, conflicts were detected but suppressed (self-attribution). */
|
|
55
|
+
suppressed_self_attribution?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare function evaluateCommit(coordRoot: string, req: CommitVerdictRequest): CommitVerdictResult;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=commit-conflict.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/commit-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAiBH,UAAU,QAAQ;IAChB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;wEACoE;IACpE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,qFAAqF;IACrF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,mBAAmB,CAsGhG"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-commit E-guard verdict. The caller (typically a bash pre-commit hook)
|
|
3
|
+
* sends a JSON request with the staged paths, resolves its own instance_id
|
|
4
|
+
* via pid-map, and prints the verdict messages + exits with
|
|
5
|
+
* verdict.exit_code.
|
|
6
|
+
*
|
|
7
|
+
* Three outcomes:
|
|
8
|
+
* 1. allow (no conflicts): exit 0.
|
|
9
|
+
* 2. allow + suppressed (self-attribution heuristic: holder's files ⊆
|
|
10
|
+
* staged set AND no live foreign pid anchors them): exit 0, prints
|
|
11
|
+
* "treating as self under transient identity".
|
|
12
|
+
* 3. block: exit 1, prints "Commit blocked by multi-agent coordination".
|
|
13
|
+
*
|
|
14
|
+
* `bypass: true` flips conflict → allow + warning lines (the
|
|
15
|
+
* `HARNERY_AGENT_COORD_BYPASS=1` escape hatch).
|
|
16
|
+
*
|
|
17
|
+
* Gitlink discrimination: a parent-repo staged submodule path is a pointer
|
|
18
|
+
* bump, NOT a claim on the submodule's contents. The caller supplies
|
|
19
|
+
* `staged_gitlinks[]` (cheap to compute via `git ls-files --stage`); paths
|
|
20
|
+
* in that set are matched with the staged-is-gitlink rule.
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { coordEnv } from "../../../lib/env.js";
|
|
26
|
+
const DEFAULT_FRESHNESS_SECS = 600;
|
|
27
|
+
export function evaluateCommit(coordRoot, req) {
|
|
28
|
+
if (req.staged_paths.length === 0) {
|
|
29
|
+
return {
|
|
30
|
+
allow: true,
|
|
31
|
+
exit_code: 0,
|
|
32
|
+
rule: "commit.pass",
|
|
33
|
+
conflicts: [],
|
|
34
|
+
log_lines: [],
|
|
35
|
+
message: "",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const peers = readActivePeers(coordRoot);
|
|
39
|
+
const cutoffMs = Date.now() - freshnessSecs() * 1000;
|
|
40
|
+
const stagedSet = new Set(req.staged_paths);
|
|
41
|
+
const gitlinkSet = new Set(req.staged_gitlinks ?? []);
|
|
42
|
+
const conflicts = [];
|
|
43
|
+
for (const peer of peers) {
|
|
44
|
+
if (peer.instance_id === req.instance_id)
|
|
45
|
+
continue;
|
|
46
|
+
const peerSession = peer.session_id ?? peer.instance_id;
|
|
47
|
+
if (req.session_id && peerSession === req.session_id)
|
|
48
|
+
continue; // same group
|
|
49
|
+
const ts = peer.last_heartbeat ? Date.parse(peer.last_heartbeat) : 0;
|
|
50
|
+
if (!Number.isFinite(ts) || ts < cutoffMs)
|
|
51
|
+
continue; // stale
|
|
52
|
+
const files = peer.files_touched ?? [];
|
|
53
|
+
if (files.length === 0)
|
|
54
|
+
continue;
|
|
55
|
+
for (const staged of req.staged_paths) {
|
|
56
|
+
const stagedIsGitlink = gitlinkSet.has(staged);
|
|
57
|
+
const hit = findOverlap(staged, files, coordRoot, stagedIsGitlink);
|
|
58
|
+
if (!hit)
|
|
59
|
+
continue;
|
|
60
|
+
conflicts.push({
|
|
61
|
+
staged_path: staged,
|
|
62
|
+
claimed_path: hit,
|
|
63
|
+
instance_id: peer.instance_id,
|
|
64
|
+
short_name: shortName(peer),
|
|
65
|
+
});
|
|
66
|
+
break; // one conflict per peer is enough
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (conflicts.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
allow: true,
|
|
72
|
+
exit_code: 0,
|
|
73
|
+
rule: "commit.pass",
|
|
74
|
+
conflicts: [],
|
|
75
|
+
log_lines: [],
|
|
76
|
+
message: "",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (req.bypass) {
|
|
80
|
+
return {
|
|
81
|
+
allow: true,
|
|
82
|
+
exit_code: 0,
|
|
83
|
+
rule: "commit.bypass",
|
|
84
|
+
conflicts,
|
|
85
|
+
log_lines: conflicts.map((c) => `COMMIT_BYPASSED path=${c.staged_path} owner=${c.short_name}`),
|
|
86
|
+
message: "⚠ Multi-Agent coordination: bypass active; staged paths claimed by other agents will be committed anyway:",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Self-attribution check (Fix #2): if every conflicting holder is plausibly
|
|
90
|
+
// us under a transient identity (held files_touched ⊆ the staged set AND no
|
|
91
|
+
// live foreign PID anchors the holder via pid-map), suppress the block.
|
|
92
|
+
const allSelfAttributed = conflicts.every((c) => isHolderSelfAttributed(coordRoot, c.instance_id, stagedSet, peers));
|
|
93
|
+
if (allSelfAttributed) {
|
|
94
|
+
return {
|
|
95
|
+
allow: true,
|
|
96
|
+
exit_code: 0,
|
|
97
|
+
rule: "commit.suppressed",
|
|
98
|
+
conflicts,
|
|
99
|
+
log_lines: conflicts.map((c) => `COMMIT_SUPPRESSED path=${c.staged_path} owner=${c.short_name} reason=self_attribution`),
|
|
100
|
+
message: "⚠ Multi-Agent coordination: self-attribution detected; staged paths\n" +
|
|
101
|
+
" are claimed by an unanchored heartbeat that holds only files in\n" +
|
|
102
|
+
" this commit's staged set. Treating as self under a transient\n" +
|
|
103
|
+
" identity; commit will proceed.",
|
|
104
|
+
suppressed_self_attribution: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
allow: false,
|
|
109
|
+
exit_code: 1,
|
|
110
|
+
rule: "commit.conflict",
|
|
111
|
+
conflicts,
|
|
112
|
+
log_lines: conflicts.map((c) => `COMMIT_BLOCKED path=${c.staged_path} owner=${c.short_name}`),
|
|
113
|
+
message: "✗ Commit blocked by multi-agent coordination (E guard).\n\n" +
|
|
114
|
+
" The following staged paths are currently claimed by other\n" +
|
|
115
|
+
" active agents:",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function findOverlap(staged, files, coordRoot, stagedIsGitlink) {
|
|
119
|
+
for (const claimed of files) {
|
|
120
|
+
if (claimed === staged)
|
|
121
|
+
return claimed;
|
|
122
|
+
if (claimed.startsWith(`${staged}/`)) {
|
|
123
|
+
// Staged is a submodule gitlink, claimed is a file inside it. Disjoint.
|
|
124
|
+
if (stagedIsGitlink)
|
|
125
|
+
continue;
|
|
126
|
+
return claimed;
|
|
127
|
+
}
|
|
128
|
+
if (staged.startsWith(`${claimed}/`)) {
|
|
129
|
+
// Claimed is a gitlink, staged is a file inside it. Disjoint.
|
|
130
|
+
if (isGitlinkInIndex(coordRoot, claimed))
|
|
131
|
+
continue;
|
|
132
|
+
return claimed;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function isGitlinkInIndex(coordRoot, path) {
|
|
138
|
+
const result = spawnSync("git", ["ls-files", "--stage", "--", path], {
|
|
139
|
+
cwd: coordRoot,
|
|
140
|
+
encoding: "utf8",
|
|
141
|
+
timeout: 2000,
|
|
142
|
+
});
|
|
143
|
+
if (result.status !== 0)
|
|
144
|
+
return false;
|
|
145
|
+
// ls-files --stage emits "<mode> <sha> <stage>\t<path>"; mode 160000 = gitlink.
|
|
146
|
+
return result.stdout.trim().startsWith("160000 ");
|
|
147
|
+
}
|
|
148
|
+
function isHolderSelfAttributed(coordRoot, holderId, stagedSet, peers) {
|
|
149
|
+
// Gate B (cheaper): live foreign pid-map entry blocks self-attribution.
|
|
150
|
+
if (holderHasLiveForeignPid(coordRoot, holderId))
|
|
151
|
+
return false;
|
|
152
|
+
// Gate A: every held path is either in the staged set or already clean in HEAD.
|
|
153
|
+
const holder = peers.find((p) => p.instance_id === holderId);
|
|
154
|
+
if (!holder)
|
|
155
|
+
return false;
|
|
156
|
+
const files = holder.files_touched ?? [];
|
|
157
|
+
if (files.length === 0)
|
|
158
|
+
return false;
|
|
159
|
+
for (const held of files) {
|
|
160
|
+
if (stagedSet.has(held))
|
|
161
|
+
continue;
|
|
162
|
+
if (isPathCleanInHead(coordRoot, held))
|
|
163
|
+
continue;
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
function holderHasLiveForeignPid(coordRoot, holderId) {
|
|
169
|
+
const dir = join(coordRoot, ".harnery", "pid-map");
|
|
170
|
+
if (!existsSync(dir))
|
|
171
|
+
return false;
|
|
172
|
+
for (const f of readdirSync(dir)) {
|
|
173
|
+
let row = "";
|
|
174
|
+
try {
|
|
175
|
+
row = readFileSync(join(dir, f), "utf8").trim();
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const owner = row.split("\t")[0]?.trim() ?? "";
|
|
181
|
+
if (owner !== holderId)
|
|
182
|
+
continue;
|
|
183
|
+
// Process still alive?
|
|
184
|
+
const pid = Number.parseInt(f, 10);
|
|
185
|
+
if (!Number.isFinite(pid))
|
|
186
|
+
continue;
|
|
187
|
+
try {
|
|
188
|
+
process.kill(pid, 0); // signal 0 = liveness probe
|
|
189
|
+
return true; // live foreign pid found
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// ESRCH (no such process): pid-map entry is stale, skip
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
function isPathCleanInHead(coordRoot, relPath) {
|
|
198
|
+
// Path is tracked + diff-clean against HEAD = "already committed, holder
|
|
199
|
+
// hasn't released the claim yet". Counts as self-attributable.
|
|
200
|
+
const tracked = spawnSync("git", ["ls-files", "--error-unmatch", "--", relPath], {
|
|
201
|
+
cwd: coordRoot,
|
|
202
|
+
encoding: "utf8",
|
|
203
|
+
timeout: 2000,
|
|
204
|
+
});
|
|
205
|
+
if (tracked.status !== 0)
|
|
206
|
+
return false;
|
|
207
|
+
const diff = spawnSync("git", ["diff", "--quiet", "HEAD", "--", relPath], {
|
|
208
|
+
cwd: coordRoot,
|
|
209
|
+
encoding: "utf8",
|
|
210
|
+
timeout: 2000,
|
|
211
|
+
});
|
|
212
|
+
return diff.status === 0;
|
|
213
|
+
}
|
|
214
|
+
function readActivePeers(coordRoot) {
|
|
215
|
+
const dir = join(coordRoot, ".harnery", "active");
|
|
216
|
+
if (!existsSync(dir))
|
|
217
|
+
return [];
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const f of readdirSync(dir)) {
|
|
220
|
+
if (!f.endsWith(".json"))
|
|
221
|
+
continue;
|
|
222
|
+
try {
|
|
223
|
+
const hb = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
224
|
+
if (hb.instance_id)
|
|
225
|
+
out.push(hb);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
/* skip */
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
function freshnessSecs() {
|
|
234
|
+
const raw = coordEnv("AGENT_FRESHNESS");
|
|
235
|
+
if (!raw)
|
|
236
|
+
return DEFAULT_FRESHNESS_SECS;
|
|
237
|
+
const n = Number.parseInt(raw, 10);
|
|
238
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_FRESHNESS_SECS;
|
|
239
|
+
}
|
|
240
|
+
function shortName(peer) {
|
|
241
|
+
if (peer.name && peer.name.length > 0)
|
|
242
|
+
return `agent-${peer.name}`;
|
|
243
|
+
return `agent-${peer.instance_id.slice(0, 8)}`;
|
|
244
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop-hook verdict: rule 1/3, 2/3, 3/3 enforcement evaluated
|
|
3
|
+
* from the canonical event stream alone (no transcript scan).
|
|
4
|
+
*
|
|
5
|
+
* Rule 1/3: `state.status_checked` event with matching turn boundary exists.
|
|
6
|
+
* Rule 2/3: latest `turn.stop` event has `status_box_present: true` (or the
|
|
7
|
+
* stop currently firing carries that field via the in-flight event
|
|
8
|
+
* agent-hook emits before the stop hook runs).
|
|
9
|
+
* Rule 3/3: `state.task_set` event with matching turn boundary exists.
|
|
10
|
+
*
|
|
11
|
+
* Pure-prose-turn exemption: when zero `tool.pre_use` events fire in
|
|
12
|
+
* the current turn, rules 1/3 and 3/3 do not apply. Rule 2/3 still applies
|
|
13
|
+
* as a user-visible mobile cue.
|
|
14
|
+
*/
|
|
15
|
+
export type VerdictResult = {
|
|
16
|
+
allow: boolean;
|
|
17
|
+
exit_code: 0 | 2;
|
|
18
|
+
rule: string;
|
|
19
|
+
reason?: string;
|
|
20
|
+
};
|
|
21
|
+
export interface StopHookRequest {
|
|
22
|
+
rule: "stop-hook";
|
|
23
|
+
instance_id: string;
|
|
24
|
+
session_id?: string;
|
|
25
|
+
/** Firing harness. Selects the end-of-turn ack signal (see `ackSignalFor`).
|
|
26
|
+
* Undefined → Claude Code semantics (transcript-scanned status box). */
|
|
27
|
+
harness?: string;
|
|
28
|
+
/** Wall-clock cutoff for the current turn; events strictly after this are not yet relevant. */
|
|
29
|
+
now_ms?: number;
|
|
30
|
+
/** Override the turn-window discovery (used by tests). */
|
|
31
|
+
turn_window?: {
|
|
32
|
+
start_ms: number;
|
|
33
|
+
end_ms: number;
|
|
34
|
+
};
|
|
35
|
+
/** Bypass switch: operator escape hatch identical to HARNERY_AGENT_COORD_BYPASS_STOP. */
|
|
36
|
+
bypass?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Evaluate the Stop-hook verdict. Returns the first failing rule (or allow
|
|
40
|
+
* when all three pass). Soft-fails open when the event stream isn't
|
|
41
|
+
* readable, fail-open posture on a verdict failure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function evaluateStopHook(coordRoot: string, req: StopHookRequest): VerdictResult;
|
|
44
|
+
//# sourceMappingURL=stop-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stop-hook.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/stop-hook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAcF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;4EACwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA+BD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,aAAa,CA2FvF"}
|