heyspex 0.2.0 → 0.2.1

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 (145) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +163 -11
  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 +162 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +20 -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 +309 -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/lib/console-url-resolver.mjs +3 -2
  72. package/scripts/lib/customer-kit.mjs +327 -10
  73. package/scripts/lib/dev-mode.mjs +7 -6
  74. package/scripts/lib/directive-obligations.mjs +349 -0
  75. package/scripts/lib/hook-dispositions.mjs +210 -0
  76. package/scripts/lib/hook-vault-header.mjs +30 -0
  77. package/scripts/lib/hs-source-branch.mjs +196 -0
  78. package/scripts/lib/hs-source-log.mjs +2 -1
  79. package/scripts/lib/hs-source-preflight.mjs +18 -4
  80. package/scripts/lib/hs-source-push.mjs +386 -356
  81. package/scripts/lib/window-placement.mjs +109 -0
  82. package/spexharness/src/adapters/claude-code.mjs +11 -3
  83. package/spexharness/src/adapters/copilot.mjs +236 -0
  84. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  85. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  86. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  87. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  88. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  89. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  90. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  91. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  93. package/spexharness/src/intent-classifier.mjs +4 -2
  94. package/spexharness/src/intent-directive.mjs +626 -541
  95. package/spexharness/src/rule-table.mjs +31 -1
  96. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  97. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  98. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  99. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  100. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  101. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  102. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  103. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  104. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  105. package/vendor/heyspex-mcp/cli.mjs +7 -1
  106. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  107. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  108. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  109. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  110. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  111. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  112. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  113. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  114. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  115. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  116. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  117. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  118. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  119. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  120. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  121. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  122. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  123. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  124. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  125. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  126. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  127. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  128. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  129. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  130. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  131. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  132. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  133. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  134. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  135. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  136. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  137. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  138. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  139. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  140. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  141. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  142. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  143. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  144. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  145. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -65,6 +65,7 @@
65
65
  * behavior, so nothing regresses when no registry exists.
66
66
  */
67
67
 
68
+ import { heyspexHome } from '../../scripts/lib/heyspex-home.mjs';
68
69
  import { existsSync, readFileSync } from 'node:fs';
69
70
  import { homedir, tmpdir } from 'node:os';
70
71
  import { dirname, join, resolve } from 'node:path';
@@ -85,7 +86,7 @@ export const UNREGISTERED_VAULT_ID = 'unregistered';
85
86
  * @returns {string}
86
87
  */
87
88
  function vaultRegistryDir(opts = {}) {
88
- return opts.registryDir ?? join(homedir(), '.heyspex');
89
+ return opts.registryDir ?? join(heyspexHome(), '.heyspex');
89
90
  }
90
91
 
91
92
  /**
@@ -175,6 +176,33 @@ export function registryPointerVaultId(json) {
175
176
  return legacy && isNonEmptyString(legacy.active_vault_id) ? legacy.active_vault_id : null;
176
177
  }
177
178
 
179
+ /**
180
+ * The account the MACHINE-LOCAL registry row for `repoRootAbs` was registered
181
+ * under (`vaults[].account_id`), or null (no row, or the pre-account `legacy`
182
+ * bucket). The repo-to-account binding lives here - per machine, per user -
183
+ * and NEVER in the committed heyspex.yaml: a second developer with another
184
+ * account must not inherit the first one's account (owner ruling 2026-10-16).
185
+ *
186
+ * @param {string} repoRootAbs
187
+ * @param {{ registryDir?: string }} [opts]
188
+ * @returns {string|null}
189
+ */
190
+ export function registryAccountIdForRoot(repoRootAbs, opts = {}) {
191
+ if (!isNonEmptyString(repoRootAbs)) return null;
192
+ const file = vaultRegistryFilePath(opts);
193
+ let json;
194
+ try {
195
+ json = JSON.parse(readFileSync(file, 'utf8'));
196
+ } catch {
197
+ return null;
198
+ }
199
+ const target = normalizeRoot(repoRootAbs);
200
+ const rows = Array.isArray(json?.vaults) ? json.vaults : [];
201
+ const row = rows.find((v) => v && isNonEmptyString(v.repo_root_abs) && normalizeRoot(v.repo_root_abs) === target);
202
+ const id = row?.account_id;
203
+ return isNonEmptyString(id) && id !== LEGACY_REGISTRY_ACCOUNT_ID ? id.trim() : null;
204
+ }
205
+
178
206
  /** Find a vault row by id. Returns null when absent. */
179
207
  function findVaultById(registry, vaultId) {
180
208
  if (!registry || !isNonEmptyString(vaultId)) return null;
@@ -260,6 +288,46 @@ function isAbsolutePin(p) {
260
288
  * @returns {string|null} the attached vault's absolute repo root
261
289
  */
262
290
  export function readSatellitePin(dir) {
291
+ const d = inspectSatellitePin(dir);
292
+ return d && d.kind === 'pin' ? d.root : null;
293
+ }
294
+
295
+ /**
296
+ * Expand the client placeholders a PORTABLE pin may carry against the
297
+ * manifest's own directory: ${CLAUDE_PROJECT_DIR[:-default]} (Claude Code) and
298
+ * ${workspaceFolder} (Cursor). Anything else left as ${...} is unresolvable.
299
+ */
300
+ function expandPinPlaceholders(value, dir) {
301
+ return value.replace(/\$\{(?:CLAUDE_PROJECT_DIR|workspaceFolder)(?::-[^}]*)?\}/g, dir);
302
+ }
303
+
304
+ /**
305
+ * Classify the heyspex server's HEYSPEX_VAULT_ROOT in `dir`'s MCP manifests.
306
+ * Returns null when no manifest names a pin at all, else one of:
307
+ * { kind: 'pin', root, raw, source } an attachment to a vault OUTSIDE dir
308
+ * { kind: 'self' } ordinary install (pin is dir or under it)
309
+ * { kind: 'broken', raw, source, reason } a pin that cannot be resolved
310
+ * (unknown ${...} placeholder). A broken pin must REFUSE, never fall
311
+ * through to the account-wide pointer (owner principle P1).
312
+ *
313
+ * PORTABLE pins (owner principle P3, 2026-10-16): a pin may be RELATIVE
314
+ * ('../mlc-specs') or built on the project-dir placeholder
315
+ * ('${CLAUDE_PROJECT_DIR:-.}/../mlc-specs'); both resolve against the
316
+ * MANIFEST'S OWN directory, never the process cwd, so the committed manifest
317
+ * carries no machine-specific absolute path.
318
+ *
319
+ * @param {string} dir
320
+ */
321
+ export function inspectSatellitePin(dir) {
322
+ // ONE precedence rule, identical in the TS twin (satellite.ts
323
+ // readPinnedVaultRoot + the resolver walk): at each directory a committed
324
+ // satellite MARKER (heyspex.yaml with vault_path) beats any manifest pin; of
325
+ // the manifests, the first VALID outside pin in read order (.mcp.json, then
326
+ // .cursor/mcp.json) wins no matter what the other manifest says; a broken
327
+ // pin refuses only when no manifest carries a valid one; 'self' (an ordinary
328
+ // install) is the weakest signal. Shared table: satellite-precedence-fixtures.json.
329
+ let broken = null;
330
+ let self = false;
263
331
  for (const rel of SATELLITE_MANIFESTS) {
264
332
  let raw;
265
333
  try {
@@ -274,13 +342,58 @@ export function readSatellitePin(dir) {
274
342
  continue;
275
343
  }
276
344
  if (!isNonEmptyString(value)) continue;
345
+ const source = rel.join('/');
277
346
  const trimmed = value.trim();
278
- if (trimmed.includes('${')) continue;
279
- if (!isAbsolutePin(trimmed)) continue;
280
- const pinned = normalizeRoot(trimmed);
347
+ const expanded = expandPinPlaceholders(trimmed, dir);
348
+ if (expanded.includes('${')) {
349
+ broken ??= { kind: 'broken', raw: trimmed, source, reason: 'it contains a placeholder other than ${CLAUDE_PROJECT_DIR} / ${workspaceFolder}' };
350
+ continue;
351
+ }
352
+ if (/^[a-zA-Z]:(?![\\/])/.test(expanded)) {
353
+ broken ??= { kind: 'broken', raw: trimmed, source, reason: 'a drive-relative path (C:dir) names no single directory' };
354
+ continue;
355
+ }
356
+ const abs = isAbsolutePin(expanded) ? resolve(expanded) : resolve(dir, expanded);
357
+ const pinned = normalizeRoot(abs);
281
358
  const here = normalizeRoot(dir);
282
- if (pinned === here || pinned.startsWith(`${here}/`)) continue; // ordinary install
283
- return resolve(trimmed);
359
+ if (pinned === here || pinned.startsWith(`${here}/`)) {
360
+ self = true;
361
+ continue;
362
+ }
363
+ return { kind: 'pin', root: abs, raw: trimmed, source };
364
+ }
365
+ if (broken) return broken;
366
+ return self ? { kind: 'self' } : null;
367
+ }
368
+
369
+ /**
370
+ * Walk up from `startDir` looking for the NEAREST manifest pin that cannot be
371
+ * honoured: unresolvable placeholder, or a pin whose target is not a vault
372
+ * root on this machine. Returns null when the nearest pin (or marker) is fine
373
+ * or there is none. `hs` uses it to REFUSE instead of falling back to the
374
+ * account-wide pointer ([[HS-O40H]], owner principle P1).
375
+ *
376
+ * @param {string} startDir
377
+ * @param {string} [stopAt]
378
+ * @returns {{ dir: string, raw: string, source: string, reason: string }|null}
379
+ */
380
+ export function findBrokenSatellitePin(startDir, stopAt) {
381
+ let dir = resolve(startDir);
382
+ const boundary = isNonEmptyString(stopAt) ? resolve(stopAt) : null;
383
+ for (let i = 0; i < 256; i += 1) {
384
+ if (readMarkerSatellite(dir)) return null; // marker outranks any pin
385
+ const d = inspectSatellitePin(dir);
386
+ if (d?.kind === 'broken') return { dir, raw: d.raw, source: d.source, reason: d.reason };
387
+ if (d?.kind === 'pin') {
388
+ if (isVaultRoot(d.root)) return null;
389
+ return { dir, raw: d.raw, source: d.source, reason: `${d.root} is not a vault on this machine (no heyspex.yaml or specs/ there; clone the vault repo there or fix the pin)` };
390
+ }
391
+ if (d?.kind === 'self') return null;
392
+ if (readMarkerSatellite(dir) || isVaultRoot(dir)) return null;
393
+ if (boundary && dir === boundary) return null;
394
+ const parent = dirname(dir);
395
+ if (parent === dir) return null;
396
+ dir = parent;
284
397
  }
285
398
  return null;
286
399
  }
@@ -342,6 +455,59 @@ function readMarkerVaultId(dir) {
342
455
  }
343
456
  }
344
457
 
458
+ /**
459
+ * Extract `vault_path: <value>` from raw heyspex.yaml text, or `null`.
460
+ * The PORTABLE satellite declaration (owner principles P1/P3, 2026-10-16): a
461
+ * code repo (e.g. mlc-app) commits a `heyspex.yaml` carrying its `vault_id`
462
+ * plus a RELATIVE `vault_path` (e.g. `../mlc-specs`) to where the attached
463
+ * vault is usually cloned. No absolute path is ever committed. An absolute,
464
+ * empty or unexpanded `${...}` value is not a declaration (returns null).
465
+ *
466
+ * @param {string} content
467
+ * @returns {string|null}
468
+ */
469
+ export function parseMarkerVaultPath(content) {
470
+ const m = String(content ?? '').match(/^vault_path:[ \t]*(.+?)[ \t]*$/m);
471
+ if (!m) return null;
472
+ const v = m[1].trim().replace(/\s+#.*$/, '').replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1').trim();
473
+ if (!v || v.includes('${') || isAbsolutePin(v)) return null;
474
+ return v;
475
+ }
476
+
477
+ /**
478
+ * Is `<dir>/heyspex.yaml` a committed SATELLITE marker (it carries a relative
479
+ * `vault_path`)? Returns the attachment it declares, or `null`.
480
+ *
481
+ * - `vaultId`: the marker's `vault_id` (the vault this repo is bound to; the
482
+ * ONLY thing a fresh machine without the vault clone needs, because
483
+ * cloud-routed writes name the vault by id).
484
+ * - `vaultRootAbs`: `resolve(dir, vault_path)` when that directory exists on
485
+ * THIS machine and is a vault root whose own marker does not name a
486
+ * DIFFERENT vault id; otherwise `null` (not cloned here, or a different
487
+ * vault sits at that path - never read the wrong vault's disk).
488
+ *
489
+ * @param {string} dir
490
+ * @returns {{ vaultId: string|null, vaultPath: string, vaultRootAbs: string|null }|null}
491
+ */
492
+ export function readMarkerSatellite(dir) {
493
+ let raw;
494
+ try {
495
+ raw = readFileSync(join(dir, 'heyspex.yaml'), 'utf8');
496
+ } catch {
497
+ return null;
498
+ }
499
+ const vaultPath = parseMarkerVaultPath(raw);
500
+ if (!vaultPath) return null;
501
+ const vaultId = parseMarkerVaultId(raw);
502
+ const candidate = resolve(dir, vaultPath);
503
+ let vaultRootAbs = null;
504
+ if (normalizeRoot(candidate) !== normalizeRoot(dir) && isVaultRoot(candidate)) {
505
+ const theirs = readMarkerVaultId(candidate);
506
+ if (!vaultId || !theirs || theirs === vaultId) vaultRootAbs = candidate;
507
+ }
508
+ return { vaultId, vaultPath, vaultRootAbs };
509
+ }
510
+
345
511
  /**
346
512
  * The marker id and the registry row for the same root disagree. The marker
347
513
  * wins (it is the repo's own on-disk declaration, written by `hs init` /
@@ -371,12 +537,18 @@ function logMarkerRegistryMismatch(repoRootAbs, markerVaultId, registryVaultId)
371
537
  * always reaches the real filesystem root; tests use it to fence the walk
372
538
  * inside a disposable tmp fixture so ambient/unrelated directories higher
373
539
  * up the real tree cannot produce a false match.
374
- * @returns {{ repoRootAbs: string, satellite: boolean }|null}
540
+ * @returns {{ repoRootAbs: string, satellite: boolean, markerSatellite?: ReturnType<typeof readMarkerSatellite> }|null}
375
541
  */
376
542
  function findCwdVaultOrAttachment(startDir, stopAt) {
377
543
  let dir = startDir;
378
544
  const boundary = isNonEmptyString(stopAt) ? resolve(stopAt) : null;
379
545
  for (let i = 0; i < 256; i += 1) {
546
+ // Precedence (see inspectSatellitePin): committed satellite marker, then
547
+ // any valid manifest pin, then the generic vault-root test.
548
+ const marked = readMarkerSatellite(dir);
549
+ if (marked) {
550
+ return { repoRootAbs: marked.vaultRootAbs ?? dir, satellite: true, markerSatellite: marked };
551
+ }
380
552
  const pinned = readSatellitePin(dir);
381
553
  if (pinned) return { repoRootAbs: pinned, satellite: true };
382
554
  if (isVaultRoot(dir)) return { repoRootAbs: dir, satellite: false };
@@ -501,11 +673,21 @@ export function resolveActiveVault(options = {}) {
501
673
  // for the gate sites that consume this (all of them reject only
502
674
  // `'cwd-heuristic'`, so both labels are accepted identically).
503
675
  const pinnedRecord = findVaultByRoot(registry, cwdMatch.repoRootAbs);
504
- return finalize(
505
- pinnedRecord?.id ?? UNREGISTERED_VAULT_ID,
676
+ // A committed satellite marker names the vault by id: that id IS the
677
+ // binding (works on a fresh machine with no registry row and no clone).
678
+ const markerId = cwdMatch.markerSatellite?.vaultId ?? null;
679
+ const bound = finalize(
680
+ markerId ?? pinnedRecord?.id ?? UNREGISTERED_VAULT_ID,
506
681
  cwdMatch.repoRootAbs,
507
682
  'satellite-pin',
508
683
  );
684
+ // The marker names a vault that is NOT cloned on this machine: the
685
+ // binding is cloud-only. Disk readers must not report an empty vault -
686
+ // they answer from the cloud and say so (read-path.mjs).
687
+ if (cwdMatch.markerSatellite && !cwdMatch.markerSatellite.vaultRootAbs) {
688
+ return { ...bound, vaultCloned: false, vaultPath: cwdMatch.markerSatellite.vaultPath };
689
+ }
690
+ return bound;
509
691
  }
510
692
  // A heyspex.yaml `vault_id` IS the vault's id ([[HS-VVNQ]]): `hs init` /
511
693
  // `hs connect` wrote it there. Before this, the id came ONLY from a
@@ -561,6 +743,12 @@ export function resolveActiveVault(options = {}) {
561
743
  // anywhere up the tree) and no env var names one. This is how an MCP
562
744
  // restart picks up a CLI `hs vault use` switch without Neon. Mirrors the TS
563
745
  // resolver's account-pointer tier, sourced from the registry file here.
746
+ // The ORDER is pinned (decision memory `vault-resolver-precedence`); what a
747
+ // consumer does with `source: 'account-pointer'` is its own call — the
748
+ // console transport (console-fetch.mjs `vaultIdHeaderFor`) REFUSES
749
+ // vault-scoped requests on it (`VAULT_NOT_PINNED`) unless
750
+ // HEYSPEX_ALLOW_POINTER_VAULT=1, because the pointer mirrors whatever the
751
+ // owner last clicked in the console, not anything this repo declared.
564
752
  if (registry && isNonEmptyString(registry.active_vault_id)) {
565
753
  const record = findVaultById(registry, registry.active_vault_id);
566
754
  if (record) return finalize(record.id, record.repo_root_abs, 'account-pointer');
@@ -9,6 +9,7 @@
9
9
  // TypeScript, same as folder-trust-store.mjs). Never prints, logs or returns
10
10
  // token material: callers only ever see an identity verdict.
11
11
 
12
+ import { heyspexHome } from '../../scripts/lib/heyspex-home.mjs';
12
13
  import { homedir } from 'node:os';
13
14
 
14
15
  import { readBearer } from './credentials.mjs';
@@ -29,7 +30,7 @@ export const SESSION_PATH = '/api/mcp/session';
29
30
  * @param {{ env?: NodeJS.ProcessEnv | Record<string, string | undefined>, homeDir?: string, readFile?: (p: string) => string }} [opts]
30
31
  * @returns {string | null}
31
32
  */
32
- export function readStoredBearer({ env = process.env, homeDir = homedir(), readFile } = {}) {
33
+ export function readStoredBearer({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
33
34
  return readBearer({ env, homeDir, readFile }).token;
34
35
  }
35
36