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,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hs settings — CLI face of the HeySpex settings capability.
|
|
4
|
+
* Dispatched from bin/hs.mjs. Delegates to the ONE settings core
|
|
5
|
+
* (vendor/heyspex-mcp/config-settings.mjs) per the three-faces principle
|
|
6
|
+
* ([[capability-three-faces-no-per-capability-slash]]).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* hs settings list [prefix] [--json]
|
|
10
|
+
* hs settings get <dotted.key> [--json]
|
|
11
|
+
* hs settings set <dotted.key> <value> [--json] [--confirmed-by-user]
|
|
12
|
+
*
|
|
13
|
+
* WRITE-PERMISSION GUARDRAIL (agent-settings-protocol, owner directive
|
|
14
|
+
* 2026-07-11): settings writes require explicit user permission. A human typing
|
|
15
|
+
* `hs settings set` in their own terminal IS the permission. But when this CLI
|
|
16
|
+
* runs inside an agent harness (Claude Code sets CLAUDECODE=1 / CLAUDE_AGENT for
|
|
17
|
+
* its shell tool), the write is agent-initiated: it is refused unless
|
|
18
|
+
* `--confirmed-by-user` is passed, and that flag may ONLY be passed after the
|
|
19
|
+
* user explicitly approved the exact change in the conversation. This holds
|
|
20
|
+
* regardless of agent_behaviors.ask_forgiveness_not_permission — that switch
|
|
21
|
+
* governs behaviors, never its own controls. All writes are audited to
|
|
22
|
+
* .heyspex/settings-history.jsonl.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import process from 'node:process';
|
|
28
|
+
import { pathToFileURL } from 'node:url';
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
listConfigLeaves,
|
|
32
|
+
getEffectiveSetting,
|
|
33
|
+
setConfigLeaf,
|
|
34
|
+
validateSettingKey,
|
|
35
|
+
} from '../vendor/heyspex-mcp/config-settings.mjs';
|
|
36
|
+
import {
|
|
37
|
+
SETTINGS_REGISTRY,
|
|
38
|
+
getSettingEntry,
|
|
39
|
+
isKnownSetting,
|
|
40
|
+
} from '../vendor/heyspex-mcp/settings-registry.mjs';
|
|
41
|
+
import { getActiveConfig } from '../vendor/heyspex-mcp/vault-resolve.mjs';
|
|
42
|
+
|
|
43
|
+
const HELP = `hs settings — read/change HeySpex workflow settings (.heyspex/config.yaml at the VAULT root)
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
hs settings list [prefix] [--json] List all scalar settings (optionally under a dotted prefix)
|
|
47
|
+
hs settings get <dotted.key> [--json] Read one setting (e.g. agent_behaviors.capture_on_idea.enabled)
|
|
48
|
+
hs settings set <dotted.key> <value> Change one setting (value: string | number | true | false | null)
|
|
49
|
+
[--confirmed-by-user] [--json]
|
|
50
|
+
|
|
51
|
+
The settings file lives at <vault root>/.heyspex/config.yaml (ADR-0009). If it is
|
|
52
|
+
missing, \`hs settings set\` creates it from the shipped seed template first and
|
|
53
|
+
then applies the change (reads never create it) — \`hs init\` seeds the same file.
|
|
54
|
+
|
|
55
|
+
Write permission (agent-settings-protocol):
|
|
56
|
+
A human running \`hs settings set\` directly is inherently permissioned. Inside an
|
|
57
|
+
agent harness (CLAUDECODE / CLAUDE_AGENT env present) the write is refused unless
|
|
58
|
+
--confirmed-by-user is passed — and an agent may pass it ONLY after the user
|
|
59
|
+
explicitly approved the exact change. Writes are audited to
|
|
60
|
+
.heyspex/settings-history.jsonl. Agents should prefer the MCP tool
|
|
61
|
+
heyspex_settings_set (two-step confirm).
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The vault root this face reads/writes. `bin/hs.mjs` always forwards
|
|
66
|
+
* `HS_VAULT_ROOT`; the last fallback (this script run directly) asks the shared
|
|
67
|
+
* vault resolver — NEVER this code checkout. Since the 2026-08-31 repo split the
|
|
68
|
+
* vault is a sibling repo, so falling back to `scripts/..` silently read/wrote a
|
|
69
|
+
* `.heyspex/config.yaml` the console and MCP never look at
|
|
70
|
+
* ([[HS-4C76-9W86-DIAG-heyspex-config-yaml-has-no-single-home]]).
|
|
71
|
+
*/
|
|
72
|
+
function vaultRoot() {
|
|
73
|
+
return process.env.HS_VAULT_ROOT || process.env.HEYSPEX_VAULT_ROOT || getActiveConfig().repoRootAbs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isAgentHarness() {
|
|
77
|
+
return Boolean(
|
|
78
|
+
process.env.CLAUDECODE ||
|
|
79
|
+
process.env.CLAUDE_AGENT ||
|
|
80
|
+
process.env.CLAUDE_CODE_SESSION_ID ||
|
|
81
|
+
process.env.CURSOR_AGENT,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when `root` is the HeySpex platform vault rather than a
|
|
87
|
+
* customer-provisioned one. `hs init --customer-vault` unconditionally stamps
|
|
88
|
+
* `template_version: <…>` into the scaffolded vault's `heyspex.yaml`
|
|
89
|
+
* (`spex-console/src/lib/cli/init-git-finalize.ts` `gitFinalizeHook` /
|
|
90
|
+
* `ensureTemplateVersionLine`) — the platform vault's own marker never
|
|
91
|
+
* carries that field. That stamp is the one on-disk, offline-readable signal
|
|
92
|
+
* that distinguishes the two without a network/account lookup (the MCP/CLI
|
|
93
|
+
* process is standalone, per `vendor/heyspex-mcp/vault-resolve.mjs`'s module
|
|
94
|
+
* doc). A vault with no `heyspex.yaml` at all (e.g. this code checkout run
|
|
95
|
+
* directly, with no vault marker) is treated as platform — it is not a
|
|
96
|
+
* customer scaffold. Registry-declared admin-only entries (e.g.
|
|
97
|
+
* `close_out.run_command_drift_doctor`) are hidden from `hs settings list`
|
|
98
|
+
* everywhere `template_version` is present ([[HS-5MNS-15XI-TASK]] AC4).
|
|
99
|
+
*/
|
|
100
|
+
export function isHeyspexPlatformVault(root) {
|
|
101
|
+
let raw;
|
|
102
|
+
try {
|
|
103
|
+
raw = fs.readFileSync(path.join(root, 'heyspex.yaml'), 'utf8');
|
|
104
|
+
} catch {
|
|
105
|
+
return true; // no marker at all — not a customer scaffold
|
|
106
|
+
}
|
|
107
|
+
return !/^template_version:\s*\S+/m.test(raw);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** How a value's `source` reads in a one-line human sentence. */
|
|
111
|
+
function describeSource(source) {
|
|
112
|
+
if (source === 'file') return 'settings file';
|
|
113
|
+
if (source === 'registry_default') return 'shipped default';
|
|
114
|
+
return 'absent';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build the `hs settings list` result: every registry entry this vault ships
|
|
119
|
+
* (admin-only entries dropped in a non-platform vault), each carrying its
|
|
120
|
+
* effective value/source plus its shipped default and description, followed
|
|
121
|
+
* by any keys the settings file has that the registry does not declare
|
|
122
|
+
* (marked `unrecognised: true`) — the freeform namespaces the registry has
|
|
123
|
+
* not been taught yet (`agent_behaviors.*`, `mvp_mode.*`, …) land here too,
|
|
124
|
+
* unchanged from their pre-registry values.
|
|
125
|
+
* ([[HS-5MNS-15XI-TASK-show-every-declared-setting-in-the]])
|
|
126
|
+
*
|
|
127
|
+
* @param {string} root
|
|
128
|
+
* @param {string} [prefix] - optional dotted-key prefix filter
|
|
129
|
+
* @returns {{ declared: object[], unrecognised: object[] }}
|
|
130
|
+
*/
|
|
131
|
+
export function buildListEntries(root, prefix, options = {}) {
|
|
132
|
+
const trimmedPrefix = (prefix ?? '').trim();
|
|
133
|
+
const matchesPrefix = (key) =>
|
|
134
|
+
!trimmedPrefix || key === trimmedPrefix || key.startsWith(trimmedPrefix + '.');
|
|
135
|
+
const platform = isHeyspexPlatformVault(root);
|
|
136
|
+
|
|
137
|
+
const declared = [];
|
|
138
|
+
for (const entry of SETTINGS_REGISTRY) {
|
|
139
|
+
if (entry.adminOnly && !platform) continue;
|
|
140
|
+
if (!matchesPrefix(entry.key)) continue;
|
|
141
|
+
const effective = getEffectiveSetting(root, entry.key, options);
|
|
142
|
+
declared.push({
|
|
143
|
+
key: entry.key,
|
|
144
|
+
value: effective.value,
|
|
145
|
+
source: effective.source,
|
|
146
|
+
default: entry.default,
|
|
147
|
+
type: entry.type,
|
|
148
|
+
description: entry.description,
|
|
149
|
+
cliFlag: entry.cliFlag,
|
|
150
|
+
adminOnly: entry.adminOnly,
|
|
151
|
+
enforced: entry.enforced,
|
|
152
|
+
...(entry.type === 'enum' ? { values: entry.values } : {}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const unrecognised = listConfigLeaves(root, options)
|
|
157
|
+
.filter((leaf) => !isKnownSetting(leaf.key))
|
|
158
|
+
.filter((leaf) => matchesPrefix(leaf.key))
|
|
159
|
+
.map((leaf) => ({ key: leaf.key, value: leaf.value, unrecognised: true }));
|
|
160
|
+
|
|
161
|
+
return { declared, unrecognised };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build the `hs settings get <key>` result: the effective value (file value
|
|
166
|
+
* when present, else the registry's shipped default), which of the two it
|
|
167
|
+
* came from, and — for a name the registry does not declare AND that has no
|
|
168
|
+
* value in the file either — an explicit "not declared" report instead of a
|
|
169
|
+
* silent empty value. ([[HS-7W0S-15XI-TASK-return-the-effective-value-from-the]])
|
|
170
|
+
*
|
|
171
|
+
* @param {string} root
|
|
172
|
+
* @param {string} key
|
|
173
|
+
* @returns {object}
|
|
174
|
+
*/
|
|
175
|
+
export function resolveGet(root, key, options = {}) {
|
|
176
|
+
const effective = getEffectiveSetting(root, key, options);
|
|
177
|
+
if (effective.source === 'absent') {
|
|
178
|
+
return { ok: true, key, found: false, known: false, value: null, source: 'absent' };
|
|
179
|
+
}
|
|
180
|
+
const entry = getSettingEntry(key);
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
key,
|
|
184
|
+
found: effective.found,
|
|
185
|
+
known: Boolean(entry),
|
|
186
|
+
value: effective.value,
|
|
187
|
+
source: effective.source,
|
|
188
|
+
...(entry ? { default: entry.default, description: entry.description } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Render `resolveGet`'s result as the plain-text `hs settings get` body. */
|
|
193
|
+
function renderGetText(result) {
|
|
194
|
+
if (result.source === 'absent') {
|
|
195
|
+
return `${result.key} is not set and is not declared in the settings registry (no shipped default to fall back to)\n`;
|
|
196
|
+
}
|
|
197
|
+
return `${result.key} = ${JSON.stringify(result.value)} [${describeSource(result.source)}]\n`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Render `buildListEntries`'s result as the plain-text `hs settings list` body. */
|
|
201
|
+
function renderListText({ declared, unrecognised }) {
|
|
202
|
+
const lines = [];
|
|
203
|
+
for (const row of declared) {
|
|
204
|
+
lines.push(`${row.key} = ${JSON.stringify(row.value)} [${describeSource(row.source)}]`);
|
|
205
|
+
lines.push(` ${row.description}`);
|
|
206
|
+
}
|
|
207
|
+
if (unrecognised.length) {
|
|
208
|
+
if (declared.length) lines.push('');
|
|
209
|
+
lines.push('Unrecognised (not declared in the settings registry):');
|
|
210
|
+
for (const row of unrecognised) {
|
|
211
|
+
lines.push(` ${row.key} = ${JSON.stringify(row.value)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!declared.length && !unrecognised.length) return 'no settings found\n';
|
|
215
|
+
return lines.join('\n') + '\n';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseValueToken(token) {
|
|
219
|
+
if (token === undefined) return undefined;
|
|
220
|
+
if (token === 'null') return null;
|
|
221
|
+
if (token === 'true') return true;
|
|
222
|
+
if (token === 'false') return false;
|
|
223
|
+
if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
|
|
224
|
+
return token;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function out(obj, asJson, plain) {
|
|
228
|
+
if (asJson) process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
229
|
+
else process.stdout.write(plain);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function main(argv) {
|
|
233
|
+
// bin/hs.mjs forwards the full argv (leading `settings` token included).
|
|
234
|
+
const args = argv[0] === 'settings' ? argv.slice(1) : argv.slice();
|
|
235
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
236
|
+
process.stdout.write(HELP);
|
|
237
|
+
process.exit(args.length === 0 ? 2 : 0);
|
|
238
|
+
}
|
|
239
|
+
const asJson = args.includes('--json');
|
|
240
|
+
const confirmedByUser = args.includes('--confirmed-by-user');
|
|
241
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
242
|
+
const [verb, keyArg, ...valueTokens] = positional;
|
|
243
|
+
const root = vaultRoot();
|
|
244
|
+
|
|
245
|
+
if (verb === 'list') {
|
|
246
|
+
const prefix = keyArg?.trim() || '';
|
|
247
|
+
const { declared, unrecognised } = buildListEntries(root, prefix);
|
|
248
|
+
out(
|
|
249
|
+
{ ok: true, count: declared.length + unrecognised.length, declared, unrecognised },
|
|
250
|
+
asJson,
|
|
251
|
+
renderListText({ declared, unrecognised }),
|
|
252
|
+
);
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (verb === 'get') {
|
|
257
|
+
const check = validateSettingKey(keyArg ?? '');
|
|
258
|
+
if (!check.ok) {
|
|
259
|
+
process.stderr.write(`hs settings get: ${check.reason}\n`);
|
|
260
|
+
process.exit(2);
|
|
261
|
+
}
|
|
262
|
+
const result = resolveGet(root, keyArg);
|
|
263
|
+
out(result, asJson, renderGetText(result));
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (verb === 'set') {
|
|
268
|
+
const check = validateSettingKey(keyArg ?? '');
|
|
269
|
+
if (!check.ok) {
|
|
270
|
+
process.stderr.write(`hs settings set: ${check.reason}\n`);
|
|
271
|
+
process.exit(2);
|
|
272
|
+
}
|
|
273
|
+
if (valueTokens.length === 0) {
|
|
274
|
+
process.stderr.write('hs settings set: missing <value> (string | number | true | false | null)\n');
|
|
275
|
+
process.exit(2);
|
|
276
|
+
}
|
|
277
|
+
const value = parseValueToken(valueTokens.join(' '));
|
|
278
|
+
|
|
279
|
+
if (isAgentHarness() && !confirmedByUser) {
|
|
280
|
+
const msg =
|
|
281
|
+
`hs settings set: REFUSED — this shell is running inside an agent harness, and settings writes ` +
|
|
282
|
+
`require explicit user permission (agent-settings-protocol; independent of ` +
|
|
283
|
+
`ask_forgiveness_not_permission). Show the user the exact change and, only after they approve, ` +
|
|
284
|
+
`re-run with --confirmed-by-user (or use the MCP tool heyspex_settings_set, which has a ` +
|
|
285
|
+
`built-in confirm step).\n`;
|
|
286
|
+
process.stderr.write(msg);
|
|
287
|
+
process.exit(3);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const actor = isAgentHarness() ? 'agent (CLI, user-confirmed)' : 'human (CLI)';
|
|
291
|
+
const result = setConfigLeaf(root, keyArg, value, { actor, face: 'cli' });
|
|
292
|
+
if (!result.ok) {
|
|
293
|
+
process.stderr.write(`hs settings set: ${result.error} — ${result.message}\n`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
// HS-4C76 fix 3: the core seeds a missing config.yaml from the template on
|
|
297
|
+
// the first write instead of refusing — say so, since the user may not have
|
|
298
|
+
// known the file was absent (it is git-tracked at the vault root).
|
|
299
|
+
if (result.seeded === true) {
|
|
300
|
+
process.stderr.write(
|
|
301
|
+
`hs settings set: note — ${path.join(root, '.heyspex', 'config.yaml')} did not exist and was created from ` +
|
|
302
|
+
`the seed template before applying this change. Commit it with the vault (ADR-0009).\n`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
// The CLI face shares the core with heyspex_settings_set, so it inherits the
|
|
306
|
+
// measured `audited` flag ([[HS-IOJW-5YDH-DIAG]] sweep). A settings write with
|
|
307
|
+
// no audit row is a partial, and must not print as a clean success.
|
|
308
|
+
if (result.audited !== true) {
|
|
309
|
+
process.stderr.write(
|
|
310
|
+
`hs settings set: WARNING — the value was applied to .heyspex/config.yaml but the audit append to ` +
|
|
311
|
+
`.heyspex/settings-history.jsonl FAILED. The change is live and UNAUDITED; check that file is writable.\n`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
out(
|
|
315
|
+
{ ok: true, ...result },
|
|
316
|
+
asJson,
|
|
317
|
+
`${keyArg}: ${JSON.stringify(result.previous)} → ${JSON.stringify(result.value)}${result.created ? ' (created)' : ''}${result.seeded === true ? ' (config.yaml seeded from template)' : ''}${result.audited === true ? '' : ' (UNAUDITED — audit append failed)'}\n`,
|
|
318
|
+
);
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
process.stderr.write(`hs settings: unknown subcommand \`${verb ?? ''}\` (expected: list | get | set)\n${HELP}`);
|
|
323
|
+
process.exit(2);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Only auto-run when executed directly (`node scripts/hs-settings.mjs …` or via
|
|
327
|
+
// bin/hs.mjs's spawn) — not when a test imports this module for its exported
|
|
328
|
+
// helpers (buildListEntries, isHeyspexPlatformVault, …).
|
|
329
|
+
const isMain = (() => {
|
|
330
|
+
try {
|
|
331
|
+
return import.meta.url === pathToFileURL(process.argv[1] ?? '').href;
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
})();
|
|
336
|
+
if (isMain) {
|
|
337
|
+
main(process.argv.slice(2));
|
|
338
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hs view <surface> [spec-id] — open ONE console surface in a chromeless
|
|
4
|
+
* app-mode browser window (the "solo surface" console gauge).
|
|
5
|
+
*
|
|
6
|
+
* Per [[HS-RU2R-I87U-RES-solo-surface-cli-launcher-single]]: launches
|
|
7
|
+
* Edge/Chrome with `--app=<url>` (NOT `--kiosk`, which forces fullscreen and
|
|
8
|
+
* ignores window sizing) pointed at the console's `/solo/<surface>` route.
|
|
9
|
+
* Probes ground-truth dev-server status first (same core as `hs server
|
|
10
|
+
* status`) so a down console yields a clear message, never a blank window.
|
|
11
|
+
*
|
|
12
|
+
* Open = run it; close = close the window; re-run = reopen (idempotent).
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* hs view pipeline # follow the focus pin
|
|
16
|
+
* hs view pipeline HS-I87U-CAP # fixed anchor spec
|
|
17
|
+
* hs view docs HS-NP1Y # single-doc reader, live-refreshing
|
|
18
|
+
* hs view pipeline --follow capture # track the capture-on-deck pin
|
|
19
|
+
* hs view pipeline --follow capture --url https://console.example.com
|
|
20
|
+
* hs view pipeline --width 510 --height 900 --browser edge|chrome
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
|
+
|
|
26
|
+
import { getDevServerStatus } from './dev-server-status.mjs';
|
|
27
|
+
import { resolveConsoleUrl, DEFAULT_CONSOLE_URL } from './lib/console-url-resolver.mjs';
|
|
28
|
+
// The runnable form of a HeySpex command on THIS machine — `hs …` when the bin
|
|
29
|
+
// is on PATH, else an absolute-path `node "<install>/bin/hs.mjs" …`. Never an
|
|
30
|
+
// `npx` form: npx resolves from the READER's working directory, and outside a
|
|
31
|
+
// checkout `npx hs` installs the unrelated public package hs@2.0.1
|
|
32
|
+
// ([[HS-I6HT-EKYU-DIAG-every-message-tells-the-user-to-run-hs]]).
|
|
33
|
+
import { hsCommand } from './lib/hs-command.mjs';
|
|
34
|
+
import { collectOccupiedFamilyKeysFromVault, parseSpecId } from '../vendor/heyspex-mcp/spec-id-lib.mjs';
|
|
35
|
+
|
|
36
|
+
function defaultVaultRoot() {
|
|
37
|
+
// This file lives at <vaultRoot>/scripts/, so one level up is the root.
|
|
38
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Levenshtein edit distance — small local helper, no dependency pulled in for it. */
|
|
42
|
+
function editDistance(a, b) {
|
|
43
|
+
const m = a.length;
|
|
44
|
+
const n = b.length;
|
|
45
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
|
|
46
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
47
|
+
for (let i = 1; i <= m; i++) {
|
|
48
|
+
for (let j = 1; j <= n; j++) {
|
|
49
|
+
dp[i][j] =
|
|
50
|
+
a[i - 1] === b[j - 1]
|
|
51
|
+
? dp[i - 1][j - 1]
|
|
52
|
+
: 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return dp[m][n];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Nearest occupied family keys to a typo'd spec token — walks the vault
|
|
60
|
+
* (fresh every call, unlike `.heyspex/vault-index.json`) via the shared
|
|
61
|
+
* spec-id-lib helper, never re-implements the vault scan.
|
|
62
|
+
*/
|
|
63
|
+
function suggestFamilyKeys(rawSpec, vaultRoot, limit = 5) {
|
|
64
|
+
const parsed = parseSpecId(rawSpec);
|
|
65
|
+
const needle = (parsed?.familyKey ?? rawSpec.replace(/^HS-/i, '')).toUpperCase();
|
|
66
|
+
const occupied = [...collectOccupiedFamilyKeysFromVault(vaultRoot)];
|
|
67
|
+
return occupied
|
|
68
|
+
.map((key) => ({ key, dist: editDistance(needle, key) }))
|
|
69
|
+
.sort((x, y) => x.dist - y.dist || x.key.localeCompare(y.key))
|
|
70
|
+
.slice(0, limit)
|
|
71
|
+
.filter((c) => c.dist <= Math.max(2, Math.ceil(needle.length / 2)))
|
|
72
|
+
.map((c) => `HS-${c.key}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a spec id/token against the running console before opening a
|
|
77
|
+
* browser window (same resolution rules as `GET /api/specs/:id`, including
|
|
78
|
+
* its bare-family-token prefix fallback). Returns `{ ok: true }`,
|
|
79
|
+
* `{ ok: false, kind: 'ambiguous', candidates }`, or
|
|
80
|
+
* `{ ok: false, kind: 'not_found', suggestions }`.
|
|
81
|
+
*/
|
|
82
|
+
async function resolveSpec(base, rawSpec, vaultRoot) {
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
res = await fetch(`${base}/api/specs/${encodeURIComponent(rawSpec)}`);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Network hiccup against a console we already confirmed is up — don't
|
|
88
|
+
// block the window on it, just open unvalidated.
|
|
89
|
+
return { ok: true, skipped: true, reason: err?.message || String(err) };
|
|
90
|
+
}
|
|
91
|
+
if (res.ok) return { ok: true };
|
|
92
|
+
let body = null;
|
|
93
|
+
try {
|
|
94
|
+
body = await res.json();
|
|
95
|
+
} catch {
|
|
96
|
+
// non-JSON error body — fall through to generic not_found handling
|
|
97
|
+
}
|
|
98
|
+
if (res.status === 404 && body?.error === 'ambiguous_id' && Array.isArray(body.candidates)) {
|
|
99
|
+
return { ok: false, kind: 'ambiguous', candidates: body.candidates };
|
|
100
|
+
}
|
|
101
|
+
if (res.status !== 404) {
|
|
102
|
+
// Not a verdict on the spec — a hosted console answers an unauthenticated
|
|
103
|
+
// probe with a sign-in redirect or a 401/403. Open unvalidated rather than
|
|
104
|
+
// claiming the doc does not exist.
|
|
105
|
+
return { ok: true, skipped: true, reason: `HTTP ${res.status}` };
|
|
106
|
+
}
|
|
107
|
+
return { ok: false, kind: 'not_found', suggestions: suggestFamilyKeys(rawSpec, vaultRoot) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Anchor sources the window can follow live. `pin` is the default behavior. */
|
|
111
|
+
const FOLLOW_MODES = new Set(['pin', 'capture']);
|
|
112
|
+
|
|
113
|
+
/** Opening width for a surface with nothing better to say. */
|
|
114
|
+
export const DEFAULT_WINDOW_WIDTH = 510;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Surfaces that want a different opening width than the default.
|
|
118
|
+
*
|
|
119
|
+
* `inspector` is one narrow pane meant to sit beside a terminal — at 510 it
|
|
120
|
+
* takes screen it does not need, and its one-pane layout is designed around
|
|
121
|
+
* 350 ([[HS-LO2G-AQVI-TASK-one-pane-inspector-window-doc-takes-the]]).
|
|
122
|
+
*/
|
|
123
|
+
export const SURFACE_WINDOW_WIDTH = {
|
|
124
|
+
inspector: 350,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the window width: an explicit `--width` always wins, otherwise the
|
|
129
|
+
* surface's own default, otherwise {@link DEFAULT_WINDOW_WIDTH}.
|
|
130
|
+
*
|
|
131
|
+
* `explicitWidth` is null/undefined when the flag was not passed. Deciding that
|
|
132
|
+
* by COMPARING against the fallback would silently drop `--width 510` on the
|
|
133
|
+
* inspector back to 350, so the caller tracks "was it passed" instead.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveWindowWidth(surface, explicitWidth) {
|
|
136
|
+
if (typeof explicitWidth === 'number' && Number.isFinite(explicitWidth) && explicitWidth > 0) {
|
|
137
|
+
return explicitWidth;
|
|
138
|
+
}
|
|
139
|
+
return SURFACE_WINDOW_WIDTH[surface] ?? DEFAULT_WINDOW_WIDTH;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseArgs(argv) {
|
|
143
|
+
const args = {
|
|
144
|
+
surface: null,
|
|
145
|
+
spec: null,
|
|
146
|
+
// null until `--width` is seen; resolved per-surface once the surface is known.
|
|
147
|
+
width: null,
|
|
148
|
+
height: 900,
|
|
149
|
+
browser: 'edge',
|
|
150
|
+
follow: null,
|
|
151
|
+
url: null,
|
|
152
|
+
};
|
|
153
|
+
for (let i = 0; i < argv.length; i++) {
|
|
154
|
+
const a = argv[i];
|
|
155
|
+
if (a === '--help' || a === '-h') return { help: true };
|
|
156
|
+
if (a === '--width') { args.width = Number(argv[++i]) || args.width; continue; }
|
|
157
|
+
if (a === '--height') { args.height = Number(argv[++i]) || args.height; continue; }
|
|
158
|
+
if (a === '--browser') { args.browser = String(argv[++i] || 'edge'); continue; }
|
|
159
|
+
if (a === '--url') {
|
|
160
|
+
const raw = String(argv[++i] || '').trim();
|
|
161
|
+
if (!/^https?:\/\/\S+$/.test(raw)) {
|
|
162
|
+
process.stderr.write(`hs view: --url expects an http(s) URL (got "${raw || '(nothing)'}")\n`);
|
|
163
|
+
process.exitCode = 2;
|
|
164
|
+
return { help: true };
|
|
165
|
+
}
|
|
166
|
+
args.url = raw.replace(/\/+$/, '');
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (a === '--follow') {
|
|
170
|
+
const mode = String(argv[++i] || '').trim().toLowerCase();
|
|
171
|
+
if (!FOLLOW_MODES.has(mode)) {
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`hs view: --follow expects ${[...FOLLOW_MODES].join(' or ')} (got "${mode || '(nothing)'}")\n`,
|
|
174
|
+
);
|
|
175
|
+
process.exitCode = 2;
|
|
176
|
+
return { help: true };
|
|
177
|
+
}
|
|
178
|
+
args.follow = mode;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (a.startsWith('--')) {
|
|
182
|
+
process.stderr.write(`hs view: unknown flag ${a}\n`);
|
|
183
|
+
process.exitCode = 2;
|
|
184
|
+
return { help: true };
|
|
185
|
+
}
|
|
186
|
+
if (!args.surface) args.surface = a;
|
|
187
|
+
else if (!args.spec) args.spec = a;
|
|
188
|
+
}
|
|
189
|
+
return args;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function usage() {
|
|
193
|
+
process.stdout.write(
|
|
194
|
+
[
|
|
195
|
+
'hs view <surface> [spec-id] [--follow pin|capture] [--url https://…]',
|
|
196
|
+
' [--width N] [--height N] [--browser edge|chrome]',
|
|
197
|
+
'',
|
|
198
|
+
'Open one console surface in a chromeless app-mode browser window.',
|
|
199
|
+
'Surfaces: pipeline, docs, inspector, whats-next, captures, session-log, settings',
|
|
200
|
+
' (registry: src/app/solo/[surface]/page.tsx).',
|
|
201
|
+
'Without a spec-id the pipeline and docs views follow the focus pin live.',
|
|
202
|
+
'--follow capture tracks the capture-on-deck pin instead, so the window',
|
|
203
|
+
' re-anchors itself onto each new capture rather than opening another.',
|
|
204
|
+
'--url opens against a named console (a hosted one) instead of probing for',
|
|
205
|
+
' a local dev server — the cloud-relay path the desktop companion uses.',
|
|
206
|
+
'',
|
|
207
|
+
].join('\n'),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Only a localhost/loopback resolution still requires a running local dev
|
|
213
|
+
* server; anything else (a paired or `hs dev mode:cloud` hosted console) is
|
|
214
|
+
* opened directly. An unparseable URL is treated as local so it still goes
|
|
215
|
+
* through the dev-server probe instead of being launched blind.
|
|
216
|
+
*/
|
|
217
|
+
function isLocalConsoleUrl(url) {
|
|
218
|
+
try {
|
|
219
|
+
const host = new URL(url).hostname;
|
|
220
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
221
|
+
} catch {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function launchAppWindow(url, { width, height, browser }) {
|
|
227
|
+
const exe = browser === 'chrome' ? 'chrome' : 'msedge';
|
|
228
|
+
const flags = [`--app=${url}`, `--window-size=${width},${height}`];
|
|
229
|
+
let child;
|
|
230
|
+
if (process.platform === 'win32') {
|
|
231
|
+
// `start` resolves msedge/chrome via the App Paths registry; the empty ''
|
|
232
|
+
// is start's window-title slot, without it the exe would be eaten as title.
|
|
233
|
+
child = spawn('cmd.exe', ['/c', 'start', '', exe, ...flags], {
|
|
234
|
+
detached: true,
|
|
235
|
+
stdio: 'ignore',
|
|
236
|
+
});
|
|
237
|
+
} else if (process.platform === 'darwin') {
|
|
238
|
+
const app = browser === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
|
|
239
|
+
child = spawn('open', ['-na', app, '--args', ...flags], { detached: true, stdio: 'ignore' });
|
|
240
|
+
} else {
|
|
241
|
+
const bin = browser === 'chrome' ? 'google-chrome' : 'microsoft-edge';
|
|
242
|
+
child = spawn(bin, flags, { detached: true, stdio: 'ignore' });
|
|
243
|
+
}
|
|
244
|
+
child.unref();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function main() {
|
|
248
|
+
const args = parseArgs(process.argv.slice(2));
|
|
249
|
+
if (args.help || !args.surface) {
|
|
250
|
+
usage();
|
|
251
|
+
if (!args.help) process.exitCode = 2;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Now that the surface is known, settle the width: an explicit `--width`
|
|
256
|
+
// stands, otherwise the surface's own opening width.
|
|
257
|
+
args.width = resolveWindowWidth(args.surface, args.width);
|
|
258
|
+
|
|
259
|
+
// `--url` names the console explicitly — the cloud-relay path, where the
|
|
260
|
+
// companion opens a window against the hosted console it is paired with and
|
|
261
|
+
// there is no local dev server to probe. Without it, the machine's own
|
|
262
|
+
// console resolution decides: `hs dev mode:cloud` / `hs mcp pair` stamp a
|
|
263
|
+
// hosted URL into ~/.heyspex/credentials.yaml, and a non-local resolution
|
|
264
|
+
// needs no dev server either. Only a localhost resolution keeps the old
|
|
265
|
+
// behavior of requiring (and probing for) a running local console.
|
|
266
|
+
const vaultRoot = process.env.HS_VAULT_ROOT || defaultVaultRoot();
|
|
267
|
+
let base = args.url;
|
|
268
|
+
if (!base) {
|
|
269
|
+
const resolved = resolveConsoleUrl({ repoRoot: vaultRoot });
|
|
270
|
+
if (!isLocalConsoleUrl(resolved)) {
|
|
271
|
+
base = resolved.replace(/\/+$/, '');
|
|
272
|
+
} else {
|
|
273
|
+
const status = await getDevServerStatus({ vaultRoot: process.env.HS_VAULT_ROOT });
|
|
274
|
+
if (!status.running) {
|
|
275
|
+
process.stderr.write(
|
|
276
|
+
'hs view: this machine points at a local console but no spex-console dev server is running.\n' +
|
|
277
|
+
' Start it first (npm run dev), then re-run this command.\n' +
|
|
278
|
+
` (Meant the hosted console? Run \`${hsCommand('dev mode:cloud')}\`, or pass --url https://…)\n`,
|
|
279
|
+
);
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const consoleServer = (status.servers || []).find((s) => s.probe === 'console');
|
|
284
|
+
base = (consoleServer?.url || DEFAULT_CONSOLE_URL).replace(/\/+$/, '');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (args.spec) {
|
|
289
|
+
const resolution = await resolveSpec(base, args.spec, vaultRoot);
|
|
290
|
+
if (!resolution.ok) {
|
|
291
|
+
if (resolution.kind === 'ambiguous') {
|
|
292
|
+
process.stderr.write(
|
|
293
|
+
`hs view: "${args.spec}" is ambiguous — it matches more than one spec:\n` +
|
|
294
|
+
resolution.candidates.map((c) => ` ${c}\n`).join('') +
|
|
295
|
+
'Pass the full basename to disambiguate.\n',
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
process.stderr.write(`hs view: no spec found for "${args.spec}".\n`);
|
|
299
|
+
if (resolution.suggestions.length) {
|
|
300
|
+
process.stderr.write(
|
|
301
|
+
'Did you mean:\n' + resolution.suggestions.map((s) => ` ${s}\n`).join(''),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
process.exitCode = 1;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// A fixed `?spec=` wins over any follow mode (both here and in the route),
|
|
311
|
+
// so only send `follow` when there is no fixed anchor to override it.
|
|
312
|
+
const params = new URLSearchParams();
|
|
313
|
+
if (args.spec) params.set('spec', args.spec);
|
|
314
|
+
else if (args.follow) params.set('follow', args.follow);
|
|
315
|
+
const search = params.toString();
|
|
316
|
+
const query = search ? `?${search}` : '';
|
|
317
|
+
const url = `${base}/solo/${encodeURIComponent(args.surface)}${query}`;
|
|
318
|
+
|
|
319
|
+
launchAppWindow(url, args);
|
|
320
|
+
process.stdout.write(
|
|
321
|
+
`hs view: opened ${url} in an app-mode ${args.browser === 'chrome' ? 'Chrome' : 'Edge'} window ` +
|
|
322
|
+
`(${args.width}x${args.height}). Close the window to close; re-run to reopen.\n`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Run only as the CLI entry point. Without this guard, importing the module —
|
|
327
|
+
// which is how `resolveWindowWidth` is tested — would spawn a browser window.
|
|
328
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
329
|
+
main().catch((err) => {
|
|
330
|
+
process.stderr.write(`hs view: ${err?.message || err}\n`);
|
|
331
|
+
process.exitCode = 1;
|
|
332
|
+
});
|
|
333
|
+
}
|