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,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code hook → the HeySpex Intent Classifier's SINGLE UserPromptSubmit entry point.
|
|
3
|
+
//
|
|
4
|
+
// HS-5ISS ENGINE CUTOVER: this hook now classifies through the engine module
|
|
5
|
+
// (spexharness/src/intent-classifier.mjs's classifyPrompt) instead of the old
|
|
6
|
+
// per-prompt Layer 2 "self_check" prose block (_intent-classifier-router.mjs's
|
|
7
|
+
// buildSelfCheckBlock — ~4,200 tokens / ~6.5 KB, pasted into EVERY prompt that missed
|
|
8
|
+
// every lexical pattern, i.e. almost every ordinary work order). The engine module is
|
|
9
|
+
// LEXICAL ONLY: a prompt that matches none of the five classifier rules
|
|
10
|
+
// (capture-on-idea, research-before-decision, settings-intent, defer-intent,
|
|
11
|
+
// spec-reference-lookup) now injects NOTHING — under 500 bytes, usually zero — instead
|
|
12
|
+
// of the old self-check block (AC3, [[HS-5ISS]]).
|
|
13
|
+
//
|
|
14
|
+
// A rule the engine matches still has to clear the SAME gating the old lexical sweep
|
|
15
|
+
// applied (env kill switch, agent_behaviors.<key>.enabled, the injected-content guard)
|
|
16
|
+
// before it fires — the engine module itself knows only patterns, not config or env,
|
|
17
|
+
// so isBehaviorEligible() re-applies that gating here. Once eligible, the directive is
|
|
18
|
+
// built through the SAME per-behavior transform (capture's confirm-mode substitution +
|
|
19
|
+
// hints, research's source hint, settings/defer/spec-reference's raw inject) and
|
|
20
|
+
// wrapped in the same CONFIRMED MATCH marker as the old lexical path
|
|
21
|
+
// (buildConfirmedDirective) — so a gate that fires emits a byte-identical directive to
|
|
22
|
+
// before. The five gates' own matching patterns and inject text are untouched.
|
|
23
|
+
//
|
|
24
|
+
// CUTOVER FLAG: agent_behaviors.intent_classifier.engine (.heyspex/config.yaml, read
|
|
25
|
+
// from the VAULT root via the loader's default config root — this hook never joins a
|
|
26
|
+
// config path itself; [[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
|
|
27
|
+
// Defaults ON (isConfigKeyEnabled semantics — absent/anything but explicit `false` is
|
|
28
|
+
// enabled). Set to `false` to fall back to the pre-HS-5ISS path in full — the unified
|
|
29
|
+
// lexical-sweep-plus-Layer-2-self-check hook, unchanged (_intent-classifier-router.mjs's
|
|
30
|
+
// runClassifierHook()).
|
|
31
|
+
//
|
|
32
|
+
// UNGATED by CLAUDE_AGENT — interactive human typing is the point. Per-behavior mutes
|
|
33
|
+
// still work: HEYSPEX_CAPTURE_TRIGGER_OFF / HEYSPEX_RESEARCH_TRIGGER_OFF /
|
|
34
|
+
// HEYSPEX_SETTINGS_INTENT_OFF / HEYSPEX_DEFER_INTENT_OFF / HEYSPEX_SPEC_REFERENCE_LOOKUP_OFF
|
|
35
|
+
// = 1. HEYSPEX_INTENT_ENGINE_OFF=1 is the cutover's own env kill switch — same
|
|
36
|
+
// contract as every other behavior here (a config_key AND an env var, never just
|
|
37
|
+
// one) — and forces the old fallback path without touching the git-tracked config
|
|
38
|
+
// file, which is what tests use to exercise the fallback safely. Decisions audited
|
|
39
|
+
// to .heyspex/intent-classifier-history.jsonl.
|
|
40
|
+
//
|
|
41
|
+
// FAIL-SAFE: exits 0 on every path. A missing/corrupt config, a missing rule table, or
|
|
42
|
+
// any thrown error degrades to "no injection" (engine path) or defers to the untouched
|
|
43
|
+
// fallback (runClassifierHook never throws either).
|
|
44
|
+
|
|
45
|
+
import { classifyPrompt } from '../../spexharness/src/intent-classifier.mjs';
|
|
46
|
+
import { isConfigKeyEnabled, startsWithInjectedContent } from './_guardrail-loader.mjs';
|
|
47
|
+
import {
|
|
48
|
+
appendClassifierEvent,
|
|
49
|
+
classifyIntent,
|
|
50
|
+
readIntentClassifierConfig,
|
|
51
|
+
} from './_intent-classifier-semantic.mjs';
|
|
52
|
+
import {
|
|
53
|
+
runClassifierHook,
|
|
54
|
+
readStdinJson,
|
|
55
|
+
behaviorKeyForRuleId,
|
|
56
|
+
isBehaviorEligible,
|
|
57
|
+
buildConfirmedDirective,
|
|
58
|
+
buildSelfCheckBlock,
|
|
59
|
+
CLASSIFIER_BEHAVIORS,
|
|
60
|
+
MIN_SELF_CHECK_CHARS,
|
|
61
|
+
} from './_intent-classifier-router.mjs';
|
|
62
|
+
|
|
63
|
+
const ENGINE_FLAG = 'agent_behaviors.intent_classifier.engine';
|
|
64
|
+
|
|
65
|
+
// THE SEMANTIC NET ON THE ENGINE PATH ([[HS-3J57-7EDT-DIAG-intent-classifier-s-semantic-fallback]],
|
|
66
|
+
// [[HS-8A3Y-LELB-DIAG-a-prose-feature-request-bypassed]]). The HS-5ISS cutover made a
|
|
67
|
+
// lexical miss a silent no-op — the ~6.5 KB Layer 2 self-check block was dropped and
|
|
68
|
+
// nothing took its place, so a paraphrased idea, or an owner's work-order-shaped
|
|
69
|
+
// feature ask ("please also add this to heyspex-app … should we create our own …"),
|
|
70
|
+
// reached the agent with no capture directive at all and was built straight from the
|
|
71
|
+
// prompt. This is the replacement: on a miss, when the semantic layer is on
|
|
72
|
+
// (agent_behaviors.intent_classifier.semantic_enabled, env HEYSPEX_INTENT_SEMANTIC_OFF=1
|
|
73
|
+
// to mute), the hook's behavior depends on the resolved mode
|
|
74
|
+
// (agent_behaviors.intent_classifier.mode, default self_check):
|
|
75
|
+
// - mode:self_check (DEFAULT) — the SAME deterministic SELF-CHECK block the legacy
|
|
76
|
+
// _intent-classifier-router.mjs path builds (buildSelfCheckBlock, reused verbatim
|
|
77
|
+
// — not a second copy of its prose) over whichever behaviors are still eligible on
|
|
78
|
+
// this prompt (env kill switch / config_key / injected-content guard, via
|
|
79
|
+
// isBehaviorEligible). This is a REAL directive, not an advisory: the agent judges
|
|
80
|
+
// the gates and, on a hit, fetches and follows the exact directive via
|
|
81
|
+
// `--directive <key>`, byte-identical to Layer 1. A prompt with zero eligible
|
|
82
|
+
// behaviors emits nothing (nothing to self-check).
|
|
83
|
+
// - mode:model (OPT-IN) — the local classifier is consulted first and a confident hit
|
|
84
|
+
// fires the full CONFIRMED MATCH directive, exactly as the legacy router's opt-in
|
|
85
|
+
// path did. When the model is silent, low-confidence, or unreachable, the hook
|
|
86
|
+
// falls back to the COMPACT_GATE_CHECK advisory below (under 400 bytes) rather than
|
|
87
|
+
// going fully silent again.
|
|
88
|
+
// A sub-12-char prompt ("yes", "proceed") and a harness envelope (task-notification /
|
|
89
|
+
// system-reminder as the prompt) never get either treatment.
|
|
90
|
+
export const COMPACT_GATE_CHECK =
|
|
91
|
+
'HEYSPEX GATE CHECK (system note): no lexical rule matched. If this is a raw idea or a ' +
|
|
92
|
+
'feature ask (please add/build X, should we build our own X), run `node ' +
|
|
93
|
+
'scripts/claude-hooks/_intent-classifier-router.mjs --directive capture_on_idea` and ' +
|
|
94
|
+
'follow it. Other keys: research_before_decision, settings_intent, defer_intent, ' +
|
|
95
|
+
'spec_reference_lookup. Otherwise proceed silently.';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The engine-miss fallback. Returns the additionalContext to emit, or null when the
|
|
99
|
+
* miss must stay silent (semantic layer off, prompt too short, harness envelope).
|
|
100
|
+
* Never throws.
|
|
101
|
+
*/
|
|
102
|
+
export async function engineMissFallback(prompt) {
|
|
103
|
+
try {
|
|
104
|
+
if (typeof prompt !== 'string' || prompt.trim().length < MIN_SELF_CHECK_CHARS) return null;
|
|
105
|
+
if (startsWithInjectedContent(prompt)) return null;
|
|
106
|
+
const cfg = readIntentClassifierConfig();
|
|
107
|
+
if (!cfg.semanticEnabled) return null;
|
|
108
|
+
|
|
109
|
+
if (cfg.semanticMode === 'model') {
|
|
110
|
+
const result = await classifyIntent(prompt);
|
|
111
|
+
if (
|
|
112
|
+
result?.behavior &&
|
|
113
|
+
result.confidence >= cfg.minConfidence &&
|
|
114
|
+
isBehaviorEligible(result.behavior, prompt)
|
|
115
|
+
) {
|
|
116
|
+
const directive = buildConfirmedDirective(result.behavior, undefined, prompt);
|
|
117
|
+
if (directive) {
|
|
118
|
+
appendClassifierEvent({
|
|
119
|
+
surface: 'userpromptsubmit',
|
|
120
|
+
rule: result.behavior,
|
|
121
|
+
layer: 'semantic',
|
|
122
|
+
decision: 'fire',
|
|
123
|
+
engine: true,
|
|
124
|
+
confidence: result.confidence,
|
|
125
|
+
latency_ms: result.latencyMs,
|
|
126
|
+
prompt_chars: prompt.length,
|
|
127
|
+
});
|
|
128
|
+
return directive;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// model silent/low-confidence/unreachable/ineligible → fall through to the
|
|
132
|
+
// COMPACT_GATE_CHECK advisory below (unchanged from before this fix).
|
|
133
|
+
} else if (cfg.semanticMode === 'self_check') {
|
|
134
|
+
// THE FIX: reuse the legacy router's own self-check block builder
|
|
135
|
+
// (_intent-classifier-router.mjs's buildSelfCheckBlock) rather than the weak
|
|
136
|
+
// COMPACT_GATE_CHECK advisory — this is what closes the Layer 2 hole if
|
|
137
|
+
// `engine` is ever flipped back to true while mode stays self_check (the
|
|
138
|
+
// default and, on this machine, the only reachable mode: `model` needs an
|
|
139
|
+
// Ollama endpoint that is off the network here). Byte-identical to what the
|
|
140
|
+
// legacy path would emit for the SAME considered set, so a gate an agent
|
|
141
|
+
// judges to fire resolves to the exact same directive either way.
|
|
142
|
+
const considered = CLASSIFIER_BEHAVIORS.filter((behavior) =>
|
|
143
|
+
isBehaviorEligible(behavior.key, prompt),
|
|
144
|
+
).map((behavior) => ({ behavior }));
|
|
145
|
+
if (considered.length) {
|
|
146
|
+
const additionalContext = buildSelfCheckBlock(considered, undefined, prompt);
|
|
147
|
+
if (additionalContext) {
|
|
148
|
+
appendClassifierEvent({
|
|
149
|
+
surface: 'userpromptsubmit',
|
|
150
|
+
rule: 'intent-classifier',
|
|
151
|
+
layer: 'semantic',
|
|
152
|
+
decision: 'self_check_injected',
|
|
153
|
+
engine: true,
|
|
154
|
+
offered: considered.map((entry) => entry.behavior.key),
|
|
155
|
+
prompt_chars: prompt.length,
|
|
156
|
+
});
|
|
157
|
+
return additionalContext;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// No behavior is currently eligible (every gate env/config-disabled) — mirror
|
|
161
|
+
// the legacy router's routePrompt(), which returns null before ever building a
|
|
162
|
+
// self-check block when its `eligible` set is empty. Nothing to self-check.
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
appendClassifierEvent({
|
|
167
|
+
surface: 'userpromptsubmit',
|
|
168
|
+
rule: null,
|
|
169
|
+
layer: 'semantic',
|
|
170
|
+
decision: 'compact_gate_check',
|
|
171
|
+
engine: true,
|
|
172
|
+
mode: cfg.semanticMode,
|
|
173
|
+
prompt_chars: prompt.length,
|
|
174
|
+
});
|
|
175
|
+
return COMPACT_GATE_CHECK;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function emit(additionalContext) {
|
|
182
|
+
if (!additionalContext) return;
|
|
183
|
+
process.stdout.write(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The engine-cutover hook body: read the payload, classify via the engine module, and
|
|
192
|
+
* emit a directive ONLY when a gate both matches lexically AND is eligible (env/config/
|
|
193
|
+
* guard). Every other path — a full lexical miss, an unknown rule id, an ineligible
|
|
194
|
+
* behavior, a malformed payload — emits nothing at all, which is the point (AC3).
|
|
195
|
+
* Never throws (mirrors runClassifierHook's own contract).
|
|
196
|
+
*/
|
|
197
|
+
export async function runEngineClassifierHook() {
|
|
198
|
+
try {
|
|
199
|
+
const raw = await readStdinJson();
|
|
200
|
+
if (!raw) return;
|
|
201
|
+
let payload;
|
|
202
|
+
try {
|
|
203
|
+
payload = JSON.parse(raw);
|
|
204
|
+
} catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (payload?.hook_event_name !== 'UserPromptSubmit') return;
|
|
208
|
+
const prompt = payload?.prompt;
|
|
209
|
+
if (typeof prompt !== 'string' || !prompt.trim()) return;
|
|
210
|
+
|
|
211
|
+
const hit = classifyPrompt(prompt);
|
|
212
|
+
if (!hit) {
|
|
213
|
+
// Lexical miss: the compact semantic net (see engineMissFallback) — never the
|
|
214
|
+
// old multi-KB self-check block, and nothing at all when the layer is off.
|
|
215
|
+
emit(await engineMissFallback(prompt));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const behaviorKey = behaviorKeyForRuleId(hit.rule);
|
|
220
|
+
if (!behaviorKey) return; // an engine rule id this router doesn't know as a behavior
|
|
221
|
+
|
|
222
|
+
if (!isBehaviorEligible(behaviorKey, prompt)) return; // env-off / config-disabled / injected-content guard
|
|
223
|
+
|
|
224
|
+
const directive = buildConfirmedDirective(behaviorKey, undefined, prompt);
|
|
225
|
+
if (!directive) return;
|
|
226
|
+
|
|
227
|
+
appendClassifierEvent({
|
|
228
|
+
surface: 'userpromptsubmit',
|
|
229
|
+
rule: hit.rule,
|
|
230
|
+
layer: 'lexical',
|
|
231
|
+
decision: 'fire',
|
|
232
|
+
engine: true,
|
|
233
|
+
prompt_chars: prompt.length,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
emit(directive);
|
|
237
|
+
} catch {
|
|
238
|
+
// Never break Claude Code on a hook failure.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const engineForcedOff = process.env.HEYSPEX_INTENT_ENGINE_OFF === '1';
|
|
243
|
+
if (!engineForcedOff && isConfigKeyEnabled(ENGINE_FLAG)) {
|
|
244
|
+
runEngineClassifierHook();
|
|
245
|
+
} else {
|
|
246
|
+
runClassifierHook();
|
|
247
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// log-session-activity.mjs — HS-6QEW-TASK (epic HS-JAAB-EPIC)
|
|
3
|
+
// PreToolUse + PostToolUse hook (matcher .*): append one `agent.tool_called`
|
|
4
|
+
// event row per tool call to the git-tracked .heyspex/sesslog-history.jsonl.
|
|
5
|
+
//
|
|
6
|
+
// Contract (see the task spec's Builder brief):
|
|
7
|
+
// - Reuse the stdin-payload parse + resolveSessionId from claim-liveness-shared.mjs.
|
|
8
|
+
// - Fail-open: any error, malformed payload, or missing field exits 0 — never
|
|
9
|
+
// break the host session.
|
|
10
|
+
// - Privacy: args_hash (deterministic hash) by default; always redact keys
|
|
11
|
+
// matching /token|secret|password|api_key|auth/i; full args only behind the
|
|
12
|
+
// per-vault audit_log_full_args opt-in.
|
|
13
|
+
// - Event fields: kind=agent.tool_called, source=hook, transport=local,
|
|
14
|
+
// tool_kind (edit|command|mcp|task|read|other), tool_name, session_id,
|
|
15
|
+
// session_short, spec_id, args_hash, latency_ms, success, error_kind, ts.
|
|
16
|
+
// - `--self-test` entrypoint: exercises well-formed payload → one valid JSONL
|
|
17
|
+
// row (temp file), malformed payload → exit 0 no throw, api_key/token keys →
|
|
18
|
+
// redacted, identical args → identical args_hash. Exits 0 on pass.
|
|
19
|
+
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import {
|
|
25
|
+
normalizeHookEvent,
|
|
26
|
+
parseArgs,
|
|
27
|
+
readStdin,
|
|
28
|
+
resolveSessionId,
|
|
29
|
+
} from "./claim-liveness-shared.mjs";
|
|
30
|
+
|
|
31
|
+
const REPO_ROOT = path.resolve(
|
|
32
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
33
|
+
"..",
|
|
34
|
+
"..",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function getLogPath() {
|
|
38
|
+
const env = process.env.HEYSPEX_SESSLOG_PATH?.trim();
|
|
39
|
+
return env || path.join(REPO_ROOT, ".heyspex", "sesslog-history.jsonl");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getClaimsPath() {
|
|
43
|
+
const env = process.env.HEYSPEX_CLAIMS_PATH?.trim();
|
|
44
|
+
return env || path.join(REPO_ROOT, ".heyspex", "active-claims.json");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getPendingDir() {
|
|
48
|
+
const env = process.env.HEYSPEX_SESSLOG_PENDING_DIR?.trim();
|
|
49
|
+
return env || path.join(REPO_ROOT, ".heyspex", "pending");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getFocusPath() {
|
|
53
|
+
const env = process.env.HEYSPEX_FOCUS_PATH?.trim();
|
|
54
|
+
return env || path.join(REPO_ROOT, ".heyspex", "focus.yaml");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Tool kind mapping
|
|
58
|
+
function getToolKind(toolName) {
|
|
59
|
+
if (!toolName) return "other";
|
|
60
|
+
const toolMap = {
|
|
61
|
+
Edit: "edit",
|
|
62
|
+
Write: "edit",
|
|
63
|
+
NotebookEdit: "edit",
|
|
64
|
+
Bash: "command",
|
|
65
|
+
PowerShell: "command",
|
|
66
|
+
Read: "read",
|
|
67
|
+
Grep: "read",
|
|
68
|
+
Glob: "read",
|
|
69
|
+
Task: "task",
|
|
70
|
+
Agent: "task",
|
|
71
|
+
};
|
|
72
|
+
if (toolMap[toolName] !== undefined) return toolMap[toolName];
|
|
73
|
+
// Claude Code reports MCP tools as mcp__<server>__<tool>; Command Code's
|
|
74
|
+
// hooks engine reports the bare heyspex_<tool> name (seen in live rows).
|
|
75
|
+
if (
|
|
76
|
+
typeof toolName === "string" &&
|
|
77
|
+
(toolName.startsWith("mcp__") || toolName.startsWith("heyspex_"))
|
|
78
|
+
)
|
|
79
|
+
return "mcp";
|
|
80
|
+
return "other";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Redact sensitive keys (shallow)
|
|
84
|
+
function redactSensitiveKeys(obj) {
|
|
85
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
86
|
+
const redacted = {};
|
|
87
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
88
|
+
if (
|
|
89
|
+
typeof key === "string" &&
|
|
90
|
+
/token|secret|password|api_key|auth/i.test(key)
|
|
91
|
+
) {
|
|
92
|
+
redacted[key] = "[REDACTED]";
|
|
93
|
+
} else {
|
|
94
|
+
redacted[key] = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return redacted;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Generate deterministic hash from redacted args (sha256, first 16 hex chars)
|
|
101
|
+
function generateArgsHash(args) {
|
|
102
|
+
if (!args || typeof args !== "object") return null;
|
|
103
|
+
const clean = redactSensitiveKeys(args);
|
|
104
|
+
return createHash("sha256")
|
|
105
|
+
.update(JSON.stringify(clean))
|
|
106
|
+
.digest("hex")
|
|
107
|
+
.slice(0, 16);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Write session log entry
|
|
111
|
+
function writeSessionLogEntry(entry) {
|
|
112
|
+
try {
|
|
113
|
+
const logPath = getLogPath();
|
|
114
|
+
const dir = path.dirname(logPath);
|
|
115
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
116
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
117
|
+
} catch {
|
|
118
|
+
// Fail-safe: silently ignore write errors
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Read active-claims.json and find claim for session
|
|
123
|
+
function resolveClaim(sessionId) {
|
|
124
|
+
try {
|
|
125
|
+
const claimsPath = getClaimsPath();
|
|
126
|
+
if (!fs.existsSync(claimsPath)) return null;
|
|
127
|
+
const raw = fs.readFileSync(claimsPath, "utf8");
|
|
128
|
+
const data = JSON.parse(raw);
|
|
129
|
+
const claims = data?.claims;
|
|
130
|
+
if (!claims || typeof claims !== "object") return null;
|
|
131
|
+
// A session may hold several claims (e.g. an epic and its task) —
|
|
132
|
+
// attribute to the most recently active one.
|
|
133
|
+
const mine = Object.values(claims)
|
|
134
|
+
.filter((c) => c?.sessionId === sessionId)
|
|
135
|
+
.sort(
|
|
136
|
+
(a, b) =>
|
|
137
|
+
Date.parse(b.lastActivityAt ?? b.claimedAt ?? 0) -
|
|
138
|
+
Date.parse(a.lastActivityAt ?? a.claimedAt ?? 0),
|
|
139
|
+
);
|
|
140
|
+
if (mine.length > 0) {
|
|
141
|
+
return { specId: mine[0].specId, actor: mine[0].agent };
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fallback attribution: the vault-wide focus pin (.heyspex/focus.yaml).
|
|
150
|
+
// Coarser than a session claim — every unclaimed session inherits the pinned
|
|
151
|
+
// spec — but it is the epic's named v1 mechanism for spec_id attribution.
|
|
152
|
+
function resolveFocusSpecId() {
|
|
153
|
+
try {
|
|
154
|
+
const focusPath = getFocusPath();
|
|
155
|
+
if (!fs.existsSync(focusPath)) return null;
|
|
156
|
+
const raw = fs.readFileSync(focusPath, "utf8");
|
|
157
|
+
const m = /^spec_path:\s*(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/m.exec(raw);
|
|
158
|
+
const specPath = m?.[1] ?? m?.[2] ?? m?.[3];
|
|
159
|
+
if (!specPath) return null;
|
|
160
|
+
// basename "HS-XXXX-KIND-slug.md" → id "HS-XXXX-KIND"
|
|
161
|
+
const base = path.basename(specPath, ".md");
|
|
162
|
+
const idMatch = /^(HS-[A-Za-z0-9]+-(?:MIL|EPIC|TASK|DIAG|RES|CAP|REV))/.exec(
|
|
163
|
+
base,
|
|
164
|
+
);
|
|
165
|
+
return idMatch?.[1] ?? null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Sweep orphaned pending markers. A tool call that never gets a PostToolUse
|
|
172
|
+
// (interrupt, denied permission, crashed session) leaks its marker forever, so
|
|
173
|
+
// the dir accumulates. Anything older than 1 hour is dead — a stale marker only
|
|
174
|
+
// costs one missing latency_ms, so deletion is always safe. Non-fatal.
|
|
175
|
+
const STALE_MARKER_MS = 60 * 60 * 1000;
|
|
176
|
+
function sweepStaleMarkers(dir) {
|
|
177
|
+
try {
|
|
178
|
+
const cutoff = Date.now() - STALE_MARKER_MS;
|
|
179
|
+
for (const name of fs.readdirSync(dir)) {
|
|
180
|
+
try {
|
|
181
|
+
const p = path.join(dir, name);
|
|
182
|
+
const st = fs.statSync(p);
|
|
183
|
+
if (st.isFile() && st.mtimeMs < cutoff) fs.unlinkSync(p);
|
|
184
|
+
} catch {
|
|
185
|
+
// fail-safe: skip entries we cannot stat/delete
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// fail-safe
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Write a pending marker file for PreToolUse
|
|
194
|
+
function writePendingMarker(toolUseId) {
|
|
195
|
+
try {
|
|
196
|
+
const dir = getPendingDir();
|
|
197
|
+
if (!dir) return;
|
|
198
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
199
|
+
sweepStaleMarkers(dir);
|
|
200
|
+
const marker = { startTime: Date.now() };
|
|
201
|
+
fs.writeFileSync(path.join(dir, toolUseId), JSON.stringify(marker));
|
|
202
|
+
} catch {
|
|
203
|
+
// fail-safe
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Read and remove pending marker for PostToolUse; returns startTime or null
|
|
208
|
+
function readPendingMarker(toolUseId) {
|
|
209
|
+
try {
|
|
210
|
+
const dir = getPendingDir();
|
|
211
|
+
if (!dir) return null;
|
|
212
|
+
const filePath = path.join(dir, toolUseId);
|
|
213
|
+
if (!fs.existsSync(filePath)) return null;
|
|
214
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
215
|
+
fs.unlinkSync(filePath);
|
|
216
|
+
const data = JSON.parse(raw);
|
|
217
|
+
return typeof data?.startTime === "number" ? data.startTime : null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Process hook event
|
|
224
|
+
async function processHookEvent() {
|
|
225
|
+
try {
|
|
226
|
+
const payload = await readStdin();
|
|
227
|
+
if (!payload) return;
|
|
228
|
+
|
|
229
|
+
const parsedPayload = JSON.parse(payload);
|
|
230
|
+
const args = parsedPayload?.tool_input;
|
|
231
|
+
const toolName = parsedPayload?.tool_name;
|
|
232
|
+
// Claude Code sends hook_event_name in the payload (PascalCase); Cursor
|
|
233
|
+
// passes --event=camelCase on argv instead. Normalize both.
|
|
234
|
+
const cliArgs = parseArgs(process.argv);
|
|
235
|
+
const event = normalizeHookEvent(
|
|
236
|
+
parsedPayload?.hook_event_name ?? cliArgs.event,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Skip if not a tool call event
|
|
240
|
+
if (event !== "PreToolUse" && event !== "PostToolUse") return;
|
|
241
|
+
|
|
242
|
+
const sessionId = resolveSessionId(parsedPayload);
|
|
243
|
+
if (!sessionId) return;
|
|
244
|
+
|
|
245
|
+
const fullArgsEnabled =
|
|
246
|
+
process.env.HEYSPEX_AUDIT_LOG_FULL_ARGS?.trim() === "1";
|
|
247
|
+
const argsHash = generateArgsHash(args);
|
|
248
|
+
|
|
249
|
+
// Determine tool kind
|
|
250
|
+
const toolKind = getToolKind(toolName);
|
|
251
|
+
|
|
252
|
+
// Resolve spec_id and actor: session claim first, focus pin as fallback
|
|
253
|
+
const claim = resolveClaim(sessionId);
|
|
254
|
+
const specId = claim?.specId ?? resolveFocusSpecId();
|
|
255
|
+
const actor = claim?.actor ?? "unknown";
|
|
256
|
+
|
|
257
|
+
const toolUseId = parsedPayload?.tool_use_id;
|
|
258
|
+
|
|
259
|
+
// PreToolUse: store pending marker, do NOT write session log row.
|
|
260
|
+
// (No claim-liveness emit here — that is the liveness hook's own job;
|
|
261
|
+
// calling it from this logger would double-emit heartbeats and add an
|
|
262
|
+
// HTTP round-trip to every tool call.)
|
|
263
|
+
if (event === "PreToolUse") {
|
|
264
|
+
if (toolUseId) writePendingMarker(toolUseId);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// PostToolUse: compute latency (null = no matching marker), build entry, write row
|
|
269
|
+
let latencyMs = null;
|
|
270
|
+
if (toolUseId) {
|
|
271
|
+
const startTime = readPendingMarker(toolUseId);
|
|
272
|
+
if (startTime !== null) {
|
|
273
|
+
latencyMs = Math.max(0, Date.now() - startTime);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const response = parsedPayload?.tool_response;
|
|
278
|
+
let success = true;
|
|
279
|
+
let errorKind = null;
|
|
280
|
+
|
|
281
|
+
if (response && typeof response === "object") {
|
|
282
|
+
if (response.is_error === true) {
|
|
283
|
+
success = false;
|
|
284
|
+
errorKind = "tool_error";
|
|
285
|
+
} else if (typeof response.error === "string" && response.error.trim()) {
|
|
286
|
+
success = false;
|
|
287
|
+
errorKind = "tool_error";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Build metadata
|
|
292
|
+
const metadata = {
|
|
293
|
+
tool_name: toolName,
|
|
294
|
+
tool_kind: toolKind,
|
|
295
|
+
session_id: sessionId,
|
|
296
|
+
session_short: sessionId.slice(0, 8),
|
|
297
|
+
source: "hook",
|
|
298
|
+
transport: "local",
|
|
299
|
+
args_hash: argsHash,
|
|
300
|
+
success,
|
|
301
|
+
latency_ms: latencyMs,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (errorKind) {
|
|
305
|
+
metadata.error_kind = errorKind;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (fullArgsEnabled && args && typeof args === "object") {
|
|
309
|
+
metadata.args = redactSensitiveKeys(args);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const entry = {
|
|
313
|
+
timestamp: new Date().toISOString(),
|
|
314
|
+
event_kind: "agent.tool_called",
|
|
315
|
+
actor,
|
|
316
|
+
spec_id: specId,
|
|
317
|
+
metadata,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
writeSessionLogEntry(entry);
|
|
321
|
+
} catch {
|
|
322
|
+
// Fail-open: silently ignore all errors
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Self-test: DELEGATES to the sibling smoke file (the deterministic gate).
|
|
327
|
+
// DO NOT replace this block with inline assertions — the gate contract requires
|
|
328
|
+
// `node <this file> --self-test` to run log-session-activity.smoke.mjs verbatim.
|
|
329
|
+
if (process.argv.includes("--self-test")) {
|
|
330
|
+
const { spawnSync } = await import("node:child_process");
|
|
331
|
+
const smoke = path.join(
|
|
332
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
333
|
+
"log-session-activity.smoke.mjs",
|
|
334
|
+
);
|
|
335
|
+
const proc = spawnSync(process.execPath, [smoke], { stdio: "inherit" });
|
|
336
|
+
process.exit(proc.status ?? 1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Main execution
|
|
340
|
+
await processHookEvent();
|
|
341
|
+
process.exit(0);
|
|
@@ -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/naughty-list-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/naughty-list-gate.mjs';
|
|
10
|
+
|
|
11
|
+
await runGateAsHook(gate);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code hook → play tada.wav when a spec flips to `status: in_review`.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
import { isShipEvent, readStdin, repoRoot } from './terminal-close-shared.mjs';
|
|
9
|
+
|
|
10
|
+
const SOUND_PATH = 'C:\\Windows\\Media\\tada.wav';
|
|
11
|
+
|
|
12
|
+
function resolveSoundEnabledFromSettings() {
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(path.join(repoRoot(), 'spex-console', 'settings.json'), 'utf8');
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return parsed?.soundEnabled === true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function playSound() {
|
|
23
|
+
try {
|
|
24
|
+
const child = spawn(
|
|
25
|
+
'powershell.exe',
|
|
26
|
+
[
|
|
27
|
+
'-NoProfile',
|
|
28
|
+
'-WindowStyle',
|
|
29
|
+
'Hidden',
|
|
30
|
+
'-Command',
|
|
31
|
+
`[System.Media.SoundPlayer]::new('${SOUND_PATH}').PlaySync()`,
|
|
32
|
+
],
|
|
33
|
+
{ detached: true, stdio: 'ignore', windowsHide: true },
|
|
34
|
+
);
|
|
35
|
+
child.unref();
|
|
36
|
+
} catch {
|
|
37
|
+
// Silent
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
fs.writeFileSync(path.join(repoRoot(), '.heyspex', '.last-success-sound-at'), String(Date.now()));
|
|
41
|
+
} catch {
|
|
42
|
+
// never block
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const stdinRaw = await readStdin();
|
|
48
|
+
if (!stdinRaw) return;
|
|
49
|
+
|
|
50
|
+
let payload;
|
|
51
|
+
try {
|
|
52
|
+
payload = JSON.parse(stdinRaw);
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!isShipEvent(payload)) return;
|
|
58
|
+
if (!resolveSoundEnabledFromSettings()) return;
|
|
59
|
+
|
|
60
|
+
const filePath = payload?.file_path ?? payload?.tool_input?.file_path;
|
|
61
|
+
if (typeof filePath === 'string') {
|
|
62
|
+
try {
|
|
63
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
playSound();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main().catch(() => {});
|