harnery 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/bin/agent-coord +42 -0
- package/bin/agent-hook +44 -0
- package/bin/harn +40 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/commander.d.ts +128 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +126 -0
- package/dist/commands/agents.d.ts +18 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +3946 -0
- package/dist/commands/backup.d.ts +22 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +262 -0
- package/dist/commands/browse-ai.d.ts +4 -0
- package/dist/commands/browse-ai.d.ts.map +1 -0
- package/dist/commands/browse-ai.js +156 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.d.ts.map +1 -0
- package/dist/commands/browse.js +590 -0
- package/dist/commands/callers.d.ts +4 -0
- package/dist/commands/callers.d.ts.map +1 -0
- package/dist/commands/callers.js +276 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +158 -0
- package/dist/commands/config-get.d.ts +4 -0
- package/dist/commands/config-get.d.ts.map +1 -0
- package/dist/commands/config-get.js +131 -0
- package/dist/commands/context.d.ts +11 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +185 -0
- package/dist/commands/cookies.d.ts +4 -0
- package/dist/commands/cookies.d.ts.map +1 -0
- package/dist/commands/cookies.js +140 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +137 -0
- package/dist/commands/doctor.d.ts +25 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +200 -0
- package/dist/commands/edit-batch.d.ts +18 -0
- package/dist/commands/edit-batch.d.ts.map +1 -0
- package/dist/commands/edit-batch.js +172 -0
- package/dist/commands/eml.d.ts +4 -0
- package/dist/commands/eml.d.ts.map +1 -0
- package/dist/commands/eml.js +428 -0
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +201 -0
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +99 -0
- package/dist/commands/file-history.d.ts +4 -0
- package/dist/commands/file-history.d.ts.map +1 -0
- package/dist/commands/file-history.js +152 -0
- package/dist/commands/grep.d.ts +4 -0
- package/dist/commands/grep.d.ts.map +1 -0
- package/dist/commands/grep.js +317 -0
- package/dist/commands/init.d.ts +82 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +288 -0
- package/dist/commands/outline.d.ts +4 -0
- package/dist/commands/outline.d.ts.map +1 -0
- package/dist/commands/outline.js +494 -0
- package/dist/commands/presence.d.ts +12 -0
- package/dist/commands/presence.d.ts.map +1 -0
- package/dist/commands/presence.js +123 -0
- package/dist/commands/read.d.ts +7 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +46 -0
- package/dist/commands/scratch.d.ts +4 -0
- package/dist/commands/scratch.d.ts.map +1 -0
- package/dist/commands/scratch.js +426 -0
- package/dist/commands/session.d.ts +4 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +162 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +275 -0
- package/dist/commands/toc.d.ts +5 -0
- package/dist/commands/toc.d.ts.map +1 -0
- package/dist/commands/toc.js +153 -0
- package/dist/commands/tokens.d.ts +4 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +48 -0
- package/dist/commands/tunnel.d.ts +4 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +513 -0
- package/dist/commands/uninstall.d.ts +22 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +126 -0
- package/dist/commands/web.d.ts +4 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +165 -0
- package/dist/core/agents/canonical-emit.d.ts +27 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -0
- package/dist/core/agents/canonical-emit.js +72 -0
- package/dist/core/agents/cli-emit.d.ts +27 -0
- package/dist/core/agents/cli-emit.d.ts.map +1 -0
- package/dist/core/agents/cli-emit.js +57 -0
- package/dist/core/agents/cli.d.ts +10 -0
- package/dist/core/agents/cli.d.ts.map +1 -0
- package/dist/core/agents/cli.js +757 -0
- package/dist/core/agents/codex-replay.d.ts +29 -0
- package/dist/core/agents/codex-replay.d.ts.map +1 -0
- package/dist/core/agents/codex-replay.js +138 -0
- package/dist/core/agents/coord-client.d.ts +98 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -0
- package/dist/core/agents/coord-client.js +212 -0
- package/dist/core/agents/events/consume.d.ts +59 -0
- package/dist/core/agents/events/consume.d.ts.map +1 -0
- package/dist/core/agents/events/consume.js +147 -0
- package/dist/core/agents/events/emit.d.ts +42 -0
- package/dist/core/agents/events/emit.d.ts.map +1 -0
- package/dist/core/agents/events/emit.js +70 -0
- package/dist/core/agents/events/ulid.d.ts +11 -0
- package/dist/core/agents/events/ulid.d.ts.map +1 -0
- package/dist/core/agents/events/ulid.js +47 -0
- package/dist/core/agents/index.d.ts +14 -0
- package/dist/core/agents/index.d.ts.map +1 -0
- package/dist/core/agents/index.js +13 -0
- package/dist/core/agents/paths.d.ts +6 -0
- package/dist/core/agents/paths.d.ts.map +1 -0
- package/dist/core/agents/paths.js +17 -0
- package/dist/core/agents/render/prompt-context.d.ts +43 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
- package/dist/core/agents/render/prompt-context.js +335 -0
- package/dist/core/agents/render/session-context.d.ts +39 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -0
- package/dist/core/agents/render/session-context.js +283 -0
- package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/claim-conflict.js +244 -0
- package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/commit-conflict.js +244 -0
- package/dist/core/agents/rules/stop-hook.d.ts +44 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
- package/dist/core/agents/rules/stop-hook.js +161 -0
- package/dist/core/agents/session-events.d.ts +41 -0
- package/dist/core/agents/session-events.d.ts.map +1 -0
- package/dist/core/agents/session-events.js +205 -0
- package/dist/core/agents/state/activity-log.d.ts +18 -0
- package/dist/core/agents/state/activity-log.d.ts.map +1 -0
- package/dist/core/agents/state/activity-log.js +34 -0
- package/dist/core/agents/state/council.d.ts +39 -0
- package/dist/core/agents/state/council.d.ts.map +1 -0
- package/dist/core/agents/state/council.js +216 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-projector.js +436 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-writer.js +271 -0
- package/dist/core/agents/state/names.d.ts +35 -0
- package/dist/core/agents/state/names.d.ts.map +1 -0
- package/dist/core/agents/state/names.js +376 -0
- package/dist/core/agents/state/pidmap.d.ts +11 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -0
- package/dist/core/agents/state/pidmap.js +32 -0
- package/dist/core/agents/state/scratch.d.ts +27 -0
- package/dist/core/agents/state/scratch.d.ts.map +1 -0
- package/dist/core/agents/state/scratch.js +90 -0
- package/dist/core/agents/state/shell-mutation.d.ts +17 -0
- package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
- package/dist/core/agents/state/shell-mutation.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts +16 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
- package/dist/core/agents/state/stale-sweep.js +166 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +108 -0
- package/dist/core/hooks/cli.d.ts +21 -0
- package/dist/core/hooks/cli.d.ts.map +1 -0
- package/dist/core/hooks/cli.js +1123 -0
- package/dist/core/hooks/effects/image-capture.d.ts +43 -0
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
- package/dist/core/hooks/effects/image-capture.js +288 -0
- package/dist/core/hooks/effects/index.d.ts +64 -0
- package/dist/core/hooks/effects/index.d.ts.map +1 -0
- package/dist/core/hooks/effects/index.js +197 -0
- package/dist/core/hooks/events/emit.d.ts +31 -0
- package/dist/core/hooks/events/emit.d.ts.map +1 -0
- package/dist/core/hooks/events/emit.js +89 -0
- package/dist/core/hooks/events/schema.d.ts +235 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -0
- package/dist/core/hooks/events/schema.js +12 -0
- package/dist/core/hooks/events/ulid.d.ts +10 -0
- package/dist/core/hooks/events/ulid.d.ts.map +1 -0
- package/dist/core/hooks/events/ulid.js +47 -0
- package/dist/core/hooks/harness/detect.d.ts +9 -0
- package/dist/core/hooks/harness/detect.d.ts.map +1 -0
- package/dist/core/hooks/harness/detect.js +29 -0
- package/dist/core/hooks/harness/events.d.ts +45 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -0
- package/dist/core/hooks/harness/events.js +71 -0
- package/dist/core/hooks/harness/output.d.ts +46 -0
- package/dist/core/hooks/harness/output.d.ts.map +1 -0
- package/dist/core/hooks/harness/output.js +87 -0
- package/dist/core/hooks/harness/parse.d.ts +67 -0
- package/dist/core/hooks/harness/parse.d.ts.map +1 -0
- package/dist/core/hooks/harness/parse.js +132 -0
- package/dist/core/hooks/index.d.ts +8 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +7 -0
- package/dist/core/hooks/resolve/anchor.d.ts +37 -0
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
- package/dist/core/hooks/resolve/anchor.js +48 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
- package/dist/core/hooks/resolve/coord-root.js +27 -0
- package/dist/core/hooks/resolve/intent.d.ts +33 -0
- package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
- package/dist/core/hooks/resolve/intent.js +79 -0
- package/dist/core/hooks/resolve/owner.d.ts +42 -0
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
- package/dist/core/hooks/resolve/owner.js +140 -0
- package/dist/core/hooks/resolve/transcript.d.ts +26 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
- package/dist/core/hooks/resolve/transcript.js +73 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/lib/agent-browser/client.d.ts +99 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -0
- package/dist/lib/agent-browser/client.js +177 -0
- package/dist/lib/agent-browser/index.d.ts +2 -0
- package/dist/lib/agent-browser/index.d.ts.map +1 -0
- package/dist/lib/agent-browser/index.js +1 -0
- package/dist/lib/browser/client.d.ts +193 -0
- package/dist/lib/browser/client.d.ts.map +1 -0
- package/dist/lib/browser/client.js +325 -0
- package/dist/lib/browser/dev-overlay.d.ts +23 -0
- package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
- package/dist/lib/browser/dev-overlay.js +153 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.d.ts.map +1 -0
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/layout.d.ts +79 -0
- package/dist/lib/browser/layout.d.ts.map +1 -0
- package/dist/lib/browser/layout.js +220 -0
- package/dist/lib/browser/visibility.d.ts +86 -0
- package/dist/lib/browser/visibility.d.ts.map +1 -0
- package/dist/lib/browser/visibility.js +333 -0
- package/dist/lib/browser/visual-diff.d.ts +38 -0
- package/dist/lib/browser/visual-diff.d.ts.map +1 -0
- package/dist/lib/browser/visual-diff.js +107 -0
- package/dist/lib/completion/bash.d.ts +25 -0
- package/dist/lib/completion/bash.d.ts.map +1 -0
- package/dist/lib/completion/bash.js +284 -0
- package/dist/lib/completion/fish.d.ts +16 -0
- package/dist/lib/completion/fish.d.ts.map +1 -0
- package/dist/lib/completion/fish.js +118 -0
- package/dist/lib/completion/index.d.ts +5 -0
- package/dist/lib/completion/index.d.ts.map +1 -0
- package/dist/lib/completion/index.js +4 -0
- package/dist/lib/completion/walk.d.ts +68 -0
- package/dist/lib/completion/walk.d.ts.map +1 -0
- package/dist/lib/completion/walk.js +102 -0
- package/dist/lib/completion/zsh.d.ts +13 -0
- package/dist/lib/completion/zsh.d.ts.map +1 -0
- package/dist/lib/completion/zsh.js +249 -0
- package/dist/lib/context/index.d.ts +107 -0
- package/dist/lib/context/index.d.ts.map +1 -0
- package/dist/lib/context/index.js +275 -0
- package/dist/lib/cookies/client.d.ts +131 -0
- package/dist/lib/cookies/client.d.ts.map +1 -0
- package/dist/lib/cookies/client.js +239 -0
- package/dist/lib/cookies/index.d.ts +2 -0
- package/dist/lib/cookies/index.d.ts.map +1 -0
- package/dist/lib/cookies/index.js +1 -0
- package/dist/lib/council/index.d.ts +266 -0
- package/dist/lib/council/index.d.ts.map +1 -0
- package/dist/lib/council/index.js +674 -0
- package/dist/lib/docs-index.d.ts +28 -0
- package/dist/lib/docs-index.d.ts.map +1 -0
- package/dist/lib/docs-index.js +169 -0
- package/dist/lib/docs-lint.d.ts +26 -0
- package/dist/lib/docs-lint.d.ts.map +1 -0
- package/dist/lib/docs-lint.js +378 -0
- package/dist/lib/docs-sweep.d.ts +34 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -0
- package/dist/lib/docs-sweep.js +304 -0
- package/dist/lib/docs.d.ts +27 -0
- package/dist/lib/docs.d.ts.map +1 -0
- package/dist/lib/docs.js +142 -0
- package/dist/lib/env.d.ts +11 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +12 -0
- package/dist/lib/exec.d.ts +32 -0
- package/dist/lib/exec.d.ts.map +1 -0
- package/dist/lib/exec.js +54 -0
- package/dist/lib/format.d.ts +29 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +139 -0
- package/dist/lib/http/client.d.ts +56 -0
- package/dist/lib/http/client.d.ts.map +1 -0
- package/dist/lib/http/client.js +160 -0
- package/dist/lib/http/index.d.ts +2 -0
- package/dist/lib/http/index.d.ts.map +1 -0
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/identities/index.d.ts +77 -0
- package/dist/lib/identities/index.d.ts.map +1 -0
- package/dist/lib/identities/index.js +190 -0
- package/dist/lib/machine.d.ts +19 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/machine.js +61 -0
- package/dist/lib/presence.d.ts +48 -0
- package/dist/lib/presence.d.ts.map +1 -0
- package/dist/lib/presence.js +123 -0
- package/dist/lib/readability/client.d.ts +32 -0
- package/dist/lib/readability/client.d.ts.map +1 -0
- package/dist/lib/readability/client.js +119 -0
- package/dist/lib/readability/index.d.ts +2 -0
- package/dist/lib/readability/index.d.ts.map +1 -0
- package/dist/lib/readability/index.js +1 -0
- package/dist/lib/scratch/index.d.ts +74 -0
- package/dist/lib/scratch/index.d.ts.map +1 -0
- package/dist/lib/scratch/index.js +393 -0
- package/dist/lib/tunnel/gate.d.ts +12 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -0
- package/dist/lib/tunnel/gate.js +101 -0
- package/dist/lib/tunnel/state.d.ts +34 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -0
- package/dist/lib/tunnel/state.js +132 -0
- package/package.json +160 -8
- package/schemas/.gitkeep +0 -0
- package/schemas/config.schema.json +109 -0
- package/src/cli.ts +22 -0
- package/src/commander.ts +242 -0
- package/src/commands/.gitkeep +0 -0
- package/src/commands/agents.ts +4567 -0
- package/src/commands/backup.ts +305 -0
- package/src/commands/browse-ai.ts +198 -0
- package/src/commands/browse.ts +849 -0
- package/src/commands/callers.ts +363 -0
- package/src/commands/completion.ts +193 -0
- package/src/commands/config-get.ts +161 -0
- package/src/commands/context.ts +209 -0
- package/src/commands/cookies.ts +198 -0
- package/src/commands/docs.ts +174 -0
- package/src/commands/doctor.ts +231 -0
- package/src/commands/edit-batch.ts +233 -0
- package/src/commands/eml.ts +519 -0
- package/src/commands/env.ts +254 -0
- package/src/commands/fetch.ts +136 -0
- package/src/commands/file-history.ts +202 -0
- package/src/commands/grep.ts +371 -0
- package/src/commands/init.ts +335 -0
- package/src/commands/outline.ts +564 -0
- package/src/commands/presence.ts +152 -0
- package/src/commands/read.ts +64 -0
- package/src/commands/scratch.ts +445 -0
- package/src/commands/session.ts +187 -0
- package/src/commands/sync.ts +306 -0
- package/src/commands/toc.ts +218 -0
- package/src/commands/tokens.ts +79 -0
- package/src/commands/tunnel.ts +633 -0
- package/src/commands/uninstall.ts +144 -0
- package/src/commands/web.ts +193 -0
- package/src/core/agents/canonical-emit.ts +77 -0
- package/src/core/agents/cli-emit.ts +64 -0
- package/src/core/agents/cli.ts +838 -0
- package/src/core/agents/codex-replay.ts +163 -0
- package/src/core/agents/coord-client.ts +249 -0
- package/src/core/agents/events/consume.ts +196 -0
- package/src/core/agents/events/emit.ts +108 -0
- package/src/core/agents/events/ulid.ts +51 -0
- package/src/core/agents/index.ts +14 -0
- package/src/core/agents/paths.ts +16 -0
- package/src/core/agents/render/prompt-context.ts +401 -0
- package/src/core/agents/render/session-context.ts +341 -0
- package/src/core/agents/rules/claim-conflict.ts +282 -0
- package/src/core/agents/rules/commit-conflict.ts +303 -0
- package/src/core/agents/rules/stop-hook.ts +229 -0
- package/src/core/agents/session-events.ts +228 -0
- package/src/core/agents/state/activity-log.ts +33 -0
- package/src/core/agents/state/council.ts +265 -0
- package/src/core/agents/state/heartbeat-projector.ts +488 -0
- package/src/core/agents/state/heartbeat-writer.ts +333 -0
- package/src/core/agents/state/names.ts +399 -0
- package/src/core/agents/state/pidmap.ts +38 -0
- package/src/core/agents/state/scratch.ts +121 -0
- package/src/core/agents/state/shell-mutation.ts +44 -0
- package/src/core/agents/state/stale-sweep.ts +190 -0
- package/src/core/config.ts +111 -0
- package/src/core/hooks/cli.ts +1247 -0
- package/src/core/hooks/effects/image-capture.ts +330 -0
- package/src/core/hooks/effects/index.ts +210 -0
- package/src/core/hooks/events/emit.ts +120 -0
- package/src/core/hooks/events/schema.ts +430 -0
- package/src/core/hooks/events/ulid.ts +51 -0
- package/src/core/hooks/harness/detect.ts +30 -0
- package/src/core/hooks/harness/events.ts +102 -0
- package/src/core/hooks/harness/output.ts +100 -0
- package/src/core/hooks/harness/parse.ts +180 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/resolve/anchor.ts +51 -0
- package/src/core/hooks/resolve/coord-root.ts +25 -0
- package/src/core/hooks/resolve/intent.ts +89 -0
- package/src/core/hooks/resolve/owner.ts +140 -0
- package/src/core/hooks/resolve/transcript.ts +72 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/index.ts +15 -0
- package/src/lib/agent-browser/client.ts +239 -0
- package/src/lib/agent-browser/index.ts +1 -0
- package/src/lib/browser/client.ts +449 -0
- package/src/lib/browser/dev-overlay.ts +207 -0
- package/src/lib/browser/index.ts +24 -0
- package/src/lib/browser/layout.ts +288 -0
- package/src/lib/browser/visibility.ts +419 -0
- package/src/lib/browser/visual-diff.ts +150 -0
- package/src/lib/completion/bash.ts +291 -0
- package/src/lib/completion/fish.ts +134 -0
- package/src/lib/completion/index.ts +10 -0
- package/src/lib/completion/walk.ts +184 -0
- package/src/lib/completion/zsh.ts +262 -0
- package/src/lib/context/index.ts +386 -0
- package/src/lib/cookies/client.ts +301 -0
- package/src/lib/cookies/index.ts +13 -0
- package/src/lib/council/index.ts +803 -0
- package/src/lib/docs-index.ts +216 -0
- package/src/lib/docs-lint.ts +413 -0
- package/src/lib/docs-sweep.ts +348 -0
- package/src/lib/docs.ts +199 -0
- package/src/lib/env.ts +12 -0
- package/src/lib/exec.ts +74 -0
- package/src/lib/format.ts +147 -0
- package/src/lib/http/client.ts +211 -0
- package/src/lib/http/index.ts +1 -0
- package/src/lib/identities/index.ts +210 -0
- package/src/lib/machine.ts +61 -0
- package/src/lib/presence.ts +154 -0
- package/src/lib/readability/client.ts +156 -0
- package/src/lib/readability/index.ts +5 -0
- package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
- package/src/lib/scratch/index.ts +470 -0
- package/src/lib/tunnel/gate.ts +113 -0
- package/src/lib/tunnel/state.ts +167 -0
- package/src/web/.gitkeep +0 -0
- package/index.js +0 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { existsSync as __existsSyncForDocs } from "node:fs";
|
|
2
|
+
import { resolve as __resolveForDocs } from "node:path";
|
|
3
|
+
|
|
4
|
+
// Module-level docs context, initialized by initDocsContext() before any
|
|
5
|
+
// other function in this file is called. The repo root + submodule list
|
|
6
|
+
// are passed via the context provided to registerDocsCommand.
|
|
7
|
+
let REPO_ROOT = "";
|
|
8
|
+
let SUBMODULES: readonly string[] = [];
|
|
9
|
+
|
|
10
|
+
export function initDocsContext(opts: { repoRoot: string; submodules: readonly string[] }): void {
|
|
11
|
+
REPO_ROOT = opts.repoRoot;
|
|
12
|
+
SUBMODULES = opts.submodules;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function submodulePath(name: string): string {
|
|
16
|
+
return __resolveForDocs(REPO_ROOT, name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isSubmoduleInitialized(name: string): boolean {
|
|
20
|
+
return __existsSyncForDocs(__resolveForDocs(REPO_ROOT, name, ".git"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { join, relative } from "node:path";
|
|
25
|
+
import { resolveBinName } from "../core/config.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Regenerates index READMEs in docs/audits/ and docs/issues/ directories.
|
|
29
|
+
* Idempotent: safe to run on every commit. Reads the first ~30 lines of
|
|
30
|
+
* each dated file to extract title and (for issues) status.
|
|
31
|
+
*
|
|
32
|
+
* Preserves hand-written preambles. The command updates only the section
|
|
33
|
+
* between `<!-- BEGIN INDEX -->` and `<!-- END INDEX -->` markers. If a
|
|
34
|
+
* README exists without markers, the command refuses to overwrite it and
|
|
35
|
+
* reports it as `needs-markers` so the human can insert them explicitly.
|
|
36
|
+
* New READMEs (no file yet) are created with a minimal default preamble.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export interface IndexOpts {
|
|
40
|
+
dryRun?: boolean;
|
|
41
|
+
repo?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type IndexStatus = "unchanged" | "updated" | "needs-markers" | "created";
|
|
45
|
+
|
|
46
|
+
export interface IndexResult {
|
|
47
|
+
path: string; // the README being regenerated
|
|
48
|
+
status: IndexStatus;
|
|
49
|
+
before: string;
|
|
50
|
+
after: string; // what would be written (identical to before if status=unchanged or needs-markers)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface DatedEntry {
|
|
54
|
+
file: string; // e.g. "2026-04-11_merge-dup-key.md"
|
|
55
|
+
date: string; // YYYY-MM-DD
|
|
56
|
+
title: string; // first H1 or filename-derived
|
|
57
|
+
status?: string; // for issues
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DATED_FILE_PATTERN = /^(\d{4}-\d{2}-\d{2})_([a-z0-9][a-z0-9_-]*)\.md$/i;
|
|
61
|
+
|
|
62
|
+
function extractTitle(content: string, fallbackSlug: string): string {
|
|
63
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
64
|
+
if (h1Match) return h1Match[1]!.trim();
|
|
65
|
+
// Fallback: slug → Title Case
|
|
66
|
+
return fallbackSlug.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractStatus(content: string): string | undefined {
|
|
70
|
+
const head = content.split("\n").slice(0, 20).join("\n");
|
|
71
|
+
const match = head.match(/\*\*Status:\*\*\s*([a-zA-Z][a-zA-Z-]*)/);
|
|
72
|
+
return match ? match[1]!.toLowerCase() : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readEntries(dir: string, includeStatus: boolean): DatedEntry[] {
|
|
76
|
+
if (!existsSync(dir)) return [];
|
|
77
|
+
const entries: DatedEntry[] = [];
|
|
78
|
+
for (const f of readdirSync(dir)) {
|
|
79
|
+
if (f === "README.md") continue;
|
|
80
|
+
const m = f.match(DATED_FILE_PATTERN);
|
|
81
|
+
if (!m) continue;
|
|
82
|
+
const [, date, slug] = m;
|
|
83
|
+
let content = "";
|
|
84
|
+
try {
|
|
85
|
+
content = readFileSync(join(dir, f), "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
entries.push({
|
|
90
|
+
file: f,
|
|
91
|
+
date: date!,
|
|
92
|
+
title: extractTitle(content, slug!),
|
|
93
|
+
status: includeStatus ? extractStatus(content) : undefined,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Newest first
|
|
97
|
+
entries.sort((a, b) => b.date.localeCompare(a.date));
|
|
98
|
+
return entries;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderAuditsTable(entries: DatedEntry[]): string {
|
|
102
|
+
if (entries.length === 0) return "_No audits recorded._\n";
|
|
103
|
+
const lines = ["| Date | File | Description |", "|------|------|-------------|"];
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
lines.push(`| ${e.date} | [${e.file}](${e.file}) | ${e.title} |`);
|
|
106
|
+
}
|
|
107
|
+
return `${lines.join("\n")}\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderIssuesTable(entries: DatedEntry[]): string {
|
|
111
|
+
if (entries.length === 0) return "_No issues recorded._\n";
|
|
112
|
+
const lines = [
|
|
113
|
+
"| Date | File | Status | Description |",
|
|
114
|
+
"|------|------|--------|-------------|",
|
|
115
|
+
];
|
|
116
|
+
for (const e of entries) {
|
|
117
|
+
const status = e.status ?? "unknown";
|
|
118
|
+
lines.push(`| ${e.date} | [${e.file}](${e.file}) | ${status} | ${e.title} |`);
|
|
119
|
+
}
|
|
120
|
+
return `${lines.join("\n")}\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const BEGIN_MARKER = "<!-- BEGIN INDEX -->";
|
|
124
|
+
const END_MARKER = "<!-- END INDEX -->";
|
|
125
|
+
|
|
126
|
+
function defaultPreamble(kind: "audits" | "issues"): string {
|
|
127
|
+
const regen = `The table below is regenerated by \`${resolveBinName()} docs index\`; do not hand-edit it. Add prose above the \`BEGIN INDEX\` marker instead.`;
|
|
128
|
+
return kind === "audits"
|
|
129
|
+
? [
|
|
130
|
+
"# Audit Documents",
|
|
131
|
+
"",
|
|
132
|
+
"Immutable, date-stamped reports. File names follow `YYYY-MM-DD_<slug>.md`.",
|
|
133
|
+
"",
|
|
134
|
+
regen,
|
|
135
|
+
"",
|
|
136
|
+
].join("\n")
|
|
137
|
+
: [
|
|
138
|
+
"# Issues",
|
|
139
|
+
"",
|
|
140
|
+
"Date-stamped post-mortems and investigations. File names follow `YYYY-MM-DD_<slug>.md`.",
|
|
141
|
+
"Each file carries a `**Status:**` line (open | resolved | wontfix).",
|
|
142
|
+
"",
|
|
143
|
+
regen,
|
|
144
|
+
"",
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Replace ONLY the section between the markers. Never touch content outside them. */
|
|
149
|
+
function spliceBetweenMarkers(existing: string, body: string): string {
|
|
150
|
+
const beginIdx = existing.indexOf(BEGIN_MARKER);
|
|
151
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
152
|
+
if (beginIdx < 0 || endIdx < 0 || endIdx < beginIdx) {
|
|
153
|
+
// Caller should have checked hasMarkers() first; return unchanged as a safe fallback.
|
|
154
|
+
return existing;
|
|
155
|
+
}
|
|
156
|
+
const before = existing.slice(0, beginIdx + BEGIN_MARKER.length);
|
|
157
|
+
const after = existing.slice(endIdx);
|
|
158
|
+
return `${before}\n${body}${after}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function hasMarkers(existing: string): boolean {
|
|
162
|
+
return existing.includes(BEGIN_MARKER) && existing.includes(END_MARKER);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildNewReadme(kind: "audits" | "issues", body: string): string {
|
|
166
|
+
return `${defaultPreamble(kind)}\n${BEGIN_MARKER}\n${body}${END_MARKER}\n`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function runIndex(opts: IndexOpts): Promise<IndexResult[]> {
|
|
170
|
+
const targets: { name: string; path: string }[] = [{ name: "(root)", path: REPO_ROOT }];
|
|
171
|
+
for (const name of SUBMODULES) {
|
|
172
|
+
if (!isSubmoduleInitialized(name)) continue;
|
|
173
|
+
targets.push({ name, path: submodulePath(name) });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const filter = opts.repo === "." ? "(root)" : opts.repo;
|
|
177
|
+
const filtered = filter ? targets.filter((t) => t.name === filter) : targets;
|
|
178
|
+
|
|
179
|
+
const results: IndexResult[] = [];
|
|
180
|
+
for (const { name: _name, path } of filtered) {
|
|
181
|
+
for (const kind of ["audits", "issues"] as const) {
|
|
182
|
+
const dir = join(path, "docs", kind);
|
|
183
|
+
if (!existsSync(dir)) continue;
|
|
184
|
+
|
|
185
|
+
const entries = readEntries(dir, kind === "issues");
|
|
186
|
+
const body = kind === "audits" ? renderAuditsTable(entries) : renderIssuesTable(entries);
|
|
187
|
+
|
|
188
|
+
const readmePath = join(dir, "README.md");
|
|
189
|
+
const exists = existsSync(readmePath);
|
|
190
|
+
const before = exists ? readFileSync(readmePath, "utf8") : "";
|
|
191
|
+
const displayPath = relative(REPO_ROOT, readmePath);
|
|
192
|
+
|
|
193
|
+
let status: IndexStatus;
|
|
194
|
+
let after: string;
|
|
195
|
+
if (!exists) {
|
|
196
|
+
after = buildNewReadme(kind, body);
|
|
197
|
+
status = "created";
|
|
198
|
+
} else if (!hasMarkers(before)) {
|
|
199
|
+
// Hands off: the human-written README has no markers. Don't touch it.
|
|
200
|
+
after = before;
|
|
201
|
+
status = "needs-markers";
|
|
202
|
+
} else {
|
|
203
|
+
after = spliceBetweenMarkers(before, body);
|
|
204
|
+
status = before === after ? "unchanged" : "updated";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
results.push({ path: displayPath, status, before, after });
|
|
208
|
+
|
|
209
|
+
if (!opts.dryRun && (status === "updated" || status === "created")) {
|
|
210
|
+
writeFileSync(readmePath, after, "utf8");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return results;
|
|
216
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { existsSync as __existsSyncForDocs } from "node:fs";
|
|
2
|
+
import { resolve as __resolveForDocs } from "node:path";
|
|
3
|
+
import { sh } from "./exec.ts";
|
|
4
|
+
|
|
5
|
+
// Module-level docs context, initialized by initDocsContext() before any
|
|
6
|
+
// other function in this file is called. Consumers pass repo metadata
|
|
7
|
+
// + an optional list of extra excluded path prefixes for project-specific
|
|
8
|
+
// directories that shouldn't be subject to doc-lint conventions.
|
|
9
|
+
let REPO_ROOT = "";
|
|
10
|
+
let SUBMODULES: readonly string[] = [];
|
|
11
|
+
let EXTRA_EXCLUDED_PREFIXES: readonly string[] = [];
|
|
12
|
+
|
|
13
|
+
export function initDocsContext(opts: {
|
|
14
|
+
repoRoot: string;
|
|
15
|
+
submodules: readonly string[];
|
|
16
|
+
extraExcludedPrefixes?: readonly string[];
|
|
17
|
+
}): void {
|
|
18
|
+
REPO_ROOT = opts.repoRoot;
|
|
19
|
+
SUBMODULES = opts.submodules;
|
|
20
|
+
EXTRA_EXCLUDED_PREFIXES = opts.extraExcludedPrefixes ?? [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function submodulePath(name: string): string {
|
|
24
|
+
return __resolveForDocs(REPO_ROOT, name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isSubmoduleInitialized(name: string): boolean {
|
|
28
|
+
return __existsSyncForDocs(__resolveForDocs(REPO_ROOT, name, ".git"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
32
|
+
import { basename, join } from "node:path";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Documentation linter. Enforces the docs directory-layout + naming contract.
|
|
36
|
+
*
|
|
37
|
+
* Each violation carries a severity: `error` fails the lint, `warning` is
|
|
38
|
+
* informational. `--fast` mode skips content-reading checks so the pre-commit
|
|
39
|
+
* hook stays cheap.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export type Severity = "error" | "warning";
|
|
43
|
+
|
|
44
|
+
export interface Violation {
|
|
45
|
+
severity: Severity;
|
|
46
|
+
repo: string;
|
|
47
|
+
path: string; // relative to monorepo root
|
|
48
|
+
rule: string;
|
|
49
|
+
message: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface LintOpts {
|
|
53
|
+
fast?: boolean;
|
|
54
|
+
repo?: string; // limit to one submodule or "." for parent
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Files allowed at a submodule root level. Includes:
|
|
58
|
+
* - In-repo conventions: README.md, CLAUDE.md, LLM-BRIEFING.md, AGENTS.md
|
|
59
|
+
* - GitHub OSS conventions: CHANGELOG.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md,
|
|
60
|
+
* SECURITY.md, SUPPORT.md, AUTHORS.md, MAINTAINERS.md, PULL_REQUEST_TEMPLATE.md
|
|
61
|
+
* (these are recognized by the GitHub UI, and renaming them breaks the integration)
|
|
62
|
+
*/
|
|
63
|
+
const ROOT_FILE_ALLOWLIST = new Set([
|
|
64
|
+
"README.md",
|
|
65
|
+
"CLAUDE.md",
|
|
66
|
+
"LLM-BRIEFING.md",
|
|
67
|
+
"AGENTS.md",
|
|
68
|
+
// GitHub-recognized OSS package files
|
|
69
|
+
"CHANGELOG.md",
|
|
70
|
+
"CONTRIBUTING.md",
|
|
71
|
+
"CODE_OF_CONDUCT.md",
|
|
72
|
+
"SECURITY.md",
|
|
73
|
+
"SUPPORT.md",
|
|
74
|
+
"AUTHORS.md",
|
|
75
|
+
"MAINTAINERS.md",
|
|
76
|
+
"PULL_REQUEST_TEMPLATE.md",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/** Paths that are excluded from markdown discipline even when git-tracked.
|
|
80
|
+
*
|
|
81
|
+
* Covers auto-generated reference dumps and vendored content pages that happen
|
|
82
|
+
* to be checked into git but aren't subject to doc conventions. Paths are
|
|
83
|
+
* relative to the scanned repo root; any file under one of these prefixes is
|
|
84
|
+
* ignored.
|
|
85
|
+
*
|
|
86
|
+
* Note: git-ignored directories (node_modules, .venv, vendor, dbt_packages,
|
|
87
|
+
* target, dist, build) are already excluded because we use `git ls-files`.
|
|
88
|
+
*/
|
|
89
|
+
// Built-in exclusions: auto-generated/framework dirs that ship in any
|
|
90
|
+
// project. Consumers extend this via `extraExcludedPrefixes` in
|
|
91
|
+
// initDocsContext for their own project-specific dirs (auto-generated
|
|
92
|
+
// API references, vendored content, etc.).
|
|
93
|
+
const EXCLUDED_PREFIXES = [
|
|
94
|
+
".agents/", // canonical AI-config sources (skills, subagents, rules, claude-addendum)
|
|
95
|
+
".claude/", // Claude Code framework files (SKILL.md, agents)
|
|
96
|
+
".harnery/", // harnery coord/skill state
|
|
97
|
+
".codex/", // OpenAI Codex framework files (skills/, agents/)
|
|
98
|
+
".cursor/", // auto-generated Cursor rules
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/** Filename patterns that should never exist */
|
|
102
|
+
const FORBIDDEN_ROOT_NAMES = new Set([
|
|
103
|
+
"TODO.md",
|
|
104
|
+
"PROJECT.md",
|
|
105
|
+
"VISION.md",
|
|
106
|
+
"NOTES.md",
|
|
107
|
+
"DECISIONS.md", // should be docs/decisions.md
|
|
108
|
+
"CHANGELOG.md", // should be docs/changelogs/YYYY-MM.md
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
/** YYYY-MM-DD_<slug>.md */
|
|
112
|
+
const DATED_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}_[a-z0-9][a-z0-9_-]*\.md$/i;
|
|
113
|
+
|
|
114
|
+
/** YYYY-MM.md for changelogs */
|
|
115
|
+
const CHANGELOG_PATTERN = /^\d{4}-\d{2}\.md$/;
|
|
116
|
+
|
|
117
|
+
/** SCREAMING_SNAKE_CASE.md, excluding allowlisted entry files */
|
|
118
|
+
const SCREAMING_SNAKE_PATTERN = /^[A-Z][A-Z0-9_]+\.md$/;
|
|
119
|
+
|
|
120
|
+
/** kebab-case.md: lowercase letters, digits, hyphens */
|
|
121
|
+
const KEBAB_CASE_PATTERN = /^[a-z0-9][a-z0-9-]*\.md$/;
|
|
122
|
+
|
|
123
|
+
/** Target repos to lint: parent + every initialized submodule */
|
|
124
|
+
function getTargetRepos(opts: LintOpts): { name: string; path: string; isSubmodule: boolean }[] {
|
|
125
|
+
const all: { name: string; path: string; isSubmodule: boolean }[] = [
|
|
126
|
+
{ name: "(root)", path: REPO_ROOT, isSubmodule: false },
|
|
127
|
+
];
|
|
128
|
+
for (const name of SUBMODULES) {
|
|
129
|
+
if (!isSubmoduleInitialized(name)) continue;
|
|
130
|
+
all.push({ name, path: submodulePath(name), isSubmodule: true });
|
|
131
|
+
}
|
|
132
|
+
if (opts.repo) {
|
|
133
|
+
const filter = opts.repo === "." ? "(root)" : opts.repo;
|
|
134
|
+
return all.filter((r) => r.name === filter);
|
|
135
|
+
}
|
|
136
|
+
return all;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** List all tracked .md files in the given repo via `git ls-files`.
|
|
140
|
+
* Returns paths relative to the repo root. Automatically skips node_modules,
|
|
141
|
+
* vendor, .venv, dbt_packages, build/ etc. because they're gitignored.
|
|
142
|
+
*/
|
|
143
|
+
async function findMarkdownFiles(root: string): Promise<string[]> {
|
|
144
|
+
const result = await sh('git ls-files --cached "**/*.md" "*.md"', { cwd: root });
|
|
145
|
+
if (result.exitCode !== 0 || !result.stdout) return [];
|
|
146
|
+
return result.stdout
|
|
147
|
+
.split("\n")
|
|
148
|
+
.filter((f) => f.endsWith(".md"))
|
|
149
|
+
.filter(
|
|
150
|
+
(f) =>
|
|
151
|
+
// Framework dirs are excluded at any depth, not just repo root;
|
|
152
|
+
// in-tree repos (monorepos like harnery) nest .claude/.agents/etc.
|
|
153
|
+
!EXCLUDED_PREFIXES.some((p) => f.startsWith(p) || f.includes(`/${p}`)) &&
|
|
154
|
+
!EXTRA_EXCLUDED_PREFIXES.some((p) => f.startsWith(p) || f.includes(`/${p}`)),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Read first N lines of a file for header inspection */
|
|
159
|
+
function readHead(path: string, lines = 20): string {
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(path, "utf8");
|
|
162
|
+
return content.split("\n").slice(0, lines).join("\n");
|
|
163
|
+
} catch {
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Detect whether a file declares itself an intentional monolith */
|
|
169
|
+
function isDeclaredMonolith(path: string): boolean {
|
|
170
|
+
const head = readHead(path, 10);
|
|
171
|
+
return /INTENTIONAL-MONOLITH/i.test(head);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Detect whether a file carries a Status line in its opening block */
|
|
175
|
+
function hasStatusHeader(path: string): boolean {
|
|
176
|
+
const head = readHead(path, 15);
|
|
177
|
+
return /\*\*Status:\*\*/i.test(head);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Individual checks ---
|
|
181
|
+
|
|
182
|
+
/** Entry tier files exist at the repo root */
|
|
183
|
+
function checkEntryTier(repoName: string, repoPath: string, isSubmodule: boolean): Violation[] {
|
|
184
|
+
const violations: Violation[] = [];
|
|
185
|
+
// README.md is required for any repo
|
|
186
|
+
if (!existsSync(join(repoPath, "README.md"))) {
|
|
187
|
+
violations.push({
|
|
188
|
+
severity: "error",
|
|
189
|
+
repo: repoName,
|
|
190
|
+
path: join(repoName === "(root)" ? "" : repoName, "README.md"),
|
|
191
|
+
rule: "entry-tier",
|
|
192
|
+
message: "README.md missing at repo root",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// CLAUDE.md required for submodules (primary LLM context)
|
|
196
|
+
if (isSubmodule && !existsSync(join(repoPath, "CLAUDE.md"))) {
|
|
197
|
+
violations.push({
|
|
198
|
+
severity: "error",
|
|
199
|
+
repo: repoName,
|
|
200
|
+
path: join(repoName, "CLAUDE.md"),
|
|
201
|
+
rule: "entry-tier",
|
|
202
|
+
message: "CLAUDE.md missing: primary LLM context file",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return violations;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** No forbidden root-level files */
|
|
209
|
+
function checkRootAllowlist(repoName: string, repoPath: string): Violation[] {
|
|
210
|
+
const violations: Violation[] = [];
|
|
211
|
+
let entries: string[];
|
|
212
|
+
try {
|
|
213
|
+
entries = readdirSync(repoPath);
|
|
214
|
+
} catch {
|
|
215
|
+
return violations;
|
|
216
|
+
}
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
if (!entry.endsWith(".md")) continue;
|
|
219
|
+
if (ROOT_FILE_ALLOWLIST.has(entry)) continue;
|
|
220
|
+
const displayPath = join(repoName === "(root)" ? "" : repoName, entry);
|
|
221
|
+
if (FORBIDDEN_ROOT_NAMES.has(entry)) {
|
|
222
|
+
violations.push({
|
|
223
|
+
severity: "error",
|
|
224
|
+
repo: repoName,
|
|
225
|
+
path: displayPath,
|
|
226
|
+
rule: "forbidden-root-file",
|
|
227
|
+
message: `${entry} is not allowed at repo root`,
|
|
228
|
+
});
|
|
229
|
+
} else if (SCREAMING_SNAKE_PATTERN.test(entry)) {
|
|
230
|
+
violations.push({
|
|
231
|
+
severity: "error",
|
|
232
|
+
repo: repoName,
|
|
233
|
+
path: displayPath,
|
|
234
|
+
rule: "root-caps-file",
|
|
235
|
+
message: `${entry} is an ad-hoc caps file at repo root: entry tier is reserved for README.md / CLAUDE.md / LLM-BRIEFING.md / AGENTS.md`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return violations;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** No SCREAMING_SNAKE_CASE filenames anywhere */
|
|
243
|
+
function checkNamingConvention(repoName: string, _repoPath: string, files: string[]): Violation[] {
|
|
244
|
+
const violations: Violation[] = [];
|
|
245
|
+
for (const rel of files) {
|
|
246
|
+
const name = basename(rel);
|
|
247
|
+
// Allowlisted names
|
|
248
|
+
if (ROOT_FILE_ALLOWLIST.has(name)) continue;
|
|
249
|
+
// Dated files (audits/issues)
|
|
250
|
+
if (DATED_FILE_PATTERN.test(name)) continue;
|
|
251
|
+
// Changelogs
|
|
252
|
+
if (CHANGELOG_PATTERN.test(name)) continue;
|
|
253
|
+
// decisions.md, runbook.md: explicit
|
|
254
|
+
if (name === "decisions.md" || name === "runbook.md") continue;
|
|
255
|
+
// Known-good kebab-case
|
|
256
|
+
if (KEBAB_CASE_PATTERN.test(name)) continue;
|
|
257
|
+
// README.md inside a subdir is fine
|
|
258
|
+
if (name === "README.md") continue;
|
|
259
|
+
// SCREAMING_SNAKE_CASE violations
|
|
260
|
+
if (SCREAMING_SNAKE_PATTERN.test(name)) {
|
|
261
|
+
violations.push({
|
|
262
|
+
severity: "error",
|
|
263
|
+
repo: repoName,
|
|
264
|
+
path: join(repoName === "(root)" ? "" : repoName, rel),
|
|
265
|
+
rule: "screaming-snake-case",
|
|
266
|
+
message: `filename ${name} uses SCREAMING_SNAKE_CASE; rename to kebab-case`,
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// Anything else that's not kebab-case is a warning (Title Case, mixed)
|
|
271
|
+
if (!/^[a-z0-9]/.test(name)) {
|
|
272
|
+
violations.push({
|
|
273
|
+
severity: "warning",
|
|
274
|
+
repo: repoName,
|
|
275
|
+
path: join(repoName === "(root)" ? "" : repoName, rel),
|
|
276
|
+
rule: "non-kebab-filename",
|
|
277
|
+
message: `filename ${name} is not kebab-case`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return violations;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Files in docs/audits/ and docs/issues/ must match YYYY-MM-DD_<slug>.md */
|
|
285
|
+
function checkDatedDirs(repoName: string, _repoPath: string, files: string[]): Violation[] {
|
|
286
|
+
const violations: Violation[] = [];
|
|
287
|
+
const datedDirs = ["docs/audits/", "docs/issues/"];
|
|
288
|
+
for (const rel of files) {
|
|
289
|
+
for (const d of datedDirs) {
|
|
290
|
+
if (!rel.startsWith(d)) continue;
|
|
291
|
+
const name = basename(rel);
|
|
292
|
+
// README.md is the index file, allowed
|
|
293
|
+
if (name === "README.md") continue;
|
|
294
|
+
if (!DATED_FILE_PATTERN.test(name)) {
|
|
295
|
+
violations.push({
|
|
296
|
+
severity: "error",
|
|
297
|
+
repo: repoName,
|
|
298
|
+
path: join(repoName === "(root)" ? "" : repoName, rel),
|
|
299
|
+
rule: "undated-in-dated-dir",
|
|
300
|
+
message: `${d} file must match YYYY-MM-DD_<slug>.md; got ${name}`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return violations;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Changelog files must match YYYY-MM.md */
|
|
309
|
+
function checkChangelogNames(repoName: string, _repoPath: string, files: string[]): Violation[] {
|
|
310
|
+
const violations: Violation[] = [];
|
|
311
|
+
for (const rel of files) {
|
|
312
|
+
if (!rel.startsWith("docs/changelogs/")) continue;
|
|
313
|
+
const name = basename(rel);
|
|
314
|
+
if (name === "README.md") continue;
|
|
315
|
+
if (!CHANGELOG_PATTERN.test(name)) {
|
|
316
|
+
violations.push({
|
|
317
|
+
severity: "error",
|
|
318
|
+
repo: repoName,
|
|
319
|
+
path: join(repoName === "(root)" ? "" : repoName, rel),
|
|
320
|
+
rule: "bad-changelog-name",
|
|
321
|
+
message: `changelog must match YYYY-MM.md; got ${name}`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return violations;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Plans and issues must carry a Status header (content check, slow) */
|
|
329
|
+
function checkStatusHeaders(repoName: string, repoPath: string, files: string[]): Violation[] {
|
|
330
|
+
const violations: Violation[] = [];
|
|
331
|
+
const targetDirs = ["docs/plans/", "docs/issues/"];
|
|
332
|
+
for (const rel of files) {
|
|
333
|
+
const dirMatch = targetDirs.some((d) => rel.startsWith(d));
|
|
334
|
+
if (!dirMatch) continue;
|
|
335
|
+
const name = basename(rel);
|
|
336
|
+
if (name === "README.md") continue;
|
|
337
|
+
// Skip archive subdir
|
|
338
|
+
if (rel.includes("/archive/")) continue;
|
|
339
|
+
const full = join(repoPath, rel);
|
|
340
|
+
if (!hasStatusHeader(full)) {
|
|
341
|
+
const kind = rel.startsWith("docs/plans/") ? "plan" : "issue";
|
|
342
|
+
violations.push({
|
|
343
|
+
severity: "warning",
|
|
344
|
+
repo: repoName,
|
|
345
|
+
path: join(repoName === "(root)" ? "" : repoName, rel),
|
|
346
|
+
rule: "missing-status-header",
|
|
347
|
+
message: `${kind} missing **Status:** line in opening block`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return violations;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Intentional monoliths >30KB need a declaration banner */
|
|
355
|
+
function checkMonolithDeclaration(
|
|
356
|
+
repoName: string,
|
|
357
|
+
repoPath: string,
|
|
358
|
+
files: string[],
|
|
359
|
+
): Violation[] {
|
|
360
|
+
const violations: Violation[] = [];
|
|
361
|
+
const SIZE_THRESHOLD = 30 * 1024;
|
|
362
|
+
for (const rel of files) {
|
|
363
|
+
// Only flag top-level docs/ files, not per-repo entry tier (LLM-BRIEFING
|
|
364
|
+
// files are monoliths by convention, no banner needed).
|
|
365
|
+
if (repoName !== "(root)") continue;
|
|
366
|
+
if (!rel.startsWith("docs/")) continue;
|
|
367
|
+
if (rel.startsWith("docs/plans/")) continue;
|
|
368
|
+
if (rel.startsWith("docs/audits/")) continue;
|
|
369
|
+
if (rel.startsWith("docs/issues/")) continue;
|
|
370
|
+
if (rel.startsWith("docs/changelogs/")) continue;
|
|
371
|
+
const full = join(repoPath, rel);
|
|
372
|
+
let size: number;
|
|
373
|
+
try {
|
|
374
|
+
size = statSync(full).size;
|
|
375
|
+
} catch {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (size < SIZE_THRESHOLD) continue;
|
|
379
|
+
if (isDeclaredMonolith(full)) continue;
|
|
380
|
+
violations.push({
|
|
381
|
+
severity: "warning",
|
|
382
|
+
repo: repoName,
|
|
383
|
+
path: rel,
|
|
384
|
+
rule: "undeclared-monolith",
|
|
385
|
+
message: `${(size / 1024).toFixed(0)} KB file has no INTENTIONAL-MONOLITH banner; add one or split`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return violations;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// --- Runner ---
|
|
392
|
+
|
|
393
|
+
export async function runLint(opts: LintOpts): Promise<Violation[]> {
|
|
394
|
+
const violations: Violation[] = [];
|
|
395
|
+
const repos = getTargetRepos(opts);
|
|
396
|
+
|
|
397
|
+
for (const { name, path, isSubmodule } of repos) {
|
|
398
|
+
violations.push(...checkEntryTier(name, path, isSubmodule));
|
|
399
|
+
violations.push(...checkRootAllowlist(name, path));
|
|
400
|
+
|
|
401
|
+
const files = await findMarkdownFiles(path);
|
|
402
|
+
violations.push(...checkNamingConvention(name, path, files));
|
|
403
|
+
violations.push(...checkDatedDirs(name, path, files));
|
|
404
|
+
violations.push(...checkChangelogNames(name, path, files));
|
|
405
|
+
|
|
406
|
+
if (!opts.fast) {
|
|
407
|
+
violations.push(...checkStatusHeaders(name, path, files));
|
|
408
|
+
violations.push(...checkMonolithDeclaration(name, path, files));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return violations;
|
|
413
|
+
}
|