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,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hs-gateway.mjs — `hs gateway <command>` face of the user-customizable
|
|
3
|
+
// gateway (HS-WEC3; design: specs/research/HS-P861-G9JR-RES-*).
|
|
4
|
+
//
|
|
5
|
+
// hs gateway validate [slug] [--json] validate user skills + scope coverage
|
|
6
|
+
// hs gateway register … blocked on HS-DCCP (prints why, exit 2)
|
|
7
|
+
//
|
|
8
|
+
// Validation is read-only and report-only: it never edits, deletes, or
|
|
9
|
+
// auto-disables user content, and never rewrites a scope declaration.
|
|
10
|
+
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import process from 'node:process';
|
|
13
|
+
|
|
14
|
+
import { buildGatewayReport, USER_SKILLS_REL } from './gateway-skills-lib.mjs';
|
|
15
|
+
|
|
16
|
+
const BLOCKED_COMMANDS = new Set(['register', 'rotate-credentials', 'disable', 'unregister', 'list']);
|
|
17
|
+
|
|
18
|
+
function usage() {
|
|
19
|
+
process.stderr.write(
|
|
20
|
+
[
|
|
21
|
+
'Usage: hs gateway <command>',
|
|
22
|
+
'',
|
|
23
|
+
' validate [slug] [--json] Validate user-authored skill files under',
|
|
24
|
+
` ${USER_SKILLS_REL}${path.sep}<slug>${path.sep}SKILL.md and report`,
|
|
25
|
+
' skills whose declared scope no registered server provides.',
|
|
26
|
+
' register | rotate-credentials | disable | unregister | list',
|
|
27
|
+
' Not available yet — server registration lands with',
|
|
28
|
+
' HS-DCCP-EPIC-thin-project-scoped-mcp-connector.',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n'),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderHuman(report, { slugFilter }) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
const { skills, registry, orphans } = report;
|
|
37
|
+
if (!skills.exists) {
|
|
38
|
+
lines.push(`No user skills — ${USER_SKILLS_REL} does not exist yet.`);
|
|
39
|
+
lines.push(`Author one at ${path.join(USER_SKILLS_REL, '<slug>', 'SKILL.md')} (see the README there or the HS-P861 research doc for the frontmatter format).`);
|
|
40
|
+
return { text: lines.join('\n'), exitCode: 0 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const shown = slugFilter ? skills.skills.filter((s) => s.slug === slugFilter) : skills.skills;
|
|
44
|
+
if (slugFilter && shown.length === 0) {
|
|
45
|
+
return { text: `No user skill named "${slugFilter}" under ${skills.root}.`, exitCode: 1 };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const s of shown) {
|
|
49
|
+
const flag = s.ok ? (s.warnings.length ? 'warn' : ' ok ') : 'FAIL';
|
|
50
|
+
lines.push(`[${flag}] ${s.slug} (${s.relPath})`);
|
|
51
|
+
for (const e of s.errors) lines.push(` error: ${e.message}`);
|
|
52
|
+
for (const w of s.warnings) lines.push(` warning: ${w.message}`);
|
|
53
|
+
}
|
|
54
|
+
for (const m of skills.misplaced) lines.push(`[warn] ${m.name} — ${m.reason}`);
|
|
55
|
+
|
|
56
|
+
lines.push('');
|
|
57
|
+
if (!registry.available) lines.push(`Server registry: not yet available — ${registry.reason ?? report.orphans.registry_reason ?? ''}`.trimEnd());
|
|
58
|
+
const orphanList = slugFilter ? orphans.orphans.filter((o) => o.slug === slugFilter) : orphans.orphans;
|
|
59
|
+
if (orphanList.length) {
|
|
60
|
+
lines.push(`Scope coverage: ${orphanList.length} skill(s) declare a scope no registered server provides (they stay inert until fixed):`);
|
|
61
|
+
for (const o of orphanList) lines.push(` - ${o.slug}: ${o.missing.join(', ')}`);
|
|
62
|
+
} else {
|
|
63
|
+
lines.push('Scope coverage: no orphaned scope declarations.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const invalidShown = shown.filter((s) => !s.ok).length;
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push(`${shown.length} skill(s) checked · ${invalidShown} invalid · ${orphanList.length} orphaned`);
|
|
69
|
+
return { text: lines.join('\n'), exitCode: invalidShown > 0 ? 1 : 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main(argv) {
|
|
73
|
+
const command = argv[0];
|
|
74
|
+
if (!command || command === '--help' || command === '-h') {
|
|
75
|
+
usage();
|
|
76
|
+
return command ? 0 : 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (BLOCKED_COMMANDS.has(command)) {
|
|
80
|
+
process.stderr.write(
|
|
81
|
+
`hs gateway ${command}: not available yet — user MCP server registration (registration table, encrypted credentials, live probe) lands with HS-DCCP-EPIC-thin-project-scoped-mcp-connector, which is still open. Skill validation works today: hs gateway validate\n`,
|
|
82
|
+
);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (command !== 'validate') {
|
|
87
|
+
process.stderr.write(`hs gateway: unknown command "${command}"\n\n`);
|
|
88
|
+
usage();
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rest = argv.slice(1);
|
|
93
|
+
const json = rest.includes('--json');
|
|
94
|
+
const slugFilter = rest.find((a) => !a.startsWith('--')) ?? null;
|
|
95
|
+
const vaultRoot = process.env.HS_VAULT_ROOT ? path.resolve(process.env.HS_VAULT_ROOT) : process.cwd();
|
|
96
|
+
|
|
97
|
+
const report = buildGatewayReport(vaultRoot);
|
|
98
|
+
if (json) {
|
|
99
|
+
const filtered = slugFilter
|
|
100
|
+
? {
|
|
101
|
+
...report,
|
|
102
|
+
skills: { ...report.skills, skills: report.skills.skills.filter((s) => s.slug === slugFilter) },
|
|
103
|
+
orphans: { ...report.orphans, orphans: report.orphans.orphans.filter((o) => o.slug === slugFilter) },
|
|
104
|
+
}
|
|
105
|
+
: report;
|
|
106
|
+
process.stdout.write(`${JSON.stringify(filtered, null, 2)}\n`);
|
|
107
|
+
return filtered.skills.skills.some((s) => !s.ok) ? 1 : 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { text, exitCode } = renderHuman(report, { slugFilter });
|
|
111
|
+
process.stdout.write(`${text}\n`);
|
|
112
|
+
return exitCode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
process.exit(await main(process.argv.slice(2)));
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hs-mode.mjs — CLI for the HeySpex master on/off switch.
|
|
3
|
+
//
|
|
4
|
+
// hs mode → status
|
|
5
|
+
// hs mode status → what is on, what is off, and why
|
|
6
|
+
// hs mode on|off → flip the whole layer
|
|
7
|
+
// hs mode toggle → flip to the opposite of the current mode
|
|
8
|
+
//
|
|
9
|
+
// Flags: --json (machine-readable), --quiet (exit code only).
|
|
10
|
+
// Exit codes: 0 ok · 1 usage · 2 flip failed.
|
|
11
|
+
|
|
12
|
+
import { REPO_ROOT, resolveMode, setMode, mcpEnabled, claudeMdStubbed } from './heyspex-mode.mjs';
|
|
13
|
+
|
|
14
|
+
const argv = process.argv.slice(2).filter((a) => a !== 'mode');
|
|
15
|
+
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
|
16
|
+
const verb = argv.find((a) => !a.startsWith('--')) ?? 'status';
|
|
17
|
+
const JSON_OUT = flags.has('--json');
|
|
18
|
+
const QUIET = flags.has('--quiet');
|
|
19
|
+
|
|
20
|
+
const say = (s = '') => { if (!QUIET && !JSON_OUT) console.log(s); };
|
|
21
|
+
|
|
22
|
+
function statusReport() {
|
|
23
|
+
const { mode, source, overriddenByEnv, persisted, state } = resolveMode(REPO_ROOT);
|
|
24
|
+
return {
|
|
25
|
+
mode,
|
|
26
|
+
source,
|
|
27
|
+
overriddenByEnv,
|
|
28
|
+
persisted,
|
|
29
|
+
changedAt: state?.changedAt ?? null,
|
|
30
|
+
surfaces: {
|
|
31
|
+
hooks: mode === 'on' ? 'live' : 'silenced',
|
|
32
|
+
mcp: mcpEnabled(REPO_ROOT) ? 'enabled' : 'disabled',
|
|
33
|
+
statusline: mode === 'on' ? 'live' : 'blank',
|
|
34
|
+
claudeMd: claudeMdStubbed(REPO_ROOT) ? 'stubbed' : 'full agent contract',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printStatus(r) {
|
|
40
|
+
const badge = r.mode === 'on' ? 'ON' : 'OFF';
|
|
41
|
+
say(`HeySpex is ${badge} (from ${r.source})`);
|
|
42
|
+
say('');
|
|
43
|
+
say(` hooks ${r.surfaces.hooks}`);
|
|
44
|
+
say(` MCP server ${r.surfaces.mcp}`);
|
|
45
|
+
say(` statusline ${r.surfaces.statusline}`);
|
|
46
|
+
say(` CLAUDE.md ${r.surfaces.claudeMd}`);
|
|
47
|
+
if (r.changedAt) say(`\n last changed ${r.changedAt}`);
|
|
48
|
+
if (r.overriddenByEnv) {
|
|
49
|
+
say('');
|
|
50
|
+
say(` ! HEYSPEX_MODE is set in this shell and overrides the saved mode`);
|
|
51
|
+
say(` (saved: ${r.persisted}). Unset it to go back to the saved value.`);
|
|
52
|
+
}
|
|
53
|
+
// A mismatch here means Claude Code was never restarted after the flip.
|
|
54
|
+
const mcpShouldBe = r.mode === 'on' ? 'enabled' : 'disabled';
|
|
55
|
+
if (r.surfaces.mcp !== mcpShouldBe) {
|
|
56
|
+
say(`\n ! MCP server is ${r.surfaces.mcp} but mode is ${r.mode} — rerun \`hs mode ${r.mode}\`.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (flags.has('--help') || flags.has('-h')) {
|
|
61
|
+
console.log(`hs mode — turn the HeySpex agent layer on or off
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
hs mode [status] Show what is on, what is off, and why
|
|
65
|
+
hs mode on Turn HeySpex on (hooks, MCP, statusline, agent contract)
|
|
66
|
+
hs mode off Turn HeySpex off (this repo behaves like a plain codebase)
|
|
67
|
+
hs mode toggle Flip to the opposite mode
|
|
68
|
+
|
|
69
|
+
Flags:
|
|
70
|
+
--force With \`on\`: restore CLAUDE.md even if you edited it
|
|
71
|
+
while HeySpex was off (your version is overwritten;
|
|
72
|
+
the real contract is at .heyspex/mode-backup/).
|
|
73
|
+
--json Machine-readable output
|
|
74
|
+
--quiet Exit code only
|
|
75
|
+
--help, -h This help
|
|
76
|
+
|
|
77
|
+
Both \`on\` and \`off\` require restarting Claude Code: the MCP server and
|
|
78
|
+
CLAUDE.md are read once at startup. Hooks and the statusline take effect
|
|
79
|
+
immediately.
|
|
80
|
+
|
|
81
|
+
Per-shell override without touching saved state:
|
|
82
|
+
HEYSPEX_MODE=off claude # off for this session only
|
|
83
|
+
`);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (verb === 'status') {
|
|
88
|
+
const r = statusReport();
|
|
89
|
+
if (JSON_OUT) console.log(JSON.stringify(r, null, 2));
|
|
90
|
+
else printStatus(r);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let target = verb;
|
|
95
|
+
if (verb === 'toggle') target = resolveMode(REPO_ROOT).mode === 'on' ? 'off' : 'on';
|
|
96
|
+
|
|
97
|
+
if (target !== 'on' && target !== 'off') {
|
|
98
|
+
process.stderr.write(`hs mode: expected \`on\`, \`off\`, \`toggle\`, or \`status\` (got \`${verb}\`)\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const before = resolveMode(REPO_ROOT).mode;
|
|
103
|
+
const result = setMode(target, { repoRoot: REPO_ROOT, force: flags.has('--force') });
|
|
104
|
+
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
if (JSON_OUT) console.log(JSON.stringify(result, null, 2));
|
|
107
|
+
else process.stderr.write(`hs mode: could not turn HeySpex ${target} — ${result.error}\n`);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (JSON_OUT) {
|
|
112
|
+
console.log(JSON.stringify({ ...result, previous: before, ...statusReport() }, null, 2));
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
say(before === target ? `HeySpex was already ${target} — reapplied.` : `HeySpex is now ${target.toUpperCase()}.`);
|
|
117
|
+
say('');
|
|
118
|
+
for (const step of result.steps) say(` ${step.surface.padEnd(12)} ${step.action}${step.reason ? ` (${step.reason})` : ''}`);
|
|
119
|
+
say('');
|
|
120
|
+
say(' Restart Claude Code for the MCP server and CLAUDE.md to take effect.');
|
|
121
|
+
if (target === 'off') {
|
|
122
|
+
say(' Turn it back on any time with: npm run heyspex:on');
|
|
123
|
+
} else {
|
|
124
|
+
say(' Verify the whole setup with: npm run doctor');
|
|
125
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hs next — on-deck resolver CLI fallback ([[HS-TASK-ISK7]]).
|
|
4
|
+
* Dispatched from bin/hs.mjs. One call, no MCP required.
|
|
5
|
+
*/
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import readline from 'node:readline/promises';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
+
import {
|
|
13
|
+
isRunFrontToken,
|
|
14
|
+
normalizeRunFrontToken,
|
|
15
|
+
runFrontCliHint,
|
|
16
|
+
} from '../vendor/heyspex-mcp/run-front.mjs';
|
|
17
|
+
import { normalizeBareSpecIdToken } from '../vendor/heyspex-mcp/spec-id-lib.mjs';
|
|
18
|
+
import { resolveCodeRoot } from './lib/shared-path-helpers.mjs';
|
|
19
|
+
|
|
20
|
+
process.env.HEYSPEX_DISABLE_STDIO_BOOTSTRAP = '1';
|
|
21
|
+
|
|
22
|
+
// This script's own install location — vendor/heyspex-mcp/cli.mjs is CODE,
|
|
23
|
+
// not vault content, so it must resolve against the code checkout, not
|
|
24
|
+
// HS_VAULT_ROOT. Since the repo split, HS_VAULT_ROOT can point at a vault-only
|
|
25
|
+
// checkout (e.g. heyspex-specs) with no vendor/ at all
|
|
26
|
+
// ([[HS-1LCC-4XKS-DIAG-frozen-checks-run-against-the-vault]]).
|
|
27
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
|
|
29
|
+
const HELP = `hs next — resolve the on-deck /hs-next brief
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
hs next [front] [spec_id] [--summary] [--json] [--text] [--ignore-focus] [--include-horizon]
|
|
33
|
+
hs next --help
|
|
34
|
+
|
|
35
|
+
Spec id (optional):
|
|
36
|
+
A bare key works everywhere a full id does: hs next CJV2 == hs next HS-CJV2.
|
|
37
|
+
|
|
38
|
+
Front (optional — the default is whatever 'hs next front' last set; handheld until then):
|
|
39
|
+
handheld Handheld: drive one on-deck task, you in the loop
|
|
40
|
+
assist | assisted Assisted: arm a dispatcher run on the milestone, you on the loop
|
|
41
|
+
auto | autonomous Autonomous: arm an unattended dispatcher run on the milestone
|
|
42
|
+
|
|
43
|
+
Flags:
|
|
44
|
+
--summary Lightweight claim banner only (default: on)
|
|
45
|
+
--no-summary Full execution brief (post-claim shape)
|
|
46
|
+
--json Machine-readable JSON on stdout
|
|
47
|
+
--text Print user_prompt_markdown only (default when not --json)
|
|
48
|
+
--ignore-focus Skip focus pin / manifest focus override
|
|
49
|
+
--include-horizon Include horizon backlog when queue is empty
|
|
50
|
+
--roadmap <id> Roadmap id (default: HS-ROADMAP-PRODUCT)
|
|
51
|
+
--front <name> Explicit front (${runFrontCliHint()})
|
|
52
|
+
|
|
53
|
+
At a terminal, a claim-ready brief ends with a real y/N question: answer y
|
|
54
|
+
(or yes / proceed / confirm) to claim the spec and launch an agent on it
|
|
55
|
+
(same as: hs next <id> --launch --claim). Piped or --json output never
|
|
56
|
+
prompts; it prints that command instead. HS_NEXT_NO_PROMPT=1 disables the
|
|
57
|
+
question.
|
|
58
|
+
|
|
59
|
+
Under an assist/auto front the terminal question is "Arm the run?" instead:
|
|
60
|
+
yes arms it (same as: hs next assist|auto <HS-MIL…> --launch), which writes
|
|
61
|
+
the queue + front files the running dispatcher reads — no terminal opens. An
|
|
62
|
+
epic or task id arms its parent milestone, because the dispatcher runs at
|
|
63
|
+
milestone grain.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
hs next HS-EPIC-YQ5A --summary --json
|
|
67
|
+
hs next CJV2 (same as: hs next HS-CJV2)
|
|
68
|
+
hs next auto HS-EPIC-YQ5A --summary --json
|
|
69
|
+
hs next assist HS-MIL-DECK --text
|
|
70
|
+
|
|
71
|
+
Launch + per-kind defaults (needs the console): this help covers the brief
|
|
72
|
+
resolver only. hs next <id> --launch, hs next model|permission-mode [kind]
|
|
73
|
+
[value] and hs next claim [on|off] are handled by the console half - run
|
|
74
|
+
hs next --launch --help
|
|
75
|
+
for that help.
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
export const EXIT = {
|
|
79
|
+
OK: 0,
|
|
80
|
+
USAGE: 1,
|
|
81
|
+
RESOLVE_FAILED: 2,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** @param {string[]} argv tokens after "next" */
|
|
85
|
+
/** Replies that mean "yes, claim it" at the terminal prompt (case-insensitive). */
|
|
86
|
+
export const CLAIM_CONFIRM_TOKENS = new Set(['y', 'yes', 'proceed', 'confirm']);
|
|
87
|
+
|
|
88
|
+
const AGENT_CLAIM_LINE =
|
|
89
|
+
/^Ready to claim \*\*.+?\*\* `[^`]+` to \*\*.+?\*\*(?:, as \*\*[^*]*\*\*)?\. Type `proceed`.*$/m;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The on-deck brief's last line is written for an agent sitting in a chat
|
|
93
|
+
* ("Type `proceed` ... to claim it and begin"). At a shell nothing listens
|
|
94
|
+
* for that reply — typing `yes` into PowerShell runs a command named `yes`.
|
|
95
|
+
* Rewrite the line for a terminal: keep "Ready to claim ... to <intent>",
|
|
96
|
+
* drop the agent-identity clause (no agent is attached yet), and say what
|
|
97
|
+
* actually claims it from here. Returns the markdown unchanged when the
|
|
98
|
+
* brief is not a claim question.
|
|
99
|
+
*/
|
|
100
|
+
export function renderTerminalClaimPrompt(md, result, { interactive = false } = {}) {
|
|
101
|
+
const claimId = result?.claim_target?.spec_id;
|
|
102
|
+
if (result?.agent_dispatch?.phase !== 'claim_authorization' || !claimId) return md;
|
|
103
|
+
const m = AGENT_CLAIM_LINE.exec(md);
|
|
104
|
+
if (!m) return md;
|
|
105
|
+
const head = m[0]
|
|
106
|
+
.replace(/, as \*\*[^*]*\*\*\./, '.')
|
|
107
|
+
.replace(/\. Type `proceed`.*$/, '.');
|
|
108
|
+
const launchCmd = `hs next ${claimId} --launch --claim`;
|
|
109
|
+
const line = interactive
|
|
110
|
+
? head
|
|
111
|
+
: `${head}\nTo claim it and launch an agent on it: \`${launchCmd}\``;
|
|
112
|
+
return md.replace(m[0], line);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Ask at the terminal whether to claim + launch. Resolves to true on a yes-token. */
|
|
116
|
+
async function askClaimAtTerminal(claimId) {
|
|
117
|
+
return askYesNoAtTerminal(`Claim ${claimId} and launch an agent on it now? [y/N] `);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One y/N question at the terminal. Resolves to true on a yes-token. */
|
|
121
|
+
async function askYesNoAtTerminal(question) {
|
|
122
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
+
try {
|
|
124
|
+
const answer = await rl.question(question);
|
|
125
|
+
return CLAIM_CONFIRM_TOKENS.has(answer.trim().toLowerCase());
|
|
126
|
+
} finally {
|
|
127
|
+
rl.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Hand off to the console half of `hs next` (POST /api/agent/start); exits with its code. */
|
|
132
|
+
function launchClaimedSpec(claimId) {
|
|
133
|
+
spawnHsNext([claimId, '--launch', '--claim']);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Run `hs next <args…>` through the console half and exit with its code. */
|
|
137
|
+
function spawnHsNext(nextArgs) {
|
|
138
|
+
const hsBin = path.join(PACKAGE_ROOT, 'bin', 'hs.mjs');
|
|
139
|
+
const child = spawn(process.execPath, [hsBin, 'next', ...nextArgs], {
|
|
140
|
+
cwd: PACKAGE_ROOT,
|
|
141
|
+
stdio: 'inherit',
|
|
142
|
+
env: process.env,
|
|
143
|
+
});
|
|
144
|
+
child.on('exit', (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
145
|
+
child.on('error', (err) => {
|
|
146
|
+
process.stderr.write(`hs next: failed to launch: ${err.message}\n`);
|
|
147
|
+
process.exit(EXIT.RESOLVE_FAILED);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The stored default front — `launchModels.defaultRunMode` in
|
|
153
|
+
* `spex-console/settings.json`, the file `hs next front <name>` and the
|
|
154
|
+
* console's Pin & Launch modal both write. Read straight off disk so a bare
|
|
155
|
+
* `hs next <id>` needs no console. Null when unset or unreadable (caller
|
|
156
|
+
* keeps handheld).
|
|
157
|
+
* @param {string} codeRoot
|
|
158
|
+
* @returns {import('../vendor/heyspex-mcp/run-front.mjs').RunFront | null}
|
|
159
|
+
*/
|
|
160
|
+
export function readDefaultRunFront(...roots) {
|
|
161
|
+
// The console keeps settings.json under its ACTIVE VAULT root (repoRoot() is
|
|
162
|
+
// the vault checkout), so the vault root is tried first; the code checkout
|
|
163
|
+
// is the pre-split location and stays as the fallback.
|
|
164
|
+
for (const root of roots) {
|
|
165
|
+
if (!root) continue;
|
|
166
|
+
try {
|
|
167
|
+
const raw = fs.readFileSync(path.join(root, 'spex-console', 'settings.json'), 'utf8');
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
const front = normalizeRunFrontToken(parsed?.launchModels?.defaultRunMode);
|
|
170
|
+
if (front) return front;
|
|
171
|
+
} catch {
|
|
172
|
+
// try the next root
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `assist` / `auto` — the word the CLI takes for a run front. */
|
|
179
|
+
export function frontWord(front) {
|
|
180
|
+
return front === 'autonomous' ? 'auto' : 'assist';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Under a run front (assisted / autonomous) the brief is not a one-task claim
|
|
185
|
+
* question — the unit the dispatcher takes is a MILESTONE. Resolve which one:
|
|
186
|
+
* the id itself when it is a milestone, else the top of the claim target's
|
|
187
|
+
* breadcrumb (an epic or task arms its parent milestone). Null under handheld
|
|
188
|
+
* or when no milestone can be named.
|
|
189
|
+
* @param {Record<string, any> | null | undefined} result
|
|
190
|
+
* @param {string} front
|
|
191
|
+
* @returns {{ milestoneId: string, from: 'milestone' | 'epic' | 'task', specId: string } | null}
|
|
192
|
+
*/
|
|
193
|
+
export function resolveArmTarget(result, front) {
|
|
194
|
+
if (!result || (front !== 'assisted' && front !== 'autonomous')) return null;
|
|
195
|
+
const specPath = String(result.spec_path || '').replace(/\\/g, '/');
|
|
196
|
+
const specId = result.spec_id ? String(result.spec_id) : '';
|
|
197
|
+
if (specId && /(^|\/)milestones\//.test(specPath)) {
|
|
198
|
+
return { milestoneId: specId, from: 'milestone', specId };
|
|
199
|
+
}
|
|
200
|
+
const crumb = result.claim_target?.hierarchy_breadcrumb;
|
|
201
|
+
const top = Array.isArray(crumb) && crumb.length > 1 ? String(crumb[0]) : '';
|
|
202
|
+
if (!top) return null;
|
|
203
|
+
const from = /(^|\/)epics\//.test(specPath) ? 'epic' : 'task';
|
|
204
|
+
return { milestoneId: top, from, specId: specId || String(result.claim_target?.spec_id || '') };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The console-half command that arms the run for `target`. */
|
|
208
|
+
export function armCommand(target, front) {
|
|
209
|
+
return `hs next ${frontWord(front)} ${target.milestoneId} --launch`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Terminal-facing lines that replace the one-task claim question under a run front. */
|
|
213
|
+
export function renderArmLines(target, front) {
|
|
214
|
+
const lines = ['', '---', ''];
|
|
215
|
+
if (target.from !== 'milestone') {
|
|
216
|
+
lines.push(
|
|
217
|
+
`${front} runs arm at milestone grain — ${target.specId} sits under milestone ${target.milestoneId}, so that whole milestone is what gets queued.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
lines.push(`To arm the ${front} run: \`${armCommand(target, front)}\``);
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function parseNextArgs(argv) {
|
|
225
|
+
let specId;
|
|
226
|
+
let roadmapId;
|
|
227
|
+
/** @type {import('../vendor/heyspex-mcp/run-front.mjs').RunFront} */
|
|
228
|
+
let front = 'handheld';
|
|
229
|
+
let frontExplicit = false;
|
|
230
|
+
let summary = true;
|
|
231
|
+
let json = false;
|
|
232
|
+
let text = false;
|
|
233
|
+
let ignoreFocus = false;
|
|
234
|
+
let includeHorizon = false;
|
|
235
|
+
|
|
236
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
237
|
+
const a = argv[i];
|
|
238
|
+
if (a === '--help' || a === '-h') return { help: true };
|
|
239
|
+
if (a === '--json') {
|
|
240
|
+
json = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (a === '--text') {
|
|
244
|
+
text = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (a === '--summary') {
|
|
248
|
+
summary = true;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (a === '--no-summary') {
|
|
252
|
+
summary = false;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (a === '--ignore-focus') {
|
|
256
|
+
ignoreFocus = true;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (a === '--include-horizon') {
|
|
260
|
+
includeHorizon = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (a === '--roadmap') {
|
|
264
|
+
const v = argv[++i];
|
|
265
|
+
if (!v) return { error: { code: EXIT.USAGE, message: '--roadmap requires a value' } };
|
|
266
|
+
roadmapId = v;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (a === '--front') {
|
|
270
|
+
const v = argv[++i];
|
|
271
|
+
if (!v) return { error: { code: EXIT.USAGE, message: '--front requires a value' } };
|
|
272
|
+
const normalized = normalizeRunFrontToken(v);
|
|
273
|
+
if (!normalized) {
|
|
274
|
+
return {
|
|
275
|
+
error: {
|
|
276
|
+
code: EXIT.USAGE,
|
|
277
|
+
message: `unknown front: ${v} (expected ${runFrontCliHint()})`,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
front = normalized;
|
|
282
|
+
frontExplicit = true;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (a.startsWith('-')) {
|
|
286
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${a}` } };
|
|
287
|
+
}
|
|
288
|
+
if (isRunFrontToken(a)) {
|
|
289
|
+
front = normalizeRunFrontToken(a);
|
|
290
|
+
frontExplicit = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// A bare key resolves like a full id: `hs next CJV2` is `hs next HS-CJV2`
|
|
294
|
+
// (owner ruling 2026-09-01). Only tokens that already failed as a front or
|
|
295
|
+
// a flag reach here, so this widens what used to be a hard error rather
|
|
296
|
+
// than changing any input that already worked.
|
|
297
|
+
if (!specId) {
|
|
298
|
+
const normalized = normalizeBareSpecIdToken(a);
|
|
299
|
+
if (normalized) {
|
|
300
|
+
specId = normalized;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${a}` } };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
args: {
|
|
309
|
+
spec_id: specId,
|
|
310
|
+
roadmap_id: roadmapId,
|
|
311
|
+
front,
|
|
312
|
+
front_explicit: frontExplicit,
|
|
313
|
+
summary,
|
|
314
|
+
json,
|
|
315
|
+
text: text || !json,
|
|
316
|
+
ignore_focus: ignoreFocus,
|
|
317
|
+
include_horizon: includeHorizon,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function main() {
|
|
323
|
+
const parsed = parseNextArgs(process.argv.slice(2));
|
|
324
|
+
if (parsed.help) {
|
|
325
|
+
process.stdout.write(`${HELP}\n`);
|
|
326
|
+
process.exit(EXIT.OK);
|
|
327
|
+
}
|
|
328
|
+
if (parsed.error) {
|
|
329
|
+
process.stderr.write(`hs next: ${parsed.error.message}\n`);
|
|
330
|
+
process.exit(parsed.error.code);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const vaultRoot = process.env.HS_VAULT_ROOT;
|
|
334
|
+
if (!vaultRoot) {
|
|
335
|
+
process.stderr.write('hs next: HS_VAULT_ROOT is not set (the dispatcher must pass it).\n');
|
|
336
|
+
process.exit(EXIT.USAGE);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const codeRoot = resolveCodeRoot({ packageRoot: PACKAGE_ROOT });
|
|
340
|
+
// No front word typed: the stored default (`hs next front <name>`) decides.
|
|
341
|
+
if (!parsed.args.front_explicit) {
|
|
342
|
+
const stored = readDefaultRunFront(vaultRoot, codeRoot);
|
|
343
|
+
if (stored) parsed.args.front = stored;
|
|
344
|
+
}
|
|
345
|
+
// resolveNextOnDeckPayload / applyOnDeckHttpAugments / injectCloudWipState
|
|
346
|
+
// live in on-deck-brief.mjs, not cli.mjs (cli.mjs never re-exports them —
|
|
347
|
+
// every other caller, e.g. prompts.mjs and claim-tools.mjs, imports them
|
|
348
|
+
// straight from on-deck-brief.mjs).
|
|
349
|
+
const onDeckBriefPath = path.join(codeRoot, 'vendor', 'heyspex-mcp', 'on-deck-brief.mjs');
|
|
350
|
+
const { resolveNextOnDeckAroundActiveClaims, applyOnDeckHttpAugments, injectCloudWipState } =
|
|
351
|
+
await import(pathToFileURL(onDeckBriefPath).href);
|
|
352
|
+
const config = {
|
|
353
|
+
vaultRoot,
|
|
354
|
+
defaultRoadmapId: process.env.HEYSPEX_ROADMAP_ID || 'HS-ROADMAP-PRODUCT',
|
|
355
|
+
};
|
|
356
|
+
const toolArgs = {
|
|
357
|
+
summary: parsed.args.summary,
|
|
358
|
+
ignore_focus: parsed.args.ignore_focus,
|
|
359
|
+
include_horizon: parsed.args.include_horizon,
|
|
360
|
+
front: parsed.args.front,
|
|
361
|
+
};
|
|
362
|
+
if (parsed.args.spec_id) toolArgs.spec_id = parsed.args.spec_id;
|
|
363
|
+
if (parsed.args.roadmap_id) toolArgs.roadmap_id = parsed.args.roadmap_id;
|
|
364
|
+
|
|
365
|
+
// Cloud reliance ([[HS-VOCF]]): the hand-off ledger lives in Neon there, so
|
|
366
|
+
// pre-fetch it (GET /api/wip via the MCP transport) and let the WIP enricher
|
|
367
|
+
// surface the row instead of the stale local file. Best-effort — falls back
|
|
368
|
+
// to the file ledger on any failure.
|
|
369
|
+
const argsWithCloudWip = await injectCloudWipState(vaultRoot, toolArgs);
|
|
370
|
+
// Steps past tasks another agent actively holds and lists who holds what,
|
|
371
|
+
// so a second terminal gets the next launchable task ([[HS-3C9M-A4TX-CAP]]).
|
|
372
|
+
const { error, result } = await resolveNextOnDeckAroundActiveClaims(
|
|
373
|
+
vaultRoot,
|
|
374
|
+
config,
|
|
375
|
+
argsWithCloudWip,
|
|
376
|
+
);
|
|
377
|
+
if (error) {
|
|
378
|
+
if (parsed.args.json) {
|
|
379
|
+
process.stderr.write(`${JSON.stringify({ error })}\n`);
|
|
380
|
+
} else {
|
|
381
|
+
process.stderr.write(`hs next: ${error.error ?? 'RESOLVE_FAILED'} — ${error.message ?? ''}\n`);
|
|
382
|
+
}
|
|
383
|
+
process.exit(EXIT.RESOLVE_FAILED);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
await applyOnDeckHttpAugments(result, toolArgs);
|
|
387
|
+
|
|
388
|
+
if (parsed.args.json) {
|
|
389
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
390
|
+
process.exit(EXIT.OK);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const rawMd = result?.user_prompt_markdown ?? result?.prompt_markdown ?? '';
|
|
394
|
+
const interactive =
|
|
395
|
+
Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.HS_NEXT_NO_PROMPT;
|
|
396
|
+
// A run front never asks to claim one task — drop the agent claim line and
|
|
397
|
+
// ask about arming the milestone instead.
|
|
398
|
+
const arm = resolveArmTarget(result, parsed.args.front);
|
|
399
|
+
const md = arm
|
|
400
|
+
? rawMd.replace(AGENT_CLAIM_LINE, '').trimEnd()
|
|
401
|
+
: renderTerminalClaimPrompt(rawMd, result, { interactive });
|
|
402
|
+
process.stdout.write(`${md}\n`);
|
|
403
|
+
|
|
404
|
+
if (arm) {
|
|
405
|
+
// The brief's own orchestration section (phase arm_run) already names the
|
|
406
|
+
// command; only print the CLI's lines when an older brief shape lacks it.
|
|
407
|
+
if (result?.agent_dispatch?.phase !== 'arm_run') {
|
|
408
|
+
process.stdout.write(`${renderArmLines(arm, parsed.args.front)}\n`);
|
|
409
|
+
}
|
|
410
|
+
if (!interactive) process.exit(EXIT.OK);
|
|
411
|
+
if (await askYesNoAtTerminal(`Arm the ${parsed.args.front} run on ${arm.milestoneId} now? [y/N] `)) {
|
|
412
|
+
spawnHsNext([frontWord(parsed.args.front), arm.milestoneId, '--launch']);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
process.stdout.write(`Not armed. Later: ${armCommand(arm, parsed.args.front)}\n`);
|
|
416
|
+
process.exit(EXIT.OK);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const claimId = result?.claim_target?.spec_id;
|
|
420
|
+
const isClaimQuestion = result?.agent_dispatch?.phase === 'claim_authorization' && claimId;
|
|
421
|
+
if (!interactive || !isClaimQuestion) process.exit(EXIT.OK);
|
|
422
|
+
|
|
423
|
+
// Attached to a real terminal: actually wait for the answer the brief asks
|
|
424
|
+
// for, then hand off to the launch half of `hs next` on yes.
|
|
425
|
+
if (await askClaimAtTerminal(claimId)) {
|
|
426
|
+
launchClaimedSpec(claimId);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
process.stdout.write(`Not claimed. Later: hs next ${claimId} --launch --claim\n`);
|
|
430
|
+
process.exit(EXIT.OK);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const invoked = process.argv[1]
|
|
434
|
+
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
435
|
+
|
|
436
|
+
if (invoked) {
|
|
437
|
+
main().catch((err) => {
|
|
438
|
+
process.stderr.write(`hs next: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
439
|
+
process.exit(EXIT.RESOLVE_FAILED);
|
|
440
|
+
});
|
|
441
|
+
}
|