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.
- package/README.md +157 -0
- package/bin/hs.mjs +1028 -0
- package/bin/postinstall.mjs +88 -0
- package/dev-factory/lib/fleet-readiness.mjs +274 -0
- package/dev-factory/lib/fleet-state.mjs +250 -0
- package/dev-factory/lib/sufficiency-score.mjs +441 -0
- package/dist/cli/hs-check.mjs +747 -0
- package/dist/cli/hs-claim.mjs +541 -0
- package/dist/cli/hs-connect.mjs +3436 -0
- package/dist/cli/hs-defer.mjs +8455 -0
- package/dist/cli/hs-env.mjs +36845 -0
- package/dist/cli/hs-init.mjs +1145 -0
- package/dist/cli/hs-install.mjs +1390 -0
- package/dist/cli/hs-join.mjs +38553 -0
- package/dist/cli/hs-vault-pull.mjs +7782 -0
- package/dist/cli/hs-vault.mjs +4166 -0
- package/dist/cli/hs-workspace.mjs +2898 -0
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
- package/docs/mcp-client-config.md +200 -0
- package/package.json +124 -0
- package/scripts/check-mcp-freshness.mjs +379 -0
- package/scripts/dev-server-status.mjs +722 -0
- package/scripts/gateway-skills-lib.mjs +421 -0
- package/scripts/lib/console-url-resolver.mjs +152 -0
- package/scripts/lib/dev-mode.mjs +369 -0
- package/scripts/lib/hs-link.mjs +154 -0
- package/scripts/lib/hs-source-commit.mjs +207 -0
- package/scripts/lib/hs-source-log.mjs +366 -0
- package/scripts/lib/hs-source-preflight.mjs +402 -0
- package/scripts/lib/hs-source-push.mjs +307 -0
- package/scripts/lib/hs-source-secret-scan.mjs +263 -0
- package/scripts/lib/standalone-doctor.mjs +89 -0
- package/scripts/vault-id-lib.mjs +173 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
- package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
- package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
- package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
- package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
- package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
- package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
- package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
- package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
- package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
- package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
- package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
- package/vendor/heyspex-mcp/cli.mjs +15754 -0
- package/vendor/heyspex-mcp/config-settings.mjs +419 -0
- package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
- package/vendor/heyspex-mcp/design-tools.mjs +449 -0
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
- package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
- package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
- package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
- package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
- package/vendor/heyspex-mcp/next-review.mjs +548 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
- package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
- package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
- package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
- package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
- package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
- package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
- package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
- package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
- package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
- package/vendor/heyspex-mcp/read-path.mjs +138 -0
- package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
- package/vendor/heyspex-mcp/register-spec.mjs +417 -0
- package/vendor/heyspex-mcp/research-focus.mjs +476 -0
- package/vendor/heyspex-mcp/research-tools.mjs +109 -0
- package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
- package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
- package/vendor/heyspex-mcp/review-focus.mjs +55 -0
- package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
- package/vendor/heyspex-mcp/run-front.mjs +60 -0
- package/vendor/heyspex-mcp/scheduler.mjs +813 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
- package/vendor/heyspex-mcp/split-research.mjs +832 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
- package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
- package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
- package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
- package/vendor/heyspex-mcp/why-now.mjs +64 -0
- package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vault-resolve.mjs — the MCP-side port of the console's active-vault resolver.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `spex-console/src/lib/vault-resolve/index.ts` (`resolveActiveVaultRoot`)
|
|
5
|
+
* so that, given the same registry/env state, the MCP face and the console face
|
|
6
|
+
* agree on which vault is active (same repo root; specs dir = repoRoot/specs).
|
|
7
|
+
* Replaces the divergent one-line resolution that used to live in `cli.mjs`
|
|
8
|
+
* `getConfig()` (`HEYSPEX_VAULT_ROOT || CLAUDE_PROJECT_DIR || cwd`) and the
|
|
9
|
+
* `hooks-engine.mjs` fallback (`HEYSPEX_VAULT_ROOT || cwd`). Convergence tracked
|
|
10
|
+
* under [[HS-ICDI-TASK]] / [[HS-PNH2-RES]].
|
|
11
|
+
*
|
|
12
|
+
* Pure Node ESM — only node:fs / node:path / node:os. NO import from
|
|
13
|
+
* spex-console, no TypeScript. The MCP process is standalone (Neon /
|
|
14
|
+
* DATABASE_URL may be absent), so this port does NOT make a network/account
|
|
15
|
+
* lookup. Instead it reads the offline registry file
|
|
16
|
+
* (`~/.heyspex/registry.json`, same shape as
|
|
17
|
+
* `spex-console/src/lib/vault-registry/registry-file.ts`) for tier-1
|
|
18
|
+
* (explicit vaultId) and tier-4 (the offline active-vault pointer). That
|
|
19
|
+
* offline pointer is how an MCP restart picks up a CLI `hs vault use` switch
|
|
20
|
+
* without Neon.
|
|
21
|
+
*
|
|
22
|
+
* Precedence stack (highest wins) — REORDERED by [[HS-C87T-0JNK-TASK]]: a
|
|
23
|
+
* packaged server started with cwd inside an empty target vault used to
|
|
24
|
+
* resolve to whatever vault another terminal/console last marked active on
|
|
25
|
+
* the machine (old tier order put the registry pointer above cwd), silently
|
|
26
|
+
* writing its bookkeeping into the WRONG repo. Now the folder the process was
|
|
27
|
+
* actually started in/under wins over the registry's stale-by-comparison
|
|
28
|
+
* pointer:
|
|
29
|
+
* 1. explicit vaultId -> registry lookup (vaults[].id)
|
|
30
|
+
* 2. HEYSPEX_VAULT_ROOT env -> repo root (legacy escape hatch)
|
|
31
|
+
* 2. HEYSPEX_VAULT_DIR env -> the specs/ dir itself (repo root = parent)
|
|
32
|
+
* 3. cwd-contained vault -> walk UP from cwd through every ancestor
|
|
33
|
+
* looking for a heyspex.yaml marker or a
|
|
34
|
+
* specs/ dir; the nearest match wins. Logs
|
|
35
|
+
* one stderr notice iff this differs from
|
|
36
|
+
* the registry's active vault (tier 4) —
|
|
37
|
+
* silent when they agree (the common case:
|
|
38
|
+
* the owner's own sessions start inside the
|
|
39
|
+
* platform repo, which is also the
|
|
40
|
+
* registry's active vault).
|
|
41
|
+
* 4. registry active_vault_id -> the offline active pointer (vaults[].id)
|
|
42
|
+
* — reached only when cwd sits inside no
|
|
43
|
+
* vault at all (tier 3 found nothing)
|
|
44
|
+
* 5. cwd fallback -> cwd itself (no marker found anywhere;
|
|
45
|
+
* preserves the pre-existing default)
|
|
46
|
+
*
|
|
47
|
+
* Resolution is SYNCHRONOUS (all inputs are fs/env) and, via getActiveConfig(),
|
|
48
|
+
* memoized once per process. If the registry can't be read, tier 4 simply
|
|
49
|
+
* finds nothing and resolution falls through to tier 5 — i.e. today's
|
|
50
|
+
* behavior, so nothing regresses when no registry exists.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
54
|
+
import { homedir } from 'node:os';
|
|
55
|
+
import { dirname, join, resolve } from 'node:path';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Placeholder vaultId for tiers 3-5, which resolve a filesystem path but have
|
|
59
|
+
* no registry row to source a real id from. Mirrors UNREGISTERED_VAULT_ID in
|
|
60
|
+
* the TS resolver.
|
|
61
|
+
*/
|
|
62
|
+
export const UNREGISTERED_VAULT_ID = 'unregistered';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Registry directory. Mirrors `registry-file.ts` `vaultRegistryDir`: default
|
|
66
|
+
* `~/.heyspex`, overridable via `opts.registryDir` (tests only — registry-file.ts
|
|
67
|
+
* uses an opts override, not an env var, so we do the same).
|
|
68
|
+
*
|
|
69
|
+
* @param {{ registryDir?: string }} [opts]
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function vaultRegistryDir(opts = {}) {
|
|
73
|
+
return opts.registryDir ?? join(homedir(), '.heyspex');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Full path to `registry.json` under the resolved registry directory.
|
|
78
|
+
* @param {{ registryDir?: string }} [opts]
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function vaultRegistryFilePath(opts = {}) {
|
|
82
|
+
return join(vaultRegistryDir(opts), 'registry.json');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isNonEmptyString(v) {
|
|
86
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read + defensively parse `~/.heyspex/registry.json`. A missing or malformed
|
|
91
|
+
* file returns `null` (never throws) so callers fall through to env/cwd tiers —
|
|
92
|
+
* mirroring registry-file.ts `loadVaultRegistry`'s fail-closed contract.
|
|
93
|
+
*
|
|
94
|
+
* @param {{ registryDir?: string }} [opts]
|
|
95
|
+
* @returns {{ vaults: Array<{ id: string, repo_root_abs: string }>, active_vault_id: string|null }|null}
|
|
96
|
+
*/
|
|
97
|
+
function loadRegistry(opts = {}) {
|
|
98
|
+
const filePath = vaultRegistryFilePath(opts);
|
|
99
|
+
if (!existsSync(filePath)) return null;
|
|
100
|
+
let json;
|
|
101
|
+
try {
|
|
102
|
+
json = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (!json || typeof json !== 'object') return null;
|
|
107
|
+
|
|
108
|
+
const vaultsRaw = Array.isArray(json.vaults) ? json.vaults : [];
|
|
109
|
+
const vaults = [];
|
|
110
|
+
for (const raw of vaultsRaw) {
|
|
111
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
112
|
+
if (!isNonEmptyString(raw.id)) continue;
|
|
113
|
+
if (!isNonEmptyString(raw.repo_root_abs)) continue;
|
|
114
|
+
vaults.push({ id: raw.id, repo_root_abs: raw.repo_root_abs });
|
|
115
|
+
}
|
|
116
|
+
const active_vault_id = isNonEmptyString(json.active_vault_id) ? json.active_vault_id : null;
|
|
117
|
+
return { vaults, active_vault_id };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Find a vault row by id. Returns null when absent. */
|
|
121
|
+
function findVaultById(registry, vaultId) {
|
|
122
|
+
if (!registry || !isNonEmptyString(vaultId)) return null;
|
|
123
|
+
return registry.vaults.find((v) => v.id === vaultId) ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Walk UP from `startDir` through every ancestor directory looking for a
|
|
128
|
+
* `heyspex.yaml` marker file or a `specs/` dir — the two on-disk signals that
|
|
129
|
+
* a folder IS a vault root (mirrors `hs init`'s marker + the entity-model's
|
|
130
|
+
* folder layout). Returns the nearest matching ancestor (closest to
|
|
131
|
+
* `startDir` wins), or `null` if the walk reaches the filesystem root (or
|
|
132
|
+
* `stopAt`, inclusive) with no match. Bounded to guard against any path
|
|
133
|
+
* pathology; real filesystems never nest this deep.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} startDir
|
|
136
|
+
* @param {string|undefined} stopAt - test-only walk boundary (checked, then
|
|
137
|
+
* the walk stops — never climbs past it). Unset in production, so the walk
|
|
138
|
+
* always reaches the real filesystem root; tests use it to fence the walk
|
|
139
|
+
* inside a disposable tmp fixture so ambient/unrelated directories higher up
|
|
140
|
+
* the real tree (e.g. a stray `specs` folder under the OS temp dir) can't
|
|
141
|
+
* produce a false match.
|
|
142
|
+
* @returns {string|null}
|
|
143
|
+
*/
|
|
144
|
+
function findCwdVaultRoot(startDir, stopAt) {
|
|
145
|
+
let dir = startDir;
|
|
146
|
+
const boundary = isNonEmptyString(stopAt) ? resolve(stopAt) : null;
|
|
147
|
+
for (let i = 0; i < 256; i += 1) {
|
|
148
|
+
if (existsSync(join(dir, 'heyspex.yaml')) || existsSync(join(dir, 'specs'))) {
|
|
149
|
+
return dir;
|
|
150
|
+
}
|
|
151
|
+
if (boundary && dir === boundary) break; // hit the test-only fence
|
|
152
|
+
const parent = dirname(dir);
|
|
153
|
+
if (parent === dir) break; // reached filesystem root
|
|
154
|
+
dir = parent;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* When the cwd-contained vault (tier 3) wins, tell the operator if that
|
|
161
|
+
* silently overrode the registry's active vault — the customer-visible
|
|
162
|
+
* failure this task fixes (packaged server writing bookkeeping into the
|
|
163
|
+
* WRONG repo with no warning). Prints nothing when there is no registry
|
|
164
|
+
* pointer, the pointer is stale/unresolvable, or the two vaults are the same
|
|
165
|
+
* repo (the owner's own sessions: cwd = the platform repo = the registry's
|
|
166
|
+
* active vault too — must stay silent and unaffected).
|
|
167
|
+
*
|
|
168
|
+
* @param {string} cwdVaultRoot
|
|
169
|
+
* @param {{ vaults: Array<{id:string, repo_root_abs:string}>, active_vault_id: string|null }|null} registry
|
|
170
|
+
*/
|
|
171
|
+
function maybeLogCwdOverrideNotice(cwdVaultRoot, registry) {
|
|
172
|
+
if (!registry || !isNonEmptyString(registry.active_vault_id)) return;
|
|
173
|
+
const activeRecord = findVaultById(registry, registry.active_vault_id);
|
|
174
|
+
if (!activeRecord) return;
|
|
175
|
+
const registryRoot = resolve(activeRecord.repo_root_abs);
|
|
176
|
+
const resolvedCwdRoot = resolve(cwdVaultRoot);
|
|
177
|
+
if (registryRoot === resolvedCwdRoot) return; // same vault — no notice
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
`[heyspex-mcp] vault resolution: started inside ${resolvedCwdRoot} — overriding the registry's active vault ${registryRoot}, using the cwd vault.\n`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Build the resolved shape from a repo root. specsDirAbs is ALWAYS repoRoot/specs
|
|
185
|
+
* regardless of which tier resolved the root — matching the TS resolver.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} vaultId
|
|
188
|
+
* @param {string} repoRootAbs
|
|
189
|
+
* @param {'explicit'|'account-pointer'|'env-vault-root'|'env-vault-dir'|'cwd-heuristic'} source
|
|
190
|
+
*/
|
|
191
|
+
function finalize(vaultId, repoRootAbs, source) {
|
|
192
|
+
const resolvedRoot = resolve(repoRootAbs);
|
|
193
|
+
return {
|
|
194
|
+
vaultId,
|
|
195
|
+
repoRootAbs: resolvedRoot,
|
|
196
|
+
specsDirAbs: join(resolvedRoot, 'specs'),
|
|
197
|
+
source,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the active vault synchronously via the 5-tier precedence stack.
|
|
203
|
+
*
|
|
204
|
+
* @param {{ vaultId?: string, cwd?: string, registryDir?: string, cwdWalkRoot?: string }} [options]
|
|
205
|
+
* - `vaultId`: explicit override (tier 1).
|
|
206
|
+
* - `cwd`: working dir for the cwd-contained-vault walk-up (tier 3; defaults
|
|
207
|
+
* to `CLAUDE_PROJECT_DIR` then `process.cwd()`).
|
|
208
|
+
* - `registryDir`: registry dir override (tests only; defaults to ~/.heyspex).
|
|
209
|
+
* - `cwdWalkRoot`: test-only fence for the tier-3 walk-up (see
|
|
210
|
+
* {@link findCwdVaultRoot}); production code never sets this.
|
|
211
|
+
* @returns {{ vaultId: string, repoRootAbs: string, specsDirAbs: string, source: string }}
|
|
212
|
+
*/
|
|
213
|
+
export function resolveActiveVault(options = {}) {
|
|
214
|
+
const registry = loadRegistry(options);
|
|
215
|
+
|
|
216
|
+
// Tier 1 — explicit vaultId (CLI --vault / caller override). Unchanged:
|
|
217
|
+
// an explicit ask always wins over everything else.
|
|
218
|
+
if (isNonEmptyString(options.vaultId)) {
|
|
219
|
+
const record = findVaultById(registry, options.vaultId);
|
|
220
|
+
if (record) return finalize(record.id, record.repo_root_abs, 'explicit');
|
|
221
|
+
// Unknown explicit id: cascade rather than fail closed (matches TS resolver).
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Tier 2 — HEYSPEX_VAULT_ROOT / HEYSPEX_VAULT_DIR env (legacy escape hatch).
|
|
225
|
+
// [[HS-C87T-0JNK-TASK]]: moved ABOVE both the cwd-contained vault and the
|
|
226
|
+
// registry pointer — an explicit env override must win "regardless" of
|
|
227
|
+
// where the process was started or what the registry says is active.
|
|
228
|
+
const envRoot = process.env.HEYSPEX_VAULT_ROOT;
|
|
229
|
+
if (isNonEmptyString(envRoot)) {
|
|
230
|
+
return finalize(UNREGISTERED_VAULT_ID, resolve(envRoot), 'env-vault-root');
|
|
231
|
+
}
|
|
232
|
+
const envDir = process.env.HEYSPEX_VAULT_DIR;
|
|
233
|
+
if (isNonEmptyString(envDir)) {
|
|
234
|
+
// Historically points AT the specs/ dir itself, so repo root = its parent
|
|
235
|
+
// (specsDirAbs then round-trips back).
|
|
236
|
+
const specsDirFromEnv = resolve(envDir);
|
|
237
|
+
return finalize(UNREGISTERED_VAULT_ID, dirname(specsDirFromEnv), 'env-vault-dir');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Tier 3 — cwd-contained vault (HS-C87T). Backward-compat note: the OLD
|
|
241
|
+
// getConfig() fell back to CLAUDE_PROJECT_DIR then process.cwd() as a bare
|
|
242
|
+
// repo root. We keep honoring CLAUDE_PROJECT_DIR as the cwd base (an
|
|
243
|
+
// explicit `options.cwd` caller/test override still takes precedence over
|
|
244
|
+
// it), but now WALK UP from that base looking for a heyspex.yaml marker or
|
|
245
|
+
// a specs/ dir at any ancestor level — not just the base and its immediate
|
|
246
|
+
// parent — so the vault the process was actually started inside/under wins
|
|
247
|
+
// over a stale registry pointer from a different terminal/console tab.
|
|
248
|
+
const cwdBase = resolve(
|
|
249
|
+
isNonEmptyString(options.cwd)
|
|
250
|
+
? options.cwd
|
|
251
|
+
: (isNonEmptyString(process.env.CLAUDE_PROJECT_DIR)
|
|
252
|
+
? process.env.CLAUDE_PROJECT_DIR
|
|
253
|
+
: process.cwd()),
|
|
254
|
+
);
|
|
255
|
+
const cwdVaultRoot = findCwdVaultRoot(cwdBase, options.cwdWalkRoot);
|
|
256
|
+
if (cwdVaultRoot) {
|
|
257
|
+
maybeLogCwdOverrideNotice(cwdVaultRoot, registry);
|
|
258
|
+
return finalize(UNREGISTERED_VAULT_ID, cwdVaultRoot, 'cwd-heuristic');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Tier 4 — registry active_vault_id (the OFFLINE active pointer). Reached
|
|
262
|
+
// only when cwd sits inside no vault at all (tier 3 found neither marker
|
|
263
|
+
// anywhere up the tree) — e.g. an ad-hoc script dir. This is how an MCP
|
|
264
|
+
// restart picks up a CLI `hs vault use` switch without Neon. Mirrors the TS
|
|
265
|
+
// resolver's account-pointer tier, sourced from the registry file here.
|
|
266
|
+
if (registry && isNonEmptyString(registry.active_vault_id)) {
|
|
267
|
+
const record = findVaultById(registry, registry.active_vault_id);
|
|
268
|
+
if (record) return finalize(record.id, record.repo_root_abs, 'account-pointer');
|
|
269
|
+
// Stale/corrupt pointer (no matching row): cascade rather than throw.
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Tier 5 — last-resort cwd fallback: no marker found anywhere up the tree,
|
|
273
|
+
// no registry pointer either. Preserves the pre-existing default value old
|
|
274
|
+
// getConfig() would have produced.
|
|
275
|
+
return finalize(UNREGISTERED_VAULT_ID, cwdBase, 'cwd-heuristic');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let _cached = null;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Cached, process-lifetime resolution. Resolves once (default options) and
|
|
282
|
+
* memoizes — the MCP process is spawned once at session start, so re-resolving
|
|
283
|
+
* per call buys nothing. Use {@link resolveActiveVault} directly for per-call
|
|
284
|
+
* overrides (e.g. an explicit --vault), or {@link resetActiveConfigCache} in
|
|
285
|
+
* tests.
|
|
286
|
+
*
|
|
287
|
+
* @returns {{ vaultId: string, repoRootAbs: string, specsDirAbs: string, source: string }}
|
|
288
|
+
*/
|
|
289
|
+
export function getActiveConfig() {
|
|
290
|
+
if (_cached === null) _cached = resolveActiveVault();
|
|
291
|
+
return _cached;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Test-only: clear the getActiveConfig() memo so a controlled env re-resolves. */
|
|
295
|
+
export function resetActiveConfigCache() {
|
|
296
|
+
_cached = null;
|
|
297
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless vault knob resolution for MCP — mirrors spex-console resolvers
|
|
3
|
+
* ([[HS-TASK-9PJV]]). Reads `spex-console/settings.json` first, then env, then
|
|
4
|
+
* shipped defaults. Repo root is the directory containing `spex-console/`.
|
|
5
|
+
*
|
|
6
|
+
* Headless mirror seam ([[HS-TASK-UEBN]]): optional `.heyspex/` JSON files are
|
|
7
|
+
* read-only fallbacks when the primary store is absent (free-tier headless MCP).
|
|
8
|
+
* Nothing in the codebase writes the mirror — the console persists only to
|
|
9
|
+
* `spex-console/settings.json` via PUT /api/settings. To sync manually:
|
|
10
|
+
* `cp spex-console/settings.json .heyspex/spex-console-settings.json`.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_USE_INDEXER = true;
|
|
16
|
+
export const DEFAULT_VAULT_FRONTMATTER_AUTO_REPAIR = true;
|
|
17
|
+
|
|
18
|
+
/** Repo-relative primary settings store (gitignored). */
|
|
19
|
+
export const SPEX_CONSOLE_SETTINGS_REL = join('spex-console', 'settings.json');
|
|
20
|
+
|
|
21
|
+
/** Canonical headless mirror filename under `.heyspex/`. */
|
|
22
|
+
export const SPEX_CONSOLE_SETTINGS_MIRROR_REL = join(
|
|
23
|
+
'.heyspex',
|
|
24
|
+
'spex-console-settings.json',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** Pre-rename mirror — read during transition only; do not write. */
|
|
28
|
+
export const SPEX_CONSOLE_SETTINGS_MIRROR_LEGACY_REL = join(
|
|
29
|
+
'.heyspex',
|
|
30
|
+
'console-settings.json',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
function readJsonSafe(filePath) {
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(filePath)) return null;
|
|
36
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Candidate settings paths in precedence order: primary store, canonical mirror,
|
|
44
|
+
* legacy mirror (back-compat). First existing readable JSON wins.
|
|
45
|
+
*/
|
|
46
|
+
export function spexConsoleSettingsCandidates(repoRoot) {
|
|
47
|
+
if (!repoRoot) return [];
|
|
48
|
+
return [
|
|
49
|
+
join(repoRoot, SPEX_CONSOLE_SETTINGS_REL),
|
|
50
|
+
join(repoRoot, SPEX_CONSOLE_SETTINGS_MIRROR_REL),
|
|
51
|
+
join(repoRoot, SPEX_CONSOLE_SETTINGS_MIRROR_LEGACY_REL),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readSpexConsoleSettingsRaw(repoRoot) {
|
|
56
|
+
for (const p of spexConsoleSettingsCandidates(repoRoot)) {
|
|
57
|
+
const raw = readJsonSafe(p);
|
|
58
|
+
if (raw && typeof raw === 'object') return raw;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseEnvTriState(raw) {
|
|
64
|
+
if (typeof raw !== 'string') return undefined;
|
|
65
|
+
const v = raw.trim().toLowerCase();
|
|
66
|
+
if (v === 'false' || v === '0' || v === 'off') return false;
|
|
67
|
+
if (v === 'true' || v === '1' || v === 'on') return true;
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveUseIndexerEnabled(repoRoot) {
|
|
72
|
+
const raw = readSpexConsoleSettingsRaw(repoRoot);
|
|
73
|
+
if (raw && typeof raw.useIndexer === 'boolean') return raw.useIndexer;
|
|
74
|
+
const env = parseEnvTriState(process.env.HEYSPEX_USE_INDEX);
|
|
75
|
+
if (env !== undefined) return env;
|
|
76
|
+
return DEFAULT_USE_INDEXER;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveVaultFrontmatterAutoRepairEnabled(repoRoot) {
|
|
80
|
+
const raw = readSpexConsoleSettingsRaw(repoRoot);
|
|
81
|
+
if (raw && typeof raw.vaultFrontmatterAutoRepair === 'boolean') {
|
|
82
|
+
return raw.vaultFrontmatterAutoRepair;
|
|
83
|
+
}
|
|
84
|
+
const env = parseEnvTriState(process.env.HEYSPEX_VAULT_AUTO_REPAIR);
|
|
85
|
+
if (env !== undefined) return env;
|
|
86
|
+
return DEFAULT_VAULT_FRONTMATTER_AUTO_REPAIR;
|
|
87
|
+
}
|