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.
- package/.claude-plugin/marketplace.json +14 -0
- package/bin/hs.mjs +181 -15
- package/dist/cli/hs-check.mjs +88 -46
- package/dist/cli/hs-claim.mjs +84 -39
- package/dist/cli/hs-connect.mjs +583 -129
- package/dist/cli/hs-defer.mjs +251 -30
- package/dist/cli/hs-dev.mjs +20 -8
- package/dist/cli/hs-env.mjs +62 -34
- package/dist/cli/hs-index-rebuild.mjs +288 -83
- package/dist/cli/hs-index-reconcile.mjs +397 -111
- package/dist/cli/hs-init.mjs +2143 -91
- package/dist/cli/hs-install.mjs +628 -106
- package/dist/cli/hs-join.mjs +686 -236
- package/dist/cli/hs-vault-pull.mjs +3189 -179
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +396 -178
- package/dist/cli/hs-workspace.mjs +289 -151
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
- package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
- package/docs/copilot-plugin-install.md +177 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/package.json +21 -2
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
- package/plugins/heyspex/README.md +312 -0
- package/plugins/heyspex/bin/run-hook.mjs +20 -2
- package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
- package/plugins/heyspex/hooks/hooks.json +94 -37
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
- package/scripts/claude-hooks/_run.mjs +316 -23
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
- package/scripts/claude-hooks/session-end.mjs +2 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
- package/scripts/cursor-hooks/manifest.mjs +46 -0
- package/scripts/heyspex-mode.mjs +20 -0
- package/scripts/hs-mode.mjs +1 -1
- package/scripts/hs-view.mjs +180 -17
- package/scripts/install-claude-code-hooks.mjs +3 -1
- package/scripts/install-copilot-hooks.mjs +288 -0
- package/scripts/lib/console-url-resolver.mjs +3 -2
- package/scripts/lib/customer-kit.mjs +327 -10
- package/scripts/lib/dev-mode.mjs +7 -6
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-log.mjs +2 -1
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -356
- package/scripts/lib/window-placement.mjs +109 -0
- package/spexharness/src/adapters/claude-code.mjs +11 -3
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/intent-classifier.mjs +4 -2
- package/spexharness/src/intent-directive.mjs +626 -541
- package/spexharness/src/rule-table.mjs +31 -1
- package/vendor/heyspex-mcp/account-switch.mjs +6 -5
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
- package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
- package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
- package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
- package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
- package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
- package/vendor/heyspex-mcp/cli.mjs +7 -1
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
- package/vendor/heyspex-mcp/console-transport.mjs +7 -1
- package/vendor/heyspex-mcp/credentials.mjs +9 -8
- package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
- package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
- package/vendor/heyspex-mcp/http-transport.mjs +2 -0
- package/vendor/heyspex-mcp/list-tools.mjs +1 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
- package/vendor/heyspex-mcp/plan-push.mjs +3 -2
- package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
- package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
- package/vendor/heyspex-mcp/promote-research.mjs +6 -0
- package/vendor/heyspex-mcp/read-path.mjs +162 -6
- package/vendor/heyspex-mcp/request-context.mjs +25 -1
- package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/session-owner.mjs +3 -2
- package/vendor/heyspex-mcp/session-tools.mjs +10 -0
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
- package/vendor/heyspex-mcp/split-research.mjs +3 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
- package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
- package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
- package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
- package/vendor/heyspex-mcp/vault-io.mjs +720 -724
- package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
- package/vendor/heyspex-mcp/whoami.mjs +2 -1
- 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(
|
|
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
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
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}/`))
|
|
283
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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 =
|
|
33
|
+
export function readStoredBearer({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
|
|
33
34
|
return readBearer({ env, homeDir, readFile }).token;
|
|
34
35
|
}
|
|
35
36
|
|