heyspex 0.1.0 → 0.2.1
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/.claude-plugin/marketplace.json +14 -0
- package/README.md +21 -0
- package/bin/hs.mjs +1089 -104
- 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 +1127 -103
- package/dist/cli/hs-claim.mjs +981 -63
- package/dist/cli/hs-connect.mjs +10059 -1729
- package/dist/cli/hs-defer.mjs +5507 -3072
- package/dist/cli/hs-dev.mjs +576 -0
- package/dist/cli/hs-env.mjs +416 -302
- package/dist/cli/hs-index-rebuild.mjs +7334 -0
- package/dist/cli/hs-index-reconcile.mjs +8138 -0
- package/dist/cli/hs-init.mjs +2840 -149
- package/dist/cli/hs-install.mjs +7214 -594
- package/dist/cli/hs-join.mjs +10640 -2421
- package/dist/cli/hs-vault-pull.mjs +8157 -6370
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +4029 -1555
- package/dist/cli/hs-workspace.mjs +2702 -538
- 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 +30 -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 +28 -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 +33 -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 +32 -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/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +249 -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/README.md +309 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +80 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +308 -0
- package/plugins/heyspex/hooks/hooks.json +418 -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 +456 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +356 -0
- package/scripts/claude-hooks/_run.mjs +413 -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-execution-gate.mjs +51 -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 +577 -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/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -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 +86 -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 +279 -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 +451 -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/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +302 -0
- package/scripts/cursor-hooks/manifest.mjs +281 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +288 -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 +496 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +272 -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 +199 -61
- package/scripts/lib/customer-kit.mjs +1203 -0
- package/scripts/lib/dev-mode.mjs +249 -32
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-branch.mjs +196 -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 +13 -22
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -307
- 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/lib/window-placement.mjs +109 -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 +449 -0
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -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/_shell-write-targets.mjs +77 -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 +275 -0
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -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/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +196 -0
- package/spexharness/src/intent-directive.mjs +626 -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 +849 -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 +96 -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 +77 -8
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +117 -3
- package/vendor/heyspex-mcp/capture-focus.mjs +1061 -31
- package/vendor/heyspex-mcp/capture-outbox.mjs +15 -5
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +1264 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1147 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2074 -0
- package/vendor/heyspex-mcp/cli.mjs +207 -15524
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +602 -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 +822 -17
- package/vendor/heyspex-mcp/console-transport.mjs +172 -0
- package/vendor/heyspex-mcp/credentials.mjs +585 -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 +131 -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 +723 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +71 -15
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +60 -0
- package/vendor/heyspex-mcp/http-transport.mjs +302 -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 +107 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +142 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3377 -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 +908 -22
- 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 +327 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -970
- package/vendor/heyspex-mcp/promote-capture.mjs +388 -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 +666 -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 +191 -15
- 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 +83 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +625 -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/roadmap-default.mjs +70 -0
- 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 +109 -0
- package/vendor/heyspex-mcp/session-tools.mjs +162 -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 +453 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +304 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +150 -7
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +228 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +123 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2819 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +657 -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 +720 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +554 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +92 -0
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
package/dist/cli/hs-init.mjs
CHANGED
|
@@ -1,19 +1,890 @@
|
|
|
1
1
|
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
2
|
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
10
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
11
|
+
}) : x)(function(x) {
|
|
12
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
13
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
14
|
+
});
|
|
15
|
+
var __esm = (fn, res) => function __init() {
|
|
16
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
17
|
+
};
|
|
18
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
19
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
|
|
38
|
+
// scripts/lib/heyspex-home.mjs
|
|
39
|
+
import { homedir } from "node:os";
|
|
40
|
+
function heyspexHome(env = process.env) {
|
|
41
|
+
const override = env?.HEYSPEX_HOME;
|
|
42
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
43
|
+
return override.trim();
|
|
44
|
+
}
|
|
45
|
+
return homedir();
|
|
46
|
+
}
|
|
47
|
+
var init_heyspex_home = __esm({
|
|
48
|
+
"scripts/lib/heyspex-home.mjs"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// vendor/heyspex-mcp/credentials.mjs
|
|
54
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
55
|
+
import os, { homedir as homedir2 } from "node:os";
|
|
56
|
+
import { dirname, join, resolve as resolve2 } from "node:path";
|
|
57
|
+
var init_credentials = __esm({
|
|
58
|
+
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
59
|
+
"use strict";
|
|
60
|
+
init_heyspex_home();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// scripts/lib/dev-mode.mjs
|
|
65
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
66
|
+
import { homedir as homedir3 } from "node:os";
|
|
67
|
+
import { join as join2 } from "node:path";
|
|
68
|
+
function devModeFilePath(homeDir = heyspexHome()) {
|
|
69
|
+
return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
70
|
+
}
|
|
71
|
+
function normalizeMode(raw) {
|
|
72
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
73
|
+
if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
|
|
74
|
+
if (v === "dev") return { mode: "dev", reliance: null };
|
|
75
|
+
if (v === "local") return { mode: "dev", reliance: "local" };
|
|
76
|
+
if (v === "cloud") return { mode: "dev", reliance: "cloud" };
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function normalizeReliance(raw) {
|
|
80
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
81
|
+
return v === "local" || v === "cloud" ? v : null;
|
|
82
|
+
}
|
|
83
|
+
function deriveSemantics(mode, reliance) {
|
|
84
|
+
const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
|
|
85
|
+
const local = mode === "dev" && effectiveReliance === "local";
|
|
86
|
+
return {
|
|
87
|
+
reliance: effectiveReliance,
|
|
88
|
+
consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
|
|
89
|
+
readTier: local ? "disk" : "neon",
|
|
90
|
+
vaultWriteMode: local ? "local" : "cloud"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = readFile(devModeFilePath(homeDir));
|
|
97
|
+
} catch {
|
|
98
|
+
return { record: null, parseError: null };
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(raw);
|
|
102
|
+
const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
|
|
103
|
+
const reliance = normalizeReliance(parsed?.reliance);
|
|
104
|
+
if (!mode || mode === "dev" && !reliance) {
|
|
105
|
+
return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
record: {
|
|
109
|
+
mode,
|
|
110
|
+
reliance: mode === "prod" ? "cloud" : reliance,
|
|
111
|
+
console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
|
|
112
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
|
|
113
|
+
source: typeof parsed.source === "string" ? parsed.source : null
|
|
114
|
+
},
|
|
115
|
+
parseError: null
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function resolveDevMode({
|
|
122
|
+
env = process.env,
|
|
123
|
+
homeDir = heyspexHome(),
|
|
124
|
+
readFile = defaultReadFile,
|
|
125
|
+
cloudSetting = void 0
|
|
126
|
+
} = {}) {
|
|
127
|
+
const local = readLocalDevModeRecord({ homeDir, readFile });
|
|
128
|
+
const cloudKnown = cloudSetting !== void 0;
|
|
129
|
+
const cloudValue = cloudKnown && cloudSetting ? {
|
|
130
|
+
mode: cloudSetting.mode === "dev" ? "dev" : "prod",
|
|
131
|
+
reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
|
|
132
|
+
} : null;
|
|
133
|
+
let mode;
|
|
134
|
+
let reliance;
|
|
135
|
+
let source;
|
|
136
|
+
let relianceSource;
|
|
137
|
+
const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
|
|
138
|
+
if (envParsed) {
|
|
139
|
+
mode = envParsed.mode;
|
|
140
|
+
const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
|
|
141
|
+
reliance = envReliance ?? "local";
|
|
142
|
+
source = "env";
|
|
143
|
+
relianceSource = envReliance ? "env" : "assumed-local";
|
|
144
|
+
} else if (local.record) {
|
|
145
|
+
mode = local.record.mode;
|
|
146
|
+
reliance = local.record.reliance;
|
|
147
|
+
source = "local-override";
|
|
148
|
+
} else if (cloudValue) {
|
|
149
|
+
mode = cloudValue.mode;
|
|
150
|
+
reliance = cloudValue.reliance;
|
|
151
|
+
source = "cloud-setting";
|
|
152
|
+
} else {
|
|
153
|
+
mode = "prod";
|
|
154
|
+
reliance = "cloud";
|
|
155
|
+
source = "default";
|
|
156
|
+
}
|
|
157
|
+
const semantics = deriveSemantics(mode, reliance);
|
|
158
|
+
const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
|
|
159
|
+
const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
|
|
160
|
+
return {
|
|
161
|
+
mode,
|
|
162
|
+
reliance: semantics.reliance,
|
|
163
|
+
consoleUrl,
|
|
164
|
+
readTier: semantics.readTier,
|
|
165
|
+
vaultWriteMode: semantics.vaultWriteMode,
|
|
166
|
+
sources: {
|
|
167
|
+
mode: source,
|
|
168
|
+
reliance: relianceSource ?? source,
|
|
169
|
+
consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
|
|
170
|
+
readTier: "derived",
|
|
171
|
+
vaultWriteMode: "derived"
|
|
172
|
+
},
|
|
173
|
+
localOverride: {
|
|
174
|
+
present: local.record !== null,
|
|
175
|
+
record: local.record,
|
|
176
|
+
parseError: local.parseError,
|
|
177
|
+
staleAgainstCloud
|
|
178
|
+
},
|
|
179
|
+
cloudSetting: { known: cloudKnown, value: cloudValue }
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
183
|
+
var init_dev_mode = __esm({
|
|
184
|
+
"scripts/lib/dev-mode.mjs"() {
|
|
185
|
+
"use strict";
|
|
186
|
+
init_heyspex_home();
|
|
187
|
+
init_credentials();
|
|
188
|
+
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
189
|
+
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
190
|
+
DEV_MODE_FILENAME = "dev-mode.json";
|
|
191
|
+
defaultReadFile = (p) => readFileSync2(p, "utf8");
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// node_modules/kind-of/index.js
|
|
196
|
+
var require_kind_of = __commonJS({
|
|
197
|
+
"node_modules/kind-of/index.js"(exports2, module2) {
|
|
198
|
+
"use strict";
|
|
199
|
+
var toString = Object.prototype.toString;
|
|
200
|
+
module2.exports = function kindOf(val) {
|
|
201
|
+
if (val === void 0) return "undefined";
|
|
202
|
+
if (val === null) return "null";
|
|
203
|
+
var type = typeof val;
|
|
204
|
+
if (type === "boolean") return "boolean";
|
|
205
|
+
if (type === "string") return "string";
|
|
206
|
+
if (type === "number") return "number";
|
|
207
|
+
if (type === "symbol") return "symbol";
|
|
208
|
+
if (type === "function") {
|
|
209
|
+
return isGeneratorFn(val) ? "generatorfunction" : "function";
|
|
210
|
+
}
|
|
211
|
+
if (isArray(val)) return "array";
|
|
212
|
+
if (isBuffer(val)) return "buffer";
|
|
213
|
+
if (isArguments(val)) return "arguments";
|
|
214
|
+
if (isDate(val)) return "date";
|
|
215
|
+
if (isError(val)) return "error";
|
|
216
|
+
if (isRegexp(val)) return "regexp";
|
|
217
|
+
switch (ctorName(val)) {
|
|
218
|
+
case "Symbol":
|
|
219
|
+
return "symbol";
|
|
220
|
+
case "Promise":
|
|
221
|
+
return "promise";
|
|
222
|
+
// Set, Map, WeakSet, WeakMap
|
|
223
|
+
case "WeakMap":
|
|
224
|
+
return "weakmap";
|
|
225
|
+
case "WeakSet":
|
|
226
|
+
return "weakset";
|
|
227
|
+
case "Map":
|
|
228
|
+
return "map";
|
|
229
|
+
case "Set":
|
|
230
|
+
return "set";
|
|
231
|
+
// 8-bit typed arrays
|
|
232
|
+
case "Int8Array":
|
|
233
|
+
return "int8array";
|
|
234
|
+
case "Uint8Array":
|
|
235
|
+
return "uint8array";
|
|
236
|
+
case "Uint8ClampedArray":
|
|
237
|
+
return "uint8clampedarray";
|
|
238
|
+
// 16-bit typed arrays
|
|
239
|
+
case "Int16Array":
|
|
240
|
+
return "int16array";
|
|
241
|
+
case "Uint16Array":
|
|
242
|
+
return "uint16array";
|
|
243
|
+
// 32-bit typed arrays
|
|
244
|
+
case "Int32Array":
|
|
245
|
+
return "int32array";
|
|
246
|
+
case "Uint32Array":
|
|
247
|
+
return "uint32array";
|
|
248
|
+
case "Float32Array":
|
|
249
|
+
return "float32array";
|
|
250
|
+
case "Float64Array":
|
|
251
|
+
return "float64array";
|
|
252
|
+
}
|
|
253
|
+
if (isGeneratorObj(val)) {
|
|
254
|
+
return "generator";
|
|
255
|
+
}
|
|
256
|
+
type = toString.call(val);
|
|
257
|
+
switch (type) {
|
|
258
|
+
case "[object Object]":
|
|
259
|
+
return "object";
|
|
260
|
+
// iterators
|
|
261
|
+
case "[object Map Iterator]":
|
|
262
|
+
return "mapiterator";
|
|
263
|
+
case "[object Set Iterator]":
|
|
264
|
+
return "setiterator";
|
|
265
|
+
case "[object String Iterator]":
|
|
266
|
+
return "stringiterator";
|
|
267
|
+
case "[object Array Iterator]":
|
|
268
|
+
return "arrayiterator";
|
|
269
|
+
}
|
|
270
|
+
return type.slice(8, -1).toLowerCase().replace(/\s/g, "");
|
|
271
|
+
};
|
|
272
|
+
function ctorName(val) {
|
|
273
|
+
return typeof val.constructor === "function" ? val.constructor.name : null;
|
|
274
|
+
}
|
|
275
|
+
function isArray(val) {
|
|
276
|
+
if (Array.isArray) return Array.isArray(val);
|
|
277
|
+
return val instanceof Array;
|
|
278
|
+
}
|
|
279
|
+
function isError(val) {
|
|
280
|
+
return val instanceof Error || typeof val.message === "string" && val.constructor && typeof val.constructor.stackTraceLimit === "number";
|
|
281
|
+
}
|
|
282
|
+
function isDate(val) {
|
|
283
|
+
if (val instanceof Date) return true;
|
|
284
|
+
return typeof val.toDateString === "function" && typeof val.getDate === "function" && typeof val.setDate === "function";
|
|
285
|
+
}
|
|
286
|
+
function isRegexp(val) {
|
|
287
|
+
if (val instanceof RegExp) return true;
|
|
288
|
+
return typeof val.flags === "string" && typeof val.ignoreCase === "boolean" && typeof val.multiline === "boolean" && typeof val.global === "boolean";
|
|
289
|
+
}
|
|
290
|
+
function isGeneratorFn(name, val) {
|
|
291
|
+
return ctorName(name) === "GeneratorFunction";
|
|
292
|
+
}
|
|
293
|
+
function isGeneratorObj(val) {
|
|
294
|
+
return typeof val.throw === "function" && typeof val.return === "function" && typeof val.next === "function";
|
|
295
|
+
}
|
|
296
|
+
function isArguments(val) {
|
|
297
|
+
try {
|
|
298
|
+
if (typeof val.length === "number" && typeof val.callee === "function") {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (err.message.indexOf("callee") !== -1) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
function isBuffer(val) {
|
|
309
|
+
if (val.constructor && typeof val.constructor.isBuffer === "function") {
|
|
310
|
+
return val.constructor.isBuffer(val);
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// node_modules/is-extendable/index.js
|
|
318
|
+
var require_is_extendable = __commonJS({
|
|
319
|
+
"node_modules/is-extendable/index.js"(exports2, module2) {
|
|
320
|
+
"use strict";
|
|
321
|
+
module2.exports = function isExtendable(val) {
|
|
322
|
+
return typeof val !== "undefined" && val !== null && (typeof val === "object" || typeof val === "function");
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// node_modules/extend-shallow/index.js
|
|
328
|
+
var require_extend_shallow = __commonJS({
|
|
329
|
+
"node_modules/extend-shallow/index.js"(exports2, module2) {
|
|
330
|
+
"use strict";
|
|
331
|
+
var isObject = require_is_extendable();
|
|
332
|
+
module2.exports = function extend(o) {
|
|
333
|
+
if (!isObject(o)) {
|
|
334
|
+
o = {};
|
|
335
|
+
}
|
|
336
|
+
var len = arguments.length;
|
|
337
|
+
for (var i = 1; i < len; i++) {
|
|
338
|
+
var obj = arguments[i];
|
|
339
|
+
if (isObject(obj)) {
|
|
340
|
+
assign(o, obj);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return o;
|
|
344
|
+
};
|
|
345
|
+
function assign(a, b) {
|
|
346
|
+
for (var key in b) {
|
|
347
|
+
if (hasOwn(b, key)) {
|
|
348
|
+
a[key] = b[key];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function hasOwn(obj, key) {
|
|
353
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// node_modules/section-matter/index.js
|
|
359
|
+
var require_section_matter = __commonJS({
|
|
360
|
+
"node_modules/section-matter/index.js"(exports2, module2) {
|
|
361
|
+
"use strict";
|
|
362
|
+
var typeOf = require_kind_of();
|
|
363
|
+
var extend = require_extend_shallow();
|
|
364
|
+
module2.exports = function(input, options2) {
|
|
365
|
+
if (typeof options2 === "function") {
|
|
366
|
+
options2 = { parse: options2 };
|
|
367
|
+
}
|
|
368
|
+
var file = toObject(input);
|
|
369
|
+
var defaults = { section_delimiter: "---", parse: identity };
|
|
370
|
+
var opts = extend({}, defaults, options2);
|
|
371
|
+
var delim = opts.section_delimiter;
|
|
372
|
+
var lines = file.content.split(/\r?\n/);
|
|
373
|
+
var sections = null;
|
|
374
|
+
var section = createSection();
|
|
375
|
+
var content = [];
|
|
376
|
+
var stack = [];
|
|
377
|
+
function initSections(val) {
|
|
378
|
+
file.content = val;
|
|
379
|
+
sections = [];
|
|
380
|
+
content = [];
|
|
381
|
+
}
|
|
382
|
+
function closeSection(val) {
|
|
383
|
+
if (stack.length) {
|
|
384
|
+
section.key = getKey(stack[0], delim);
|
|
385
|
+
section.content = val;
|
|
386
|
+
opts.parse(section, sections);
|
|
387
|
+
sections.push(section);
|
|
388
|
+
section = createSection();
|
|
389
|
+
content = [];
|
|
390
|
+
stack = [];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
for (var i = 0; i < lines.length; i++) {
|
|
394
|
+
var line = lines[i];
|
|
395
|
+
var len = stack.length;
|
|
396
|
+
var ln = line.trim();
|
|
397
|
+
if (isDelimiter(ln, delim)) {
|
|
398
|
+
if (ln.length === 3 && i !== 0) {
|
|
399
|
+
if (len === 0 || len === 2) {
|
|
400
|
+
content.push(line);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
stack.push(ln);
|
|
404
|
+
section.data = content.join("\n");
|
|
405
|
+
content = [];
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (sections === null) {
|
|
409
|
+
initSections(content.join("\n"));
|
|
410
|
+
}
|
|
411
|
+
if (len === 2) {
|
|
412
|
+
closeSection(content.join("\n"));
|
|
413
|
+
}
|
|
414
|
+
stack.push(ln);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
content.push(line);
|
|
418
|
+
}
|
|
419
|
+
if (sections === null) {
|
|
420
|
+
initSections(content.join("\n"));
|
|
421
|
+
} else {
|
|
422
|
+
closeSection(content.join("\n"));
|
|
423
|
+
}
|
|
424
|
+
file.sections = sections;
|
|
425
|
+
return file;
|
|
426
|
+
};
|
|
427
|
+
function isDelimiter(line, delim) {
|
|
428
|
+
if (line.slice(0, delim.length) !== delim) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
if (line.charAt(delim.length + 1) === delim.slice(-1)) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
function toObject(input) {
|
|
437
|
+
if (typeOf(input) !== "object") {
|
|
438
|
+
input = { content: input };
|
|
439
|
+
}
|
|
440
|
+
if (typeof input.content !== "string" && !isBuffer(input.content)) {
|
|
441
|
+
throw new TypeError("expected a buffer or string");
|
|
442
|
+
}
|
|
443
|
+
input.content = input.content.toString();
|
|
444
|
+
input.sections = [];
|
|
445
|
+
return input;
|
|
446
|
+
}
|
|
447
|
+
function getKey(val, delim) {
|
|
448
|
+
return val ? val.slice(delim.length).trim() : "";
|
|
449
|
+
}
|
|
450
|
+
function createSection() {
|
|
451
|
+
return { key: "", data: "", content: "" };
|
|
452
|
+
}
|
|
453
|
+
function identity(val) {
|
|
454
|
+
return val;
|
|
455
|
+
}
|
|
456
|
+
function isBuffer(val) {
|
|
457
|
+
if (val && val.constructor && typeof val.constructor.isBuffer === "function") {
|
|
458
|
+
return val.constructor.isBuffer(val);
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// node_modules/gray-matter/lib/engines.js
|
|
466
|
+
var require_engines = __commonJS({
|
|
467
|
+
"node_modules/gray-matter/lib/engines.js"(exports, module) {
|
|
468
|
+
"use strict";
|
|
469
|
+
var yaml = __require("js-yaml");
|
|
470
|
+
var engines = exports = module.exports;
|
|
471
|
+
engines.yaml = {
|
|
472
|
+
parse: yaml.safeLoad.bind(yaml),
|
|
473
|
+
stringify: yaml.safeDump.bind(yaml)
|
|
474
|
+
};
|
|
475
|
+
engines.json = {
|
|
476
|
+
parse: JSON.parse.bind(JSON),
|
|
477
|
+
stringify: function(obj, options2) {
|
|
478
|
+
const opts = Object.assign({ replacer: null, space: 2 }, options2);
|
|
479
|
+
return JSON.stringify(obj, opts.replacer, opts.space);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
engines.javascript = {
|
|
483
|
+
parse: function parse(str, options, wrap) {
|
|
484
|
+
try {
|
|
485
|
+
if (wrap !== false) {
|
|
486
|
+
str = "(function() {\nreturn " + str.trim() + ";\n}());";
|
|
487
|
+
}
|
|
488
|
+
return eval(str) || {};
|
|
489
|
+
} catch (err) {
|
|
490
|
+
if (wrap !== false && /(unexpected|identifier)/i.test(err.message)) {
|
|
491
|
+
return parse(str, options, false);
|
|
492
|
+
}
|
|
493
|
+
throw new SyntaxError(err);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
stringify: function() {
|
|
497
|
+
throw new Error("stringifying JavaScript is not supported");
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// node_modules/strip-bom-string/index.js
|
|
504
|
+
var require_strip_bom_string = __commonJS({
|
|
505
|
+
"node_modules/strip-bom-string/index.js"(exports2, module2) {
|
|
506
|
+
"use strict";
|
|
507
|
+
module2.exports = function(str2) {
|
|
508
|
+
if (typeof str2 === "string" && str2.charAt(0) === "\uFEFF") {
|
|
509
|
+
return str2.slice(1);
|
|
510
|
+
}
|
|
511
|
+
return str2;
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// node_modules/gray-matter/lib/utils.js
|
|
517
|
+
var require_utils = __commonJS({
|
|
518
|
+
"node_modules/gray-matter/lib/utils.js"(exports2) {
|
|
519
|
+
"use strict";
|
|
520
|
+
var stripBom = require_strip_bom_string();
|
|
521
|
+
var typeOf = require_kind_of();
|
|
522
|
+
exports2.define = function(obj, key, val) {
|
|
523
|
+
Reflect.defineProperty(obj, key, {
|
|
524
|
+
enumerable: false,
|
|
525
|
+
configurable: true,
|
|
526
|
+
writable: true,
|
|
527
|
+
value: val
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
exports2.isBuffer = function(val) {
|
|
531
|
+
return typeOf(val) === "buffer";
|
|
532
|
+
};
|
|
533
|
+
exports2.isObject = function(val) {
|
|
534
|
+
return typeOf(val) === "object";
|
|
535
|
+
};
|
|
536
|
+
exports2.toBuffer = function(input) {
|
|
537
|
+
return typeof input === "string" ? Buffer.from(input) : input;
|
|
538
|
+
};
|
|
539
|
+
exports2.toString = function(input) {
|
|
540
|
+
if (exports2.isBuffer(input)) return stripBom(String(input));
|
|
541
|
+
if (typeof input !== "string") {
|
|
542
|
+
throw new TypeError("expected input to be a string or buffer");
|
|
543
|
+
}
|
|
544
|
+
return stripBom(input);
|
|
545
|
+
};
|
|
546
|
+
exports2.arrayify = function(val) {
|
|
547
|
+
return val ? Array.isArray(val) ? val : [val] : [];
|
|
548
|
+
};
|
|
549
|
+
exports2.startsWith = function(str2, substr, len) {
|
|
550
|
+
if (typeof len !== "number") len = substr.length;
|
|
551
|
+
return str2.slice(0, len) === substr;
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// node_modules/gray-matter/lib/defaults.js
|
|
557
|
+
var require_defaults = __commonJS({
|
|
558
|
+
"node_modules/gray-matter/lib/defaults.js"(exports2, module2) {
|
|
559
|
+
"use strict";
|
|
560
|
+
var engines2 = require_engines();
|
|
561
|
+
var utils = require_utils();
|
|
562
|
+
module2.exports = function(options2) {
|
|
563
|
+
const opts = Object.assign({}, options2);
|
|
564
|
+
opts.delimiters = utils.arrayify(opts.delims || opts.delimiters || "---");
|
|
565
|
+
if (opts.delimiters.length === 1) {
|
|
566
|
+
opts.delimiters.push(opts.delimiters[0]);
|
|
567
|
+
}
|
|
568
|
+
opts.language = (opts.language || opts.lang || "yaml").toLowerCase();
|
|
569
|
+
opts.engines = Object.assign({}, engines2, opts.parsers, opts.engines);
|
|
570
|
+
return opts;
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// node_modules/gray-matter/lib/engine.js
|
|
576
|
+
var require_engine = __commonJS({
|
|
577
|
+
"node_modules/gray-matter/lib/engine.js"(exports2, module2) {
|
|
578
|
+
"use strict";
|
|
579
|
+
module2.exports = function(name, options2) {
|
|
580
|
+
let engine = options2.engines[name] || options2.engines[aliase(name)];
|
|
581
|
+
if (typeof engine === "undefined") {
|
|
582
|
+
throw new Error('gray-matter engine "' + name + '" is not registered');
|
|
583
|
+
}
|
|
584
|
+
if (typeof engine === "function") {
|
|
585
|
+
engine = { parse: engine };
|
|
586
|
+
}
|
|
587
|
+
return engine;
|
|
588
|
+
};
|
|
589
|
+
function aliase(name) {
|
|
590
|
+
switch (name.toLowerCase()) {
|
|
591
|
+
case "js":
|
|
592
|
+
case "javascript":
|
|
593
|
+
return "javascript";
|
|
594
|
+
case "coffee":
|
|
595
|
+
case "coffeescript":
|
|
596
|
+
case "cson":
|
|
597
|
+
return "coffee";
|
|
598
|
+
case "yaml":
|
|
599
|
+
case "yml":
|
|
600
|
+
return "yaml";
|
|
601
|
+
default: {
|
|
602
|
+
return name;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// node_modules/gray-matter/lib/stringify.js
|
|
610
|
+
var require_stringify = __commonJS({
|
|
611
|
+
"node_modules/gray-matter/lib/stringify.js"(exports2, module2) {
|
|
612
|
+
"use strict";
|
|
613
|
+
var typeOf = require_kind_of();
|
|
614
|
+
var getEngine = require_engine();
|
|
615
|
+
var defaults = require_defaults();
|
|
616
|
+
module2.exports = function(file, data, options2) {
|
|
617
|
+
if (data == null && options2 == null) {
|
|
618
|
+
switch (typeOf(file)) {
|
|
619
|
+
case "object":
|
|
620
|
+
data = file.data;
|
|
621
|
+
options2 = {};
|
|
622
|
+
break;
|
|
623
|
+
case "string":
|
|
624
|
+
return file;
|
|
625
|
+
default: {
|
|
626
|
+
throw new TypeError("expected file to be a string or object");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const str2 = file.content;
|
|
631
|
+
const opts = defaults(options2);
|
|
632
|
+
if (data == null) {
|
|
633
|
+
if (!opts.data) return file;
|
|
634
|
+
data = opts.data;
|
|
635
|
+
}
|
|
636
|
+
const language = file.language || opts.language;
|
|
637
|
+
const engine = getEngine(language, opts);
|
|
638
|
+
if (typeof engine.stringify !== "function") {
|
|
639
|
+
throw new TypeError('expected "' + language + '.stringify" to be a function');
|
|
640
|
+
}
|
|
641
|
+
data = Object.assign({}, file.data, data);
|
|
642
|
+
const open = opts.delimiters[0];
|
|
643
|
+
const close = opts.delimiters[1];
|
|
644
|
+
const matter2 = engine.stringify(data, options2).trim();
|
|
645
|
+
let buf = "";
|
|
646
|
+
if (matter2 !== "{}") {
|
|
647
|
+
buf = newline(open) + newline(matter2) + newline(close);
|
|
648
|
+
}
|
|
649
|
+
if (typeof file.excerpt === "string" && file.excerpt !== "") {
|
|
650
|
+
if (str2.indexOf(file.excerpt.trim()) === -1) {
|
|
651
|
+
buf += newline(file.excerpt) + newline(close);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return buf + newline(str2);
|
|
655
|
+
};
|
|
656
|
+
function newline(str2) {
|
|
657
|
+
return str2.slice(-1) !== "\n" ? str2 + "\n" : str2;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// node_modules/gray-matter/lib/excerpt.js
|
|
663
|
+
var require_excerpt = __commonJS({
|
|
664
|
+
"node_modules/gray-matter/lib/excerpt.js"(exports2, module2) {
|
|
665
|
+
"use strict";
|
|
666
|
+
var defaults = require_defaults();
|
|
667
|
+
module2.exports = function(file, options2) {
|
|
668
|
+
const opts = defaults(options2);
|
|
669
|
+
if (file.data == null) {
|
|
670
|
+
file.data = {};
|
|
671
|
+
}
|
|
672
|
+
if (typeof opts.excerpt === "function") {
|
|
673
|
+
return opts.excerpt(file, opts);
|
|
674
|
+
}
|
|
675
|
+
const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
|
|
676
|
+
if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
|
|
677
|
+
return file;
|
|
678
|
+
}
|
|
679
|
+
const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
|
|
680
|
+
const idx = file.content.indexOf(delimiter2);
|
|
681
|
+
if (idx !== -1) {
|
|
682
|
+
file.excerpt = file.content.slice(0, idx);
|
|
683
|
+
}
|
|
684
|
+
return file;
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// node_modules/gray-matter/lib/to-file.js
|
|
690
|
+
var require_to_file = __commonJS({
|
|
691
|
+
"node_modules/gray-matter/lib/to-file.js"(exports2, module2) {
|
|
692
|
+
"use strict";
|
|
693
|
+
var typeOf = require_kind_of();
|
|
694
|
+
var stringify = require_stringify();
|
|
695
|
+
var utils = require_utils();
|
|
696
|
+
module2.exports = function(file) {
|
|
697
|
+
if (typeOf(file) !== "object") {
|
|
698
|
+
file = { content: file };
|
|
699
|
+
}
|
|
700
|
+
if (typeOf(file.data) !== "object") {
|
|
701
|
+
file.data = {};
|
|
702
|
+
}
|
|
703
|
+
if (file.contents && file.content == null) {
|
|
704
|
+
file.content = file.contents;
|
|
705
|
+
}
|
|
706
|
+
utils.define(file, "orig", utils.toBuffer(file.content));
|
|
707
|
+
utils.define(file, "language", file.language || "");
|
|
708
|
+
utils.define(file, "matter", file.matter || "");
|
|
709
|
+
utils.define(file, "stringify", function(data, options2) {
|
|
710
|
+
if (options2 && options2.language) {
|
|
711
|
+
file.language = options2.language;
|
|
712
|
+
}
|
|
713
|
+
return stringify(file, data, options2);
|
|
714
|
+
});
|
|
715
|
+
file.content = utils.toString(file.content);
|
|
716
|
+
file.isEmpty = false;
|
|
717
|
+
file.excerpt = "";
|
|
718
|
+
return file;
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// node_modules/gray-matter/lib/parse.js
|
|
724
|
+
var require_parse = __commonJS({
|
|
725
|
+
"node_modules/gray-matter/lib/parse.js"(exports2, module2) {
|
|
726
|
+
"use strict";
|
|
727
|
+
var getEngine = require_engine();
|
|
728
|
+
var defaults = require_defaults();
|
|
729
|
+
module2.exports = function(language, str2, options2) {
|
|
730
|
+
const opts = defaults(options2);
|
|
731
|
+
const engine = getEngine(language, opts);
|
|
732
|
+
if (typeof engine.parse !== "function") {
|
|
733
|
+
throw new TypeError('expected "' + language + '.parse" to be a function');
|
|
734
|
+
}
|
|
735
|
+
return engine.parse(str2, opts);
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// node_modules/gray-matter/index.js
|
|
741
|
+
var require_gray_matter = __commonJS({
|
|
742
|
+
"node_modules/gray-matter/index.js"(exports2, module2) {
|
|
743
|
+
"use strict";
|
|
744
|
+
var fs4 = __require("fs");
|
|
745
|
+
var sections = require_section_matter();
|
|
746
|
+
var defaults = require_defaults();
|
|
747
|
+
var stringify = require_stringify();
|
|
748
|
+
var excerpt = require_excerpt();
|
|
749
|
+
var engines2 = require_engines();
|
|
750
|
+
var toFile = require_to_file();
|
|
751
|
+
var parse2 = require_parse();
|
|
752
|
+
var utils = require_utils();
|
|
753
|
+
function matter2(input, options2) {
|
|
754
|
+
if (input === "") {
|
|
755
|
+
return { data: {}, content: input, excerpt: "", orig: input };
|
|
756
|
+
}
|
|
757
|
+
let file = toFile(input);
|
|
758
|
+
const cached = matter2.cache[file.content];
|
|
759
|
+
if (!options2) {
|
|
760
|
+
if (cached) {
|
|
761
|
+
file = Object.assign({}, cached);
|
|
762
|
+
file.orig = cached.orig;
|
|
763
|
+
return file;
|
|
764
|
+
}
|
|
765
|
+
matter2.cache[file.content] = file;
|
|
766
|
+
}
|
|
767
|
+
return parseMatter(file, options2);
|
|
768
|
+
}
|
|
769
|
+
function parseMatter(file, options2) {
|
|
770
|
+
const opts = defaults(options2);
|
|
771
|
+
const open = opts.delimiters[0];
|
|
772
|
+
const close = "\n" + opts.delimiters[1];
|
|
773
|
+
let str2 = file.content;
|
|
774
|
+
if (opts.language) {
|
|
775
|
+
file.language = opts.language;
|
|
776
|
+
}
|
|
777
|
+
const openLen = open.length;
|
|
778
|
+
if (!utils.startsWith(str2, open, openLen)) {
|
|
779
|
+
excerpt(file, opts);
|
|
780
|
+
return file;
|
|
781
|
+
}
|
|
782
|
+
if (str2.charAt(openLen) === open.slice(-1)) {
|
|
783
|
+
return file;
|
|
784
|
+
}
|
|
785
|
+
str2 = str2.slice(openLen);
|
|
786
|
+
const len = str2.length;
|
|
787
|
+
const language = matter2.language(str2, opts);
|
|
788
|
+
if (language.name) {
|
|
789
|
+
file.language = language.name;
|
|
790
|
+
str2 = str2.slice(language.raw.length);
|
|
791
|
+
}
|
|
792
|
+
let closeIndex = str2.indexOf(close);
|
|
793
|
+
if (closeIndex === -1) {
|
|
794
|
+
closeIndex = len;
|
|
795
|
+
}
|
|
796
|
+
file.matter = str2.slice(0, closeIndex);
|
|
797
|
+
const block = file.matter.replace(/^\s*#[^\n]+/gm, "").trim();
|
|
798
|
+
if (block === "") {
|
|
799
|
+
file.isEmpty = true;
|
|
800
|
+
file.empty = file.content;
|
|
801
|
+
file.data = {};
|
|
802
|
+
} else {
|
|
803
|
+
file.data = parse2(file.language, file.matter, opts);
|
|
804
|
+
}
|
|
805
|
+
if (closeIndex === len) {
|
|
806
|
+
file.content = "";
|
|
807
|
+
} else {
|
|
808
|
+
file.content = str2.slice(closeIndex + close.length);
|
|
809
|
+
if (file.content[0] === "\r") {
|
|
810
|
+
file.content = file.content.slice(1);
|
|
811
|
+
}
|
|
812
|
+
if (file.content[0] === "\n") {
|
|
813
|
+
file.content = file.content.slice(1);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
excerpt(file, opts);
|
|
817
|
+
if (opts.sections === true || typeof opts.section === "function") {
|
|
818
|
+
sections(file, opts.section);
|
|
819
|
+
}
|
|
820
|
+
return file;
|
|
821
|
+
}
|
|
822
|
+
matter2.engines = engines2;
|
|
823
|
+
matter2.stringify = function(file, data, options2) {
|
|
824
|
+
if (typeof file === "string") file = matter2(file, options2);
|
|
825
|
+
return stringify(file, data, options2);
|
|
826
|
+
};
|
|
827
|
+
matter2.read = function(filepath, options2) {
|
|
828
|
+
const str2 = fs4.readFileSync(filepath, "utf8");
|
|
829
|
+
const file = matter2(str2, options2);
|
|
830
|
+
file.path = filepath;
|
|
831
|
+
return file;
|
|
832
|
+
};
|
|
833
|
+
matter2.test = function(str2, options2) {
|
|
834
|
+
return utils.startsWith(str2, defaults(options2).delimiters[0]);
|
|
835
|
+
};
|
|
836
|
+
matter2.language = function(str2, options2) {
|
|
837
|
+
const opts = defaults(options2);
|
|
838
|
+
const open = opts.delimiters[0];
|
|
839
|
+
if (matter2.test(str2)) {
|
|
840
|
+
str2 = str2.slice(open.length);
|
|
841
|
+
}
|
|
842
|
+
const language = str2.slice(0, str2.search(/\r?\n/));
|
|
843
|
+
return {
|
|
844
|
+
raw: language,
|
|
845
|
+
name: language ? language.trim() : ""
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
matter2.cache = {};
|
|
849
|
+
matter2.clearCache = function() {
|
|
850
|
+
matter2.cache = {};
|
|
851
|
+
};
|
|
852
|
+
module2.exports = matter2;
|
|
853
|
+
}
|
|
854
|
+
});
|
|
3
855
|
|
|
4
856
|
// spex-console/bin/hs-init.ts
|
|
5
857
|
import {
|
|
6
858
|
copyFileSync,
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
readFileSync as
|
|
11
|
-
|
|
859
|
+
existsSync as existsSync5,
|
|
860
|
+
mkdirSync as mkdirSync6,
|
|
861
|
+
readdirSync as readdirSync2,
|
|
862
|
+
readFileSync as readFileSync11,
|
|
863
|
+
statSync,
|
|
864
|
+
writeFileSync as writeFileSync6
|
|
12
865
|
} from "node:fs";
|
|
13
|
-
import { dirname, join as
|
|
14
|
-
import
|
|
866
|
+
import { dirname as dirname6, join as join11, resolve as resolve5 } from "node:path";
|
|
867
|
+
import process4 from "node:process";
|
|
15
868
|
import { createInterface } from "node:readline/promises";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
869
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
870
|
+
|
|
871
|
+
// scripts/lib/shared-path-helpers.mjs
|
|
872
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
873
|
+
function resolveCodeRoot({
|
|
874
|
+
explicit,
|
|
875
|
+
packageRoot,
|
|
876
|
+
vaultRoot,
|
|
877
|
+
env = process.env
|
|
878
|
+
} = {}) {
|
|
879
|
+
const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
|
|
880
|
+
if (fromExplicit) return fromExplicit;
|
|
881
|
+
const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
|
|
882
|
+
if (fromEnv) return fromEnv;
|
|
883
|
+
const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
|
|
884
|
+
if (fromPackage) return fromPackage;
|
|
885
|
+
const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
|
|
886
|
+
return fromVault;
|
|
887
|
+
}
|
|
17
888
|
|
|
18
889
|
// spex-console/src/lib/cli/customer-claude-md.ts
|
|
19
890
|
function renderCustomerClaudeMd({
|
|
@@ -65,15 +936,38 @@ The vault under \`specs/\` is the only persistent queue. File new work via \`hey
|
|
|
65
936
|
This CLAUDE.md was generated for **${vaultName}** from HeySpex template version **${templateVersion}** \u2014 it is yours to edit. Unlike HeySpex's own platform contract (which carries HeySpex's internal build tooling), everything above applies to *your* vault and *your* agents. Edit it freely as your project's conventions evolve; a later template update will not overwrite your changes.
|
|
66
937
|
`;
|
|
67
938
|
}
|
|
68
|
-
function
|
|
939
|
+
function writeProjectContract(ctx, options2 = {}) {
|
|
940
|
+
const content = renderCustomerClaudeMd({
|
|
941
|
+
vaultName: ctx.vaultName,
|
|
942
|
+
templateVersion: ctx.templateVersion
|
|
943
|
+
});
|
|
944
|
+
const results = [];
|
|
945
|
+
for (const file of options2.files ?? ["CLAUDE.md"]) {
|
|
946
|
+
const destPath = ctx.deps.joinPath(ctx.targetDir, file);
|
|
947
|
+
if (!ctx.deps.exists(destPath)) {
|
|
948
|
+
ctx.deps.writeFile(destPath, content);
|
|
949
|
+
ctx.recordWrite?.(destPath);
|
|
950
|
+
results.push({ file, action: "created", path: destPath });
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (ctx.deps.readFile?.(destPath) === content) {
|
|
954
|
+
results.push({ file, action: "unchanged" });
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const templatePath = ctx.deps.joinPath(ctx.targetDir, `${file}.template`);
|
|
958
|
+
if (ctx.deps.readFile?.(templatePath) !== content) {
|
|
959
|
+
ctx.deps.writeFile(templatePath, content);
|
|
960
|
+
ctx.recordWrite?.(templatePath);
|
|
961
|
+
}
|
|
962
|
+
const hint = `${file} already exists - left untouched. Generated contract written to ${file}.template; compare with: diff ${file} ${file}.template`;
|
|
963
|
+
options2.notify?.(hint);
|
|
964
|
+
results.push({ file, action: "template", path: templatePath, hint });
|
|
965
|
+
}
|
|
966
|
+
return results;
|
|
967
|
+
}
|
|
968
|
+
function customerClaudeMdHook(notify) {
|
|
69
969
|
return (ctx) => {
|
|
70
|
-
|
|
71
|
-
vaultName: ctx.vaultName,
|
|
72
|
-
templateVersion: ctx.templateVersion
|
|
73
|
-
});
|
|
74
|
-
const destPath = ctx.deps.joinPath(ctx.targetDir, "CLAUDE.md");
|
|
75
|
-
ctx.deps.writeFile(destPath, content);
|
|
76
|
-
ctx.recordWrite(destPath);
|
|
970
|
+
writeProjectContract(ctx, { notify });
|
|
77
971
|
};
|
|
78
972
|
}
|
|
79
973
|
|
|
@@ -81,69 +975,200 @@ function customerClaudeMdHook() {
|
|
|
81
975
|
import { randomUUID } from "node:crypto";
|
|
82
976
|
|
|
83
977
|
// scripts/lib/console-url-resolver.mjs
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
import {
|
|
87
|
-
|
|
978
|
+
init_heyspex_home();
|
|
979
|
+
init_dev_mode();
|
|
980
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
981
|
+
import { homedir as homedir4 } from "node:os";
|
|
982
|
+
import { join as join3 } from "node:path";
|
|
983
|
+
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
88
984
|
function trimTrailingSlash(url) {
|
|
89
985
|
return url.replace(/\/+$/, "");
|
|
90
986
|
}
|
|
91
|
-
|
|
987
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
988
|
+
function isLoopbackConsoleUrl(url) {
|
|
989
|
+
if (typeof url !== "string" || !url) return false;
|
|
92
990
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
991
|
+
return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
992
|
+
} catch {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
var staleLoopbackCredentialsWarned = false;
|
|
997
|
+
function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
998
|
+
if (staleLoopbackCredentialsWarned) return;
|
|
999
|
+
staleLoopbackCredentialsWarned = true;
|
|
1000
|
+
try {
|
|
1001
|
+
log(
|
|
1002
|
+
`[heyspex] ignoring console_url ${credentialsUrl} from ~/.heyspex/credentials.yaml: the resolved reliance is cloud, and a loopback console cannot serve it \u2014 using ${derivedUrl} (set HEYSPEX_CONSOLE_URL to override explicitly, or run \`hs dev mode:local\` to go back to a local console).
|
|
1003
|
+
`
|
|
1004
|
+
);
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
|
|
1009
|
+
try {
|
|
1010
|
+
const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
|
|
1011
|
+
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
95
1012
|
const value = match?.[1]?.trim();
|
|
96
1013
|
return value && value.length > 0 ? trimTrailingSlash(value) : null;
|
|
97
1014
|
} catch {
|
|
98
1015
|
return null;
|
|
99
1016
|
}
|
|
100
1017
|
}
|
|
101
|
-
function
|
|
1018
|
+
function defaultReadFile2(p) {
|
|
1019
|
+
return readFileSync3(p, "utf8");
|
|
1020
|
+
}
|
|
1021
|
+
function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
|
|
102
1022
|
if (!repoRoot) return null;
|
|
103
|
-
|
|
104
|
-
"console-url"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
]) {
|
|
108
|
-
try {
|
|
109
|
-
const discovery = readFileSync(join(repoRoot, ".heyspex", name), "utf8").trim();
|
|
110
|
-
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
111
|
-
} catch {
|
|
112
|
-
}
|
|
1023
|
+
try {
|
|
1024
|
+
const discovery = String(readDiscovery(join3(repoRoot, ".heyspex", "console-url")) ?? "").trim();
|
|
1025
|
+
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
1026
|
+
} catch {
|
|
113
1027
|
}
|
|
114
1028
|
return null;
|
|
115
1029
|
}
|
|
116
1030
|
var ConsoleUrlUnresolvedError = class extends Error {
|
|
117
1031
|
constructor() {
|
|
118
1032
|
super(
|
|
119
|
-
|
|
1033
|
+
"resolveConsoleUrl: local reliance with no HEYSPEX_CONSOLE_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the localhost:3000 default in strict mode."
|
|
120
1034
|
);
|
|
121
1035
|
this.name = "ConsoleUrlUnresolvedError";
|
|
122
1036
|
}
|
|
123
1037
|
};
|
|
1038
|
+
function resolveModeEnvelope({ env, homeDir }) {
|
|
1039
|
+
try {
|
|
1040
|
+
const resolved = resolveDevMode({ env, homeDir });
|
|
1041
|
+
return {
|
|
1042
|
+
reliance: resolved.reliance === "cloud" ? "cloud" : "local",
|
|
1043
|
+
consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
|
|
1044
|
+
};
|
|
1045
|
+
} catch {
|
|
1046
|
+
return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
124
1049
|
function resolveConsoleUrl({
|
|
125
1050
|
repoRoot,
|
|
126
1051
|
env = process.env,
|
|
127
|
-
homeDir =
|
|
128
|
-
strict = false
|
|
1052
|
+
homeDir = heyspexHome(),
|
|
1053
|
+
strict = false,
|
|
1054
|
+
devMode,
|
|
1055
|
+
log = (line) => process.stderr.write(line),
|
|
1056
|
+
readFile = defaultReadFile2,
|
|
1057
|
+
readDiscovery = defaultReadFile2
|
|
129
1058
|
} = {}) {
|
|
130
|
-
const envUrl =
|
|
1059
|
+
const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
|
|
131
1060
|
if (envUrl) return trimTrailingSlash(envUrl);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1061
|
+
const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
|
|
1062
|
+
const cloud = mode.reliance === "cloud";
|
|
1063
|
+
const derivedUrl = trimTrailingSlash(
|
|
1064
|
+
cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
|
|
1065
|
+
);
|
|
1066
|
+
const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
|
|
1067
|
+
if (credentialsUrl) {
|
|
1068
|
+
if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
|
|
1069
|
+
warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
|
|
1070
|
+
} else {
|
|
1071
|
+
return credentialsUrl;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (!cloud) {
|
|
1075
|
+
const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery });
|
|
1076
|
+
if (discoveryUrl) return discoveryUrl;
|
|
1077
|
+
if (strict) throw new ConsoleUrlUnresolvedError();
|
|
1078
|
+
}
|
|
1079
|
+
return derivedUrl;
|
|
138
1080
|
}
|
|
139
1081
|
|
|
140
1082
|
// spex-console/src/lib/cli/control-shared.ts
|
|
141
|
-
import
|
|
142
|
-
|
|
143
|
-
|
|
1083
|
+
import path from "node:path";
|
|
1084
|
+
|
|
1085
|
+
// scripts/lib/hs-command.mjs
|
|
1086
|
+
import { existsSync } from "node:fs";
|
|
1087
|
+
import { delimiter, dirname as dirname2, join as join4 } from "node:path";
|
|
1088
|
+
import { fileURLToPath } from "node:url";
|
|
1089
|
+
var HS_BIN_NAME = "hs";
|
|
1090
|
+
var HS_DISPATCHER_PATH = (() => {
|
|
1091
|
+
try {
|
|
1092
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
1093
|
+
return join4(dirname2(dirname2(here)), "bin", "hs.mjs");
|
|
1094
|
+
} catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
})();
|
|
1098
|
+
function shellQuote(p) {
|
|
1099
|
+
return /[\\\s]/.test(p) ? `"${p}"` : p;
|
|
1100
|
+
}
|
|
1101
|
+
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
1102
|
+
|
|
1103
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
1104
|
+
init_heyspex_home();
|
|
1105
|
+
init_credentials();
|
|
1106
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1107
|
+
import { homedir as homedir7 } from "node:os";
|
|
1108
|
+
import { join as join8 } from "node:path";
|
|
1109
|
+
|
|
1110
|
+
// vendor/heyspex-mcp/session-owner.mjs
|
|
1111
|
+
init_heyspex_home();
|
|
1112
|
+
init_credentials();
|
|
1113
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1114
|
+
import { homedir as homedir5 } from "node:os";
|
|
1115
|
+
import { join as join5 } from "node:path";
|
|
1116
|
+
|
|
1117
|
+
// vendor/heyspex-mcp/vault-less-endpoints.mjs
|
|
1118
|
+
var VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
1119
|
+
"* /api/mcp/session",
|
|
1120
|
+
"* /api/mcp/pairing-codes",
|
|
1121
|
+
"POST /api/vaults",
|
|
1122
|
+
"GET /api/vaults",
|
|
1123
|
+
"POST /api/workspaces",
|
|
1124
|
+
"GET /api/workspaces",
|
|
1125
|
+
"* /api/admin/dev-mode"
|
|
1126
|
+
]);
|
|
1127
|
+
|
|
1128
|
+
// vendor/heyspex-mcp/write-outbox.mjs
|
|
1129
|
+
import {
|
|
1130
|
+
readdirSync,
|
|
1131
|
+
readFileSync as readFileSync5,
|
|
1132
|
+
writeFileSync as writeFileSync3,
|
|
1133
|
+
mkdirSync as mkdirSync3,
|
|
1134
|
+
existsSync as existsSync2,
|
|
1135
|
+
renameSync as renameSync2,
|
|
1136
|
+
rmSync
|
|
1137
|
+
} from "node:fs";
|
|
1138
|
+
import { basename, join as join6, isAbsolute as isAbsolute2 } from "node:path";
|
|
1139
|
+
|
|
1140
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
1141
|
+
init_heyspex_home();
|
|
1142
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
1143
|
+
import { homedir as homedir6, tmpdir } from "node:os";
|
|
1144
|
+
import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
|
|
1145
|
+
|
|
1146
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
1147
|
+
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
1148
|
+
|
|
1149
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
1150
|
+
init_credentials();
|
|
1151
|
+
|
|
1152
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
1153
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1154
|
+
import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
|
|
1155
|
+
|
|
1156
|
+
// spex-console/src/lib/heyspex-home.ts
|
|
1157
|
+
import { homedir as homedir8 } from "node:os";
|
|
1158
|
+
|
|
1159
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
144
1160
|
var EXIT = {
|
|
145
1161
|
/** Success. */
|
|
146
1162
|
OK: 0,
|
|
1163
|
+
/**
|
|
1164
|
+
* A read-only CHECK ran correctly and its answer is "no" — `hs connect
|
|
1165
|
+
* --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
|
|
1166
|
+
* script can tell "you typed the command wrong" from "the command worked,
|
|
1167
|
+
* the repo is not connected". Both were `1` until 2026-09-18, which made
|
|
1168
|
+
* `--check` unusable in a conditional: every caller had to parse stdout to
|
|
1169
|
+
* find out which of the two had happened.
|
|
1170
|
+
*/
|
|
1171
|
+
CHECK_FAILED: 2,
|
|
147
1172
|
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
148
1173
|
USAGE: 1,
|
|
149
1174
|
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
@@ -165,12 +1190,63 @@ var EXIT = {
|
|
|
165
1190
|
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
166
1191
|
* malformed command.
|
|
167
1192
|
*/
|
|
168
|
-
FORBIDDEN: 15
|
|
1193
|
+
FORBIDDEN: 15,
|
|
1194
|
+
/**
|
|
1195
|
+
* A CLOUD write refused because no account id could be resolved (no
|
|
1196
|
+
* `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
|
|
1197
|
+
* `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
|
|
1198
|
+
* ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
|
|
1199
|
+
* `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
|
|
1200
|
+
* authorized for this override".
|
|
1201
|
+
*/
|
|
1202
|
+
SIGN_IN_REQUIRED: 16,
|
|
1203
|
+
/**
|
|
1204
|
+
* Signed in, but the account a repo would be attached to was not confirmed:
|
|
1205
|
+
* a non-interactive install/connect with no matching `--account` / `--yes`,
|
|
1206
|
+
* an `account_id` that disagrees with the bearer's user, or the human
|
|
1207
|
+
* aborting the identity prompt (`install-identity.ts`,
|
|
1208
|
+
* [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
|
|
1209
|
+
*/
|
|
1210
|
+
IDENTITY_UNCONFIRMED: 17,
|
|
1211
|
+
/**
|
|
1212
|
+
* The repo this verb runs in is not registered on this machine (no
|
|
1213
|
+
* `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
|
|
1214
|
+
* cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
|
|
1215
|
+
* nothing was sent. Names `hs connect`
|
|
1216
|
+
* ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
|
|
1217
|
+
*/
|
|
1218
|
+
VAULT_NOT_REGISTERED: 18,
|
|
1219
|
+
/**
|
|
1220
|
+
* The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
|
|
1221
|
+
* *`) was written but the CLOUD half did not land -- the console refused
|
|
1222
|
+
* it or was never reached. The verb's receipt names the failed half and the
|
|
1223
|
+
* exact command to re-run
|
|
1224
|
+
* ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
|
|
1225
|
+
* Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
|
|
1226
|
+
* sync the cloud" from a generic server failure.
|
|
1227
|
+
*/
|
|
1228
|
+
CLOUD_WRITE_FAILED: 19,
|
|
1229
|
+
/**
|
|
1230
|
+
* The repo's committed `heyspex.yaml` records `account: <A>` but the machine
|
|
1231
|
+
* is signed in as another account, so `hs connect` refused and wrote
|
|
1232
|
+
* nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
|
|
1233
|
+
* ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
|
|
1234
|
+
*/
|
|
1235
|
+
ACCOUNT_MISMATCH: 20,
|
|
1236
|
+
/**
|
|
1237
|
+
* Both sides of a setup verb's receipt were read back and still DISAGREE
|
|
1238
|
+
* after a short bounded re-read of the cloud side (a briefly stale read is
|
|
1239
|
+
* absorbed first). The write may well have landed; what the verb cannot
|
|
1240
|
+
* claim is that this machine and the console now say the same thing. The
|
|
1241
|
+
* receipt prints the diffs and the command to re-run
|
|
1242
|
+
* ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
|
|
1243
|
+
* Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
|
|
1244
|
+
*/
|
|
1245
|
+
RECEIPT_DISAGREE: 21
|
|
169
1246
|
};
|
|
170
1247
|
|
|
171
1248
|
// spex-console/src/lib/cli/init.ts
|
|
172
1249
|
var HEYSPEX_YAML_SCHEMA_VERSION = 2;
|
|
173
|
-
var DEFAULT_CONSOLE_URL2 = resolveConsoleUrl();
|
|
174
1250
|
function mintVaultId() {
|
|
175
1251
|
return randomUUID();
|
|
176
1252
|
}
|
|
@@ -181,6 +1257,30 @@ function parseVaultId(content) {
|
|
|
181
1257
|
const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
182
1258
|
return unquoted.length > 0 ? unquoted : null;
|
|
183
1259
|
}
|
|
1260
|
+
function parseAccountId(content) {
|
|
1261
|
+
const m = content.match(/^account:[ \t]*(.+?)\s*$/m);
|
|
1262
|
+
if (!m) return null;
|
|
1263
|
+
const raw = m[1].replace(/\s+#.*$/, "").trim();
|
|
1264
|
+
const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
1265
|
+
return unquoted.length > 0 ? unquoted : null;
|
|
1266
|
+
}
|
|
1267
|
+
var MARKER_ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
|
|
1268
|
+
function recordMarkerAccount(content, accountId) {
|
|
1269
|
+
if (!MARKER_ACCOUNT_ID_RE.test(accountId)) {
|
|
1270
|
+
throw new Error(`recordMarkerAccount: "${accountId}" is not a valid account id`);
|
|
1271
|
+
}
|
|
1272
|
+
const recorded = parseAccountId(content);
|
|
1273
|
+
if (recorded === accountId) return { action: "unchanged", content };
|
|
1274
|
+
if (recorded !== null) return { action: "conflict", recorded, content };
|
|
1275
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
1276
|
+
const lines = content.replace(/(\r?\n)+$/, "").split(/\r?\n/);
|
|
1277
|
+
const kept = lines.filter((l) => !/^account:\s*$/.test(l));
|
|
1278
|
+
const vaultIdx = kept.findIndex((l) => /^vault_id:/.test(l));
|
|
1279
|
+
const line = `account: ${accountId}`;
|
|
1280
|
+
if (vaultIdx >= 0) kept.splice(vaultIdx + 1, 0, line);
|
|
1281
|
+
else kept.push(line);
|
|
1282
|
+
return { action: "added", content: `${kept.join(eol)}${eol}` };
|
|
1283
|
+
}
|
|
184
1284
|
function appendVaultIdLine(content, vaultId) {
|
|
185
1285
|
const trimmed = content.replace(/\n+$/, "");
|
|
186
1286
|
return `${trimmed}
|
|
@@ -188,21 +1288,22 @@ vault_id: ${vaultId}
|
|
|
188
1288
|
`;
|
|
189
1289
|
}
|
|
190
1290
|
function parseInitArgs(argv) {
|
|
191
|
-
let consoleUrl = (
|
|
192
|
-
DEFAULT_CONSOLE_URL2;
|
|
1291
|
+
let consoleUrl = resolveConsoleUrl();
|
|
193
1292
|
let vaultRoot = "specs/";
|
|
194
1293
|
let force = false;
|
|
195
1294
|
let json = false;
|
|
196
1295
|
let customerVaultDir;
|
|
197
1296
|
let principles;
|
|
198
1297
|
let vaultName;
|
|
199
|
-
let
|
|
1298
|
+
let git = false;
|
|
200
1299
|
for (let i = 0; i < argv.length; i++) {
|
|
201
1300
|
const arg = argv[i];
|
|
202
1301
|
if (arg === "--help" || arg === "-h") return { help: true };
|
|
203
1302
|
else if (arg === "--json") json = true;
|
|
204
1303
|
else if (arg === "--force") force = true;
|
|
205
|
-
else if (arg === "--
|
|
1304
|
+
else if (arg === "--git") git = true;
|
|
1305
|
+
else if (arg === "--no-git")
|
|
1306
|
+
git = false;
|
|
206
1307
|
else if (arg === "--principles" || arg.startsWith("--principles=")) {
|
|
207
1308
|
const v = arg === "--principles" ? argv[++i] : arg.slice("--principles=".length);
|
|
208
1309
|
if (v === void 0 || v.trim() === "") {
|
|
@@ -215,8 +1316,7 @@ function parseInitArgs(argv) {
|
|
|
215
1316
|
return { error: { code: EXIT.INVALID_ARG, message: "--vault-name requires a value" } };
|
|
216
1317
|
}
|
|
217
1318
|
vaultName = v.trim();
|
|
218
|
-
} else if (arg === "--console-url" || arg.startsWith("--console-url=")
|
|
219
|
-
arg.startsWith("--cockpit-url=")) {
|
|
1319
|
+
} else if (arg === "--console-url" || arg.startsWith("--console-url=")) {
|
|
220
1320
|
const eq = arg.indexOf("=");
|
|
221
1321
|
const v = eq === -1 ? argv[++i] : arg.slice(eq + 1);
|
|
222
1322
|
if (v === void 0 || v.trim() === "") {
|
|
@@ -247,17 +1347,36 @@ function parseInitArgs(argv) {
|
|
|
247
1347
|
}
|
|
248
1348
|
}
|
|
249
1349
|
return {
|
|
250
|
-
args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName,
|
|
1350
|
+
args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName, git }
|
|
251
1351
|
};
|
|
252
1352
|
}
|
|
1353
|
+
var HEYSPEX_CONFIG_DIR = ".heyspex";
|
|
1354
|
+
var HEYSPEX_CONFIG_FILE = "config.yaml";
|
|
1355
|
+
function seedConfigYaml(root, readSeed, fs4) {
|
|
1356
|
+
const configDir = fs4.joinPath(root, HEYSPEX_CONFIG_DIR);
|
|
1357
|
+
const configPath = fs4.joinPath(configDir, HEYSPEX_CONFIG_FILE);
|
|
1358
|
+
if (fs4.exists(configPath)) return { status: "kept", path: configPath };
|
|
1359
|
+
if (!readSeed) return { status: "no_seed", path: configPath };
|
|
1360
|
+
let seed;
|
|
1361
|
+
try {
|
|
1362
|
+
seed = readSeed();
|
|
1363
|
+
} catch {
|
|
1364
|
+
seed = null;
|
|
1365
|
+
}
|
|
1366
|
+
if (seed === null) return { status: "no_seed", path: configPath };
|
|
1367
|
+
fs4.mkdirRecursive(configDir);
|
|
1368
|
+
fs4.writeFile(configPath, seed);
|
|
1369
|
+
return { status: "seeded", path: configPath };
|
|
1370
|
+
}
|
|
253
1371
|
function renderHeyspexYaml(args) {
|
|
254
|
-
|
|
1372
|
+
const body = [
|
|
255
1373
|
`schema_version: ${HEYSPEX_YAML_SCHEMA_VERSION}`,
|
|
256
1374
|
`console_url: ${args.consoleUrl}`,
|
|
257
1375
|
`vault_root: ${args.vaultRoot}`,
|
|
258
1376
|
`vault_id: ${args.vaultId}`,
|
|
259
1377
|
""
|
|
260
1378
|
].join("\n");
|
|
1379
|
+
return args.accountId ? recordMarkerAccount(body, args.accountId).content : body;
|
|
261
1380
|
}
|
|
262
1381
|
function readExistingContent(deps, markerPath) {
|
|
263
1382
|
if (!deps.readFile) return null;
|
|
@@ -272,17 +1391,60 @@ function runInit(args, deps) {
|
|
|
272
1391
|
const exists = deps.fileExists(markerPath);
|
|
273
1392
|
const existingContent = exists ? readExistingContent(deps, markerPath) : null;
|
|
274
1393
|
const existingVaultId = existingContent ? parseVaultId(existingContent) : null;
|
|
1394
|
+
if (deps.mkdirRecursive) {
|
|
1395
|
+
const specsPath = deps.joinPath(deps.repoRoot, args.vaultRoot);
|
|
1396
|
+
try {
|
|
1397
|
+
deps.mkdirRecursive(specsPath);
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1400
|
+
return {
|
|
1401
|
+
code: EXIT.WRITE_FAILED,
|
|
1402
|
+
message: `failed to create ${specsPath}: ${message}`,
|
|
1403
|
+
payload: { ok: false, error: "write_failed", path: specsPath, message },
|
|
1404
|
+
isError: true
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
let configSeed = null;
|
|
1409
|
+
const mkdirRecursive = deps.mkdirRecursive;
|
|
1410
|
+
if (mkdirRecursive) {
|
|
1411
|
+
const readConfigSeed2 = deps.readConfigSeed;
|
|
1412
|
+
try {
|
|
1413
|
+
configSeed = seedConfigYaml(
|
|
1414
|
+
deps.repoRoot,
|
|
1415
|
+
readConfigSeed2 ? () => readConfigSeed2.call(deps) : void 0,
|
|
1416
|
+
{
|
|
1417
|
+
exists: (p) => deps.fileExists(p),
|
|
1418
|
+
mkdirRecursive: (p) => mkdirRecursive.call(deps, p),
|
|
1419
|
+
writeFile: (p, c) => deps.writeFile(p, c),
|
|
1420
|
+
joinPath: (...s) => deps.joinPath(...s)
|
|
1421
|
+
}
|
|
1422
|
+
);
|
|
1423
|
+
} catch (err) {
|
|
1424
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1425
|
+
const configPath = deps.joinPath(deps.repoRoot, HEYSPEX_CONFIG_DIR, HEYSPEX_CONFIG_FILE);
|
|
1426
|
+
return {
|
|
1427
|
+
code: EXIT.WRITE_FAILED,
|
|
1428
|
+
message: `failed to seed ${configPath}: ${message}`,
|
|
1429
|
+
payload: { ok: false, error: "write_failed", path: configPath, message },
|
|
1430
|
+
isError: true
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const configNote = configSeed?.status === "seeded" ? ` Seeded ${configSeed.path}.` : configSeed?.status === "kept" ? ` Kept existing ${configSeed.path}.` : "";
|
|
1435
|
+
const configPayload = configSeed === null || configSeed.status === "no_seed" ? {} : { config_yaml: configSeed.path, config_yaml_seeded: configSeed.status === "seeded" };
|
|
275
1436
|
if (exists && !args.force) {
|
|
276
1437
|
if (existingVaultId) {
|
|
277
1438
|
return {
|
|
278
1439
|
code: EXIT.OK,
|
|
279
|
-
message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite)
|
|
1440
|
+
message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).${configNote}`,
|
|
280
1441
|
payload: {
|
|
281
1442
|
ok: true,
|
|
282
1443
|
created: false,
|
|
283
1444
|
path: markerPath,
|
|
284
1445
|
reason: "already_initialized",
|
|
285
|
-
vault_id: existingVaultId
|
|
1446
|
+
vault_id: existingVaultId,
|
|
1447
|
+
...configPayload
|
|
286
1448
|
},
|
|
287
1449
|
isError: false
|
|
288
1450
|
};
|
|
@@ -290,8 +1452,14 @@ function runInit(args, deps) {
|
|
|
290
1452
|
if (existingContent === null) {
|
|
291
1453
|
return {
|
|
292
1454
|
code: EXIT.OK,
|
|
293
|
-
message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite)
|
|
294
|
-
payload: {
|
|
1455
|
+
message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).${configNote}`,
|
|
1456
|
+
payload: {
|
|
1457
|
+
ok: true,
|
|
1458
|
+
created: false,
|
|
1459
|
+
path: markerPath,
|
|
1460
|
+
reason: "already_initialized",
|
|
1461
|
+
...configPayload
|
|
1462
|
+
},
|
|
295
1463
|
isError: false
|
|
296
1464
|
};
|
|
297
1465
|
}
|
|
@@ -310,19 +1478,21 @@ function runInit(args, deps) {
|
|
|
310
1478
|
}
|
|
311
1479
|
return {
|
|
312
1480
|
code: EXIT.OK,
|
|
313
|
-
message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged)
|
|
1481
|
+
message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).${configNote}`,
|
|
314
1482
|
payload: {
|
|
315
1483
|
ok: true,
|
|
316
1484
|
created: false,
|
|
317
1485
|
backfilled: true,
|
|
318
1486
|
path: markerPath,
|
|
319
|
-
vault_id: mintedId
|
|
1487
|
+
vault_id: mintedId,
|
|
1488
|
+
...configPayload
|
|
320
1489
|
},
|
|
321
1490
|
isError: false
|
|
322
1491
|
};
|
|
323
1492
|
}
|
|
324
1493
|
const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
|
|
325
|
-
const
|
|
1494
|
+
const accountId = null;
|
|
1495
|
+
const content = renderHeyspexYaml({ ...args, vaultId, accountId });
|
|
326
1496
|
try {
|
|
327
1497
|
deps.writeFile(markerPath, content);
|
|
328
1498
|
} catch (err) {
|
|
@@ -336,7 +1506,7 @@ function runInit(args, deps) {
|
|
|
336
1506
|
}
|
|
337
1507
|
return {
|
|
338
1508
|
code: EXIT.OK,
|
|
339
|
-
message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION})
|
|
1509
|
+
message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).${configNote}`,
|
|
340
1510
|
payload: {
|
|
341
1511
|
ok: true,
|
|
342
1512
|
created: true,
|
|
@@ -345,7 +1515,9 @@ function runInit(args, deps) {
|
|
|
345
1515
|
schema_version: HEYSPEX_YAML_SCHEMA_VERSION,
|
|
346
1516
|
console_url: args.consoleUrl,
|
|
347
1517
|
vault_root: args.vaultRoot,
|
|
348
|
-
vault_id: vaultId
|
|
1518
|
+
vault_id: vaultId,
|
|
1519
|
+
...accountId ? { account: accountId } : {},
|
|
1520
|
+
...configPayload
|
|
349
1521
|
},
|
|
350
1522
|
isError: false
|
|
351
1523
|
};
|
|
@@ -354,27 +1526,38 @@ var HELP = [
|
|
|
354
1526
|
"Usage:",
|
|
355
1527
|
" hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
|
|
356
1528
|
" hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
|
|
357
|
-
" [--vault-name <name>] [--
|
|
1529
|
+
" [--vault-name <name>] [--git] [--json]",
|
|
358
1530
|
"",
|
|
359
1531
|
"Write the top-level heyspex.yaml vault marker (schema_version, console_url,",
|
|
360
|
-
"vault_root, vault_id
|
|
1532
|
+
"vault_root, vault_id; `hs connect` also records `account:`, the account id the",
|
|
1533
|
+
"repo was connected under) \u2014 the committed file the console / web companion read",
|
|
361
1534
|
"to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
|
|
362
1535
|
"--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
|
|
363
1536
|
"compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
|
|
364
1537
|
"present; a fresh one is minted only when absent.",
|
|
365
|
-
"
|
|
1538
|
+
"Also scaffolds the vault_root folder and seeds .heyspex/config.yaml (the",
|
|
1539
|
+
"HeySpex workflow settings, ADR-0009) from the shipped template",
|
|
1540
|
+
"(vendor/heyspex-mcp/config-seed.yaml) when that file is absent. An existing",
|
|
1541
|
+
".heyspex/config.yaml is never rewritten \u2014 not even with --force; change its",
|
|
1542
|
+
"values with `hs settings set <key> <value>`.",
|
|
366
1543
|
"",
|
|
367
1544
|
"--customer-vault <dir> scaffolds a REAL customer vault at <dir> instead: the",
|
|
368
|
-
"specs/ folder tree, heyspex.yaml,
|
|
369
|
-
"
|
|
1545
|
+
"specs/ folder tree, heyspex.yaml, the seeded .heyspex/config.yaml, and every",
|
|
1546
|
+
"row of the template shipping manifest",
|
|
1547
|
+
"(spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
|
|
370
1548
|
"copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
|
|
371
|
-
"plus a generated CLAUDE.md, the kept-principles index, and
|
|
372
|
-
"one initial `git init` + commit
|
|
373
|
-
"
|
|
1549
|
+
"plus a generated CLAUDE.md, the kept-principles index, and the template_version",
|
|
1550
|
+
"pin; with --git, one initial `git init` + commit. Rerunnable: a <dir> that already",
|
|
1551
|
+
"holds heyspex.yaml and an empty/placeholder-only specs/ is accepted, existing",
|
|
1552
|
+
"files are never overwritten (an existing CLAUDE.md gets a CLAUDE.md.template",
|
|
1553
|
+
"beside it), and the result lists each manifest row as created or kept. Refuses a",
|
|
1554
|
+
"non-empty <dir> without heyspex.yaml, or a specs/ holding managed specs.",
|
|
1555
|
+
"See init.customer-vault.ts.",
|
|
374
1556
|
"",
|
|
375
1557
|
"Flags:",
|
|
376
|
-
" --console-url <url> console_url value (default:
|
|
377
|
-
"
|
|
1558
|
+
" --console-url <url> console_url value (default: the mode-aware console --",
|
|
1559
|
+
" $HEYSPEX_CONSOLE_URL, else the signed-in console, else",
|
|
1560
|
+
" https://app.heyspex.com in prod mode).",
|
|
378
1561
|
" --vault-root <path> vault_root value (default: specs/).",
|
|
379
1562
|
" --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
|
|
380
1563
|
" --principles <all|none|a,b> Which universal principle docs to ship",
|
|
@@ -382,8 +1565,9 @@ var HELP = [
|
|
|
382
1565
|
" keep/skip prompt per principle. Omitted + no TTY: all.",
|
|
383
1566
|
" --vault-name <name> Display name for the scaffolded vault (--customer-vault",
|
|
384
1567
|
" only; default: <dir>'s basename).",
|
|
385
|
-
" --
|
|
386
|
-
"
|
|
1568
|
+
" --git Also run git init + one initial commit (--customer-vault only;",
|
|
1569
|
+
" default: no git). --no-git is accepted as a no-op.",
|
|
1570
|
+
" --force Overwrite an existing heyspex.yaml (never .heyspex/config.yaml).",
|
|
387
1571
|
" --json Emit a machine-readable JSON result.",
|
|
388
1572
|
" --help, -h Show this help.",
|
|
389
1573
|
"",
|
|
@@ -391,7 +1575,7 @@ var HELP = [
|
|
|
391
1575
|
].join("\n");
|
|
392
1576
|
|
|
393
1577
|
// spex-console/src/lib/cli/init.customer-vault.ts
|
|
394
|
-
import { basename } from "node:path";
|
|
1578
|
+
import { basename as basename2 } from "node:path";
|
|
395
1579
|
|
|
396
1580
|
// spex-console/src/lib/cli/template-manifest.sources.mjs
|
|
397
1581
|
var UNIVERSAL_TEMPLATE_ROWS = [
|
|
@@ -407,14 +1591,69 @@ var UNIVERSAL_TEMPLATE_ROWS = [
|
|
|
407
1591
|
{
|
|
408
1592
|
platformSource: "specs/protocols/memory-protocol.md",
|
|
409
1593
|
dest: "specs/protocols/memory-protocol.md"
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
// ~455 lines of HS-REV / MVP-grain / guardrail detail that cannot be
|
|
1597
|
+
// curated cleanly: ships a template-shape abridgement instead.
|
|
1598
|
+
platformSource: "specs/protocols/session-close-protocol.md",
|
|
1599
|
+
abridgedSource: "spex-console/templates/abridged/specs/protocols/session-close-protocol.md",
|
|
1600
|
+
dest: "specs/protocols/session-close-protocol.md"
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
platformSource: "specs/protocols/spec-summary-authoring.md",
|
|
1604
|
+
dest: "specs/protocols/spec-summary-authoring.md",
|
|
1605
|
+
dropSections: ["Keeping the code snapshot in sync", "Change log"],
|
|
1606
|
+
dropLinePatterns: ["vendor/heyspex-mcp/summary-contract\\.mjs"]
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
platformSource: "specs/protocols/destructive-action-checklist.md",
|
|
1610
|
+
dest: "specs/protocols/destructive-action-checklist.md",
|
|
1611
|
+
dropSections: ["Change log"],
|
|
1612
|
+
dropLinePatterns: ["spex-console/scripts/validate-destructive-actions\\.mjs"],
|
|
1613
|
+
replacements: [
|
|
1614
|
+
[
|
|
1615
|
+
"The 2026-05-22 idea-discard incident showed that strong UX *principles*\n([[errors-destructive-recovery]], [[reversible-user-actions]]) are not\nenough",
|
|
1616
|
+
"A past idea-discard data-loss incident showed that strong UX *principles*\n(recoverable destructive actions, reversible user actions) are not\nenough"
|
|
1617
|
+
],
|
|
1618
|
+
[
|
|
1619
|
+
"Today: linked from `CLAUDE.md` Console pre-ship checklist; agents must\nwalk it",
|
|
1620
|
+
"Today: agents must\nwalk it"
|
|
1621
|
+
],
|
|
1622
|
+
[
|
|
1623
|
+
'- [ ] If a *new* destructive surface is introduced, append it to the\n "Where this is applied" table in\n [[destructive-action-needs-explicit-button-and-undo]].',
|
|
1624
|
+
"- [ ] If a *new* destructive surface is introduced, record it in a vault memory."
|
|
1625
|
+
],
|
|
1626
|
+
[
|
|
1627
|
+
"- [ ] Read [[errors-destructive-recovery]] and\n [[destructive-action-needs-explicit-button-and-undo]] in full.",
|
|
1628
|
+
"- [ ] Read any vault memories on destructive-action recovery and Undo."
|
|
1629
|
+
],
|
|
1630
|
+
[
|
|
1631
|
+
" (Not covered here; see existing dirty-editor\n pattern in `specs-surface.tsx`.)",
|
|
1632
|
+
" (Not covered here.)"
|
|
1633
|
+
]
|
|
1634
|
+
]
|
|
410
1635
|
}
|
|
411
1636
|
];
|
|
412
1637
|
|
|
413
1638
|
// dist/cli/._template-manifest-sources.cli-shim.mjs
|
|
414
1639
|
var CURATED_TEMPLATE_ROOT = "dist/cli-templates/customer-vault";
|
|
415
1640
|
|
|
1641
|
+
// scripts/lib/template-pin.mjs
|
|
1642
|
+
var TEMPLATE_VERSION = "1.1.0";
|
|
1643
|
+
var TEMPLATE_CORE_DESTS = Object.freeze([
|
|
1644
|
+
"specs/north-star.md",
|
|
1645
|
+
"specs/principles-index.md",
|
|
1646
|
+
"specs/roadmap.md",
|
|
1647
|
+
"specs/entity-model.md",
|
|
1648
|
+
"specs/protocols/canonical-frontmatter-templates.md",
|
|
1649
|
+
"specs/protocols/spec-hierarchy.md",
|
|
1650
|
+
"specs/protocols/memory-protocol.md",
|
|
1651
|
+
"specs/protocols/session-close-protocol.md",
|
|
1652
|
+
"specs/protocols/spec-summary-authoring.md",
|
|
1653
|
+
"specs/protocols/destructive-action-checklist.md"
|
|
1654
|
+
]);
|
|
1655
|
+
|
|
416
1656
|
// spex-console/src/lib/cli/template-manifest.ts
|
|
417
|
-
var TEMPLATE_VERSION = "1.0.0";
|
|
418
1657
|
var TODO_NORTH_STAR = `---
|
|
419
1658
|
id: HS-L1
|
|
420
1659
|
name: TODO \u2014 your product name
|
|
@@ -804,7 +2043,7 @@ var TEMPLATE_MANIFEST = [
|
|
|
804
2043
|
];
|
|
805
2044
|
|
|
806
2045
|
// spex-console/src/lib/cli/init.customer-vault.ts
|
|
807
|
-
var
|
|
2046
|
+
var DEFAULT_CONSOLE_URL2 = "https://console.heyspex.local";
|
|
808
2047
|
var SCAFFOLD_FOLDERS = [
|
|
809
2048
|
"tasks",
|
|
810
2049
|
"epics",
|
|
@@ -821,23 +2060,63 @@ var SCAFFOLD_FOLDERS = [
|
|
|
821
2060
|
"checks",
|
|
822
2061
|
"events"
|
|
823
2062
|
];
|
|
2063
|
+
var PLACEHOLDER_FILES = /* @__PURE__ */ new Set([".gitkeep"]);
|
|
2064
|
+
function isDir(deps, path5, name) {
|
|
2065
|
+
if (deps.isDirectory) return deps.isDirectory(path5);
|
|
2066
|
+
try {
|
|
2067
|
+
const kids = deps.readdir(path5);
|
|
2068
|
+
return kids.length > 0 || !name.includes(".");
|
|
2069
|
+
} catch {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
function findManagedSpecs(deps, targetDir, manifestDests) {
|
|
2074
|
+
const out = [];
|
|
2075
|
+
const walk = (abs, rel) => {
|
|
2076
|
+
for (const name of deps.readdir(abs)) {
|
|
2077
|
+
const childAbs = deps.joinPath(abs, name);
|
|
2078
|
+
const childRel = `${rel}/${name}`;
|
|
2079
|
+
if (isDir(deps, childAbs, name)) walk(childAbs, childRel);
|
|
2080
|
+
else if (!PLACEHOLDER_FILES.has(name) && name.endsWith(".md") && !manifestDests.has(childRel))
|
|
2081
|
+
out.push(childRel);
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
const specsRoot = deps.joinPath(targetDir, "specs");
|
|
2085
|
+
if (deps.exists(specsRoot)) walk(specsRoot, "specs");
|
|
2086
|
+
return out;
|
|
2087
|
+
}
|
|
824
2088
|
function defaultVaultName(targetDir) {
|
|
825
|
-
return
|
|
2089
|
+
return basename2(targetDir) || targetDir;
|
|
826
2090
|
}
|
|
827
|
-
function runCustomerVaultInit(args, deps,
|
|
2091
|
+
function runCustomerVaultInit(args, deps, options2 = {}) {
|
|
828
2092
|
const { targetDir } = args;
|
|
829
2093
|
const written = [];
|
|
2094
|
+
const filter = options2.manifestFilter ?? (() => true);
|
|
2095
|
+
const rows = TEMPLATE_MANIFEST.filter(filter);
|
|
830
2096
|
if (deps.exists(targetDir)) {
|
|
831
|
-
const
|
|
832
|
-
if (
|
|
2097
|
+
const markerPresent = deps.exists(deps.joinPath(targetDir, "heyspex.yaml"));
|
|
2098
|
+
if (!markerPresent && deps.readdir(targetDir).length > 0) {
|
|
833
2099
|
return {
|
|
834
2100
|
code: EXIT.INVALID_ARG,
|
|
835
|
-
message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory.`,
|
|
2101
|
+
message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory that has no heyspex.yaml.`,
|
|
836
2102
|
payload: { ok: false, error: "target_not_empty", path: targetDir },
|
|
837
2103
|
isError: true,
|
|
838
2104
|
written: []
|
|
839
2105
|
};
|
|
840
2106
|
}
|
|
2107
|
+
if (markerPresent) {
|
|
2108
|
+
const manifestDests = new Set(TEMPLATE_MANIFEST.map((r) => r.dest));
|
|
2109
|
+
const managed = findManagedSpecs(deps, targetDir, manifestDests);
|
|
2110
|
+
if (managed.length > 0) {
|
|
2111
|
+
return {
|
|
2112
|
+
code: EXIT.INVALID_ARG,
|
|
2113
|
+
message: `${targetDir}/specs already holds managed spec(s) (${managed.slice(0, 3).join(", ")}${managed.length > 3 ? ", ..." : ""}) \u2014 customer-vault init refuses to write over an existing vault.`,
|
|
2114
|
+
payload: { ok: false, error: "specs_not_empty", path: targetDir, specs: managed },
|
|
2115
|
+
isError: true,
|
|
2116
|
+
written: []
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
841
2120
|
} else {
|
|
842
2121
|
deps.mkdirRecursive(targetDir);
|
|
843
2122
|
}
|
|
@@ -846,19 +2125,37 @@ function runCustomerVaultInit(args, deps, options = {}) {
|
|
|
846
2125
|
for (const folder of SCAFFOLD_FOLDERS) {
|
|
847
2126
|
deps.mkdirRecursive(deps.joinPath(specsRoot, folder));
|
|
848
2127
|
}
|
|
849
|
-
const vaultId = mintVaultId();
|
|
850
|
-
const yamlContent = renderHeyspexYaml({
|
|
851
|
-
consoleUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL3,
|
|
852
|
-
vaultRoot: "specs/",
|
|
853
|
-
vaultId
|
|
854
|
-
});
|
|
855
2128
|
const heyspexYamlPath = deps.joinPath(targetDir, "heyspex.yaml");
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
2129
|
+
let vaultId;
|
|
2130
|
+
if (deps.exists(heyspexYamlPath)) {
|
|
2131
|
+
const existingYaml = deps.readFile?.(heyspexYamlPath) ?? null;
|
|
2132
|
+
vaultId = (existingYaml ? parseVaultId(existingYaml) : null) ?? "";
|
|
2133
|
+
} else {
|
|
2134
|
+
vaultId = mintVaultId();
|
|
2135
|
+
deps.writeFile(
|
|
2136
|
+
heyspexYamlPath,
|
|
2137
|
+
renderHeyspexYaml({
|
|
2138
|
+
consoleUrl: options2.consoleUrl ?? DEFAULT_CONSOLE_URL2,
|
|
2139
|
+
vaultRoot: "specs/",
|
|
2140
|
+
vaultId
|
|
2141
|
+
})
|
|
2142
|
+
);
|
|
2143
|
+
written.push(heyspexYamlPath);
|
|
2144
|
+
}
|
|
2145
|
+
const readConfigSeed2 = deps.readConfigSeed;
|
|
2146
|
+
const configSeed = seedConfigYaml(
|
|
2147
|
+
targetDir,
|
|
2148
|
+
readConfigSeed2 ? () => readConfigSeed2.call(deps) : void 0,
|
|
2149
|
+
deps
|
|
2150
|
+
);
|
|
2151
|
+
if (configSeed.status === "seeded") written.push(configSeed.path);
|
|
2152
|
+
const rowResults = [];
|
|
860
2153
|
for (const row of rows) {
|
|
861
2154
|
const destAbs = deps.joinPath(targetDir, ...row.dest.split("/"));
|
|
2155
|
+
if (deps.exists(destAbs)) {
|
|
2156
|
+
rowResults.push({ dest: row.dest, action: "kept" });
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
862
2159
|
if (row.classification === "universal-template") {
|
|
863
2160
|
const srcAbs = deps.joinPath(deps.platformRoot, ...row.source.split("/"));
|
|
864
2161
|
deps.copyFile(srcAbs, destAbs);
|
|
@@ -871,29 +2168,36 @@ function runCustomerVaultInit(args, deps, options = {}) {
|
|
|
871
2168
|
deps.writeFile(destAbs, row.content);
|
|
872
2169
|
}
|
|
873
2170
|
written.push(destAbs);
|
|
2171
|
+
rowResults.push({ dest: row.dest, action: "created" });
|
|
874
2172
|
}
|
|
875
|
-
if (
|
|
2173
|
+
if (options2.afterScaffold && options2.afterScaffold.length > 0) {
|
|
876
2174
|
const ctx = {
|
|
877
2175
|
targetDir,
|
|
878
|
-
vaultName:
|
|
2176
|
+
vaultName: options2.vaultName ?? defaultVaultName(targetDir),
|
|
879
2177
|
templateVersion: TEMPLATE_VERSION,
|
|
880
2178
|
manifestRows: rows,
|
|
2179
|
+
rows: rowResults,
|
|
881
2180
|
deps,
|
|
882
|
-
recordWrite: (
|
|
883
|
-
written.push(
|
|
2181
|
+
recordWrite: (path5) => {
|
|
2182
|
+
written.push(path5);
|
|
884
2183
|
}
|
|
885
2184
|
};
|
|
886
|
-
for (const hook of
|
|
2185
|
+
for (const hook of options2.afterScaffold) hook(ctx);
|
|
887
2186
|
}
|
|
2187
|
+
const createdCount = rowResults.filter((r) => r.action === "created").length;
|
|
2188
|
+
const keptCount = rowResults.length - createdCount;
|
|
888
2189
|
return {
|
|
889
2190
|
code: EXIT.OK,
|
|
890
|
-
message: `Initialized customer vault at ${targetDir} (${written.length} files written, template v${TEMPLATE_VERSION}).`,
|
|
2191
|
+
message: `Initialized customer vault at ${targetDir} (${written.length} files written, ${createdCount} manifest rows created, ${keptCount} kept, template v${TEMPLATE_VERSION}).`,
|
|
891
2192
|
payload: {
|
|
892
2193
|
ok: true,
|
|
893
2194
|
targetDir,
|
|
894
2195
|
templateVersion: TEMPLATE_VERSION,
|
|
895
2196
|
vaultId,
|
|
896
|
-
written
|
|
2197
|
+
written,
|
|
2198
|
+
rows: rowResults,
|
|
2199
|
+
created: createdCount,
|
|
2200
|
+
kept: keptCount
|
|
897
2201
|
},
|
|
898
2202
|
isError: false,
|
|
899
2203
|
written
|
|
@@ -902,7 +2206,6 @@ function runCustomerVaultInit(args, deps, options = {}) {
|
|
|
902
2206
|
|
|
903
2207
|
// spex-console/src/lib/cli/init-git-finalize.ts
|
|
904
2208
|
import { spawnSync } from "node:child_process";
|
|
905
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
906
2209
|
var nodeGitRunner = {
|
|
907
2210
|
run(args, cwd) {
|
|
908
2211
|
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
@@ -929,14 +2232,19 @@ function ensureTemplateVersionLine(content, version) {
|
|
|
929
2232
|
template_version: ${version}
|
|
930
2233
|
`;
|
|
931
2234
|
}
|
|
2235
|
+
function templatePinHook() {
|
|
2236
|
+
return (ctx) => {
|
|
2237
|
+
const yamlPath = ctx.deps.joinPath(ctx.targetDir, "heyspex.yaml");
|
|
2238
|
+
const existingYaml = ctx.deps.readFile?.(yamlPath);
|
|
2239
|
+
if (existingYaml === null || existingYaml === void 0) return;
|
|
2240
|
+
if (readTemplateVersion(existingYaml) === ctx.templateVersion) return;
|
|
2241
|
+
ctx.deps.writeFile(yamlPath, ensureTemplateVersionLine(existingYaml, ctx.templateVersion));
|
|
2242
|
+
ctx.recordWrite(yamlPath);
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
932
2245
|
function gitFinalizeHook(runner, notify) {
|
|
933
2246
|
return (ctx) => {
|
|
934
2247
|
const target = ctx.targetDir;
|
|
935
|
-
const yamlPath = ctx.deps.joinPath(target, "heyspex.yaml");
|
|
936
|
-
const existingYaml = readFileSync3(yamlPath, "utf8");
|
|
937
|
-
const updatedYaml = ensureTemplateVersionLine(existingYaml, ctx.templateVersion);
|
|
938
|
-
ctx.deps.writeFile(yamlPath, updatedYaml);
|
|
939
|
-
ctx.recordWrite(yamlPath);
|
|
940
2248
|
const probe = runner.run(["--version"], target);
|
|
941
2249
|
if (!probe.ok) {
|
|
942
2250
|
notify("git not found - vault left uninitialized, run git init yourself");
|
|
@@ -962,15 +2270,1102 @@ function gitFinalizeHook(runner, notify) {
|
|
|
962
2270
|
};
|
|
963
2271
|
}
|
|
964
2272
|
|
|
2273
|
+
// spex-console/src/lib/cli/install.ts
|
|
2274
|
+
import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
|
|
2275
|
+
|
|
2276
|
+
// scripts/install-cursor-hooks.mjs
|
|
2277
|
+
import fs2 from "node:fs";
|
|
2278
|
+
import path3 from "node:path";
|
|
2279
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2280
|
+
|
|
2281
|
+
// scripts/cursor-hooks/manifest.mjs
|
|
2282
|
+
var SHIM = "scripts/cursor-hooks/_run.mjs";
|
|
2283
|
+
var DEFAULT_TIMEOUT_SECONDS = 5;
|
|
2284
|
+
var HOOKS = [
|
|
2285
|
+
// ── Session start ───────────────────────────────────────────────────────
|
|
2286
|
+
// Cursor's sessionStart honors `additional_context`, so these injections
|
|
2287
|
+
// land in context exactly as they do in Claude Code.
|
|
2288
|
+
{ event: "sessionStart", hook: "wip-checkpoint", why: "Surface the previous session WIP handoff before work resumes." },
|
|
2289
|
+
{ event: "sessionStart", hook: "inject-dev-server-awareness", why: "Once-per-session reminder that dev-server status is a tool call, not a guess." },
|
|
2290
|
+
{ event: "sessionStart", hook: "inject-coo-seat", why: "Inject the dev-factory COO seat mandate when the COO window is open." },
|
|
2291
|
+
{ event: "sessionStart", hook: "inject-doctrine", why: "Inject the baseline SpexCode Doctrine edition at session start." },
|
|
2292
|
+
{ event: "sessionStart", hook: "coo-guard-check", why: "Warn at session start when a breakdown has not been delegated." },
|
|
2293
|
+
{ event: "sessionStart", hook: "check-auto-memory-budget", why: "Self-heal and budget-check the auto-memory index." },
|
|
2294
|
+
{ event: "sessionStart", hook: "check-claude-md-budget", why: "Advisory budget check on the always-loaded agent contract." },
|
|
2295
|
+
{ event: "sessionStart", hook: "check-mcp-freshness-hook", why: "Warn when the running MCP server is stale versus vendor." },
|
|
2296
|
+
{ event: "sessionStart", hook: "git-reflog-audit", why: "Session-start git audit (stash/reset/rebase witness)." },
|
|
2297
|
+
{ event: "sessionStart", hook: "vault-mirror-catchup", why: "Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ)." },
|
|
2298
|
+
// ── Compaction ──────────────────────────────────────────────────────────
|
|
2299
|
+
// Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
|
|
2300
|
+
// preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
|
|
2301
|
+
// to keep wip-checkpoint's "rollover, not fresh start" framing.
|
|
2302
|
+
{ event: "preCompact", hook: "wip-checkpoint", claudeEvent: "SessionStart", why: "Carry the WIP handoff across a context compaction." },
|
|
2303
|
+
// ── Every user turn ─────────────────────────────────────────────────────
|
|
2304
|
+
// beforeSubmitPrompt is Cursor's UserPromptSubmit. It honors `user_message`
|
|
2305
|
+
// but not `additional_context`, so these three surface in the transcript
|
|
2306
|
+
// rather than as invisible context — the routing still reaches the agent,
|
|
2307
|
+
// and the user can see why it fired.
|
|
2308
|
+
{ event: "beforeSubmitPrompt", hook: "preflight-reminder", why: "Name the required procedure before a filing/destructive/protocol turn." },
|
|
2309
|
+
{ event: "beforeSubmitPrompt", hook: "intent-classifier-trigger", why: "Route capture / research / settings / defer intents to the right skill." },
|
|
2310
|
+
{ event: "beforeSubmitPrompt", hook: "inject-communication-style", why: "Re-assert the plain-language communication rules each turn." },
|
|
2311
|
+
{ event: "beforeSubmitPrompt", hook: "inject-vault-context", timeout: 10, why: "Lexical vault retrieval for the prompt (surfaced as a user message)." },
|
|
2312
|
+
// ── Before every tool call (any tool) ───────────────────────────────────
|
|
2313
|
+
{ event: "preToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], why: "Claim liveness heartbeat to the console." },
|
|
2314
|
+
{ event: "preToolUse", matcher: ".*", hook: "coo-denylist-gate", why: "Hard deny the COO-window deny list." },
|
|
2315
|
+
{ event: "preToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PreToolUse"], why: "Append agent.tool_called rows to the session log." },
|
|
2316
|
+
// ── Before a file write / delete ─────────────────────────────────────────
|
|
2317
|
+
// Cursor's `Write` covers create AND patch (Claude's Write|Edit|MultiEdit);
|
|
2318
|
+
// the shim re-derives which one from the input shape. `Delete` is included on
|
|
2319
|
+
// the vault guard because removing a profile file is the loss it exists to
|
|
2320
|
+
// prevent, and Claude had no separate delete tool to list.
|
|
2321
|
+
{ event: "preToolUse", matcher: "Write|Delete", hook: "env-vault-guard", why: "Block destructive writes to the encrypted env vault (ALWAYS ON)." },
|
|
2322
|
+
{ event: "preToolUse", matcher: "Write", hook: "vault-write-tier-guard", why: "Deny disk writes to vault specs in cloud write mode (HS-FCNH)." },
|
|
2323
|
+
{ event: "preToolUse", matcher: "Write", hook: "settings-write-gate", why: "Require approval before .heyspex/config.yaml changes." },
|
|
2324
|
+
{ event: "preToolUse", matcher: "Write", hook: "enforce-vault-naming", why: "Block vault filenames that violate the naming registry." },
|
|
2325
|
+
{ event: "preToolUse", matcher: "Write", hook: "guard-auto-memory-writes", why: "Block new topic files in the auto-memory directory." },
|
|
2326
|
+
{ event: "preToolUse", matcher: "Write", hook: "preflight-reminder", why: "Preflight reminder on the edit path itself." },
|
|
2327
|
+
{ event: "preToolUse", matcher: "Write", hook: "claim-before-code-gate", why: "Deny the first code edit after plan exit with no claim." },
|
|
2328
|
+
{ event: "preToolUse", matcher: "Write", hook: "directive-obligation-gate", why: "Deny product-code writes while a capture_on_idea obligation is unmet, and (claim-before-build) with no claim/capture (HS-20SW)." },
|
|
2329
|
+
// ── Before a shell command ──────────────────────────────────────────────
|
|
2330
|
+
// beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
|
|
2331
|
+
// to carry the command string, and Cursor honors allow/deny/ask on it.
|
|
2332
|
+
{ event: "beforeShellExecution", hook: "env-vault-guard", why: "Block shell writes/exfiltration targeting the env vault (ALWAYS ON)." },
|
|
2333
|
+
{ event: "beforeShellExecution", hook: "vault-write-tier-guard", why: "Deny shell writes to vault specs in cloud write mode (HS-FCNH)." },
|
|
2334
|
+
{ event: "beforeShellExecution", hook: "settings-write-gate", why: "Catch shell writes to .heyspex/config.yaml." },
|
|
2335
|
+
{ event: "beforeShellExecution", hook: "enforce-vault-naming", why: "Catch vault files created via the shell." },
|
|
2336
|
+
{ event: "beforeShellExecution", hook: "guard-auto-memory-writes", why: "Catch auto-memory topic files created via the shell." },
|
|
2337
|
+
{ event: "beforeShellExecution", hook: "preflight-reminder", why: "Preflight reminder on rm -rf / force-push shaped commands." },
|
|
2338
|
+
{ event: "beforeShellExecution", hook: "dev-server-task-reminder", timeout: 10, why: "Dev-server start/stop hard gate plus DB-safety reminder." },
|
|
2339
|
+
{ event: "beforeShellExecution", hook: "git-stash-gate", timeout: 10, why: "Always ask before a mutating git stash." },
|
|
2340
|
+
{ event: "beforeShellExecution", hook: "git-identity-guard", why: "Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON)." },
|
|
2341
|
+
{ event: "beforeShellExecution", hook: "capture-execution-gate", why: "Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform)." },
|
|
2342
|
+
{ event: "beforeShellExecution", hook: "directive-obligation-gate", why: "Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW)." },
|
|
2343
|
+
// ── Before specific MCP tools ───────────────────────────────────────────
|
|
2344
|
+
// Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
|
|
2345
|
+
// stay on preToolUse rather than beforeMCPExecution — registering both would
|
|
2346
|
+
// fire each gate twice. Scoping is the shim's `tool` regex because a Cursor
|
|
2347
|
+
// matcher cannot name one MCP tool per entry portably.
|
|
2348
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_claim_spec$", hook: "coo-guard-check", why: "Block a claim whose breakdown was never delegated." },
|
|
2349
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "capture-workflow-gate", timeout: 8, why: "Deny filing an implementation spec straight out of an open capture." },
|
|
2350
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_settings_set$", hook: "settings-write-gate", why: "Require approval for settings changes made through MCP." },
|
|
2351
|
+
{ event: "preToolUse", matcher: ".*", tool: "[Nn]eon__(prepare_database_migration|complete_database_migration|reset_from_parent|delete_branch)$", hook: "dev-server-task-reminder", timeout: 10, why: "DB-safety gate on Neon migration/reset/delete tools." },
|
|
2352
|
+
{ event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "diagnose-capture-reminder", why: "Warn when a diagnose is filed with no capture holding the owner words (HS-IK2D)." },
|
|
2353
|
+
{ event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "research-method-guard", why: "Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON)." },
|
|
2354
|
+
{ event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "deep-research-cost-gate", why: "Cost confirmation before a paid deep-research harness launch." },
|
|
2355
|
+
{ event: "preToolUse", matcher: ".*", tool: "^(WebSearch|WebFetch|web_search|web_fetch)$|heyspex_save_research$", hook: "research-protocol-gate", why: "WebSearch/WebFetch/save_research refused until the research preflight ran (HS-K0ZP, HS-EN4F)." },
|
|
2356
|
+
// ── Before a subagent spawn ─────────────────────────────────────────────
|
|
2357
|
+
// subagentStart over preToolUse/Task: it carries subagent_type and the model,
|
|
2358
|
+
// which is exactly what the routing gate decides on, and it can deny.
|
|
2359
|
+
{ event: "subagentStart", hook: "agent-spawn-delegation-gate", why: "Block a build/breakdown/critic subagent on an off-routing model." },
|
|
2360
|
+
{ event: "subagentStart", hook: "capture-execution-gate", why: "Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform)." },
|
|
2361
|
+
{ event: "subagentStart", hook: "directive-obligation-gate", why: "Deny a build subagent while a capture obligation is unmet or the session has no claim/capture (HS-20SW)." },
|
|
2362
|
+
// ── After every tool call ───────────────────────────────────────────────
|
|
2363
|
+
{ event: "postToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], why: "Claim liveness heartbeat after the call resolves." },
|
|
2364
|
+
{ event: "postToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PostToolUse"], why: "Append the tool result row to the session log." },
|
|
2365
|
+
{ event: "postToolUse", matcher: ".*", hook: "directive-obligation-gate", why: "Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW)." },
|
|
2366
|
+
{ event: "postToolUse", matcher: ".*", hook: "close-terminal-on-ship", why: "Close the Launch terminal once the spec ships." },
|
|
2367
|
+
{ event: "postToolUse", matcher: "Write", hook: "notify-status-flip", why: "Sound on an in_review status flip." },
|
|
2368
|
+
{ event: "postToolUse", matcher: "Write", hook: "scripts/stamp-provenance-on-edit.mjs", why: "Stamp assisted_by provenance on direct spec edits." },
|
|
2369
|
+
// afterFileEdit is a distinct signal, not a duplicate: it reports the edit
|
|
2370
|
+
// that actually landed (with old/new strings) even when the write came from a
|
|
2371
|
+
// path postToolUse did not describe as an edit.
|
|
2372
|
+
{ event: "afterFileEdit", matcher: ".*", hook: "close-terminal-on-ship", why: "Ship detection from the concrete file edit." },
|
|
2373
|
+
// ── End of the agent turn ───────────────────────────────────────────────
|
|
2374
|
+
// Cursor's `stop` honors `followup_message`, which is how a blocking Stop
|
|
2375
|
+
// hook re-drives the agent — the same effect as Claude's exit-2 block.
|
|
2376
|
+
{ event: "stop", hook: "emit-claim-status", args: ["--event=Stop"], why: "Mark the claim pending-on-response." },
|
|
2377
|
+
{ event: "stop", hook: "naughty-list-gate", why: "Re-drive the turn when the final message uses banned jargon." },
|
|
2378
|
+
{ event: "stop", hook: "id-display-format-gate", why: "Re-drive the turn when a reply breaks the Naming V3 display-id format." },
|
|
2379
|
+
{ event: "stop", hook: "epic-milestone-phrasing-gate", why: "Re-drive the turn when a reply breaks the epic/milestone phrasing rules." },
|
|
2380
|
+
{ event: "stop", hook: "git-reflog-audit", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
|
|
2381
|
+
{ event: "stop", hook: "enforce-session-sequence", why: "Re-drive the turn when work shipped without record_wip." },
|
|
2382
|
+
{ event: "stop", hook: "directive-obligation-gate", why: "Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW)." },
|
|
2383
|
+
// ── Session end ─────────────────────────────────────────────────────────
|
|
2384
|
+
{ event: "sessionEnd", hook: "session-end", why: "Release the claim and notify the console." },
|
|
2385
|
+
{ event: "sessionEnd", hook: "close-terminal-on-ship", why: "Final terminal cleanup." }
|
|
2386
|
+
];
|
|
2387
|
+
var UNPORTABLE = [
|
|
2388
|
+
{
|
|
2389
|
+
hook: "plan-review-gate",
|
|
2390
|
+
claudeEvent: "PreToolUse/ExitPlanMode",
|
|
2391
|
+
reason: "Cursor exposes no ExitPlanMode tool call to hooks.",
|
|
2392
|
+
coveredBy: ".cursor/rules/plan-review-required.mdc (always-on rule)"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
hook: "plan-exit-ingest-gate",
|
|
2396
|
+
claudeEvent: "PreToolUse/ExitPlanMode",
|
|
2397
|
+
reason: "Same missing trigger; it is also what arms claim-before-code-gate.",
|
|
2398
|
+
coveredBy: ".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"
|
|
2399
|
+
}
|
|
2400
|
+
];
|
|
2401
|
+
function commandFor(entry) {
|
|
2402
|
+
const parts = ["node", SHIM, entry.hook];
|
|
2403
|
+
if (entry.tool) parts.push(`--hs-tool="${entry.tool}"`);
|
|
2404
|
+
if (entry.claudeEvent) parts.push(`--hs-claude-event=${entry.claudeEvent}`);
|
|
2405
|
+
for (const arg of entry.args ?? []) parts.push(arg);
|
|
2406
|
+
return parts.join(" ");
|
|
2407
|
+
}
|
|
2408
|
+
function commandForPlugin(entry) {
|
|
2409
|
+
return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${SHIM}"`);
|
|
2410
|
+
}
|
|
2411
|
+
var CUSTOMER_LAUNCHER = ".cursor/heyspex/run-hook.mjs";
|
|
2412
|
+
function commandForCustomer(entry) {
|
|
2413
|
+
return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${CUSTOMER_LAUNCHER}"`);
|
|
2414
|
+
}
|
|
2415
|
+
function identityFor(entry) {
|
|
2416
|
+
return `${entry.hook}::${entry.tool ?? ""}::${(entry.args ?? []).join(" ")}`;
|
|
2417
|
+
}
|
|
2418
|
+
function timeoutFor(entry) {
|
|
2419
|
+
return entry.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// scripts/lib/customer-kit.mjs
|
|
2423
|
+
import { createHash } from "node:crypto";
|
|
2424
|
+
import fs from "node:fs";
|
|
2425
|
+
import path2 from "node:path";
|
|
2426
|
+
import os2 from "node:os";
|
|
2427
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
2428
|
+
|
|
2429
|
+
// scripts/lib/hook-dispositions.mjs
|
|
2430
|
+
var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
|
|
2431
|
+
var DISPOSITIONS = Object.freeze(["mapped", "advisory-degraded", "unportable", "not-shipped"]);
|
|
2432
|
+
var mapped = (note) => Object.freeze({ disposition: "mapped", ...note ? { note } : {} });
|
|
2433
|
+
var degraded = (note) => Object.freeze({ disposition: "advisory-degraded", note });
|
|
2434
|
+
var unportable = (control) => Object.freeze({ disposition: "unportable", control });
|
|
2435
|
+
var notShipped = Object.freeze({ disposition: "not-shipped" });
|
|
2436
|
+
var CC = mapped();
|
|
2437
|
+
var COPILOT_PROMPT_DROPPED = "Copilot drops command-hook output on userPromptSubmitted (no prompt-time context channel); the text moves to sessionStart / .github/copilot-instructions.md.";
|
|
2438
|
+
var CURSOR_PROMPT_USER_MESSAGE = "Cursor beforeSubmitPrompt honors user_message only (no additional_context): the text surfaces in the transcript instead of as invisible context.";
|
|
2439
|
+
var COPILOT_NO_PLAN_TOOL = "MCP preflight refusals (heyspex_claim_spec / heyspex_submit_for_review / heyspex_save_research) + .github/copilot-instructions.md plan rule (state the plan in chat, claim the spec before editing) + HS-20SW directive-obligation-gate claim-before-build deny";
|
|
2440
|
+
var HOOK_DISPOSITIONS = Object.freeze({
|
|
2441
|
+
// ── customer: session start / compaction ──────────────────────────────────
|
|
2442
|
+
"wip-checkpoint": {
|
|
2443
|
+
"claude-code": CC,
|
|
2444
|
+
cursor: mapped("preCompact is relayed as SessionStart(compact)."),
|
|
2445
|
+
copilot: mapped("sessionStart additionalContext is delivered; the preCompact leg writes WIP only (no output channel), re-injected at the next sessionStart.")
|
|
2446
|
+
},
|
|
2447
|
+
"inject-doctrine": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
2448
|
+
"check-auto-memory-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
2449
|
+
"check-claude-md-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
2450
|
+
"git-reflog-audit": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext and agentStop.") },
|
|
2451
|
+
"vault-mirror-catchup": { "claude-code": CC, cursor: mapped(), copilot: mapped("Detached side effect on sessionStart.") },
|
|
2452
|
+
// ── customer: every user turn ─────────────────────────────────────────────
|
|
2453
|
+
"preflight-reminder": {
|
|
2454
|
+
"claude-code": CC,
|
|
2455
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2456
|
+
copilot: degraded(`${COPILOT_PROMPT_DROPPED} Real enforcement is the MCP preflight refusal.`)
|
|
2457
|
+
},
|
|
2458
|
+
"intent-classifier-trigger": {
|
|
2459
|
+
"claude-code": CC,
|
|
2460
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2461
|
+
copilot: degraded(
|
|
2462
|
+
`${COPILOT_PROMPT_DROPPED} The hook still runs and arms obligation state; the HS-20SW directive-obligation-gate turns that state into a hard deny.`
|
|
2463
|
+
)
|
|
2464
|
+
},
|
|
2465
|
+
"inject-communication-style": {
|
|
2466
|
+
"claude-code": CC,
|
|
2467
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2468
|
+
copilot: degraded(COPILOT_PROMPT_DROPPED)
|
|
2469
|
+
},
|
|
2470
|
+
"inject-vault-context": {
|
|
2471
|
+
"claude-code": CC,
|
|
2472
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
2473
|
+
copilot: degraded(`${COPILOT_PROMPT_DROPPED} Per-prompt vault retrieval has no Copilot channel at all.`)
|
|
2474
|
+
},
|
|
2475
|
+
// ── customer: before a tool call (hard gates) ─────────────────────────────
|
|
2476
|
+
"git-identity-guard": { "claude-code": CC, cursor: mapped("beforeShellExecution. Always on."), copilot: mapped("preToolUse on bash/powershell. Always on.") },
|
|
2477
|
+
"vault-write-tier-guard": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell.") },
|
|
2478
|
+
"settings-write-gate": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell and the MCP settings tool.") },
|
|
2479
|
+
"emit-claim-status": { "claude-code": CC, cursor: mapped(), copilot: mapped("Telemetry on preToolUse/postToolUse/agentStop; the cloud agent needs a network allow rule for the console.") },
|
|
2480
|
+
"enforce-vault-naming": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
|
|
2481
|
+
"guard-auto-memory-writes": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
|
|
2482
|
+
"directive-obligation-gate": {
|
|
2483
|
+
"claude-code": CC,
|
|
2484
|
+
cursor: mapped(),
|
|
2485
|
+
copilot: mapped("HS-20SW: hard deny on task/create/edit/write-shaped shell while a capture obligation is unmet; the only hard capture-on-idea guarantee on Copilot.")
|
|
2486
|
+
},
|
|
2487
|
+
"claim-before-code-gate": {
|
|
2488
|
+
"claude-code": CC,
|
|
2489
|
+
cursor: degraded("Armed only by plan-exit-ingest-gate, which has no Cursor trigger, so it stays dormant; covered by the HS-20SW claim-before-build rule."),
|
|
2490
|
+
copilot: degraded("Armed only by plan-exit-ingest-gate, which has no Copilot trigger (no ExitPlanMode tool), so it stays dormant; covered by the HS-20SW claim-before-build rule.")
|
|
2491
|
+
},
|
|
2492
|
+
"capture-workflow-gate": {
|
|
2493
|
+
"claude-code": CC,
|
|
2494
|
+
cursor: mapped(),
|
|
2495
|
+
copilot: mapped("preToolUse on the MCP heyspex_file_spec tool; MCP tool naming in hook payloads is unverified live (matched by suffix).")
|
|
2496
|
+
},
|
|
2497
|
+
"diagnose-capture-reminder": {
|
|
2498
|
+
"claude-code": CC,
|
|
2499
|
+
cursor: mapped("preToolUse agent_message delivers the reminder."),
|
|
2500
|
+
copilot: degraded("Copilot preToolUse has no context channel for an inject, so this advisory is silent (lost).")
|
|
2501
|
+
},
|
|
2502
|
+
"git-stash-gate": {
|
|
2503
|
+
"claude-code": CC,
|
|
2504
|
+
cursor: mapped(),
|
|
2505
|
+
copilot: mapped('The Claude "ask" becomes a deny where no user is present (cloud agent, CLI autopilot).')
|
|
2506
|
+
},
|
|
2507
|
+
"research-protocol-gate": { "claude-code": CC, cursor: mapped("Always on."), copilot: mapped("preToolUse on web_fetch/web_search and the MCP save_research tool. Always on; the MCP refusal duplicates it.") },
|
|
2508
|
+
"research-method-guard": {
|
|
2509
|
+
"claude-code": CC,
|
|
2510
|
+
cursor: mapped("preToolUse with a Skill|Workflow tool filter. Always on."),
|
|
2511
|
+
copilot: unportable("MCP preflight refusal on heyspex_save_research (hard floor, hook-independent) + .github/copilot-instructions.md research rule; Copilot has no Skill or Workflow tool to hook.")
|
|
2512
|
+
},
|
|
2513
|
+
"deep-research-cost-gate": {
|
|
2514
|
+
"claude-code": CC,
|
|
2515
|
+
cursor: mapped("preToolUse with a Workflow|Skill tool filter."),
|
|
2516
|
+
copilot: unportable("MCP preflight refusal on heyspex_save_research + .github/copilot-instructions.md cost-confirmation rule; Copilot has no Skill or Workflow tool to hook.")
|
|
2517
|
+
},
|
|
2518
|
+
"plan-review-gate": {
|
|
2519
|
+
"claude-code": CC,
|
|
2520
|
+
cursor: unportable(".cursor/rules/plan-review-required.mdc (always-on rule)"),
|
|
2521
|
+
copilot: unportable(COPILOT_NO_PLAN_TOOL)
|
|
2522
|
+
},
|
|
2523
|
+
"plan-exit-ingest-gate": {
|
|
2524
|
+
"claude-code": CC,
|
|
2525
|
+
cursor: unportable(".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"),
|
|
2526
|
+
copilot: unportable(COPILOT_NO_PLAN_TOOL)
|
|
2527
|
+
},
|
|
2528
|
+
// ── customer: end of turn / session ───────────────────────────────────────
|
|
2529
|
+
"naughty-list-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped('agentStop {decision:"block"} re-drive, capped at 8 consecutive blocks.') },
|
|
2530
|
+
"id-display-format-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
|
|
2531
|
+
"epic-milestone-phrasing-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
|
|
2532
|
+
"enforce-session-sequence": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive, capped at 8 consecutive blocks.") },
|
|
2533
|
+
"notify-status-flip": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
|
|
2534
|
+
"close-terminal-on-ship": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse / sessionEnd.") },
|
|
2535
|
+
"scripts/stamp-provenance-on-edit.mjs": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
|
|
2536
|
+
"session-end": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionEnd.") },
|
|
2537
|
+
// ── platform: heyspex-app development machinery (never installed in a customer repo) ──
|
|
2538
|
+
"inject-dev-server-awareness": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2539
|
+
"inject-coo-seat": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2540
|
+
"coo-guard-check": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2541
|
+
"coo-denylist-gate": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2542
|
+
"agent-spawn-delegation-gate": { "claude-code": CC, cursor: mapped("subagentStart."), copilot: notShipped },
|
|
2543
|
+
"capture-execution-gate": { "claude-code": CC, cursor: mapped("beforeShellExecution + subagentStart."), copilot: notShipped },
|
|
2544
|
+
"check-mcp-freshness-hook": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2545
|
+
"dev-server-task-reminder": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
2546
|
+
"env-vault-guard": { "claude-code": CC, cursor: mapped("Always on."), copilot: notShipped },
|
|
2547
|
+
"log-session-activity": { "claude-code": CC, cursor: mapped(), copilot: notShipped }
|
|
2548
|
+
});
|
|
2549
|
+
function dispositionFor(hook, host) {
|
|
2550
|
+
return HOOK_DISPOSITIONS[hook]?.[host] ?? null;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// scripts/lib/customer-kit.mjs
|
|
2554
|
+
var HOOK_SCOPES = Object.freeze({
|
|
2555
|
+
// ── customer: vault / workflow / safety gates ─────────────────────────────
|
|
2556
|
+
"wip-checkpoint": { scope: "customer", why: "Previous-session WIP handoff, read from the active vault." },
|
|
2557
|
+
"inject-doctrine": {
|
|
2558
|
+
scope: "customer",
|
|
2559
|
+
why: "Owner ruling 2026-10-03 (HS-A2PO): ships. A customer session gets the baseline edition of SpexCode Doctrine (CUSTOMER_HOOK_DATA), the foundation its own learned layer is built on; never the platform's learned file."
|
|
2560
|
+
},
|
|
2561
|
+
"check-auto-memory-budget": { scope: "customer", why: "Auto-memory index budget for the session project." },
|
|
2562
|
+
"check-claude-md-budget": { scope: "customer", why: "Budget check on the project's always-loaded agent contract." },
|
|
2563
|
+
"git-reflog-audit": { scope: "customer", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
|
|
2564
|
+
"preflight-reminder": { scope: "customer", why: "Names the required procedure before filing/destructive turns." },
|
|
2565
|
+
"intent-classifier-trigger": { scope: "customer", why: "Routes capture/research/settings/defer intents." },
|
|
2566
|
+
"inject-communication-style": { scope: "customer", why: "Plain-language communication rules each turn." },
|
|
2567
|
+
"inject-vault-context": { scope: "customer", why: "Lexical vault retrieval for the prompt." },
|
|
2568
|
+
"git-identity-guard": { scope: "customer", why: "Agents never write git identity (owner ruling 2026-09-19)." },
|
|
2569
|
+
"vault-write-tier-guard": { scope: "customer", why: "Cloud write mode: disk edits to vault specs are a mirror the console never reads back (HS-FCNH)." },
|
|
2570
|
+
"settings-write-gate": { scope: "customer", why: "Approval before the vault's .heyspex/config.yaml changes." },
|
|
2571
|
+
"emit-claim-status": { scope: "customer", why: "Claim liveness heartbeat to the console." },
|
|
2572
|
+
"enforce-vault-naming": { scope: "customer", why: "Naming V3 filename guard (HS-RY2N)." },
|
|
2573
|
+
"guard-auto-memory-writes": { scope: "customer", why: "No new topic files in the auto-memory directory." },
|
|
2574
|
+
"plan-review-gate": { scope: "customer", why: "Plan review + QC markers before ExitPlanMode." },
|
|
2575
|
+
"plan-exit-ingest-gate": { scope: "customer", why: "Plan ingest on ExitPlanMode; arms claim-before-code." },
|
|
2576
|
+
"claim-before-code-gate": { scope: "customer", why: "First code edit after plan exit needs a claim." },
|
|
2577
|
+
"directive-obligation-gate": { scope: "customer", why: "A capture_on_idea directive leaves a durable obligation; an unmet one denies sub-agent spawns, product-code writes and builds, and re-drives Stop once. Also claim-before-build (HS-20SW)." },
|
|
2578
|
+
"capture-workflow-gate": { scope: "customer", why: "No implementation spec straight out of an open capture." },
|
|
2579
|
+
"vault-mirror-catchup": { scope: "customer", why: "Cloud write mode: a SessionStart detached `hs pull --apply` closes a stale disk mirror before the first tool call (HS-XJFQ)." },
|
|
2580
|
+
"diagnose-capture-reminder": { scope: "customer", why: "Warn when a diagnose is filed with no capture holding the owner's words (HS-IK2D)." },
|
|
2581
|
+
"git-stash-gate": { scope: "customer", why: "Always ask before a mutating git stash." },
|
|
2582
|
+
"research-method-guard": { scope: "customer", why: "Research method guard (HS-EN4F)." },
|
|
2583
|
+
"deep-research-cost-gate": { scope: "customer", why: "Cost confirmation before a paid deep-research launch." },
|
|
2584
|
+
"research-protocol-gate": { scope: "customer", why: "WebSearch/WebFetch refused until research preflight ran (HS-K0ZP, HS-EN4F)." },
|
|
2585
|
+
"naughty-list-gate": { scope: "customer", why: "Re-drive a final message that uses banned jargon." },
|
|
2586
|
+
"id-display-format-gate": { scope: "customer", why: "Naming V3 display-id format in replies." },
|
|
2587
|
+
"epic-milestone-phrasing-gate": { scope: "customer", why: "Epic/milestone phrasing rules in replies." },
|
|
2588
|
+
"enforce-session-sequence": { scope: "customer", why: "Re-drive a turn that shipped without record_wip." },
|
|
2589
|
+
"notify-status-flip": { scope: "customer", why: "Sound on an in_review status flip." },
|
|
2590
|
+
"close-terminal-on-ship": { scope: "customer", why: "Close the launch terminal once the spec ships." },
|
|
2591
|
+
"scripts/stamp-provenance-on-edit.mjs": { scope: "customer", why: "Stamp assisted_by provenance on direct spec edits (active vault)." },
|
|
2592
|
+
"session-end": { scope: "customer", why: "Release the claim and export session events." },
|
|
2593
|
+
// ── platform: heyspex-app's own development machinery ─────────────────────
|
|
2594
|
+
"inject-dev-server-awareness": { scope: "platform", why: "About the spex-console dev server in heyspex-app." },
|
|
2595
|
+
"inject-coo-seat": { scope: "platform", why: "dev-factory COO seat mandate." },
|
|
2596
|
+
"coo-guard-check": { scope: "platform", why: "dev-factory breakdown-delegation guard." },
|
|
2597
|
+
"coo-denylist-gate": { scope: "platform", why: "dev-factory COO window deny list." },
|
|
2598
|
+
"agent-spawn-delegation-gate": { scope: "platform", why: "dev-factory seat routing for subagents." },
|
|
2599
|
+
"capture-execution-gate": { scope: "platform", why: "Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8)." },
|
|
2600
|
+
"check-mcp-freshness-hook": { scope: "platform", why: "Freshness of the vendored MCP build \u2014 a platform-dev concern." },
|
|
2601
|
+
"dev-server-task-reminder": { scope: "platform", why: "spex-console dev server + Neon DB safety." },
|
|
2602
|
+
"env-vault-guard": { scope: "platform", why: "Guards heyspex-app's encrypted env vault paths." },
|
|
2603
|
+
"log-session-activity": { scope: "platform", why: "Resolves .heyspex state from the platform checkout's own location, not the project \u2014 would log customer sessions into heyspex-app." }
|
|
2604
|
+
});
|
|
2605
|
+
var CUSTOMER_HOOK_DATA = Object.freeze({
|
|
2606
|
+
"inject-doctrine": Object.freeze(["spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json"])
|
|
2607
|
+
});
|
|
2608
|
+
var CLAUDE_HOOK_ROWS = Object.freeze([
|
|
2609
|
+
{ event: "SessionStart", hook: "wip-checkpoint", timeout: 5 },
|
|
2610
|
+
{ event: "SessionStart", hook: "inject-dev-server-awareness", timeout: 5 },
|
|
2611
|
+
{ event: "SessionStart", hook: "inject-coo-seat", timeout: 5 },
|
|
2612
|
+
{ event: "SessionStart", hook: "inject-doctrine", timeout: 5 },
|
|
2613
|
+
{ event: "SessionStart", hook: "coo-guard-check", timeout: 5 },
|
|
2614
|
+
{ event: "SessionStart", hook: "check-auto-memory-budget", timeout: 5 },
|
|
2615
|
+
{ event: "SessionStart", hook: "check-claude-md-budget", timeout: 5 },
|
|
2616
|
+
{ event: "SessionStart", hook: "check-mcp-freshness-hook", timeout: 5 },
|
|
2617
|
+
{ event: "SessionStart", hook: "git-reflog-audit", timeout: 5 },
|
|
2618
|
+
{ event: "SessionStart", hook: "vault-mirror-catchup", timeout: 5 },
|
|
2619
|
+
{ event: "PreCompact", hook: "wip-checkpoint", timeout: 5 },
|
|
2620
|
+
{ event: "UserPromptSubmit", hook: "preflight-reminder", timeout: 5 },
|
|
2621
|
+
{ event: "UserPromptSubmit", hook: "intent-classifier-trigger", timeout: 5 },
|
|
2622
|
+
{ event: "UserPromptSubmit", hook: "inject-communication-style", timeout: 5 },
|
|
2623
|
+
{ event: "UserPromptSubmit", hook: "inject-vault-context", timeout: 10 },
|
|
2624
|
+
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "env-vault-guard", timeout: 5 },
|
|
2625
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-identity-guard", timeout: 5 },
|
|
2626
|
+
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "vault-write-tier-guard", timeout: 5 },
|
|
2627
|
+
{ event: "PreToolUse", matcher: "Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set", hook: "settings-write-gate", timeout: 5 },
|
|
2628
|
+
{ event: "PreToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], timeout: 5 },
|
|
2629
|
+
{ event: "PreToolUse", matcher: ".*", hook: "coo-denylist-gate", timeout: 5 },
|
|
2630
|
+
{ event: "PreToolUse", matcher: "Agent|Task", hook: "agent-spawn-delegation-gate", timeout: 5 },
|
|
2631
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell|Agent|Task", hook: "capture-execution-gate", timeout: 5 },
|
|
2632
|
+
{ event: "PreToolUse", matcher: "Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "directive-obligation-gate", timeout: 5 },
|
|
2633
|
+
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_claim_spec", hook: "coo-guard-check", timeout: 5 },
|
|
2634
|
+
{ event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "enforce-vault-naming", timeout: 5 },
|
|
2635
|
+
{ event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "guard-auto-memory-writes", timeout: 5 },
|
|
2636
|
+
{ event: "PreToolUse", matcher: "Edit|MultiEdit|Write|Bash|PowerShell", hook: "preflight-reminder", timeout: 5 },
|
|
2637
|
+
{ event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-review-gate", timeout: 8 },
|
|
2638
|
+
{ event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-exit-ingest-gate", timeout: 5 },
|
|
2639
|
+
{ event: "PreToolUse", matcher: "Edit|MultiEdit|Write", hook: "claim-before-code-gate", timeout: 5 },
|
|
2640
|
+
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "capture-workflow-gate", timeout: 8 },
|
|
2641
|
+
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "diagnose-capture-reminder", timeout: 5 },
|
|
2642
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell|mcp__neon__prepare_database_migration|mcp__neon__complete_database_migration|mcp__neon__reset_from_parent|mcp__neon__delete_branch|mcp__Neon__prepare_database_migration|mcp__Neon__complete_database_migration|mcp__Neon__reset_from_parent|mcp__Neon__delete_branch", hook: "dev-server-task-reminder", timeout: 10 },
|
|
2643
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-stash-gate", timeout: 10 },
|
|
2644
|
+
{ event: "PreToolUse", matcher: "Skill|Workflow", hook: "research-method-guard", timeout: 5 },
|
|
2645
|
+
{ event: "PreToolUse", matcher: "Workflow|Skill", hook: "deep-research-cost-gate", timeout: 5 },
|
|
2646
|
+
{ event: "PreToolUse", matcher: "WebSearch|WebFetch|mcp__heyspex__heyspex_save_research|mcp__plugin_heyspex_heyspex__heyspex_save_research", hook: "research-protocol-gate", timeout: 5 },
|
|
2647
|
+
{ event: "PreToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
|
|
2648
|
+
{ event: "Stop", hook: "emit-claim-status", args: ["--event=Stop"], timeout: 5 },
|
|
2649
|
+
{ event: "Stop", hook: "naughty-list-gate", timeout: 5 },
|
|
2650
|
+
{ event: "Stop", hook: "id-display-format-gate", timeout: 5 },
|
|
2651
|
+
{ event: "Stop", hook: "epic-milestone-phrasing-gate", timeout: 5 },
|
|
2652
|
+
{ event: "Stop", hook: "git-reflog-audit", timeout: 5 },
|
|
2653
|
+
{ event: "Stop", hook: "enforce-session-sequence", timeout: 5 },
|
|
2654
|
+
{ event: "Stop", hook: "directive-obligation-gate", timeout: 5 },
|
|
2655
|
+
{ event: "PostToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], timeout: 5 },
|
|
2656
|
+
{ event: "PostToolUse", matcher: "Skill|mcp__heyspex__heyspex_update_capture|mcp__heyspex__heyspex_promote_capture.*|mcp__heyspex__heyspex_file_spec|mcp__heyspex__heyspex_claim_spec|mcp__heyspex__heyspex_release_spec|mcp__heyspex__heyspex_defer_spec", hook: "directive-obligation-gate", timeout: 5 },
|
|
2657
|
+
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "notify-status-flip", timeout: 5 },
|
|
2658
|
+
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "close-terminal-on-ship", timeout: 5 },
|
|
2659
|
+
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "scripts/stamp-provenance-on-edit.mjs", timeout: 5 },
|
|
2660
|
+
{ event: "PostToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
|
|
2661
|
+
{ event: "SessionEnd", hook: "session-end", timeout: 5 },
|
|
2662
|
+
{ event: "SessionEnd", hook: "close-terminal-on-ship", timeout: 5 }
|
|
2663
|
+
]);
|
|
2664
|
+
var COMMAND_SCOPES = Object.freeze({
|
|
2665
|
+
"hs-backfill.md": { scope: "customer", why: "Backfill completed work into the vault." },
|
|
2666
|
+
"hs-capture.md": { scope: "customer", why: "Capture intake." },
|
|
2667
|
+
"hs-defer.md": { scope: "customer", why: "Split + defer remaining work." },
|
|
2668
|
+
"hs-doctor.md": { scope: "customer", why: "Install-surface repair when heyspex_* tools are missing." },
|
|
2669
|
+
"hs-ground.md": { scope: "customer", why: "Pre-plan grounding report." },
|
|
2670
|
+
"hs-handoff.md": { scope: "customer", why: "Short-code lane handoff." },
|
|
2671
|
+
"hs-next.md": { scope: "customer", why: "On-deck prompt + claim handshake." },
|
|
2672
|
+
"hs-promote.md": { scope: "customer", why: "Capture \u2192 research \u2192 milestone promotion." },
|
|
2673
|
+
"hs-push.md": { scope: "customer", why: "Push an epic plan into the vault." },
|
|
2674
|
+
"hs-qc.md": { scope: "customer", why: "Post-plan QC gate." },
|
|
2675
|
+
"hs-res.md": { scope: "customer", why: "Research method entrypoint (SpexCrawl/DeepSeek)." },
|
|
2676
|
+
"hs-resolve.md": { scope: "customer", why: "Resolution lane." },
|
|
2677
|
+
"hs-review.md": { scope: "customer", why: "Review queue." },
|
|
2678
|
+
"hs-view.md": { scope: "customer", why: "Open a console surface." },
|
|
2679
|
+
"hs-auto.md": { scope: "platform", why: "Routes work into dev-factory (platform build system)." },
|
|
2680
|
+
"hs-dev.md": { scope: "platform", why: "Admin/dev-only HeySpex dev/prod mode switch." },
|
|
2681
|
+
"hs-log.md": { scope: "platform", why: "Admin/dev-only dev-server log watcher." },
|
|
2682
|
+
"kill-ports.md": { scope: "platform", why: "Kills local dev-server ports." },
|
|
2683
|
+
"prototype.md": { scope: "platform", why: "Console design-preview prototypes inside spex-console." }
|
|
2684
|
+
});
|
|
2685
|
+
function hookScope(hook) {
|
|
2686
|
+
return HOOK_SCOPES[hook]?.scope ?? null;
|
|
2687
|
+
}
|
|
2688
|
+
function toPosixPath(p) {
|
|
2689
|
+
return String(p).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2690
|
+
}
|
|
2691
|
+
function samePlatformPath(a, b) {
|
|
2692
|
+
if (!a || !b) return false;
|
|
2693
|
+
const norm = (p) => {
|
|
2694
|
+
const s = toPosixPath(path2.resolve(p));
|
|
2695
|
+
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
2696
|
+
};
|
|
2697
|
+
return norm(a) === norm(b);
|
|
2698
|
+
}
|
|
2699
|
+
var MCP_PROJECT_PREFIX = "mcp__heyspex__";
|
|
2700
|
+
var MCP_PLUGIN_PREFIX = "mcp__plugin_heyspex_heyspex__";
|
|
2701
|
+
function widenMcpMatcher(matcher) {
|
|
2702
|
+
if (!matcher) return matcher;
|
|
2703
|
+
const parts = matcher.split("|");
|
|
2704
|
+
const out = [];
|
|
2705
|
+
for (const part of parts) {
|
|
2706
|
+
if (!out.includes(part)) out.push(part);
|
|
2707
|
+
if (part.startsWith(MCP_PROJECT_PREFIX)) {
|
|
2708
|
+
const plugin = MCP_PLUGIN_PREFIX + part.slice(MCP_PROJECT_PREFIX.length);
|
|
2709
|
+
if (!parts.includes(plugin) && !out.includes(plugin)) out.push(plugin);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return out.join("|");
|
|
2713
|
+
}
|
|
2714
|
+
function customerHookRows() {
|
|
2715
|
+
return CLAUDE_HOOK_ROWS.filter((r) => hookScope(r.hook) === "customer");
|
|
2716
|
+
}
|
|
2717
|
+
function hookTail(row) {
|
|
2718
|
+
return [row.hook, ...row.args ?? []].join(" ");
|
|
2719
|
+
}
|
|
2720
|
+
var CLAUDE_HOOK_SHIM = "scripts/claude-hooks/_run.mjs";
|
|
2721
|
+
var CURSOR_HOOK_SHIM = "scripts/cursor-hooks/_run.mjs";
|
|
2722
|
+
var CUSTOMER_LAUNCHERS = Object.freeze({
|
|
2723
|
+
claude: Object.freeze({
|
|
2724
|
+
rel: Object.freeze([".claude", "heyspex", "run-hook.mjs"]),
|
|
2725
|
+
projectDirEnv: "CLAUDE_PROJECT_DIR",
|
|
2726
|
+
shim: CLAUDE_HOOK_SHIM,
|
|
2727
|
+
registry: ".claude/settings.json"
|
|
2728
|
+
}),
|
|
2729
|
+
cursor: Object.freeze({
|
|
2730
|
+
rel: Object.freeze([".cursor", "heyspex", "run-hook.mjs"]),
|
|
2731
|
+
projectDirEnv: "CURSOR_PROJECT_DIR",
|
|
2732
|
+
shim: CURSOR_HOOK_SHIM,
|
|
2733
|
+
registry: ".cursor/hooks.json"
|
|
2734
|
+
})
|
|
2735
|
+
});
|
|
2736
|
+
var BOOTSTRAP_WALK = "var d=process.cwd(),r=e.CLAUDE_PROJECT_DIR;if(!r||!f.existsSync(p.join(r,...n))){r='';for(;;){if(f.existsSync(p.join(d,...n))){r=d;break}var q=p.dirname(d);if(q===d)break;d=q}}";
|
|
2737
|
+
function buildBootstrap({ plugin = false } = {}) {
|
|
2738
|
+
const walk = BOOTSTRAP_WALK;
|
|
2739
|
+
const body = plugin ? "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='',m=e.CLAUDE_PLUGIN_ROOT||e.COPILOT_PLUGIN_ROOT||e.PLUGIN_ROOT;if(m&&f.existsSync(p.join(m,'bin','run-hook.mjs')))s=p.join(m,'bin','run-hook.mjs');if(!s){" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}}" : "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='';" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}";
|
|
2740
|
+
return body + "if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});" + (plugin ? "//bin/run-hook.mjs" : "//scripts/claude-hooks/_run.mjs");
|
|
2741
|
+
}
|
|
2742
|
+
var PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
|
|
2743
|
+
var PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
|
|
2744
|
+
function buildCustomerBootstrap() {
|
|
2745
|
+
const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(",");
|
|
2746
|
+
return "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=[" + rel + "],s='';" + BOOTSTRAP_WALK + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});//" + CUSTOMER_LAUNCHERS.claude.rel.join("/");
|
|
2747
|
+
}
|
|
2748
|
+
var CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
|
|
2749
|
+
var LAUNCHER_TEMPLATE = [
|
|
2750
|
+
"#!/usr/bin/env node",
|
|
2751
|
+
"// HeySpex hook launcher -- GENERATED by hs install / hs connect. Commit it with this repo.",
|
|
2752
|
+
"//",
|
|
2753
|
+
"// Every HeySpex hook registered in __REGISTRY__ runs",
|
|
2754
|
+
'// node "$__ENV__/__LAUNCHER__" <hook> [args]',
|
|
2755
|
+
"// This file carries NO machine-specific path. It finds the heyspex package this",
|
|
2756
|
+
"// repo depends on (node_modules/heyspex) and hands off in-process to the",
|
|
2757
|
+
"// package's hook shim (__SHIM__), so the same committed command works on every",
|
|
2758
|
+
"// machine the repo is cloned to.",
|
|
2759
|
+
"//",
|
|
2760
|
+
"// Resolution order (the first root that carries the shim wins):",
|
|
2761
|
+
"// 1. HEYSPEX_PLATFORM_ROOT explicit machine-local override",
|
|
2762
|
+
"// 2. HEYSPEX_MCP_ENTRY -> its package root machine-local override",
|
|
2763
|
+
"// 3. node_modules/heyspex, walking up from this file (the repo-local dependency)",
|
|
2764
|
+
"// 4. node_modules/heyspex up from $__ENV__, $CLAUDE_PROJECT_DIR, $CURSOR_PROJECT_DIR, cwd",
|
|
2765
|
+
"// 5. <npm root -g>/heyspex a global install, last resort",
|
|
2766
|
+
"// Nothing found -> one stderr line and exit 0 (fail-open, like the shim itself).",
|
|
2767
|
+
"import { spawnSync } from 'node:child_process';",
|
|
2768
|
+
"import { existsSync } from 'node:fs';",
|
|
2769
|
+
"import { dirname, resolve } from 'node:path';",
|
|
2770
|
+
"import { fileURLToPath, pathToFileURL } from 'node:url';",
|
|
2771
|
+
"",
|
|
2772
|
+
"const SHIM = '__SHIM__'.split('/');",
|
|
2773
|
+
"const here = dirname(fileURLToPath(import.meta.url));",
|
|
2774
|
+
"const env = process.env;",
|
|
2775
|
+
"const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);",
|
|
2776
|
+
"const hasShim = (root) => Boolean(root) && existsSync(resolve(root, ...SHIM));",
|
|
2777
|
+
"",
|
|
2778
|
+
"function packageUpFrom(start) {",
|
|
2779
|
+
" let dir = resolve(start);",
|
|
2780
|
+
" for (;;) {",
|
|
2781
|
+
" const candidate = resolve(dir, 'node_modules', 'heyspex');",
|
|
2782
|
+
" if (hasShim(candidate)) return candidate;",
|
|
2783
|
+
" const parent = dirname(dir);",
|
|
2784
|
+
" if (parent === dir) return null;",
|
|
2785
|
+
" dir = parent;",
|
|
2786
|
+
" }",
|
|
2787
|
+
"}",
|
|
2788
|
+
"",
|
|
2789
|
+
"function globalPackage() {",
|
|
2790
|
+
" try {",
|
|
2791
|
+
" // One command string through the shell: npm is a .cmd on Windows, and a",
|
|
2792
|
+
" // shell + args array is what node deprecates (DEP0190).",
|
|
2793
|
+
" const res = spawnSync('npm root -g', { encoding: 'utf8', timeout: 5000, shell: true });",
|
|
2794
|
+
" const root = String(res.stdout ?? '').trim();",
|
|
2795
|
+
" return root ? resolve(root, 'heyspex') : null;",
|
|
2796
|
+
" } catch {",
|
|
2797
|
+
" return null;",
|
|
2798
|
+
" }",
|
|
2799
|
+
"}",
|
|
2800
|
+
"",
|
|
2801
|
+
"const mcpEntry = text(env.HEYSPEX_MCP_ENTRY);",
|
|
2802
|
+
"const starts = [...new Set([here, text(env.__ENV__), text(env.CLAUDE_PROJECT_DIR), text(env.CURSOR_PROJECT_DIR), process.cwd()].filter(Boolean))];",
|
|
2803
|
+
"let root =",
|
|
2804
|
+
" [text(env.HEYSPEX_PLATFORM_ROOT), mcpEntry && resolve(mcpEntry, '..', '..', '..')].find(hasShim) ??",
|
|
2805
|
+
" starts.map(packageUpFrom).find(Boolean) ??",
|
|
2806
|
+
" null;",
|
|
2807
|
+
"if (!root) {",
|
|
2808
|
+
" const global = globalPackage();",
|
|
2809
|
+
" if (hasShim(global)) root = global;",
|
|
2810
|
+
"}",
|
|
2811
|
+
"",
|
|
2812
|
+
"if (!root) {",
|
|
2813
|
+
" process.stderr.write(",
|
|
2814
|
+
" '[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: no heyspex package found ' +",
|
|
2815
|
+
` '(run "npm install heyspex" in this repo, or set HEYSPEX_PLATFORM_ROOT). Looked for __SHIM__ under ' +`,
|
|
2816
|
+
" 'HEYSPEX_PLATFORM_ROOT, HEYSPEX_MCP_ENTRY, node_modules/heyspex up from ' + starts.join(', ') + ', and npm root -g.\\n',",
|
|
2817
|
+
" );",
|
|
2818
|
+
" process.exit(0);",
|
|
2819
|
+
"}",
|
|
2820
|
+
"",
|
|
2821
|
+
"// argv is already [node, <this file>, <hook>, ...args] -- the shape the shim",
|
|
2822
|
+
'// reads; the shim path replaces this file so a shim guarded by "am I main?"',
|
|
2823
|
+
"// (the Cursor one) runs exactly as if Cursor had invoked it directly.",
|
|
2824
|
+
"const shim = resolve(root, ...SHIM);",
|
|
2825
|
+
"process.argv[1] = shim;",
|
|
2826
|
+
"try {",
|
|
2827
|
+
" await import(pathToFileURL(shim).href);",
|
|
2828
|
+
"} catch (err) {",
|
|
2829
|
+
" process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
|
|
2830
|
+
" process.exit(0);",
|
|
2831
|
+
"}",
|
|
2832
|
+
""
|
|
2833
|
+
].join("\n");
|
|
2834
|
+
function renderCustomerLauncher(surface = "claude") {
|
|
2835
|
+
const l = CUSTOMER_LAUNCHERS[surface];
|
|
2836
|
+
return LAUNCHER_TEMPLATE.replace(
|
|
2837
|
+
/__(REGISTRY|ENV|LAUNCHER|SHIM)__/g,
|
|
2838
|
+
(_m, token) => token === "REGISTRY" ? l.registry : token === "ENV" ? l.projectDirEnv : token === "LAUNCHER" ? l.rel.join("/") : l.shim
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
function planCustomerLauncherWrite(existing, surface = "claude") {
|
|
2842
|
+
const text = renderCustomerLauncher(surface);
|
|
2843
|
+
if (existing === null || existing === void 0) return { write: true, reason: "missing", text };
|
|
2844
|
+
if (String(existing).replace(/\r\n/g, "\n") === text) return { write: false, reason: "unchanged", text };
|
|
2845
|
+
return { write: true, reason: "update", text };
|
|
2846
|
+
}
|
|
2847
|
+
function renderCustomerHooks(runnerPrefix) {
|
|
2848
|
+
return customerHookRows().map((row) => ({
|
|
2849
|
+
event: row.event,
|
|
2850
|
+
...row.matcher ? { matcher: widenMcpMatcher(row.matcher) } : {},
|
|
2851
|
+
command: `${runnerPrefix} ${hookTail(row)}`,
|
|
2852
|
+
timeout: row.timeout,
|
|
2853
|
+
tail: hookTail(row)
|
|
2854
|
+
}));
|
|
2855
|
+
}
|
|
2856
|
+
function renderPluginHooksDocument() {
|
|
2857
|
+
const hooks = {};
|
|
2858
|
+
for (const d of renderCustomerHooks(PLUGIN_RUNNER_PREFIX)) {
|
|
2859
|
+
(hooks[d.event] ??= []).push({
|
|
2860
|
+
...d.matcher ? { matcher: d.matcher } : {},
|
|
2861
|
+
hooks: [{ type: "command", command: d.command, timeout: d.timeout }]
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
return {
|
|
2865
|
+
hooks
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
function renderPluginHooksText() {
|
|
2869
|
+
return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}
|
|
2870
|
+
`;
|
|
2871
|
+
}
|
|
2872
|
+
var COPILOT_EVENT_NAMES = Object.freeze({
|
|
2873
|
+
SessionStart: "sessionStart",
|
|
2874
|
+
SessionEnd: "sessionEnd",
|
|
2875
|
+
UserPromptSubmit: "userPromptSubmitted",
|
|
2876
|
+
PreToolUse: "preToolUse",
|
|
2877
|
+
PostToolUse: "postToolUse",
|
|
2878
|
+
Stop: "agentStop",
|
|
2879
|
+
PreCompact: "preCompact"
|
|
2880
|
+
});
|
|
2881
|
+
var GITHUB_HOOKS_STATE_ARMING = Object.freeze(["intent-classifier-trigger"]);
|
|
2882
|
+
var COPILOT_TOOL_NAMES = Object.freeze({
|
|
2883
|
+
Write: "create",
|
|
2884
|
+
Edit: "edit",
|
|
2885
|
+
MultiEdit: "edit",
|
|
2886
|
+
NotebookEdit: "edit",
|
|
2887
|
+
Bash: "bash",
|
|
2888
|
+
PowerShell: "powershell",
|
|
2889
|
+
Agent: "task",
|
|
2890
|
+
Task: "task",
|
|
2891
|
+
WebFetch: "web_fetch",
|
|
2892
|
+
WebSearch: "web_search"
|
|
2893
|
+
});
|
|
2894
|
+
function toCopilotMatcher(matcher) {
|
|
2895
|
+
if (!matcher) return void 0;
|
|
2896
|
+
const out = [];
|
|
2897
|
+
for (const part of matcher.split("|")) {
|
|
2898
|
+
let t = null;
|
|
2899
|
+
if (part === ".*") t = ".*";
|
|
2900
|
+
else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
|
|
2901
|
+
else if (part.startsWith(MCP_PROJECT_PREFIX)) t = ".*" + part.slice(MCP_PROJECT_PREFIX.length);
|
|
2902
|
+
else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = ".*" + part.slice(MCP_PLUGIN_PREFIX.length);
|
|
2903
|
+
if (t && !out.includes(t)) out.push(t);
|
|
2904
|
+
}
|
|
2905
|
+
if (out.includes(".*")) return ".*";
|
|
2906
|
+
return out.length ? out.join("|") : null;
|
|
2907
|
+
}
|
|
2908
|
+
function githubHookNames() {
|
|
2909
|
+
const names = /* @__PURE__ */ new Set();
|
|
2910
|
+
for (const r of customerHookRows()) {
|
|
2911
|
+
if (dispositionFor(r.hook, "copilot")?.disposition === "mapped" || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
|
|
2912
|
+
}
|
|
2913
|
+
return names;
|
|
2914
|
+
}
|
|
2915
|
+
function githubHookCommand(row) {
|
|
2916
|
+
const tail = hookTail(row);
|
|
2917
|
+
return `R=node_modules/heyspex/scripts/claude-hooks/_run.mjs; [ -f "$R" ] || R=./scripts/claude-hooks/_run.mjs; [ -f "$R" ] || exit 0; exec node "$R" ${tail}`;
|
|
2918
|
+
}
|
|
2919
|
+
function renderGithubHooksDocument() {
|
|
2920
|
+
const allowed = githubHookNames();
|
|
2921
|
+
const hooks = {};
|
|
2922
|
+
for (const row of customerHookRows()) {
|
|
2923
|
+
if (!allowed.has(row.hook)) continue;
|
|
2924
|
+
const event = COPILOT_EVENT_NAMES[row.event];
|
|
2925
|
+
if (!event) continue;
|
|
2926
|
+
const matcher = toCopilotMatcher(row.matcher);
|
|
2927
|
+
if (matcher === null) continue;
|
|
2928
|
+
(hooks[event] ??= []).push({
|
|
2929
|
+
type: "command",
|
|
2930
|
+
...matcher ? { matcher } : {},
|
|
2931
|
+
bash: githubHookCommand(row),
|
|
2932
|
+
timeoutSec: row.timeout
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
return { version: 1, hooks };
|
|
2936
|
+
}
|
|
2937
|
+
function renderGithubHooksText() {
|
|
2938
|
+
return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}
|
|
2939
|
+
`;
|
|
2940
|
+
}
|
|
2941
|
+
var HARNESS_COMMAND_SURFACES = Object.freeze([
|
|
2942
|
+
{
|
|
2943
|
+
installedDir: Object.freeze([".claude", "commands"]),
|
|
2944
|
+
packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "claude"]),
|
|
2945
|
+
devSourceDir: Object.freeze([".claude", "commands"])
|
|
2946
|
+
},
|
|
2947
|
+
{
|
|
2948
|
+
installedDir: Object.freeze([".cursor", "commands"]),
|
|
2949
|
+
packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "cursor"]),
|
|
2950
|
+
devSourceDir: Object.freeze([".cursor", "commands"])
|
|
2951
|
+
}
|
|
2952
|
+
]);
|
|
2953
|
+
var NPM_ALIASES = Object.freeze({ doctor: "doctor", setup: "doctor --fix" });
|
|
2954
|
+
var SCRIPT_VERBS = Object.freeze({
|
|
2955
|
+
"scripts/heyspex-doctor.mjs": "doctor",
|
|
2956
|
+
"scripts/hs-view.mjs": "view",
|
|
2957
|
+
"scripts/hs-capture-cli.mjs": "capture",
|
|
2958
|
+
"scripts/hs-next.mjs": "next",
|
|
2959
|
+
"scripts/hs-mode.mjs": "mode",
|
|
2960
|
+
"scripts/hs-settings.mjs": "settings",
|
|
2961
|
+
"scripts/hs-companion.mjs": "companion",
|
|
2962
|
+
"scripts/wip-ledger-health.mjs": "wip",
|
|
2963
|
+
"scripts/dev-server-status.mjs": "server status",
|
|
2964
|
+
"scripts/dev-server-stop.mjs": "server stop"
|
|
2965
|
+
});
|
|
2966
|
+
var PORTABLE_PACKAGE_DIR = "node_modules/heyspex";
|
|
2967
|
+
var fsDeps = {
|
|
2968
|
+
joinPath: (...s) => path2.join(...s),
|
|
2969
|
+
readFile: (p) => {
|
|
2970
|
+
try {
|
|
2971
|
+
return fs.readFileSync(p, "utf8");
|
|
2972
|
+
} catch {
|
|
2973
|
+
return null;
|
|
2974
|
+
}
|
|
2975
|
+
},
|
|
2976
|
+
listDir: (d) => {
|
|
2977
|
+
try {
|
|
2978
|
+
return fs.readdirSync(d);
|
|
2979
|
+
} catch {
|
|
2980
|
+
return [];
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
};
|
|
2984
|
+
function invokedDirectly(importMetaUrl, selfBasename, argv1 = process.argv[1]) {
|
|
2985
|
+
try {
|
|
2986
|
+
if (!argv1) return false;
|
|
2987
|
+
const self = fileURLToPath2(importMetaUrl);
|
|
2988
|
+
if (path2.basename(self) !== selfBasename) return false;
|
|
2989
|
+
return pathToFileURL(path2.resolve(argv1)).href === pathToFileURL(self).href;
|
|
2990
|
+
} catch {
|
|
2991
|
+
return false;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
var SELF = fileURLToPath2(import.meta.url);
|
|
2995
|
+
if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
|
|
2996
|
+
const platformRoot = path2.resolve(path2.dirname(SELF), "..", "..");
|
|
2997
|
+
const target = path2.join(platformRoot, "plugins", "heyspex", "hooks", "hooks.json");
|
|
2998
|
+
const want = renderPluginHooksText();
|
|
2999
|
+
const same = (fsDeps.readFile(target) ?? "").replace(/\r\n/g, "\n") === want;
|
|
3000
|
+
if (process.argv.includes("--write-plugin-hooks")) {
|
|
3001
|
+
if (!same) fs.writeFileSync(target, want, "utf8");
|
|
3002
|
+
console.log(`customer-kit: ${same ? "unchanged" : "wrote"} ${path2.relative(platformRoot, target)}`);
|
|
3003
|
+
} else if (process.argv.includes("--check-plugin-hooks")) {
|
|
3004
|
+
if (!same) {
|
|
3005
|
+
console.error("customer-kit: plugins/heyspex/hooks/hooks.json is stale \u2014 run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`");
|
|
3006
|
+
process.exit(1);
|
|
3007
|
+
}
|
|
3008
|
+
console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
|
|
3009
|
+
} else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
|
|
3010
|
+
const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
|
|
3011
|
+
const ghWant = renderGithubHooksText();
|
|
3012
|
+
const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
|
|
3013
|
+
if (process.argv.includes("--write-github-hooks")) {
|
|
3014
|
+
if (!ghSame) {
|
|
3015
|
+
fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
|
|
3016
|
+
fs.writeFileSync(ghTarget, ghWant, "utf8");
|
|
3017
|
+
}
|
|
3018
|
+
console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
|
|
3019
|
+
} else if (!ghSame) {
|
|
3020
|
+
console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
|
|
3021
|
+
process.exit(1);
|
|
3022
|
+
} else {
|
|
3023
|
+
console.log("customer-kit: .github/hooks/heyspex.json up to date");
|
|
3024
|
+
}
|
|
3025
|
+
} else {
|
|
3026
|
+
console.log("usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks");
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// scripts/install-cursor-hooks.mjs
|
|
3031
|
+
var PLATFORM_ROOT = path3.resolve(path3.dirname(fileURLToPath3(import.meta.url)), "..");
|
|
3032
|
+
var REPO_ROOT = process.env.HS_SETUP_TARGET?.trim() ? path3.resolve(process.env.HS_SETUP_TARGET.trim()) : PLATFORM_ROOT;
|
|
3033
|
+
var SETTINGS_PATH = path3.join(REPO_ROOT, ".cursor", "hooks.json");
|
|
3034
|
+
function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
|
|
3035
|
+
const customer = !samePlatformPath(targetRoot, platformRoot);
|
|
3036
|
+
return {
|
|
3037
|
+
customer,
|
|
3038
|
+
hooks: customer ? HOOKS.filter((h) => hookScope(h.hook) === "customer") : HOOKS,
|
|
3039
|
+
commandFor: customer ? commandForCustomer : commandFor,
|
|
3040
|
+
launcher: customer ? { rel: CUSTOMER_LAUNCHERS.cursor.rel, plan: planCustomerLauncherWrite } : null
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
var KIT = cursorKitFor(REPO_ROOT);
|
|
3044
|
+
var ACTIVE_HOOKS = KIT.hooks;
|
|
3045
|
+
var LAUNCHER_PATH = path3.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
|
|
3046
|
+
var LEGACY_MARKER = "scripts/claude-hooks/";
|
|
3047
|
+
function readSettings() {
|
|
3048
|
+
try {
|
|
3049
|
+
const raw = fs2.readFileSync(SETTINGS_PATH, "utf8").trim();
|
|
3050
|
+
if (!raw) return { version: 1, hooks: {} };
|
|
3051
|
+
const parsed = JSON.parse(raw);
|
|
3052
|
+
if (parsed && typeof parsed === "object") {
|
|
3053
|
+
parsed.version ??= 1;
|
|
3054
|
+
parsed.hooks ??= {};
|
|
3055
|
+
return parsed;
|
|
3056
|
+
}
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
if (err && err.code === "ENOENT") return { version: 1, hooks: {} };
|
|
3059
|
+
throw err;
|
|
3060
|
+
}
|
|
3061
|
+
return { version: 1, hooks: {} };
|
|
3062
|
+
}
|
|
3063
|
+
function writeSettings(settings) {
|
|
3064
|
+
fs2.mkdirSync(path3.dirname(SETTINGS_PATH), { recursive: true });
|
|
3065
|
+
fs2.writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}
|
|
3066
|
+
`, "utf8");
|
|
3067
|
+
}
|
|
3068
|
+
var commandOf = (entry) => entry && typeof entry.command === "string" ? entry.command : "";
|
|
3069
|
+
var isOurRunner = (token) => token.includes(SHIM) || token.includes(CUSTOMER_LAUNCHER);
|
|
3070
|
+
var isShimEntry = (raw) => isOurRunner(commandOf(raw));
|
|
3071
|
+
var isLegacyEntry = (raw) => !isShimEntry(raw) && commandOf(raw).includes(LEGACY_MARKER);
|
|
3072
|
+
function identityOfCommand(command) {
|
|
3073
|
+
const tokens = command.trim().split(/\s+/);
|
|
3074
|
+
const shimAt = tokens.findIndex(isOurRunner);
|
|
3075
|
+
if (shimAt === -1) return null;
|
|
3076
|
+
const rest = tokens.slice(shimAt + 1);
|
|
3077
|
+
const hook = rest[0] ?? "";
|
|
3078
|
+
let tool = "";
|
|
3079
|
+
const args = [];
|
|
3080
|
+
for (const token of rest.slice(1)) {
|
|
3081
|
+
if (token.startsWith("--hs-tool=")) {
|
|
3082
|
+
tool = token.slice("--hs-tool=".length).replace(/^"([\s\S]*)"$/, "$1");
|
|
3083
|
+
} else if (token.startsWith("--hs-claude-event=")) continue;
|
|
3084
|
+
else args.push(token);
|
|
3085
|
+
}
|
|
3086
|
+
return `${hook}::${tool}::${args.join(" ")}`;
|
|
3087
|
+
}
|
|
3088
|
+
function desiredEntry(entry, render = KIT.commandFor) {
|
|
3089
|
+
return {
|
|
3090
|
+
command: render(entry),
|
|
3091
|
+
timeout: timeoutFor(entry),
|
|
3092
|
+
...entry.matcher ? { matcher: entry.matcher } : {}
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
function sameEntry(raw, entry, render = KIT.commandFor) {
|
|
3096
|
+
const want = desiredEntry(entry, render);
|
|
3097
|
+
return commandOf(raw) === want.command && raw.timeout === want.timeout && (raw.matcher ?? void 0) === (want.matcher ?? void 0);
|
|
3098
|
+
}
|
|
3099
|
+
var PLUGIN_HOOKS_PATH = path3.join(REPO_ROOT, "plugins", "heyspex", "hooks", "cursor-hooks.json");
|
|
3100
|
+
function pluginHooksDocument() {
|
|
3101
|
+
const hooks = {};
|
|
3102
|
+
for (const entry of HOOKS) {
|
|
3103
|
+
hooks[entry.event] ??= [];
|
|
3104
|
+
hooks[entry.event].push({
|
|
3105
|
+
command: commandForPlugin(entry),
|
|
3106
|
+
timeout: timeoutFor(entry),
|
|
3107
|
+
...entry.matcher ? { matcher: entry.matcher } : {}
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
return {
|
|
3111
|
+
_comment: "GENERATED by scripts/install-cursor-hooks.mjs from scripts/cursor-hooks/manifest.mjs \u2014 do not edit by hand; run `npm run hooks:install-cursor`. Commands resolve through $CURSOR_PROJECT_DIR because the shim and hook bodies live in the host vault checkout, not the plugin cache. WARNING: enabling this plugin in a repo whose .cursor/hooks.json still registers the same hooks runs every hook twice \u2014 validate in a fresh clone, or run `node scripts/install-cursor-hooks.mjs --uninstall` there first.",
|
|
3112
|
+
version: 1,
|
|
3113
|
+
hooks
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function pluginHooksText() {
|
|
3117
|
+
return `${JSON.stringify(pluginHooksDocument(), null, 2)}
|
|
3118
|
+
`;
|
|
3119
|
+
}
|
|
3120
|
+
function syncPluginHooks() {
|
|
3121
|
+
const want = pluginHooksText();
|
|
3122
|
+
let have = null;
|
|
3123
|
+
try {
|
|
3124
|
+
have = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
|
|
3125
|
+
} catch {
|
|
3126
|
+
have = null;
|
|
3127
|
+
}
|
|
3128
|
+
if (have === want) return false;
|
|
3129
|
+
fs2.mkdirSync(path3.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
|
|
3130
|
+
fs2.writeFileSync(PLUGIN_HOOKS_PATH, want, "utf8");
|
|
3131
|
+
return true;
|
|
3132
|
+
}
|
|
3133
|
+
function mergeCursorHooks(settings, { hooks = ACTIVE_HOOKS, commandFor: render = KIT.commandFor } = {}) {
|
|
3134
|
+
settings.version = 1;
|
|
3135
|
+
settings.hooks ??= {};
|
|
3136
|
+
const added = [];
|
|
3137
|
+
const repaired = [];
|
|
3138
|
+
const removed = [];
|
|
3139
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
3140
|
+
if (!Array.isArray(entries)) continue;
|
|
3141
|
+
const keep = entries.filter((raw) => {
|
|
3142
|
+
if (!isLegacyEntry(raw)) return true;
|
|
3143
|
+
removed.push(`${event}: ${commandOf(raw)}`);
|
|
3144
|
+
return false;
|
|
3145
|
+
});
|
|
3146
|
+
settings.hooks[event] = keep;
|
|
3147
|
+
}
|
|
3148
|
+
for (const entry of hooks) {
|
|
3149
|
+
settings.hooks[entry.event] ??= [];
|
|
3150
|
+
const entries = settings.hooks[entry.event];
|
|
3151
|
+
const identity = identityFor(entry);
|
|
3152
|
+
const existing = entries.filter(
|
|
3153
|
+
(raw2) => isShimEntry(raw2) && identityOfCommand(commandOf(raw2)) === identity
|
|
3154
|
+
);
|
|
3155
|
+
if (existing.length === 0) {
|
|
3156
|
+
entries.push(desiredEntry(entry, render));
|
|
3157
|
+
added.push(`${entry.event}/${entry.hook}`);
|
|
3158
|
+
continue;
|
|
3159
|
+
}
|
|
3160
|
+
for (const extra of existing.slice(1)) {
|
|
3161
|
+
entries.splice(entries.indexOf(extra), 1);
|
|
3162
|
+
removed.push(`${entry.event}: duplicate ${entry.hook}`);
|
|
3163
|
+
}
|
|
3164
|
+
const raw = existing[0];
|
|
3165
|
+
if (!sameEntry(raw, entry, render)) {
|
|
3166
|
+
Object.assign(raw, desiredEntry(entry, render));
|
|
3167
|
+
if (!entry.matcher && "matcher" in raw) delete raw.matcher;
|
|
3168
|
+
repaired.push(`${entry.event}/${entry.hook}`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
3172
|
+
if (!Array.isArray(entries)) continue;
|
|
3173
|
+
const wanted = new Set(hooks.filter((h) => h.event === event).map(identityFor));
|
|
3174
|
+
settings.hooks[event] = entries.filter((raw) => {
|
|
3175
|
+
if (!isShimEntry(raw)) return true;
|
|
3176
|
+
if (wanted.has(identityOfCommand(commandOf(raw)))) return true;
|
|
3177
|
+
removed.push(`${event}: retired ${commandOf(raw)}`);
|
|
3178
|
+
return false;
|
|
3179
|
+
});
|
|
3180
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
3181
|
+
}
|
|
3182
|
+
return { added, repaired, removed };
|
|
3183
|
+
}
|
|
3184
|
+
function launcherPlan() {
|
|
3185
|
+
let existing = null;
|
|
3186
|
+
try {
|
|
3187
|
+
existing = fs2.readFileSync(LAUNCHER_PATH, "utf8");
|
|
3188
|
+
} catch {
|
|
3189
|
+
existing = null;
|
|
3190
|
+
}
|
|
3191
|
+
return planCustomerLauncherWrite(existing, "cursor");
|
|
3192
|
+
}
|
|
3193
|
+
function install({ json = false } = {}) {
|
|
3194
|
+
const settings = readSettings();
|
|
3195
|
+
const { added, repaired, removed } = mergeCursorHooks(settings);
|
|
3196
|
+
const settingsChanged = added.length + repaired.length + removed.length > 0;
|
|
3197
|
+
if (settingsChanged) writeSettings(settings);
|
|
3198
|
+
let launcherWritten = false;
|
|
3199
|
+
if (KIT.customer) {
|
|
3200
|
+
const plan = launcherPlan();
|
|
3201
|
+
if (plan.write) {
|
|
3202
|
+
fs2.mkdirSync(path3.dirname(LAUNCHER_PATH), { recursive: true });
|
|
3203
|
+
fs2.writeFileSync(LAUNCHER_PATH, plan.text, "utf8");
|
|
3204
|
+
launcherWritten = true;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
const pluginChanged = KIT.customer ? false : syncPluginHooks();
|
|
3208
|
+
if (json) {
|
|
3209
|
+
console.log(
|
|
3210
|
+
JSON.stringify(
|
|
3211
|
+
{
|
|
3212
|
+
ok: true,
|
|
3213
|
+
changed: settingsChanged || pluginChanged || launcherWritten,
|
|
3214
|
+
added,
|
|
3215
|
+
repaired,
|
|
3216
|
+
removed,
|
|
3217
|
+
pluginChanged,
|
|
3218
|
+
launcherWritten,
|
|
3219
|
+
total: ACTIVE_HOOKS.length,
|
|
3220
|
+
customer: KIT.customer
|
|
3221
|
+
},
|
|
3222
|
+
null,
|
|
3223
|
+
2
|
|
3224
|
+
)
|
|
3225
|
+
);
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
if (pluginChanged) {
|
|
3229
|
+
console.log(`install-cursor-hooks: regenerated ${path3.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
|
|
3230
|
+
}
|
|
3231
|
+
if (launcherWritten) {
|
|
3232
|
+
console.log(`install-cursor-hooks: wrote ${path3.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
|
|
3233
|
+
}
|
|
3234
|
+
if (!settingsChanged) {
|
|
3235
|
+
console.log(`install-cursor-hooks: already installed (${ACTIVE_HOOKS.length} hooks, no changes).`);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
const parts = [];
|
|
3239
|
+
if (added.length) parts.push(`added ${added.length}`);
|
|
3240
|
+
if (repaired.length) parts.push(`repaired ${repaired.length}`);
|
|
3241
|
+
if (removed.length) parts.push(`removed ${removed.length}`);
|
|
3242
|
+
console.log(
|
|
3243
|
+
`install-cursor-hooks: ${parts.join("; ")} \u2192 ${path3.relative(REPO_ROOT, SETTINGS_PATH)} (${ACTIVE_HOOKS.length} hooks registered)`
|
|
3244
|
+
);
|
|
3245
|
+
for (const line of removed) console.log(` - ${line}`);
|
|
3246
|
+
console.log(
|
|
3247
|
+
KIT.customer ? ` Launcher: ${CUSTOMER_LAUNCHER} \u2192 node_modules/heyspex/${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)` : ` Shim: ${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)`
|
|
3248
|
+
);
|
|
3249
|
+
for (const gap of UNPORTABLE) {
|
|
3250
|
+
console.log(` Not registerable in Cursor: ${gap.hook} (${gap.reason}) \u2192 covered by ${gap.coveredBy}`);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
function check({ json = false } = {}) {
|
|
3254
|
+
const settings = readSettings();
|
|
3255
|
+
const issues = [];
|
|
3256
|
+
const entriesFor = (event) => Array.isArray(settings?.hooks?.[event]) ? settings.hooks[event] : [];
|
|
3257
|
+
for (const entry of ACTIVE_HOOKS) {
|
|
3258
|
+
const identity = identityFor(entry);
|
|
3259
|
+
const ours = entriesFor(entry.event).filter(
|
|
3260
|
+
(raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity
|
|
3261
|
+
);
|
|
3262
|
+
if (ours.length === 0) {
|
|
3263
|
+
issues.push(`${entry.event}: missing ${entry.hook}`);
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
if (ours.length > 1) issues.push(`${entry.event}: ${entry.hook} registered ${ours.length}\xD7`);
|
|
3267
|
+
for (const raw of ours) {
|
|
3268
|
+
if (!sameEntry(raw, entry)) {
|
|
3269
|
+
issues.push(
|
|
3270
|
+
`${entry.event}/${entry.hook}: drift \u2014 expected \`${KIT.commandFor(entry)}\` (timeout ${timeoutFor(entry)}, matcher ${entry.matcher ?? "none"}), found \`${commandOf(raw)}\` (timeout ${raw.timeout}, matcher ${raw.matcher ?? "none"})`
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
3276
|
+
if (!Array.isArray(entries)) continue;
|
|
3277
|
+
for (const raw of entries) {
|
|
3278
|
+
if (isLegacyEntry(raw)) {
|
|
3279
|
+
issues.push(`${event}: superseded direct registration \u2014 ${commandOf(raw)}`);
|
|
3280
|
+
} else if (isShimEntry(raw) && !ACTIVE_HOOKS.some((h) => h.event === event && identityFor(h) === identityOfCommand(commandOf(raw)))) {
|
|
3281
|
+
issues.push(`${event}: not part of this repo's hook set \u2014 ${commandOf(raw)}`);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
let pluginOnDisk = null;
|
|
3286
|
+
try {
|
|
3287
|
+
pluginOnDisk = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
|
|
3288
|
+
} catch {
|
|
3289
|
+
pluginOnDisk = null;
|
|
3290
|
+
}
|
|
3291
|
+
if (!KIT.customer && pluginOnDisk !== pluginHooksText()) {
|
|
3292
|
+
issues.push(
|
|
3293
|
+
`plugins/heyspex/hooks/cursor-hooks.json: ${pluginOnDisk === null ? "missing" : "stale"} \u2014 regenerate with \`npm run hooks:install-cursor\``
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
if (KIT.customer) {
|
|
3297
|
+
const plan = launcherPlan();
|
|
3298
|
+
if (plan.write) {
|
|
3299
|
+
issues.push(`${CUSTOMER_LAUNCHER}: ${plan.reason === "missing" ? "missing" : "stale"} \u2014 the hook rows point at it`);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
if (json) {
|
|
3303
|
+
console.log(JSON.stringify({ ok: issues.length === 0, issues, total: ACTIVE_HOOKS.length, customer: KIT.customer }, null, 2));
|
|
3304
|
+
process.exit(issues.length === 0 ? 0 : 1);
|
|
3305
|
+
}
|
|
3306
|
+
if (issues.length === 0) {
|
|
3307
|
+
console.log(`install-cursor-hooks --check: OK (${ACTIVE_HOOKS.length} hooks registered)`);
|
|
3308
|
+
process.exit(0);
|
|
3309
|
+
}
|
|
3310
|
+
console.error("install-cursor-hooks --check: drift detected");
|
|
3311
|
+
for (const issue of issues) console.error(` - ${issue}`);
|
|
3312
|
+
console.error(" Run `node scripts/install-cursor-hooks.mjs` to repair.");
|
|
3313
|
+
process.exit(1);
|
|
3314
|
+
}
|
|
3315
|
+
function uninstall() {
|
|
3316
|
+
const settings = readSettings();
|
|
3317
|
+
if (!settings.hooks) {
|
|
3318
|
+
console.log("install-cursor-hooks: nothing to uninstall (no hooks block).");
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
let removed = 0;
|
|
3322
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
3323
|
+
if (!Array.isArray(entries)) continue;
|
|
3324
|
+
const keep = entries.filter((raw) => !isShimEntry(raw) && !isLegacyEntry(raw));
|
|
3325
|
+
removed += entries.length - keep.length;
|
|
3326
|
+
if (keep.length === 0) delete settings.hooks[event];
|
|
3327
|
+
else settings.hooks[event] = keep;
|
|
3328
|
+
}
|
|
3329
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
3330
|
+
writeSettings(settings);
|
|
3331
|
+
console.log(`install-cursor-hooks: uninstalled ${removed} hook entr${removed === 1 ? "y" : "ies"}.`);
|
|
3332
|
+
}
|
|
3333
|
+
if (invokedDirectly(import.meta.url, "install-cursor-hooks.mjs")) {
|
|
3334
|
+
const argv = process.argv.slice(2);
|
|
3335
|
+
const json = argv.includes("--json");
|
|
3336
|
+
if (argv.includes("--uninstall")) uninstall();
|
|
3337
|
+
else if (argv.includes("--check")) check({ json });
|
|
3338
|
+
else install({ json });
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
// spex-console/src/lib/cli/install.ts
|
|
3342
|
+
init_credentials();
|
|
3343
|
+
|
|
3344
|
+
// spex-console/src/lib/cli/connect-checks.ts
|
|
3345
|
+
import { dirname as dirname5 } from "node:path";
|
|
3346
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
3347
|
+
|
|
3348
|
+
// spex-console/src/lib/dev-mode.ts
|
|
3349
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3350
|
+
import { join as join10 } from "node:path";
|
|
3351
|
+
init_credentials();
|
|
3352
|
+
|
|
3353
|
+
// spex-console/src/lib/cli/connect-checks.ts
|
|
3354
|
+
init_credentials();
|
|
3355
|
+
|
|
3356
|
+
// spex-console/src/lib/cli/satellite.ts
|
|
3357
|
+
import { resolve as resolveNodePath } from "node:path";
|
|
3358
|
+
import process2 from "node:process";
|
|
3359
|
+
|
|
965
3360
|
// spex-console/src/lib/cli/principle-selection.ts
|
|
966
|
-
import { basename as
|
|
3361
|
+
import { basename as basename3 } from "node:path";
|
|
967
3362
|
var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
|
|
968
3363
|
function principleKey(row) {
|
|
969
|
-
const base =
|
|
3364
|
+
const base = basename3(row.dest, ".md");
|
|
970
3365
|
return base.startsWith("principle-") ? base.slice("principle-".length) : base;
|
|
971
3366
|
}
|
|
972
3367
|
function principleWikilink(row) {
|
|
973
|
-
return
|
|
3368
|
+
return basename3(row.dest, ".md");
|
|
974
3369
|
}
|
|
975
3370
|
function parsePrinciplesFlag(value, rows) {
|
|
976
3371
|
const principleRows = rows.filter((row) => row.group === "principle");
|
|
@@ -1015,6 +3410,7 @@ function principleManifestFilter(selected) {
|
|
|
1015
3410
|
}
|
|
1016
3411
|
function principlesIndexHook(selected, rows) {
|
|
1017
3412
|
return (ctx) => {
|
|
3413
|
+
if (ctx.rows.some((r) => r.dest === PRINCIPLES_INDEX_DEST && r.action === "kept")) return;
|
|
1018
3414
|
const indexRow = rows.find((row) => row.dest === PRINCIPLES_INDEX_DEST);
|
|
1019
3415
|
if (!indexRow || indexRow.content === void 0) {
|
|
1020
3416
|
throw new Error(
|
|
@@ -1031,10 +3427,292 @@ function principlesIndexHook(selected, rows) {
|
|
|
1031
3427
|
};
|
|
1032
3428
|
}
|
|
1033
3429
|
|
|
3430
|
+
// spex-console/src/lib/cli/receipt.ts
|
|
3431
|
+
var RECEIPT_AGREEMENT_FIELDS = [
|
|
3432
|
+
"vault_id",
|
|
3433
|
+
"workspace_id",
|
|
3434
|
+
"account_id",
|
|
3435
|
+
"active_vault_id"
|
|
3436
|
+
];
|
|
3437
|
+
var ALL_FIELDS = [
|
|
3438
|
+
...RECEIPT_AGREEMENT_FIELDS,
|
|
3439
|
+
"display_name",
|
|
3440
|
+
"archived"
|
|
3441
|
+
];
|
|
3442
|
+
|
|
3443
|
+
// spex-console/src/lib/cli/vault-workspace-shared.ts
|
|
3444
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
3445
|
+
|
|
3446
|
+
// spex-console/src/lib/indexer/parser.ts
|
|
3447
|
+
var import_gray_matter = __toESM(require_gray_matter(), 1);
|
|
3448
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3449
|
+
import { promises as fs3 } from "node:fs";
|
|
3450
|
+
import path4 from "node:path";
|
|
3451
|
+
|
|
3452
|
+
// spex-console/src/lib/cli/install.ts
|
|
3453
|
+
var MCP_REL = ["vendor", "heyspex-mcp", "cli.mjs"];
|
|
3454
|
+
var PORTABLE_MCP_CLI = Object.freeze({
|
|
3455
|
+
claude: `\${CLAUDE_PROJECT_DIR:-.}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`,
|
|
3456
|
+
cursor: `\${workspaceFolder}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`
|
|
3457
|
+
});
|
|
3458
|
+
function resolveCallerPath(p, env, fallbackCwd) {
|
|
3459
|
+
const trimmed = p?.trim() ?? "";
|
|
3460
|
+
if (!trimmed) return "";
|
|
3461
|
+
if (isAbsolute3(trimmed)) return trimmed;
|
|
3462
|
+
const base = env.HS_INVOKED_CWD?.trim() || fallbackCwd;
|
|
3463
|
+
return resolveNodePath2(base, trimmed);
|
|
3464
|
+
}
|
|
3465
|
+
var DEFAULT_REMOTE_MCP_URL = "https://app.heyspex.com/api/mcp";
|
|
3466
|
+
var REMOTE_MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
3467
|
+
var PLUGIN_MARKETPLACE_NAME = "heyspex-marketplace";
|
|
3468
|
+
var PLUGIN_INSTALL_SPEC = `heyspex@${PLUGIN_MARKETPLACE_NAME}`;
|
|
3469
|
+
var WRITE_TARGET_KIND = {
|
|
3470
|
+
specsGitkeep: "specs_gitkeep",
|
|
3471
|
+
heyspexYaml: "heyspex_yaml",
|
|
3472
|
+
cursorMcp: "cursor_mcp",
|
|
3473
|
+
claudeMcp: "claude_mcp",
|
|
3474
|
+
claudeMd: "claude_md",
|
|
3475
|
+
agentsMd: "agents_md",
|
|
3476
|
+
statuslineVendor: "statusline_vendor",
|
|
3477
|
+
gitignore: "gitignore",
|
|
3478
|
+
claudeMdTemplate: "claude_md_template",
|
|
3479
|
+
agentsMdTemplate: "agents_md_template"
|
|
3480
|
+
};
|
|
3481
|
+
var INSTALL_WRITE_TARGETS = [
|
|
3482
|
+
{
|
|
3483
|
+
kind: WRITE_TARGET_KIND.specsGitkeep,
|
|
3484
|
+
rel: "specs/.gitkeep",
|
|
3485
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "specs", ".gitkeep"),
|
|
3486
|
+
appliesTo: (_args, opts) => !opts.attached
|
|
3487
|
+
},
|
|
3488
|
+
{
|
|
3489
|
+
kind: WRITE_TARGET_KIND.heyspexYaml,
|
|
3490
|
+
rel: "heyspex.yaml",
|
|
3491
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "heyspex.yaml"),
|
|
3492
|
+
appliesTo: (_args, opts) => !opts.attached
|
|
3493
|
+
},
|
|
3494
|
+
{
|
|
3495
|
+
kind: WRITE_TARGET_KIND.cursorMcp,
|
|
3496
|
+
rel: ".cursor/mcp.json",
|
|
3497
|
+
path: (targetDir, joinPath) => joinPath(targetDir, ".cursor", "mcp.json"),
|
|
3498
|
+
appliesTo: (args) => args.cursor
|
|
3499
|
+
},
|
|
3500
|
+
{
|
|
3501
|
+
kind: WRITE_TARGET_KIND.claudeMcp,
|
|
3502
|
+
rel: ".mcp.json",
|
|
3503
|
+
path: (targetDir, joinPath) => joinPath(targetDir, ".mcp.json"),
|
|
3504
|
+
appliesTo: (args) => args.claude
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
kind: WRITE_TARGET_KIND.claudeMd,
|
|
3508
|
+
rel: "CLAUDE.md",
|
|
3509
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md"),
|
|
3510
|
+
appliesTo: (args, opts) => args.claude && opts.installContract
|
|
3511
|
+
},
|
|
3512
|
+
{
|
|
3513
|
+
kind: WRITE_TARGET_KIND.agentsMd,
|
|
3514
|
+
rel: "AGENTS.md",
|
|
3515
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md"),
|
|
3516
|
+
appliesTo: (_args, opts) => opts.installContract
|
|
3517
|
+
},
|
|
3518
|
+
// The customer-template step (--register-vault) writes the project contract
|
|
3519
|
+
// beside a hand-authored CLAUDE.md/AGENTS.md as <file>.template; a rerun
|
|
3520
|
+
// rewrites it when the generated text changed.
|
|
3521
|
+
{
|
|
3522
|
+
kind: WRITE_TARGET_KIND.claudeMdTemplate,
|
|
3523
|
+
rel: "CLAUDE.md.template",
|
|
3524
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md.template"),
|
|
3525
|
+
appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && args.claude && opts.installContract
|
|
3526
|
+
},
|
|
3527
|
+
{
|
|
3528
|
+
kind: WRITE_TARGET_KIND.agentsMdTemplate,
|
|
3529
|
+
rel: "AGENTS.md.template",
|
|
3530
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md.template"),
|
|
3531
|
+
appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && opts.installContract
|
|
3532
|
+
},
|
|
3533
|
+
{
|
|
3534
|
+
kind: WRITE_TARGET_KIND.statuslineVendor,
|
|
3535
|
+
rel: "scripts/claude-statusline",
|
|
3536
|
+
path: (targetDir, joinPath) => joinPath(targetDir, "scripts", "claude-statusline"),
|
|
3537
|
+
appliesTo: (args) => (args.statusline ?? true) && args.claude
|
|
3538
|
+
},
|
|
3539
|
+
{
|
|
3540
|
+
kind: WRITE_TARGET_KIND.gitignore,
|
|
3541
|
+
rel: ".gitignore",
|
|
3542
|
+
path: (targetDir, joinPath) => joinPath(targetDir, ".gitignore"),
|
|
3543
|
+
// A satellite owns no vault content and writes no machine-local
|
|
3544
|
+
// manifests of its own (its .mcp.json/.cursor/mcp.json point AT the
|
|
3545
|
+
// attached vault, not a fresh one this repo would need to gitignore) --
|
|
3546
|
+
// only a real vault bootstrap gets one.
|
|
3547
|
+
appliesTo: (_args, opts) => !opts.attached
|
|
3548
|
+
}
|
|
3549
|
+
];
|
|
3550
|
+
var INSTALL_HELP = [
|
|
3551
|
+
"Usage:",
|
|
3552
|
+
" hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
|
|
3553
|
+
" [--no-cursor] [--no-claude] [--claude-apply]",
|
|
3554
|
+
" [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
|
|
3555
|
+
" [--copilot-hooks | --copilot-plugin | --copilot-plugin-apply]",
|
|
3556
|
+
" [--no-statusline]",
|
|
3557
|
+
" [--cc | --command-code] [--register-vault [--workspace <id>]]",
|
|
3558
|
+
" [--remote[=<url>]]",
|
|
3559
|
+
" hs install --attach-vault <vault-id|vault-path> [--target <dir>] [...]",
|
|
3560
|
+
"",
|
|
3561
|
+
"Bootstrap a project vault for HeySpex agent tooling:",
|
|
3562
|
+
" \u2022 specs/ + heyspex.yaml (via hs init)",
|
|
3563
|
+
" \u2022 .cursor/mcp.json \u2014 Cursor MCP (gitignored, machine-local path to platform MCP)",
|
|
3564
|
+
" \u2022 .mcp.json \u2014 Claude Code project MCP (absolute platform path)",
|
|
3565
|
+
" \u2022 CLAUDE.md / AGENTS.md \u2014 thin agent contract pointing at the HeySpex MCP server",
|
|
3566
|
+
" \u2022 agent kit (customer repos) \u2014 the customer-scoped HeySpex hooks in",
|
|
3567
|
+
" .claude/settings.json + .cursor/hooks.json and the /hs-* commands in",
|
|
3568
|
+
" .claude/commands + .cursor/commands, every platform path absolute.",
|
|
3569
|
+
" Scope table: scripts/lib/customer-kit.mjs. Re-runs are no-ops; a command",
|
|
3570
|
+
" file you edited is left alone (--force overwrites it). `hs doctor` flags",
|
|
3571
|
+
" a missing or stale kit.",
|
|
3572
|
+
"",
|
|
3573
|
+
"Satellite repo (--attach-vault): wire a CODE repo to an EXISTING vault instead",
|
|
3574
|
+
"of making it a vault. Writes the MCP manifests + agent contract only, with",
|
|
3575
|
+
"HEYSPEX_VAULT_ROOT pinned to the attached vault's absolute root -- no specs/,",
|
|
3576
|
+
"no heyspex.yaml, no new registry row. The target is the repo you are standing",
|
|
3577
|
+
"in (or --target), never the vault being attached.",
|
|
3578
|
+
"",
|
|
3579
|
+
"That pin IS the attachment, so it must land somewhere: --attach-vault refuses a",
|
|
3580
|
+
"flag combination that writes no stdio manifest (--no-cursor --no-claude), and",
|
|
3581
|
+
"refuses to re-pin over manifests that name a different vault unless you pass",
|
|
3582
|
+
"--force. Re-attaching to the SAME vault is a no-op and says so.",
|
|
3583
|
+
"",
|
|
3584
|
+
'--attach-vault --remote: the stdio pin under "heyspex" in .mcp.json is preserved',
|
|
3585
|
+
"(never replaced by the HTTP form, even with --force) and the cloud form is",
|
|
3586
|
+
'written under a second key, "heyspex-cloud" -- Claude Code registers both the',
|
|
3587
|
+
"attached-vault local server and the cloud server. Without --force, a pin that",
|
|
3588
|
+
"already names a DIFFERENT vault still refuses and names the conflicting key,",
|
|
3589
|
+
"same as the non-remote case.",
|
|
3590
|
+
"",
|
|
3591
|
+
"Verify with `hs connect --check` from the satellite -- it is the command that",
|
|
3592
|
+
"audits THIS repo's rows and reports the attachment. `hs doctor` resolves",
|
|
3593
|
+
"THROUGH the pin and audits the ATTACHED VAULT repo, so it reports that vault's",
|
|
3594
|
+
"connection state, not the satellite's.",
|
|
3595
|
+
"",
|
|
3596
|
+
"Conflict scan: every install checks the target repo against the known-conflict",
|
|
3597
|
+
"registry (beads, workflow-pm, bd hooks, \u2026). See:",
|
|
3598
|
+
" specs/protocols/customer-vault-local-install-protocol.md",
|
|
3599
|
+
"",
|
|
3600
|
+
"Run from the HeySpex platform repo, or pass --platform to its checkout.",
|
|
3601
|
+
"Target defaults to the current directory.",
|
|
3602
|
+
"",
|
|
3603
|
+
"Flags:",
|
|
3604
|
+
" --target <dir> Project repo to bootstrap (default: cwd).",
|
|
3605
|
+
" --platform <dir> HeySpex platform checkout (default: HS_INSTALL_PLATFORM or dispatcher root).",
|
|
3606
|
+
" --claude-apply Run `claude mcp add --scope project` after writing files.",
|
|
3607
|
+
" --plugin Print the Claude Code plugin registration block (self-hosted",
|
|
3608
|
+
" marketplace at the platform root \u2014 HS-QTBK prototype).",
|
|
3609
|
+
" --plugin-apply Run `claude plugin marketplace add` + `claude plugin install`",
|
|
3610
|
+
" after writing files. Marketplace + plugin names are read from",
|
|
3611
|
+
` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
|
|
3612
|
+
" --plugin-scope <s> Plugin install scope: user, project (default), or local.",
|
|
3613
|
+
" Implies --plugin.",
|
|
3614
|
+
" --copilot-hooks Write .github/hooks/heyspex.json (GitHub Copilot hooks) into the",
|
|
3615
|
+
" target. Commands resolve node_modules/heyspex first, so an npm-only",
|
|
3616
|
+
" install (no heyspex repo checkout) works. Pick this OR the plugin.",
|
|
3617
|
+
" --copilot-plugin Print `copilot plugin marketplace add '<platform>'` +",
|
|
3618
|
+
" `copilot plugin install <plugin>@<marketplace>` (the platform root is",
|
|
3619
|
+
" node_modules/heyspex for a published install).",
|
|
3620
|
+
" --copilot-plugin-apply Run those two commands when `copilot` is on PATH.",
|
|
3621
|
+
" Requesting the Copilot plugin AND --copilot-hooks warns and skips the",
|
|
3622
|
+
" hooks file (both would fire every hook twice).",
|
|
3623
|
+
" --remote[=<url>] Write .mcp.json for the REMOTE (cloud) MCP endpoint instead of",
|
|
3624
|
+
` the local stdio server \u2014 the Streamable-HTTP form against`,
|
|
3625
|
+
` ${DEFAULT_REMOTE_MCP_URL} (or <url>), authenticated by the`,
|
|
3626
|
+
` env reference \`\${${REMOTE_MCP_TOKEN_ENV}}\` (never the literal token).`,
|
|
3627
|
+
' With --attach-vault, the stdio pin survives under "heyspex" and',
|
|
3628
|
+
' this is written under "heyspex-cloud" instead (see above). If',
|
|
3629
|
+
" this machine already ran `hs login` / `hs mcp pair <code>`, the",
|
|
3630
|
+
" printed guidance resolves the bearer from",
|
|
3631
|
+
" ~/.heyspex/credentials.yaml automatically -- no manual export",
|
|
3632
|
+
" needed on that machine.",
|
|
3633
|
+
" --no-statusline Skip the branded statusline (statusLine entry in the target's",
|
|
3634
|
+
" .claude/settings.json + machine-level glyph install).",
|
|
3635
|
+
" --cc, --command-code Print `cmdc mcp add` PowerShell one-liner for Command Code.",
|
|
3636
|
+
" --attach-vault <v> SATELLITE mode: attach this repo to the EXISTING vault named",
|
|
3637
|
+
" by a registry id (`hs vault list`) or the vault repo's path,",
|
|
3638
|
+
" instead of scaffolding specs/ + heyspex.yaml here. The vault",
|
|
3639
|
+
" must already be registered on this machine. Mutually exclusive",
|
|
3640
|
+
" with --register-vault and --workspace, and with any flag",
|
|
3641
|
+
" combination that would leave no stdio manifest to pin.",
|
|
3642
|
+
" Combines with --remote (see --remote above): the pin and the",
|
|
3643
|
+
" cloud endpoint coexist in .mcp.json under separate keys.",
|
|
3644
|
+
" --register-vault Register this repo's vault into ~/.heyspex/registry.json during install,",
|
|
3645
|
+
" and -- when this machine is signed in -- on the console too; both",
|
|
3646
|
+
" sides are read back (GET /api/vaults) and printed as a receipt.",
|
|
3647
|
+
" Re-running changes nothing and sends no cloud write.",
|
|
3648
|
+
" --workspace <id> With --register-vault: ensure this workspace exists, make it active,",
|
|
3649
|
+
" and associate the vault with it (default: the vault's current",
|
|
3650
|
+
" workspace, else your per-account default ws-<account>).",
|
|
3651
|
+
" --no-cursor Skip .cursor/mcp.json.",
|
|
3652
|
+
" --no-claude Skip .mcp.json and Claude instructions.",
|
|
3653
|
+
" --force Overwrite existing MCP manifests and heyspex.yaml.",
|
|
3654
|
+
" --dry-run Print the plan without writing.",
|
|
3655
|
+
" --skip-conflict-check Skip the known-conflict registry scan.",
|
|
3656
|
+
" --json Machine-readable result.",
|
|
3657
|
+
" --account <email|id> Confirm the account this repo is attached to. An interactive",
|
|
3658
|
+
" install asks (continue / sign in as someone else / create an",
|
|
3659
|
+
" account / abort); a non-interactive one with --register-vault or",
|
|
3660
|
+
" --remote refuses without this or --yes. See `hs whoami`.",
|
|
3661
|
+
" --yes Confirm the signed-in account shown, without typing it.",
|
|
3662
|
+
" --help, -h Show this help.",
|
|
3663
|
+
"",
|
|
3664
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed \xB7 16 sign-in required \xB7",
|
|
3665
|
+
"12 console unreachable (account check) \xB7 17 account not confirmed \xB7",
|
|
3666
|
+
"19 the cloud half did not land (the receipt names the retry) \xB7 21 both sides read back and disagree"
|
|
3667
|
+
].join("\n");
|
|
3668
|
+
|
|
3669
|
+
// spex-console/bin/run-entry.ts
|
|
3670
|
+
import process3 from "node:process";
|
|
3671
|
+
var BinExit = class extends Error {
|
|
3672
|
+
code;
|
|
3673
|
+
constructor(code) {
|
|
3674
|
+
super(`exit ${code}`);
|
|
3675
|
+
this.name = "BinExit";
|
|
3676
|
+
this.code = code;
|
|
3677
|
+
}
|
|
3678
|
+
};
|
|
3679
|
+
function runEntry(name, main2, options2 = {}) {
|
|
3680
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options2;
|
|
3681
|
+
void (async () => {
|
|
3682
|
+
try {
|
|
3683
|
+
await main2();
|
|
3684
|
+
} catch (err) {
|
|
3685
|
+
if (err instanceof BinExit) {
|
|
3686
|
+
process3.exitCode = err.code;
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3690
|
+
process3.stderr.write(`${name}: ${unhandledLabel}: ${message}
|
|
3691
|
+
`);
|
|
3692
|
+
process3.exitCode = fallbackCode;
|
|
3693
|
+
}
|
|
3694
|
+
})();
|
|
3695
|
+
}
|
|
3696
|
+
|
|
1034
3697
|
// spex-console/bin/hs-init.ts
|
|
1035
|
-
var INTERACTIVE =
|
|
3698
|
+
var INTERACTIVE = process4.stdin.isTTY === true;
|
|
3699
|
+
var PKG_ROOT = resolve5(
|
|
3700
|
+
resolveCodeRoot({ packageRoot: resolve5(dirname6(fileURLToPath4(import.meta.url)), "..", "..") })
|
|
3701
|
+
);
|
|
3702
|
+
var CONFIG_SEED_PATH = join11(PKG_ROOT, "vendor", "heyspex-mcp", "config-seed.yaml");
|
|
3703
|
+
function readConfigSeed() {
|
|
3704
|
+
try {
|
|
3705
|
+
return readFileSync11(CONFIG_SEED_PATH, "utf8");
|
|
3706
|
+
} catch {
|
|
3707
|
+
process4.stderr.write(
|
|
3708
|
+
`hs init: warning \u2014 config seed template not found at ${CONFIG_SEED_PATH}; .heyspex/config.yaml was not seeded (run \`hs settings set <key> <value>\` to create it).
|
|
3709
|
+
`
|
|
3710
|
+
);
|
|
3711
|
+
return null;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
1036
3714
|
async function askYesNo(question) {
|
|
1037
|
-
const rl = createInterface({ input:
|
|
3715
|
+
const rl = createInterface({ input: process4.stdin, output: process4.stdout });
|
|
1038
3716
|
try {
|
|
1039
3717
|
const answer = (await rl.question(`${question} `)).trim().toLowerCase();
|
|
1040
3718
|
return answer === "y" || answer === "yes" || answer === "";
|
|
@@ -1047,9 +3725,9 @@ async function resolvePrincipleSelection(principlesFlag) {
|
|
|
1047
3725
|
return parsePrinciplesFlag(principlesFlag, TEMPLATE_MANIFEST);
|
|
1048
3726
|
}
|
|
1049
3727
|
if (INTERACTIVE) {
|
|
1050
|
-
|
|
3728
|
+
process4.stdout.write("Select which universal principle docs to ship with this vault:\n");
|
|
1051
3729
|
for (const choice of listPrincipleChoices(TEMPLATE_MANIFEST)) {
|
|
1052
|
-
|
|
3730
|
+
process4.stdout.write(` - ${choice.key}: ${choice.summary}
|
|
1053
3731
|
`);
|
|
1054
3732
|
}
|
|
1055
3733
|
const selected = await promptPrincipleSelection(askYesNo, TEMPLATE_MANIFEST);
|
|
@@ -1058,47 +3736,60 @@ async function resolvePrincipleSelection(principlesFlag) {
|
|
|
1058
3736
|
return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
|
|
1059
3737
|
}
|
|
1060
3738
|
async function main() {
|
|
1061
|
-
const repoRoot =
|
|
1062
|
-
const parsed = parseInitArgs(
|
|
3739
|
+
const repoRoot = process4.env.HS_VAULT_ROOT ? resolve5(process4.env.HS_VAULT_ROOT) : process4.cwd();
|
|
3740
|
+
const parsed = parseInitArgs(process4.argv.slice(3));
|
|
1063
3741
|
if (parsed.help) {
|
|
1064
|
-
|
|
3742
|
+
process4.stdout.write(`${HELP}
|
|
1065
3743
|
`);
|
|
1066
|
-
|
|
3744
|
+
process4.exitCode = EXIT.OK;
|
|
3745
|
+
return;
|
|
1067
3746
|
}
|
|
1068
3747
|
if (parsed.error) {
|
|
1069
|
-
|
|
3748
|
+
process4.stderr.write(`hs init: ${parsed.error.message}
|
|
1070
3749
|
`);
|
|
1071
|
-
|
|
3750
|
+
process4.exitCode = parsed.error.code;
|
|
3751
|
+
return;
|
|
1072
3752
|
}
|
|
1073
3753
|
const args = parsed.args;
|
|
1074
3754
|
if (args.customerVaultDir) {
|
|
1075
|
-
const here =
|
|
1076
|
-
const platformRoot =
|
|
1077
|
-
const targetDir =
|
|
3755
|
+
const here = dirname6(fileURLToPath4(import.meta.url));
|
|
3756
|
+
const platformRoot = resolve5(here, "..", "..");
|
|
3757
|
+
const targetDir = resolve5(resolveCallerPath(args.customerVaultDir, process4.env, process4.cwd()));
|
|
1078
3758
|
const deps = {
|
|
1079
3759
|
platformRoot,
|
|
1080
|
-
exists: (p) =>
|
|
1081
|
-
readdir: (p) =>
|
|
3760
|
+
exists: (p) => existsSync5(p),
|
|
3761
|
+
readdir: (p) => readdirSync2(p),
|
|
1082
3762
|
mkdirRecursive: (p) => {
|
|
1083
|
-
|
|
3763
|
+
mkdirSync6(p, { recursive: true });
|
|
1084
3764
|
},
|
|
1085
|
-
writeFile: (p, c) =>
|
|
3765
|
+
writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
|
|
1086
3766
|
copyFile: (src, dest) => copyFileSync(src, dest),
|
|
1087
|
-
joinPath: (...s) =>
|
|
3767
|
+
joinPath: (...s) => join11(...s),
|
|
3768
|
+
readConfigSeed,
|
|
3769
|
+
readFile: (p) => {
|
|
3770
|
+
try {
|
|
3771
|
+
return readFileSync11(p, "utf8");
|
|
3772
|
+
} catch {
|
|
3773
|
+
return null;
|
|
3774
|
+
}
|
|
3775
|
+
},
|
|
3776
|
+
isDirectory: (p) => statSync(p).isDirectory()
|
|
1088
3777
|
};
|
|
1089
3778
|
const principlesResult = await resolvePrincipleSelection(args.principles);
|
|
1090
3779
|
if (!principlesResult.ok) {
|
|
1091
|
-
|
|
3780
|
+
process4.stderr.write(`hs init: ${principlesResult.message}
|
|
1092
3781
|
`);
|
|
1093
|
-
|
|
3782
|
+
process4.exitCode = EXIT.INVALID_ARG;
|
|
3783
|
+
return;
|
|
1094
3784
|
}
|
|
1095
3785
|
const selected = principlesResult.selected;
|
|
1096
3786
|
const notices = [];
|
|
1097
3787
|
const afterScaffold = [
|
|
1098
|
-
customerClaudeMdHook(),
|
|
1099
|
-
principlesIndexHook(selected, TEMPLATE_MANIFEST)
|
|
3788
|
+
customerClaudeMdHook((msg) => notices.push(msg)),
|
|
3789
|
+
principlesIndexHook(selected, TEMPLATE_MANIFEST),
|
|
3790
|
+
templatePinHook()
|
|
1100
3791
|
];
|
|
1101
|
-
if (
|
|
3792
|
+
if (args.git) {
|
|
1102
3793
|
afterScaffold.push(gitFinalizeHook(nodeGitRunner, (msg) => notices.push(msg)));
|
|
1103
3794
|
}
|
|
1104
3795
|
const result2 = runCustomerVaultInit({ targetDir }, deps, {
|
|
@@ -1107,7 +3798,7 @@ async function main() {
|
|
|
1107
3798
|
manifestFilter: principleManifestFilter(selected),
|
|
1108
3799
|
afterScaffold
|
|
1109
3800
|
});
|
|
1110
|
-
const sink2 = result2.isError ?
|
|
3801
|
+
const sink2 = result2.isError ? process4.stderr : process4.stdout;
|
|
1111
3802
|
if (args.json) {
|
|
1112
3803
|
sink2.write(`${JSON.stringify({ ...result2.payload, notices })}
|
|
1113
3804
|
`);
|
|
@@ -1117,29 +3808,29 @@ async function main() {
|
|
|
1117
3808
|
for (const notice of notices) sink2.write(`${notice}
|
|
1118
3809
|
`);
|
|
1119
3810
|
}
|
|
1120
|
-
|
|
3811
|
+
process4.exitCode = result2.code;
|
|
3812
|
+
return;
|
|
1121
3813
|
}
|
|
1122
3814
|
const result = runInit(args, {
|
|
1123
3815
|
repoRoot,
|
|
1124
|
-
fileExists: (p) =>
|
|
3816
|
+
fileExists: (p) => existsSync5(p),
|
|
1125
3817
|
readFile: (p) => {
|
|
1126
3818
|
try {
|
|
1127
|
-
return
|
|
3819
|
+
return readFileSync11(p, "utf8");
|
|
1128
3820
|
} catch {
|
|
1129
3821
|
return null;
|
|
1130
3822
|
}
|
|
1131
3823
|
},
|
|
1132
|
-
writeFile: (p, c) =>
|
|
1133
|
-
joinPath: (...s) =>
|
|
3824
|
+
writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
|
|
3825
|
+
joinPath: (...s) => join11(...s),
|
|
3826
|
+
mkdirRecursive: (p) => {
|
|
3827
|
+
mkdirSync6(p, { recursive: true });
|
|
3828
|
+
},
|
|
3829
|
+
readConfigSeed
|
|
1134
3830
|
});
|
|
1135
|
-
const sink = result.isError ?
|
|
3831
|
+
const sink = result.isError ? process4.stderr : process4.stdout;
|
|
1136
3832
|
sink.write(`${args.json ? JSON.stringify(result.payload) : result.message}
|
|
1137
3833
|
`);
|
|
1138
|
-
|
|
3834
|
+
process4.exitCode = result.code;
|
|
1139
3835
|
}
|
|
1140
|
-
|
|
1141
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1142
|
-
process2.stderr.write(`hs init: unhandled: ${message}
|
|
1143
|
-
`);
|
|
1144
|
-
process2.exit(EXIT.WRITE_FAILED);
|
|
1145
|
-
});
|
|
3836
|
+
runEntry("hs init", main, { fallbackCode: EXIT.WRITE_FAILED });
|