heyspex 0.2.5 → 0.2.7

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.
Files changed (110) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/bin/hs.mjs +349 -38
  3. package/bin/spexcode.mjs +31 -0
  4. package/dev-factory/lib/dispatch-guard.mjs +75 -0
  5. package/dev-factory/lib/local-firecrawl.mjs +294 -0
  6. package/dev-factory/lib/runner-path.mjs +51 -0
  7. package/dev-factory/lib/seat-context.mjs +20 -4
  8. package/dev-factory/model-catalog.json +222 -222
  9. package/dev-factory/spexcrawl/firecrawl-cli.mjs +755 -0
  10. package/dev-factory/spexcrawl/spexcrawl-cli.mjs +734 -0
  11. package/dist/cli/hs-connect.mjs +40 -3
  12. package/dist/cli/hs-defer.mjs +3817 -3741
  13. package/dist/cli/hs-events.mjs +1296 -0
  14. package/dist/cli/hs-export-events.mjs +1252 -0
  15. package/dist/cli/hs-handoff.mjs +2576 -0
  16. package/dist/cli/hs-index-rebuild.mjs +228 -37
  17. package/dist/cli/hs-index-reconcile.mjs +231 -40
  18. package/dist/cli/hs-init.mjs +22 -3
  19. package/dist/cli/hs-install.mjs +22 -3
  20. package/dist/cli/hs-join.mjs +40 -3
  21. package/dist/cli/hs-next-launch.mjs +2777 -0
  22. package/dist/cli/hs-pin.mjs +1336 -0
  23. package/dist/cli/hs-prereq.mjs +1380 -0
  24. package/dist/cli/hs-revalidate.mjs +3257 -0
  25. package/dist/cli/hs-spawn.mjs +12403 -0
  26. package/dist/cli/hs-test.mjs +1046 -0
  27. package/dist/cli/hs-trash.mjs +362 -0
  28. package/dist/cli/hs-trust.mjs +197 -0
  29. package/dist/cli/hs-vault-pull.mjs +233 -42
  30. package/dist/cli/hs-vault-push.mjs +231 -40
  31. package/dist/cli/hs-vault.mjs +21 -7
  32. package/dist/cli/hs-workspace.mjs +19 -1
  33. package/dist/cli-templates/customer-vault/specs/entity-model.md +1728 -1728
  34. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +464 -464
  35. package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -122
  36. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +46 -46
  37. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +530 -530
  38. package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -83
  39. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  40. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  41. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  42. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -30
  43. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  44. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  45. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  46. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  47. package/dist/cli-templates/harness-commands/claude/hs-res.md +83 -63
  48. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  49. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -28
  50. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  51. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  52. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  53. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  54. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  55. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -24
  56. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  57. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  58. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  59. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  60. package/dist/cli-templates/harness-commands/cursor/hs-res.md +78 -58
  61. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  62. package/dist/cli-templates/harness-commands/cursor/hs-view.md +23 -23
  63. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  64. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  65. package/docs/copilot-plugin-install.md +9 -2
  66. package/docs/mcp-client-config.md +6 -0
  67. package/package.json +266 -257
  68. package/plugins/heyspex/.claude-plugin/plugin.json +1 -1
  69. package/plugins/heyspex/.cursor-plugin/plugin.json +1 -1
  70. package/plugins/heyspex/README.md +6 -0
  71. package/plugins/heyspex/bin/launch-mcp.mjs +17 -0
  72. package/plugins/heyspex/hooks/hooks.json +418 -418
  73. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +2 -2
  74. package/scripts/claude-hooks/wip-checkpoint.mjs +17 -2
  75. package/scripts/claude-statusline/statusline-command.mjs +287 -27
  76. package/scripts/hs-companion.mjs +41 -3
  77. package/scripts/install-claude-code-hooks.mjs +104 -3
  78. package/scripts/install-copilot-hooks.mjs +21 -1
  79. package/scripts/install-cursor-hooks.mjs +4 -3
  80. package/scripts/lib/capture-execution-guard.mjs +221 -0
  81. package/scripts/lib/companion-install.mjs +768 -0
  82. package/scripts/lib/customer-kit.mjs +53 -3
  83. package/scripts/lib/hs-companion-settings.mjs +26 -0
  84. package/scripts/lib/hs-source-push.mjs +386 -386
  85. package/scripts/record-grounding.mjs +38 -0
  86. package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -29
  87. package/spexharness/src/adapters/copilot-models.mjs +179 -0
  88. package/spexharness/src/adapters/harness-detect.mjs +148 -0
  89. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -132
  90. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +153 -125
  91. package/spexharness/src/seat-context.mjs +10 -5
  92. package/spexharness/src/session-bootstrap.mjs +6 -3
  93. package/vendor/heyspex-mcp/cli.mjs +6 -1
  94. package/vendor/heyspex-mcp/config-seed.yaml +15 -15
  95. package/vendor/heyspex-mcp/dev-factory-overlay.mjs +142 -0
  96. package/vendor/heyspex-mcp/http-transport.mjs +1 -1
  97. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  98. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  99. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -327
  100. package/vendor/heyspex-mcp/procedure-tools.mjs +1209 -1181
  101. package/vendor/heyspex-mcp/research-tools.mjs +22 -2
  102. package/vendor/heyspex-mcp/seat-assign-cli.mjs +242 -0
  103. package/vendor/heyspex-mcp/seat-assignment-rules.mjs +96 -0
  104. package/vendor/heyspex-mcp/seat-assignments-client.mjs +265 -0
  105. package/vendor/heyspex-mcp/seat-defaults-cli.mjs +45 -0
  106. package/vendor/heyspex-mcp/seat-defaults.mjs +232 -0
  107. package/vendor/heyspex-mcp/seat-identity.mjs +171 -0
  108. package/vendor/heyspex-mcp/test-helpers.mjs +37 -23
  109. package/vendor/heyspex-mcp/vault-io.mjs +720 -720
  110. package/bin/postinstall.mjs +0 -88
@@ -9,7 +9,7 @@
9
9
  "name": "heyspex",
10
10
  "source": "./plugins/heyspex",
11
11
  "displayName": "HeySpex",
12
- "description": "HeySpex plugin for Claude Code and GitHub Copilot CLI: vault integration, MCP server (spec vault tools + hs-* prompts) and lifecycle hooks. Prototype — see plugins/heyspex/README.md before enabling."
12
+ "description": "SpexCode plugin (for Claude Code), also installable in GitHub Copilot CLI: HeySpex vault integration, MCP server (spec vault tools + hs-* prompts) and lifecycle hooks. Prototype — see plugins/heyspex/README.md before enabling."
13
13
  }
14
14
  ]
15
15
  }
package/bin/hs.mjs CHANGED
@@ -12,6 +12,8 @@
12
12
  * node_modules) executes the .ts entrypoints.
13
13
  */
14
14
  import { BINDING_VERBS, runVaultBindingCommand } from "./hs-vault-binding.mjs";
15
+ import { findProjectRoot } from "../scripts/lib/customer-kit.mjs";
16
+ import { companionDoctorRow } from "../scripts/lib/companion-install.mjs";
15
17
  import { spawn } from "node:child_process";
16
18
  import { existsSync, readFileSync, realpathSync } from "node:fs";
17
19
  import path from "node:path";
@@ -36,6 +38,77 @@ import {
36
38
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
37
39
  const PKG_ROOT = path.resolve(__dirname, "..");
38
40
 
41
+ /**
42
+ * True when this `hs` runs from an installed `heyspex` npm package rather than
43
+ * a HeySpex source checkout. Same signal `hs doctor` already dispatches on:
44
+ * scripts/heyspex-doctor.mjs only exists in a source clone (never in `files`).
45
+ */
46
+ const IS_INSTALLED_PACKAGE = !existsSync(path.join(PKG_ROOT, "scripts", "heyspex-doctor.mjs"));
47
+
48
+ /**
49
+ * Help lines for verbs that only work inside a HeySpex source checkout (dev
50
+ * server, Dev-Factory, seat registry, Companion build, the SpexCrawl dev path).
51
+ * `hs --help` from an installed package drops them and says so once.
52
+ */
53
+ const DEV_ONLY_HELP_PREFIXES = [
54
+ " hs server ",
55
+ " hs factory ",
56
+ " hs seat ",
57
+ // Platform-operator verbs whose implementations are not in the npm package:
58
+ // test-customer minting (admin), the Neon fleet queue, SpexCode Doctrine
59
+ // synthesis over this checkout's spexharness/ tree.
60
+ " hs admin ",
61
+ " hs queue ",
62
+ " hs doctrine ",
63
+ // Verbs listed in the source-checkout help whose implementation the npm
64
+ // package does not carry (see NOT_IN_PACKAGE_VERBS below for each reason).
65
+ // Invoking one from an installed package prints a clear "not available in
66
+ // the npm package" line instead of "install is incomplete".
67
+ " hs agent ",
68
+ " hs chat ",
69
+ " hs code-watcher ",
70
+ " hs harness ",
71
+ " hs hooks ",
72
+ " hs import ",
73
+ " hs milestone ",
74
+ " hs model ",
75
+ " hs registry ",
76
+ " hs skills ",
77
+ " hs voice ",
78
+ " hs check judge ",
79
+ " hs bench ",
80
+ " node dev-factory/",
81
+ " search/scrape/crawl CLI",
82
+ " not an `hs` verb",
83
+ " Companion)",
84
+ ];
85
+
86
+ function helpForThisInstall(lines) {
87
+ if (!IS_INSTALLED_PACKAGE) return lines;
88
+ const out = [];
89
+ for (const line of lines) {
90
+ if (line.startsWith(" hs seat set|reset|assignments")) {
91
+ out.push(" hs seat list|set|reset [--harness <h>] Your own model per SpexFleet seat in this vault (Claude Code / Copilot)");
92
+ continue;
93
+ }
94
+ if (DEV_ONLY_HELP_PREFIXES.some((p) => line.startsWith(p))) continue;
95
+ if (line.startsWith(" hs companion start|stop|status|build")) {
96
+ out.push(" hs companion start|stop|status Desktop companion tray app (installed by `hs companion install`)");
97
+ continue;
98
+ }
99
+ out.push(line);
100
+ }
101
+ out.push(
102
+ "HeySpex developer-only verbs (hs server, hs factory, hs seat add|update|disable, hs admin, hs queue,",
103
+ "hs doctrine, hs companion build) need a HeySpex source checkout and are not",
104
+ "listed here. Neither are verbs whose implementation the npm package does not",
105
+ "carry (hs agent, hs chat, hs code-watcher, hs harness, hs hooks, hs import,",
106
+ "hs milestone, hs model, hs registry, hs skills, hs voice, hs check judge).",
107
+ "",
108
+ );
109
+ return out;
110
+ }
111
+
39
112
  /**
40
113
  * Every script path `hs` spawns directly by filesystem path (as opposed to
41
114
  * `dispatchSpexConsoleEntry`'s own three-case tsx/bundle/missing resolution,
@@ -545,8 +618,23 @@ function extractGlobalVaultFlag(argv) {
545
618
  }
546
619
 
547
620
  function usage() {
548
- process.stdout.write(
549
- [
621
+ process.stdout.write(usageLines().join("\n"));
622
+ }
623
+
624
+ /**
625
+ * `hs <namespace> --help` for a namespace whose own handler has no help text
626
+ * (its first token must be a sub-verb): print this install's `hs --help`
627
+ * lines for that namespace and exit 0, instead of "unknown verb --help".
628
+ */
629
+ const HELP_FROM_USAGE_NAMESPACES = new Set(["index", "mcp", "wip", "worktree", "skills"]);
630
+
631
+ /** @param {string} namespace */
632
+ export function namespaceHelpLines(namespace, lines = usageLines()) {
633
+ return lines.filter((l) => l.startsWith(` hs ${namespace} `));
634
+ }
635
+
636
+ function usageLines() {
637
+ return helpForThisInstall([
550
638
  "hs — HeySpex vault CLI",
551
639
  "",
552
640
  "Usage:",
@@ -573,7 +661,7 @@ function usage() {
573
661
  " hs gateway validate [slug] [--json] Validate user-authored gateway skills (.heyspex/skills/user/)",
574
662
  " hs mode on|off|status|toggle Master switch: HeySpex layer on or off (restart Claude Code)",
575
663
  " hs dev [mode:local|mode:cloud|mode:prod] Dev/prod mode + local/cloud reliance (status when bare)",
576
- " hs setup claude|cursor|copilot [--check|--uninstall] Install agent lifecycle hooks (copilot also: --print, --hooks)",
664
+ " hs setup claude|cursor|copilot [--check|--uninstall] Install agent lifecycle hooks (claude, copilot also: --print; copilot: --hooks)",
577
665
  " hs skills sync|check Propagate skills to Cursor/Codex/Claude (cwd-safe)",
578
666
  " hs skills add <module-path> --name <n> --trigger </slash> [--export <n>] [--description <t>] Register a skill (extension seam)",
579
667
  " hs skills remove <name> Remove a customer-registered skill",
@@ -595,13 +683,12 @@ function usage() {
595
683
  " hs worktree list|alloc|release|reap [--json] [--dry-run] Session/build worktree registry (HS-03DO)",
596
684
  " hs view <surface> [spec-id] [--width N --height N --browser edge|chrome] Open one surface in a chromeless app-mode window",
597
685
  " hs server stop [--json] Stop the spex-console dev server (any terminal)",
598
- " hs source status|preflight|commit|push SpexSource local git operations",
599
- " hs source identity [show|set|clear] Git author identity (set/clear are human-only; see hs source --help)",
600
686
  " hs defer <id> [--until <when>] [--json] Time-shift a spec (defer_until)",
601
687
  " hs undefer <id> [--json] Clear a spec's defer_until",
602
688
  " hs spawn [<id>] [--dry-run] [--json] Launch a fresh /hs-next agent in a new terminal",
603
689
  ' hs capture "<text>"|--clipboard|--file <p> [--add] Capture → console drafts (--add chains a group)',
604
690
  " hs companion start|stop|status|build Desktop companion tray app",
691
+ " hs companion install [--quiet] [--dry-run] Download, verify and run the signed SpexCompanion setup (Windows x64)",
605
692
  " hs companion settings [list|get <k>|set <k> <v>] [--json] Every Companion setting (console-backed)",
606
693
  " hs companion mute|unmute|volume|orb|wake|activation|hotkey [<v>] Setting shortcuts (omit <v> to read)",
607
694
  " hs registry list [--json] List launch registry entries",
@@ -619,6 +706,7 @@ function usage() {
619
706
  " hs next assist|auto <milestone...> --launch Arm a dispatcher run on one or more milestones",
620
707
  " hs model [<seat>] [--launch|--fleet] Every seat and the model set for it",
621
708
  " hs seat list|add|update|disable [--json] Read/add/update/disable a seat in the registry (dev-factory/seats.json)",
709
+ " hs seat set|reset|assignments [--harness] Your own model per SpexFleet seat, this vault (customers: hs seat list|set|reset)",
622
710
  " hs voice add|build|show|set|test|refresh|export Build and manage a coded voice profile",
623
711
  " hs trash [list|restore|empty] The bin spec deletes go into",
624
712
  " hs auto [filename] [--json] Resolve /hs-auto dispatch plan",
@@ -648,6 +736,9 @@ function usage() {
648
736
  " search/scrape/crawl CLI (standalone script,",
649
737
  " not an `hs` verb; also reachable from the",
650
738
  " Companion)",
739
+ " node dev-factory/spexcrawl/firecrawl-cli.mjs search|scrape Firecrawl — finds the",
740
+ " self-hosted Firecrawl in this machine's",
741
+ " Docker (no env var, no API key)",
651
742
  "",
652
743
  "Control plane (Neon — requires a reachable console):",
653
744
  " hs pin get|set <id>|clear Read/set/clear the on-deck pin",
@@ -674,8 +765,7 @@ function usage() {
674
765
  "`hs install --register-vault` without --dry-run in that case refuses instead of",
675
766
  "registering, asking for --target. --attach-vault (satellite mode) is exempt.",
676
767
  "",
677
- ].join("\n"),
678
- );
768
+ ]);
679
769
  }
680
770
 
681
771
  /**
@@ -689,8 +779,11 @@ function usage() {
689
779
  * 2. Published install of a BUNDLED verb — currently `hs-init.ts`,
690
780
  * `hs-install.ts`, `hs-claim.ts`, `hs-check.ts`, `hs-env.ts`,
691
781
  * `hs-connect.ts`, `hs-join.ts`, `hs-vault.ts`, `hs-workspace.ts`,
692
- * `hs-defer.ts`, `hs-vault-pull.ts`, `hs-dev.ts`, `hs-index-rebuild.ts`,
693
- * `hs-index-reconcile.ts` — see `scripts/build-cli-bundle.mjs`'s
782
+ * `hs-defer.ts`, `hs-vault-pull.ts`, `hs-vault-push.ts`, `hs-dev.ts`,
783
+ * `hs-index-rebuild.ts`, `hs-index-reconcile.ts`, `hs-handoff.ts`,
784
+ * `hs-pin.ts`, `hs-events.ts`, `hs-export-events.ts`, `hs-prereq.ts`,
785
+ * `hs-trash.ts`, `hs-trust.ts`, `hs-next-launch.ts`, `hs-revalidate.ts`,
786
+ * `hs-test.ts`, `hs-spawn.ts` — see `scripts/build-cli-bundle.mjs`'s
694
787
  * `ENTRIES` array and doc comment for the authoritative list and why
695
788
  * each qualifies: neither the source nor tsx exists, but
696
789
  * `dist/cli/<name>.mjs` does — run the plain-JS bundle directly with
@@ -778,6 +871,10 @@ function dispatchSpexConsoleEntry(entryFile, args, vaultRoot, extraEnv = {}) {
778
871
  * version.
779
872
  */
780
873
  function runStandaloneDoctor(args, vaultRoot) {
874
+ if (args.includes("--help") || args.includes("-h")) {
875
+ process.stdout.write(`Usage:\n${namespaceHelpLines("doctor").join("\n")}\n`);
876
+ process.exit(0);
877
+ }
781
878
  const jsonOut = args.includes("--json");
782
879
  let version = "0.0.0";
783
880
  try {
@@ -827,6 +924,11 @@ function runStandaloneDoctor(args, vaultRoot) {
827
924
  if (templateRow.status === "fail") report.ok = false;
828
925
  report.checks.push(templateRow);
829
926
 
927
+ // `companion` (HS-056P): is SpexCompanion installed? Optional, so it is never
928
+ // a failure -- missing on Windows is a warn that offers
929
+ // `npx heyspex companion install`; other platforms get a skipped note.
930
+ report.checks.push(companionDoctorRow());
931
+
830
932
  // `connection` ([[HS-W73V-WW9R-TASK-make-both-doctor-shapes-fail-on-cloud]],
831
933
  // invariants 8 and 10): the BUNDLED `hs connect --check --json` from the
832
934
  // invoked cwd, folded by the SAME function the full doctor uses
@@ -891,6 +993,45 @@ function runStandaloneDoctor(args, vaultRoot) {
891
993
  printStandaloneDoctorReport(report, jsonOut);
892
994
  }
893
995
 
996
+ /** Usage for `hs setup`; printed by --help/-h (exit 0) and on a bad target (exit 2). */
997
+ const SETUP_USAGE =
998
+ "Usage:\n" +
999
+ " hs setup claude [--check|--uninstall|--print] Claude Code lifecycle hooks (--print: setup steps; alias claude-code)\n" +
1000
+ " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n" +
1001
+ " hs setup copilot [--check|--uninstall|--print|--hooks|--statusline [--force]] GitHub Copilot (plugin; --hooks for the cloud agent; --statusline opt-in status line)\n" +
1002
+ " hs setup <target> --help This help (never installs anything)\n";
1003
+
1004
+ /**
1005
+ * `hs setup claude --print` (alias `claude-code`): the Claude Code steps for a
1006
+ * repo that uses the heyspex npm package — the sibling of `hs setup copilot
1007
+ * --print`. Text only; writes nothing. The one-source-of-hooks note is backed
1008
+ * by the `yields-to-plugin` marker in the committed launcher
1009
+ * (scripts/lib/customer-kit.mjs LAUNCHER_YIELD_MARKER).
1010
+ */
1011
+ function claudeCodeSetupSteps() {
1012
+ return [
1013
+ "# Using HeySpex in Claude Code",
1014
+ "",
1015
+ "1. Open this repo in Claude Code (run `claude` from the repo root).",
1016
+ "2. On first open Claude Code asks whether to use the project MCP server `heyspex`",
1017
+ " (declared in this repo's .mcp.json). Approve it. No .mcp.json yet? Run `npx hs install`.",
1018
+ " Not signed in on this machine? Run `npx hs login` first.",
1019
+ "3. Check: ask Claude to run `heyspex_preflight`, or run `npx hs doctor`.",
1020
+ "",
1021
+ "Optional: the SpexCode plugin (for Claude Code), from the copy inside node_modules:",
1022
+ " claude plugin marketplace add ./node_modules/heyspex",
1023
+ " claude plugin install heyspex@heyspex-marketplace",
1024
+ " (or in a session: /plugin marketplace add ./node_modules/heyspex, then",
1025
+ " /plugin install heyspex@heyspex-marketplace)",
1026
+ "",
1027
+ "One source of hooks: the plugin and this repo's .claude/heyspex/run-hook.mjs yield to",
1028
+ "each other. When the plugin is enabled on this machine the committed launcher exits",
1029
+ "silently and the plugin runs every hook, so each hook fires once. Set",
1030
+ "HEYSPEX_REPO_HOOKS=1 to keep the repo hooks as the source instead.",
1031
+ "",
1032
+ ].join("\n");
1033
+ }
1034
+
894
1035
  function printStandaloneDoctorReport(report, jsonOut) {
895
1036
  if (jsonOut) {
896
1037
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
@@ -913,14 +1054,115 @@ function printStandaloneDoctorReport(report, jsonOut) {
913
1054
  if (check.human) process.stdout.write(` ${check.human}\n`);
914
1055
  }
915
1056
  process.stdout.write(
916
- "\nNote: the full `hs doctor` (git hooks, Cursor wiring, console secrets, and\n" +
917
- "dozens of other source-checkout-only checks) only ships inside a clone of\n" +
918
- "https://github.com/pwhermanson/workflow-protocol — this is the subset that\n" +
919
- "applies to a published install.\n",
1057
+ "\nNote: this is the setup check for a published install. The full platform\n" +
1058
+ "doctor (git hooks, console secrets and other source-checkout checks) is for\n" +
1059
+ "HeySpex developers only and is not part of the npm package.\n",
920
1060
  );
921
1061
  process.exit(report.ok ? 0 : 1);
922
1062
  }
923
1063
 
1064
+ /** The tray-host subcommands of `hs companion` (bare `hs companion` means start). */
1065
+ const COMPANION_HOST_COMMANDS = new Set(["start", "stop", "status", "build"]);
1066
+
1067
+ /**
1068
+ * The `hs companion` host subcommand to answer with "not distributed through
1069
+ * the npm package yet", or null. Non-null only for start|stop|status|build (or
1070
+ * a bare `hs companion`) when `pkgRoot` has no companion/ source -- an
1071
+ * installed `heyspex` package. Pure apart from the injectable `exists`.
1072
+ * @param {string | undefined} namespace
1073
+ * @param {string | undefined} command
1074
+ * @param {{ pkgRoot?: string, exists?: (p: string) => boolean }} [opts]
1075
+ * @returns {string | null}
1076
+ */
1077
+ export function companionHostWithoutSource(namespace, command, { pkgRoot = PKG_ROOT, exists = existsSync } = {}) {
1078
+ if (namespace !== "companion") return null;
1079
+ const sub = command === undefined ? "start" : command;
1080
+ if (!COMPANION_HOST_COMMANDS.has(sub)) return null;
1081
+ return exists(path.join(pkgRoot, "companion")) ? null : sub;
1082
+ }
1083
+
1084
+ /**
1085
+ * Verbs (`"<namespace>"` or `"<namespace> <sub>"`) a source checkout runs but
1086
+ * the npm package deliberately does not carry, each with the reason a
1087
+ * customer sees. `hs --help` from an installed package hides their lines
1088
+ * (DEV_ONLY_HELP_PREFIXES); invoking one exits 1 with
1089
+ * {@link notInPackageMessage} instead of dispatchSpexConsoleEntry's
1090
+ * "install is incomplete". A verb that IS bundled (scripts/build-cli-bundle.mjs
1091
+ * ENTRIES) never belongs here.
1092
+ */
1093
+ export const NOT_IN_PACKAGE_VERBS = Object.freeze({
1094
+ admin: "platform-operator test-customer minting.",
1095
+ queue: "the SpexFleet build queue runs only on the HeySpex platform.",
1096
+ seat: "SpexFleet seat registry, platform build system only.",
1097
+ agent: "the vendored agent collections it imports from are not shipped.",
1098
+ chat: "the SpexCode chat front needs the SpexCode engine, which is not in the npm package.",
1099
+ "code-watcher": "it needs the HeySpex source tree and its TypeScript toolchain.",
1100
+ harness: "SpexCode gate fixtures live in the SpexCode engine, which is not in the npm package.",
1101
+ hooks: "the SpexCode extension seam is read by the SpexCode engine, which is not in the npm package.",
1102
+ import: "it uses the SpexFleet model router, which is not in the npm package.",
1103
+ milestone: "use the console or the heyspex_* MCP tools to read a milestone.",
1104
+ model: "the SpexFleet seat routing table is not in the npm package.",
1105
+ registry: "the launch registry is managed from the console.",
1106
+ skills: "skill sync and the SpexCode skill registry need a HeySpex source checkout.",
1107
+ voice: "voice profiles feed the SpexVoice Companion, which is not in the npm package yet.",
1108
+ "check judge": "the decorrelated judge seat runs on the HeySpex platform.",
1109
+ factory: "SpexFleet/dev-factory internals are platform-only.",
1110
+ doctrine: "SpexCode Doctrine synthesis internals are platform-only.",
1111
+ bench: "SpexCode bench internals are platform-only.",
1112
+ server: "the spex-console dev server is part of the HeySpex source checkout; the hosted console is app.heyspex.com.",
1113
+ });
1114
+
1115
+ /**
1116
+ * Is this `hs <verb> ... --help|-h` (a help flag after the verb, before any
1117
+ * `--` end-of-options marker)? Bare `hs --help` is handled before dispatch.
1118
+ * @param {string[]} argv the argv after the global --vault flag is removed
1119
+ */
1120
+ export function isHelpInvocation(argv) {
1121
+ for (const a of argv.slice(1)) {
1122
+ if (a === "--") return false;
1123
+ if (a === "--help" || a === "-h") return true;
1124
+ }
1125
+ return false;
1126
+ }
1127
+
1128
+ /**
1129
+ * The NOT_IN_PACKAGE_VERBS key this invocation hits from an installed
1130
+ * package, or null. Pure apart from `installed`.
1131
+ * @param {string | undefined} namespace
1132
+ * @param {string | undefined} command
1133
+ * @param {{ installed?: boolean }} [opts]
1134
+ * @returns {string | null}
1135
+ */
1136
+ export function notInPackageVerb(namespace, command, { installed = IS_INSTALLED_PACKAGE } = {}) {
1137
+ if (!installed || !namespace) return null;
1138
+ const two = command ? `${namespace} ${command}` : null;
1139
+ if (two && Object.hasOwn(NOT_IN_PACKAGE_VERBS, two)) return two;
1140
+ return Object.hasOwn(NOT_IN_PACKAGE_VERBS, namespace) ? namespace : null;
1141
+ }
1142
+
1143
+ /** @param {string} key a NOT_IN_PACKAGE_VERBS key */
1144
+ export function notInPackageMessage(key) {
1145
+ return (
1146
+ `hs ${key}: not available in the npm package — ${NOT_IN_PACKAGE_VERBS[key]}\n` +
1147
+ ` It runs only inside a HeySpex source checkout. \`hs --help\` lists every verb this install can run.\n`
1148
+ );
1149
+ }
1150
+
1151
+ /**
1152
+ * Does `hs seat <command>` go to the customer seat-assignment CLI
1153
+ * (vendor/heyspex-mcp/seat-assign-cli.mjs, epic HS-K9L7)? Installed package:
1154
+ * list|assignments|set|reset|defaults|report-fallback|help (and a bare `hs seat`).
1155
+ * Source checkout: the same minus `list`/bare, which stay the dev-factory
1156
+ * registry (`hs-seat.ts`).
1157
+ */
1158
+ export function routesToSeatAssignCli(namespace, command, { installed = IS_INSTALLED_PACKAGE } = {}) {
1159
+ if (namespace !== "seat") return false;
1160
+ if (installed) {
1161
+ return command === undefined || ["list", "assignments", "set", "reset", "defaults", "report-fallback", "help", "--help", "-h"].includes(command);
1162
+ }
1163
+ return ["assignments", "set", "reset", "defaults", "report-fallback"].includes(command);
1164
+ }
1165
+
924
1166
  function main(argv) {
925
1167
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
926
1168
  usage();
@@ -952,12 +1194,66 @@ function main(argv) {
952
1194
  const command = argv[1];
953
1195
  const rest = argv.slice(2);
954
1196
 
955
- const {
956
- vaultRoot,
957
- usedRegistryFallback,
958
- error: vaultError,
959
- brokenPin,
960
- } = resolveVaultRootForMain(process.cwd(), process.env);
1197
+ // `hs seat list|set|reset|defaults|report-fallback` — the signed-in user's OWN
1198
+ // seat models (HS-K9L7). Installed package: these subcommands. Source checkout:
1199
+ // set|reset|defaults|assignments|report-fallback (list stays the registry).
1200
+ if (routesToSeatAssignCli(namespace, command)) {
1201
+ import(pathToFileURL(path.join(PKG_ROOT, "vendor", "heyspex-mcp", "seat-assign-cli.mjs")).href)
1202
+ .then((m) => m.runSeatAssignCli(argv.slice(1)))
1203
+ .then((code) => process.exit(code));
1204
+ return;
1205
+ }
1206
+
1207
+ // A source-checkout-only verb from an installed package: say so before vault
1208
+ // resolution can refuse with an unrelated binding error.
1209
+ const missingVerb = notInPackageVerb(namespace, command);
1210
+ if (missingVerb) {
1211
+ process.stderr.write(notInPackageMessage(missingVerb));
1212
+ process.exit(1);
1213
+ }
1214
+ if (
1215
+ HELP_FROM_USAGE_NAMESPACES.has(namespace) &&
1216
+ (command === "--help" || command === "-h")
1217
+ ) {
1218
+ const lines = namespaceHelpLines(namespace);
1219
+ if (lines.length) {
1220
+ process.stdout.write(`Usage:\n${lines.join("\n")}\n`);
1221
+ process.exit(0);
1222
+ }
1223
+ }
1224
+
1225
+ // `hs companion start|stop|status|build` from an installed package (no
1226
+ // companion/ source in the tarball) and `hs companion install` anywhere are
1227
+ // machine-local and need no vault: run them BEFORE vault resolution can
1228
+ // refuse with "not bound to a vault", which sends a customer to fix a binding
1229
+ // that would not help. scripts/hs-companion.mjs drives an installed
1230
+ // SpexCompanion, or offers `npx heyspex companion install` when there is none
1231
+ // (HS-056P; this replaced the "not distributed through the npm package yet" answer).
1232
+ const companionSub = companionHostWithoutSource(namespace, command);
1233
+ if (companionSub || (namespace === "companion" && command === "install")) {
1234
+ const companionArgs = command ? [command, ...rest] : ["start", ...rest];
1235
+ const child = spawn(process.execPath, [spawnTargetPath("companion"), ...companionArgs], {
1236
+ cwd: INVOKED_CWD,
1237
+ stdio: "inherit",
1238
+ });
1239
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
1240
+ child.on("error", (err) => {
1241
+ process.stderr.write(`hs companion: ${err.message}\n`);
1242
+ process.exit(1);
1243
+ });
1244
+ return;
1245
+ }
1246
+
1247
+ const resolved = resolveVaultRootForMain(process.cwd(), process.env);
1248
+ const { usedRegistryFallback, error: vaultError } = resolved;
1249
+ // `hs <verb> --help` must always print help and exit 0, bound vault or not
1250
+ // (install QC: ~28 verbs refused with VAULT_UNBOUND from an unbound folder).
1251
+ // Help never reads or writes a vault, so an unresolved vault falls back to the
1252
+ // invocation folder and the vault refusals below (broken pin, no vault,
1253
+ // uncloned satellite) are skipped; each verb's own handler prints its help.
1254
+ const helpRequested = isHelpInvocation(argv);
1255
+ const vaultRoot = resolved.vaultRoot || (helpRequested ? INVOKED_CWD : resolved.vaultRoot);
1256
+ const brokenPin = helpRequested ? null : resolved.brokenPin;
961
1257
  // The registry pointer is never consulted ([[HS-TYJA]]), so the only P1
962
1258
  // refusal left is a satellite pin that cannot be honoured; onboarding /
963
1259
  // registry verbs and `hs doctor` still run so the pin can be repaired.
@@ -1066,7 +1362,18 @@ function main(argv) {
1066
1362
 
1067
1363
  // `hs setup claude|cursor` — install agent lifecycle hooks into the vault root.
1068
1364
  if (namespace === "setup") {
1069
- const agent = command?.toLowerCase();
1365
+ const agent = command?.toLowerCase() === "claude-code" ? "claude" : command?.toLowerCase();
1366
+ // --help/-h anywhere (`hs setup --help`, `hs setup copilot --help`) prints usage and
1367
+ // exits 0 BEFORE any installer is spawned: the installers ignore unknown flags, so a
1368
+ // forwarded --help used to run the real install (Copilot: into ~/.copilot).
1369
+ if ([command, ...rest].some((a) => a === "--help" || a === "-h")) {
1370
+ process.stdout.write(SETUP_USAGE);
1371
+ process.exit(0);
1372
+ }
1373
+ if (agent === "claude" && rest.includes("--print")) {
1374
+ process.stdout.write(claudeCodeSetupSteps());
1375
+ process.exit(0);
1376
+ }
1070
1377
  const SPAWN_KEYS = {
1071
1378
  claude: "setup:claude",
1072
1379
  cursor: "setup:cursor",
@@ -1074,10 +1381,7 @@ function main(argv) {
1074
1381
  };
1075
1382
  if (!agent || !Object.hasOwn(SPAWN_KEYS, agent)) {
1076
1383
  process.stderr.write(
1077
- `hs setup: expected \`claude\`, \`cursor\` or \`copilot\` (got \`${command ?? ""}\`)\n` +
1078
- " hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
1079
- " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n" +
1080
- " hs setup copilot [--check|--uninstall|--print|--hooks|--statusline [--force]] GitHub Copilot (plugin; --hooks for the cloud agent; --statusline opt-in status line)\n",
1384
+ `hs setup: expected \`claude\`, \`cursor\` or \`copilot\` (got \`${command ?? ""}\`)\n` + SETUP_USAGE,
1081
1385
  );
1082
1386
  process.exit(2);
1083
1387
  }
@@ -1095,8 +1399,10 @@ function main(argv) {
1095
1399
  }
1096
1400
  }
1097
1401
  const script = spawnTargetPath(SPAWN_KEYS[agent]);
1098
- // Copilot sets up the repo you are standing in, never the (possibly sibling) vault root.
1099
- const target = agent === "copilot" ? process.cwd() : vaultRoot || process.cwd();
1402
+ // Every agent sets up the PROJECT you are standing in (nearest .git /
1403
+ // heyspex.yaml at or above cwd) -- never the (possibly sibling) vault
1404
+ // root, and never this package's own folder under node_modules/.
1405
+ const target = findProjectRoot(INVOKED_CWD);
1100
1406
  const child = spawn(process.execPath, [script, ...setupFlags], {
1101
1407
  cwd: target,
1102
1408
  stdio: "inherit",
@@ -1315,6 +1621,22 @@ function main(argv) {
1315
1621
  return;
1316
1622
  }
1317
1623
 
1624
+ // `hs trust list` / `hs trust revoke [path]` — standalone management of the machine-local
1625
+ // folder-trust store `hs chat`'s first-run prompt writes to
1626
+ // ([[HS-25AJ-8CMW-TASK-folder-trust-revoke-through-the]], deferred from
1627
+ // [[HS-ZBEB-8CMW-TASK-folder-trust-for-spexcode-first-run]], which shipped only `hs chat
1628
+ // --revoke-trust`). Thin CLI verb — same shell shape as `chat`/`hooks`/`bench` below; the real
1629
+ // logic lives in `spex-console/src/lib/cli/trust-cli.ts` over `folder-trust.ts`'s pure
1630
+ // functions. Forwards the full argv; the entrypoint drops the leading `trust` token.
1631
+ // Machine-local, never acts on a vault, so it runs ABOVE the hard vault gate
1632
+ // (it is in POINTER_FALLBACK_NAMESPACES): since HS-TYJA (a2bbb0698) dropped the
1633
+ // registry-pointer fallback, `hs trust revoke` from an unbound dir hit
1634
+ // VAULT_UNBOUND here.
1635
+ if (namespace === "trust") {
1636
+ dispatchSpexConsoleEntry("hs-trust.ts", argv, vaultRoot || process.cwd());
1637
+ return;
1638
+ }
1639
+
1318
1640
  if (!vaultRoot) {
1319
1641
  process.stderr.write(noVaultMessage(vaultError));
1320
1642
  process.exit(1);
@@ -1325,6 +1647,7 @@ function main(argv) {
1325
1647
  // vault's files to disk must never scaffold specs/ into a code repo.
1326
1648
  if (
1327
1649
  ["pull", "push", "index"].includes(namespace) &&
1650
+ !helpRequested &&
1328
1651
  readMarkerSatellite(vaultRoot) &&
1329
1652
  !existsSync(path.join(vaultRoot, "specs"))
1330
1653
  ) {
@@ -1936,18 +2259,6 @@ function main(argv) {
1936
2259
  return;
1937
2260
  }
1938
2261
 
1939
- // `hs trust list` / `hs trust revoke [path]` — standalone management of the machine-local
1940
- // folder-trust store `hs chat`'s first-run prompt writes to
1941
- // ([[HS-25AJ-8CMW-TASK-folder-trust-revoke-through-the]], deferred from
1942
- // [[HS-ZBEB-8CMW-TASK-folder-trust-for-spexcode-first-run]], which shipped only `hs chat
1943
- // --revoke-trust`). Thin CLI verb — same shell shape as `chat`/`hooks`/`bench` above; the real
1944
- // logic lives in `spex-console/src/lib/cli/trust-cli.ts` over `folder-trust.ts`'s pure
1945
- // functions. Forwards the full argv; the entrypoint drops the leading `trust` token.
1946
- if (namespace === "trust") {
1947
- dispatchSpexConsoleEntry("hs-trust.ts", argv, vaultRoot || process.cwd());
1948
- return;
1949
- }
1950
-
1951
2262
  // `hs doctrine extract|learn|show` — SpexCode Doctrine corpus reader + synthesis
1952
2263
  // ([[HS-TWTC-8CMW-TASK-dr1-doctrine-corpus-reader-stream-every]],
1953
2264
  // [[HS-YMPY-8CMW-TASK-dr2-doctrine-synthesis-a-routed]] of epic
package/bin/spexcode.mjs CHANGED
@@ -33,6 +33,8 @@ import { readFileSync, realpathSync } from "node:fs";
33
33
  import path from "node:path";
34
34
  import process from "node:process";
35
35
  import { fileURLToPath } from "node:url";
36
+ import { PRODUCT_SEATS, canonicalSeatId, defaultModelForSeat } from "../vendor/heyspex-mcp/seat-defaults.mjs";
37
+ import { detectHarness } from "../spexharness/src/adapters/harness-detect.mjs";
36
38
 
37
39
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
40
 
@@ -200,6 +202,29 @@ export function resolveSeatGuard(argv, seatsRegistry) {
200
202
  };
201
203
  }
202
204
 
205
+ /**
206
+ * HS-MDZB (epic HS-C65J): a customer install ships no dev-factory/seats.json (that file is the
207
+ * owner's own fleet). When it is ABSENT, a product seat id is allowed and runs on the harness
208
+ * default (Claude tiers under Claude Code, `auto` under Copilot); an unknown id is still refused.
209
+ * Pure: env in, a guard verdict plus a one-line notice out.
210
+ * @returns {{allowed: boolean, message?: string, notice?: string}}
211
+ */
212
+ export function resolveSeatGuardWithoutRegistry(seatId, env = {}) {
213
+ const id = canonicalSeatId(seatId);
214
+ const known = PRODUCT_SEATS.some((s) => s.id === id);
215
+ if (!known) {
216
+ return {
217
+ allowed: false,
218
+ message: `spexcode: refusing --seat "${seatId}" — not a HeySpex seat (known: ${PRODUCT_SEATS.map((s) => s.id).join(", ")}).`,
219
+ };
220
+ }
221
+ const d = defaultModelForSeat(id, detectHarness(env).harness);
222
+ const notice = d.model
223
+ ? `spexcode: seat "${id}" uses the ${d.harness} default model "${d.model}".`
224
+ : `spexcode: seat "${id}" has no default model (no harness detected; set HEYSPEX_HARNESS=claude-code or copilot).`;
225
+ return { allowed: true, notice };
226
+ }
227
+
203
228
  /** Load + JSON.parse `dev-factory/seats.json`. Isolated so tests can stub it. */
204
229
  export function loadSeatsRegistry(seatsJsonPath = SEATS_JSON_PATH) {
205
230
  return JSON.parse(readFileSync(seatsJsonPath, "utf8"));
@@ -273,6 +298,12 @@ export async function runSpexcode(argv, deps = {}) {
273
298
  try {
274
299
  registry = loadRegistry();
275
300
  } catch (err) {
301
+ if (err && err.code === "ENOENT") {
302
+ const guard = resolveSeatGuardWithoutRegistry(seatId, deps.env ?? process.env);
303
+ stderr.write((guard.allowed ? guard.notice : guard.message) + "\n");
304
+ if (!guard.allowed) return 2;
305
+ return runForward(argv, { hsPath, spawnFn });
306
+ }
276
307
  stderr.write(
277
308
  `spexcode: could not read dev-factory/seats.json to check --seat "${seatId}": ` +
278
309
  `${err && err.message ? err.message : err}\n`,