heyspex 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/bin/hs.mjs +931 -98
- package/bin/spexcode.mjs +262 -0
- package/dev-factory/engine/worktree.mjs +740 -0
- package/dev-factory/lib/code-repo-root.mjs +67 -0
- package/dev-factory/lib/context-budget.mjs +84 -0
- package/dev-factory/lib/epic-detect.mjs +190 -0
- package/dev-factory/lib/fleet-queue-file.mjs +846 -0
- package/dev-factory/lib/fleet-queue.mjs +426 -0
- package/dev-factory/lib/fleet-state.mjs +14 -18
- package/dev-factory/lib/model-catalog.mjs +338 -0
- package/dev-factory/lib/seat-context.mjs +199 -0
- package/dev-factory/lib/spec-io.mjs +93 -0
- package/dev-factory/lib/sufficiency-score.mjs +8 -2
- package/dev-factory/lib/worktree-allocator.mjs +744 -0
- package/dev-factory/model-catalog.json +222 -0
- package/dist/cli/hs-check.mjs +1085 -103
- package/dist/cli/hs-claim.mjs +936 -63
- package/dist/cli/hs-connect.mjs +9786 -1910
- package/dist/cli/hs-defer.mjs +5267 -3053
- package/dist/cli/hs-dev.mjs +564 -0
- package/dist/cli/hs-env.mjs +385 -299
- package/dist/cli/hs-index-rebuild.mjs +7129 -0
- package/dist/cli/hs-index-reconcile.mjs +7852 -0
- package/dist/cli/hs-init.mjs +777 -138
- package/dist/cli/hs-install.mjs +6583 -485
- package/dist/cli/hs-join.mjs +10137 -2368
- package/dist/cli/hs-vault-pull.mjs +4659 -5882
- package/dist/cli/hs-vault.mjs +3793 -1537
- package/dist/cli/hs-workspace.mjs +2550 -524
- package/dist/cli-templates/customer-vault/specs/entity-model.md +594 -601
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +110 -177
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +13 -15
- package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +91 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +150 -168
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -0
- package/dist/cli-templates/harness-commands/claude/hs-auto.md +36 -0
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -0
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-dev.md +20 -0
- package/dist/cli-templates/harness-commands/claude/hs-doctor.md +32 -0
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +28 -0
- package/dist/cli-templates/harness-commands/claude/hs-handoff.md +33 -0
- package/dist/cli-templates/harness-commands/claude/hs-log.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -0
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -0
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -0
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -0
- package/dist/cli-templates/harness-commands/claude/hs-res.md +63 -0
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -0
- package/dist/cli-templates/harness-commands/claude/hs-view.md +26 -0
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -0
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -0
- package/dist/cli-templates/harness-commands/cursor/hs-auto.md +40 -0
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +65 -0
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-dev.md +24 -0
- package/dist/cli-templates/harness-commands/cursor/hs-doctor.md +36 -0
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-handoff.md +37 -0
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +31 -0
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +32 -0
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +73 -0
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +38 -0
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +67 -0
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +26 -0
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +30 -0
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +35 -0
- package/dist/cli-templates/harness-commands/cursor/prototype.md +95 -0
- package/docs/mcp-client-config.md +259 -6
- package/package.json +231 -124
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -0
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -0
- package/plugins/heyspex/.mcp.json +12 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +95 -0
- package/plugins/heyspex/bin/run-hook.mjs +62 -0
- package/plugins/heyspex/hooks/cursor-hooks.json +227 -0
- package/plugins/heyspex/hooks/hooks.json +361 -0
- package/plugins/heyspex/mcp.json +13 -0
- package/scripts/check-mcp-freshness.mjs +17 -9
- package/scripts/claude-hooks/_env-vault-events.mjs +68 -0
- package/scripts/claude-hooks/_guardrail-events.mjs +96 -0
- package/scripts/claude-hooks/_guardrail-loader.mjs +494 -0
- package/scripts/claude-hooks/_intent-classifier-router.mjs +410 -0
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +351 -0
- package/scripts/claude-hooks/_run.mjs +120 -0
- package/scripts/claude-hooks/_transcript-text.mjs +60 -0
- package/scripts/claude-hooks/_vault-retrieval.mjs +322 -0
- package/scripts/claude-hooks/agent-spawn-delegation-gate.mjs +133 -0
- package/scripts/claude-hooks/capture-workflow-gate.mjs +11 -0
- package/scripts/claude-hooks/check-auto-memory-budget.mjs +396 -0
- package/scripts/claude-hooks/check-claude-md-budget.mjs +327 -0
- package/scripts/claude-hooks/check-mcp-freshness-hook.mjs +14 -0
- package/scripts/claude-hooks/claim-before-code-gate.mjs +11 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +575 -0
- package/scripts/claude-hooks/close-terminal-on-ship.mjs +64 -0
- package/scripts/claude-hooks/coo-denylist-gate.mjs +11 -0
- package/scripts/claude-hooks/coo-guard-check.mjs +11 -0
- package/scripts/claude-hooks/deep-research-cost-gate.mjs +11 -0
- package/scripts/claude-hooks/dev-server-task-reminder.mjs +15 -0
- package/scripts/claude-hooks/emit-claim-status.mjs +41 -0
- package/scripts/claude-hooks/enforce-session-sequence.mjs +11 -0
- package/scripts/claude-hooks/enforce-vault-naming.mjs +11 -0
- package/scripts/claude-hooks/env-vault-guard.mjs +11 -0
- package/scripts/claude-hooks/epic-milestone-phrasing-gate.mjs +11 -0
- package/scripts/claude-hooks/git-identity-guard.mjs +11 -0
- package/scripts/claude-hooks/git-reflog-audit.mjs +15 -0
- package/scripts/claude-hooks/git-stash-gate.mjs +11 -0
- package/scripts/claude-hooks/guard-auto-memory-writes.mjs +137 -0
- package/scripts/claude-hooks/id-display-format-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +66 -0
- package/scripts/claude-hooks/inject-coo-seat.mjs +78 -0
- package/scripts/claude-hooks/inject-dev-server-awareness.mjs +11 -0
- package/scripts/claude-hooks/inject-doctrine.mjs +13 -0
- package/scripts/claude-hooks/inject-vault-context.mjs +14 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +247 -0
- package/scripts/claude-hooks/log-session-activity.mjs +341 -0
- package/scripts/claude-hooks/naughty-list-gate.mjs +11 -0
- package/scripts/claude-hooks/notify-status-flip.mjs +72 -0
- package/scripts/claude-hooks/plan-exit-ingest-gate.mjs +170 -0
- package/scripts/claude-hooks/plan-ingest-close-reminder.mjs +66 -0
- package/scripts/claude-hooks/plan-review-gate.mjs +11 -0
- package/scripts/claude-hooks/preflight-reminder.mjs +262 -0
- package/scripts/claude-hooks/protected-action-intent.mjs +221 -0
- package/scripts/claude-hooks/research-method-guard.mjs +398 -0
- package/scripts/claude-hooks/research-protocol-gate.mjs +11 -0
- package/scripts/claude-hooks/session-end.mjs +449 -0
- package/scripts/claude-hooks/settings-write-gate.mjs +11 -0
- package/scripts/claude-hooks/settlement-check-gate.mjs +39 -0
- package/scripts/claude-hooks/status-flip-detect.mjs +17 -0
- package/scripts/claude-hooks/terminal-close-shared.mjs +426 -0
- package/scripts/claude-hooks/terminal-close-watcher.mjs +122 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +298 -0
- package/scripts/cursor-hooks/manifest.mjs +235 -0
- package/scripts/dev-server-status.mjs +34 -13
- package/scripts/dev-server-stop.mjs +313 -0
- package/scripts/heyspex-mode.mjs +268 -0
- package/scripts/hs-auto-resolve.mjs +240 -0
- package/scripts/hs-auto-source.mjs +398 -0
- package/scripts/hs-auto.mjs +134 -0
- package/scripts/hs-capture-cli.mjs +326 -0
- package/scripts/hs-classifier.mjs +136 -0
- package/scripts/hs-companion.mjs +88 -0
- package/scripts/hs-gateway.mjs +115 -0
- package/scripts/hs-mode.mjs +125 -0
- package/scripts/hs-next.mjs +441 -0
- package/scripts/hs-settings.mjs +338 -0
- package/scripts/hs-view.mjs +333 -0
- package/scripts/hs-worktree.mjs +201 -0
- package/scripts/install-claude-code-hooks.mjs +270 -0
- package/scripts/install-cursor-hooks.mjs +471 -0
- package/scripts/lib/agent-env-markers.mjs +38 -0
- package/scripts/lib/console-url-resolver.mjs +197 -60
- package/scripts/lib/customer-kit.mjs +886 -0
- package/scripts/lib/dev-mode.mjs +244 -28
- package/scripts/lib/heyspex-home.mjs +72 -0
- package/scripts/lib/hs-command.mjs +277 -0
- package/scripts/lib/hs-companion-settings.mjs +472 -0
- package/scripts/lib/hs-source-commit.mjs +59 -5
- package/scripts/lib/hs-source-identity.mjs +526 -0
- package/scripts/lib/hs-source-log.mjs +11 -21
- package/scripts/lib/hs-source-push.mjs +107 -58
- package/scripts/lib/hs-worktree-args.mjs +115 -0
- package/scripts/lib/shared-path-helpers.mjs +134 -0
- package/scripts/lib/standalone-doctor.mjs +540 -3
- package/scripts/lib/subagent-git-lock.mjs +109 -0
- package/scripts/lib/template-pin.mjs +34 -0
- package/scripts/provenance-lib.mjs +91 -0
- package/scripts/stamp-provenance-on-edit.mjs +222 -0
- package/scripts/wip-ledger-health.mjs +653 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -0
- package/spexharness/src/adapters/audit.mjs +197 -0
- package/spexharness/src/adapters/claude-code.mjs +441 -0
- package/spexharness/src/chat/tui/ansi.mjs +448 -0
- package/spexharness/src/chat/tui/palette.mjs +22 -0
- package/spexharness/src/chat/tui/select.mjs +208 -0
- package/spexharness/src/config-root.mjs +54 -0
- package/spexharness/src/context-budget.mjs +141 -0
- package/spexharness/src/dev-server-control.mjs +389 -0
- package/spexharness/src/doctrine/behaviour.mjs +371 -0
- package/spexharness/src/doctrine/corpus.mjs +733 -0
- package/spexharness/src/doctrine/inject.mjs +338 -0
- package/spexharness/src/doctrine/merge.mjs +184 -0
- package/spexharness/src/doctrine/sources.mjs +252 -0
- package/spexharness/src/doctrine/synthesize.mjs +401 -0
- package/spexharness/src/doctrine/vault-source.mjs +574 -0
- package/spexharness/src/doctrine/writer.mjs +176 -0
- package/spexharness/src/events.mjs +84 -0
- package/spexharness/src/gates/capture-workflow-gate.mjs +202 -0
- package/spexharness/src/gates/claim-before-code-gate.decide.mjs +33 -0
- package/spexharness/src/gates/claim-before-code-gate.mjs +129 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +274 -0
- package/spexharness/src/gates/enforce-session-sequence.mjs +895 -0
- package/spexharness/src/gates/enforce-vault-naming.mjs +215 -0
- package/spexharness/src/gates/epic-milestone-phrasing-gate.mjs +233 -0
- package/spexharness/src/gates/git-identity-guard.decide.mjs +436 -0
- package/spexharness/src/gates/git-identity-guard.mjs +61 -0
- package/spexharness/src/gates/git-reflog-audit.mjs +151 -0
- package/spexharness/src/gates/git-stash-gate.decide.mjs +361 -0
- package/spexharness/src/gates/git-stash-gate.mjs +159 -0
- package/spexharness/src/gates/id-display-format-gate.mjs +190 -0
- package/spexharness/src/gates/inject-doctrine-gate.mjs +89 -0
- package/spexharness/src/gates/inject-vault-context.mjs +239 -0
- package/spexharness/src/gates/naughty-list-gate.mjs +213 -0
- package/spexharness/src/gates/plan-review-gate.mjs +233 -0
- package/spexharness/src/gates/preflight-reminder-gate.mjs +265 -0
- package/spexharness/src/gates/research-protocol-gate.mjs +175 -0
- package/spexharness/src/gates/settings-write-gate.decide.mjs +96 -0
- package/spexharness/src/gates/settings-write-gate.mjs +104 -0
- package/spexharness/src/git-reflog-audit.mjs +294 -0
- package/spexharness/src/intent-classifier.mjs +194 -0
- package/spexharness/src/intent-directive.mjs +541 -0
- package/spexharness/src/profile/learn.mjs +551 -0
- package/spexharness/src/profile/proposals.mjs +357 -0
- package/spexharness/src/profile/vault-inclusion.mjs +386 -0
- package/spexharness/src/redact.mjs +151 -0
- package/spexharness/src/rule-table.mjs +819 -0
- package/spexharness/src/seat-context.mjs +122 -0
- package/spexharness/src/session-bootstrap.mjs +512 -0
- package/spexharness/src/session-record.mjs +815 -0
- package/spexharness/src/token-inventory.mjs +207 -0
- package/vendor/heyspex-mcp/account-switch.mjs +95 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +315 -1
- package/vendor/heyspex-mcp/blocker-resolver.mjs +37 -2
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +119 -4
- package/vendor/heyspex-mcp/breakdown-source.mjs +271 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +23 -3
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +46 -5
- package/vendor/heyspex-mcp/capture-channel.mjs +55 -5
- package/vendor/heyspex-mcp/capture-console-client.mjs +80 -1
- package/vendor/heyspex-mcp/capture-focus.mjs +1035 -29
- package/vendor/heyspex-mcp/capture-outbox.mjs +5 -1
- package/vendor/heyspex-mcp/capture-tools.mjs +1219 -0
- package/vendor/heyspex-mcp/claim-tools.mjs +1124 -0
- package/vendor/heyspex-mcp/cli-verbs.mjs +2076 -0
- package/vendor/heyspex-mcp/cli.mjs +201 -15524
- package/vendor/heyspex-mcp/cloud-tool-surface.mjs +128 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +412 -0
- package/vendor/heyspex-mcp/config-seed.yaml +15 -0
- package/vendor/heyspex-mcp/config-settings.mjs +290 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +657 -16
- package/vendor/heyspex-mcp/console-transport.mjs +166 -0
- package/vendor/heyspex-mcp/credentials.mjs +584 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +44 -4
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +29 -3
- package/vendor/heyspex-mcp/design-handlers.mjs +31 -0
- package/vendor/heyspex-mcp/design-tools.mjs +4 -1
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +4 -4
- package/vendor/heyspex-mcp/dev-mode-core.mjs +45 -0
- package/vendor/heyspex-mcp/dev-tools.mjs +124 -0
- package/vendor/heyspex-mcp/diagnose-handlers.mjs +54 -0
- package/vendor/heyspex-mcp/events-tools.mjs +207 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +688 -0
- package/vendor/heyspex-mcp/fleet-handlers.mjs +37 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +69 -14
- package/vendor/heyspex-mcp/focus-tools.mjs +190 -0
- package/vendor/heyspex-mcp/folder-trust-store.mjs +59 -0
- package/vendor/heyspex-mcp/http-transport.mjs +300 -0
- package/vendor/heyspex-mcp/list-tools.mjs +257 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +15 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +94 -0
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +53 -6
- package/vendor/heyspex-mcp/occupied-keys.mjs +141 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +3360 -0
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -0
- package/vendor/heyspex-mcp/package-root.mjs +32 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +8 -1
- package/vendor/heyspex-mcp/plan-ground.mjs +621 -652
- package/vendor/heyspex-mcp/plan-push.mjs +906 -21
- package/vendor/heyspex-mcp/plan-review.mjs +124 -30
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +325 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +540 -351
- package/vendor/heyspex-mcp/promote-capture.mjs +386 -69
- package/vendor/heyspex-mcp/promote-diagnose.mjs +74 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +9 -2
- package/vendor/heyspex-mcp/promote-research.mjs +660 -53
- package/vendor/heyspex-mcp/promotion-helpers.mjs +251 -0
- package/vendor/heyspex-mcp/prompts.mjs +1460 -0
- package/vendor/heyspex-mcp/read-path.mjs +29 -9
- package/vendor/heyspex-mcp/receipt-shape.mjs +146 -0
- package/vendor/heyspex-mcp/recipe-handlers.mjs +117 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +210 -3
- package/vendor/heyspex-mcp/register-spec.mjs +266 -54
- package/vendor/heyspex-mcp/request-context.mjs +59 -0
- package/vendor/heyspex-mcp/research-focus.mjs +207 -4
- package/vendor/heyspex-mcp/research-handlers.mjs +623 -0
- package/vendor/heyspex-mcp/research-tools.mjs +4 -4
- package/vendor/heyspex-mcp/response-budget.mjs +348 -0
- package/vendor/heyspex-mcp/review-focus.mjs +25 -1
- package/vendor/heyspex-mcp/search-specs.mjs +280 -0
- package/vendor/heyspex-mcp/seat-block-ledger.mjs +113 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +2 -6
- package/vendor/heyspex-mcp/session-owner.mjs +108 -0
- package/vendor/heyspex-mcp/session-tools.mjs +152 -0
- package/vendor/heyspex-mcp/settings-registry.mjs +232 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +85 -12
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +446 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +93 -17
- package/vendor/heyspex-mcp/split-research.mjs +301 -56
- package/vendor/heyspex-mcp/stdio-rpc.mjs +22 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +132 -6
- package/vendor/heyspex-mcp/summary-contract.mjs +105 -0
- package/vendor/heyspex-mcp/summary-write-tools.mjs +224 -0
- package/vendor/heyspex-mcp/symptom-query.mjs +146 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +108 -1
- package/vendor/heyspex-mcp/tool-catalog.mjs +2813 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +639 -0
- package/vendor/heyspex-mcp/tool-helpers.mjs +325 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +414 -27
- package/vendor/heyspex-mcp/vault-io.mjs +724 -0
- package/vendor/heyspex-mcp/vault-less-endpoints.mjs +52 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +366 -68
- package/vendor/heyspex-mcp/verify-reproduction.mjs +270 -30
- package/vendor/heyspex-mcp/whoami.mjs +91 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dev-server-stop — stop the spex-console Next.js dev server from any terminal.
|
|
4
|
+
*
|
|
5
|
+
* Pairs with scripts/dev-server-status.mjs (ground-truth probe). Kills ONLY
|
|
6
|
+
* processes attributable to the console — the lockfile-recorded launcher pid
|
|
7
|
+
* (+ its child tree), HTTP-identified console listeners, and node processes on
|
|
8
|
+
* the lockfile's own port — then clears the discovery files the launcher
|
|
9
|
+
* leaves behind when force-killed (exit handlers do not run under
|
|
10
|
+
* `taskkill /F`).
|
|
11
|
+
*
|
|
12
|
+
* HS-W7E3: this script used to kill EVERY listener on the candidate port range
|
|
13
|
+
* and took down Docker Desktop's backend (listening on 3002) as collateral.
|
|
14
|
+
* Unattributable listeners are now left alone and reported; the old sweep is
|
|
15
|
+
* opt-in behind `--force-ports`, never the default.
|
|
16
|
+
*
|
|
17
|
+
* Faces:
|
|
18
|
+
* - npm run console:stop
|
|
19
|
+
* - hs server stop
|
|
20
|
+
*
|
|
21
|
+
* Run: node scripts/dev-server-stop.mjs [--json] [--dry-run] [--force-ports]
|
|
22
|
+
*/
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { getDevServerStatus, summarizeStatus } from './dev-server-status.mjs';
|
|
29
|
+
|
|
30
|
+
function defaultVaultRoot() {
|
|
31
|
+
if (process.env.HS_VAULT_ROOT) return process.env.HS_VAULT_ROOT;
|
|
32
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function discoveryPaths(vaultRoot) {
|
|
36
|
+
const dir = path.join(vaultRoot, '.heyspex');
|
|
37
|
+
return {
|
|
38
|
+
lock: path.join(dir, 'dev-server.lock.json'),
|
|
39
|
+
consoleUrl: path.join(dir, 'console-url'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clearDiscovery(vaultRoot) {
|
|
44
|
+
const paths = discoveryPaths(vaultRoot);
|
|
45
|
+
const removed = [];
|
|
46
|
+
for (const [key, file] of Object.entries(paths)) {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(file)) {
|
|
49
|
+
fs.rmSync(file, { force: true });
|
|
50
|
+
removed.push(key);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Best-effort — status re-check is the source of truth.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return removed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pidAlive(pid) {
|
|
60
|
+
if (!pid || typeof pid !== 'number') return false;
|
|
61
|
+
try {
|
|
62
|
+
process.kill(pid, 0);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return err.code === 'EPERM';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Kill a process and its descendants. Mirrors ensure-next-dev.mjs killChildTree:
|
|
71
|
+
* on Windows `shell: true` means the listening pid is often a grandchild, so
|
|
72
|
+
* `taskkill /T /F` is required.
|
|
73
|
+
*/
|
|
74
|
+
function killProcessTree(pid) {
|
|
75
|
+
if (!pidAlive(pid)) {
|
|
76
|
+
return { pid, killed: false, reason: 'not_alive' };
|
|
77
|
+
}
|
|
78
|
+
if (process.platform === 'win32') {
|
|
79
|
+
const result = spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], {
|
|
80
|
+
stdio: 'ignore',
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
pid,
|
|
85
|
+
killed: result.status === 0,
|
|
86
|
+
reason: result.status === 0 ? 'taskkill' : `taskkill_exit_${result.status ?? 'err'}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
process.kill(-pid, 'SIGTERM'); // process group when possible
|
|
91
|
+
} catch {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 'SIGTERM');
|
|
94
|
+
} catch {
|
|
95
|
+
// Already gone.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Sync grace (~1s) then SIGKILL — Atomics.wait is the portable sleep.
|
|
99
|
+
try {
|
|
100
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
|
|
101
|
+
} catch {
|
|
102
|
+
// SharedArrayBuffer unavailable — fall through to immediate SIGKILL.
|
|
103
|
+
}
|
|
104
|
+
if (pidAlive(pid)) {
|
|
105
|
+
try {
|
|
106
|
+
process.kill(-pid, 'SIGKILL');
|
|
107
|
+
} catch {
|
|
108
|
+
try {
|
|
109
|
+
process.kill(pid, 'SIGKILL');
|
|
110
|
+
} catch {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { pid, killed: !pidAlive(pid), reason: 'signal' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Local twin of dev-server-status.mjs's portFromUrl (not exported there).
|
|
119
|
+
function portFromUrl(url) {
|
|
120
|
+
if (typeof url !== 'string') return null;
|
|
121
|
+
try {
|
|
122
|
+
const u = new URL(url);
|
|
123
|
+
if (u.port) return Number(u.port);
|
|
124
|
+
return u.protocol === 'https:' ? 443 : 80;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Decide what to kill — pure and unit-testable (HS-W7E3). A pid is targeted
|
|
132
|
+
* ONLY when it is attributable to the console:
|
|
133
|
+
* - the lockfile's recorded launcher pid (killProcessTree covers its
|
|
134
|
+
* children, so the `next dev` grandchild goes with it), or
|
|
135
|
+
* - a listener the HTTP probe identified as spex-console, or
|
|
136
|
+
* - a node-named listener on the exact port the lockfile points at (a hung
|
|
137
|
+
* console child that stopped answering HTTP).
|
|
138
|
+
* Every other listener on the candidate range (Docker's backend, a Firecrawl
|
|
139
|
+
* container, any unrelated dev tool) is SKIPPED and reported, unless the
|
|
140
|
+
* caller explicitly opted into the old sweep with `--force-ports`.
|
|
141
|
+
*/
|
|
142
|
+
export function planKills(status, { forcePorts = false } = {}) {
|
|
143
|
+
const targets = new Map();
|
|
144
|
+
const skipped = [];
|
|
145
|
+
const addTarget = (pid, reason, port = null) => {
|
|
146
|
+
if (!pid || typeof pid !== 'number') return;
|
|
147
|
+
const entry = targets.get(pid) || { pid, ports: [], reasons: [] };
|
|
148
|
+
if (port != null && !entry.ports.includes(port)) entry.ports.push(port);
|
|
149
|
+
if (!entry.reasons.includes(reason)) entry.reasons.push(reason);
|
|
150
|
+
targets.set(pid, entry);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const lock = status.lockfile || {};
|
|
154
|
+
const lockPort = portFromUrl(lock.url);
|
|
155
|
+
if (lock.present && lock.pidAlive && lock.pid) {
|
|
156
|
+
addTarget(lock.pid, 'lockfile pid (console launcher; tree kill covers its children)');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const server of status.servers || []) {
|
|
160
|
+
if (server.probe === 'console') {
|
|
161
|
+
addTarget(server.pid, `HTTP-identified spex-console on :${server.port}`, server.port);
|
|
162
|
+
} else if (
|
|
163
|
+
lock.present &&
|
|
164
|
+
lockPort != null &&
|
|
165
|
+
server.port === lockPort &&
|
|
166
|
+
/^node(\.exe)?$/i.test(server.name || '')
|
|
167
|
+
) {
|
|
168
|
+
addTarget(server.pid, `node process on the lockfile's port :${server.port}`, server.port);
|
|
169
|
+
} else if (forcePorts) {
|
|
170
|
+
addTarget(server.pid, `--force-ports sweep on :${server.port}`, server.port);
|
|
171
|
+
} else if (server.pid) {
|
|
172
|
+
skipped.push({
|
|
173
|
+
pid: server.pid,
|
|
174
|
+
port: server.port,
|
|
175
|
+
name: server.name || '',
|
|
176
|
+
reason: 'not attributable to the console (use --force-ports to sweep)',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { targets: [...targets.values()], skipped };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function describeTarget(t) {
|
|
185
|
+
const ports = t.ports.length ? ` (:${t.ports.join(', :')})` : '';
|
|
186
|
+
return `pid ${t.pid}${ports} — ${t.reasons.join('; ')}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function describeSkipped(s) {
|
|
190
|
+
return `pid ${s.pid}${s.name ? ` (${s.name})` : ''} on :${s.port} — ${s.reason}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function stopDevServer({ vaultRoot, forcePorts = false, dryRun = false } = {}) {
|
|
194
|
+
const root = vaultRoot || defaultVaultRoot();
|
|
195
|
+
const before = await getDevServerStatus({ vaultRoot: root });
|
|
196
|
+
const plan = planKills(before, { forcePorts });
|
|
197
|
+
const hadLock = before.lockfile?.present === true;
|
|
198
|
+
// HS-W7E3: "running" means console-attributed work to do — an unrelated
|
|
199
|
+
// listener on a candidate port (Docker on 3002…) is NOT the console running.
|
|
200
|
+
const wasRunning = before.running || plan.targets.length > 0;
|
|
201
|
+
const skippedNote = plan.skipped.length
|
|
202
|
+
? `\nLeft alone (not the console):\n ${plan.skipped.map(describeSkipped).join('\n ')}`
|
|
203
|
+
: '';
|
|
204
|
+
|
|
205
|
+
if (dryRun) {
|
|
206
|
+
const wouldKill = plan.targets.length
|
|
207
|
+
? `Would kill:\n ${plan.targets.map(describeTarget).join('\n ')}`
|
|
208
|
+
: 'Would kill: nothing (no process attributable to the console).';
|
|
209
|
+
return {
|
|
210
|
+
stopped: false,
|
|
211
|
+
alreadyStopped: !wasRunning && !hadLock,
|
|
212
|
+
dryRun: true,
|
|
213
|
+
killed: [],
|
|
214
|
+
wouldKill: plan.targets,
|
|
215
|
+
skipped: plan.skipped,
|
|
216
|
+
cleared: [],
|
|
217
|
+
before,
|
|
218
|
+
after: before,
|
|
219
|
+
message: `Dev server stop — DRY RUN (nothing killed, discovery files untouched).\n${wouldKill}${skippedNote}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!wasRunning && !hadLock) {
|
|
224
|
+
return {
|
|
225
|
+
stopped: false,
|
|
226
|
+
alreadyStopped: true,
|
|
227
|
+
killed: [],
|
|
228
|
+
skipped: plan.skipped,
|
|
229
|
+
cleared: [],
|
|
230
|
+
before,
|
|
231
|
+
after: before,
|
|
232
|
+
message: `Dev server: already stopped.${skippedNote}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const killed = plan.targets.map((t) => ({ ...killProcessTree(t.pid), attribution: t.reasons }));
|
|
237
|
+
|
|
238
|
+
// Force-kill skips launcher exit handlers — clear discovery ourselves.
|
|
239
|
+
const cleared = clearDiscovery(root);
|
|
240
|
+
|
|
241
|
+
// Give OS sockets a beat to release before re-probe.
|
|
242
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
243
|
+
const after = await getDevServerStatus({ vaultRoot: root });
|
|
244
|
+
|
|
245
|
+
// Success = no CONSOLE-identified server remains. Unrelated listeners on
|
|
246
|
+
// candidate ports are deliberately left standing (HS-W7E3), so the old
|
|
247
|
+
// `servers.length === 0` condition would false-fail every stop.
|
|
248
|
+
if (!after.running) {
|
|
249
|
+
if (after.lockfile?.present) clearDiscovery(root);
|
|
250
|
+
return {
|
|
251
|
+
stopped: true,
|
|
252
|
+
alreadyStopped: false,
|
|
253
|
+
killed,
|
|
254
|
+
skipped: plan.skipped,
|
|
255
|
+
cleared,
|
|
256
|
+
before,
|
|
257
|
+
after: await getDevServerStatus({ vaultRoot: root }),
|
|
258
|
+
message: wasRunning
|
|
259
|
+
? `Dev server: stopped${killed.length ? ` (killed pid ${killed.map((k) => k.pid).join(', ')})` : ''}.${skippedNote}`
|
|
260
|
+
: `Dev server: cleared stale lock (nothing console-attributable was listening).${skippedNote}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
stopped: false,
|
|
266
|
+
alreadyStopped: false,
|
|
267
|
+
killed,
|
|
268
|
+
skipped: plan.skipped,
|
|
269
|
+
cleared,
|
|
270
|
+
before,
|
|
271
|
+
after,
|
|
272
|
+
message:
|
|
273
|
+
`Dev server: STOP FAILED — console still listening.\n` +
|
|
274
|
+
summarizeStatus(after) +
|
|
275
|
+
`\nTry: npm run console:status then node scripts/dev-server-stop.mjs --force-ports (sweeps ALL candidate-port listeners — check --dry-run first)`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
280
|
+
if (invokedDirectly) {
|
|
281
|
+
const asJson = process.argv.includes('--json');
|
|
282
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
283
|
+
const forcePorts = process.argv.includes('--force-ports');
|
|
284
|
+
stopDevServer({ dryRun, forcePorts })
|
|
285
|
+
.then((result) => {
|
|
286
|
+
if (asJson) {
|
|
287
|
+
process.stdout.write(
|
|
288
|
+
JSON.stringify(
|
|
289
|
+
{
|
|
290
|
+
stopped: result.stopped,
|
|
291
|
+
alreadyStopped: result.alreadyStopped,
|
|
292
|
+
...(result.dryRun ? { dryRun: true, wouldKill: result.wouldKill } : {}),
|
|
293
|
+
killed: result.killed,
|
|
294
|
+
skipped: result.skipped,
|
|
295
|
+
cleared: result.cleared,
|
|
296
|
+
before: result.before,
|
|
297
|
+
after: result.after,
|
|
298
|
+
message: result.message,
|
|
299
|
+
},
|
|
300
|
+
null,
|
|
301
|
+
2,
|
|
302
|
+
) + '\n',
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
process.stdout.write(result.message + '\n');
|
|
306
|
+
}
|
|
307
|
+
process.exit(result.stopped || result.alreadyStopped || result.dryRun ? 0 : 1);
|
|
308
|
+
})
|
|
309
|
+
.catch((err) => {
|
|
310
|
+
process.stderr.write(`Dev server stop failed: ${err?.message ?? err}\n`);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// heyspex-mode.mjs — the master on/off switch for the HeySpex agent layer.
|
|
3
|
+
//
|
|
4
|
+
// HeySpex layers a lot onto a plain Claude Code session: ~25 lifecycle hooks,
|
|
5
|
+
// an MCP server, a statusline, and an always-loaded agent contract (CLAUDE.md).
|
|
6
|
+
// That is the point when you are working the HeySpex protocol — and pure noise
|
|
7
|
+
// when you just want to edit a file in this repo. This module is the single
|
|
8
|
+
// place that answers "is HeySpex on right now?", and the code that flips it.
|
|
9
|
+
//
|
|
10
|
+
// Resolution order (first match wins):
|
|
11
|
+
// 1. HEYSPEX_MODE env var — `off`/`0`/`disabled` or `on`/`1`/`enabled`.
|
|
12
|
+
// A per-shell escape hatch that never touches disk.
|
|
13
|
+
// 2. .heyspex/mode.json — the persisted machine-local choice (gitignored).
|
|
14
|
+
// 3. Default: ON. Absent/unreadable/corrupt state means HeySpex works out of
|
|
15
|
+
// the box, and a broken state file can never silently disable the
|
|
16
|
+
// guardrails. Fail-open is deliberate.
|
|
17
|
+
//
|
|
18
|
+
// `isHeySpexOff()` runs on EVERY hook invocation, so it stays dependency-free
|
|
19
|
+
// and does at most one small synchronous read.
|
|
20
|
+
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
|
|
28
|
+
const STATE_REL = '.heyspex/mode.json';
|
|
29
|
+
const BACKUP_DIR_REL = '.heyspex/mode-backup';
|
|
30
|
+
const CLAUDE_MD_REL = 'CLAUDE.md';
|
|
31
|
+
const CLAUDE_MD_OFF_REL = 'CLAUDE.heyspex-off.md';
|
|
32
|
+
const LOCAL_SETTINGS_REL = '.claude/settings.local.json';
|
|
33
|
+
|
|
34
|
+
const OFF_WORDS = new Set(['off', '0', 'false', 'disabled', 'disable', 'no']);
|
|
35
|
+
const ON_WORDS = new Set(['on', '1', 'true', 'enabled', 'enable', 'yes']);
|
|
36
|
+
|
|
37
|
+
/** Resolve a repo-relative path against the repo root. */
|
|
38
|
+
function at(repoRoot, rel) {
|
|
39
|
+
return path.join(repoRoot, ...rel.split('/'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readJson(file) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeJson(file, value) {
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sha256(buf) {
|
|
56
|
+
return createHash('sha256').update(buf).digest('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The env override, if the user set one. Returns 'on' | 'off' | null.
|
|
61
|
+
* Exported so `hs mode status` can tell the user their env var is overriding
|
|
62
|
+
* the persisted state — otherwise "I turned it off but it's still on" is
|
|
63
|
+
* an unexplainable mystery.
|
|
64
|
+
*/
|
|
65
|
+
export function envOverride() {
|
|
66
|
+
const raw = String(process.env.HEYSPEX_MODE ?? '').trim().toLowerCase();
|
|
67
|
+
if (!raw) return null;
|
|
68
|
+
if (OFF_WORDS.has(raw)) return 'off';
|
|
69
|
+
if (ON_WORDS.has(raw)) return 'on';
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The persisted state file, or null when absent/corrupt. */
|
|
74
|
+
export function readState(repoRoot = REPO_ROOT) {
|
|
75
|
+
return readJson(at(repoRoot, STATE_REL));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* THE hot-path question, called by every hook through `_run.mjs`.
|
|
80
|
+
* Fail-open: any surprise resolves to "HeySpex is on".
|
|
81
|
+
*/
|
|
82
|
+
export function isHeySpexOff(repoRoot = REPO_ROOT) {
|
|
83
|
+
try {
|
|
84
|
+
const override = envOverride();
|
|
85
|
+
if (override) return override === 'off';
|
|
86
|
+
return readState(repoRoot)?.mode === 'off';
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Current effective mode plus where it came from. */
|
|
93
|
+
export function resolveMode(repoRoot = REPO_ROOT) {
|
|
94
|
+
const override = envOverride();
|
|
95
|
+
const state = readState(repoRoot);
|
|
96
|
+
const persisted = state?.mode === 'off' ? 'off' : 'on';
|
|
97
|
+
return {
|
|
98
|
+
mode: override ?? persisted,
|
|
99
|
+
source: override ? 'env:HEYSPEX_MODE' : state ? STATE_REL : 'default',
|
|
100
|
+
overriddenByEnv: Boolean(override) && override !== persisted,
|
|
101
|
+
persisted,
|
|
102
|
+
state,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Surface 1 — the MCP server
|
|
108
|
+
//
|
|
109
|
+
// `.claude/settings.local.json` is gitignored and machine-local, which makes it
|
|
110
|
+
// exactly the right lever: turning HeySpex off must not dirty a tracked file
|
|
111
|
+
// that a teammate would then see in `git status`. Claude Code reads this at
|
|
112
|
+
// startup, so a restart is required either way.
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
function applyMcp(repoRoot, enabled) {
|
|
116
|
+
const file = at(repoRoot, LOCAL_SETTINGS_REL);
|
|
117
|
+
const settings = readJson(file) ?? {};
|
|
118
|
+
if (enabled) {
|
|
119
|
+
settings.enableAllProjectMcpServers = true;
|
|
120
|
+
const list = new Set(settings.enabledMcpjsonServers ?? []);
|
|
121
|
+
list.add('heyspex');
|
|
122
|
+
settings.enabledMcpjsonServers = [...list];
|
|
123
|
+
delete settings.disabledMcpjsonServers;
|
|
124
|
+
} else {
|
|
125
|
+
settings.enableAllProjectMcpServers = false;
|
|
126
|
+
settings.enabledMcpjsonServers = (settings.enabledMcpjsonServers ?? []).filter((s) => s !== 'heyspex');
|
|
127
|
+
const disabled = new Set(settings.disabledMcpjsonServers ?? []);
|
|
128
|
+
disabled.add('heyspex');
|
|
129
|
+
settings.disabledMcpjsonServers = [...disabled];
|
|
130
|
+
}
|
|
131
|
+
writeJson(file, settings);
|
|
132
|
+
return path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function mcpEnabled(repoRoot = REPO_ROOT) {
|
|
136
|
+
const settings = readJson(at(repoRoot, LOCAL_SETTINGS_REL)) ?? {};
|
|
137
|
+
if ((settings.disabledMcpjsonServers ?? []).includes('heyspex')) return false;
|
|
138
|
+
if (settings.enableAllProjectMcpServers === true) return true;
|
|
139
|
+
return (settings.enabledMcpjsonServers ?? []).includes('heyspex');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Surface 2 — CLAUDE.md, the always-loaded agent contract
|
|
144
|
+
//
|
|
145
|
+
// This is the one tracked file the switch has to touch: as long as CLAUDE.md
|
|
146
|
+
// carries the HeySpex contract, the agent keeps following HeySpex protocol even
|
|
147
|
+
// with every hook silenced. So `off` swaps in a short stub and stashes the real
|
|
148
|
+
// file under .heyspex/mode-backup/ with its sha256. `on` restores it and
|
|
149
|
+
// verifies the hash, so a round-trip is provably byte-identical.
|
|
150
|
+
//
|
|
151
|
+
// If you edit CLAUDE.md while HeySpex is off, `on` refuses to clobber your work
|
|
152
|
+
// and tells you where the backup is.
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
const CLAUDE_MD_STUB = `# Project Instructions for AI Agents
|
|
156
|
+
|
|
157
|
+
**HeySpex is currently OFF for this checkout.**
|
|
158
|
+
|
|
159
|
+
This repository normally runs the HeySpex agent protocol — a spec vault under
|
|
160
|
+
\`specs/\`, an MCP server, lifecycle hooks, and a full agent contract in this
|
|
161
|
+
file. All of that is disabled right now, so treat this as an ordinary codebase:
|
|
162
|
+
read the code, make the change that was asked for, run the tests.
|
|
163
|
+
|
|
164
|
+
Do **not** file specs, claim work, recall vault memories, or run HeySpex
|
|
165
|
+
procedures while this notice is here — the tools backing them are turned off.
|
|
166
|
+
|
|
167
|
+
To turn HeySpex back on:
|
|
168
|
+
|
|
169
|
+
\`\`\`bash
|
|
170
|
+
npm run heyspex:on # or: node bin/hs.mjs mode on
|
|
171
|
+
\`\`\`
|
|
172
|
+
|
|
173
|
+
Then restart Claude Code. The real agent contract is restored from
|
|
174
|
+
\`.heyspex/mode-backup/CLAUDE.md\`.
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
function applyClaudeMd(repoRoot, enabled, state, force = false) {
|
|
178
|
+
const live = at(repoRoot, CLAUDE_MD_REL);
|
|
179
|
+
const backup = at(repoRoot, `${BACKUP_DIR_REL}/CLAUDE.md`);
|
|
180
|
+
|
|
181
|
+
if (!enabled) {
|
|
182
|
+
if (state?.claudeMd?.stubbed) return { action: 'already-stubbed' };
|
|
183
|
+
if (!fs.existsSync(live)) return { action: 'skipped', reason: 'no CLAUDE.md' };
|
|
184
|
+
const original = fs.readFileSync(live);
|
|
185
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
186
|
+
fs.writeFileSync(backup, original);
|
|
187
|
+
// Ship the stub alongside the backup too, so `mode status` can tell a
|
|
188
|
+
// pristine stub from one the user has since edited.
|
|
189
|
+
fs.writeFileSync(live, CLAUDE_MD_STUB, 'utf8');
|
|
190
|
+
return {
|
|
191
|
+
action: 'stubbed',
|
|
192
|
+
originalSha256: sha256(original),
|
|
193
|
+
stubSha256: sha256(Buffer.from(CLAUDE_MD_STUB, 'utf8')),
|
|
194
|
+
backup: path.relative(repoRoot, backup).replace(/\\/g, '/'),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!state?.claudeMd?.stubbed) return { action: 'nothing-to-restore' };
|
|
199
|
+
if (!fs.existsSync(backup)) {
|
|
200
|
+
return { action: 'failed', reason: `backup missing at ${BACKUP_DIR_REL}/CLAUDE.md` };
|
|
201
|
+
}
|
|
202
|
+
// Refuse to silently discard edits made to the stub while HeySpex was off.
|
|
203
|
+
// `--force` is the way out: it says "I've read my edits, restore anyway".
|
|
204
|
+
if (fs.existsSync(live) && !force) {
|
|
205
|
+
const current = sha256(fs.readFileSync(live));
|
|
206
|
+
if (state.claudeMd.stubSha256 && current !== state.claudeMd.stubSha256) {
|
|
207
|
+
return {
|
|
208
|
+
action: 'failed',
|
|
209
|
+
reason:
|
|
210
|
+
`CLAUDE.md changed while HeySpex was off — refusing to overwrite it.\n` +
|
|
211
|
+
` Your edits: CLAUDE.md · the real contract: ${BACKUP_DIR_REL}/CLAUDE.md\n` +
|
|
212
|
+
` Salvage anything you want to keep, then rerun with --force to restore.`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const restored = fs.readFileSync(backup);
|
|
217
|
+
fs.writeFileSync(live, restored);
|
|
218
|
+
const ok = !state.claudeMd.originalSha256 || sha256(restored) === state.claudeMd.originalSha256;
|
|
219
|
+
return { action: 'restored', verified: ok };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function claudeMdStubbed(repoRoot = REPO_ROOT) {
|
|
223
|
+
return Boolean(readState(repoRoot)?.claudeMd?.stubbed);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// The flip
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Turn the whole HeySpex layer on or off.
|
|
232
|
+
*
|
|
233
|
+
* Hooks and the statusline need no file surgery — they read `isHeySpexOff()`
|
|
234
|
+
* themselves at fire time, via `scripts/claude-hooks/_run.mjs`. Only the MCP
|
|
235
|
+
* server and CLAUDE.md have to be physically changed, because Claude Code
|
|
236
|
+
* consumes both before any HeySpex code gets to run.
|
|
237
|
+
*/
|
|
238
|
+
export function setMode(target, { repoRoot = REPO_ROOT, force = false } = {}) {
|
|
239
|
+
if (target !== 'on' && target !== 'off') {
|
|
240
|
+
throw new Error(`setMode: expected 'on' or 'off', got '${target}'`);
|
|
241
|
+
}
|
|
242
|
+
const enabled = target === 'on';
|
|
243
|
+
const prior = readState(repoRoot);
|
|
244
|
+
const steps = [];
|
|
245
|
+
|
|
246
|
+
const claudeMd = applyClaudeMd(repoRoot, enabled, prior, force);
|
|
247
|
+
if (claudeMd.action === 'failed') {
|
|
248
|
+
return { ok: false, mode: prior?.mode ?? 'on', error: claudeMd.reason, steps };
|
|
249
|
+
}
|
|
250
|
+
steps.push({ surface: 'CLAUDE.md', ...claudeMd });
|
|
251
|
+
|
|
252
|
+
steps.push({ surface: 'MCP server', action: enabled ? 'enabled' : 'disabled', file: applyMcp(repoRoot, enabled) });
|
|
253
|
+
steps.push({ surface: 'hooks', action: enabled ? 'live' : 'silenced (all exit 0)' });
|
|
254
|
+
steps.push({ surface: 'statusline', action: enabled ? 'live' : 'blank' });
|
|
255
|
+
|
|
256
|
+
const next = {
|
|
257
|
+
mode: target,
|
|
258
|
+
changedAt: new Date().toISOString(),
|
|
259
|
+
claudeMd: enabled
|
|
260
|
+
? { stubbed: false }
|
|
261
|
+
: claudeMd.action === 'stubbed'
|
|
262
|
+
? { stubbed: true, originalSha256: claudeMd.originalSha256, stubSha256: claudeMd.stubSha256 }
|
|
263
|
+
: (prior?.claudeMd ?? { stubbed: false }),
|
|
264
|
+
};
|
|
265
|
+
writeJson(at(repoRoot, STATE_REL), next);
|
|
266
|
+
|
|
267
|
+
return { ok: true, mode: target, steps, restartRequired: true };
|
|
268
|
+
}
|