heyspex 0.1.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.
Files changed (104) hide show
  1. package/README.md +157 -0
  2. package/bin/hs.mjs +1028 -0
  3. package/bin/postinstall.mjs +88 -0
  4. package/dev-factory/lib/fleet-readiness.mjs +274 -0
  5. package/dev-factory/lib/fleet-state.mjs +250 -0
  6. package/dev-factory/lib/sufficiency-score.mjs +441 -0
  7. package/dist/cli/hs-check.mjs +747 -0
  8. package/dist/cli/hs-claim.mjs +541 -0
  9. package/dist/cli/hs-connect.mjs +3436 -0
  10. package/dist/cli/hs-defer.mjs +8455 -0
  11. package/dist/cli/hs-env.mjs +36845 -0
  12. package/dist/cli/hs-init.mjs +1145 -0
  13. package/dist/cli/hs-install.mjs +1390 -0
  14. package/dist/cli/hs-join.mjs +38553 -0
  15. package/dist/cli/hs-vault-pull.mjs +7782 -0
  16. package/dist/cli/hs-vault.mjs +4166 -0
  17. package/dist/cli/hs-workspace.mjs +2898 -0
  18. package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
  19. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
  20. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
  21. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
  22. package/docs/mcp-client-config.md +200 -0
  23. package/package.json +124 -0
  24. package/scripts/check-mcp-freshness.mjs +379 -0
  25. package/scripts/dev-server-status.mjs +722 -0
  26. package/scripts/gateway-skills-lib.mjs +421 -0
  27. package/scripts/lib/console-url-resolver.mjs +152 -0
  28. package/scripts/lib/dev-mode.mjs +369 -0
  29. package/scripts/lib/hs-link.mjs +154 -0
  30. package/scripts/lib/hs-source-commit.mjs +207 -0
  31. package/scripts/lib/hs-source-log.mjs +366 -0
  32. package/scripts/lib/hs-source-preflight.mjs +402 -0
  33. package/scripts/lib/hs-source-push.mjs +307 -0
  34. package/scripts/lib/hs-source-secret-scan.mjs +263 -0
  35. package/scripts/lib/standalone-doctor.mjs +89 -0
  36. package/scripts/vault-id-lib.mjs +173 -0
  37. package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
  38. package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
  39. package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
  40. package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
  41. package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
  42. package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
  43. package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
  44. package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
  45. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
  46. package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
  47. package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
  48. package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
  49. package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
  50. package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
  51. package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
  52. package/vendor/heyspex-mcp/cli.mjs +15754 -0
  53. package/vendor/heyspex-mcp/config-settings.mjs +419 -0
  54. package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
  55. package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
  56. package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
  57. package/vendor/heyspex-mcp/design-tools.mjs +449 -0
  58. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
  59. package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
  60. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
  61. package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
  62. package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
  63. package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
  64. package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
  65. package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
  66. package/vendor/heyspex-mcp/next-review.mjs +548 -0
  67. package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
  68. package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
  69. package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
  70. package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
  71. package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
  72. package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
  73. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  74. package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
  75. package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
  76. package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
  77. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
  78. package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
  79. package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
  80. package/vendor/heyspex-mcp/read-path.mjs +138 -0
  81. package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
  82. package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
  83. package/vendor/heyspex-mcp/register-spec.mjs +417 -0
  84. package/vendor/heyspex-mcp/research-focus.mjs +476 -0
  85. package/vendor/heyspex-mcp/research-tools.mjs +109 -0
  86. package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
  87. package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
  88. package/vendor/heyspex-mcp/review-focus.mjs +55 -0
  89. package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
  90. package/vendor/heyspex-mcp/run-front.mjs +60 -0
  91. package/vendor/heyspex-mcp/scheduler.mjs +813 -0
  92. package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
  93. package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
  94. package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
  95. package/vendor/heyspex-mcp/split-research.mjs +832 -0
  96. package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
  97. package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
  98. package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
  99. package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
  100. package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
  101. package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
  102. package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
  103. package/vendor/heyspex-mcp/why-now.mjs +64 -0
  104. package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
package/bin/hs.mjs ADDED
@@ -0,0 +1,1028 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * hs — HeySpex vault CLI (foundation for indexer / future commands).
4
+ *
5
+ * Per [[HS-TASK-DE5Z-hs-index-rebuild]] and [[HS-EPIC-0027-implement-hs-bd-successor-m4-cli-mcp]].
6
+ *
7
+ * Dispatcher only: parses `hs <namespace> <command>`, locates the vault root,
8
+ * and delegates to the matching implementation inside `spex-console/bin/`.
9
+ *
10
+ * Implementations live in spex-console/ because they import the TypeScript indexer
11
+ * service directly. tsx (a workspace devDep, hoisted to the repo-root
12
+ * node_modules) executes the .ts entrypoints.
13
+ */
14
+ import { spawn } from "node:child_process";
15
+ import { existsSync, readFileSync, statSync } from "node:fs";
16
+ import path from "node:path";
17
+ import process from "node:process";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+
20
+ import { runtimeFilesCheckRow } from "../scripts/lib/standalone-doctor.mjs";
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const PKG_ROOT = path.resolve(__dirname, "..");
24
+
25
+ // Captured before anything in this process could ever chdir (it doesn't
26
+ // today, but every dispatched entry below is spawned with its own `cwd`
27
+ // override — see dispatchSpexConsoleEntry). This is the ONE true record of
28
+ // where the caller actually invoked `hs` from; forward it (HS_INVOKED_CWD)
29
+ // to every dispatched entry whose cwd-derived logic must resolve the
30
+ // caller's repo, not the platform checkout dir the entry is spawned in
31
+ // (dispatchSpexConsoleEntry's spawns run with cwd=spex-console/; several raw
32
+ // spawns below run with cwd=PKG_ROOT). Set on process.env itself (rather than
33
+ // threaded through each individual spawn call) so it reaches EVERY dispatch —
34
+ // both the ones that build `env` by spreading `...process.env` and the ones
35
+ // that pass no `env` override at all and so inherit process.env verbatim —
36
+ // without having to touch every call site. Per
37
+ // [[HS-NXNP-36X3-DIAG-the-hs-launcher-loses-the-caller-s]]: `hs connect` was
38
+ // the first verb fixed this way (its own `HS_INVOKED_CWD` env entry below is
39
+ // now redundant with this but kept — see the connect dispatch — as it costs
40
+ // nothing and documents the specific need).
41
+ const INVOKED_CWD = process.cwd();
42
+ process.env.HS_INVOKED_CWD = INVOKED_CWD;
43
+
44
+ /**
45
+ * Walk up from `start` until we find a directory containing `specs/`.
46
+ * That directory is the vault root.
47
+ */
48
+ function findVaultRoot(start) {
49
+ let dir = path.resolve(start);
50
+ while (true) {
51
+ const candidate = path.join(dir, "specs");
52
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
53
+ return dir;
54
+ }
55
+ const parent = path.dirname(dir);
56
+ if (parent === dir) return null;
57
+ dir = parent;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Extract the global `hs --vault <id> <subcommand>` one-shot override
63
+ * ([[HS-Q1HZ-TASK-hs-vault-cli-list-register-switch-active]]). `--vault`
64
+ * (or `--vault=<id>`) may appear anywhere in argv -- it is stripped out here
65
+ * so the remaining tokens parse as a normal `hs <namespace> <command>`
66
+ * invocation; the extracted id is forwarded via `HS_VAULT_ID_OVERRIDE` env
67
+ * to whatever entry gets dispatched. This never mutates the persisted active
68
+ * pointer -- it only feeds `resolveActiveVaultRoot`'s `options.vaultId` for
69
+ * that one invocation (consumed today by `hs vault active`).
70
+ */
71
+ function extractGlobalVaultFlag(argv) {
72
+ const idx = argv.findIndex(
73
+ (a) => a === "--vault" || a.startsWith("--vault="),
74
+ );
75
+ if (idx === -1) return { vaultIdOverride: undefined, rest: argv };
76
+ const arg = argv[idx];
77
+ if (arg.startsWith("--vault=")) {
78
+ return {
79
+ vaultIdOverride: arg.slice("--vault=".length),
80
+ rest: [...argv.slice(0, idx), ...argv.slice(idx + 1)],
81
+ };
82
+ }
83
+ return {
84
+ vaultIdOverride: argv[idx + 1],
85
+ rest: [...argv.slice(0, idx), ...argv.slice(idx + 2)],
86
+ };
87
+ }
88
+
89
+ function usage() {
90
+ process.stdout.write(
91
+ [
92
+ "hs — HeySpex vault CLI",
93
+ "",
94
+ "Usage:",
95
+ " hs login [--console-url <url>] [--label <t>] [--json] Sign in via your browser (no code to type)",
96
+ " hs init [--console-url <url>] [--force] Write the heyspex.yaml vault marker",
97
+ " hs install [--target <dir>] [--platform <dir>] Bootstrap vault + Cursor/Claude MCP",
98
+ " hs invite [<name>] Mint a short pairing code (other computer: hs join CODE)",
99
+ " hs join <code> Join with a pairing code (LAN auto-find; no IP to type)",
100
+ " hs join [<repo>] [--dir d] [--code c] [--at url] [--profile p] [--skip steps] [--json] [--yes] Fresh-machine onboarding (clone → toolchain → deps → doctor → agent layer → secrets → verify)",
101
+ " hs vault list|register|switch|active Manage registered vaults + active pointer",
102
+ " hs workspace list|create|switch Manage registered workspaces (company containers)",
103
+ " hs connect [--dry-run] [--json] | --check [--json] Detect+scaffold+register this repo (or read-only check)",
104
+ " hs --vault <id> <subcommand> One-shot vault override for this invocation",
105
+ " hs doctor [--fix] [--json] [--check-gateway] Check every install surface; --fix repairs what it can",
106
+ " hs gateway validate [slug] [--json] Validate user-authored gateway skills (.heyspex/skills/user/)",
107
+ " hs mode on|off|status|toggle Master switch: HeySpex layer on or off (restart Claude Code)",
108
+ " hs dev [mode:local|mode:cloud|mode:prod] Dev/prod mode + local/cloud reliance (status when bare)",
109
+ " hs setup claude|cursor [--check|--uninstall] Install agent lifecycle hooks",
110
+ " hs skills sync|check Propagate skills to Cursor/Codex/Claude (cwd-safe)",
111
+ " hs index rebuild [--verbose] [--dry-run] Full idempotent rebuild of the index",
112
+ " hs pull [--apply] [--only <id>] [--overwrite] Bring cloud-written specs down to the disk vault (reports unless --apply)",
113
+ " hs milestone show <id> [--json] Show a milestone (flags needs-breakdown when epicless)",
114
+ " hs factory status|fleet|results [--json] Dev-Factory read-only status (fleet, routing, calibrations, results)",
115
+ " hs factory run status [--json] Live assisted/autonomous dispatcher run (pids, queue, stop hints)",
116
+ " hs code-watcher scan [--json] Capture build-correctness failures (tsc) → Resolution",
117
+ " hs server status [--json] Ground-truth dev-server status (running? which port[s]?)",
118
+ " hs view <surface> [spec-id] [--width N --height N --browser edge|chrome] Open one surface in a chromeless app-mode window",
119
+ " hs server stop [--json] Stop the spex-console dev server (any terminal)",
120
+ " hs defer <id> [--until <when>] [--json] Time-shift a spec (defer_until)",
121
+ " hs undefer <id> [--json] Clear a spec's defer_until",
122
+ " hs spawn [<id>] [--dry-run] [--json] Launch a fresh /hs-next agent in a new terminal",
123
+ " hs capture \"<text>\"|--clipboard|--file <p> [--add] Capture → console drafts (--add chains a group)",
124
+ " hs companion start|stop|status|build Desktop companion tray app",
125
+ " hs registry list [--json] List launch registry entries",
126
+ " hs env init|set|list|run|export|rotate Encrypted env vault (ADR-0020)",
127
+ " hs env profiles | key export|import Named profiles + second-machine key transfer",
128
+ " hs env grant add|list|remove | key generate Contractor/CI decrypt grants (ADR-0021)",
129
+ " hs settings list|get|set Read/change workflow settings (.heyspex/config.yaml)",
130
+ " hs classifier stats [--json] [--recent N] Intent Classifier decision stats (read-only)",
131
+ " hs next [spec_id] [--summary] [--json] Resolve on-deck brief (MCP fallback)",
132
+ " hs auto [filename] [--json] Resolve /hs-auto dispatch plan",
133
+ " hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json] One-shot MCP tool",
134
+ " hs mcp pair <code> [--console-url <url>] Redeem a console pairing code (cross-machine fallback for hs login)",
135
+ " hs test run <id|manifest> [--format ...] Run open-format API tests (Hurl/.http) per AC",
136
+ " hs test gate [dir] [--report <j>] [--threshold n] Mutation-testing gate — reject suites that kill no mutants",
137
+ " hs test conform <spec> --url <u> | --report <x> OpenAPI conformance / docs↔impl drift (Schemathesis)",
138
+ " hs check run <id|manifest> [--json] Verify acceptance criteria vs the frozen checks manifest (per-AC pass/fail)",
139
+ " hs revalidate [--json] [--apply] Re-run the frozen checks of already-stamped work; report what has decayed",
140
+ " hs agent import <collection> <role> Import a community agent into a type:agent vault spec",
141
+ " hs agent list [collection] List vendored agent collections / roles",
142
+ "",
143
+ "Control plane (Neon — requires a reachable console):",
144
+ " hs pin get|set <id>|clear Read/set/clear the on-deck pin",
145
+ " hs claim acquire|release <id> | list Atomic spec claims",
146
+ " hs claim progress <id> --stage <verb> Report live work-stage",
147
+ " hs handoff record|load [--lane <k>] Resume-context coordination",
148
+ " hs events show <id> [--kind --since] Per-spec audit log",
149
+ " hs export-events [--since <date>] Idempotent audit export → specs/events/",
150
+ " hs prereq <blocked-id> [<action> <prereq>] Cross-milestone prerequisite resolution",
151
+ " hs queue submit|next|claim|complete|fail|list Fleet-queue dispatch (Neon, direct — no console hop)",
152
+ "",
153
+ " hs <verb> --help Per-verb help",
154
+ " hs --help Show this help",
155
+ " hs --version Show CLI version",
156
+ "",
157
+ "Vault root is detected by walking up from the current directory until a",
158
+ "specs/ folder is found. Override with HS_VAULT_ROOT.",
159
+ "",
160
+ ].join("\n"),
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Run a spex-console-side TypeScript entrypoint (under `spex-console/bin/`) via
166
+ * tsx, passing `args` and the resolved vault root. Used by every `hs` verb whose
167
+ * implementation imports the spex-console TypeScript directly.
168
+ *
169
+ * Three cases, in order ([[HS-JOQX-0JNK-TASK-published-package-runs-hs-init-and-hs]]):
170
+ * 1. Monorepo checkout: `.ts` source AND `tsx` both present — run via tsx,
171
+ * exactly as before this task.
172
+ * 2. Published install of a BUNDLED verb (currently only `hs-init.ts` /
173
+ * `hs-install.ts` — see `scripts/build-cli-bundle.mjs`'s doc comment
174
+ * for why only these two): neither the source nor tsx exists, but
175
+ * `dist/cli/<name>.mjs` does — run the plain-JS bundle directly with
176
+ * `node`, no tsx/TypeScript involved.
177
+ * 3. Neither exists: the package install is incomplete (a verb this
178
+ * package version never bundled, or a broken install) — same exit
179
+ * code as before, reworded.
180
+ */
181
+ function dispatchSpexConsoleEntry(entryFile, args, vaultRoot, extraEnv = {}) {
182
+ const consoleDir = path.join(PKG_ROOT, "spex-console");
183
+ const tsEntry = path.join(consoleDir, "bin", entryFile);
184
+ const tsxEntry = path.join(
185
+ PKG_ROOT,
186
+ "node_modules",
187
+ "tsx",
188
+ "dist",
189
+ "cli.mjs",
190
+ );
191
+ const bundleEntry = path.join(
192
+ PKG_ROOT,
193
+ "dist",
194
+ "cli",
195
+ entryFile.replace(/\.ts$/, ".mjs"),
196
+ );
197
+
198
+ const spawnEnv = { ...process.env, HS_VAULT_ROOT: vaultRoot, ...extraEnv };
199
+ let child;
200
+
201
+ if (existsSync(tsEntry) && existsSync(tsxEntry)) {
202
+ // Case 1 — monorepo: invoke tsx's ESM entry directly with node. This
203
+ // avoids npm's .cmd shim (which triggers Node 24's `shell:true`
204
+ // deprecation warning on Windows) and works identically across
205
+ // platforms. tsx is hoisted to the repo-root node_modules by the npm
206
+ // workspace install, not spex-console/node_modules.
207
+ child = spawn(process.execPath, [tsxEntry, tsEntry, ...args], {
208
+ cwd: consoleDir,
209
+ stdio: "inherit",
210
+ env: spawnEnv,
211
+ });
212
+ } else if (existsSync(bundleEntry)) {
213
+ // Case 2 — published install, verb was bundled: plain node, no tsx/TS.
214
+ // cwd is the CALLER's real invocation directory (not a spex-console/
215
+ // that doesn't exist in a published install) so a relative path the
216
+ // caller typed (e.g. `hs init --customer-vault vault`) resolves the
217
+ // same way it would for any other CLI the caller ran from that shell.
218
+ child = spawn(process.execPath, [bundleEntry, ...args], {
219
+ cwd: INVOKED_CWD,
220
+ stdio: "inherit",
221
+ env: spawnEnv,
222
+ });
223
+ } else {
224
+ // Case 3 — package incomplete: this verb has neither a monorepo source
225
+ // (with tsx) nor a bundled fallback.
226
+ process.stderr.write(
227
+ `hs: this package install is incomplete — no implementation for ` +
228
+ `\`${entryFile}\` (checked ${tsEntry}, ${bundleEntry}).\n` +
229
+ ` If you're in a source checkout, run \`npm install\` first.\n` +
230
+ ` If you installed the published \`heyspex\` package, this verb ` +
231
+ `is not yet available outside a source checkout.\n`,
232
+ );
233
+ process.exit(1);
234
+ }
235
+
236
+ child.on("exit", (code, signal) => {
237
+ if (signal) {
238
+ process.exit(1);
239
+ }
240
+ process.exit(code ?? 0);
241
+ });
242
+ child.on("error", (err) => {
243
+ process.stderr.write(`hs: failed to spawn ${entryFile}: ${err.message}\n`);
244
+ process.exit(1);
245
+ });
246
+ }
247
+
248
+ /**
249
+ * `hs doctor` when running from a published-package install (no
250
+ * `scripts/heyspex-doctor.mjs` on disk — that script only ships inside a
251
+ * source clone; see the dispatch site for why). Checks the one thing that
252
+ * genuinely applies here and is exactly what
253
+ * [[HS-BFXF-WBJU-DIAG-hs-never-resolves-on-a-fresh-machine]] is about: did
254
+ * `npm install` actually put the sibling `heyspex` / `heyspex-mcp` bins on
255
+ * PATH alongside this one. `hs` itself trivially "resolved" (this process IS
256
+ * it), so there is nothing to check about `hs` other than reporting its
257
+ * version.
258
+ */
259
+ function runStandaloneDoctor(args) {
260
+ const jsonOut = args.includes("--json");
261
+ let version = "0.0.0";
262
+ try {
263
+ version =
264
+ JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"))
265
+ .version || version;
266
+ } catch {
267
+ // fall through with the placeholder
268
+ }
269
+
270
+ const report = {
271
+ mode: "published-install",
272
+ version,
273
+ binPath: process.argv[1],
274
+ ok: true,
275
+ checks: [],
276
+ };
277
+ report.checks.push({
278
+ id: "hs-resolved",
279
+ status: "ok",
280
+ detail: `hs ${version} → ${process.argv[1]}`,
281
+ });
282
+
283
+ // `runtime-files` ([[HS-67ES-0JNK-TASK-fresh-install-honesty-statusline-only]]
284
+ // AC3): verifies the package's own runtime files (dynamic-imported/spawned
285
+ // by the MCP server and `hs source`, plus the two `dist/cli` bundles `hs
286
+ // init`/`hs install` dispatch to) actually exist next to THIS `bin/hs.mjs`
287
+ // — the check `hs doctor` did not have before, so a broken package (a
288
+ // runtime file missing from `package.json` `files`) passed every existing
289
+ // check and only surfaced on the customer's FIFTH command
290
+ // (`heyspex_preflight`'s `MODULE_NOT_FOUND`), not the first.
291
+ const runtimeFilesRow = runtimeFilesCheckRow(PKG_ROOT);
292
+ if (runtimeFilesRow.status !== "ok") report.ok = false;
293
+ report.checks.push(runtimeFilesRow);
294
+
295
+ const hsLinkLib = path.join(PKG_ROOT, "scripts", "lib", "hs-link.mjs");
296
+ if (existsSync(hsLinkLib)) {
297
+ // Deliberately synchronous top-level work below via a nested async IIFE
298
+ // — `missingSiblingBins` is a plain sync fs walk, but `import()` itself
299
+ // is async and this function must stay callable from a non-async call
300
+ // site without turning the whole dispatcher async.
301
+ import(pathToFileURL(hsLinkLib).href)
302
+ .then(({ missingSiblingBins }) => {
303
+ const strays = missingSiblingBins();
304
+ if (strays.length > 0) {
305
+ report.ok = false;
306
+ report.checks.push({
307
+ id: "sibling-bins-on-path",
308
+ status: "fail",
309
+ detail: `${strays.join(", ")} not on PATH`,
310
+ human:
311
+ `This \`hs\` resolved because it was invoked directly (npx, or ` +
312
+ `node_modules/.bin) — but ${strays.join(" / ")} will not resolve ` +
313
+ `from anywhere else on this machine unless you run ` +
314
+ "`npm install -g heyspex`.",
315
+ });
316
+ } else {
317
+ report.checks.push({
318
+ id: "sibling-bins-on-path",
319
+ status: "ok",
320
+ detail: "hs / heyspex / heyspex-mcp all resolve on PATH",
321
+ });
322
+ }
323
+ printStandaloneDoctorReport(report, jsonOut);
324
+ })
325
+ .catch(() => {
326
+ printStandaloneDoctorReport(report, jsonOut);
327
+ });
328
+ return;
329
+ }
330
+ printStandaloneDoctorReport(report, jsonOut);
331
+ }
332
+
333
+ function printStandaloneDoctorReport(report, jsonOut) {
334
+ if (jsonOut) {
335
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
336
+ process.exit(report.ok ? 0 : 1);
337
+ return;
338
+ }
339
+ process.stdout.write(
340
+ "HeySpex setup check — published-package install (not a source clone)\n\n",
341
+ );
342
+ for (const check of report.checks) {
343
+ const label = check.status === "ok" ? " ok " : " FAIL ";
344
+ process.stdout.write(`[ ${label} ] ${check.detail}\n`);
345
+ if (check.human) process.stdout.write(` ${check.human}\n`);
346
+ }
347
+ process.stdout.write(
348
+ "\nNote: the full `hs doctor` (git hooks, Cursor wiring, console secrets, and\n" +
349
+ "dozens of other source-checkout-only checks) only ships inside a clone of\n" +
350
+ "https://github.com/pwhermanson/workflow-protocol — this is the subset that\n" +
351
+ "applies to a published install.\n",
352
+ );
353
+ process.exit(report.ok ? 0 : 1);
354
+ }
355
+
356
+ function main(argv) {
357
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
358
+ usage();
359
+ process.exit(0);
360
+ }
361
+ if (argv[0] === "--version" || argv[0] === "-V") {
362
+ // Read root package.json for version. This file is ESM (no `require`
363
+ // in scope) — a prior version of this block called the undefined global
364
+ // `require(...)`, which threw and was silently swallowed by the catch
365
+ // below, so `hs --version` always printed the "0.0.0" fallback instead
366
+ // of the real version (found while proving the HS-BFXF fresh-install
367
+ // repro against a packed `heyspex` tarball).
368
+ try {
369
+ const pkg = JSON.parse(
370
+ readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"),
371
+ );
372
+ process.stdout.write(`hs ${pkg.version || "0.0.0"}\n`);
373
+ } catch {
374
+ process.stdout.write("hs 0.0.0\n");
375
+ }
376
+ process.exit(0);
377
+ }
378
+
379
+ const { vaultIdOverride, rest: argvAfterVaultFlag } =
380
+ extractGlobalVaultFlag(argv);
381
+ argv = argvAfterVaultFlag;
382
+
383
+ const namespace = argv[0];
384
+ const command = argv[1];
385
+ const rest = argv.slice(2);
386
+
387
+ const vaultRoot = process.env.HS_VAULT_ROOT || findVaultRoot(process.cwd());
388
+
389
+ // `hs init` writes the top-level heyspex.yaml vault marker
390
+ // ([[HS-EPIC-AEHR-audit-export-heyspex-yaml-and-heyspex-retirement]] §2). It
391
+ // may run on a fresh directory that has no specs/ yet, so it bypasses the
392
+ // hard vault gate below and falls back to the current directory.
393
+ if (namespace === "init") {
394
+ dispatchSpexConsoleEntry("hs-init.ts", argv, vaultRoot || process.cwd());
395
+ return;
396
+ }
397
+
398
+ // `hs install` bootstraps a customer/project vault + agent MCP wiring. May run
399
+ // on a fresh directory with no specs/ yet (like init).
400
+ if (namespace === "install") {
401
+ dispatchSpexConsoleEntry(
402
+ "hs-install.ts",
403
+ argv,
404
+ vaultRoot || process.cwd(),
405
+ {
406
+ HS_PKG_ROOT: PKG_ROOT,
407
+ },
408
+ );
409
+ return;
410
+ }
411
+
412
+ // `hs join` — fresh-machine onboarding orchestrator (R18): clone → toolchain
413
+ // → deps → doctor → agent layer → secrets → verify. May run on a machine
414
+ // with no vault at all (that is its whole point), so it bypasses the hard
415
+ // vault gate like init/install. The launch cwd is forwarded because the
416
+ // dispatcher runs entries with cwd=spex-console.
417
+ if (namespace === "join") {
418
+ dispatchSpexConsoleEntry("hs-join.ts", argv, vaultRoot || process.cwd(), {
419
+ HS_JOIN_CWD: process.cwd(),
420
+ });
421
+ return;
422
+ }
423
+
424
+ // `hs invite` — TeamViewer-short host side. Alias of `hs env invite`
425
+ // (optional name defaults to "peer"). Needs a vault (this machine owns it).
426
+ if (namespace === "invite") {
427
+ if (!vaultRoot) {
428
+ process.stderr.write(
429
+ "hs: no vault detected — could not find a specs/ folder " +
430
+ "walking up from the current directory. " +
431
+ "Set HS_VAULT_ROOT or run from inside a vault.\n",
432
+ );
433
+ process.exit(1);
434
+ }
435
+ dispatchSpexConsoleEntry("hs-env.ts", ["env", "invite", ...argv.slice(1)], vaultRoot);
436
+ return;
437
+ }
438
+
439
+ // `hs setup claude|cursor` — install agent lifecycle hooks into the vault root.
440
+ if (namespace === "setup") {
441
+ const agent = command?.toLowerCase();
442
+ const SCRIPTS = {
443
+ claude: "install-claude-code-hooks.mjs",
444
+ cursor: "install-cursor-hooks.mjs",
445
+ };
446
+ if (!agent || !Object.hasOwn(SCRIPTS, agent)) {
447
+ process.stderr.write(
448
+ `hs setup: expected \`claude\` or \`cursor\` (got \`${command ?? ""}\`)\n` +
449
+ " hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
450
+ " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n",
451
+ );
452
+ process.exit(2);
453
+ }
454
+ const setupFlags = rest.filter(
455
+ (a) => a === "--check" || a === "--uninstall",
456
+ );
457
+ const script = path.join(PKG_ROOT, "scripts", SCRIPTS[agent]);
458
+ const target = vaultRoot || process.cwd();
459
+ const child = spawn(process.execPath, [script, ...setupFlags], {
460
+ cwd: target,
461
+ stdio: "inherit",
462
+ env: { ...process.env, HS_SETUP_TARGET: target },
463
+ });
464
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
465
+ child.on("error", (err) => {
466
+ process.stderr.write(
467
+ `hs setup: failed to run ${SCRIPTS[agent]}: ${err.message}\n`,
468
+ );
469
+ process.exit(1);
470
+ });
471
+ return;
472
+ }
473
+
474
+ // `hs skills sync|check` — propagate .commandcode/skills → Cursor/Codex/Claude shims.
475
+ if (namespace === "skills") {
476
+ const action = command?.toLowerCase();
477
+ if (action !== "sync" && action !== "check") {
478
+ process.stderr.write(
479
+ `hs skills: expected \`sync\` or \`check\` (got \`${command ?? ""}\`)\n` +
480
+ " hs skills sync Copy skills + slash shims to all agent harnesses\n" +
481
+ " hs skills check Exit 1 if any harness copy is stale\n",
482
+ );
483
+ process.exit(2);
484
+ }
485
+ const script = path.join(PKG_ROOT, "scripts", "sync-skills.mjs");
486
+ const target = vaultRoot || PKG_ROOT;
487
+ const childArgs = action === "check" ? ["--check"] : [];
488
+ const child = spawn(process.execPath, [script, ...childArgs], {
489
+ cwd: target,
490
+ stdio: "inherit",
491
+ });
492
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
493
+ child.on("error", (err) => {
494
+ process.stderr.write(
495
+ `hs skills: failed to run sync-skills.mjs: ${err.message}\n`,
496
+ );
497
+ process.exit(1);
498
+ });
499
+ return;
500
+ }
501
+
502
+ // `hs vault list|register|switch|active` — the vault registry + active
503
+ // pointer CLI ([[HS-Q1HZ-TASK-hs-vault-cli-list-register-switch-active]]).
504
+ // May run outside any vault (registering a fresh directory, or listing/
505
+ // switching from anywhere), so it bypasses the hard vault gate below like
506
+ // init/install/setup.
507
+ if (namespace === "vault") {
508
+ dispatchSpexConsoleEntry("hs-vault.ts", argv, vaultRoot || process.cwd(), {
509
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
510
+ });
511
+ return;
512
+ }
513
+
514
+ // `hs workspace list|create|switch` — the workspace (company container)
515
+ // registry CLI ([[HS-Q1HZ-TASK-hs-workspace-cli-list-create-switch]]). Same
516
+ // no-vault-required rationale as `hs vault` above.
517
+ if (namespace === "workspace") {
518
+ dispatchSpexConsoleEntry(
519
+ "hs-workspace.ts",
520
+ argv,
521
+ vaultRoot || process.cwd(),
522
+ );
523
+ return;
524
+ }
525
+
526
+ // `hs login` — browser hand-off pairing (RFC 8252 loopback):
527
+ // one-shot 127.0.0.1 listener → default browser at <console>/companion/connect
528
+ // → the console mints the single-use code and redirects it back → silent
529
+ // redeem into ~/.heyspex/credentials.yaml. Same redemption as `hs mcp pair`,
530
+ // which stays as the cross-machine typed-code fallback
531
+ // ([[HS-IFQM-IZQ7-EPIC-hs-login-browser-hand-off-for-cli-and]]). This is an
532
+ // auth verb, so it bypasses the hard vault gate below like init/install —
533
+ // signing in is exactly what you do BEFORE you have a vault here.
534
+ if (namespace === "login") {
535
+ const cliPath = path.join(PKG_ROOT, "vendor", "heyspex-mcp", "cli.mjs");
536
+ const loginRoot = vaultRoot || process.cwd();
537
+ const child = spawn(process.execPath, [cliPath, "login", ...argv.slice(1)], {
538
+ cwd: PKG_ROOT,
539
+ stdio: "inherit",
540
+ env: {
541
+ ...process.env,
542
+ HEYSPEX_VAULT_ROOT: loginRoot,
543
+ HS_VAULT_ROOT: loginRoot,
544
+ HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
545
+ },
546
+ });
547
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
548
+ child.on("error", (err) => {
549
+ process.stderr.write(`hs: failed to run login: ${err.message}\n`);
550
+ process.exit(1);
551
+ });
552
+ return;
553
+ }
554
+
555
+ // `hs connect --check [--json]` — the read-only connect-checks report
556
+ // ([[HS-I2OO-EKLJ-TASK-shared-connect-checks-module-with-a]] on
557
+ // [[HS-QYH8-EKLJ-EPIC-hs-connect-one-command-repo-connect-verb]]). May run
558
+ // in an unconnected repo (no specs/ yet — that is the point), so it
559
+ // bypasses the hard vault gate below like init/install/vault/workspace.
560
+ if (namespace === "connect") {
561
+ dispatchSpexConsoleEntry("hs-connect.ts", argv, vaultRoot || process.cwd(), {
562
+ HS_INVOKED_CWD: INVOKED_CWD,
563
+ });
564
+ return;
565
+ }
566
+
567
+ // `hs doctor [--fix]` — verify every install surface (MCP, hooks, skills,
568
+ // statusline, git hooks, console secrets) and repair what can repair itself.
569
+ // Diagnosing whether the install itself is healthy must not first require a
570
+ // vault to exist, so — like init/install/connect — this bypasses the hard
571
+ // vault gate below. [[HS-BFXF-WBJU-DIAG-hs-never-resolves-on-a-fresh-machine]]:
572
+ // the very first thing a stranger runs after `npm install heyspex` in an
573
+ // empty project is exactly this, from a directory with no `specs/` yet.
574
+ if (namespace === "doctor") {
575
+ const script = path.join(PKG_ROOT, "scripts", "heyspex-doctor.mjs");
576
+ if (!existsSync(script)) {
577
+ // Published-package install (per HS-ZJSP-0JNK): the full doctor audits
578
+ // dozens of source-checkout-only surfaces (git hooks, Cursor wiring,
579
+ // console secrets…) that a `npm install heyspex` consumer never has, so
580
+ // it is deliberately not in the package's `files`. Run the small
581
+ // subset that DOES apply to a published install instead of crashing
582
+ // with MODULE_NOT_FOUND.
583
+ runStandaloneDoctor(argv.slice(1));
584
+ return;
585
+ }
586
+ const child = spawn(process.execPath, [script, ...argv.slice(1)], {
587
+ cwd: PKG_ROOT,
588
+ stdio: "inherit",
589
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
590
+ });
591
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
592
+ child.on("error", (err) => {
593
+ process.stderr.write(`hs: failed to run hs doctor: ${err.message}\n`);
594
+ process.exit(1);
595
+ });
596
+ return;
597
+ }
598
+
599
+ if (!vaultRoot) {
600
+ process.stderr.write(
601
+ "hs: no vault detected — could not find a specs/ folder " +
602
+ "walking up from the current directory. " +
603
+ "Set HS_VAULT_ROOT or run from inside a vault.\n",
604
+ );
605
+ process.exit(1);
606
+ }
607
+
608
+ if (namespace === "index" && command === "rebuild") {
609
+ dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot);
610
+ return;
611
+ }
612
+
613
+ // `hs pull` — cloud → disk vault reconciliation. In cloud mode a filed spec
614
+ // lands in the cloud content store ONLY, invisible to promote / vault scans /
615
+ // wikilink resolution / git ([[HS-OQNH-21DO-DIAG-cloud-written-specs-are-invisible-to]]).
616
+ // This is the missing pull half. Additive and one-directional: it never
617
+ // pushes, never deletes, and only REPORTS unless `--apply` is given.
618
+ // Forwards the full argv; the entrypoint drops the leading `pull` token.
619
+ if (namespace === "pull") {
620
+ dispatchSpexConsoleEntry("hs-vault-pull.ts", argv, vaultRoot);
621
+ return;
622
+ }
623
+
624
+ // `hs milestone show <id>` — print a milestone, flagging needs-breakdown when
625
+ // it has no epics ([[HS-TASK-HMSE]] / [[HS-EPIC-MNBD]]). Forwards the full
626
+ // argv; the entrypoint drops the leading `milestone` token.
627
+ if (namespace === "milestone") {
628
+ dispatchSpexConsoleEntry("hs-milestone.ts", argv, vaultRoot);
629
+ return;
630
+ }
631
+
632
+ if (namespace === "factory" && command === "run") {
633
+ dispatchSpexConsoleEntry("hs-factory-run.ts", argv, vaultRoot);
634
+ return;
635
+ }
636
+
637
+ // `hs factory status|fleet|results` — Dev-Factory read-only status surface
638
+ // (fleet, routing table, calibrations, launch results). CLI face of the
639
+ // shared core reader that also backs /api/admin/factory/* ([[HS-0DG1-EPIC]] —
640
+ // capability-three-faces). Forwards the full argv; the entrypoint drops the
641
+ // leading `factory` token.
642
+ if (namespace === "factory") {
643
+ dispatchSpexConsoleEntry("hs-factory.ts", argv, vaultRoot);
644
+ return;
645
+ }
646
+
647
+ // `hs code-watcher scan` — run the build tools (tsc --noEmit; test/lint to
648
+ // follow), parse failures, and write the build-correctness snapshot the
649
+ // Resolution surface reads ([[HS-TASK-2B4P]] / [[HS-EPIC-RHNJ]]). Forwards the
650
+ // full argv; the entrypoint drops the leading `code-watcher` token.
651
+ if (namespace === "code-watcher") {
652
+ dispatchSpexConsoleEntry("hs-code-watcher.ts", argv, vaultRoot);
653
+ return;
654
+ }
655
+
656
+ // `hs server status` / `hs server stop` — ground-truth status and stop for
657
+ // the spex-console Next.js dev server. Pure Node cores under scripts/
658
+ // (same as npm run console:status / console:stop). Call status before any
659
+ // server-impacting task; stop from any terminal (not only the one that
660
+ // started it). See [[dev-server-lifecycle-and-db-safety]].
661
+ if (namespace === "server" && (command === "status" || command === "stop")) {
662
+ const scriptName =
663
+ command === "stop" ? "dev-server-stop.mjs" : "dev-server-status.mjs";
664
+ const script = path.join(PKG_ROOT, "scripts", scriptName);
665
+ const child = spawn(process.execPath, [script, ...rest], {
666
+ cwd: PKG_ROOT,
667
+ stdio: "inherit",
668
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
669
+ });
670
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
671
+ child.on("error", (err) => {
672
+ process.stderr.write(
673
+ `hs: failed to run ${scriptName}: ${err.message}\n`,
674
+ );
675
+ process.exit(1);
676
+ });
677
+ return;
678
+ }
679
+
680
+ // `hs source status|preflight|commit|push` — SpexSource: the local-git
681
+ // preflight/commit/push path for agent sessions
682
+ // ([[HS-Q0K8-CWPX-EPIC-spexsource-local-git-preflight-commit]]). Pure Node
683
+ // cores under scripts/lib/, invoked directly (not via dispatchSpexConsoleEntry
684
+ // — these are plain .mjs modules, not spex-console TS entries). Forwards the
685
+ // full argv; each script drops the leading `source <verb>` tokens itself.
686
+ if (namespace === "source") {
687
+ // `hs source --help` / `-h` — bare usage, no subprocess spawned. Must be
688
+ // checked BEFORE the SOURCE_ENTRYPOINTS lookup below: `command` here is
689
+ // "--help"/"-h", which is not a known verb, so without this special case
690
+ // it fell into the "unknown verb" error path (exit 2) instead of exiting
691
+ // 0 with usage ([[HS-PO09-0JNK-TASK-packed-mcp-server-runs-the-agent-loop]] AC4).
692
+ if (command === "--help" || command === "-h") {
693
+ process.stdout.write(
694
+ "hs source — SpexSource local-git preflight/commit/push for agent sessions\n\n" +
695
+ " hs source status Read-only tree status + path-scoped stage plan\n" +
696
+ " hs source preflight Alias of status\n" +
697
+ " hs source commit Stage + commit per the preflight plan\n" +
698
+ " hs source push Push the committed branch\n" +
699
+ " hs source log <spec-id> Commit-history lookup for a spec (console settlement,\n" +
700
+ " falling back to local git log)\n",
701
+ );
702
+ process.exit(0);
703
+ }
704
+ const SOURCE_ENTRYPOINTS = {
705
+ status: "hs-source-preflight.mjs",
706
+ preflight: "hs-source-preflight.mjs",
707
+ commit: "hs-source-commit.mjs",
708
+ push: "hs-source-push.mjs",
709
+ log: "hs-source-log.mjs",
710
+ };
711
+ const scriptName = SOURCE_ENTRYPOINTS[command];
712
+ if (!scriptName) {
713
+ process.stderr.write(
714
+ `hs: unknown \`hs source\` verb \`${command ?? ""}\` — expected one of: ${Object.keys(SOURCE_ENTRYPOINTS).join(", ")}\n`,
715
+ );
716
+ process.exit(2);
717
+ }
718
+ const script = path.join(PKG_ROOT, "scripts", "lib", scriptName);
719
+ const child = spawn(process.execPath, [script, command, ...rest], {
720
+ cwd: PKG_ROOT,
721
+ stdio: "inherit",
722
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
723
+ });
724
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
725
+ child.on("error", (err) => {
726
+ process.stderr.write(`hs: failed to run ${scriptName}: ${err.message}\n`);
727
+ process.exit(1);
728
+ });
729
+ return;
730
+ }
731
+
732
+ // `hs view <surface> [spec-id]` — open one console surface in a chromeless
733
+ // app-mode browser window (the solo console gauge —
734
+ // [[HS-RU2R-I87U-RES-solo-surface-cli-launcher-single]]). Probes ground-truth
735
+ // server status first; a down console prints a clear message, never a blank window.
736
+ if (namespace === "view") {
737
+ const script = path.join(PKG_ROOT, "scripts", "hs-view.mjs");
738
+ const child = spawn(process.execPath, [script, ...argv.slice(1)], {
739
+ cwd: PKG_ROOT,
740
+ stdio: "inherit",
741
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
742
+ });
743
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
744
+ child.on("error", (err) => {
745
+ process.stderr.write(`hs: failed to run hs view: ${err.message}\n`);
746
+ process.exit(1);
747
+ });
748
+ return;
749
+ }
750
+
751
+ // `hs dev [mode:local|mode:cloud|mode:prod]` — dev/prod mode + local/cloud
752
+ // reliance (epic HS-Q6M2). Bare `hs dev` prints the resolved mode + sources;
753
+ // mode:* switches (writes ~/.heyspex/dev-mode.json + credentials.yaml
754
+ // console_url, best-effort cloud admin setting). Forwards the full argv;
755
+ // the entrypoint drops the leading `dev` token.
756
+ if (namespace === "dev") {
757
+ dispatchSpexConsoleEntry("hs-dev.ts", argv, vaultRoot);
758
+ return;
759
+ }
760
+
761
+ // `hs defer <id>` / `hs undefer <id>` are top-level verbs (not namespaced):
762
+ // forward the full argv (verb + args) to the spex-console entrypoint.
763
+ if (namespace === "defer" || namespace === "undefer") {
764
+ dispatchSpexConsoleEntry("hs-defer.ts", argv, vaultRoot);
765
+ return;
766
+ }
767
+
768
+ // `hs spawn [<id>] [--prompt ...] [--dry-run] [--json]` — launch a FRESH
769
+ // /hs-next agent session in a new terminal ([[HS-EPIC-NWUE]] / [[HS-TASK-MAPK]]).
770
+ // The CLI face of the What's Next hero Launch button + POST /api/agent/start;
771
+ // forwards the full argv (verb + args), the entrypoint drops the leading token.
772
+ if (namespace === "spawn") {
773
+ dispatchSpexConsoleEntry("hs-spawn.ts", argv, vaultRoot);
774
+ return;
775
+ }
776
+
777
+ // `hs capture "<text>"|--clipboard|--file <p> [--add]` — CLI capture into
778
+ // the console drafts inbox; --add chains into the previous CLI capture's
779
+ // group (the multi-capture "add another?" gesture). Pure Node, posts the
780
+ // same heyspex.capture/0.1 envelope the companion sends to /api/drafts.
781
+ if (namespace === "capture") {
782
+ const script = path.join(PKG_ROOT, "scripts", "hs-capture-cli.mjs");
783
+ const captureArgs = command ? [command, ...rest] : rest;
784
+ const child = spawn(process.execPath, [script, ...captureArgs], {
785
+ cwd: PKG_ROOT,
786
+ stdio: "inherit",
787
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
788
+ });
789
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
790
+ child.on("error", (err) => {
791
+ process.stderr.write(`hs: failed to run capture: ${err.message}\n`);
792
+ process.exit(1);
793
+ });
794
+ return;
795
+ }
796
+
797
+ // `hs companion start|stop|status|build` — companion tray host (companion/src/HeySpex.Companion.Host).
798
+ if (namespace === "companion") {
799
+ const script = path.join(PKG_ROOT, "scripts", "hs-companion.mjs");
800
+ const companionArgs = command ? [command, ...rest] : ["start", ...rest];
801
+ const child = spawn(process.execPath, [script, ...companionArgs], {
802
+ cwd: PKG_ROOT,
803
+ stdio: "inherit",
804
+ });
805
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
806
+ child.on("error", (err) => {
807
+ process.stderr.write(`hs: failed to run companion: ${err.message}\n`);
808
+ process.exit(1);
809
+ });
810
+ return;
811
+ }
812
+
813
+ if (namespace === "registry") {
814
+ dispatchSpexConsoleEntry("hs-registry.ts", argv, vaultRoot);
815
+ return;
816
+ }
817
+
818
+ // `hs env init|set|list|run|export|rotate` — encrypted env vault (ADR-0020).
819
+ // Pure local fs (no console): forwards the full argv, the entrypoint drops the
820
+ // leading `env` token. [[HS-CDFS-TASK]] / [[HS-9VD2-TASK]].
821
+ if (namespace === "env") {
822
+ dispatchSpexConsoleEntry("hs-env.ts", argv, vaultRoot);
823
+ return;
824
+ }
825
+
826
+ // `hs next [spec_id] [--summary] [--json]` — resolve the on-deck brief without
827
+ // MCP ([[HS-TASK-ISK7]] / [[tool-agnostic-mcp-first-cli-second]]).
828
+ if (namespace === "next") {
829
+ const script = path.join(PKG_ROOT, "scripts", "hs-next.mjs");
830
+ const child = spawn(process.execPath, [script, ...argv.slice(1)], {
831
+ cwd: PKG_ROOT,
832
+ stdio: "inherit",
833
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
834
+ });
835
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
836
+ child.on("error", (err) => {
837
+ process.stderr.write(`hs: failed to run hs next: ${err.message}\n`);
838
+ process.exit(1);
839
+ });
840
+ return;
841
+ }
842
+
843
+ // `hs mode on|off|status|toggle` — the master switch for the whole HeySpex
844
+ // agent layer (hooks, MCP server, statusline, CLAUDE.md agent contract).
845
+ // Turning it off makes this repo behave like a plain codebase in Claude Code.
846
+ // Core: scripts/heyspex-mode.mjs · docs/claude-code-setup.md.
847
+ if (namespace === "mode") {
848
+ const script = path.join(PKG_ROOT, "scripts", "hs-mode.mjs");
849
+ const child = spawn(process.execPath, [script, ...argv.slice(1)], {
850
+ cwd: PKG_ROOT,
851
+ stdio: "inherit",
852
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
853
+ });
854
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
855
+ child.on("error", (err) => {
856
+ process.stderr.write(`hs: failed to run hs mode: ${err.message}\n`);
857
+ process.exit(1);
858
+ });
859
+ return;
860
+ }
861
+
862
+ // `hs gateway validate [slug] [--json]` — validate user-authored gateway
863
+ // skills and their scope coverage (HS-WEC3; core: scripts/gateway-skills-lib.mjs).
864
+ // Registration subcommands print a blocked-on-HS-DCCP notice until the
865
+ // connector's server-registration table exists.
866
+ if (namespace === "gateway") {
867
+ const script = path.join(PKG_ROOT, "scripts", "hs-gateway.mjs");
868
+ const child = spawn(process.execPath, [script, ...argv.slice(1)], {
869
+ cwd: PKG_ROOT,
870
+ stdio: "inherit",
871
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
872
+ });
873
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
874
+ child.on("error", (err) => {
875
+ process.stderr.write(`hs: failed to run hs gateway: ${err.message}\n`);
876
+ process.exit(1);
877
+ });
878
+ return;
879
+ }
880
+
881
+ // `hs settings list|get|set` — read/change HeySpex workflow settings in
882
+ // .heyspex/config.yaml (settings capability; core: vendor/heyspex-mcp/
883
+ // config-settings.mjs; write-permission rules: agent-settings-protocol).
884
+ if (namespace === "settings") {
885
+ const script = path.join(PKG_ROOT, "scripts", "hs-settings.mjs");
886
+ const child = spawn(process.execPath, [script, ...argv], {
887
+ cwd: PKG_ROOT,
888
+ stdio: "inherit",
889
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
890
+ });
891
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
892
+ child.on("error", (err) => {
893
+ process.stderr.write(`hs: failed to run hs settings: ${err.message}\n`);
894
+ process.exit(1);
895
+ });
896
+ return;
897
+ }
898
+
899
+ // `hs classifier stats` — read-only view of HeySpex Intent Classifier decisions
900
+ // (.heyspex/intent-classifier-history.jsonl). Pure local read.
901
+ if (namespace === "classifier") {
902
+ const script = path.join(PKG_ROOT, "scripts", "hs-classifier.mjs");
903
+ const child = spawn(process.execPath, [script, ...argv], {
904
+ cwd: PKG_ROOT,
905
+ stdio: "inherit",
906
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
907
+ });
908
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
909
+ child.on("error", (err) => {
910
+ process.stderr.write(`hs: failed to run hs classifier: ${err.message}\n`);
911
+ process.exit(1);
912
+ });
913
+ return;
914
+ }
915
+
916
+ // `hs auto [filename] [--json]` — resolve /hs-auto COO dispatch plan.
917
+ if (namespace === "auto") {
918
+ const script = path.join(PKG_ROOT, "scripts", "hs-auto.mjs");
919
+ const child = spawn(process.execPath, [script, ...argv.slice(1)], {
920
+ cwd: PKG_ROOT,
921
+ stdio: "inherit",
922
+ env: { ...process.env, HS_VAULT_ROOT: vaultRoot },
923
+ });
924
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
925
+ child.on("error", (err) => {
926
+ process.stderr.write(`hs: failed to run hs auto: ${err.message}\n`);
927
+ process.exit(1);
928
+ });
929
+ return;
930
+ }
931
+
932
+ // `hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json]`
933
+ // without the stdio JSON-RPC loop (parity for agents when heyspex_* tools are
934
+ // missing from the palette — [[cursor-mcp-official-notes]]).
935
+ // `hs mcp pair <code> [--console-url <url>]` — redeem a console pairing
936
+ // code for a persistent `hsmcp_` bearer, stored in ~/.heyspex/credentials.yaml
937
+ // ([[HS-GD45-MCPC-TASK-mcp-cloud-auth-shared-console-resolver]]).
938
+ if (namespace === "mcp") {
939
+ if (command !== "call" && command !== "pair" && command !== "login") {
940
+ process.stderr.write(
941
+ `hs: unknown mcp subcommand \`${command ?? ""}\` (expected: call | pair | login)\n` +
942
+ " hs mcp call <tool> [--arg key=value ...] [--args '<json>'] [--args-file <path>] [--json]\n" +
943
+ " hs mcp pair <code> [--console-url <url>] [--label <text>] [--json]\n" +
944
+ " hs mcp login [--console-url <url>] [--label <text>] [--json] (alias of `hs login`)\n",
945
+ );
946
+ process.exit(2);
947
+ }
948
+ const cliPath = path.join(PKG_ROOT, "vendor", "heyspex-mcp", "cli.mjs");
949
+ const child = spawn(process.execPath, [cliPath, command, ...rest], {
950
+ cwd: PKG_ROOT,
951
+ stdio: "inherit",
952
+ env: {
953
+ ...process.env,
954
+ HEYSPEX_VAULT_ROOT: vaultRoot,
955
+ HS_VAULT_ROOT: vaultRoot,
956
+ // One-shot MCP tool — no stdio loop, no stderr brief-tier noise in PS.
957
+ HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
958
+ },
959
+ });
960
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
961
+ child.on("error", (err) => {
962
+ process.stderr.write(`hs: failed to run mcp ${command}: ${err.message}\n`);
963
+ process.exit(1);
964
+ });
965
+ return;
966
+ }
967
+
968
+ // `hs test run <id|manifest>` — run open-format API/contract tests (Hurl/.http)
969
+ // bound to acceptance criteria ([[HS-EPIC-Q0KU-open-format-test-substrate-hs-test-run-h]];
970
+ // supersedes the .spex plan per [[HS-RES-W0F6-testing-direction-pivot-open-formats-mut]]).
971
+ // Forwards the full argv; the entrypoint drops the leading `test` token.
972
+ if (namespace === "test") {
973
+ dispatchSpexConsoleEntry("hs-test.ts", argv, vaultRoot);
974
+ return;
975
+ }
976
+
977
+ // `hs check run <id|manifest>` — verify a spec's acceptance criteria against its
978
+ // frozen checks manifest, per-criterion pass/fail against the real tree
979
+ // ([[HS-EPIC-DCEP-acceptance-criteria-checks-manifest-dete]], the deterministic
980
+ // kernel of [[HS-MIL-Y5DN-verification-engine-shared-deterministic]]). Forwards
981
+ // the full argv; the entrypoint drops the leading `check` token.
982
+ if (namespace === "check") {
983
+ dispatchSpexConsoleEntry("hs-check.ts", argv, vaultRoot);
984
+ return;
985
+ }
986
+
987
+ // `hs revalidate` — re-run the FROZEN manifests of work already stamped
988
+ // `review_pending`/`in_review` and report what has decayed since the stamp
989
+ // ([[HS-R5LF-36QL-EPIC-stamped-work-is-re-verified-on-a]]). Read-only with
990
+ // respect to the repo; the Resolution plan is a dry run unless `--apply`.
991
+ if (namespace === "revalidate") {
992
+ dispatchSpexConsoleEntry("hs-revalidate.ts", argv, vaultRoot);
993
+ return;
994
+ }
995
+
996
+ // `hs agent import <collection> <role>` / `hs agent list` — import a community
997
+ // agent definition from a vendored collection into a `type: agent` vault spec
998
+ // ([[HS-TASK-6D5P]] / [[HS-EPIC-TMXZ-default-agent-roster-and-import-path]] AC#2).
999
+ // The CLI face of the shared import normalizer; forwards the full argv, the
1000
+ // entrypoint drops the leading `agent` token.
1001
+ if (namespace === "agent") {
1002
+ dispatchSpexConsoleEntry("hs-agent.ts", argv, vaultRoot);
1003
+ return;
1004
+ }
1005
+
1006
+ // Neon control-plane verbs ([[HS-EPIC-CLMW-hs-cli-mcp-wrappers-for-control-plane]]).
1007
+ // Each forwards the full argv (namespace/verb + args); the entrypoint drops
1008
+ // the leading namespace token and parses the rest.
1009
+ const CONTROL_ENTRYPOINTS = {
1010
+ pin: "hs-pin.ts",
1011
+ claim: "hs-claim.ts",
1012
+ handoff: "hs-handoff.ts",
1013
+ events: "hs-events.ts",
1014
+ "export-events": "hs-export-events.ts",
1015
+ prereq: "hs-prereq.ts",
1016
+ queue: "hs-queue.ts",
1017
+ };
1018
+ if (Object.hasOwn(CONTROL_ENTRYPOINTS, namespace)) {
1019
+ dispatchSpexConsoleEntry(CONTROL_ENTRYPOINTS[namespace], argv, vaultRoot);
1020
+ return;
1021
+ }
1022
+
1023
+ process.stderr.write(`hs: unknown command \`${argv.join(" ")}\`\n\n`);
1024
+ usage();
1025
+ process.exit(2);
1026
+ }
1027
+
1028
+ main(process.argv.slice(2));