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,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code SessionStart hook → auto-memory index (MEMORY.md) self-healing + budget check.
|
|
3
|
+
//
|
|
4
|
+
// The Claude Code auto-memory index at ~/.claude/projects/<repo>/memory/MEMORY.md
|
|
5
|
+
// is injected into context at the start of every session, but only the first 200
|
|
6
|
+
// lines OR 25KB (whichever comes first) are loaded — content past that threshold
|
|
7
|
+
// is silently dropped from session-start context, degrading recall. Detail kept
|
|
8
|
+
// in the sibling topic files is NOT loaded at startup, so it is free.
|
|
9
|
+
//
|
|
10
|
+
// Per the research-backed retention policy ([[auto-memory-index-hygiene]] /
|
|
11
|
+
// [[HS-VN9W-RES]]), the auto-memory index is a thin curated-working-set + ROUTER
|
|
12
|
+
// over the HeySpex vault — it must NOT mirror vault memory. The vault
|
|
13
|
+
// (specs/memory/) is the retrieval store, served dynamically by
|
|
14
|
+
// heyspex_recall_memories. So any index entry whose link already targets
|
|
15
|
+
// specs/memory/... is, by construction, a duplicate of content that lives in the
|
|
16
|
+
// vault — provably safe to remove from the always-loaded index.
|
|
17
|
+
//
|
|
18
|
+
// This hook therefore does two things at session start:
|
|
19
|
+
// 1. SELF-HEAL (write) — evicts those provably-mirrored entries in place (content
|
|
20
|
+
// stays in the vault; only the redundant index line leaves). A sanctioned
|
|
21
|
+
// PINNED allowlist (lines tagged [pinned]) is never touched. This is the
|
|
22
|
+
// eviction mechanism that keeps the index from re-bloating (MemGPT/Letta
|
|
23
|
+
// eviction pattern applied to the index tier).
|
|
24
|
+
// 2. ADVISE (emit) — for whatever remains, stays SILENT while healthy and, once
|
|
25
|
+
// the index crosses ~70% of the cap, names the specific over-length entries
|
|
26
|
+
// to relocate. The advisory half never blocks.
|
|
27
|
+
//
|
|
28
|
+
// Eviction is conservative: it only removes entries whose link path contains
|
|
29
|
+
// `specs/memory/` (those have a guaranteed vault home). Domain entries backed by
|
|
30
|
+
// local topic files are left for the one-time migration + the PreToolUse
|
|
31
|
+
// write-guard; this hook never drops a line it cannot prove is mirrored.
|
|
32
|
+
//
|
|
33
|
+
// Env gate (mirrors wip-checkpoint.mjs):
|
|
34
|
+
// CLAUDE_AGENT=true opt in (unset → silent + no self-heal, so
|
|
35
|
+
// interactive humans aren't spammed/mutated).
|
|
36
|
+
// HEYSPEX_AUTO_MEMORY_BUDGET_OFF=1 hard kill switch — silent + no self-heal.
|
|
37
|
+
// HEYSPEX_AUTO_MEMORY_SELF_HEAL_OFF=1 disable only the WRITE (stay advisory-only).
|
|
38
|
+
// HEYSPEX_AUTO_MEMORY_FILE=<path> override the resolved MEMORY.md path
|
|
39
|
+
// (used by the smoke test; also a manual escape hatch).
|
|
40
|
+
//
|
|
41
|
+
// MUST exit 0 on every path — a hook that breaks Claude Code is worse than one
|
|
42
|
+
// that silently does nothing. Writes are best-effort: a failed write is swallowed
|
|
43
|
+
// and the advisory still emits.
|
|
44
|
+
//
|
|
45
|
+
// Output shape: { hookSpecificOutput: { hookEventName, additionalContext } }.
|
|
46
|
+
|
|
47
|
+
import fs from 'node:fs';
|
|
48
|
+
import os from 'node:os';
|
|
49
|
+
import path from 'node:path';
|
|
50
|
+
import { fileURLToPath } from 'node:url';
|
|
51
|
+
|
|
52
|
+
import { defaultConfigRoot } from '../../spexharness/src/config-root.mjs';
|
|
53
|
+
|
|
54
|
+
export const CAP_BYTES = 25000; // harness session-start load cap (~24.4 KiB), per the official memory docs
|
|
55
|
+
export const CAP_LINES = 200;
|
|
56
|
+
export const TRIGGER = 0.7; // compact proactively at 70% of the cap, not at overflow
|
|
57
|
+
export const HOOK_MAX_CHARS = 200; // per-entry index-hook limit ([[auto-memory-index-hygiene]])
|
|
58
|
+
const MAX_OFFENDERS = 8;
|
|
59
|
+
|
|
60
|
+
// A top-level index entry whose markdown link target contains `specs/memory/` is a
|
|
61
|
+
// duplicate of content that already lives in the vault retrieval store — provably
|
|
62
|
+
// safe to evict from the always-loaded index. Captures the link target so the
|
|
63
|
+
// eviction can confirm the vault home actually exists before dropping the line
|
|
64
|
+
// (a dangling link has no other home, so evicting it would be lossy).
|
|
65
|
+
const VAULT_MIRROR_RE = /^- \[[^\]]*\]\(([^)]*specs\/memory\/[^)]*)\)/;
|
|
66
|
+
|
|
67
|
+
// Lines carrying an explicit pinned marker are the sanctioned curated working set
|
|
68
|
+
// (identity / mode / standing rules) — never evicted, even if they somehow match.
|
|
69
|
+
const PINNED_RE = /\[pinned\]|\(pinned\)/i;
|
|
70
|
+
|
|
71
|
+
function repoRoot() {
|
|
72
|
+
// scripts/claude-hooks/ → up two levels.
|
|
73
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function readStdinJson() {
|
|
77
|
+
if (process.stdin.isTTY) return null;
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
let buf = '';
|
|
80
|
+
let settled = false;
|
|
81
|
+
const finish = (value) => {
|
|
82
|
+
if (settled) return;
|
|
83
|
+
settled = true;
|
|
84
|
+
resolve(value);
|
|
85
|
+
};
|
|
86
|
+
const timer = setTimeout(() => finish(buf || ''), 200);
|
|
87
|
+
process.stdin.setEncoding('utf8');
|
|
88
|
+
process.stdin.on('data', (chunk) => {
|
|
89
|
+
buf += chunk;
|
|
90
|
+
});
|
|
91
|
+
process.stdin.on('end', () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
finish(buf);
|
|
94
|
+
});
|
|
95
|
+
process.stdin.on('error', () => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
finish(buf);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function emit(eventName, additionalContext) {
|
|
103
|
+
if (!additionalContext) return;
|
|
104
|
+
process.stdout.write(
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
hookSpecificOutput: { hookEventName: eventName, additionalContext },
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readJsonSafe(file) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Claude Code derives the auto-memory dir name by replacing path separators and
|
|
120
|
+
// the drive colon with '-' (e.g. C:\Users\pwher\dev-projects\workflow-protocol →
|
|
121
|
+
// C--Users-pwher-dev-projects-workflow-protocol).
|
|
122
|
+
function mangleProjectDir(dir) {
|
|
123
|
+
return dir.replace(/[:\\/]+/g, '-');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveAutoMemoryDir(projectDir) {
|
|
127
|
+
// Honor an explicit autoMemoryDirectory setting (local overrides project).
|
|
128
|
+
for (const name of ['settings.local.json', 'settings.json']) {
|
|
129
|
+
const settings = readJsonSafe(path.join(repoRoot(), '.claude', name));
|
|
130
|
+
const dir = settings?.autoMemoryDirectory;
|
|
131
|
+
if (typeof dir === 'string' && dir.trim()) {
|
|
132
|
+
return dir.startsWith('~/') ? path.join(os.homedir(), dir.slice(2)) : dir;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return path.join(os.homedir(), '.claude', 'projects', mangleProjectDir(projectDir), 'memory');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveMemoryFile(projectDir) {
|
|
139
|
+
const override = process.env.HEYSPEX_AUTO_MEMORY_FILE;
|
|
140
|
+
if (override && override.trim()) return override;
|
|
141
|
+
return path.join(resolveAutoMemoryDir(projectDir), 'MEMORY.md');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function analyze(content) {
|
|
145
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
146
|
+
const allLines = content.split(/\r?\n/);
|
|
147
|
+
const offenders = [];
|
|
148
|
+
for (const line of allLines) {
|
|
149
|
+
if (!line.startsWith('- ')) continue; // only top-level index entries
|
|
150
|
+
if (line.length <= HOOK_MAX_CHARS) continue;
|
|
151
|
+
const m = line.match(/^- \[([^\]]+)\]/);
|
|
152
|
+
offenders.push({ label: m ? m[1] : line.slice(2, 60), length: line.length });
|
|
153
|
+
}
|
|
154
|
+
offenders.sort((a, b) => b.length - a.length);
|
|
155
|
+
return { bytes, lineCount: allLines.length, offenders };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Remove provably-mirrored entries (link → specs/memory/) from the index. Pinned
|
|
159
|
+
// lines and any non-entry lines (router line, headers, blank lines) are preserved
|
|
160
|
+
// verbatim. Returns the rewritten content, the evicted entry labels, and whether
|
|
161
|
+
// anything changed. Pure — does not touch disk.
|
|
162
|
+
export function evictVaultMirrors(content, vaultHomeExists) {
|
|
163
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
164
|
+
const lines = content.split(/\r?\n/);
|
|
165
|
+
const kept = [];
|
|
166
|
+
const evicted = [];
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
const m = VAULT_MIRROR_RE.exec(line);
|
|
169
|
+
// Evict only a non-pinned line whose specs/memory/ target ACTUALLY resolves —
|
|
170
|
+
// a dangling link has no vault home, so dropping it would lose the only pointer.
|
|
171
|
+
if (m && !PINNED_RE.test(line) && vaultHomeExists(m[1])) {
|
|
172
|
+
const label = line.match(/^- \[([^\]]+)\]/);
|
|
173
|
+
evicted.push(label ? label[1] : line.slice(2, 60));
|
|
174
|
+
continue; // drop the redundant index line
|
|
175
|
+
}
|
|
176
|
+
kept.push(line);
|
|
177
|
+
}
|
|
178
|
+
return { newContent: kept.join(eol), evicted, changed: evicted.length > 0 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildEvictionNote(evicted) {
|
|
182
|
+
if (!evicted.length) return null;
|
|
183
|
+
const shown = evicted.slice(0, MAX_OFFENDERS);
|
|
184
|
+
const out = [
|
|
185
|
+
`Auto-memory self-heal — evicted ${evicted.length} vault-mirrored ` +
|
|
186
|
+
`${evicted.length === 1 ? 'entry' : 'entries'} from the always-loaded index ` +
|
|
187
|
+
`(their content lives in the vault; recall via heyspex_recall_memories). ` +
|
|
188
|
+
`Per [[auto-memory-index-hygiene]], the index must not mirror specs/memory/.`,
|
|
189
|
+
];
|
|
190
|
+
for (const label of shown) out.push(` • ${label}`);
|
|
191
|
+
if (evicted.length > MAX_OFFENDERS) out.push(` • …and ${evicted.length - MAX_OFFENDERS} more`);
|
|
192
|
+
return out.join('\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Compaction engine ────────────────────────────────────────────────────────
|
|
196
|
+
// [[HS-AE28-NYPR-TASK-add-a-compaction-rule-that-runs-when]]: eviction above
|
|
197
|
+
// only removes entries with a PROVABLE vault home (specs/memory/ mirrors) —
|
|
198
|
+
// safe, but not sufficient on its own once the index is bloated with long,
|
|
199
|
+
// non-mirrored hook entries too. This adds the second lever: once the index
|
|
200
|
+
// (post-eviction) has crossed `trigger` (70%) of its byte/line cap, truncate
|
|
201
|
+
// any remaining over-`hookMaxChars` entry to a one-line hook in place. Pure —
|
|
202
|
+
// does not touch disk; `compactMemoryIndex` is a fixture-testable engine
|
|
203
|
+
// module callers can run against ANY injected content/path (never the
|
|
204
|
+
// caller's real MEMORY.md by default — the caller decides what to read/write).
|
|
205
|
+
|
|
206
|
+
/** Truncate every non-pinned, over-length `- ` entry to `hookMaxChars`. Pure. */
|
|
207
|
+
export function truncateOverLengthEntries(content, { hookMaxChars = HOOK_MAX_CHARS } = {}) {
|
|
208
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
209
|
+
const lines = content.split(/\r?\n/);
|
|
210
|
+
const truncated = [];
|
|
211
|
+
const out = lines.map((line) => {
|
|
212
|
+
if (!line.startsWith('- ')) return line; // only top-level index entries
|
|
213
|
+
if (PINNED_RE.test(line)) return line; // sanctioned curated working set — never touched
|
|
214
|
+
if (line.length <= hookMaxChars) return line;
|
|
215
|
+
const m = line.match(/^- \[([^\]]+)\]/);
|
|
216
|
+
truncated.push({ label: m ? m[1] : line.slice(2, 60), originalLength: line.length });
|
|
217
|
+
const cut = line.slice(0, Math.max(0, hookMaxChars - 1)).trimEnd();
|
|
218
|
+
return `${cut}…`;
|
|
219
|
+
});
|
|
220
|
+
return { newContent: out.join(eol), truncated, changed: truncated.length > 0 };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The compaction rule: runs ONLY once `content` has crossed `trigger` (70% by
|
|
225
|
+
* default) of its byte/line cap, and truncates over-length entries to bring it
|
|
226
|
+
* back down. Returns `ranCompaction: false` (no-op, `newContent === content`)
|
|
227
|
+
* below the trigger — the rule stays silent while the index is healthy, same
|
|
228
|
+
* discipline as the advisory report.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} content
|
|
231
|
+
* @param {{ capBytes?: number, capLines?: number, trigger?: number, hookMaxChars?: number }} [opts]
|
|
232
|
+
*/
|
|
233
|
+
export function compactMemoryIndex(
|
|
234
|
+
content,
|
|
235
|
+
{ capBytes = CAP_BYTES, capLines = CAP_LINES, trigger = TRIGGER, hookMaxChars = HOOK_MAX_CHARS } = {},
|
|
236
|
+
) {
|
|
237
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
238
|
+
const lineCount = content.split(/\r?\n/).length;
|
|
239
|
+
const before = { bytes, lineCount };
|
|
240
|
+
if (bytes < capBytes * trigger && lineCount < capLines * trigger) {
|
|
241
|
+
return { newContent: content, ranCompaction: false, changed: false, truncated: [], before, after: before };
|
|
242
|
+
}
|
|
243
|
+
const { newContent, truncated, changed } = truncateOverLengthEntries(content, { hookMaxChars });
|
|
244
|
+
return {
|
|
245
|
+
newContent,
|
|
246
|
+
ranCompaction: true,
|
|
247
|
+
changed,
|
|
248
|
+
truncated,
|
|
249
|
+
before,
|
|
250
|
+
after: { bytes: Buffer.byteLength(newContent, 'utf8'), lineCount: newContent.split(/\r?\n/).length },
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildCompactionNote(result, { capBytes = CAP_BYTES, capLines = CAP_LINES, hookMaxChars = HOOK_MAX_CHARS } = {}) {
|
|
255
|
+
if (!result.ranCompaction || result.truncated.length === 0) return null;
|
|
256
|
+
const capKb = (capBytes / 1024).toFixed(1);
|
|
257
|
+
const out = [
|
|
258
|
+
`Auto-memory compaction — MEMORY.md crossed ${Math.round(TRIGGER * 100)}% of its ${capKb}KB / ${capLines}-line ` +
|
|
259
|
+
`cap, so ${result.truncated.length} over-length ${result.truncated.length === 1 ? 'entry was' : 'entries were'} ` +
|
|
260
|
+
`truncated to ${hookMaxChars} chars in place (detail belongs in the linked topic file / vault, not the ` +
|
|
261
|
+
`always-loaded index — [[auto-memory-index-hygiene]]).`,
|
|
262
|
+
];
|
|
263
|
+
for (const t of result.truncated.slice(0, MAX_OFFENDERS)) {
|
|
264
|
+
out.push(` • ${t.originalLength} → ${hookMaxChars} chars — ${t.label}`);
|
|
265
|
+
}
|
|
266
|
+
if (result.truncated.length > MAX_OFFENDERS) out.push(` • …and ${result.truncated.length - MAX_OFFENDERS} more`);
|
|
267
|
+
return out.join('\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildReport(analysis, filePath) {
|
|
271
|
+
const { bytes, lineCount, offenders } = analysis;
|
|
272
|
+
if (bytes < CAP_BYTES * TRIGGER && lineCount < CAP_LINES * TRIGGER) return null; // healthy → silent
|
|
273
|
+
const kb = (bytes / 1024).toFixed(1);
|
|
274
|
+
const capKb = (CAP_BYTES / 1024).toFixed(1);
|
|
275
|
+
const pct = Math.round((bytes / CAP_BYTES) * 100);
|
|
276
|
+
const out = [
|
|
277
|
+
`Auto-memory index budget check — ${filePath}`,
|
|
278
|
+
`MEMORY.md is ${kb}KB / ${lineCount} lines (${pct}% of the ${capKb}KB / ${CAP_LINES}-line ` +
|
|
279
|
+
`session-start load cap). Compact toward one-line hooks per ` +
|
|
280
|
+
`[[auto-memory-index-hygiene]] before it overflows — detail belongs in the linked ` +
|
|
281
|
+
`topic files, which are not loaded at startup.`,
|
|
282
|
+
];
|
|
283
|
+
if (offenders.length) {
|
|
284
|
+
out.push(`Entries over ${HOOK_MAX_CHARS} chars (compact these first):`);
|
|
285
|
+
for (const o of offenders.slice(0, MAX_OFFENDERS)) out.push(` • ${o.length} chars — ${o.label}`);
|
|
286
|
+
if (offenders.length > MAX_OFFENDERS) out.push(` • …and ${offenders.length - MAX_OFFENDERS} more`);
|
|
287
|
+
}
|
|
288
|
+
return out.join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function main() {
|
|
292
|
+
if (process.env.HEYSPEX_AUTO_MEMORY_BUDGET_OFF === '1') return;
|
|
293
|
+
// No CLAUDE_AGENT gate ([[HS-DT9L-NLR7-DIAG-nine-hooks-are-switched-off-in]]).
|
|
294
|
+
// The memory index is loaded into EVERY session, interactive included, so the
|
|
295
|
+
// context it costs is paid in both — gating the check on unattended sessions
|
|
296
|
+
// only meant the one place a human reads the warning never got it.
|
|
297
|
+
// Safe to run everywhere: it emits NOTHING while the index is inside budget
|
|
298
|
+
// (measured 0 bytes on 2026-08-15). Break-glass stays HEYSPEX_AUTO_MEMORY_BUDGET_OFF=1.
|
|
299
|
+
|
|
300
|
+
const raw = await readStdinJson();
|
|
301
|
+
if (!raw) return;
|
|
302
|
+
let payload;
|
|
303
|
+
try {
|
|
304
|
+
payload = JSON.parse(raw);
|
|
305
|
+
} catch {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (payload?.hook_event_name !== 'SessionStart') return;
|
|
309
|
+
|
|
310
|
+
const projectDir =
|
|
311
|
+
(typeof payload.cwd === 'string' && payload.cwd) || process.env.CLAUDE_PROJECT_DIR || repoRoot();
|
|
312
|
+
const file = resolveMemoryFile(projectDir);
|
|
313
|
+
|
|
314
|
+
let content;
|
|
315
|
+
try {
|
|
316
|
+
content = fs.readFileSync(file, 'utf8');
|
|
317
|
+
} catch {
|
|
318
|
+
return; // absent / unreadable → silent
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 1. Self-heal: evict provably-mirrored entries and persist (best-effort).
|
|
322
|
+
// A `specs/memory/<…>` link is resolved against the repo before eviction —
|
|
323
|
+
// a dangling link is kept (its content has no other home). `specs/` is a
|
|
324
|
+
// VAULT artifact (per ADR-0009's two-root split, spexharness/src/config-
|
|
325
|
+
// root.mjs), not a code-checkout one — since the 2026-08-31 repo split put
|
|
326
|
+
// the vault in its own checkout, resolving against `repoRoot()` (this code
|
|
327
|
+
// repo) would find `specs/` nowhere and treat every real mirror as
|
|
328
|
+
// dangling. `defaultConfigRoot()` resolves the ACTIVE vault the same way
|
|
329
|
+
// every other hook's config-gated rule does (env HEYSPEX_VAULT_DIR /
|
|
330
|
+
// HEYSPEX_VAULT_ROOT wins outright; falls back to a cwd-contained vault,
|
|
331
|
+
// then the registry pointer, then this code root).
|
|
332
|
+
const base = defaultConfigRoot();
|
|
333
|
+
const vaultHomeExists = (rel) => {
|
|
334
|
+
try {
|
|
335
|
+
return fs.existsSync(path.resolve(base, rel.split('#')[0].trim()));
|
|
336
|
+
} catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
let evictionNote = null;
|
|
341
|
+
const { newContent, evicted, changed } = evictVaultMirrors(content, vaultHomeExists);
|
|
342
|
+
if (changed) {
|
|
343
|
+
if (process.env.HEYSPEX_AUTO_MEMORY_SELF_HEAL_OFF === '1') {
|
|
344
|
+
// Write disabled — fall back to flagging the mirrors without mutating.
|
|
345
|
+
evictionNote = buildEvictionNote(evicted).replace('evicted', 'would evict');
|
|
346
|
+
} else {
|
|
347
|
+
try {
|
|
348
|
+
fs.writeFileSync(file, newContent);
|
|
349
|
+
content = newContent; // advise against the healed file
|
|
350
|
+
evictionNote = buildEvictionNote(evicted);
|
|
351
|
+
} catch {
|
|
352
|
+
// Write failed (permissions, etc.) — keep going; advise against the original.
|
|
353
|
+
evictionNote = buildEvictionNote(evicted).replace('evicted', 'could not evict');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 1.5. Compact: eviction alone only removes provably-mirrored entries: once
|
|
359
|
+
// what remains has crossed 70% of the cap, truncate remaining over-length
|
|
360
|
+
// entries in place too. [[HS-AE28-NYPR-TASK-add-a-compaction-rule-that-runs-when]]
|
|
361
|
+
let compactionNote = null;
|
|
362
|
+
const compaction = compactMemoryIndex(content);
|
|
363
|
+
if (compaction.changed) {
|
|
364
|
+
if (process.env.HEYSPEX_AUTO_MEMORY_SELF_HEAL_OFF === '1') {
|
|
365
|
+
compactionNote = buildCompactionNote(compaction)?.replace('were truncated', 'would be truncated') ?? null;
|
|
366
|
+
} else {
|
|
367
|
+
try {
|
|
368
|
+
fs.writeFileSync(file, compaction.newContent);
|
|
369
|
+
content = compaction.newContent; // advise against the compacted file
|
|
370
|
+
compactionNote = buildCompactionNote(compaction);
|
|
371
|
+
} catch {
|
|
372
|
+
compactionNote = buildCompactionNote(compaction)?.replace('were truncated', 'could not be truncated') ?? null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 2. Advise: budget report for whatever remains.
|
|
378
|
+
const report = buildReport(analyze(content), file);
|
|
379
|
+
const combined = [evictionNote, compactionNote, report].filter(Boolean).join('\n\n') || null;
|
|
380
|
+
emit('SessionStart', combined);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Only run the hook when this file is executed directly (the Claude Code hooks
|
|
384
|
+
// runner spawns it as a subprocess) — NOT on import. Without this guard, a
|
|
385
|
+
// test/gate script importing the pure engine functions above (evictVaultMirrors,
|
|
386
|
+
// compactMemoryIndex, truncateOverLengthEntries) would also trigger main()'s
|
|
387
|
+
// stdin read as a side effect, hanging the importing process
|
|
388
|
+
// ([[HS-STRB-NYPR-TASK-replace-every-inline-copy-of-a]] found the same defect
|
|
389
|
+
// in check-claude-md-budget.mjs).
|
|
390
|
+
const isDirectExecution =
|
|
391
|
+
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
392
|
+
if (isDirectExecution) {
|
|
393
|
+
main().catch(() => {
|
|
394
|
+
// Never break Claude Code on a hook failure.
|
|
395
|
+
});
|
|
396
|
+
}
|