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,190 @@
|
|
|
1
|
+
// spexharness/src/gates/id-display-format-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/id-display-format-gate.mjs — the Stop
|
|
4
|
+
// hook that catches a spec id written to the owner in the disallowed
|
|
5
|
+
// parenthetical-annotation shape (`HS-OFUB-CVOB (epic, ...)` — neither
|
|
6
|
+
// suffixed nor prose-prefixed, per communication-style-protocol.md). See
|
|
7
|
+
// ./PORTING.md for the recipe this follows and ./env-vault-guard.mjs for the
|
|
8
|
+
// reference port.
|
|
9
|
+
//
|
|
10
|
+
// This module does NOT reimplement the detection pattern (AC5's sibling
|
|
11
|
+
// spirit — the pattern is registry-driven, not hand-coded here either). The
|
|
12
|
+
// actual decision comes straight from the shared, already-pure loader:
|
|
13
|
+
// - scripts/claude-hooks/_guardrail-loader.mjs — loadGuardrails,
|
|
14
|
+
// rulesForSurface, isRuleEnabled, compilePatterns, proseOnly,
|
|
15
|
+
// matchContextAround. The `id-display-shape-gate` rule's regex lives in
|
|
16
|
+
// communication-style-protocol.md's `enforcement:` frontmatter, compiled
|
|
17
|
+
// into the registry by build-guardrails.mjs — this module only asks the
|
|
18
|
+
// loader to compile and apply it.
|
|
19
|
+
// - scripts/claude-hooks/_transcript-text.mjs — lastAssistantText, the same
|
|
20
|
+
// shared transcript-text reader naughty-list-gate.mjs's port also uses.
|
|
21
|
+
// Both are dependency-free / no import hazard.
|
|
22
|
+
//
|
|
23
|
+
// JUDGMENT CALL — the original script does NOT export a pure decision
|
|
24
|
+
// function; its logic is tangled inside main()'s stdin read. Per PORTING.md
|
|
25
|
+
// step 1's "otherwise" branch this would normally move into the original
|
|
26
|
+
// script itself, but this task's Do-nots forbid touching anything under
|
|
27
|
+
// scripts/claude-hooks/. What is re-derived here is NOT the detection
|
|
28
|
+
// pattern (that stays imported, per AC5's sibling spirit) — it is pure
|
|
29
|
+
// string-formatting glue (`blockReason`, the warn-stderr message template)
|
|
30
|
+
// copied verbatim from the original's own (unexported) helpers, plus the
|
|
31
|
+
// interactive-rung resolver (`interactiveRung`, reading
|
|
32
|
+
// HEYSPEX_ID_DISPLAY_GATE_INTERACTIVE) copied verbatim too — the identical
|
|
33
|
+
// shape naughty-list-gate.mjs's port already uses for its own rung resolver.
|
|
34
|
+
//
|
|
35
|
+
// Event mapping: Stop → stop. `stdin.transcript_path` → `evt.transcriptPath`.
|
|
36
|
+
//
|
|
37
|
+
// Decision mapping — TWO rungs, the EXACT same shape as naughty-list-gate.mjs's
|
|
38
|
+
// port (`warnOnly = !isAgent && interactiveRung(env) !== 'block'`, byte-for-
|
|
39
|
+
// byte the original's own condition — read carefully, this gate's rung DOES
|
|
40
|
+
// depend on CLAUDE_AGENT, same as its sibling; it is not the "warn regardless
|
|
41
|
+
// of CLAUDE_AGENT" shape a first read of the header's "RUNG" note might
|
|
42
|
+
// suggest — that note is about a *new gate class* starting warn-first, not
|
|
43
|
+
// about ignoring CLAUDE_AGENT). CLAUDE_AGENT=true (or the rung explicitly
|
|
44
|
+
// raised via HEYSPEX_ID_DISPLAY_GATE_INTERACTIVE=block) maps a hit to a real
|
|
45
|
+
// `deny`; the interactive default (agent unset, rung not raised) maps the
|
|
46
|
+
// SAME hit to a `rewrite` envelope (`{ warn: true, reason, hits }`) — the
|
|
47
|
+
// stop is still allowed, matching the original's stderr-only warn path
|
|
48
|
+
// exactly. This gives this gate natural coverage of allow (no hit), deny
|
|
49
|
+
// (agent / raised rung), and rewrite (interactive default) for AC3.
|
|
50
|
+
//
|
|
51
|
+
// Not ported: the original's `appendGuardrailEvent(...)` audit-log write on
|
|
52
|
+
// a hit — matching every other port in this set.
|
|
53
|
+
//
|
|
54
|
+
// Env hatches (unchanged):
|
|
55
|
+
// HEYSPEX_ID_DISPLAY_GATE_OFF=1 Hard kill switch.
|
|
56
|
+
// HEYSPEX_ID_DISPLAY_GATE_INTERACTIVE=block Raise the rung to a real deny.
|
|
57
|
+
// Also honors the rule's own registry configKey
|
|
58
|
+
// (agent_behaviors.id_display_shape_gate.enabled) via isRuleEnabled.
|
|
59
|
+
//
|
|
60
|
+
// `ctx` contract (all optional; every field defaults to a real fs/env read):
|
|
61
|
+
// ctx.cwd - repo root to resolve the guardrail registry from. Defaults to
|
|
62
|
+
// CLAUDE_PROJECT_DIR, else this repo's root. When given it is a
|
|
63
|
+
// self-contained fixture root and the rule's config toggle is read
|
|
64
|
+
// from it too.
|
|
65
|
+
// ctx.configRoot - the directory holding `.heyspex/config.yaml` (the rule's
|
|
66
|
+
// configKey toggle); wins over ctx.cwd for that read. With neither,
|
|
67
|
+
// the VAULT root (../config-root.mjs `defaultConfigRoot()`) — never
|
|
68
|
+
// CLAUDE_PROJECT_DIR, which under Claude Code is the app checkout,
|
|
69
|
+
// not where the settings file lives
|
|
70
|
+
// ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
|
|
71
|
+
// ctx.env - env var overrides (tests use this instead of mutating
|
|
72
|
+
// process.env).
|
|
73
|
+
|
|
74
|
+
import path from 'node:path';
|
|
75
|
+
import { fileURLToPath } from 'node:url';
|
|
76
|
+
|
|
77
|
+
import { lastAssistantText } from '../../../scripts/claude-hooks/_transcript-text.mjs';
|
|
78
|
+
import {
|
|
79
|
+
loadGuardrails,
|
|
80
|
+
rulesForSurface,
|
|
81
|
+
isRuleEnabled,
|
|
82
|
+
compilePatterns,
|
|
83
|
+
proseOnly,
|
|
84
|
+
matchContextAround,
|
|
85
|
+
} from '../../../scripts/claude-hooks/_guardrail-loader.mjs';
|
|
86
|
+
import { defaultConfigRoot } from '../config-root.mjs';
|
|
87
|
+
|
|
88
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
89
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
90
|
+
const RULE_ID = 'id-display-shape-gate';
|
|
91
|
+
|
|
92
|
+
// Copied verbatim from the original's own (unexported) interactiveRung.
|
|
93
|
+
function interactiveRung(env) {
|
|
94
|
+
const raw = String(env.HEYSPEX_ID_DISPLAY_GATE_INTERACTIVE ?? '').trim().toLowerCase();
|
|
95
|
+
return raw === 'block' ? 'block' : 'warn';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Copied verbatim from the original's own (unexported) blockReason.
|
|
99
|
+
function blockReason(hits) {
|
|
100
|
+
const lines = hits.map((h) => ` • "${h.excerpt}"`);
|
|
101
|
+
return (
|
|
102
|
+
`ID display violation: your last message wrote a spec id in a third shape — ` +
|
|
103
|
+
`neither suffixed (HS-OFUB-CVOB-EPIC) nor prose-prefixed (Epic: HS-OFUB-CVOB). ` +
|
|
104
|
+
`Rewrite using one of those two forms and resend:\n${lines.join('\n')}\n` +
|
|
105
|
+
`(Read the type off the spec's own type field, don't guess it. Break-glass: ` +
|
|
106
|
+
`HEYSPEX_ID_DISPLAY_GATE_OFF=1.)`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Copied verbatim from the original's own (unexported) warn message template.
|
|
111
|
+
function warnReason(hits) {
|
|
112
|
+
return (
|
|
113
|
+
`[id-display-format] warning — spec id written in a disallowed third shape (not blocked):\n` +
|
|
114
|
+
hits.map((h) => ` • "${h.excerpt}"`).join('\n') +
|
|
115
|
+
'\n'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The prose this stop event is about. A front that ALREADY holds the reply (`hs chat`, a
|
|
121
|
+
* headless seat, the bench — none of which write a Claude Code transcript file) passes it on
|
|
122
|
+
* the event as `text`, or on the ctx as `reply`; a Claude Code hook shell passes only
|
|
123
|
+
* `transcriptPath` and the text is read from it. Checked in that order, so the same gate runs
|
|
124
|
+
* on every front instead of silently allowing everything on the ones with no transcript --
|
|
125
|
+
* which is exactly what it did before (QC finding, 2026-09-10). The `ctx.reply` name matches
|
|
126
|
+
* enforce-session-sequence.mjs's stop-time corrector, which already took the reply that way.
|
|
127
|
+
*
|
|
128
|
+
* @param {{ text?: string, transcriptPath?: string }} evt
|
|
129
|
+
* @param {{ reply?: string }} ctx
|
|
130
|
+
* @param {(p: string) => string|null} readTranscript
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
function stopEventText(evt, ctx, readTranscript) {
|
|
134
|
+
if (typeof ctx?.reply === 'string' && ctx.reply) return ctx.reply;
|
|
135
|
+
if (typeof evt?.text === 'string' && evt.text) return evt.text;
|
|
136
|
+
const transcriptPath = evt?.transcriptPath;
|
|
137
|
+
if (!transcriptPath) return '';
|
|
138
|
+
return readTranscript(transcriptPath) ?? '';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The id-display-format gate's id — unique across the built-in registry. */
|
|
142
|
+
export const id = 'id-display-format-gate';
|
|
143
|
+
/** Runs on the Stop event, same as its Stop Claude Code hook registration. */
|
|
144
|
+
export const event = 'stop';
|
|
145
|
+
/** Caller-defined severity label; this gate can deny (the raised rung), so it is "block". */
|
|
146
|
+
export const severity = 'block';
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {{ point: string, text?: string, transcriptPath?: string }} evt
|
|
150
|
+
* @param {{ cwd?: string, configRoot?: string, reply?: string, env?: Record<string, string | undefined> }} [ctx]
|
|
151
|
+
* @returns {{ decision: 'allow'|'deny'|'rewrite', reason?: string, rewrite?: object }}
|
|
152
|
+
*/
|
|
153
|
+
export function run(evt, ctx = {}) {
|
|
154
|
+
const env = ctx.env ?? process.env;
|
|
155
|
+
if (String(env.HEYSPEX_ID_DISPLAY_GATE_OFF ?? '') === '1') {
|
|
156
|
+
return { decision: 'allow' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isAgent = env.CLAUDE_AGENT === 'true';
|
|
160
|
+
const warnOnly = !isAgent && interactiveRung(env) !== 'block';
|
|
161
|
+
|
|
162
|
+
const text = stopEventText(evt, ctx, lastAssistantText);
|
|
163
|
+
if (!text) return { decision: 'allow' };
|
|
164
|
+
|
|
165
|
+
const root = ctx.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? REPO_ROOT;
|
|
166
|
+
const configRoot = ctx.configRoot ?? (ctx.cwd ? ctx.cwd : defaultConfigRoot());
|
|
167
|
+
const guardrails = loadGuardrails(root);
|
|
168
|
+
const rule = rulesForSurface(guardrails, 'stop').find((r) => r.rule === RULE_ID);
|
|
169
|
+
if (!rule || !isRuleEnabled(rule, configRoot)) return { decision: 'allow' };
|
|
170
|
+
|
|
171
|
+
const patterns = compilePatterns(rule);
|
|
172
|
+
if (!patterns.length) return { decision: 'allow' };
|
|
173
|
+
|
|
174
|
+
const prose = proseOnly(text);
|
|
175
|
+
const hits = [];
|
|
176
|
+
for (const re of patterns) {
|
|
177
|
+
const m = re.exec(prose);
|
|
178
|
+
if (m) hits.push({ excerpt: matchContextAround(prose, m.index, m[0].length) });
|
|
179
|
+
}
|
|
180
|
+
if (!hits.length) return { decision: 'allow' };
|
|
181
|
+
|
|
182
|
+
if (!warnOnly) {
|
|
183
|
+
return { decision: 'deny', reason: blockReason(hits) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const reason = warnReason(hits);
|
|
187
|
+
return { decision: 'rewrite', reason, rewrite: { warn: true, reason, hits } };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// spexharness/src/gates/inject-doctrine-gate.mjs
|
|
2
|
+
//
|
|
3
|
+
// HS-X3S1 (DR3, epic [[HS-KNHZ-8CMW-EPIC-spexcode-doctrine-v0-the-foundational]]) — the
|
|
4
|
+
// Claude Code plugin's SessionStart injection of the SpexCode Doctrine block, as a
|
|
5
|
+
// registered engine gate (same architecture wave 6 of [[HS-AZ61-NYPR]] set: see
|
|
6
|
+
// dev-server-awareness-gate.mjs's own header for the pattern this follows). Scope 3's own
|
|
7
|
+
// words: "the SessionStart hook prints the same block (the plugin is a thin shell over the
|
|
8
|
+
// same module)" — this module IS that same module (../doctrine/inject.mjs's `loadDoctrine`/
|
|
9
|
+
// `renderDoctrineBlock`); scripts/claude-hooks/inject-doctrine.mjs is the thin shell
|
|
10
|
+
// (`runGateAsHook(gate)`, exactly like inject-vault-context.mjs's own shell) that turns this
|
|
11
|
+
// gate's decision into the Claude Code hook stdout contract. No new MCP tool — Scope 3 is
|
|
12
|
+
// explicit that the block is context, never a tool.
|
|
13
|
+
//
|
|
14
|
+
// Registered under PORTED_GATES (gates/index.mjs) rather than a new top-level group: same
|
|
15
|
+
// session_start point, same reserved band, same composition every front (registerAllGates)
|
|
16
|
+
// already walks — a DeepSeek/local seat gets the Doctrine block through
|
|
17
|
+
// session-bootstrap.mjs's own `doctrine` piece (this task's engine half), and a Claude Code
|
|
18
|
+
// session gets the byte-identical block through THIS gate, so neither front can drift.
|
|
19
|
+
//
|
|
20
|
+
// Decision mapping: always `inject` (never allow-silently) — Scope 1/AC1's "absent (with a
|
|
21
|
+
// notice) when the file is missing" applies here too: a repo with no Doctrine yet still
|
|
22
|
+
// gets the absence-notice text, not nothing, so a Claude Code session is never silently
|
|
23
|
+
// worse-informed than a DeepSeek seat's own bootstrap block would have been.
|
|
24
|
+
//
|
|
25
|
+
// Env hatch: HEYSPEX_DOCTRINE_OFF=1 (kill switch — no built-in rule-table wiring, like the
|
|
26
|
+
// original per-hook `main()`-style scripts' own env-only kill switches).
|
|
27
|
+
//
|
|
28
|
+
// Audience (HS-A2PO, owner ruling 2026-10-03: "yes, inject-doctrine ships to customers.
|
|
29
|
+
// Baseline foundational doctrine on which user's doctrine will be built ontop."): the hook
|
|
30
|
+
// ships in the customer kit. ../doctrine/inject.mjs's resolveDoctrineSource picks the file —
|
|
31
|
+
// the platform checkout (project root == package root) reads its learned
|
|
32
|
+
// SPEXCODE-DOCTRINE.json exactly as before; a customer project reads ONLY the package's
|
|
33
|
+
// baseline edition, and when that is missing or empty the gate allows silently (no absence
|
|
34
|
+
// notice telling a customer to run a learner they never asked for).
|
|
35
|
+
//
|
|
36
|
+
// `ctx` contract (all optional; every field defaults to a real fs/env read):
|
|
37
|
+
// ctx.cwd - project root of the session. Defaults to CLAUDE_PROJECT_DIR, then
|
|
38
|
+
// CURSOR_PROJECT_DIR, then process.cwd().
|
|
39
|
+
// ctx.packageRoot - the heyspex package root. Defaults to the root this module ships in.
|
|
40
|
+
// ctx.env - env var overrides (tests use this instead of mutating process.env).
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
loadDoctrine,
|
|
44
|
+
renderDoctrineBlock,
|
|
45
|
+
DOCTRINE_MAX_TOKENS,
|
|
46
|
+
PACKAGE_ROOT,
|
|
47
|
+
resolveDoctrineSource,
|
|
48
|
+
} from '../doctrine/inject.mjs';
|
|
49
|
+
|
|
50
|
+
const OFF_ENV = 'HEYSPEX_DOCTRINE_OFF';
|
|
51
|
+
|
|
52
|
+
/** The gate's id — unique across the registry. */
|
|
53
|
+
export const id = 'inject-doctrine-gate';
|
|
54
|
+
/** Runs at session start, same as its SessionStart Claude Code hook registration. */
|
|
55
|
+
export const event = 'session_start';
|
|
56
|
+
/** Caller-defined severity label; this gate only ever injects. */
|
|
57
|
+
export const severity = 'info';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{ point: string, cwd?: string, agent?: string }} evt
|
|
61
|
+
* @param {{ cwd?: string, env?: Record<string, string|undefined> }} [ctx]
|
|
62
|
+
* @returns {{ decision: 'allow' }|{ decision: 'inject', reason: string,
|
|
63
|
+
* inject: { additionalContext: string } }}
|
|
64
|
+
*/
|
|
65
|
+
export function run(evt, ctx = {}) {
|
|
66
|
+
const env = ctx.env ?? process.env;
|
|
67
|
+
if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
|
|
68
|
+
|
|
69
|
+
const projectRoot = ctx.cwd ?? env.CLAUDE_PROJECT_DIR ?? env.CURSOR_PROJECT_DIR ?? process.cwd();
|
|
70
|
+
const source = resolveDoctrineSource({ projectRoot, packageRoot: ctx.packageRoot ?? PACKAGE_ROOT });
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
const doctrine = loadDoctrine({ path: source.path });
|
|
74
|
+
const hasPrinciples = Array.isArray(doctrine?.principles) && doctrine.principles.length > 0;
|
|
75
|
+
if (source.audience === 'customer' && !hasPrinciples) return { decision: 'allow' };
|
|
76
|
+
text = renderDoctrineBlock(doctrine, { maxTokens: DOCTRINE_MAX_TOKENS }).text;
|
|
77
|
+
} catch {
|
|
78
|
+
return { decision: 'allow' }; // fail-safe: a broken read never breaks session start
|
|
79
|
+
}
|
|
80
|
+
if (!text) return { decision: 'allow' };
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
decision: 'inject',
|
|
84
|
+
reason: `${id}: SpexCode Doctrine block injected (${source.audience})`,
|
|
85
|
+
inject: { additionalContext: text },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default { id, event, severity, run };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// spexharness/src/gates/inject-vault-context.mjs
|
|
2
|
+
//
|
|
3
|
+
// Engine port of scripts/claude-hooks/inject-vault-context.mjs — the
|
|
4
|
+
// UserPromptSubmit injection of VAULT SEARCH HITS
|
|
5
|
+
// ([[HS-IAJE-2QX6-RES-grounding-agents-against-prior-decisions]],
|
|
6
|
+
// [[HS-5KD3-LTGN-DIAG-prompt-submit-vault-grounding-never]]), wave 6 of
|
|
7
|
+
// [[HS-AZ61-NYPR]]. The rule was Claude-Code-plugin-only, so a DeepSeek or
|
|
8
|
+
// local seat started every prompt from scratch instead of from what the vault
|
|
9
|
+
// had already decided. See ./PORTING.md for the recipe.
|
|
10
|
+
//
|
|
11
|
+
// This module does NOT reimplement the retrieval. Scoring, the prompt-shape
|
|
12
|
+
// gate, the rarity weighting and the character cap all stay in
|
|
13
|
+
// scripts/claude-hooks/_vault-retrieval.mjs — already a pure, shared library
|
|
14
|
+
// with no stdin/stdout of its own — which this gate imports (PORTING.md step 1:
|
|
15
|
+
// "if a pure decision function already exists, import it").
|
|
16
|
+
//
|
|
17
|
+
// P8 (HS-NRCK, plugin parity, epic [[HS-HEBV-8CMW-EPIC-spexcode-profile-v0-learn-the-developer]]):
|
|
18
|
+
// this gate ALSO appends the SpexCode Profile block after its vault-context block, via
|
|
19
|
+
// ../session-bootstrap.mjs's own `defaultProfileLoader` (P4's reader/renderer, never
|
|
20
|
+
// reimplemented here) — so a Claude Code plugin session and a SpexCode seat's own
|
|
21
|
+
// bootstrap see byte-identical profile content. Absence of the profile file (feature off,
|
|
22
|
+
// or never learned) yields no section and no error, same as a fresh SpexCode seat.
|
|
23
|
+
//
|
|
24
|
+
// Event mapping: UserPromptSubmit → `prompt_submit` (../events.mjs).
|
|
25
|
+
//
|
|
26
|
+
// Decision mapping: the hook's only outcomes are "say nothing" and "emit
|
|
27
|
+
// additionalContext". It NEVER rewrites the user's prompt (Claude Code has no
|
|
28
|
+
// field for that, and the system preserves his words verbatim) and never
|
|
29
|
+
// blocks. So: `allow` and `inject` — a PRIME injection, never a deny, which is
|
|
30
|
+
// what this task's brief asks for in so many words.
|
|
31
|
+
//
|
|
32
|
+
// CONTEXT BUDGET ([[HS-1YDJ-NYPR-EPIC-context-budget-every-injection-measured]]).
|
|
33
|
+
// Every byte this gate puts into a seat's window is measured through the ONE
|
|
34
|
+
// measurement path (../context-budget.mjs's `recordContextInjection`, itself
|
|
35
|
+
// built on ../token-inventory.mjs's `measurePiece`) and lands on the session
|
|
36
|
+
// record as a `context_injection` event with `source: "inject-vault-context"` —
|
|
37
|
+
// AC1 of that epic ("the session record shows bytes per injection with a
|
|
38
|
+
// source"). Nothing here re-derives a byte count or writes its own inventory.
|
|
39
|
+
//
|
|
40
|
+
// The epic's AC2 ("under 500 bytes on a plain prompt") is satisfied by the
|
|
41
|
+
// retrieval library's own prompt-shape gate, not by a new cap here: an ordinary
|
|
42
|
+
// work order never reaches the index at all, so the recorded injection for a
|
|
43
|
+
// plain prompt is ZERO bytes — nothing is recorded, because nothing is
|
|
44
|
+
// injected. When the gate DOES fire, `_vault-retrieval.mjs`'s `MAX_CHARS` (600,
|
|
45
|
+
// ~150 tokens) is the hard ceiling, enforced by dropping whole hits rather than
|
|
46
|
+
// cutting a line mid-reference. `ctx.maxChars` can lower that for a front on a
|
|
47
|
+
// tighter budget; it can never raise it above the library's own cap.
|
|
48
|
+
//
|
|
49
|
+
// Not ported HERE: nothing. The original hook writes no audit row (see
|
|
50
|
+
// ../adapters/audit.mjs), so the pure decision is the whole of it.
|
|
51
|
+
//
|
|
52
|
+
// `ctx` contract (all optional; every field defaults to a real read):
|
|
53
|
+
// ctx.repoRoot - the vault/app root the index is read from. Defaults to
|
|
54
|
+
// the event's `cwd`, then CLAUDE_PROJECT_DIR, then this
|
|
55
|
+
// repo's root. When given it is a self-contained fixture
|
|
56
|
+
// root and the config toggle is read from it too.
|
|
57
|
+
// ctx.configRoot - the directory holding `.heyspex/config.yaml` (the
|
|
58
|
+
// TOGGLE_KEY read); wins over ctx.repoRoot for that read.
|
|
59
|
+
// With neither, the VAULT root (../config-root.mjs
|
|
60
|
+
// `defaultConfigRoot()`) — never the event's cwd or
|
|
61
|
+
// CLAUDE_PROJECT_DIR, which under Claude Code is the app
|
|
62
|
+
// checkout, not where the settings file lives
|
|
63
|
+
// ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
|
|
64
|
+
// ctx.sessionRecord - a ../session-record.mjs record; when present the
|
|
65
|
+
// injection is measured onto it.
|
|
66
|
+
// ctx.maxChars - lower the emitted-character ceiling for this front.
|
|
67
|
+
// ctx.retrieve - override the retrieval function (tests).
|
|
68
|
+
// ctx.env - env var overrides (tests use this instead of mutating
|
|
69
|
+
// process.env).
|
|
70
|
+
//
|
|
71
|
+
// Env hatch (unchanged): HEYSPEX_VAULT_CONTEXT_OFF=1.
|
|
72
|
+
// Config toggle (unchanged): agent_behaviors.vault_context.enabled.
|
|
73
|
+
|
|
74
|
+
import path from 'node:path';
|
|
75
|
+
import { fileURLToPath } from 'node:url';
|
|
76
|
+
|
|
77
|
+
import { retrieveForPrompt, MAX_CHARS } from '../../../scripts/claude-hooks/_vault-retrieval.mjs';
|
|
78
|
+
import { readConfigLeafRaw } from '../../../scripts/claude-hooks/_guardrail-loader.mjs';
|
|
79
|
+
import { recordContextInjection } from '../context-budget.mjs';
|
|
80
|
+
import { defaultProfileLoader } from '../session-bootstrap.mjs';
|
|
81
|
+
import { defaultConfigRoot } from '../config-root.mjs';
|
|
82
|
+
|
|
83
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
84
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..', '..');
|
|
85
|
+
|
|
86
|
+
const OFF_ENV = 'HEYSPEX_VAULT_CONTEXT_OFF';
|
|
87
|
+
|
|
88
|
+
/** The `.heyspex/config.yaml` leaf that turns this injection off. */
|
|
89
|
+
export const TOGGLE_KEY = 'agent_behaviors.vault_context.enabled';
|
|
90
|
+
|
|
91
|
+
/** Trivially short prompts ("yes", "ok", "go ahead") carry nothing to retrieve on. */
|
|
92
|
+
export const MIN_PROMPT_CHARS = 20;
|
|
93
|
+
|
|
94
|
+
/** The `source` every measurement this gate records is stamped with. */
|
|
95
|
+
export const INJECTION_SOURCE = 'inject-vault-context';
|
|
96
|
+
|
|
97
|
+
/** The `source` the SpexCode Profile block (P8, HS-NRCK) is measured under — same
|
|
98
|
+
* convention P4's own registerProfileContextUsage uses for a SpexCode seat's bootstrap. */
|
|
99
|
+
export const PROFILE_INJECTION_SOURCE = 'spexcode-profile';
|
|
100
|
+
|
|
101
|
+
/** This gate's id — unique across the registry. */
|
|
102
|
+
export const id = 'inject-vault-context';
|
|
103
|
+
/** Runs at prompt submit, same as its UserPromptSubmit hook registration. */
|
|
104
|
+
export const event = 'prompt_submit';
|
|
105
|
+
/** Caller-defined severity label; a prime injection, never a block. */
|
|
106
|
+
export const severity = 'prime';
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The injected-content envelope guard: a prompt that is itself injected context
|
|
110
|
+
* from another gate must never be re-searched, or the block feeds on itself.
|
|
111
|
+
* Copied intact from the original hook.
|
|
112
|
+
*/
|
|
113
|
+
export function looksInjected(prompt) {
|
|
114
|
+
return /^(HEYSPEX|OWNER COMMUNICATION STYLE|VAULT CONTEXT)\b/.test(String(prompt ?? '').trim());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveRoot(evt, ctx) {
|
|
118
|
+
if (typeof ctx.repoRoot === 'string' && ctx.repoRoot.trim()) {
|
|
119
|
+
return ctx.repoRoot.trim().replace(/[\\/]+$/, '');
|
|
120
|
+
}
|
|
121
|
+
if (typeof evt?.cwd === 'string' && evt.cwd.trim()) return evt.cwd.trim().replace(/[\\/]+$/, '');
|
|
122
|
+
const env = ctx.env ?? process.env;
|
|
123
|
+
const fromEnv = env.CLAUDE_PROJECT_DIR?.trim();
|
|
124
|
+
if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
|
|
125
|
+
return REPO_ROOT;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {{ point: string, prompt?: string, cwd?: string }} evt
|
|
130
|
+
* @param {{ repoRoot?: string, configRoot?: string, sessionRecord?: object, maxChars?: number,
|
|
131
|
+
* retrieve?: (prompt: string, root: string, options: object) => (string|null),
|
|
132
|
+
* env?: Record<string, string | undefined> }} [ctx]
|
|
133
|
+
* @returns {{ decision: 'allow'|'inject',
|
|
134
|
+
* inject?: { additionalContext: string, severity: string },
|
|
135
|
+
* measurement?: { source: string, name: string, tokens: number, chars: number } }}
|
|
136
|
+
*/
|
|
137
|
+
export function run(evt, ctx = {}) {
|
|
138
|
+
const env = ctx.env ?? process.env;
|
|
139
|
+
if (String(env[OFF_ENV] ?? '') === '1') return { decision: 'allow' };
|
|
140
|
+
|
|
141
|
+
const root = resolveRoot(evt, ctx);
|
|
142
|
+
// Only a ctx-supplied root is a fixture; the event's cwd / CLAUDE_PROJECT_DIR are the live
|
|
143
|
+
// app checkout and never decide where the settings file is read from.
|
|
144
|
+
const fixtureRoot = typeof ctx.repoRoot === 'string' && ctx.repoRoot.trim();
|
|
145
|
+
const configRoot = ctx.configRoot ?? (fixtureRoot ? root : defaultConfigRoot());
|
|
146
|
+
|
|
147
|
+
// ── vault-context retrieval (unchanged decision logic) ──────────────────────────
|
|
148
|
+
// Config toggle, default ON when the key is absent (same semantics as the hook).
|
|
149
|
+
const vaultContextEnabled = !(() => {
|
|
150
|
+
const enabled = readConfigLeafRaw(TOGGLE_KEY, configRoot);
|
|
151
|
+
return enabled === 'false' || enabled === false;
|
|
152
|
+
})();
|
|
153
|
+
|
|
154
|
+
let vaultBlock = null;
|
|
155
|
+
if (vaultContextEnabled) {
|
|
156
|
+
const prompt = evt?.prompt;
|
|
157
|
+
if (typeof prompt === 'string' && prompt.trim().length >= MIN_PROMPT_CHARS && !looksInjected(prompt)) {
|
|
158
|
+
const maxChars = Number.isFinite(ctx.maxChars)
|
|
159
|
+
? Math.min(Number(ctx.maxChars), MAX_CHARS)
|
|
160
|
+
: MAX_CHARS;
|
|
161
|
+
const retrieve = ctx.retrieve ?? retrieveForPrompt;
|
|
162
|
+
try {
|
|
163
|
+
vaultBlock = retrieve(prompt, root, { maxChars });
|
|
164
|
+
} catch {
|
|
165
|
+
// A crashed retrieval injects nothing — never the reason a turn breaks.
|
|
166
|
+
vaultBlock = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The ONE measurement path (HS-7UAQ / epic HS-1YDJ). A front with no session
|
|
172
|
+
// record still gets the bytes back on the decision; nothing measures twice.
|
|
173
|
+
const vaultMeasurement = vaultBlock
|
|
174
|
+
? ctx.sessionRecord
|
|
175
|
+
? recordContextInjection(ctx.sessionRecord, {
|
|
176
|
+
source: INJECTION_SOURCE,
|
|
177
|
+
name: 'vault-context',
|
|
178
|
+
text: vaultBlock,
|
|
179
|
+
})
|
|
180
|
+
: {
|
|
181
|
+
source: INJECTION_SOURCE,
|
|
182
|
+
name: 'vault-context',
|
|
183
|
+
chars: vaultBlock.length,
|
|
184
|
+
tokens: Math.ceil(vaultBlock.length / 4),
|
|
185
|
+
estimated: true,
|
|
186
|
+
}
|
|
187
|
+
: null;
|
|
188
|
+
|
|
189
|
+
// ── SpexCode Profile parity (P8, HS-NRCK) ────────────────────────────────────────
|
|
190
|
+
// Thin shell over session-bootstrap.mjs's own `defaultProfileLoader` (P4) — the SAME
|
|
191
|
+
// reader/renderer every SpexCode seat's bootstrap uses; never a second rendering
|
|
192
|
+
// implementation. Absence of the profile file (feature off, or never learned) yields
|
|
193
|
+
// `null` here, same as it does for a fresh SpexCode seat — no section, no error (AC2).
|
|
194
|
+
// Positioned AFTER the vault-context block: Doctrine itself already rode in earlier, at
|
|
195
|
+
// SessionStart, via its own hook (inject-doctrine.mjs/inject-doctrine-gate.mjs), so the
|
|
196
|
+
// developer-visible order across a session stays enforced -> learned Doctrine ->
|
|
197
|
+
// per-developer Profile exactly as session-bootstrap.mjs's BOOTSTRAP_PIECES documents.
|
|
198
|
+
let profile = null;
|
|
199
|
+
try {
|
|
200
|
+
profile = defaultProfileLoader({ root });
|
|
201
|
+
} catch {
|
|
202
|
+
// A broken profile read never breaks the turn — same fail-soft posture as retrieval.
|
|
203
|
+
profile = null;
|
|
204
|
+
}
|
|
205
|
+
const profileBlock = profile && typeof profile.text === 'string' && profile.text.trim() ? profile.text : null;
|
|
206
|
+
|
|
207
|
+
const profileMeasurement = profileBlock
|
|
208
|
+
? ctx.sessionRecord
|
|
209
|
+
? recordContextInjection(ctx.sessionRecord, {
|
|
210
|
+
source: PROFILE_INJECTION_SOURCE,
|
|
211
|
+
name: 'spexcode-profile',
|
|
212
|
+
text: profileBlock,
|
|
213
|
+
tokens: profile.tokens,
|
|
214
|
+
})
|
|
215
|
+
: {
|
|
216
|
+
source: PROFILE_INJECTION_SOURCE,
|
|
217
|
+
name: 'spexcode-profile',
|
|
218
|
+
chars: profile.chars,
|
|
219
|
+
tokens: profile.tokens,
|
|
220
|
+
estimated: true,
|
|
221
|
+
}
|
|
222
|
+
: null;
|
|
223
|
+
|
|
224
|
+
if (!vaultBlock && !profileBlock) return { decision: 'allow' };
|
|
225
|
+
|
|
226
|
+
const additionalContext = [vaultBlock, profileBlock].filter(Boolean).join('\n\n');
|
|
227
|
+
const measurements = [vaultMeasurement, profileMeasurement].filter(Boolean);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
decision: 'inject',
|
|
231
|
+
inject: { additionalContext, severity: 'prime' },
|
|
232
|
+
// Single measurement, unchanged shape, when only one piece fired (the common case and
|
|
233
|
+
// every pre-existing caller's expectation); an array only when both fired in the same
|
|
234
|
+
// turn.
|
|
235
|
+
measurement: measurements.length <= 1 ? measurements[0] ?? null : measurements,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default { id, event, severity, run };
|