heyspex 0.2.0 → 0.2.2

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 (146) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +181 -15
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +177 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +21 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +312 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/install-copilot-hooks.mjs +288 -0
  72. package/scripts/lib/console-url-resolver.mjs +3 -2
  73. package/scripts/lib/customer-kit.mjs +327 -10
  74. package/scripts/lib/dev-mode.mjs +7 -6
  75. package/scripts/lib/directive-obligations.mjs +349 -0
  76. package/scripts/lib/hook-dispositions.mjs +210 -0
  77. package/scripts/lib/hook-vault-header.mjs +30 -0
  78. package/scripts/lib/hs-source-branch.mjs +196 -0
  79. package/scripts/lib/hs-source-log.mjs +2 -1
  80. package/scripts/lib/hs-source-preflight.mjs +18 -4
  81. package/scripts/lib/hs-source-push.mjs +386 -356
  82. package/scripts/lib/window-placement.mjs +109 -0
  83. package/spexharness/src/adapters/claude-code.mjs +11 -3
  84. package/spexharness/src/adapters/copilot.mjs +236 -0
  85. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  86. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  87. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  88. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  89. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  90. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  91. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  93. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  94. package/spexharness/src/intent-classifier.mjs +4 -2
  95. package/spexharness/src/intent-directive.mjs +626 -541
  96. package/spexharness/src/rule-table.mjs +31 -1
  97. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  98. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  99. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  100. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  101. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  102. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  103. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  104. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  105. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  106. package/vendor/heyspex-mcp/cli.mjs +7 -1
  107. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  108. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  109. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  110. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  111. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  112. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  113. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  114. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  115. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  116. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  117. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  118. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  119. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  120. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  121. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  122. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  123. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  124. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  125. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  126. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  127. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  128. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  129. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  130. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  131. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  132. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  133. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  134. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  135. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  136. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  137. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  138. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  139. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  140. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  141. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  142. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  143. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  144. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  145. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  146. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "heyspex-marketplace",
3
+ "owner": {
4
+ "name": "pwher",
5
+ "email": "paul@monstergrowth.com"
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "heyspex",
10
+ "source": "./plugins/heyspex",
11
+ "description": "SpexCode plugin (for Claude Code and GitHub Copilot CLI): HeySpex vault integration, MCP server (spec vault tools + hs-* prompts) and lifecycle hooks. Prototype — see plugins/heyspex/README.md before enabling."
12
+ }
13
+ ]
14
+ }
package/bin/hs.mjs CHANGED
@@ -25,7 +25,9 @@ import {
25
25
  vaultConfigCheckRow,
26
26
  } from "../scripts/lib/standalone-doctor.mjs";
27
27
  import {
28
+ findBrokenSatellitePin,
28
29
  isVaultRoot,
30
+ readMarkerSatellite,
29
31
  readSatellitePin,
30
32
  resolveActiveVault,
31
33
  } from "../vendor/heyspex-mcp/vault-resolve.mjs";
@@ -58,6 +60,7 @@ const PKG_ROOT = path.resolve(__dirname, "..");
58
60
  export const SPAWN_TARGETS = {
59
61
  "setup:claude": { path: "scripts/install-claude-code-hooks.mjs" },
60
62
  "setup:cursor": { path: "scripts/install-cursor-hooks.mjs" },
63
+ "setup:copilot": { path: "scripts/install-copilot-hooks.mjs" },
61
64
  "skills:sync": {
62
65
  path: "scripts/sync-skills.mjs",
63
66
  devOnly: true,
@@ -85,6 +88,7 @@ export const SPAWN_TARGETS = {
85
88
  "source:preflight": { path: "scripts/lib/hs-source-preflight.mjs" },
86
89
  "source:commit": { path: "scripts/lib/hs-source-commit.mjs" },
87
90
  "source:push": { path: "scripts/lib/hs-source-push.mjs" },
91
+ "source:branch": { path: "scripts/lib/hs-source-branch.mjs" },
88
92
  "source:log": { path: "scripts/lib/hs-source-log.mjs" },
89
93
  // `hs source identity` — the ONE sanctioned git-identity writer (human-only
90
94
  // set/clear, read-only show). Incident 2026-09-19, vault lesson
@@ -175,6 +179,8 @@ export const BUILTIN_VERBS = [
175
179
  "doctor",
176
180
  "index",
177
181
  "pull",
182
+ "push",
183
+ "import",
178
184
  "milestone",
179
185
  "factory",
180
186
  "code-watcher",
@@ -286,6 +292,13 @@ export function findVaultRoot(start, stopAt) {
286
292
  if (pinned) {
287
293
  return pinned;
288
294
  }
295
+ // A committed, portable satellite marker (heyspex.yaml with a relative
296
+ // `vault_path`): the attached vault's clone when it is on this machine,
297
+ // else this repo itself (cloud-only; the marker's vault_id binds it).
298
+ const marked = readMarkerSatellite(dir);
299
+ if (marked) {
300
+ return marked.vaultRootAbs ?? dir;
301
+ }
289
302
  if (isVaultRoot(dir)) {
290
303
  return dir;
291
304
  }
@@ -331,6 +344,72 @@ export function findRegistryVaultRoot(options = {}) {
331
344
  return resolved.repoRootAbs;
332
345
  }
333
346
 
347
+ /** Env hatch: follow the account-wide active-vault pointer on purpose (same name the MCP transport uses). */
348
+ export function pointerVaultAllowed(env = process.env) {
349
+ const v = String(env.HEYSPEX_ALLOW_POINTER_VAULT ?? "").trim().toLowerCase();
350
+ return v === "1" || v === "true" || v === "yes";
351
+ }
352
+
353
+ /**
354
+ * Verbs that may legitimately run with only the machine's last-active vault
355
+ * (or none): they onboard, sign in, or manage the registry itself. EVERY other
356
+ * verb acts on a vault, and P1 (owner, 2026-10-16) says that vault is the one
357
+ * bound to the repo the agent runs in -- never the account-wide pointer.
358
+ */
359
+ const POINTER_FALLBACK_NAMESPACES = new Set([
360
+ "init", "install", "join", "setup", "skills", "vault", "workspace",
361
+ "login", "whoami", "logout", "doctor", "registry", "companion", "trust",
362
+ ]);
363
+
364
+ /** True when `hs <namespace> <command>` may run on the pointer-only fallback. */
365
+ export function pointerFallbackAllowedFor(namespace, command) {
366
+ if (namespace === "mcp") return command === "pair" || command === "login" || command === "install";
367
+ return POINTER_FALLBACK_NAMESPACES.has(namespace);
368
+ }
369
+
370
+ /** The refusal printed when a satellite's pin / marker cannot be honoured. */
371
+ export function brokenPinRefusalLine(broken) {
372
+ return (
373
+ `hs: this repo's vault pin cannot be honoured, so nothing was run. ${broken.dir}/${broken.source} ` +
374
+ `names \`${broken.raw}\`, but ${broken.reason}. ` +
375
+ "Fix the pin (a relative path such as ../<vault-repo> or ${CLAUDE_PROJECT_DIR:-.}/../<vault-repo> is portable), " +
376
+ "or commit a heyspex.yaml with vault_id and vault_path; hs will NOT fall back to the account-wide active vault.\n"
377
+ );
378
+ }
379
+
380
+ /** Nearest directory at/above `start` whose heyspex.yaml is a satellite marker (has vault_path), else null. */
381
+ export function markerSatelliteDirFrom(start) {
382
+ let dir = path.resolve(start);
383
+ for (let i = 0; i < 256; i += 1) {
384
+ if (readMarkerSatellite(dir)) return dir;
385
+ const parent = path.dirname(dir);
386
+ if (parent === dir) return null;
387
+ dir = parent;
388
+ }
389
+ return null;
390
+ }
391
+
392
+ /** The refusal for disk-writing verbs in a satellite whose vault clone is missing. */
393
+ export function unclonedVaultRefusalLine(repo) {
394
+ const m = readMarkerSatellite(repo);
395
+ return (
396
+ `hs: ${repo} is bound to vault ${m?.vaultId ?? "(no vault_id)"} by its heyspex.yaml, but the vault ` +
397
+ `is not cloned at ${m?.vaultPath ?? "its vault_path"} on this machine, so this verb has no local vault to act on. ` +
398
+ "Clone the vault repo to that relative path (agents and cloud-routed MCP writes work without it).\n"
399
+ );
400
+ }
401
+
402
+ /** The refusal printed when only the account-wide pointer answered and the verb acts on a vault. */
403
+ export function pointerOnlyRefusalLine(cwd, vaultRoot) {
404
+ return (
405
+ `hs: ${cwd} is not bound to a HeySpex vault (no heyspex.yaml, no specs/, no vault pin in .mcp.json), ` +
406
+ `so \`hs\` refused to follow the account-wide active vault${vaultRoot ? ` (${vaultRoot})` : ""}. ` +
407
+ "Bind this repo: run `hs connect` in a vault repo, `hs install --attach-vault <id> --target <repo>` for a code repo, " +
408
+ "or commit a heyspex.yaml with vault_id and a relative vault_path. " +
409
+ "To follow the pointer on purpose set HEYSPEX_ALLOW_POINTER_VAULT=1.\n"
410
+ );
411
+ }
412
+
334
413
  /**
335
414
  * The vault-resolution ladder `main()` runs at the top of every invocation,
336
415
  * factored out so it is unit-testable without spawning a child process
@@ -358,6 +437,12 @@ export function resolveVaultRootForMain(cwd, env, stopAt) {
358
437
  if (env.HS_VAULT_ROOT) {
359
438
  return { vaultRoot: env.HS_VAULT_ROOT, usedRegistryFallback: false };
360
439
  }
440
+ // A pin that cannot be honoured is a REFUSAL, never a reason to fall
441
+ // through to the account-wide pointer (owner principle P1, 2026-10-16).
442
+ const brokenPin = findBrokenSatellitePin(cwd, stopAt);
443
+ if (brokenPin) {
444
+ return { vaultRoot: null, usedRegistryFallback: false, brokenPin };
445
+ }
361
446
  const walkedRoot = findVaultRoot(cwd, stopAt);
362
447
  if (walkedRoot) {
363
448
  return { vaultRoot: walkedRoot, usedRegistryFallback: false };
@@ -481,15 +566,17 @@ function usage() {
481
566
  " hs gateway validate [slug] [--json] Validate user-authored gateway skills (.heyspex/skills/user/)",
482
567
  " hs mode on|off|status|toggle Master switch: HeySpex layer on or off (restart Claude Code)",
483
568
  " hs dev [mode:local|mode:cloud|mode:prod] Dev/prod mode + local/cloud reliance (status when bare)",
484
- " hs setup claude|cursor [--check|--uninstall] Install agent lifecycle hooks",
569
+ " hs setup claude|cursor|copilot [--check|--uninstall] Install agent lifecycle hooks (copilot also: --print, --hooks)",
485
570
  " hs skills sync|check Propagate skills to Cursor/Codex/Claude (cwd-safe)",
486
571
  " hs skills add <module-path> --name <n> --trigger </slash> [--export <n>] [--description <t>] Register a skill (extension seam)",
487
572
  " hs skills remove <name> Remove a customer-registered skill",
488
573
  " hs skills list [--json] List registered skills (built-ins first, locked)",
489
574
  " hs index rebuild [--verbose] [--dry-run] Full idempotent rebuild of the index",
490
575
  " hs index reconcile [--apply] [--cap-docs keep-out|index] Per-spec newest-wins between disk and cloud (reports unless --apply)",
491
- " hs pull [--apply] [--only <id>] [--overwrite] Bring cloud-written specs down to the disk vault (reports unless --apply)",
492
- " hs source status|preflight|commit|push Local git: tree status, path-scoped checks, commit, safe push",
576
+ " hs pull [--apply] [--only <id>] [--overwrite] Bring cloud-written specs down to the disk vault (reports unless --apply; --watch --push adds the disk -> cloud leg)",
577
+ " hs push [--apply] [--only <id>] [--force] Send local spec edits up to the cloud vault (reports unless --apply)",
578
+ " hs import <source-dir> [--folders a,b] [--plan f] [--dry-run] [--limit N] Import a doc tree as GROUNDED spec-import captures (never raw, never research)",
579
+ " hs source status|preflight|commit|push|branch Local git: tree status, path-scoped checks, commit, safe push, session branch",
493
580
  " hs source identity [show|set|clear] Git author identity: show (read-only) / set, clear (HUMAN-ONLY)",
494
581
  " hs milestone show <id> [--json] Show a milestone (flags needs-breakdown when epicless)",
495
582
  " hs factory status|fleet|results [--json] Dev-Factory read-only status (fleet, routing, calibrations, results)",
@@ -858,10 +945,27 @@ function main(argv) {
858
945
  const command = argv[1];
859
946
  const rest = argv.slice(2);
860
947
 
861
- const { vaultRoot, usedRegistryFallback } = resolveVaultRootForMain(
862
- process.cwd(),
863
- process.env,
864
- );
948
+ const resolvedMain = resolveVaultRootForMain(process.cwd(), process.env);
949
+ const { usedRegistryFallback, brokenPin } = resolvedMain;
950
+ // P1: the pointer is not a binding. A verb that acts on a vault runs with
951
+ // NO vault (and is refused at the gate) rather than on whatever vault the
952
+ // console last selected; onboarding/registry verbs keep the old fallback.
953
+ const pointerOnly =
954
+ usedRegistryFallback &&
955
+ !pointerVaultAllowed(process.env) &&
956
+ !pointerFallbackAllowedFor(namespace, command);
957
+ const vaultRoot = pointerOnly ? null : resolvedMain.vaultRoot;
958
+ if (pointerOnly) {
959
+ process.stderr.write(pointerOnlyRefusalLine(process.cwd(), resolvedMain.vaultRoot));
960
+ }
961
+ if (
962
+ brokenPin &&
963
+ namespace !== "doctor" &&
964
+ !pointerFallbackAllowedFor(namespace, command)
965
+ ) {
966
+ process.stderr.write(brokenPinRefusalLine(brokenPin));
967
+ process.exit(2);
968
+ }
865
969
 
866
970
  // `hs init` writes the top-level heyspex.yaml vault marker AND scaffolds
867
971
  // specs/ ([[HS-EPIC-AEHR-audit-export-heyspex-yaml-and-heyspex-retirement]]
@@ -920,7 +1024,9 @@ function main(argv) {
920
1024
  dispatchSpexConsoleEntry(
921
1025
  "hs-install.ts",
922
1026
  argv,
923
- vaultRoot || process.cwd(),
1027
+ // In a committed-marker satellite the install target is the CODE repo
1028
+ // (MCP entry, hooks, commands), never the vault clone the marker names.
1029
+ markerSatelliteDirFrom(INVOKED_CWD) ?? (vaultRoot || process.cwd()),
924
1030
  {
925
1031
  HS_PKG_ROOT: PKG_ROOT,
926
1032
  },
@@ -965,20 +1071,33 @@ function main(argv) {
965
1071
  const SPAWN_KEYS = {
966
1072
  claude: "setup:claude",
967
1073
  cursor: "setup:cursor",
1074
+ copilot: "setup:copilot",
968
1075
  };
969
1076
  if (!agent || !Object.hasOwn(SPAWN_KEYS, agent)) {
970
1077
  process.stderr.write(
971
- `hs setup: expected \`claude\` or \`cursor\` (got \`${command ?? ""}\`)\n` +
1078
+ `hs setup: expected \`claude\`, \`cursor\` or \`copilot\` (got \`${command ?? ""}\`)\n` +
972
1079
  " hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
973
- " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n",
1080
+ " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n" +
1081
+ " hs setup copilot [--check|--uninstall|--print|--hooks] GitHub Copilot (plugin; --hooks for the cloud agent)\n",
974
1082
  );
975
1083
  process.exit(2);
976
1084
  }
977
1085
  const setupFlags = rest.filter(
978
1086
  (a) => a === "--check" || a === "--uninstall",
979
1087
  );
1088
+ if (agent === "copilot") {
1089
+ // Copilot takes more flags: --print, --hooks, --json, --target <dir>.
1090
+ setupFlags.length = 0;
1091
+ for (let i = 0; i < rest.length; i++) {
1092
+ const a = rest[i];
1093
+ if (["--check", "--uninstall", "--print", "--hooks", "--json"].includes(a)) setupFlags.push(a);
1094
+ else if (a === "--target" && rest[i + 1]) setupFlags.push(a, rest[++i]);
1095
+ else if (a.startsWith("--target=")) setupFlags.push(a);
1096
+ }
1097
+ }
980
1098
  const script = spawnTargetPath(SPAWN_KEYS[agent]);
981
- const target = vaultRoot || process.cwd();
1099
+ // Copilot sets up the repo you are standing in, never the (possibly sibling) vault root.
1100
+ const target = agent === "copilot" ? process.cwd() : vaultRoot || process.cwd();
982
1101
  const child = spawn(process.execPath, [script, ...setupFlags], {
983
1102
  cwd: target,
984
1103
  stdio: "inherit",
@@ -1197,6 +1316,18 @@ function main(argv) {
1197
1316
  process.exit(1);
1198
1317
  }
1199
1318
 
1319
+ // A committed satellite marker whose vault clone is NOT on this machine
1320
+ // resolves to the code repo itself (cloud-only binding). Verbs that write the
1321
+ // vault's files to disk must never scaffold specs/ into a code repo.
1322
+ if (
1323
+ ["pull", "push", "index"].includes(namespace) &&
1324
+ readMarkerSatellite(vaultRoot) &&
1325
+ !existsSync(path.join(vaultRoot, "specs"))
1326
+ ) {
1327
+ process.stderr.write(unclonedVaultRefusalLine(vaultRoot));
1328
+ process.exit(2);
1329
+ }
1330
+
1200
1331
  if (namespace === "index" && command === "rebuild") {
1201
1332
  dispatchSpexConsoleEntry("hs-index-rebuild.ts", rest, vaultRoot, {
1202
1333
  ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
@@ -1228,6 +1359,31 @@ function main(argv) {
1228
1359
  return;
1229
1360
  }
1230
1361
 
1362
+ // `hs import <source-dir>` — a doc tree becomes GROUNDED spec-import captures
1363
+ // in the cwd's vault (heyspex.yaml marker, never the account pointer): group-
1364
+ // intent pass, foundational-to-specific order, sequential Ground=Expand with
1365
+ // a running batch ledger, one intent-map doc, source links; never promotes
1366
+ // ([[HS-0GOV-43G9-CAP-the-spec-import-function-should-not-stop]]). Forwards
1367
+ // the full argv; the entrypoint drops the leading `import` token and resolves
1368
+ // a relative <source-dir> against HS_INVOKED_CWD.
1369
+ if (namespace === "import") {
1370
+ dispatchSpexConsoleEntry("hs-import.ts", argv, vaultRoot, {
1371
+ HS_INVOKED_CWD: INVOKED_CWD,
1372
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1373
+ });
1374
+ return;
1375
+ }
1376
+
1377
+ // `hs push` — disk -> cloud (SpexRelay local leg, HS-FCNH). Report-only
1378
+ // unless --apply; never creates specs, never pushes status, refuses over a
1379
+ // newer cloud copy. Forwards the full argv; the entrypoint drops `push`.
1380
+ if (namespace === "push") {
1381
+ dispatchSpexConsoleEntry("hs-vault-push.ts", argv, vaultRoot, {
1382
+ ...(vaultIdOverride ? { HS_VAULT_ID_OVERRIDE: vaultIdOverride } : {}),
1383
+ });
1384
+ return;
1385
+ }
1386
+
1231
1387
  // `hs milestone show <id>` — print a milestone, flagging needs-breakdown when
1232
1388
  // it has no epics ([[HS-TASK-HMSE]] / [[HS-EPIC-MNBD]]). Forwards the full
1233
1389
  // argv; the entrypoint drops the leading `milestone` token.
@@ -1357,7 +1513,9 @@ function main(argv) {
1357
1513
  " hs source status Read-only tree status + path-scoped stage plan\n" +
1358
1514
  " hs source preflight Alias of status\n" +
1359
1515
  " hs source commit Stage + commit per the preflight plan\n" +
1360
- " hs source push Push the committed branch\n" +
1516
+ " hs source push [branch] Push the committed branch (no branch: the shared session-branch resolver's target)\n" +
1517
+ " hs source branch declare [<branch>] | show\n" +
1518
+ " Declare the branch this session works on (cloud spec saves then land on it) / show the target\n" +
1361
1519
  " hs source log <spec-id> Commit-history lookup for a spec (console settlement,\n" +
1362
1520
  " falling back to local git log)\n" +
1363
1521
  " hs source identity [show] [--json]\n" +
@@ -1378,6 +1536,7 @@ function main(argv) {
1378
1536
  preflight: "source:preflight",
1379
1537
  commit: "source:commit",
1380
1538
  push: "source:push",
1539
+ branch: "source:branch",
1381
1540
  log: "source:log",
1382
1541
  identity: "source:identity",
1383
1542
  };
@@ -1648,13 +1807,20 @@ function main(argv) {
1648
1807
  process.exit(2);
1649
1808
  }
1650
1809
  const cliPath = spawnTargetPath("heyspex-mcp-cli");
1810
+ // The MCP engine resolves its vault by walking UP from ITS cwd. Launching
1811
+ // it with cwd = the HeySpex checkout made every `hs mcp call` read the
1812
+ // HeySpex vault no matter where the agent stood (found 2026-10-16 from
1813
+ // mlc-specs / mlc-app). Run it from the repo the caller is in; an explicit
1814
+ // HS_VAULT_ROOT keeps winning (the engine is started inside that vault).
1815
+ const mcpCwd = process.env.HS_VAULT_ROOT && vaultRoot ? vaultRoot : INVOKED_CWD;
1651
1816
  const child = spawn(process.execPath, [cliPath, command, ...rest], {
1652
- cwd: PKG_ROOT,
1817
+ cwd: mcpCwd,
1653
1818
  stdio: "inherit",
1654
1819
  env: {
1655
1820
  ...process.env,
1656
- HEYSPEX_VAULT_ROOT: vaultRoot,
1657
- HS_VAULT_ROOT: vaultRoot,
1821
+ CLAUDE_PROJECT_DIR: mcpCwd,
1822
+ HEYSPEX_VAULT_ROOT: vaultRoot ?? "",
1823
+ HS_VAULT_ROOT: vaultRoot ?? "",
1658
1824
  // One-shot MCP tool — no stdio loop, no stderr brief-tier noise in PS.
1659
1825
  HEYSPEX_DISABLE_STDIO_BOOTSTRAP: "1",
1660
1826
  },