heyspex 0.1.0 → 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/README.md +21 -0
- package/bin/hs.mjs +931 -98
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +231 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +449 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +441 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +819 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +95 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +906 -21
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +660 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +29 -9
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code SessionStart hook → CLAUDE.md (agent contract) budget check.
|
|
3
|
+
//
|
|
4
|
+
// CLAUDE.md is the always-loaded agent contract: every byte is injected into
|
|
5
|
+
// context at the start of every session. Unlike the auto-memory index there is no
|
|
6
|
+
// hard harness cap, but compliance degrades as it grows — Anthropic's own guidance
|
|
7
|
+
// is "bloated CLAUDE.md files cause Claude to ignore your actual instructions," and
|
|
8
|
+
// practitioner testing reports compliance dropping sharply past ~200 lines (rules
|
|
9
|
+
// get buried). So this is a SOFT quality budget, not a data-loss cap.
|
|
10
|
+
//
|
|
11
|
+
// The warn thresholds are user-configurable from the HeySpex Console at
|
|
12
|
+
// Settings → Config → Memory (persisted in spex-console/settings.json under
|
|
13
|
+
// `agentMemory`): an on/off toggle plus the line + byte target. Defaults below
|
|
14
|
+
// (120 lines / 15KB) are the lean target this file was trimmed to (see
|
|
15
|
+
// [[minimizing-claude-md-thin-agent-contract]],
|
|
16
|
+
// [[HS-TASK-AIU4-claude-md-trim-procedures-to-mcp-pointers]]); past ~200 lines
|
|
17
|
+
// compliance drops sharply (rules get buried). It stays SILENT while CLAUDE.md is
|
|
18
|
+
// lean and, once it crosses the target, names the largest sections to trim and
|
|
19
|
+
// reminds the agent of the keep/cut test: "Would removing this line cause Claude to
|
|
20
|
+
// make mistakes? If not, cut it." Detail belongs in a procedure (fetched on demand
|
|
21
|
+
// via heyspex_get_procedure), not here.
|
|
22
|
+
//
|
|
23
|
+
// Mirrors check-auto-memory-budget.mjs (same env-gate + exit-0 discipline).
|
|
24
|
+
//
|
|
25
|
+
// Env gate:
|
|
26
|
+
// CLAUDE_AGENT=true opt in (unset → silent, so interactive
|
|
27
|
+
// humans aren't spammed).
|
|
28
|
+
// HEYSPEX_CLAUDE_MD_BUDGET_OFF=1 hard kill switch — silent even when opted in.
|
|
29
|
+
// HEYSPEX_CLAUDE_MD_FILE=<path> override the resolved CLAUDE.md path
|
|
30
|
+
// (used by the smoke test; also a manual escape hatch).
|
|
31
|
+
// HEYSPEX_CLAUDE_MD_SETTINGS_FILE=<path> override the spex-console/settings.json
|
|
32
|
+
// path read for the budget knobs (smoke test).
|
|
33
|
+
//
|
|
34
|
+
// MUST exit 0 on every path — a hook that breaks Claude Code is worse than one
|
|
35
|
+
// that silently does nothing.
|
|
36
|
+
//
|
|
37
|
+
// Output shape: { hookSpecificOutput: { hookEventName, additionalContext } }.
|
|
38
|
+
|
|
39
|
+
import fs from 'node:fs';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath } from 'node:url';
|
|
42
|
+
import { getActiveConfig } from '../../vendor/heyspex-mcp/vault-resolve.mjs';
|
|
43
|
+
|
|
44
|
+
const COMPLIANCE_CLIFF_LINES = 200; // documented point where rules get buried
|
|
45
|
+
const DEFAULT_MAX_LINES = 120; // lean target; overridable in Console settings
|
|
46
|
+
const DEFAULT_MAX_BYTES = 15000; // ~14.6 KiB; overridable in Console settings
|
|
47
|
+
const MAX_SECTIONS = 6;
|
|
48
|
+
|
|
49
|
+
// Duplication detection ([[HS-UFQS-5PQ2-DIAG]]): "Size is not the violation —
|
|
50
|
+
// duplication is. A budget check that only counts lines will keep passing
|
|
51
|
+
// while the file rots." A line/byte-lean CLAUDE.md can still carry a stale
|
|
52
|
+
// inlined copy of a procedure — this check fires independent of the size
|
|
53
|
+
// budget above. Conservative on purpose (exact-line match, length floor,
|
|
54
|
+
// headers/tables excluded) to avoid flagging shared boilerplate as a false
|
|
55
|
+
// positive; it is meant to catch verbatim paragraph-level copies, not any
|
|
56
|
+
// shared phrase.
|
|
57
|
+
const DUPLICATE_MIN_LINE_LENGTH = 50;
|
|
58
|
+
const MAX_DUPLICATE_HITS_SHOWN = 5;
|
|
59
|
+
|
|
60
|
+
function normalizeLineForDupeCheck(line) {
|
|
61
|
+
return line.trim().replace(/\s+/g, ' ');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isCheckableLine(line) {
|
|
65
|
+
if (line.length < DUPLICATE_MIN_LINE_LENGTH) return false;
|
|
66
|
+
if (line.startsWith('#')) return false; // headings repeat by design, not real dup
|
|
67
|
+
if (line.startsWith('|')) return false; // table rows (change-log rows etc.)
|
|
68
|
+
if (line.startsWith('```')) return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveProtocolsDir(projectDir) {
|
|
73
|
+
const override = process.env.HEYSPEX_CLAUDE_MD_PROTOCOLS_DIR;
|
|
74
|
+
if (override && override.trim()) return override;
|
|
75
|
+
// Since the repo split (2026-08-31) protocols live in the vault clone, not the
|
|
76
|
+
// project dir — prefer the registry-resolved vault when the local dir is gone.
|
|
77
|
+
const local = path.join(projectDir, 'specs', 'protocols');
|
|
78
|
+
if (fs.existsSync(local)) return local;
|
|
79
|
+
try {
|
|
80
|
+
const vault = getActiveConfig()?.repoRootAbs;
|
|
81
|
+
if (vault) return path.join(vault, 'specs', 'protocols');
|
|
82
|
+
} catch {
|
|
83
|
+
// registry unreadable — fall through to the local default
|
|
84
|
+
}
|
|
85
|
+
return local;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** normalized line → Set of protocol basenames it appears in verbatim. PURE given fs snapshot. */
|
|
89
|
+
export function collectProtocolLines(protocolsDir) {
|
|
90
|
+
const map = new Map();
|
|
91
|
+
let entries = [];
|
|
92
|
+
try {
|
|
93
|
+
entries = fs.readdirSync(protocolsDir).filter((f) => f.toLowerCase().endsWith('.md'));
|
|
94
|
+
} catch {
|
|
95
|
+
return map; // no protocols dir (fixture / misconfigured repo) → nothing to compare against
|
|
96
|
+
}
|
|
97
|
+
for (const file of entries) {
|
|
98
|
+
let text;
|
|
99
|
+
try {
|
|
100
|
+
text = fs.readFileSync(path.join(protocolsDir, file), 'utf8');
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
105
|
+
const line = normalizeLineForDupeCheck(raw);
|
|
106
|
+
if (!isCheckableLine(line)) continue;
|
|
107
|
+
if (!map.has(line)) map.set(line, new Set());
|
|
108
|
+
map.get(line).add(file);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return map;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Lines in `content` (CLAUDE.md) that appear verbatim in a `specs/protocols/*.md`
|
|
116
|
+
* file. Returns [] when nothing matches (including when the protocols dir is
|
|
117
|
+
* absent/empty — never throws). Pure given the protocolLines snapshot.
|
|
118
|
+
*/
|
|
119
|
+
export function detectDuplicateLines(content, protocolLines) {
|
|
120
|
+
if (protocolLines.size === 0) return [];
|
|
121
|
+
const hits = [];
|
|
122
|
+
const seen = new Set(); // de-dupe repeated identical lines within CLAUDE.md itself
|
|
123
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
124
|
+
const line = normalizeLineForDupeCheck(raw);
|
|
125
|
+
if (!isCheckableLine(line) || seen.has(line)) continue;
|
|
126
|
+
const files = protocolLines.get(line);
|
|
127
|
+
if (files && files.size > 0) {
|
|
128
|
+
hits.push({ line, files: [...files] });
|
|
129
|
+
seen.add(line);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return hits;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildDuplicationReport(hits) {
|
|
136
|
+
if (!hits || hits.length === 0) return null;
|
|
137
|
+
const out = [
|
|
138
|
+
`CLAUDE.md duplication check — ${hits.length} line(s) match specs/protocols/*.md verbatim. ` +
|
|
139
|
+
`CLAUDE.md is always-loaded; an inlined copy silently overrides the live procedure once the ` +
|
|
140
|
+
`procedure is updated and the copy isn't. Replace with a one-line pointer (e.g. ` +
|
|
141
|
+
`"heyspex_get_procedure({ name: '<protocol>' })") instead of the inlined text.`,
|
|
142
|
+
];
|
|
143
|
+
for (const h of hits.slice(0, MAX_DUPLICATE_HITS_SHOWN)) {
|
|
144
|
+
const preview = h.line.length > 90 ? `${h.line.slice(0, 90)}…` : h.line;
|
|
145
|
+
out.push(` • "${preview}" — also verbatim in ${h.files.join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
if (hits.length > MAX_DUPLICATE_HITS_SHOWN) {
|
|
148
|
+
out.push(` …and ${hits.length - MAX_DUPLICATE_HITS_SHOWN} more.`);
|
|
149
|
+
}
|
|
150
|
+
return out.join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function repoRoot() {
|
|
154
|
+
// scripts/claude-hooks/ → up two levels.
|
|
155
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readStdinJson() {
|
|
159
|
+
if (process.stdin.isTTY) return null;
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
let buf = '';
|
|
162
|
+
let settled = false;
|
|
163
|
+
const finish = (value) => {
|
|
164
|
+
if (settled) return;
|
|
165
|
+
settled = true;
|
|
166
|
+
resolve(value);
|
|
167
|
+
};
|
|
168
|
+
const timer = setTimeout(() => finish(buf || ''), 200);
|
|
169
|
+
process.stdin.setEncoding('utf8');
|
|
170
|
+
process.stdin.on('data', (chunk) => {
|
|
171
|
+
buf += chunk;
|
|
172
|
+
});
|
|
173
|
+
process.stdin.on('end', () => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
finish(buf);
|
|
176
|
+
});
|
|
177
|
+
process.stdin.on('error', () => {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
finish(buf);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function emit(eventName, additionalContext) {
|
|
185
|
+
if (!additionalContext) return;
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
hookSpecificOutput: { hookEventName: eventName, additionalContext },
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveClaudeMdFile(projectDir) {
|
|
194
|
+
const override = process.env.HEYSPEX_CLAUDE_MD_FILE;
|
|
195
|
+
if (override && override.trim()) return override;
|
|
196
|
+
return path.join(projectDir, 'CLAUDE.md');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Read the user's budget knobs from the Console settings file (Settings → Config →
|
|
200
|
+
// Memory writes `agentMemory` here). Missing/malformed → defaults. Standalone JSON
|
|
201
|
+
// read so this .mjs hook needs no TS import.
|
|
202
|
+
function resolveBudget(projectDir) {
|
|
203
|
+
const file =
|
|
204
|
+
process.env.HEYSPEX_CLAUDE_MD_SETTINGS_FILE?.trim() ||
|
|
205
|
+
path.join(projectDir, 'spex-console', 'settings.json');
|
|
206
|
+
let mem = {};
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
209
|
+
if (parsed && typeof parsed.agentMemory === 'object' && parsed.agentMemory) {
|
|
210
|
+
mem = parsed.agentMemory;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// absent / malformed → defaults
|
|
214
|
+
}
|
|
215
|
+
const enabled = typeof mem.budgetEnabled === 'boolean' ? mem.budgetEnabled : true;
|
|
216
|
+
const maxLines =
|
|
217
|
+
typeof mem.budgetMaxLines === 'number' && mem.budgetMaxLines > 0
|
|
218
|
+
? Math.floor(mem.budgetMaxLines)
|
|
219
|
+
: DEFAULT_MAX_LINES;
|
|
220
|
+
const maxBytes =
|
|
221
|
+
typeof mem.budgetMaxBytes === 'number' && mem.budgetMaxBytes > 0
|
|
222
|
+
? Math.floor(mem.budgetMaxBytes)
|
|
223
|
+
: DEFAULT_MAX_BYTES;
|
|
224
|
+
return { enabled, maxLines, maxBytes };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Group lines under their nearest `## ` heading and measure each section's size,
|
|
228
|
+
// so the report can name the heaviest sections to trim first.
|
|
229
|
+
function analyze(content) {
|
|
230
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
231
|
+
const lines = content.split(/\r?\n/);
|
|
232
|
+
const sections = [];
|
|
233
|
+
let current = { title: '(preamble)', lineCount: 0 };
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
const m = line.match(/^##\s+(.+?)\s*$/); // top sections are `## `; deeper ones roll up
|
|
236
|
+
if (m) {
|
|
237
|
+
sections.push(current);
|
|
238
|
+
current = { title: m[1], lineCount: 0 };
|
|
239
|
+
}
|
|
240
|
+
current.lineCount += 1;
|
|
241
|
+
}
|
|
242
|
+
sections.push(current);
|
|
243
|
+
sections.sort((a, b) => b.lineCount - a.lineCount);
|
|
244
|
+
return { bytes, lineCount: lines.length, sections };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildReport(analysis, filePath, budget) {
|
|
248
|
+
const { bytes, lineCount, sections } = analysis;
|
|
249
|
+
if (lineCount < budget.maxLines && bytes < budget.maxBytes) return null; // lean → silent
|
|
250
|
+
const kb = (bytes / 1024).toFixed(1);
|
|
251
|
+
const maxKb = (budget.maxBytes / 1024).toFixed(1);
|
|
252
|
+
const out = [
|
|
253
|
+
`CLAUDE.md budget check — ${filePath}`,
|
|
254
|
+
`CLAUDE.md is ${lineCount} lines / ${kb}KB, past your ${budget.maxLines}-line / ${maxKb}KB ` +
|
|
255
|
+
`target (Settings → Config → Memory; past ~${COMPLIANCE_CLIFF_LINES} lines rule compliance ` +
|
|
256
|
+
`drops sharply — the compliance cliff). It is always-loaded, so trim it: move detail into a ` +
|
|
257
|
+
`procedure (fetched on demand via heyspex_get_procedure) and keep only the non-negotiable ` +
|
|
258
|
+
`contract. For each line ask "Would removing this cause Claude to make mistakes? If not, cut ` +
|
|
259
|
+
`it." See [[minimizing-claude-md-thin-agent-contract]].`,
|
|
260
|
+
];
|
|
261
|
+
const heaviest = sections.filter((s) => s.title !== '(preamble)').slice(0, MAX_SECTIONS);
|
|
262
|
+
if (heaviest.length) {
|
|
263
|
+
out.push('Largest sections (trim or push to a procedure first):');
|
|
264
|
+
for (const s of heaviest) out.push(` • ${s.lineCount} lines — ${s.title}`);
|
|
265
|
+
}
|
|
266
|
+
return out.join('\n');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function main() {
|
|
270
|
+
if (process.env.HEYSPEX_CLAUDE_MD_BUDGET_OFF === '1') return;
|
|
271
|
+
// No CLAUDE_AGENT gate ([[HS-DT9L-NLR7-DIAG-nine-hooks-are-switched-off-in]]).
|
|
272
|
+
// This used to exit early in interactive sessions, so the one check on the
|
|
273
|
+
// always-loaded agent contract never ran where that file is actually edited —
|
|
274
|
+
// and nothing else guards it either ([[HS-UFQS-5PQ2-DIAG-nothing-gates-an-edit-to-claude-md]]).
|
|
275
|
+
// Safe to run everywhere: it emits NOTHING while the file is inside budget
|
|
276
|
+
// (measured 0 bytes on 2026-08-15), so it speaks only when there is something
|
|
277
|
+
// to say. Break-glass stays HEYSPEX_CLAUDE_MD_BUDGET_OFF=1.
|
|
278
|
+
|
|
279
|
+
const raw = await readStdinJson();
|
|
280
|
+
if (!raw) return;
|
|
281
|
+
let payload;
|
|
282
|
+
try {
|
|
283
|
+
payload = JSON.parse(raw);
|
|
284
|
+
} catch {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (payload?.hook_event_name !== 'SessionStart') return;
|
|
288
|
+
|
|
289
|
+
const projectDir =
|
|
290
|
+
(typeof payload.cwd === 'string' && payload.cwd) || process.env.CLAUDE_PROJECT_DIR || repoRoot();
|
|
291
|
+
|
|
292
|
+
const budget = resolveBudget(projectDir);
|
|
293
|
+
if (!budget.enabled) return; // user turned the gate off in Console settings
|
|
294
|
+
|
|
295
|
+
const file = resolveClaudeMdFile(projectDir);
|
|
296
|
+
let content;
|
|
297
|
+
try {
|
|
298
|
+
content = fs.readFileSync(file, 'utf8');
|
|
299
|
+
} catch {
|
|
300
|
+
return; // absent / unreadable → silent
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Duplication runs independent of the size budget above ([[HS-UFQS-5PQ2-DIAG]]):
|
|
304
|
+
// a lean, under-budget CLAUDE.md can still carry a stale inlined procedure copy.
|
|
305
|
+
const sizeReport = buildReport(analyze(content), file, budget);
|
|
306
|
+
const protocolLines = collectProtocolLines(resolveProtocolsDir(projectDir));
|
|
307
|
+
const dupReport = buildDuplicationReport(detectDuplicateLines(content, protocolLines));
|
|
308
|
+
const combined = [sizeReport, dupReport].filter(Boolean).join('\n\n');
|
|
309
|
+
|
|
310
|
+
emit('SessionStart', combined || null);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Only run the hook when this file is executed directly (the Claude Code
|
|
314
|
+
// hooks runner spawns it as a subprocess) — NOT on import. Without this guard,
|
|
315
|
+
// a test/gate script that imports the pure helpers above (resolveProtocolsDir,
|
|
316
|
+
// collectProtocolLines, detectDuplicateLines — reused by
|
|
317
|
+
// scripts/check-claude-md-duplication.mjs per PORTING.md's "import, don't
|
|
318
|
+
// re-derive") would also trigger main()'s stdin read as a side effect, which
|
|
319
|
+
// registers stdin listeners that outlive the read and hang the importing
|
|
320
|
+
// process ([[HS-STRB-NYPR-TASK-replace-every-inline-copy-of-a]]).
|
|
321
|
+
const isDirectExecution =
|
|
322
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
323
|
+
if (isDirectExecution) {
|
|
324
|
+
main().catch(() => {
|
|
325
|
+
// Never break Claude Code on a hook failure.
|
|
326
|
+
});
|
|
327
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin shell over the spexharness engine port of this gate (wave 6 of
|
|
3
|
+
// [[HS-AZ61-NYPR]]). Same shape as the twelve shells
|
|
4
|
+
// scripts/generate-hook-shells.mjs writes; this one is authored rather than
|
|
5
|
+
// generated only because it is not part of that generator's frozen twelve.
|
|
6
|
+
// To change what this gate DECIDES, edit
|
|
7
|
+
// spexharness/src/gates/check-mcp-freshness-hook.mjs (the rule's mapping lives
|
|
8
|
+
// in spexharness/src/mcp-freshness.mjs, the freshness FACT in
|
|
9
|
+
// scripts/check-mcp-freshness.mjs).
|
|
10
|
+
|
|
11
|
+
import { runGateAsHook } from '../../spexharness/src/adapters/claude-code.mjs';
|
|
12
|
+
import gate from '../../spexharness/src/gates/check-mcp-freshness-hook.mjs';
|
|
13
|
+
|
|
14
|
+
await runGateAsHook(gate);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// GENERATED by scripts/generate-hook-shells.mjs (HS-IQBF) — do not hand-edit.
|
|
3
|
+
// This file only wires Claude Code's hook stdin/stdout contract to the
|
|
4
|
+
// spexharness engine port of this gate. To change what this gate DECIDES,
|
|
5
|
+
// edit spexharness/src/gates/claim-before-code-gate.mjs. To change the SHAPE of this shell,
|
|
6
|
+
// edit the generator, then rerun: node scripts/generate-hook-shells.mjs
|
|
7
|
+
|
|
8
|
+
import { runGateAsHook } from '../../spexharness/src/adapters/claude-code.mjs';
|
|
9
|
+
import gate from '../../spexharness/src/gates/claim-before-code-gate.mjs';
|
|
10
|
+
|
|
11
|
+
await runGateAsHook(gate);
|