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,449 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code SessionEnd hook (HS-EPIC-NWUE / HS-EPIC-2BKX; engine wiring per
|
|
3
|
+
// task HS-U618, epic HS-Y646 "session bootstrap and stop for every seat").
|
|
4
|
+
//
|
|
5
|
+
// Fires when a Claude Code session ends and does TWO independent things:
|
|
6
|
+
//
|
|
7
|
+
// 1. Console claim relay (original behavior, unchanged): posts the session
|
|
8
|
+
// id + termination `source` to `/api/agent/session-end`, which
|
|
9
|
+
// (server-side, tier-correctly) finds the matching active claim and
|
|
10
|
+
// releases it so the What's Next hero flips the Launch CTA back to
|
|
11
|
+
// "Re-spawn session". This is the FAST path for "the agent closed" — the
|
|
12
|
+
// 10-min stale timeout is the backstop for hard window kills / SIGKILL,
|
|
13
|
+
// which do NOT fire SessionEnd. Source gating lives SERVER-side (the
|
|
14
|
+
// route decides which `source` values count as "closed") so it's
|
|
15
|
+
// unit-testable and so this half stays a dumb relay. Notably the route
|
|
16
|
+
// must NOT release on source `resume` (session suspended for
|
|
17
|
+
// `--resume`/`--continue`) or `clear` (`/clear`) — those mean the
|
|
18
|
+
// session lives on.
|
|
19
|
+
//
|
|
20
|
+
// 2. Engine session-end (new, HS-U618): records the WIP hand-off, releases
|
|
21
|
+
// the session's active claims, and exports its events — via
|
|
22
|
+
// `runSessionEnd()` below — by calling straight into
|
|
23
|
+
// spexharness/src/session-record.mjs's durable session store
|
|
24
|
+
// (`createSessionStore`'s `updateWip`/`releaseClaim`/`finishSession`)
|
|
25
|
+
// rather than re-deriving that logic here (epic scope: "the plugin's
|
|
26
|
+
// SessionEnd and Stop hooks as engine calls"). This only does anything
|
|
27
|
+
// for a session the harness itself bootstrapped (spexharness's
|
|
28
|
+
// `createSession`) — a plain Claude Code plugin session has no durable
|
|
29
|
+
// record, so `runSessionEnd` reads `null` and no-ops. Idempotent: a
|
|
30
|
+
// session already sealed (`endedAt` set) is read back as closed and
|
|
31
|
+
// skipped rather than re-recorded, so this hook firing twice for one
|
|
32
|
+
// session (a duplicate SessionEnd, a retried close) never double-writes.
|
|
33
|
+
//
|
|
34
|
+
// Both halves are independently fail-open — MUST exit 0 on every failure
|
|
35
|
+
// path, a hook that breaks Claude Code is worse than a hook that silently
|
|
36
|
+
// does nothing (mirrors emit-claim-status.mjs) — and neither half's failure
|
|
37
|
+
// affects the other.
|
|
38
|
+
//
|
|
39
|
+
// Console URL resolution delegates to the ONE shared resolver
|
|
40
|
+
// (scripts/lib/console-url-resolver.mjs, [[HS-GD45-MCPC-TASK]]) — MODE-AWARE
|
|
41
|
+
// precedence ([[HS-VOJ7-YL1C-DIAG-console-url-resolver-ignores-dev-mode]]):
|
|
42
|
+
// HEYSPEX_CONSOLE_URL env → ~/.heyspex/credentials.yaml `console_url` →
|
|
43
|
+
// .heyspex/console-url discovery file (local reliance ONLY) → the console URL
|
|
44
|
+
// derived from the resolved dev mode (cloud → https://app.heyspex.com).
|
|
45
|
+
//
|
|
46
|
+
// P9 (HS-U95M, plugin parity, epic [[HS-HEBV-8CMW-EPIC-spexcode-profile-v0-learn-the-developer]]):
|
|
47
|
+
// a THIRD, independent half — after the console relay and the engine session-end above,
|
|
48
|
+
// this hook also surfaces any P6 profile proposals still `pending` (never accepted or
|
|
49
|
+
// declined) via spexharness/src/profile/proposals.mjs's own `listPending()` (thin shell,
|
|
50
|
+
// never a second read of the ledger). The plugin's Stop/SessionEnd hook cannot run an
|
|
51
|
+
// arrow-key select (`presentPendingProposal`, T15) the way an interactive SpexCode seat
|
|
52
|
+
// can — this is a plain summary naming each pending item and how to act on it (`/profile
|
|
53
|
+
// add|remove`, or the next SpexCode launch's own prompt). Absence of the ledger, or an
|
|
54
|
+
// empty pending queue, produces no section and never throws — own failure boundary,
|
|
55
|
+
// independent of the console-relay and engine halves, per this file's own header
|
|
56
|
+
// contract ("Both halves are independently fail-open").
|
|
57
|
+
|
|
58
|
+
import path from 'node:path';
|
|
59
|
+
import http from 'node:http';
|
|
60
|
+
import https from 'node:https';
|
|
61
|
+
import { fileURLToPath } from 'node:url';
|
|
62
|
+
|
|
63
|
+
import {
|
|
64
|
+
resolveConsoleUrl as resolveConsoleUrlShared,
|
|
65
|
+
DEFAULT_CONSOLE_URL,
|
|
66
|
+
} from '../lib/console-url-resolver.mjs';
|
|
67
|
+
// Bearer for /api/agent/session-end — same reader every other consumer uses, never
|
|
68
|
+
// re-implemented and never printed.
|
|
69
|
+
import { readCredentialsMcpToken } from '../lib/dev-mode.mjs';
|
|
70
|
+
import {
|
|
71
|
+
appendSessionRecord,
|
|
72
|
+
createSessionStore,
|
|
73
|
+
} from '../../spexharness/src/session-record.mjs';
|
|
74
|
+
import {
|
|
75
|
+
defaultProposalsLedgerPath,
|
|
76
|
+
listPending,
|
|
77
|
+
} from '../../spexharness/src/profile/proposals.mjs';
|
|
78
|
+
|
|
79
|
+
const REQUEST_TIMEOUT_MS = 800;
|
|
80
|
+
const ENGINE_TIMEOUT_MS = 1500;
|
|
81
|
+
const PROPOSALS_TIMEOUT_MS = 500;
|
|
82
|
+
|
|
83
|
+
function repoRoot() {
|
|
84
|
+
// session-end.mjs lives in scripts/claude-hooks/, so up two levels.
|
|
85
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stays fail-open (must exit 0 on every failure path, per the file-header
|
|
89
|
+
// contract) but no longer fails SILENTLY: resolve in `strict` mode first so
|
|
90
|
+
// an unresolved console URL logs loudly instead of quietly posting the release
|
|
91
|
+
// to a guessed localhost:3000
|
|
92
|
+
// ([[HS-OQ34-G59F-DIAG-console-url-silently-defaults-to]]). Since the resolver
|
|
93
|
+
// became mode-aware this can only throw in LOCAL reliance (no
|
|
94
|
+
// env/credentials/discovery-file match); cloud reliance always has a resolved
|
|
95
|
+
// answer — the derived cloud console.
|
|
96
|
+
function resolveConsoleUrl() {
|
|
97
|
+
try {
|
|
98
|
+
return resolveConsoleUrlShared({ repoRoot: repoRoot(), strict: true });
|
|
99
|
+
} catch {
|
|
100
|
+
console.error(
|
|
101
|
+
`[session-end] console URL unresolved in local reliance (no env/credentials/discovery-file ` +
|
|
102
|
+
`match) — guessing ${DEFAULT_CONSOLE_URL}. The release-claim POST may silently miss the real console.`,
|
|
103
|
+
);
|
|
104
|
+
return DEFAULT_CONSOLE_URL;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readStdin() {
|
|
109
|
+
if (process.stdin.isTTY) return null;
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
let buf = '';
|
|
112
|
+
let resolved = false;
|
|
113
|
+
const finish = (value) => {
|
|
114
|
+
if (resolved) return;
|
|
115
|
+
resolved = true;
|
|
116
|
+
resolve(value);
|
|
117
|
+
};
|
|
118
|
+
const timer = setTimeout(() => finish(buf || null), 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 || null);
|
|
126
|
+
});
|
|
127
|
+
process.stdin.on('error', () => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
finish(buf || null);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function postSessionEnd(consoleUrl, body, token) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
let url;
|
|
137
|
+
try {
|
|
138
|
+
url = new URL('/api/agent/session-end', consoleUrl);
|
|
139
|
+
} catch {
|
|
140
|
+
resolve();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
144
|
+
const payload = JSON.stringify(body);
|
|
145
|
+
const req = transport.request(
|
|
146
|
+
{
|
|
147
|
+
method: 'POST',
|
|
148
|
+
hostname: url.hostname,
|
|
149
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
150
|
+
path: `${url.pathname}${url.search}`,
|
|
151
|
+
headers: {
|
|
152
|
+
'content-type': 'application/json',
|
|
153
|
+
'content-length': Buffer.byteLength(payload),
|
|
154
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
155
|
+
},
|
|
156
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
157
|
+
},
|
|
158
|
+
(res) => {
|
|
159
|
+
res.on('data', () => {});
|
|
160
|
+
res.on('end', resolve);
|
|
161
|
+
res.on('error', resolve);
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
req.on('error', resolve);
|
|
165
|
+
req.on('timeout', () => {
|
|
166
|
+
req.destroy();
|
|
167
|
+
resolve();
|
|
168
|
+
});
|
|
169
|
+
req.write(payload);
|
|
170
|
+
req.end();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ============================================================================================
|
|
175
|
+
* Engine session-end (HS-U618) — record WIP, release claims, export events, idempotently.
|
|
176
|
+
* ============================================================================================ */
|
|
177
|
+
|
|
178
|
+
/** Where the local (network-free) events export lands — one JSONL line per session close. */
|
|
179
|
+
export function sessionEventsExportPath(root = repoRoot()) {
|
|
180
|
+
return path.join(root, '.heyspex', 'session-end-events.jsonl');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Default `exporter`: appends the session's events + tool-call log to a local JSONL file via
|
|
185
|
+
* session-record.mjs's own `appendSessionRecord` (same line-atomic append the harness uses for
|
|
186
|
+
* its own per-machine records) — no network, no Neon, so this half never depends on the console
|
|
187
|
+
* being reachable.
|
|
188
|
+
*
|
|
189
|
+
* @param {{ root?: string }} [opts]
|
|
190
|
+
* @returns {{ export: (sessionId: string, data: { events?: Array, toolCalls?: Array }) => Promise<object> }}
|
|
191
|
+
*/
|
|
192
|
+
export function createLocalEventsExporter({ root = repoRoot() } = {}) {
|
|
193
|
+
const filePath = sessionEventsExportPath(root);
|
|
194
|
+
return {
|
|
195
|
+
async export(sessionId, { events = [], toolCalls = [] } = {}) {
|
|
196
|
+
const record = { sessionId, exportedAt: new Date().toISOString(), events, toolCalls };
|
|
197
|
+
appendSessionRecord(filePath, record);
|
|
198
|
+
return { filePath, eventCount: events.length, toolCallCount: toolCalls.length };
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Default `ledger`: wraps a spexharness durable session store's `getSession` /
|
|
205
|
+
* `updateWip` / `finishSession` — reused, not re-derived. `getSession` fails soft to `null`
|
|
206
|
+
* (no durable record for this session — most Claude Code plugin sessions today, since nothing
|
|
207
|
+
* yet calls the store's `createSession` for them — or storage genuinely unreachable) so the
|
|
208
|
+
* caller can no-op rather than throw.
|
|
209
|
+
*
|
|
210
|
+
* @param {ReturnType<typeof createSessionStore>} store
|
|
211
|
+
*/
|
|
212
|
+
export function createStoreLedger(store) {
|
|
213
|
+
return {
|
|
214
|
+
async getSession(sessionId) {
|
|
215
|
+
try {
|
|
216
|
+
return await store.getSession(sessionId);
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async recordWip(sessionId, wip) {
|
|
222
|
+
await store.updateWip(sessionId, wip);
|
|
223
|
+
return store.finishSession(sessionId, null);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Default `claims`: releases every claim the store has on record for this session, by reading
|
|
230
|
+
* the session's own `claims` array and calling the store's `releaseClaim` for each — the store's
|
|
231
|
+
* existing claim bookkeeping, not a second copy of it.
|
|
232
|
+
*
|
|
233
|
+
* @param {ReturnType<typeof createSessionStore>} store
|
|
234
|
+
*/
|
|
235
|
+
export function createStoreClaims(store) {
|
|
236
|
+
return {
|
|
237
|
+
async release(sessionId) {
|
|
238
|
+
let existing;
|
|
239
|
+
try {
|
|
240
|
+
existing = await store.getSession(sessionId);
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const specIds = (existing?.claims ?? []).map((c) => c?.specId).filter(Boolean);
|
|
245
|
+
for (const specId of specIds) {
|
|
246
|
+
await store.releaseClaim(sessionId, specId);
|
|
247
|
+
}
|
|
248
|
+
return specIds;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build the production `{ ledger, claims, exporter }` deps {@link runSessionEnd} takes. One
|
|
255
|
+
* store backs both `ledger` and `claims` so a real close does one connection's worth of work,
|
|
256
|
+
* not two. `backend`/`root` are injection points for tests (an in-memory session backend, a
|
|
257
|
+
* scratch events-export dir) — omitted, `ledger`/`claims` default to the real Neon-backed store
|
|
258
|
+
* (lazy: no connection opens until a method is actually called) and `exporter` writes under the
|
|
259
|
+
* real repo's `.heyspex/`.
|
|
260
|
+
*
|
|
261
|
+
* @param {{ backend?: object, root?: string }} [opts]
|
|
262
|
+
*/
|
|
263
|
+
export function buildDefaultDeps({ backend = undefined, root = undefined } = {}) {
|
|
264
|
+
const store = createSessionStore(backend ? { backend } : {});
|
|
265
|
+
return {
|
|
266
|
+
ledger: createStoreLedger(store),
|
|
267
|
+
claims: createStoreClaims(store),
|
|
268
|
+
exporter: createLocalEventsExporter(root ? { root } : {}),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The engine session-end core (HS-U618 AC1-4): records the WIP hand-off, releases the session's
|
|
274
|
+
* active claims, and exports its events — exactly once per session id.
|
|
275
|
+
*
|
|
276
|
+
* Idempotent by design: a session the ledger already reports `endedAt` on is read back as
|
|
277
|
+
* already-closed and none of the three actions run again, so a duplicate SessionEnd (or a
|
|
278
|
+
* retried close) never double-records. A session id the ledger has no record for at all (the
|
|
279
|
+
* common case for a plain Claude Code plugin session, which nothing yet bootstraps through the
|
|
280
|
+
* harness's durable store) is likewise a no-op — there is nothing this routine can close.
|
|
281
|
+
*
|
|
282
|
+
* @param {{ sessionId: string, source?: string|null, wip?: unknown }} input
|
|
283
|
+
* @param {{
|
|
284
|
+
* ledger: { getSession: (id: string) => Promise<object|null>, recordWip: (id: string, wip: unknown) => Promise<object> },
|
|
285
|
+
* claims: { release: (id: string) => Promise<string[]> },
|
|
286
|
+
* exporter: { export: (id: string, data: { events?: Array, toolCalls?: Array }) => Promise<object> },
|
|
287
|
+
* }} deps injectable — tests pass fakes (or the real pieces above over an in-memory backend), so
|
|
288
|
+
* this never needs a reachable console or a live Neon connection to prove its contract.
|
|
289
|
+
* @returns {Promise<object>} a receipt describing what happened.
|
|
290
|
+
*/
|
|
291
|
+
export async function runSessionEnd({ sessionId, source = null, wip = null } = {}, deps) {
|
|
292
|
+
if (!sessionId) return { closed: false, reason: 'missing_session_id' };
|
|
293
|
+
const { ledger, claims, exporter } = deps;
|
|
294
|
+
|
|
295
|
+
const existing = await ledger.getSession(sessionId);
|
|
296
|
+
if (!existing) return { closed: false, reason: 'no_session_record', sessionId };
|
|
297
|
+
if (existing.endedAt) return { closed: false, reason: 'already_closed', sessionId };
|
|
298
|
+
|
|
299
|
+
const wipReceipt = await ledger.recordWip(sessionId, wip ?? existing.wip ?? null);
|
|
300
|
+
const releasedClaims = await claims.release(sessionId, { source });
|
|
301
|
+
const exportReceipt = await exporter.export(sessionId, {
|
|
302
|
+
events: existing.events ?? [],
|
|
303
|
+
toolCalls: existing.toolCalls ?? [],
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
closed: true,
|
|
308
|
+
sessionId,
|
|
309
|
+
wip: { endedAt: wipReceipt?.endedAt ?? null, value: wipReceipt?.wip ?? null },
|
|
310
|
+
releasedClaims,
|
|
311
|
+
exported: exportReceipt,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* ============================================================================================
|
|
316
|
+
* Pending profile proposals (HS-U95M, P9) — a third, independent half.
|
|
317
|
+
* ============================================================================================ */
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Render the P6 pending-proposals queue into the plain-text summary this hook prints for
|
|
321
|
+
* the developer. Thin shell over spexharness/src/profile/proposals.mjs's own `listPending`
|
|
322
|
+
* — never a second read or fold of the ledger. Returns `null` (no proposal section, AC2)
|
|
323
|
+
* when there is nothing pending, or when the ledger cannot be read at all (missing file,
|
|
324
|
+
* corrupt content) — a broken read must never surface as an error, only as silence.
|
|
325
|
+
*
|
|
326
|
+
* @param {{ ledgerPath: string, listPendingFn?: typeof listPending }} opts
|
|
327
|
+
* @returns {string|null}
|
|
328
|
+
*/
|
|
329
|
+
export function renderPendingProposalsSummary({ ledgerPath, listPendingFn = listPending }) {
|
|
330
|
+
let pending;
|
|
331
|
+
try {
|
|
332
|
+
pending = listPendingFn({ ledgerPath });
|
|
333
|
+
} catch {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
if (!Array.isArray(pending) || pending.length === 0) return null;
|
|
337
|
+
|
|
338
|
+
const count = pending.length;
|
|
339
|
+
const lines = [
|
|
340
|
+
`SpexCode Profile — ${count} pending observation${count === 1 ? '' : 's'} waiting for a decision:`,
|
|
341
|
+
];
|
|
342
|
+
pending.forEach((proposal, i) => {
|
|
343
|
+
const obs = proposal?.observation ?? {};
|
|
344
|
+
const text = typeof obs.text === 'string' && obs.text.trim() ? obs.text.trim() : '(no text)';
|
|
345
|
+
lines.push(
|
|
346
|
+
` ${i + 1}. ${text} _(source: ${obs.source ?? 'unknown'}, project: ${obs.project ?? 'unknown'}, session: ${obs.session ?? 'unknown'})_`,
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
lines.push(
|
|
350
|
+
'Act on these with `/profile add` / `/profile remove`, or you will be asked again the next time SpexCode launches.',
|
|
351
|
+
);
|
|
352
|
+
return lines.join('\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The proposals half's own dependency bundle — `root` defaults to this repo's real root
|
|
357
|
+
* (production), but a test passes a scratch `root` so the ledger never touches
|
|
358
|
+
* `~/.heyspex` or this repo's own `.heyspex/` store.
|
|
359
|
+
*
|
|
360
|
+
* @param {{ root?: string }} [opts]
|
|
361
|
+
*/
|
|
362
|
+
export function defaultProposalsDeps({ root = repoRoot() } = {}) {
|
|
363
|
+
return { ledgerPath: defaultProposalsLedgerPath(root) };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Print the pending-proposals summary to `stdout`, if there is one. Own failure boundary —
|
|
368
|
+
* this must never throw, and never affects the console relay or the engine half above.
|
|
369
|
+
*
|
|
370
|
+
* @param {{ ledgerPath: string }} deps
|
|
371
|
+
* @param {{ write: (chunk: string) => unknown }} [stdout]
|
|
372
|
+
* @returns {Promise<string|null>} the summary printed, or null when there was none.
|
|
373
|
+
*/
|
|
374
|
+
export async function reportPendingProposals(deps, stdout = process.stdout) {
|
|
375
|
+
try {
|
|
376
|
+
const summary = renderPendingProposalsSummary(deps);
|
|
377
|
+
if (summary) stdout.write(`${summary}\n`);
|
|
378
|
+
return summary;
|
|
379
|
+
} catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Race a promise against a timeout so the engine half can never stall the hook's own exit. */
|
|
385
|
+
function withTimeout(promise, ms, fallback) {
|
|
386
|
+
return Promise.race([
|
|
387
|
+
promise,
|
|
388
|
+
new Promise((resolve) => setTimeout(() => resolve(fallback), ms)),
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function main() {
|
|
393
|
+
const stdinRaw = await readStdin();
|
|
394
|
+
let payload = null;
|
|
395
|
+
if (stdinRaw) {
|
|
396
|
+
try {
|
|
397
|
+
payload = JSON.parse(stdinRaw);
|
|
398
|
+
} catch {
|
|
399
|
+
// Non-JSON stdin — nothing actionable.
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const sessionId =
|
|
404
|
+
payload?.session_id ??
|
|
405
|
+
payload?.conversation_id ??
|
|
406
|
+
process.env.CLAUDE_CODE_SESSION_ID ??
|
|
407
|
+
process.env.CURSOR_TRACE_ID ??
|
|
408
|
+
process.env.CURSOR_SESSION_ID ??
|
|
409
|
+
process.env.COMPOSER_SESSION_ID;
|
|
410
|
+
if (!sessionId) return;
|
|
411
|
+
|
|
412
|
+
// `source` may be absent on some payloads; forward what we have and let the
|
|
413
|
+
// server gate. (When absent, the server treats it as a non-closing event.)
|
|
414
|
+
const source = typeof payload?.source === 'string' ? payload.source : undefined;
|
|
415
|
+
|
|
416
|
+
await postSessionEnd(
|
|
417
|
+
resolveConsoleUrl(),
|
|
418
|
+
{ session_id: sessionId, source },
|
|
419
|
+
readCredentialsMcpToken(),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// Engine half (HS-U618) — independent of the relay above, own failure boundary, own
|
|
423
|
+
// timeout, never allowed to affect this hook's exit code.
|
|
424
|
+
await withTimeout(
|
|
425
|
+
runSessionEnd({ sessionId, source }, buildDefaultDeps()).catch(() => null),
|
|
426
|
+
ENGINE_TIMEOUT_MS,
|
|
427
|
+
null,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// Pending profile proposals half (HS-U95M, P9) — independent of both halves above, own
|
|
431
|
+
// failure boundary, own timeout. No-op (prints nothing) when nothing is pending.
|
|
432
|
+
await withTimeout(
|
|
433
|
+
reportPendingProposals(defaultProposalsDeps()).catch(() => null),
|
|
434
|
+
PROPOSALS_TIMEOUT_MS,
|
|
435
|
+
null,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Entry-guard: run main() only when executed as the hook, NOT when imported — the test file
|
|
440
|
+
// imports runSessionEnd/buildDefaultDeps/createLocalEventsExporter/createStoreLedger/
|
|
441
|
+
// createStoreClaims to unit-test the engine half with injected fakes, and importing this module
|
|
442
|
+
// must never itself read stdin or make a network/DB call. Mirrors preflight-reminder.mjs.
|
|
443
|
+
const INVOKED_DIRECTLY =
|
|
444
|
+
!!process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
445
|
+
if (INVOKED_DIRECTLY) {
|
|
446
|
+
main().catch(() => {
|
|
447
|
+
// Swallow — never break Claude Code on a hook failure.
|
|
448
|
+
});
|
|
449
|
+
}
|
|
@@ -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/settings-write-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/settings-write-gate.mjs';
|
|
10
|
+
|
|
11
|
+
await runGateAsHook(gate);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// settlement-check-gate.mjs — thin CLI shell around the pure
|
|
3
|
+
// evaluateSessionSettlement() decision ([[HS-N33D-CWPX-TASK-settlement-check-before-stop-companion]]).
|
|
4
|
+
//
|
|
5
|
+
// NOT WIRED into .claude/settings.json's live `Stop` hook array by this task
|
|
6
|
+
// (deliberately — that array governs THIS session's own Stop event, and
|
|
7
|
+
// wiring an unvalidated new gate into it live is a self-referential risk).
|
|
8
|
+
// This script is the thin shell a follow-up task can point a Stop hook at
|
|
9
|
+
// once validated outside an active session: it resolves the real signals
|
|
10
|
+
// (a settlement/drain status file, whether SpexCompanion's heartbeat is
|
|
11
|
+
// recent, whether git shows uncommitted changes) and prints the pure
|
|
12
|
+
// evaluator's verdict as JSON. Untested by design — the pure-core/thin-shell
|
|
13
|
+
// split ([[unit-test-authoring-protocol]] §1) puts all the tested logic in
|
|
14
|
+
// session-settlement-check.mjs; this file only wires real inputs to it.
|
|
15
|
+
//
|
|
16
|
+
// Usage (once wired as a Stop hook):
|
|
17
|
+
// node scripts/claude-hooks/settlement-check-gate.mjs
|
|
18
|
+
//
|
|
19
|
+
// Signals are read from environment variables so a hook runner (or a human,
|
|
20
|
+
// for now) can supply them without this script reaching into git/Companion
|
|
21
|
+
// state itself — that wiring is the follow-up task's job, not this one's.
|
|
22
|
+
|
|
23
|
+
import { evaluateSessionSettlement } from "../lib/session-settlement-check.mjs";
|
|
24
|
+
|
|
25
|
+
function boolEnv(name) {
|
|
26
|
+
return process.env[name] === "1" || process.env[name] === "true";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = evaluateSessionSettlement({
|
|
30
|
+
hasUnsettledWrites: boolEnv("HEYSPEX_HAS_UNSETTLED_WRITES"),
|
|
31
|
+
drainTriggeredImmediately: boolEnv("HEYSPEX_DRAIN_TRIGGERED_IMMEDIATELY"),
|
|
32
|
+
diskEditsThisSession: boolEnv("HEYSPEX_DISK_EDITS_THIS_SESSION"),
|
|
33
|
+
companionRunning: boolEnv("HEYSPEX_COMPANION_RUNNING"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
37
|
+
// Exit 0 always — this script is report-only until a follow-up task decides
|
|
38
|
+
// how a fired tripwire should affect the Stop hook's own exit code.
|
|
39
|
+
process.exit(0);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Back-compat re-exports — prefer terminal-close-shared.mjs for new code.
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
STATUS_REVIEW_RE,
|
|
6
|
+
armPendingTerminalClose,
|
|
7
|
+
clearPendingTerminalClose,
|
|
8
|
+
detectShipFromEditLike as detectFlip,
|
|
9
|
+
isPendingTerminalCloseArmed,
|
|
10
|
+
isRecordWipSuccess,
|
|
11
|
+
isSessionEndEvent,
|
|
12
|
+
isShipEvent as isPostToolUseShipEvent,
|
|
13
|
+
pendingTerminalClosePath,
|
|
14
|
+
readStdin,
|
|
15
|
+
repoRoot,
|
|
16
|
+
resolveCloseTerminalOnTaskCompleteFromSettings,
|
|
17
|
+
} from './terminal-close-shared.mjs';
|