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,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionStart UX renderer: combines the peer table, the wiring check, and
|
|
3
|
+
* the pending-council formatter so agent-hook session.start can emit the
|
|
4
|
+
* combined `systemMessage` JSON directly.
|
|
5
|
+
*
|
|
6
|
+
* Outputs a Claude Code SessionStart hookSpecificOutput.additionalContext
|
|
7
|
+
* string.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { resolveBinName } from "../../config.ts";
|
|
14
|
+
|
|
15
|
+
interface HeartbeatRow {
|
|
16
|
+
instance_id?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
kind?: string;
|
|
19
|
+
session_id?: string;
|
|
20
|
+
started_at?: string;
|
|
21
|
+
last_heartbeat?: string;
|
|
22
|
+
last_tool?: string;
|
|
23
|
+
last_tool_target?: string;
|
|
24
|
+
files_touched?: string[];
|
|
25
|
+
platform?: string;
|
|
26
|
+
task?: string;
|
|
27
|
+
turn_summary?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RenderOpts {
|
|
31
|
+
coordRoot: string;
|
|
32
|
+
instanceId: string;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
agentName?: string;
|
|
35
|
+
/** Harness label rendered in parens after the self-name (e.g. "Cursor", "Codex").
|
|
36
|
+
* Claude Code omits it. */
|
|
37
|
+
platformLabel?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the combined SessionStart systemMessage. Returns the additionalContext
|
|
42
|
+
* string (or "" if there's nothing to say).
|
|
43
|
+
*/
|
|
44
|
+
export function renderSessionContext(opts: RenderOpts): string {
|
|
45
|
+
const { coordRoot, instanceId, sessionId, agentName, platformLabel } = opts;
|
|
46
|
+
const messages: string[] = [];
|
|
47
|
+
|
|
48
|
+
// 1. Self-name line + peer table (folded if peers present)
|
|
49
|
+
const peers = readActivePeers(coordRoot, instanceId);
|
|
50
|
+
const peerTable = formatPeerTable(peers, sessionId);
|
|
51
|
+
if (agentName) {
|
|
52
|
+
const suffix = platformLabel ? ` (${platformLabel})` : "";
|
|
53
|
+
const selfLine = `You are agent-${agentName}${suffix}.`;
|
|
54
|
+
messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
|
|
55
|
+
} else if (peerTable) {
|
|
56
|
+
messages.push(peerTable);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Linked-worktree detection
|
|
60
|
+
if (isLinkedWorktree(coordRoot)) {
|
|
61
|
+
messages.push(
|
|
62
|
+
`Running inside worktree ${process.cwd()}. The coord layer is scoped to this worktree only; use \`${resolveBinName(coordRoot)} worktree diff\` to check for conflicts against sibling worktrees.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Council invites
|
|
67
|
+
if (agentName) {
|
|
68
|
+
const councilMsg = formatPendingCouncils(coordRoot, agentName);
|
|
69
|
+
if (councilMsg) messages.push(councilMsg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. Wiring check
|
|
73
|
+
const wiringIssues = checkWiring(coordRoot);
|
|
74
|
+
if (wiringIssues.length > 0) {
|
|
75
|
+
const wiringSummary = `Coordination hooks are NOT wired: the E-guard will not block conflicting commits, and post-commit claim pruning will not run. Run \`scripts/setup-hooks.sh\` to fix. Detected:\n${wiringIssues.map((i) => ` - ${i}`).join("\n")}`;
|
|
76
|
+
messages.push(wiringSummary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return messages.join("\n\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read all peer heartbeats from .harnery/active/, excluding self. */
|
|
83
|
+
function readActivePeers(coordRoot: string, selfInstanceId: string): HeartbeatRow[] {
|
|
84
|
+
const out: HeartbeatRow[] = [];
|
|
85
|
+
const dir = join(coordRoot, ".harnery", "active");
|
|
86
|
+
if (!existsSync(dir)) return out;
|
|
87
|
+
for (const f of readdirSync(dir)) {
|
|
88
|
+
if (!f.endsWith(".json")) continue;
|
|
89
|
+
try {
|
|
90
|
+
const hb = JSON.parse(readFileSync(join(dir, f), "utf8")) as HeartbeatRow;
|
|
91
|
+
if (!hb.instance_id || hb.instance_id === selfInstanceId) continue;
|
|
92
|
+
out.push(hb);
|
|
93
|
+
} catch {
|
|
94
|
+
/* skip */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Renders the peer table as
|
|
102
|
+
* two subsections: "Other agent groups active" (blocking) and "Your group"
|
|
103
|
+
* (subagents/siblings, no mutual block). Folds transient subagents' files
|
|
104
|
+
* into their parent session.
|
|
105
|
+
*/
|
|
106
|
+
function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
|
|
107
|
+
if (peers.length === 0) return "";
|
|
108
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
109
|
+
|
|
110
|
+
// Fold transient peers' files into their session_id parent.
|
|
111
|
+
const fold: Record<string, string[]> = {};
|
|
112
|
+
for (const p of peers) {
|
|
113
|
+
const kind = p.kind ?? "unknown";
|
|
114
|
+
if (kind === "transient" && p.session_id) {
|
|
115
|
+
fold[p.session_id] = (fold[p.session_id] ?? []).concat(p.files_touched ?? []);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build rows with display_files (own files + folded transient files).
|
|
120
|
+
type RowExt = HeartbeatRow & { display_files: string[] };
|
|
121
|
+
const rows: RowExt[] = peers
|
|
122
|
+
.filter((p) => (p.kind ?? "unknown") !== "transient")
|
|
123
|
+
.map((p) => {
|
|
124
|
+
const folded = fold[p.instance_id ?? ""] ?? [];
|
|
125
|
+
const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
|
|
126
|
+
return { ...p, display_files: display };
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const blocking = rows.filter((p) => p.session_id !== mySessionId).sort(byStartedAt);
|
|
130
|
+
const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
|
|
131
|
+
|
|
132
|
+
const sections: string[] = [];
|
|
133
|
+
const blockingSection = renderSubtable(
|
|
134
|
+
blocking,
|
|
135
|
+
"Other agent groups active (their files block you):",
|
|
136
|
+
nowSec,
|
|
137
|
+
);
|
|
138
|
+
if (blockingSection) sections.push(blockingSection);
|
|
139
|
+
const groupSection = renderSubtable(
|
|
140
|
+
group,
|
|
141
|
+
"Your group (subagents / parent / siblings; no mutual block):",
|
|
142
|
+
nowSec,
|
|
143
|
+
);
|
|
144
|
+
if (groupSection) sections.push(groupSection);
|
|
145
|
+
|
|
146
|
+
return sections.join("\n\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function byStartedAt(a: HeartbeatRow, b: HeartbeatRow): number {
|
|
150
|
+
return (a.started_at ?? "").localeCompare(b.started_at ?? "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderSubtable(
|
|
154
|
+
rows: Array<HeartbeatRow & { display_files: string[] }>,
|
|
155
|
+
header: string,
|
|
156
|
+
nowSec: number,
|
|
157
|
+
): string {
|
|
158
|
+
if (rows.length === 0) return "";
|
|
159
|
+
const first = rows.slice(0, 10).map((r) => formatRow(r, nowSec));
|
|
160
|
+
const overflow = rows.length > 10 ? `\n +${rows.length - 10} more` : "";
|
|
161
|
+
return `${header}\n${first.join("\n")}${overflow}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number): string {
|
|
165
|
+
const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
|
|
166
|
+
const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
|
|
167
|
+
const filesPart = fmtFiles(r.display_files);
|
|
168
|
+
const lastActivity = fmtLastActivity(r, nowSec);
|
|
169
|
+
const turnSummary = r.turn_summary ? `\n last turn: ${r.turn_summary.slice(0, 80)}` : "";
|
|
170
|
+
return ` - agent-${r.name ?? "unknown"}${taskPart} (${ageFrom}, ${filesPart}${lastActivity})${turnSummary}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function fmtFiles(files: string[]): string {
|
|
174
|
+
if (files.length === 0) return "nothing yet";
|
|
175
|
+
if (files.length <= 3) return `holds: ${files.join(", ")}`;
|
|
176
|
+
return `holds: ${files.slice(0, 3).join(", ")}, +${files.length - 3} more`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function fmtLastActivity(r: HeartbeatRow, nowSec: number): string {
|
|
180
|
+
if (!r.last_tool) return "";
|
|
181
|
+
const lastTs = parseIsoSec(r.last_heartbeat ?? r.started_at);
|
|
182
|
+
const ageSec = nowSec - lastTs;
|
|
183
|
+
const tail = r.last_tool_target ? ` ${r.last_tool_target.slice(0, 60)}` : "";
|
|
184
|
+
return `, last: ${r.last_tool}${tail} ${fmtAge(ageSec)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseIsoSec(iso: string | undefined): number {
|
|
188
|
+
if (!iso) return 0;
|
|
189
|
+
const ms = Date.parse(iso);
|
|
190
|
+
return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function fmtAge(secs: number): string {
|
|
194
|
+
if (secs < 60) return `${Math.floor(secs)}s ago`;
|
|
195
|
+
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
|
|
196
|
+
if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
|
|
197
|
+
return `${Math.floor(secs / 86400)}d ago`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns a list of wiring issues (empty when everything's wired). Checks
|
|
202
|
+
* parent core.hooksPath + one representative submodule.
|
|
203
|
+
*/
|
|
204
|
+
export function checkWiring(coordRoot: string): string[] {
|
|
205
|
+
const expected = join(coordRoot, "scripts", "hooks");
|
|
206
|
+
const issues: string[] = [];
|
|
207
|
+
|
|
208
|
+
// Parent repo
|
|
209
|
+
const parentHp = gitConfig(coordRoot, "core.hooksPath");
|
|
210
|
+
const parentResolved = resolveHooksPath(coordRoot, parentHp);
|
|
211
|
+
if (parentResolved !== expected) {
|
|
212
|
+
issues.push(
|
|
213
|
+
`parent core.hooksPath=${parentHp || "<unset>"} (resolves to ${parentResolved}, expected ${expected})`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// One representative submodule
|
|
218
|
+
const gitmodules = join(coordRoot, ".gitmodules");
|
|
219
|
+
if (existsSync(gitmodules)) {
|
|
220
|
+
const sampleSub = extractFirstSubmodule(gitmodules);
|
|
221
|
+
if (sampleSub) {
|
|
222
|
+
const subPath = join(coordRoot, sampleSub);
|
|
223
|
+
const subGitDir = join(subPath, ".git");
|
|
224
|
+
if (existsSync(subGitDir)) {
|
|
225
|
+
const subHp = gitConfig(subPath, "core.hooksPath");
|
|
226
|
+
const subResolved = resolveSubmoduleHooksPath(coordRoot, sampleSub, subHp);
|
|
227
|
+
if (subResolved !== expected) {
|
|
228
|
+
issues.push(
|
|
229
|
+
`submodule ${sampleSub} core.hooksPath=${subHp || "<unset>"} (resolves to ${subResolved}; other submodules likely affected too)`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return issues;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function gitConfig(cwd: string, key: string): string {
|
|
240
|
+
const result = spawnSync("git", ["-C", cwd, "config", "--get", key], { encoding: "utf8" });
|
|
241
|
+
if (result.status !== 0) return "";
|
|
242
|
+
return result.stdout.trim();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveHooksPath(root: string, hp: string): string {
|
|
246
|
+
if (!hp) return join(root, ".git", "hooks");
|
|
247
|
+
if (hp.startsWith("/")) return hp.replace(/\/$/, "");
|
|
248
|
+
return join(root, hp).replace(/\/$/, "");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveSubmoduleHooksPath(root: string, sub: string, hp: string): string {
|
|
252
|
+
if (!hp) return join(root, sub, ".git", "hooks");
|
|
253
|
+
if (hp.startsWith("/")) return hp.replace(/\/$/, "");
|
|
254
|
+
return join(root, sub, hp).replace(/\/$/, "");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function extractFirstSubmodule(gitmodulesPath: string): string | null {
|
|
258
|
+
try {
|
|
259
|
+
const content = readFileSync(gitmodulesPath, "utf8");
|
|
260
|
+
const match = content.match(/^\s*path\s*=\s*(.+)$/m);
|
|
261
|
+
return match ? match[1]!.trim() : null;
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Detect linked-worktree environment (`git worktree` created from the
|
|
269
|
+
* superproject) via a `git rev-parse --git-dir` vs `--git-common-dir` check.
|
|
270
|
+
*/
|
|
271
|
+
function isLinkedWorktree(coordRoot: string): boolean {
|
|
272
|
+
const dir = spawnSync("git", ["-C", coordRoot, "rev-parse", "--git-dir"], { encoding: "utf8" });
|
|
273
|
+
const common = spawnSync("git", ["-C", coordRoot, "rev-parse", "--git-common-dir"], {
|
|
274
|
+
encoding: "utf8",
|
|
275
|
+
});
|
|
276
|
+
if (dir.status !== 0 || common.status !== 0) return false;
|
|
277
|
+
const d = dir.stdout.trim();
|
|
278
|
+
const c = common.stdout.trim();
|
|
279
|
+
return d !== "" && c !== "" && d !== c;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Returns the formatted council invite reminder
|
|
284
|
+
* (or "" when no councils await input).
|
|
285
|
+
*/
|
|
286
|
+
export function formatPendingCouncils(coordRoot: string, agentName: string): string {
|
|
287
|
+
const councilsDir = join(coordRoot, ".harnery", "councils");
|
|
288
|
+
if (!existsSync(councilsDir)) return "";
|
|
289
|
+
const canonicalName = agentName.startsWith("agent-") ? agentName : `agent-${agentName}`;
|
|
290
|
+
const pending: string[] = [];
|
|
291
|
+
try {
|
|
292
|
+
for (const f of readdirSync(councilsDir)) {
|
|
293
|
+
if (!f.endsWith(".json")) continue;
|
|
294
|
+
const manifestPath = join(councilsDir, f);
|
|
295
|
+
try {
|
|
296
|
+
const m = JSON.parse(readFileSync(manifestPath, "utf8")) as {
|
|
297
|
+
council_id?: string;
|
|
298
|
+
status?: string;
|
|
299
|
+
round_status?: string;
|
|
300
|
+
current_round?: number;
|
|
301
|
+
members?: string[];
|
|
302
|
+
};
|
|
303
|
+
if (m.status !== "active" || m.round_status !== "open") continue;
|
|
304
|
+
if (!m.members?.includes(canonicalName)) continue;
|
|
305
|
+
const round = m.current_round ?? 1;
|
|
306
|
+
const contributionPath = join(
|
|
307
|
+
councilsDir,
|
|
308
|
+
m.council_id ?? "",
|
|
309
|
+
`round-${round}`,
|
|
310
|
+
`${canonicalName}.md`,
|
|
311
|
+
);
|
|
312
|
+
if (existsSync(contributionPath)) continue; // already contributed
|
|
313
|
+
if (m.council_id) pending.push(m.council_id);
|
|
314
|
+
} catch {
|
|
315
|
+
/* skip */
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
/* skip */
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (pending.length === 0) return "";
|
|
323
|
+
const bin = resolveBinName(coordRoot);
|
|
324
|
+
const firstThree = pending.slice(0, 3);
|
|
325
|
+
const tail = pending.length > 3 ? `, +${pending.length - 3} more` : "";
|
|
326
|
+
const list = firstThree.join(", ") + tail;
|
|
327
|
+
if (pending.length === 1) {
|
|
328
|
+
return (
|
|
329
|
+
`Council waiting on your input: \`${pending[0]}\`. ` +
|
|
330
|
+
`Run \`${bin} agents council show ${pending[0]}\` for the brief and ` +
|
|
331
|
+
`\`${bin} agents council contribute ${pending[0]} --message "<your take>"\` to weigh in.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return `Councils waiting on your input (${pending.length} open): ${list}. Run \`${bin} agents council list --mine\` to see all of them, then \`${bin} agents council show <id>\` for any brief.`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Suppress unused import warning when statSync isn't used in this file
|
|
339
|
+
* (kept exported for future renderers, e.g. heartbeat-freshness coloring).
|
|
340
|
+
*/
|
|
341
|
+
export const _ensureStatSyncImported = statSync;
|
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
const FRESHNESS_SECS = 600;
|
|
22
|
+
|
|
23
|
+
export type VerdictResult = {
|
|
24
|
+
allow: boolean;
|
|
25
|
+
exit_code: 0 | 2;
|
|
26
|
+
rule: string;
|
|
27
|
+
reason?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface PeerView {
|
|
31
|
+
instance_id: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
files_touched: string[];
|
|
34
|
+
last_heartbeat: string;
|
|
35
|
+
session_id: string;
|
|
36
|
+
/** parent_instance_id from heartbeat, present for subagent rows. Used by
|
|
37
|
+
* the group-ownership check (sibling subagents + parent share a group
|
|
38
|
+
* and don't block each other's claims).
|
|
39
|
+
*/
|
|
40
|
+
parent_instance_id?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ClaimRequest {
|
|
44
|
+
rule: "claim";
|
|
45
|
+
instance_id: string;
|
|
46
|
+
session_id?: string;
|
|
47
|
+
path: string; // canonical monorepo-relative path
|
|
48
|
+
mode?: "read" | "write"; // default "write"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate a file-claim. Returns deny when another fresh peer claims the
|
|
53
|
+
* path, OR when this owner already holds a lower-sorted claim and the new
|
|
54
|
+
* one would create a circular wait.
|
|
55
|
+
*/
|
|
56
|
+
export function evaluateClaim(coordRoot: string, req: ClaimRequest): VerdictResult {
|
|
57
|
+
const peers = readPeers(coordRoot);
|
|
58
|
+
const myPeer = peers.find((p) => p.instance_id === req.instance_id);
|
|
59
|
+
|
|
60
|
+
// Group-ownership exclusion ("Option B" semantics): the parent
|
|
61
|
+
// session and all subagents under it share a group; claims within the
|
|
62
|
+
// group don't block each other. Compute the group root for the calling
|
|
63
|
+
// instance_id, then exclude any peer in the same group from the conflict
|
|
64
|
+
// scan. session_id from the payload is the parent_owner for subagents.
|
|
65
|
+
const groupRoot = computeGroupRoot(peers, req.instance_id, req.session_id);
|
|
66
|
+
const inMyGroup = (p: PeerView): boolean =>
|
|
67
|
+
p.instance_id === req.instance_id ||
|
|
68
|
+
p.instance_id === groupRoot ||
|
|
69
|
+
p.parent_instance_id === groupRoot ||
|
|
70
|
+
(p.parent_instance_id !== undefined && p.parent_instance_id === req.instance_id);
|
|
71
|
+
|
|
72
|
+
const otherPeers = peers.filter((p) => !inMyGroup(p));
|
|
73
|
+
|
|
74
|
+
// Conflict scan: any other fresh peer claiming this exact path?
|
|
75
|
+
const conflict = otherPeers.find(
|
|
76
|
+
(p) => isFresh(p.last_heartbeat) && p.files_touched.includes(req.path),
|
|
77
|
+
);
|
|
78
|
+
if (conflict) {
|
|
79
|
+
// Self-heal probe: if the file is committed-clean (exists in the repo AND has no uncommitted
|
|
80
|
+
// changes), the peer's claim is stale (worker crashed without releasing) →
|
|
81
|
+
// prune the claim from their heartbeat and allow the edit. Skips when the
|
|
82
|
+
// file doesn't exist (intent-to-create claim); that surface IS load-bearing.
|
|
83
|
+
if (isFileCommittedClean(coordRoot, req.path)) {
|
|
84
|
+
pruneClaimFromPeer(coordRoot, conflict.instance_id, req.path);
|
|
85
|
+
// Fall through to ordering check + allow.
|
|
86
|
+
} else {
|
|
87
|
+
return {
|
|
88
|
+
allow: false,
|
|
89
|
+
exit_code: 2,
|
|
90
|
+
rule: "claim.conflict",
|
|
91
|
+
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).`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Ordering check: if we hold any claim with path < req.path, fine.
|
|
97
|
+
// Otherwise the new claim would create a backward-edge in the dependency
|
|
98
|
+
// graph and risk deadlock. Only applies when there are OTHER fresh peers:
|
|
99
|
+
// single-agent flow can't deadlock with itself, and the rule otherwise
|
|
100
|
+
// forces release-and-reacquire cycles on every reverse-order edit pair.
|
|
101
|
+
const hasFreshPeers = otherPeers.some(
|
|
102
|
+
(p) => isFresh(p.last_heartbeat) && p.files_touched.length > 0,
|
|
103
|
+
);
|
|
104
|
+
if (hasFreshPeers && myPeer && myPeer.files_touched.length > 0) {
|
|
105
|
+
const highest = [...myPeer.files_touched].sort().at(-1)!;
|
|
106
|
+
if (req.path < highest) {
|
|
107
|
+
return {
|
|
108
|
+
allow: false,
|
|
109
|
+
exit_code: 2,
|
|
110
|
+
rule: "claim.ordering_violation",
|
|
111
|
+
reason: `Cannot acquire ${req.path} while holding ${highest} (claim ordering rule: paths must be acquired in sorted order). Release the higher claim first.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Acquire the claim: atomic check-and-set. Adds req.path to my
|
|
117
|
+
// files_touched if not already present.
|
|
118
|
+
if (req.mode !== "read") {
|
|
119
|
+
addClaimToOwner(coordRoot, req.instance_id, req.path);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { allow: true, exit_code: 0, rule: "claim.pass" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add `relPath` to the owner's heartbeat `files_touched` array (idempotent;
|
|
127
|
+
* no-op if already present). Atomic temp + rename. If the owner has no
|
|
128
|
+
* heartbeat yet (subagent that hasn't been initialized), creates a minimal one.
|
|
129
|
+
*/
|
|
130
|
+
function addClaimToOwner(coordRoot: string, instanceId: string, relPath: string): void {
|
|
131
|
+
const activeDir = join(coordRoot, ".harnery", "active");
|
|
132
|
+
if (!existsSync(activeDir)) return;
|
|
133
|
+
const path = join(activeDir, `${instanceId}.json`);
|
|
134
|
+
if (!existsSync(path)) {
|
|
135
|
+
const now = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
136
|
+
const minimal = {
|
|
137
|
+
schema_version: 1,
|
|
138
|
+
instance_id: instanceId,
|
|
139
|
+
session_id: instanceId,
|
|
140
|
+
files_touched: [relPath],
|
|
141
|
+
started_at: now,
|
|
142
|
+
last_heartbeat: now,
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
146
|
+
writeFileSync(tmp, JSON.stringify(minimal, null, 2), "utf8");
|
|
147
|
+
renameSync(tmp, path);
|
|
148
|
+
} catch {
|
|
149
|
+
/* silent */
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const body = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
155
|
+
const files = (body.files_touched as string[] | undefined) ?? [];
|
|
156
|
+
if (files.includes(relPath)) return;
|
|
157
|
+
body.files_touched = [...files, relPath];
|
|
158
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
159
|
+
writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
160
|
+
renameSync(tmp, path);
|
|
161
|
+
} catch {
|
|
162
|
+
/* silent */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readPeers(coordRoot: string): PeerView[] {
|
|
167
|
+
const out: PeerView[] = [];
|
|
168
|
+
const path = join(coordRoot, ".harnery", "active");
|
|
169
|
+
if (!existsSync(path)) return out;
|
|
170
|
+
for (const f of readdirSync(path)) {
|
|
171
|
+
if (!f.endsWith(".json")) continue;
|
|
172
|
+
try {
|
|
173
|
+
const hb = JSON.parse(readFileSync(join(path, f), "utf8")) as {
|
|
174
|
+
instance_id?: string;
|
|
175
|
+
name?: string;
|
|
176
|
+
session_id?: string;
|
|
177
|
+
files_touched?: string[];
|
|
178
|
+
last_heartbeat?: string;
|
|
179
|
+
parent_instance_id?: string;
|
|
180
|
+
parent_session_id?: string;
|
|
181
|
+
};
|
|
182
|
+
if (!hb.instance_id) continue;
|
|
183
|
+
// Derive parent_instance_id: explicit field first, then infer from
|
|
184
|
+
// session_id-differs-from-instance_id (the subagent shape:
|
|
185
|
+
// instance_id is the agent_id, session_id is the parent session).
|
|
186
|
+
const inferredParent =
|
|
187
|
+
hb.parent_instance_id ??
|
|
188
|
+
hb.parent_session_id ??
|
|
189
|
+
(hb.session_id && hb.session_id !== hb.instance_id ? hb.session_id : undefined);
|
|
190
|
+
out.push({
|
|
191
|
+
instance_id: hb.instance_id,
|
|
192
|
+
name: hb.name,
|
|
193
|
+
session_id: hb.session_id ?? hb.instance_id,
|
|
194
|
+
files_touched: hb.files_touched ?? [],
|
|
195
|
+
last_heartbeat: hb.last_heartbeat ?? "",
|
|
196
|
+
parent_instance_id: inferredParent,
|
|
197
|
+
});
|
|
198
|
+
} catch {
|
|
199
|
+
/* skip */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Compute the group root for `instanceId`. For a parent session it's instanceId
|
|
207
|
+
* itself. For a subagent it's the parent_instance_id from the heartbeat
|
|
208
|
+
* (stamped on subagent heartbeats). When session_id is
|
|
209
|
+
* supplied and differs from instanceId (Claude Code's `session_id != agent_id`
|
|
210
|
+
* shape for subagents), that's the group root too.
|
|
211
|
+
*/
|
|
212
|
+
function computeGroupRoot(peers: PeerView[], instanceId: string, sessionId?: string): string {
|
|
213
|
+
// If we have a peer entry, its parent_instance_id is authoritative.
|
|
214
|
+
const myPeer = peers.find((p) => p.instance_id === instanceId);
|
|
215
|
+
if (myPeer?.parent_instance_id) return myPeer.parent_instance_id;
|
|
216
|
+
// Fallback to session_id (parent's instance_id for subagents).
|
|
217
|
+
if (sessionId && sessionId !== instanceId) return sessionId;
|
|
218
|
+
// I am the group root.
|
|
219
|
+
return instanceId;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isFresh(lastHeartbeat: string): boolean {
|
|
223
|
+
if (!lastHeartbeat) return false;
|
|
224
|
+
const ts = Date.parse(lastHeartbeat);
|
|
225
|
+
if (!Number.isFinite(ts)) return false;
|
|
226
|
+
return (Date.now() - ts) / 1000 <= FRESHNESS_SECS;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if a monorepo-relative path is committed-clean: file exists in the
|
|
231
|
+
* repo, is tracked, AND `git diff HEAD -- path` shows no uncommitted
|
|
232
|
+
* modifications. Untracked files are NOT committed-clean: `git diff HEAD`
|
|
233
|
+
* exits 0 on an untracked path (because git ignores it), so we have to
|
|
234
|
+
* positively confirm tracking via `git ls-files` before trusting the diff.
|
|
235
|
+
*
|
|
236
|
+
* Returns false when:
|
|
237
|
+
* - file doesn't exist (intent-to-create, preserve the claim)
|
|
238
|
+
* - file is untracked (peer wrote it, hasn't staged yet, preserve the claim)
|
|
239
|
+
* - any git op fails (treat as dirty, fail-safe)
|
|
240
|
+
* - diff shows non-empty output (genuinely dirty)
|
|
241
|
+
*/
|
|
242
|
+
function isFileCommittedClean(coordRoot: string, relPath: string): boolean {
|
|
243
|
+
const abs = join(coordRoot, relPath);
|
|
244
|
+
if (!existsSync(abs)) return false;
|
|
245
|
+
try {
|
|
246
|
+
const tracked = spawnSync("git", ["ls-files", "--error-unmatch", "--", relPath], {
|
|
247
|
+
cwd: coordRoot,
|
|
248
|
+
encoding: "utf8",
|
|
249
|
+
timeout: 2000,
|
|
250
|
+
});
|
|
251
|
+
if (tracked.status !== 0) return false;
|
|
252
|
+
const result = spawnSync("git", ["diff", "--quiet", "HEAD", "--", relPath], {
|
|
253
|
+
cwd: coordRoot,
|
|
254
|
+
encoding: "utf8",
|
|
255
|
+
timeout: 2000,
|
|
256
|
+
});
|
|
257
|
+
return result.status === 0;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Remove a stale claim from a peer's heartbeat. Atomic temp + rename. Silent
|
|
265
|
+
* on failure.
|
|
266
|
+
*/
|
|
267
|
+
function pruneClaimFromPeer(coordRoot: string, instanceId: string, relPath: string): void {
|
|
268
|
+
const path = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
|
|
269
|
+
if (!existsSync(path)) return;
|
|
270
|
+
try {
|
|
271
|
+
const body = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
272
|
+
const files = (body.files_touched as string[] | undefined) ?? [];
|
|
273
|
+
const next = files.filter((p) => p !== relPath);
|
|
274
|
+
if (next.length === files.length) return;
|
|
275
|
+
body.files_touched = next;
|
|
276
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
277
|
+
writeFileSync(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
278
|
+
renameSync(tmp, path);
|
|
279
|
+
} catch {
|
|
280
|
+
/* silent */
|
|
281
|
+
}
|
|
282
|
+
}
|