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,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code PostToolUse hook → ExitPlanMode reminder + session marker.
|
|
3
|
+
//
|
|
4
|
+
// [[HS-38J5-2BXD-DIAG-plan-execute-boundary-has-no-ingest-clai]] — the
|
|
5
|
+
// ExitPlanMode PreToolUse gate (plan-review-gate.mjs) hard-gates plan QUALITY
|
|
6
|
+
// (V1 review + QC), but nothing gated plan → execute: an approved plan let the
|
|
7
|
+
// agent go straight to coding with no epic filed and no claim acquired. This is
|
|
8
|
+
// item 1 of the diagnose's REMAINING fix list — the "soft (unavoidable)" half:
|
|
9
|
+
// rides the ONE event guaranteed to fire once a plan is approved (PostToolUse
|
|
10
|
+
// on ExitPlanMode), and fail-silent (never blocks — plan-review-gate already
|
|
11
|
+
// owns the hard quality gate on this same tool).
|
|
12
|
+
//
|
|
13
|
+
// It does two things on every ExitPlanMode PostToolUse:
|
|
14
|
+
// 1. Emits an unavoidable `additionalContext` reminder: ingest the approved
|
|
15
|
+
// plan as an epic (plan-ingest-agent-workflow) and claim before coding.
|
|
16
|
+
// 2. Arms a per-session marker (.heyspex/post-plan-pending/<sessionId>.json)
|
|
17
|
+
// that claim-before-code-gate.mjs (the PreToolUse Edit|Write HARD gate,
|
|
18
|
+
// item 2 of the same diagnose) reads to know "this session just exited
|
|
19
|
+
// plan mode" before deciding whether to deny the first code edit.
|
|
20
|
+
//
|
|
21
|
+
// The marker is cleared by claim-before-code-gate.mjs once a claim is observed
|
|
22
|
+
// (or left to age out — it is advisory only, never itself a security boundary).
|
|
23
|
+
//
|
|
24
|
+
// No CLAUDE_AGENT gate ([[HS-DT9L-NLR7-DIAG-nine-hooks-are-switched-off-in]]).
|
|
25
|
+
// This hook never denies anything — it only emits a reminder and arms a marker file
|
|
26
|
+
// — so there is no approval-fatigue risk in firing it everywhere. Removing the gate
|
|
27
|
+
// also unblocks claim-before-code-gate.mjs, which reads the marker this hook arms:
|
|
28
|
+
// with this hook silent in interactive sessions, the marker never existed there, so
|
|
29
|
+
// that gate was doubly inert regardless of its own env gate.
|
|
30
|
+
//
|
|
31
|
+
// Env gate:
|
|
32
|
+
// HEYSPEX_PLAN_EXIT_GATE_OFF=1 Hard kill switch.
|
|
33
|
+
//
|
|
34
|
+
// MUST exit 0 on every failure path — a reminder that breaks Claude Code is
|
|
35
|
+
// worse than no reminder.
|
|
36
|
+
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
|
|
41
|
+
import { resolveSessionId } from './claim-liveness-shared.mjs';
|
|
42
|
+
|
|
43
|
+
export function repoRoot() {
|
|
44
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR?.trim();
|
|
45
|
+
if (fromEnv) return fromEnv.replace(/[\\/]+$/, '');
|
|
46
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MARKER_DIR = 'post-plan-pending';
|
|
50
|
+
// Advisory TTL — long enough to cover a slow ingest+claim, short enough that a
|
|
51
|
+
// marker from an old plan/session can't linger forever. Not a security boundary
|
|
52
|
+
// (claim-before-code-gate's actual deny decision comes from the claim-liveness
|
|
53
|
+
// negative cache, which has its own TTL) — this only decides whether the "did
|
|
54
|
+
// this session just exit plan mode" nudge/gate applies at all.
|
|
55
|
+
export const MARKER_TTL_MS = 2 * 60 * 60 * 1000; // 2h
|
|
56
|
+
|
|
57
|
+
function markerFile(sessionId, root) {
|
|
58
|
+
return path.join(root, '.heyspex', MARKER_DIR, `${sessionId}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Arm the per-session "just exited plan mode" marker. Best-effort; never throws. */
|
|
62
|
+
export function armPlanExitMarker(sessionId, root = repoRoot()) {
|
|
63
|
+
const trimmed = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
64
|
+
if (!trimmed) return;
|
|
65
|
+
try {
|
|
66
|
+
const file = markerFile(trimmed, root);
|
|
67
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
68
|
+
fs.writeFileSync(file, JSON.stringify({ armedAt: Date.now(), sessionId: trimmed }));
|
|
69
|
+
} catch {
|
|
70
|
+
// never block the hook
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when `sessionId` has a live (unexpired) plan-exit marker. Never throws. */
|
|
75
|
+
export function hasFreshPlanExitMarker(sessionId, root = repoRoot()) {
|
|
76
|
+
const trimmed = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
77
|
+
if (!trimmed) return false;
|
|
78
|
+
try {
|
|
79
|
+
const raw = JSON.parse(fs.readFileSync(markerFile(trimmed, root), 'utf8'));
|
|
80
|
+
const armedAt = typeof raw?.armedAt === 'number' ? raw.armedAt : 0;
|
|
81
|
+
return armedAt > 0 && Date.now() - armedAt < MARKER_TTL_MS;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Clear the marker (called once a claim is acquired — no more nudging needed). */
|
|
88
|
+
export function clearPlanExitMarker(sessionId, root = repoRoot()) {
|
|
89
|
+
const trimmed = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
90
|
+
if (!trimmed) return;
|
|
91
|
+
try {
|
|
92
|
+
fs.unlinkSync(markerFile(trimmed, root));
|
|
93
|
+
} catch {
|
|
94
|
+
// absent is fine
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const PLAN_EXIT_REMINDER =
|
|
99
|
+
'HEYSPEX — plan approved (ExitPlanMode). Before writing any code: (1) ingest the ' +
|
|
100
|
+
'approved plan into the vault — heyspex_preflight({ intent: "file_epic" }) then ' +
|
|
101
|
+
'file the epic (heyspex_file_spec / heyspex_push_plan) per plan-ingest-agent-workflow ' +
|
|
102
|
+
'(heyspex_get_procedure({ name: "plan-ingest-agent-workflow" })) — unless the plan is a ' +
|
|
103
|
+
'trivial fix with no new spec warranted; (2) claim the spec you are about to work ' +
|
|
104
|
+
'(heyspex_claim_spec) before the first non-plan Edit/Write. The plan→execute boundary ' +
|
|
105
|
+
'has no other enforcement besides this reminder and the PreToolUse claim gate on ' +
|
|
106
|
+
'code edits — do not skip filing+claiming because the plan already passed review.';
|
|
107
|
+
|
|
108
|
+
async function readStdinJson() {
|
|
109
|
+
if (process.stdin.isTTY) return null;
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
let buf = '';
|
|
112
|
+
let settled = false;
|
|
113
|
+
const finish = (value) => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
settled = true;
|
|
116
|
+
resolve(value);
|
|
117
|
+
};
|
|
118
|
+
const timer = setTimeout(() => finish(buf || ''), 200);
|
|
119
|
+
process.stdin.setEncoding('utf8');
|
|
120
|
+
process.stdin.on('data', (chunk) => {
|
|
121
|
+
buf += chunk;
|
|
122
|
+
});
|
|
123
|
+
process.stdin.on('end', () => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
finish(buf);
|
|
126
|
+
});
|
|
127
|
+
process.stdin.on('error', () => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
finish(buf);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function emit(additionalContext) {
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext },
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function main() {
|
|
143
|
+
if (process.env.HEYSPEX_PLAN_EXIT_GATE_OFF === '1') return;
|
|
144
|
+
|
|
145
|
+
const raw = await readStdinJson();
|
|
146
|
+
if (!raw) return;
|
|
147
|
+
let payload;
|
|
148
|
+
try {
|
|
149
|
+
payload = JSON.parse(raw);
|
|
150
|
+
} catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (payload?.hook_event_name !== 'PostToolUse') return;
|
|
154
|
+
if (payload?.tool_name !== 'ExitPlanMode') return;
|
|
155
|
+
// A denied/errored ExitPlanMode (e.g. plan-review-gate blocked it) never actually
|
|
156
|
+
// left plan mode — nothing to arm or remind about.
|
|
157
|
+
if (payload?.tool_response?.is_error === true) return;
|
|
158
|
+
|
|
159
|
+
const sessionId = resolveSessionId(payload);
|
|
160
|
+
armPlanExitMarker(sessionId);
|
|
161
|
+
emit(PLAN_EXIT_REMINDER);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const INVOKED_DIRECTLY =
|
|
165
|
+
!!process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
166
|
+
if (INVOKED_DIRECTLY) {
|
|
167
|
+
main().catch(() => {
|
|
168
|
+
// Never break Claude Code on a hook failure.
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// UserPromptSubmit hook → after plan mode, file specs and close — do not implement.
|
|
3
|
+
//
|
|
4
|
+
// Command Code / harnesses default to "exit plan → implement". HeySpex plan-ingest
|
|
5
|
+
// sessions file milestone+epic specs via MCP, validate linkage, then end session.
|
|
6
|
+
// Fires on distinctive plan-exit phrasing in the user prompt or agent narration.
|
|
7
|
+
//
|
|
8
|
+
// Invoke via native UserPromptSubmit (Claude Code) or heyspex_hooks_invoke
|
|
9
|
+
// ({ event: "UserPromptSubmit", prompt: "..." }) in Command Code.
|
|
10
|
+
//
|
|
11
|
+
// FAIL-SAFE: exit 0 on every path.
|
|
12
|
+
|
|
13
|
+
const PLAN_EXIT_PATTERNS = [
|
|
14
|
+
/\bexited?\s+plan\s+mode\b/i,
|
|
15
|
+
/\bPLAN\s*\(\s*exit\s*\)/i,
|
|
16
|
+
/\bexit\s+plan\s+mode\b/i,
|
|
17
|
+
/\bnow\s+implementing\b/i,
|
|
18
|
+
/\bleaving\s+plan\s+mode\b/i,
|
|
19
|
+
/\bplan\s+mode\s+ended\b/i,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const INJECT =
|
|
23
|
+
'HeySpex plan-ingest close: you just left plan mode. Do NOT start coding. ' +
|
|
24
|
+
'File milestones and epics via heyspex_push_plan / heyspex_file_spec, run ' +
|
|
25
|
+
'heyspex_validate_linkage, confirm summaries, then close with ' +
|
|
26
|
+
'heyspex_preflight({ intent: "close_session", thread_type: "plan_ingest" }) ' +
|
|
27
|
+
'and heyspex_session_end({ reason: "plan_ingest_complete" }). ' +
|
|
28
|
+
'Task filing and code execution are a separate session via /hs-next.';
|
|
29
|
+
|
|
30
|
+
async function readStdinJson() {
|
|
31
|
+
if (process.stdin.isTTY) return null;
|
|
32
|
+
let buf = '';
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
for await (const chunk of process.stdin) buf += chunk;
|
|
35
|
+
if (!buf.trim()) return null;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(buf);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emit(additionalContext) {
|
|
44
|
+
process.stdout.write(JSON.stringify({
|
|
45
|
+
hookSpecificOutput: {
|
|
46
|
+
hookEventName: 'UserPromptSubmit',
|
|
47
|
+
additionalContext,
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
if (process.env.HEYSPEX_PLAN_INGEST_REMINDER_OFF === '1') return;
|
|
54
|
+
|
|
55
|
+
const payload = await readStdinJson();
|
|
56
|
+
if (!payload || payload.hook_event_name !== 'UserPromptSubmit') return;
|
|
57
|
+
|
|
58
|
+
const prompt = payload.prompt ?? payload.tool_input?.prompt ?? '';
|
|
59
|
+
if (typeof prompt !== 'string' || !prompt.trim()) return;
|
|
60
|
+
|
|
61
|
+
if (!PLAN_EXIT_PATTERNS.some((re) => re.test(prompt))) return;
|
|
62
|
+
|
|
63
|
+
emit(INJECT);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main().catch(() => {});
|
|
@@ -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/plan-review-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/plan-review-gate.mjs';
|
|
10
|
+
|
|
11
|
+
await runGateAsHook(gate);
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code hook → inject preflight reminders for high-stakes intents.
|
|
3
|
+
//
|
|
4
|
+
// Phase 3 of HS-EPIC-3QS8 (procedure discovery & enforcement). Fires on
|
|
5
|
+
// UserPromptSubmit and PreToolUse to nudge the agent toward heyspex_preflight /
|
|
6
|
+
// heyspex_get_procedure / destructive-action-checklist at the moment of need —
|
|
7
|
+
// even when the agent forgets to ask.
|
|
8
|
+
//
|
|
9
|
+
// Three intent categories:
|
|
10
|
+
// 1. UserPromptSubmit → user says "file a milestone" / "create an epic" /
|
|
11
|
+
// "file a new task" → the registry-driven
|
|
12
|
+
// spec-creation-via-minter prime ([[HS-EPIC-ZGWR]] AC#6):
|
|
13
|
+
// patterns + inject live in the guardrail registry, not here.
|
|
14
|
+
// 2. PreToolUse Edit → file_path under specs/protocols/ → protocol_edit
|
|
15
|
+
// reminder (spec-hierarchy §Agent autonomy).
|
|
16
|
+
// 3. PreToolUse Bash/PowerShell → command contains rm -rf, git push --force,
|
|
17
|
+
// git reset --hard, Remove-Item -Recurse -Force →
|
|
18
|
+
// destructive-action-checklist reminder.
|
|
19
|
+
//
|
|
20
|
+
// Env gate (per HS-EPIC-3QS8 resolved Open question on "Hook scope"):
|
|
21
|
+
// CLAUDE_AGENT=true Set to opt in. When unset, the hook exits 0 silently so
|
|
22
|
+
// humans typing the same triggers in an interactive
|
|
23
|
+
// Claude Code session don't get spammed.
|
|
24
|
+
// HEYSPEX_PREFLIGHT_REMINDER_OFF=1 Hard kill switch — exits silently even
|
|
25
|
+
// when CLAUDE_AGENT=true. Useful for parallel agents that
|
|
26
|
+
// want to mute reminders without touching the gate.
|
|
27
|
+
//
|
|
28
|
+
// MUST exit 0 on every failure path (matches the [[claude-code-hooks-integration]]
|
|
29
|
+
// fail-silent contract). A reminder that breaks Claude Code is worse than no
|
|
30
|
+
// reminder.
|
|
31
|
+
//
|
|
32
|
+
// Output shape:
|
|
33
|
+
// { hookSpecificOutput: { hookEventName, additionalContext } }
|
|
34
|
+
// Plain JSON to stdout; Claude Code injects additionalContext into the model's
|
|
35
|
+
// next turn alongside the prompt or tool result.
|
|
36
|
+
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
|
|
40
|
+
// HS-N01T: the UserPromptSubmit half (the registry-driven filing-intent prime and the
|
|
41
|
+
// grounding nudge) is PORTED to the engine — spexharness/src/gates/preflight-reminder-gate.mjs
|
|
42
|
+
// — and this hook is now the thin Claude Code shell over it: same decision function, same
|
|
43
|
+
// reminder text, same per-rule config/injected-content gating. What stays here is the
|
|
44
|
+
// plumbing (stdin, the CLAUDE_AGENT opt-in gate, the single emit) and the PreToolUse half.
|
|
45
|
+
import {
|
|
46
|
+
detectRegistryPrimeReminder as detectPrimeReminderPorted,
|
|
47
|
+
detectDesignIntent,
|
|
48
|
+
PRIME_RULES_OWNED_ELSEWHERE,
|
|
49
|
+
} from '../../spexharness/src/gates/preflight-reminder-gate.mjs';
|
|
50
|
+
import { defaultConfigRoot } from '../../spexharness/src/config-root.mjs';
|
|
51
|
+
|
|
52
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
53
|
+
|
|
54
|
+
// Filing-intent priming is REGISTRY-DRIVEN ([[HS-EPIC-ZGWR]] AC#6): the prime rules on
|
|
55
|
+
// the userpromptsubmit surface (e.g. spec-creation-via-minter) carry their patterns +
|
|
56
|
+
// inject text in specs/protocols/*.md → .heyspex/guardrails.json, so editing the nudge
|
|
57
|
+
// is a prose edit with zero JS change. research-before-decision is a userpromptsubmit
|
|
58
|
+
// prime rule too, but it has its own dedicated dispatch (via the unified
|
|
59
|
+
// intent-classifier-trigger.mjs router; the old standalone research-trigger.mjs shim
|
|
60
|
+
// was retired after burn-in, HS-IYU1) that does
|
|
61
|
+
// extra workflow orchestration — exclude it here so it never double-injects. That
|
|
62
|
+
// exclusion set (`PRIME_RULES_OWNED_ELSEWHERE`) now lives in the engine gate and is passed
|
|
63
|
+
// back in explicitly below, so this hook's sweep is unchanged.
|
|
64
|
+
|
|
65
|
+
// Destructive shell patterns. Conservative — only patterns that map to real
|
|
66
|
+
// data loss / shared-state mutation, not arbitrary "dangerous-looking" calls.
|
|
67
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
68
|
+
// POSIX rm with recursive force in any flag order
|
|
69
|
+
/\brm\s+(?:-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*|--recursive\s+--force|--force\s+--recursive)\b/,
|
|
70
|
+
// git force-push (--force, -f, --force-with-lease)
|
|
71
|
+
/\bgit\s+push\s+(?:.*\s+)?(?:--force\b|-f\b|--force-with-lease\b)/,
|
|
72
|
+
// git reset --hard
|
|
73
|
+
/\bgit\s+reset\s+--hard\b/,
|
|
74
|
+
// git clean -fd / -fdx (untracked + ignored)
|
|
75
|
+
/\bgit\s+clean\s+(?:-[a-zA-Z]*f[a-zA-Z]*d[a-zA-Z]*|-[a-zA-Z]*d[a-zA-Z]*f[a-zA-Z]*)/,
|
|
76
|
+
// git branch -D / branch --delete --force
|
|
77
|
+
/\bgit\s+branch\s+(?:.*\s+)?(?:-D\b|--delete\s+--force\b|-d\s+--force\b)/,
|
|
78
|
+
// PowerShell Remove-Item -Recurse -Force
|
|
79
|
+
/\bRemove-Item\s+(?:[^|;&\n]*\s)?-Recurse\b(?:[^|;&\n]*\s)?-Force\b/i,
|
|
80
|
+
/\bRemove-Item\s+(?:[^|;&\n]*\s)?-Force\b(?:[^|;&\n]*\s)?-Recurse\b/i,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const PROTOCOL_EDIT_REGEX = /(?:^|[\\/])specs[\\/]protocols[\\/][^\\/]+\.md$/i;
|
|
84
|
+
|
|
85
|
+
// CLAUDE.md edit gate ([[HS-UFQS-5PQ2-DIAG]]): CLAUDE.md is the always-loaded
|
|
86
|
+
// agent contract — a decision recorded correctly in a protocol/memory doc that
|
|
87
|
+
// also gets INLINED into CLAUDE.md silently wins once the protocol drifts,
|
|
88
|
+
// because CLAUDE.md is what every session actually reads. Fires on the FILE
|
|
89
|
+
// being edited (any CLAUDE.md, root or a nested customer-vault scaffold), not
|
|
90
|
+
// on a folder pattern — mirrors detectProtocolEdit above.
|
|
91
|
+
const CLAUDE_MD_EDIT_REGEX = /(?:^|[\\/])CLAUDE\.md$/;
|
|
92
|
+
|
|
93
|
+
const DESTRUCTIVE_REMINDER =
|
|
94
|
+
'Detected destructive shell command (rm -rf, force-push, reset --hard, or ' +
|
|
95
|
+
'equivalent). Walk [[destructive-action-checklist]] before proceeding — ' +
|
|
96
|
+
'`heyspex_get_procedure({ name: "destructive-action-checklist" })` returns the ' +
|
|
97
|
+
'full checklist. If this is a non-vault scratch path or sandbox, you can ' +
|
|
98
|
+
'proceed; otherwise confirm with the user.';
|
|
99
|
+
|
|
100
|
+
const PROTOCOL_EDIT_REMINDER =
|
|
101
|
+
'Detected an Edit on a file under `specs/protocols/`. Protocol edits require ' +
|
|
102
|
+
'user approval per spec-hierarchy §Agent autonomy. Fetch the full rule with ' +
|
|
103
|
+
'`heyspex_get_procedure({ name: "spec-hierarchy" })` and confirm scope with ' +
|
|
104
|
+
'the user before saving.';
|
|
105
|
+
|
|
106
|
+
const CLAUDE_MD_EDIT_REMINDER =
|
|
107
|
+
'Detected an Edit/Write on CLAUDE.md — the always-loaded agent contract. Before ' +
|
|
108
|
+
'saving, check `heyspex_preflight({ intent: "protocol_edit" })` and ' +
|
|
109
|
+
'`heyspex_get_procedure({ name: "rule-placement-protocol" })`: procedure content ' +
|
|
110
|
+
'ranks ABOVE CLAUDE.md (CLAUDE.md is rank 4 — last resort, load-bearing handshake ' +
|
|
111
|
+
'rules only). Detail belongs in a `specs/protocols/*.md` procedure, fetched on ' +
|
|
112
|
+
'demand — never inlined here. An inlined copy silently overrides the live ' +
|
|
113
|
+
'procedure once it drifts, because CLAUDE.md is what every session actually reads ' +
|
|
114
|
+
'([[HS-UFQS-5PQ2-DIAG]]). If you are adding detail rather than a one-line pointer, ' +
|
|
115
|
+
'stop and move it to a procedure instead.';
|
|
116
|
+
|
|
117
|
+
// Grounding-before-proposing nudge ([[HS-RES-2QX6]] / [[HS-EPIC-PLRV]]): the patterns and
|
|
118
|
+
// GROUNDING_REMINDER text live in the engine gate (DESIGN_INTENT_PATTERNS /
|
|
119
|
+
// GROUNDING_REMINDER there); `detectDesignIntent` is imported above.
|
|
120
|
+
|
|
121
|
+
async function readStdinJson() {
|
|
122
|
+
if (process.stdin.isTTY) return null;
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
let buf = '';
|
|
125
|
+
let settled = false;
|
|
126
|
+
const finish = (value) => {
|
|
127
|
+
if (settled) return;
|
|
128
|
+
settled = true;
|
|
129
|
+
resolve(value);
|
|
130
|
+
};
|
|
131
|
+
const timer = setTimeout(() => finish(buf || ''), 200);
|
|
132
|
+
process.stdin.setEncoding('utf8');
|
|
133
|
+
process.stdin.on('data', (chunk) => {
|
|
134
|
+
buf += chunk;
|
|
135
|
+
});
|
|
136
|
+
process.stdin.on('end', () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
finish(buf);
|
|
139
|
+
});
|
|
140
|
+
process.stdin.on('error', () => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
finish(buf);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function emit(eventName, additionalContext) {
|
|
148
|
+
if (!additionalContext) return;
|
|
149
|
+
const out = {
|
|
150
|
+
hookSpecificOutput: {
|
|
151
|
+
hookEventName: eventName,
|
|
152
|
+
additionalContext,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
process.stdout.write(JSON.stringify(out));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Registry-driven filing-intent prime ([[HS-EPIC-ZGWR]] AC#6). Reads the
|
|
159
|
+
// userpromptsubmit `prime` rules from the compiled guardrail registry, matches the
|
|
160
|
+
// prompt against each rule's authored patterns, and returns the rule's inject text.
|
|
161
|
+
// Honors each rule's config_key gate + ignore_injected_content, and skips rules owned
|
|
162
|
+
// by a dedicated hook. Fail-safe: any error → null (no reminder, never throws).
|
|
163
|
+
// Exported so the smoke can unit-test the config/injected-content behavior in-process.
|
|
164
|
+
// HS-N01T: the body is the engine gate's; this hook passes its OWN exclusion set (the
|
|
165
|
+
// engine's default is wider — see the gate's header) so the hook's sweep is byte-identical.
|
|
166
|
+
// HS-4C76: the registry (guardrails.json) stays on the code root, but the per-rule
|
|
167
|
+
// config_key toggles live in `.heyspex/config.yaml` at the VAULT root. A caller passing
|
|
168
|
+
// an explicit fixture root (the smoke) gets both from that root; the live path resolves
|
|
169
|
+
// the config root through the vault resolver.
|
|
170
|
+
export function detectRegistryPrimeReminder(
|
|
171
|
+
prompt,
|
|
172
|
+
repoRoot = REPO_ROOT,
|
|
173
|
+
configRoot = repoRoot === REPO_ROOT ? defaultConfigRoot() : repoRoot,
|
|
174
|
+
) {
|
|
175
|
+
return detectPrimeReminderPorted(prompt, repoRoot, {
|
|
176
|
+
ownedElsewhere: PRIME_RULES_OWNED_ELSEWHERE,
|
|
177
|
+
configRoot,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function detectProtocolEdit(toolName, toolInput) {
|
|
182
|
+
if (toolName !== 'Edit' && toolName !== 'MultiEdit' && toolName !== 'Write') return false;
|
|
183
|
+
const path = toolInput?.file_path ?? toolInput?.path;
|
|
184
|
+
if (typeof path !== 'string') return false;
|
|
185
|
+
return PROTOCOL_EDIT_REGEX.test(path);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectClaudeMdEdit(toolName, toolInput) {
|
|
189
|
+
if (toolName !== 'Edit' && toolName !== 'MultiEdit' && toolName !== 'Write') return false;
|
|
190
|
+
const path = toolInput?.file_path ?? toolInput?.path;
|
|
191
|
+
if (typeof path !== 'string') return false;
|
|
192
|
+
return CLAUDE_MD_EDIT_REGEX.test(path);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function detectDestructiveCommand(toolName, toolInput) {
|
|
196
|
+
if (toolName !== 'Bash' && toolName !== 'PowerShell') return false;
|
|
197
|
+
const command = toolInput?.command;
|
|
198
|
+
if (typeof command !== 'string' || !command.trim()) return false;
|
|
199
|
+
return DESTRUCTIVE_PATTERNS.some((re) => re.test(command));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function main() {
|
|
203
|
+
// Env gate: must be explicitly opted in. Hard kill switch wins.
|
|
204
|
+
if (process.env.HEYSPEX_PREFLIGHT_REMINDER_OFF === '1') return;
|
|
205
|
+
if (process.env.CLAUDE_AGENT !== 'true') return;
|
|
206
|
+
|
|
207
|
+
const raw = await readStdinJson();
|
|
208
|
+
if (!raw) return;
|
|
209
|
+
let payload;
|
|
210
|
+
try {
|
|
211
|
+
payload = JSON.parse(raw);
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const event = payload?.hook_event_name;
|
|
217
|
+
if (!event) return;
|
|
218
|
+
|
|
219
|
+
if (event === 'UserPromptSubmit') {
|
|
220
|
+
// Combine reminders into ONE emit — emitting twice would write two JSON
|
|
221
|
+
// objects to stdout and break the hook contract. The filing-intent reminder
|
|
222
|
+
// and the grounding nudge are independent and can co-fire.
|
|
223
|
+
const parts = [];
|
|
224
|
+
const intentReminder = detectRegistryPrimeReminder(payload?.prompt, REPO_ROOT);
|
|
225
|
+
if (intentReminder) parts.push(intentReminder);
|
|
226
|
+
if (process.env.HEYSPEX_GROUNDING_NUDGE_OFF !== '1') {
|
|
227
|
+
const grounding = detectDesignIntent(payload?.prompt);
|
|
228
|
+
if (grounding) parts.push(grounding);
|
|
229
|
+
}
|
|
230
|
+
if (parts.length) emit(event, parts.join('\n\n'));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (event === 'PreToolUse') {
|
|
235
|
+
const toolName = payload?.tool_name;
|
|
236
|
+
const toolInput = payload?.tool_input ?? {};
|
|
237
|
+
|
|
238
|
+
if (detectProtocolEdit(toolName, toolInput)) {
|
|
239
|
+
emit(event, PROTOCOL_EDIT_REMINDER);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (detectClaudeMdEdit(toolName, toolInput)) {
|
|
243
|
+
emit(event, CLAUDE_MD_EDIT_REMINDER);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (detectDestructiveCommand(toolName, toolInput)) {
|
|
247
|
+
emit(event, DESTRUCTIVE_REMINDER);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Entry-guard: run main() only when executed as the hook, NOT when imported (the smoke
|
|
255
|
+
// imports detectRegistryPrimeReminder to unit-test the config/injected-content paths).
|
|
256
|
+
const INVOKED_DIRECTLY =
|
|
257
|
+
!!process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
258
|
+
if (INVOKED_DIRECTLY) {
|
|
259
|
+
main().catch(() => {
|
|
260
|
+
// Never break Claude Code on a hook failure. Match emit-claim-status.mjs.
|
|
261
|
+
});
|
|
262
|
+
}
|