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
@@ -10,11 +10,26 @@ var __export = (target, all) => {
10
10
  __defProp(target, name, { get: all[name], enumerable: true });
11
11
  };
12
12
 
13
+ // scripts/lib/heyspex-home.mjs
14
+ import { homedir } from "node:os";
15
+ function heyspexHome(env = process.env) {
16
+ const override = env?.HEYSPEX_HOME;
17
+ if (typeof override === "string" && override.trim().length > 0) {
18
+ return override.trim();
19
+ }
20
+ return homedir();
21
+ }
22
+ var init_heyspex_home = __esm({
23
+ "scripts/lib/heyspex-home.mjs"() {
24
+ "use strict";
25
+ }
26
+ });
27
+
13
28
  // vendor/heyspex-mcp/credentials.mjs
14
29
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
- import os, { homedir } from "node:os";
30
+ import os, { homedir as homedir2 } from "node:os";
16
31
  import { dirname as dirname2, join as join2, resolve } from "node:path";
17
- function credentialsFilePath(homeDir = homedir()) {
32
+ function credentialsFilePath(homeDir = heyspexHome()) {
18
33
  return join2(homeDir, ".heyspex", "credentials.yaml");
19
34
  }
20
35
  function parseMcpToken(raw) {
@@ -127,14 +142,14 @@ function readLegacyConfigAccountId(read, homeDir) {
127
142
  const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
128
143
  return value || null;
129
144
  }
130
- function readBearer({ env = process.env, homeDir = homedir(), readFile: readFile2, accountId: accountId2 } = {}) {
145
+ function readBearer({ env = process.env, homeDir = heyspexHome(), readFile: readFile2, accountId: accountId2 } = {}) {
131
146
  const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
132
147
  const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
133
148
  const named = typeof accountId2 === "string" && accountId2.trim() ? accountId2.trim() : null;
134
149
  const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
135
150
  return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
136
151
  }
137
- function readCurrentAccountId({ homeDir = homedir(), readFile: readFile2 } = {}) {
152
+ function readCurrentAccountId({ homeDir = heyspexHome(), readFile: readFile2 } = {}) {
138
153
  const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
139
154
  const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
140
155
  if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
@@ -152,6 +167,7 @@ var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT
152
167
  var init_credentials = __esm({
153
168
  "vendor/heyspex-mcp/credentials.mjs"() {
154
169
  "use strict";
170
+ init_heyspex_home();
155
171
  MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
156
172
  MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
157
173
  CREDENTIALS_SCHEMA_VERSION = 2;
@@ -164,9 +180,9 @@ var init_credentials = __esm({
164
180
 
165
181
  // vendor/heyspex-mcp/session-owner.mjs
166
182
  import { readFileSync as readFileSync2 } from "node:fs";
167
- import { homedir as homedir2 } from "node:os";
183
+ import { homedir as homedir3 } from "node:os";
168
184
  import { join as join3 } from "node:path";
169
- function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile: readFile2 } = {}) {
185
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile: readFile2 } = {}) {
170
186
  const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
171
187
  if (fromEnv) return fromEnv;
172
188
  return readCurrentAccountId({ homeDir, readFile: readFile2 });
@@ -174,10 +190,26 @@ function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(),
174
190
  var init_session_owner = __esm({
175
191
  "vendor/heyspex-mcp/session-owner.mjs"() {
176
192
  "use strict";
193
+ init_heyspex_home();
177
194
  init_credentials();
178
195
  }
179
196
  });
180
197
 
198
+ // spex-console/src/lib/heyspex-home.ts
199
+ import { homedir as homedir6 } from "node:os";
200
+ function heyspexHome2(env = process.env) {
201
+ const override = env?.HEYSPEX_HOME;
202
+ if (typeof override === "string" && override.trim().length > 0) {
203
+ return override.trim();
204
+ }
205
+ return homedir6();
206
+ }
207
+ var init_heyspex_home2 = __esm({
208
+ "spex-console/src/lib/heyspex-home.ts"() {
209
+ "use strict";
210
+ }
211
+ });
212
+
181
213
  // spex-console/src/lib/vault-registry/types.ts
182
214
  function emptyVaultRegistry() {
183
215
  return {
@@ -198,14 +230,13 @@ var init_types = __esm({
198
230
  });
199
231
 
200
232
  // spex-console/src/lib/vault-registry/registry-file.ts
201
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
202
- import { homedir as homedir5 } from "node:os";
203
- import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
233
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
234
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
204
235
  function vaultRegistryDir(opts = {}) {
205
- return opts.registryDir ?? join6(homedir5(), ".heyspex");
236
+ return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
206
237
  }
207
238
  function vaultRegistryFilePath(opts = {}) {
208
- return join6(vaultRegistryDir(opts), "registry.json");
239
+ return join7(vaultRegistryDir(opts), "registry.json");
209
240
  }
210
241
  function isNonEmptyString(v) {
211
242
  return typeof v === "string" && v.trim().length > 0;
@@ -306,9 +337,9 @@ function parseVaultRegistryFile(json) {
306
337
  }
307
338
  function readRegistryJson(opts) {
308
339
  const filePath = vaultRegistryFilePath(opts);
309
- if (!existsSync3(filePath)) return { kind: "missing" };
340
+ if (!existsSync4(filePath)) return { kind: "missing" };
310
341
  try {
311
- const raw = readFileSync5(filePath, "utf8");
342
+ const raw = readFileSync6(filePath, "utf8");
312
343
  const json = JSON.parse(raw);
313
344
  if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
314
345
  return { kind: "ok", raw, json };
@@ -362,14 +393,14 @@ function underTestRunner() {
362
393
  }
363
394
  function samePathLoose(a, b) {
364
395
  const norm = (p) => {
365
- const r = resolve3(p).replace(/[\\/]+$/, "");
396
+ const r = resolve4(p).replace(/[\\/]+$/, "");
366
397
  return process.platform === "win32" ? r.toLowerCase() : r;
367
398
  };
368
399
  return norm(a) === norm(b);
369
400
  }
370
401
  function assertNotRealHomeRegistryUnderTest(filePath) {
371
402
  if (!underTestRunner()) return;
372
- if (samePathLoose(dirname4(filePath), join6(homedir5(), ".heyspex"))) {
403
+ if (samePathLoose(dirname4(filePath), join7(heyspexHome2(), ".heyspex"))) {
373
404
  throw new Error(
374
405
  `registry-file: refusing to write ${filePath} under a test runner \u2014 that is the real ~/.heyspex registry. Pass opts.registryDir (a temp dir).`
375
406
  );
@@ -378,10 +409,10 @@ function assertNotRealHomeRegistryUnderTest(filePath) {
378
409
  function writeVaultRegistry(registry, opts = {}) {
379
410
  const filePath = vaultRegistryFilePath(opts);
380
411
  assertNotRealHomeRegistryUnderTest(filePath);
381
- mkdirSync2(dirname4(filePath), { recursive: true });
412
+ mkdirSync3(dirname4(filePath), { recursive: true });
382
413
  const tmpPath = `${filePath}.tmp`;
383
- writeFileSync2(tmpPath, serializeVaultRegistry(registry), "utf8");
384
- renameSync2(tmpPath, filePath);
414
+ writeFileSync3(tmpPath, serializeVaultRegistry(registry), "utf8");
415
+ renameSync3(tmpPath, filePath);
385
416
  }
386
417
  function isValidId(id) {
387
418
  if (!isNonEmptyString(id)) return false;
@@ -550,10 +581,10 @@ function rekeyVaultRowsToCloudIds(registry, proofs, plan) {
550
581
  };
551
582
  }
552
583
  function planRegistryMigration(registry, opts = {}) {
553
- const pathExists = opts.pathExists ?? ((p) => existsSync3(p));
584
+ const pathExists = opts.pathExists ?? ((p) => existsSync4(p));
554
585
  const readMarker = opts.readMarker ?? ((root) => {
555
586
  try {
556
- return readFileSync5(join6(root, "heyspex.yaml"), "utf8");
587
+ return readFileSync6(join7(root, "heyspex.yaml"), "utf8");
557
588
  } catch {
558
589
  return null;
559
590
  }
@@ -687,6 +718,7 @@ var emptyPlan;
687
718
  var init_registry_file = __esm({
688
719
  "spex-console/src/lib/vault-registry/registry-file.ts"() {
689
720
  "use strict";
721
+ init_heyspex_home2();
690
722
  init_types();
691
723
  init_types();
692
724
  emptyPlan = (current) => ({
@@ -703,11 +735,11 @@ var init_registry_file = __esm({
703
735
  });
704
736
 
705
737
  // scripts/lib/dev-mode.mjs
706
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
707
- import { homedir as homedir6 } from "node:os";
708
- import { join as join7 } from "node:path";
709
- function devModeFilePath(homeDir = homedir6()) {
710
- return join7(homeDir, ".heyspex", DEV_MODE_FILENAME);
738
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
739
+ import { homedir as homedir7 } from "node:os";
740
+ import { join as join8 } from "node:path";
741
+ function devModeFilePath(homeDir = heyspexHome()) {
742
+ return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
711
743
  }
712
744
  function normalizeMode(raw) {
713
745
  const v = String(raw ?? "").trim().toLowerCase();
@@ -731,7 +763,7 @@ function deriveSemantics(mode, reliance) {
731
763
  vaultWriteMode: local ? "local" : "cloud"
732
764
  };
733
765
  }
734
- function readLocalDevModeRecord({ homeDir = homedir6(), readFile: readFile2 = defaultReadFile } = {}) {
766
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile } = {}) {
735
767
  let raw;
736
768
  try {
737
769
  raw = readFile2(devModeFilePath(homeDir));
@@ -761,7 +793,7 @@ function readLocalDevModeRecord({ homeDir = homedir6(), readFile: readFile2 = de
761
793
  }
762
794
  function resolveDevMode({
763
795
  env = process.env,
764
- homeDir = homedir6(),
796
+ homeDir = heyspexHome(),
765
797
  readFile: readFile2 = defaultReadFile,
766
798
  cloudSetting = void 0
767
799
  } = {}) {
@@ -824,11 +856,12 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
824
856
  var init_dev_mode = __esm({
825
857
  "scripts/lib/dev-mode.mjs"() {
826
858
  "use strict";
859
+ init_heyspex_home();
827
860
  init_credentials();
828
861
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
829
862
  LOCAL_CONSOLE_URL = "http://localhost:3000";
830
863
  DEV_MODE_FILENAME = "dev-mode.json";
831
- defaultReadFile = (p) => readFileSync6(p, "utf8");
864
+ defaultReadFile = (p) => readFileSync7(p, "utf8");
832
865
  }
833
866
  });
834
867
 
@@ -903,13 +936,12 @@ var init_default_workspace = __esm({
903
936
  });
904
937
 
905
938
  // spex-console/src/lib/cli/vault-workspace-shared.ts
906
- import { readFileSync as readFileSync8 } from "node:fs";
907
- import os2 from "node:os";
939
+ import { readFileSync as readFileSync9 } from "node:fs";
908
940
  function resolveConfiguredAccountId(opts = {}) {
909
941
  return resolveSessionOwnerUncached({
910
942
  env: opts.env ?? process.env,
911
- homeDir: opts.homeDir ?? os2.homedir(),
912
- readFile: opts.readFile ?? ((p) => readFileSync8(p, "utf-8"))
943
+ homeDir: opts.homeDir ?? heyspexHome2(),
944
+ readFile: opts.readFile ?? ((p) => readFileSync9(p, "utf-8"))
913
945
  });
914
946
  }
915
947
  function resolveCloudAccountIdOrRefuse(opts = {}) {
@@ -919,6 +951,9 @@ function resolveCloudAccountIdOrRefuse(opts = {}) {
919
951
  function vaultOwnedByOtherAccountMessage(id) {
920
952
  return `vault "${id}" is already registered to another HeySpex account, so nothing was changed. To move it to this account: sign in as the account that owns it (\`hs login --switch-account\`), remove it there (\`hs vault unregister ${id}\` or the console's vault settings; this deletes only the registry row, never repo files, and archiving alone keeps the id taken), then sign back in here and re-run this command. Or keep both by registering this repo under a new vault id (\`hs vault register --path . --id <new-id>\`). Transferring a vault between accounts is not supported.`;
921
953
  }
954
+ function vaultNotOwnedByCallerMessage(id) {
955
+ return `vault "${id}" belongs to another HeySpex account, so nothing was changed. Only the account that owns a vault can rename, move, archive, restore or unregister it (\`hs login --switch-account\` to act as that account).`;
956
+ }
922
957
  function workspaceOwnedByOtherAccountMessage(id) {
923
958
  return `workspace "${id}" belongs to another HeySpex account, so nothing was changed. Pick a different workspace id (e.g. \`--workspace <new-id>\`), or sign in as the account that owns it (\`hs login --switch-account\`). Transferring a workspace between accounts is not supported.`;
924
959
  }
@@ -1005,8 +1040,14 @@ async function mutateHttp(http, method, path5, body, landed) {
1005
1040
  const res = await cloudHttpRequest(http, method, path5, body);
1006
1041
  if (res.ok) return landed;
1007
1042
  if (res.status === 404) return "not_found";
1043
+ if (res.status === 403 && isOwnedByOtherAccountCode(errorCodeOf(res.json))) {
1044
+ return "owned_by_other_account";
1045
+ }
1008
1046
  return cloudWriteFailure(http, res);
1009
1047
  }
1048
+ function isOwnedByOtherAccountCode(code) {
1049
+ return code === VAULT_OWNED_BY_OTHER_ACCOUNT || code === WORKSPACE_OWNED_BY_OTHER_ACCOUNT;
1050
+ }
1010
1051
  function manageVerbPath(kind, id) {
1011
1052
  return `/api/${kind}/${encodeURIComponent(id)}`;
1012
1053
  }
@@ -1014,6 +1055,7 @@ var CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE, VAULT_OWNED_BY_OTHER_ACCOUNT, WORKSPACE_O
1014
1055
  var init_vault_workspace_shared = __esm({
1015
1056
  "spex-console/src/lib/cli/vault-workspace-shared.ts"() {
1016
1057
  "use strict";
1058
+ init_heyspex_home2();
1017
1059
  init_session_owner();
1018
1060
  init_default_workspace();
1019
1061
  CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE = "refusing -- no signed-in account (no HS_ACCOUNT_ID env var, and no account_id: in ~/.heyspex/config.yaml). Run `hs login` first; nothing was written.";
@@ -1058,7 +1100,7 @@ var init_interfaces = __esm({
1058
1100
  });
1059
1101
 
1060
1102
  // spex-console/src/lib/db/schema/postgres.ts
1061
- var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, tsvector, specs, tags, edges, wikilinks, specLinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, fileClaims, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, consoleSettingsStore, wipLedger, wipLedgerArchive, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, pendingVaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements;
1103
+ var edgeTypeEnum, validationSeverityEnum, outboxOpEnum, tsvector, specs, tags, edges, wikilinks, specLinks, aliases, validationIssues, indexMeta, frontmatterFields, eventOutbox, captureSessions, handoffContext, claimEventKindEnum, activeClaimStatusEnum, pendingReasonEnum, focusEventKindEnum, specEventKindEnum, focusPin, captureFocusPin, captureFocusSelection, focusSelection, focusSkips, activeClaims, claimWaiters, fileClaims, claimEvents, focusEvents, focusAreas, focusAreasEvents, specEvents, rateLimitBuckets, userVaults, milestoneSnapshots, modelRegistryEntries, launchIntentStatusEnum, launchIntents, companionRelayPresence, companionSettingsStore, consoleSettingsStore, wipLedger, wipLedgerArchive, gatherResults, agentSessions, usageEvents, streamEvents, jobStreams, resolutionStatusEnum, resolutionFocusPin, resolutionLedger, workspaces, vaults, accountSettings, vaultGrants, pendingVaultGrants, githubInstallations, fleetMachines, fleetTasks, fleetResults, userApiKeys, apiKeys, mcpSessions, pairingCodes, companionSessions, inviteRequests, notifications, webhookIngestions, specSettlements, sessionBranchDeclarations;
1062
1104
  var init_postgres = __esm({
1063
1105
  "spex-console/src/lib/db/schema/postgres.ts"() {
1064
1106
  "use strict";
@@ -1500,7 +1542,13 @@ var init_postgres = __esm({
1500
1542
  "priority_change",
1501
1543
  "parent_change",
1502
1544
  "claim_acquired",
1503
- "claim_released"
1545
+ "claim_released",
1546
+ // Collaborator write audit (HS-FWB6, migration 0077).
1547
+ "spec_created",
1548
+ "spec_edited",
1549
+ "spec_deleted",
1550
+ "capture_written",
1551
+ "wip_recorded"
1504
1552
  ]);
1505
1553
  focusPin = pgTable(
1506
1554
  "focus_pin",
@@ -2693,11 +2741,43 @@ var init_postgres = __esm({
2693
2741
  // `owner/name`
2694
2742
  branch: text("branch").notNull(),
2695
2743
  commitSha: text("commit_sha").notNull(),
2696
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
2744
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
2745
+ // The pull request that carries this settlement
2746
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
2747
+ // the branch has gone quiet and a PR was opened or found; a write made
2748
+ // after its PR merged stays NULL, which is what triggers the next PR.
2749
+ pullRequestNumber: integer("pull_request_number"),
2750
+ pullRequestUrl: text("pull_request_url")
2697
2751
  },
2698
2752
  (table) => [
2699
2753
  // "latest settlement for this spec" — the read path's only query.
2700
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
2754
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
2755
+ // The PR sweep's only query: branches with settlements not yet in a PR.
2756
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
2757
+ ]
2758
+ );
2759
+ sessionBranchDeclarations = pgTable(
2760
+ "session_branch_declarations",
2761
+ {
2762
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
2763
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
2764
+ actorKey: text("actor_key").notNull(),
2765
+ sessionId: text("session_id").notNull(),
2766
+ branch: text("branch").notNull(),
2767
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
2768
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
2769
+ },
2770
+ (table) => [
2771
+ uniqueIndex("session_branch_declarations_uq").on(
2772
+ table.vaultId,
2773
+ table.actorKey,
2774
+ table.sessionId
2775
+ ),
2776
+ index("session_branch_declarations_lookup_idx").on(
2777
+ table.vaultId,
2778
+ table.actorKey,
2779
+ table.declaredAt.desc()
2780
+ )
2701
2781
  ]
2702
2782
  );
2703
2783
  }
@@ -3352,7 +3432,7 @@ var init_spec_read_service = __esm({
3352
3432
  });
3353
3433
 
3354
3434
  // spex-console/bin/hs-vault.ts
3355
- import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
3435
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
3356
3436
  import path4 from "node:path";
3357
3437
  import process4 from "node:process";
3358
3438
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -3379,11 +3459,12 @@ function shellQuote(p) {
3379
3459
  var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
3380
3460
 
3381
3461
  // vendor/heyspex-mcp/console-fetch.mjs
3462
+ init_heyspex_home();
3382
3463
  init_credentials();
3383
3464
  init_session_owner();
3384
- import { readFileSync as readFileSync4 } from "node:fs";
3385
- import { homedir as homedir4 } from "node:os";
3386
- import { join as join5 } from "node:path";
3465
+ import { readFileSync as readFileSync5 } from "node:fs";
3466
+ import { homedir as homedir5 } from "node:os";
3467
+ import { join as join6 } from "node:path";
3387
3468
 
3388
3469
  // vendor/heyspex-mcp/vault-less-endpoints.mjs
3389
3470
  var VAULT_LESS_ENDPOINTS = Object.freeze([
@@ -3409,10 +3490,41 @@ function isVaultLessEndpoint(method, path5) {
3409
3490
  });
3410
3491
  }
3411
3492
 
3493
+ // vendor/heyspex-mcp/write-outbox.mjs
3494
+ import {
3495
+ readdirSync,
3496
+ readFileSync as readFileSync3,
3497
+ writeFileSync as writeFileSync2,
3498
+ mkdirSync as mkdirSync2,
3499
+ existsSync as existsSync2,
3500
+ renameSync as renameSync2,
3501
+ rmSync
3502
+ } from "node:fs";
3503
+ import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
3504
+
3505
+ // scripts/lib/shared-path-helpers.mjs
3506
+ import { isAbsolute, resolve as resolve2, sep } from "node:path";
3507
+ function resolveCodeRoot({
3508
+ explicit,
3509
+ packageRoot,
3510
+ vaultRoot,
3511
+ env = process.env
3512
+ } = {}) {
3513
+ const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
3514
+ if (fromExplicit) return fromExplicit;
3515
+ const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
3516
+ if (fromEnv) return fromEnv;
3517
+ const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
3518
+ if (fromPackage) return fromPackage;
3519
+ const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
3520
+ return fromVault;
3521
+ }
3522
+
3412
3523
  // vendor/heyspex-mcp/vault-resolve.mjs
3413
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
3414
- import { homedir as homedir3, tmpdir } from "node:os";
3415
- import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
3524
+ init_heyspex_home();
3525
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
3526
+ import { homedir as homedir4, tmpdir } from "node:os";
3527
+ import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
3416
3528
 
3417
3529
  // vendor/heyspex-mcp/console-fetch.mjs
3418
3530
  var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
@@ -3540,10 +3652,11 @@ function withCloudScope(deps, accountId2, vaultId) {
3540
3652
  }
3541
3653
 
3542
3654
  // scripts/lib/console-url-resolver.mjs
3655
+ init_heyspex_home();
3543
3656
  init_dev_mode();
3544
- import { readFileSync as readFileSync7 } from "node:fs";
3545
- import { homedir as homedir7 } from "node:os";
3546
- import { join as join8 } from "node:path";
3657
+ import { readFileSync as readFileSync8 } from "node:fs";
3658
+ import { homedir as homedir8 } from "node:os";
3659
+ import { join as join9 } from "node:path";
3547
3660
  var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
3548
3661
  function trimTrailingSlash(url) {
3549
3662
  return url.replace(/\/+$/, "");
@@ -3569,9 +3682,9 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
3569
3682
  } catch {
3570
3683
  }
3571
3684
  }
3572
- function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile: readFile2 = defaultReadFile2 } = {}) {
3685
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile2 } = {}) {
3573
3686
  try {
3574
- const raw = readFile2(join8(homeDir, ".heyspex", "credentials.yaml"));
3687
+ const raw = readFile2(join9(homeDir, ".heyspex", "credentials.yaml"));
3575
3688
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
3576
3689
  const value = match?.[1]?.trim();
3577
3690
  return value && value.length > 0 ? trimTrailingSlash(value) : null;
@@ -3580,12 +3693,12 @@ function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile: readFile2 =
3580
3693
  }
3581
3694
  }
3582
3695
  function defaultReadFile2(p) {
3583
- return readFileSync7(p, "utf8");
3696
+ return readFileSync8(p, "utf8");
3584
3697
  }
3585
3698
  function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
3586
3699
  if (!repoRoot) return null;
3587
3700
  try {
3588
- const discovery = String(readDiscovery(join8(repoRoot, ".heyspex", "console-url")) ?? "").trim();
3701
+ const discovery = String(readDiscovery(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
3589
3702
  if (discovery.length > 0) return trimTrailingSlash(discovery);
3590
3703
  } catch {
3591
3704
  }
@@ -3613,7 +3726,7 @@ function resolveModeEnvelope({ env, homeDir }) {
3613
3726
  function resolveConsoleUrl({
3614
3727
  repoRoot,
3615
3728
  env = process.env,
3616
- homeDir = homedir7(),
3729
+ homeDir = heyspexHome(),
3617
3730
  strict = false,
3618
3731
  devMode,
3619
3732
  log = (line) => process.stderr.write(line),
@@ -3677,37 +3790,19 @@ var HELP = [
3677
3790
  ].join("\n");
3678
3791
 
3679
3792
  // spex-console/src/lib/cli/install-identity.ts
3793
+ init_heyspex_home2();
3794
+ init_registry_file();
3680
3795
  import { spawnSync } from "node:child_process";
3681
- import { existsSync as existsSync4 } from "node:fs";
3682
- import { homedir as homedir9 } from "node:os";
3683
- import { join as join9 } from "node:path";
3796
+ import { existsSync as existsSync5 } from "node:fs";
3797
+ import { join as join10 } from "node:path";
3684
3798
  import { createInterface } from "node:readline/promises";
3685
-
3686
- // scripts/lib/shared-path-helpers.mjs
3687
- import { isAbsolute, resolve as resolve4, sep } from "node:path";
3688
- function resolveCodeRoot({
3689
- explicit,
3690
- packageRoot,
3691
- vaultRoot,
3692
- env = process.env
3693
- } = {}) {
3694
- const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
3695
- if (fromExplicit) return fromExplicit;
3696
- const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
3697
- if (fromEnv) return fromEnv;
3698
- const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
3699
- if (fromPackage) return fromPackage;
3700
- const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
3701
- return fromVault;
3702
- }
3703
-
3704
- // spex-console/src/lib/cli/install-identity.ts
3705
3799
  init_credentials();
3706
3800
  init_session_owner();
3707
3801
 
3708
3802
  // vendor/heyspex-mcp/whoami.mjs
3803
+ init_heyspex_home();
3709
3804
  init_credentials();
3710
- import { homedir as homedir8 } from "node:os";
3805
+ import { homedir as homedir9 } from "node:os";
3711
3806
  var SESSION_PATH = "/api/mcp/session";
3712
3807
  async function probeCredentialIdentity({ consoleUrl, token, fetchImpl = fetch } = {}) {
3713
3808
  if (!token) return { kind: "no_credential" };
@@ -3809,6 +3904,81 @@ var HELP2 = [
3809
3904
  "Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
3810
3905
  ].join("\n");
3811
3906
 
3907
+ // spex-console/src/lib/cli/satellite.ts
3908
+ init_registry_file();
3909
+ import { resolve as resolveNodePath } from "node:path";
3910
+ import process2 from "node:process";
3911
+ var SATELLITE_VAULT_ENV = "HEYSPEX_VAULT_ROOT";
3912
+ var SATELLITE_MANIFEST_RELS = [
3913
+ [".mcp.json"],
3914
+ [".cursor", "mcp.json"]
3915
+ ];
3916
+ function normalizePathForCompare(p) {
3917
+ return p.trim().replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
3918
+ }
3919
+ function samePath(a, b) {
3920
+ return normalizePathForCompare(a) === normalizePathForCompare(b);
3921
+ }
3922
+ function isAtOrUnder(child, parent) {
3923
+ const c = normalizePathForCompare(child);
3924
+ const p = normalizePathForCompare(parent);
3925
+ return c === p || c.startsWith(`${p}/`);
3926
+ }
3927
+ function isAbsolutePathForCompare(p) {
3928
+ const s = p.trim();
3929
+ return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
3930
+ }
3931
+ function expandPinPlaceholders(value, repoRoot) {
3932
+ return value.replace(/\$\{(?:CLAUDE_PROJECT_DIR|workspaceFolder)(?::-[^}]*)?\}/g, repoRoot);
3933
+ }
3934
+ function lexicalJoin(base, rel) {
3935
+ const parts = `${base}/${rel}`.replace(/\\/g, "/").split("/");
3936
+ const out = [];
3937
+ for (const seg of parts) {
3938
+ if (seg === "" && out.length > 0) continue;
3939
+ if (seg === ".") continue;
3940
+ if (seg === ".." && out.length > 1) {
3941
+ out.pop();
3942
+ continue;
3943
+ }
3944
+ out.push(seg);
3945
+ }
3946
+ return out.join("/");
3947
+ }
3948
+ function parseMarkerVaultPath(content) {
3949
+ const m = content.match(/^vault_path:[ \t]*(.+?)[ \t]*$/m);
3950
+ if (!m) return null;
3951
+ const v = m[1].trim().replace(/\s+#.*$/, "").replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
3952
+ if (!v || v.includes("${") || /^[\\/]/.test(v) || /^[a-zA-Z]:[\\/]/.test(v)) return null;
3953
+ return v;
3954
+ }
3955
+ function readPinnedVaultRoot(repoRoot, deps) {
3956
+ const pins = [];
3957
+ for (const rel of SATELLITE_MANIFEST_RELS) {
3958
+ const path5 = deps.joinPath(repoRoot, ...rel);
3959
+ if (!deps.fileExists(path5)) continue;
3960
+ const raw = deps.readFile(path5);
3961
+ if (!raw) continue;
3962
+ let value;
3963
+ try {
3964
+ const parsed = JSON.parse(raw);
3965
+ value = parsed?.mcpServers?.heyspex?.env?.[SATELLITE_VAULT_ENV];
3966
+ } catch {
3967
+ continue;
3968
+ }
3969
+ if (typeof value !== "string") continue;
3970
+ const trimmed = value.trim();
3971
+ if (!trimmed) continue;
3972
+ const expanded = expandPinPlaceholders(trimmed, repoRoot);
3973
+ if (expanded.includes("${")) continue;
3974
+ if (/^[a-zA-Z]:(?![\\/])/.test(expanded)) continue;
3975
+ const abs = isAbsolutePathForCompare(expanded) ? expanded.includes("..") ? lexicalJoin(expanded, ".") : expanded : lexicalJoin(repoRoot, expanded);
3976
+ if (expanded !== trimmed && isAtOrUnder(abs, repoRoot)) continue;
3977
+ pins.push({ source: rel.join("/"), value: abs });
3978
+ }
3979
+ return pins;
3980
+ }
3981
+
3812
3982
  // spex-console/src/lib/cli/install-identity.ts
3813
3983
  var SWITCH_HINT = "Sign in as another account with `hs login --switch-account` (or create one with `hs login --create-account`).";
3814
3984
  var ATTACH_WORDING = {
@@ -4044,7 +4214,7 @@ async function gateWriteVerb(opts) {
4044
4214
  if (!opts.writeVerbs.has(opts.sub)) return null;
4045
4215
  return runIdentityGate(opts.io, opts.flags, `${opts.namespace} ${opts.sub}`, WRITE_WORDING);
4046
4216
  }
4047
- function readConfiguredAccountId(env = process.env, homeDir = homedir9()) {
4217
+ function readConfiguredAccountId(env = process.env, homeDir = heyspexHome2()) {
4048
4218
  return resolveSessionOwnerUncached({ env, homeDir });
4049
4219
  }
4050
4220
  function isInteractiveTerminal() {
@@ -4052,7 +4222,7 @@ function isInteractiveTerminal() {
4052
4222
  }
4053
4223
  function defaultIdentityGateIo(opts) {
4054
4224
  const consoleUrl = opts.consoleUrl.trim().replace(/\/+$/, "");
4055
- const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join9(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync4(p));
4225
+ const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join10(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync5(p));
4056
4226
  return {
4057
4227
  consoleUrl,
4058
4228
  interactive: opts.interactive ?? isInteractiveTerminal(),
@@ -4276,12 +4446,17 @@ async function listWorkspacesNeon(accountId2, deps = {}) {
4276
4446
  id: workspaces.id,
4277
4447
  displayName: workspaces.displayName,
4278
4448
  githubOrg: workspaces.githubOrg,
4279
- createdAt: workspaces.createdAt
4449
+ createdAt: workspaces.createdAt,
4450
+ archived: workspaces.archived
4280
4451
  }).from(workspaces).where(eq(workspaces.accountId, accountId2));
4281
4452
  return rows.map((r) => ({
4282
4453
  id: r.id,
4283
4454
  display_name: r.displayName,
4284
4455
  created_at: (r.createdAt instanceof Date ? r.createdAt : /* @__PURE__ */ new Date()).toISOString(),
4456
+ // The flag `setWorkspaceArchivedNeon` writes; `list.ts` filters on it.
4457
+ // On a cloud host this row IS the roster (no registry.json to carry the
4458
+ // flag), so leaving it out made an archived workspace list forever.
4459
+ archived: r.archived,
4285
4460
  ...r.githubOrg ? { github_org: r.githubOrg } : {}
4286
4461
  }));
4287
4462
  } catch {
@@ -4299,7 +4474,8 @@ async function listVaultsNeon(accountId2, deps = {}) {
4299
4474
  repoRootAbs: vaults.repoRootAbs,
4300
4475
  githubRemote: vaults.githubRemote,
4301
4476
  schemaVersion: vaults.schemaVersion,
4302
- createdAt: vaults.createdAt
4477
+ createdAt: vaults.createdAt,
4478
+ archived: vaults.archived
4303
4479
  }).from(vaults).where(eq(vaults.accountId, accountId2));
4304
4480
  const grantsDb = deps.db ?? getPostgresAdminDb();
4305
4481
  const grantedRows = await grantsDb.select({
@@ -4309,7 +4485,10 @@ async function listVaultsNeon(accountId2, deps = {}) {
4309
4485
  repoRootAbs: vaults.repoRootAbs,
4310
4486
  githubRemote: vaults.githubRemote,
4311
4487
  schemaVersion: vaults.schemaVersion,
4312
- createdAt: vaults.createdAt
4488
+ createdAt: vaults.createdAt,
4489
+ archived: vaults.archived,
4490
+ // [[HS-RRCG-8QDO-DIAG-a-shared-vault-is-labelled-read-only-in]] -- the grant's access level must reach the client.
4491
+ access: vaultGrants.access
4313
4492
  }).from(vaultGrants).innerJoin(vaults, eq(vaults.id, vaultGrants.vaultId)).where(and(eq(vaultGrants.accountId, accountId2), ne(vaults.accountId, accountId2)));
4314
4493
  const toRecord = (r, granted) => ({
4315
4494
  id: r.id,
@@ -4321,8 +4500,16 @@ async function listVaultsNeon(accountId2, deps = {}) {
4321
4500
  repo_root_abs: r.repoRootAbs ?? "",
4322
4501
  schema_version: r.schemaVersion,
4323
4502
  registered_at: (r.createdAt instanceof Date ? r.createdAt : /* @__PURE__ */ new Date()).toISOString(),
4503
+ // The flag `setVaultArchivedNeon` writes and `list.ts` filters on. On a
4504
+ // cloud host (VERCEL=1) this row IS the roster -- there is no
4505
+ // registry.json to carry the flag -- so omitting it here is why
4506
+ // `PATCH /api/vaults/:id {archived:true}` answered 200 while
4507
+ // `GET /api/vaults` kept listing the vault with no `archived` field.
4508
+ archived: r.archived,
4324
4509
  ...r.githubRemote ? { github_remote: r.githubRemote } : {},
4325
- ...granted ? { granted: true } : {}
4510
+ ...granted ? { granted: true } : {},
4511
+ // [[HS-RRCG-8QDO-DIAG-a-shared-vault-is-labelled-read-only-in]]: granted rows only; unknown = read.
4512
+ ...granted ? { access: r.access === "write" ? "write" : "read" } : {}
4326
4513
  });
4327
4514
  return [
4328
4515
  ...ownedRows.map((r) => toRecord(r, false)),
@@ -4332,6 +4519,14 @@ async function listVaultsNeon(accountId2, deps = {}) {
4332
4519
  return null;
4333
4520
  }
4334
4521
  }
4522
+ async function classifyZeroRowVaultWrite(db, id, accountId2) {
4523
+ try {
4524
+ const rows = await db.select({ accountId: vaults.accountId }).from(vaults).where(eq(vaults.id, id)).limit(1);
4525
+ return rows[0] && rows[0].accountId !== accountId2 ? "owned_by_other_account" : "not_found";
4526
+ } catch {
4527
+ return "not_found";
4528
+ }
4529
+ }
4335
4530
  async function renameVaultNeon(id, displayName, accountId2, deps = {}) {
4336
4531
  if (shouldUseCloudHttp(deps)) {
4337
4532
  return mutateHttp(
@@ -4346,7 +4541,7 @@ async function renameVaultNeon(id, displayName, accountId2, deps = {}) {
4346
4541
  try {
4347
4542
  const db = deps.db ?? getPostgresDb();
4348
4543
  const rows = await db.update(vaults).set({ displayName, updatedAt: /* @__PURE__ */ new Date() }).where(and(eq(vaults.id, id), eq(vaults.accountId, accountId2))).returning({ id: vaults.id });
4349
- return rows.length > 0 ? "updated" : "not_found";
4544
+ return rows.length > 0 ? "updated" : classifyZeroRowVaultWrite(db, id, accountId2);
4350
4545
  } catch {
4351
4546
  return "unreachable";
4352
4547
  }
@@ -4361,6 +4556,9 @@ async function moveVaultNeon(id, workspaceId, accountId2, deps = {}) {
4361
4556
  if (res.status === 409 && code === WORKSPACE_OWNED_BY_OTHER_ACCOUNT) {
4362
4557
  return "workspace_owned_by_other_account";
4363
4558
  }
4559
+ if (res.status === 403 && code === VAULT_OWNED_BY_OTHER_ACCOUNT) {
4560
+ return "owned_by_other_account";
4561
+ }
4364
4562
  if (res.status === 404)
4365
4563
  return code === "workspace_not_found" ? "workspace_not_found" : "not_found";
4366
4564
  return cloudWriteFailure(deps.http, res);
@@ -4379,7 +4577,7 @@ async function moveVaultNeon(id, workspaceId, accountId2, deps = {}) {
4379
4577
  ).returning({ id: vaults.id });
4380
4578
  if (rows.length > 0) return "updated";
4381
4579
  const mine = await db.select({ id: vaults.id }).from(vaults).where(and(eq(vaults.id, id), eq(vaults.accountId, accountId2))).limit(1);
4382
- if (mine.length === 0) return "not_found";
4580
+ if (mine.length === 0) return classifyZeroRowVaultWrite(db, id, accountId2);
4383
4581
  const ws = await db.select({ accountId: workspaces.accountId }).from(workspaces).where(eq(workspaces.id, workspaceId)).limit(1);
4384
4582
  return ws[0] ? "workspace_owned_by_other_account" : "workspace_not_found";
4385
4583
  } catch {
@@ -4394,7 +4592,7 @@ async function setVaultArchivedNeon(id, archived, accountId2, deps = {}) {
4394
4592
  try {
4395
4593
  const db = deps.db ?? getPostgresDb();
4396
4594
  const rows = await db.update(vaults).set({ archived, updatedAt: /* @__PURE__ */ new Date() }).where(and(eq(vaults.id, id), eq(vaults.accountId, accountId2))).returning({ id: vaults.id });
4397
- return rows.length > 0 ? "updated" : "not_found";
4595
+ return rows.length > 0 ? "updated" : classifyZeroRowVaultWrite(db, id, accountId2);
4398
4596
  } catch {
4399
4597
  return "unreachable";
4400
4598
  }
@@ -4407,7 +4605,7 @@ async function deleteVaultNeon(id, accountId2, deps = {}) {
4407
4605
  try {
4408
4606
  const db = deps.db ?? getPostgresDb();
4409
4607
  const rows = await db.delete(vaults).where(and(eq(vaults.id, id), eq(vaults.accountId, accountId2))).returning({ id: vaults.id });
4410
- return rows.length > 0 ? "deleted" : "not_found";
4608
+ return rows.length > 0 ? "deleted" : classifyZeroRowVaultWrite(db, id, accountId2);
4411
4609
  } catch {
4412
4610
  return "unreachable";
4413
4611
  }
@@ -4468,7 +4666,7 @@ async function repointActiveVaultOnRemoval(accountId2, removedVaultId, removedVa
4468
4666
  return { repointed: false, newActiveId: activeId };
4469
4667
  }
4470
4668
  const registry = loadVaultRegistry(deps.registryOpts);
4471
- const candidate = registry.vaults.find(
4669
+ const candidate = removedVaultWorkspaceId === null ? void 0 : registry.vaults.find(
4472
4670
  (v) => v.id !== removedVaultId && v.workspace_id === removedVaultWorkspaceId && !v.archived
4473
4671
  );
4474
4672
  if (!candidate) {
@@ -4481,16 +4679,40 @@ async function repointActiveVaultOnRemoval(accountId2, removedVaultId, removedVa
4481
4679
 
4482
4680
  // spex-console/src/lib/vault-registry/archive.ts
4483
4681
  init_registry_file();
4682
+
4683
+ // spex-console/src/lib/vault-registry/side-effects.ts
4684
+ function isLocalRegistryHost() {
4685
+ const override = process.env.HEYSPEX_VAULT_WRITE_MODE?.trim().toLowerCase();
4686
+ if (override === "cloud") return false;
4687
+ if (override === "local") return true;
4688
+ return process.env.VERCEL !== "1";
4689
+ }
4690
+
4691
+ // spex-console/src/lib/vault-registry/archive.ts
4484
4692
  function cloudWriteFailed(failure) {
4485
4693
  return { ok: false, code: "cloud_write_failed", message: failure.message };
4486
4694
  }
4695
+ function neonUnwritten(kind, id) {
4696
+ return {
4697
+ ok: false,
4698
+ code: "neon_unwritten",
4699
+ message: `${kind} "${id}" was not written to the cloud registry (database unreachable); nothing was changed. Retry once the console can reach its database.`
4700
+ };
4701
+ }
4487
4702
  function vaultNotFound(id) {
4488
4703
  return { ok: false, code: "vault_not_found", message: `vault "${id}" not found` };
4489
4704
  }
4705
+ function vaultNotOwned(id) {
4706
+ return {
4707
+ ok: false,
4708
+ code: VAULT_OWNED_BY_OTHER_ACCOUNT,
4709
+ message: vaultNotOwnedByCallerMessage(id)
4710
+ };
4711
+ }
4490
4712
  async function setVaultArchivedState(input, archived, deps = {}) {
4491
- const registry = loadVaultRegistry(deps.registryOpts);
4492
- const existing = registry.vaults.find((v) => v.id === input.id);
4493
- if (!existing) return vaultNotFound(input.id);
4713
+ const local = isLocalRegistryHost();
4714
+ const existing = local ? loadVaultRegistry(deps.registryOpts).vaults.find((v) => v.id === input.id) : void 0;
4715
+ if (local && !existing) return vaultNotFound(input.id);
4494
4716
  const neonResult = await setVaultArchivedNeon(
4495
4717
  input.id,
4496
4718
  archived,
@@ -4498,21 +4720,23 @@ async function setVaultArchivedState(input, archived, deps = {}) {
4498
4720
  deps.neon ?? {}
4499
4721
  );
4500
4722
  if (neonResult === "not_found") return vaultNotFound(input.id);
4723
+ if (neonResult === "owned_by_other_account") return vaultNotOwned(input.id);
4501
4724
  if (isCloudWriteFailure(neonResult)) return cloudWriteFailed(neonResult);
4725
+ if (neonResult === "unreachable" && !existing) return neonUnwritten("vault", input.id);
4502
4726
  if (archived) {
4503
- await repointActiveVaultOnRemoval(input.accountId, input.id, existing.workspace_id, {
4727
+ await repointActiveVaultOnRemoval(input.accountId, input.id, existing?.workspace_id ?? null, {
4504
4728
  registryOpts: deps.registryOpts,
4505
4729
  accountPointer: { ...deps.neon, registryOpts: deps.registryOpts }
4506
4730
  });
4507
4731
  }
4508
- registerVault({ ...existing, archived }, deps.registryOpts);
4732
+ if (existing) registerVault({ ...existing, archived }, deps.registryOpts);
4509
4733
  return {
4510
4734
  ok: true,
4511
4735
  vault: {
4512
4736
  id: input.id,
4513
4737
  archived,
4514
4738
  neon_written: neonResult === "updated",
4515
- registry_written: true
4739
+ registry_written: existing !== void 0
4516
4740
  }
4517
4741
  };
4518
4742
  }
@@ -4551,19 +4775,9 @@ function buildCloseNotDestroyConfirm(kind, verb, displayName) {
4551
4775
  }
4552
4776
 
4553
4777
  // spex-console/src/lib/vault-registry/list.ts
4554
- import { existsSync as existsSync5 } from "node:fs";
4778
+ import { existsSync as existsSync6 } from "node:fs";
4555
4779
  init_account_pointer();
4556
4780
  init_registry_file();
4557
-
4558
- // spex-console/src/lib/vault-registry/side-effects.ts
4559
- function isLocalRegistryHost() {
4560
- const override = process.env.HEYSPEX_VAULT_WRITE_MODE?.trim().toLowerCase();
4561
- if (override === "cloud") return false;
4562
- if (override === "local") return true;
4563
- return process.env.VERCEL !== "1";
4564
- }
4565
-
4566
- // spex-console/src/lib/vault-registry/list.ts
4567
4781
  async function defaultNeonReadTierEnabled() {
4568
4782
  const { isNeonReadTierEnabled: isNeonReadTierEnabled2 } = await Promise.resolve().then(() => (init_spec_read_service(), spec_read_service_exports));
4569
4783
  return isNeonReadTierEnabled2();
@@ -4594,7 +4808,7 @@ async function listVaultsForWorkspace(accountId2, workspaceId, deps = {}) {
4594
4808
  const activeId = await getActiveVault(accountId2, sharedPointerDeps) ?? await adoptSole(accountId2, sharedPointerDeps);
4595
4809
  const rows = deps.includeArchived ? allRows : allRows.filter((v) => !v.archived);
4596
4810
  const scoped = workspaceId ? rows.filter((v) => v.workspace_id === workspaceId) : rows;
4597
- const pathExists = deps.pathExists ?? existsSync5;
4811
+ const pathExists = deps.pathExists ?? existsSync6;
4598
4812
  const anyPathUnusable = scoped.some((v) => !v.repo_root_abs || !pathExists(v.repo_root_abs));
4599
4813
  const neonBackedServeable = anyPathUnusable && neonRows !== null ? await (deps.neonReadTierEnabled ?? defaultNeonReadTierEnabled)() : false;
4600
4814
  return {
@@ -4642,6 +4856,25 @@ function isValidId2(id) {
4642
4856
  function notFound(id) {
4643
4857
  return { ok: false, code: "vault_not_found", message: `vault "${id}" not found` };
4644
4858
  }
4859
+ function notOwned(id) {
4860
+ return {
4861
+ ok: false,
4862
+ code: VAULT_OWNED_BY_OTHER_ACCOUNT,
4863
+ message: vaultNotOwnedByCallerMessage(id)
4864
+ };
4865
+ }
4866
+ function neonUnwritten2(id) {
4867
+ return {
4868
+ ok: false,
4869
+ code: "neon_unwritten",
4870
+ message: `vault "${id}" was not written to the cloud registry (database unreachable); nothing was changed. Retry once the console can reach its database.`
4871
+ };
4872
+ }
4873
+ function localRow(id, registryOpts) {
4874
+ const local = isLocalRegistryHost();
4875
+ const existing = local ? loadVaultRegistry(registryOpts).vaults.find((v) => v.id === id) : void 0;
4876
+ return { local, existing };
4877
+ }
4645
4878
  function cloudWriteFailed2(failure) {
4646
4879
  return { ok: false, code: "cloud_write_failed", message: failure.message };
4647
4880
  }
@@ -4654,20 +4887,21 @@ async function renameVault(input, deps = {}) {
4654
4887
  message: `display name must be 1-${MAX_DISPLAY_NAME_LEN} characters (trimmed)`
4655
4888
  };
4656
4889
  }
4657
- const registry = loadVaultRegistry(deps.registryOpts);
4658
- const existing = registry.vaults.find((v) => v.id === input.id);
4659
- if (!existing) return notFound(input.id);
4890
+ const { local, existing } = localRow(input.id, deps.registryOpts);
4891
+ if (local && !existing) return notFound(input.id);
4660
4892
  const neonResult = await renameVaultNeon(input.id, trimmedName, input.accountId, deps.neon ?? {});
4661
4893
  if (neonResult === "not_found") return notFound(input.id);
4894
+ if (neonResult === "owned_by_other_account") return notOwned(input.id);
4662
4895
  if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
4663
- registerVault({ ...existing, display_name: trimmedName }, deps.registryOpts);
4896
+ if (neonResult === "unreachable" && !existing) return neonUnwritten2(input.id);
4897
+ if (existing) registerVault({ ...existing, display_name: trimmedName }, deps.registryOpts);
4664
4898
  return {
4665
4899
  ok: true,
4666
4900
  vault: {
4667
4901
  id: input.id,
4668
4902
  display_name: trimmedName,
4669
4903
  neon_written: neonResult === "updated",
4670
- registry_written: true
4904
+ registry_written: existing !== void 0
4671
4905
  }
4672
4906
  };
4673
4907
  }
@@ -4679,9 +4913,8 @@ async function moveVault(input, deps = {}) {
4679
4913
  message: `invalid workspace id "${String(input.workspaceId)}" -- must be non-empty with no path separators`
4680
4914
  };
4681
4915
  }
4682
- const registry = loadVaultRegistry(deps.registryOpts);
4683
- const existing = registry.vaults.find((v) => v.id === input.id);
4684
- if (!existing) return notFound(input.id);
4916
+ const { local, existing } = localRow(input.id, deps.registryOpts);
4917
+ if (local && !existing) return notFound(input.id);
4685
4918
  const neonResult = await moveVaultNeon(
4686
4919
  input.id,
4687
4920
  input.workspaceId,
@@ -4689,7 +4922,9 @@ async function moveVault(input, deps = {}) {
4689
4922
  deps.neon ?? {}
4690
4923
  );
4691
4924
  if (neonResult === "not_found") return notFound(input.id);
4925
+ if (neonResult === "owned_by_other_account") return notOwned(input.id);
4692
4926
  if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
4927
+ if (neonResult === "unreachable" && !existing) return neonUnwritten2(input.id);
4693
4928
  if (neonResult === "workspace_owned_by_other_account") {
4694
4929
  return {
4695
4930
  ok: false,
@@ -4704,92 +4939,46 @@ async function moveVault(input, deps = {}) {
4704
4939
  message: `workspace "${input.workspaceId}" not found -- create it first (\`hs workspace create\`)`
4705
4940
  };
4706
4941
  }
4707
- registerVault({ ...existing, workspace_id: input.workspaceId }, deps.registryOpts);
4942
+ if (existing) {
4943
+ registerVault({ ...existing, workspace_id: input.workspaceId }, deps.registryOpts);
4944
+ }
4708
4945
  return {
4709
4946
  ok: true,
4710
4947
  vault: {
4711
4948
  id: input.id,
4712
4949
  workspace_id: input.workspaceId,
4713
4950
  neon_written: neonResult === "updated",
4714
- registry_written: true
4951
+ registry_written: existing !== void 0
4715
4952
  }
4716
4953
  };
4717
4954
  }
4718
4955
  async function unregisterVault(input, deps = {}) {
4719
- const registry = loadVaultRegistry(deps.registryOpts);
4720
- const existing = registry.vaults.find((v) => v.id === input.id);
4721
- if (!existing) return notFound(input.id);
4956
+ const { local, existing } = localRow(input.id, deps.registryOpts);
4957
+ if (local && !existing) return notFound(input.id);
4722
4958
  const neonResult = await deleteVaultNeon(input.id, input.accountId, deps.neon ?? {});
4723
4959
  if (neonResult === "not_found") return notFound(input.id);
4960
+ if (neonResult === "owned_by_other_account") return notOwned(input.id);
4724
4961
  if (isCloudWriteFailure(neonResult)) return cloudWriteFailed2(neonResult);
4725
- await repointActiveVaultOnRemoval(input.accountId, input.id, existing.workspace_id, {
4962
+ if (neonResult === "unreachable" && !existing) return neonUnwritten2(input.id);
4963
+ await repointActiveVaultOnRemoval(input.accountId, input.id, existing?.workspace_id ?? null, {
4726
4964
  registryOpts: deps.registryOpts,
4727
4965
  accountPointer: { ...deps.neon, registryOpts: deps.registryOpts }
4728
4966
  });
4729
- removeVault(input.id, deps.registryOpts);
4967
+ if (existing) removeVault(input.id, deps.registryOpts);
4730
4968
  return {
4731
4969
  ok: true,
4732
4970
  vault: {
4733
4971
  id: input.id,
4734
4972
  neon_written: neonResult === "deleted",
4735
- registry_written: true
4973
+ registry_written: existing !== void 0
4736
4974
  }
4737
4975
  };
4738
4976
  }
4739
4977
 
4740
4978
  // spex-console/src/lib/vault-resolve/index.ts
4741
- import { existsSync as existsSync6, readFileSync as readFileSync9 } from "node:fs";
4979
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "node:fs";
4742
4980
  import { tmpdir as tmpdir2 } from "node:os";
4743
4981
  import path3 from "node:path";
4744
-
4745
- // spex-console/src/lib/cli/satellite.ts
4746
- init_registry_file();
4747
- import { resolve as resolveNodePath } from "node:path";
4748
- import process2 from "node:process";
4749
- var SATELLITE_VAULT_ENV = "HEYSPEX_VAULT_ROOT";
4750
- var SATELLITE_MANIFEST_RELS = [
4751
- [".mcp.json"],
4752
- [".cursor", "mcp.json"]
4753
- ];
4754
- function normalizePathForCompare(p) {
4755
- return p.trim().replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
4756
- }
4757
- function samePath(a, b) {
4758
- return normalizePathForCompare(a) === normalizePathForCompare(b);
4759
- }
4760
- function isAtOrUnder(child, parent) {
4761
- const c = normalizePathForCompare(child);
4762
- const p = normalizePathForCompare(parent);
4763
- return c === p || c.startsWith(`${p}/`);
4764
- }
4765
- function isAbsolutePathForCompare(p) {
4766
- const s = p.trim();
4767
- return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
4768
- }
4769
- function readPinnedVaultRoot(repoRoot, deps) {
4770
- const pins = [];
4771
- for (const rel of SATELLITE_MANIFEST_RELS) {
4772
- const path5 = deps.joinPath(repoRoot, ...rel);
4773
- if (!deps.fileExists(path5)) continue;
4774
- const raw = deps.readFile(path5);
4775
- if (!raw) continue;
4776
- let value;
4777
- try {
4778
- const parsed = JSON.parse(raw);
4779
- value = parsed?.mcpServers?.heyspex?.env?.[SATELLITE_VAULT_ENV];
4780
- } catch {
4781
- continue;
4782
- }
4783
- if (typeof value !== "string") continue;
4784
- const trimmed = value.trim();
4785
- if (!trimmed || trimmed.includes("${")) continue;
4786
- if (!isAbsolutePathForCompare(trimmed)) continue;
4787
- pins.push({ source: rel.join("/"), value: trimmed });
4788
- }
4789
- return pins;
4790
- }
4791
-
4792
- // spex-console/src/lib/vault-resolve/index.ts
4793
4982
  var UNREGISTERED_VAULT_ID2 = "unregistered";
4794
4983
  var NoActiveVaultError = class _NoActiveVaultError extends Error {
4795
4984
  constructor(message2 = "resolveActiveVaultRoot: no active vault found -- no explicit vaultId, no heyspex.yaml or specs/ directory at or above cwd, no HEYSPEX_VAULT_ROOT/HEYSPEX_VAULT_DIR env, and no account active_vault_id. Run `hs vault register` to register a vault.") {
@@ -4823,22 +5012,40 @@ function parseMarkerVaultId(content) {
4823
5012
  }
4824
5013
  function readMarkerVaultId(dir) {
4825
5014
  try {
4826
- return parseMarkerVaultId(readFileSync9(path3.join(dir, "heyspex.yaml"), "utf8"));
5015
+ return parseMarkerVaultId(readFileSync10(path3.join(dir, "heyspex.yaml"), "utf8"));
4827
5016
  } catch {
4828
5017
  return null;
4829
5018
  }
4830
5019
  }
5020
+ function readMarkerSatellite(dir) {
5021
+ let raw;
5022
+ try {
5023
+ raw = readFileSync10(path3.join(dir, "heyspex.yaml"), "utf8");
5024
+ } catch {
5025
+ return null;
5026
+ }
5027
+ const vaultPath = parseMarkerVaultPath(raw);
5028
+ if (!vaultPath) return null;
5029
+ const vaultId = parseMarkerVaultId(raw);
5030
+ const candidate = path3.resolve(dir, vaultPath);
5031
+ let vaultRootAbs = null;
5032
+ if (candidate.toLowerCase() !== path3.resolve(dir).toLowerCase() && isVaultRootDir(candidate)) {
5033
+ const theirs = readMarkerVaultId(candidate);
5034
+ if (!vaultId || !theirs || theirs === vaultId) vaultRootAbs = candidate;
5035
+ }
5036
+ return { vaultId, vaultRootAbs };
5037
+ }
4831
5038
  function isVaultRootDir(dir) {
4832
5039
  const norm = (p) => path3.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
4833
5040
  if (norm(dir) === norm(tmpdir2())) return false;
4834
- return existsSync6(path3.join(dir, "heyspex.yaml")) || existsSync6(path3.join(dir, "specs"));
5041
+ return existsSync7(path3.join(dir, "heyspex.yaml")) || existsSync7(path3.join(dir, "specs"));
4835
5042
  }
4836
5043
  function findCwdVaultOrAttachment(startDir, stopAt) {
4837
5044
  const deps = {
4838
- fileExists: (p) => existsSync6(p),
5045
+ fileExists: (p) => existsSync7(p),
4839
5046
  readFile: (p) => {
4840
5047
  try {
4841
- return readFileSync9(p, "utf8");
5048
+ return readFileSync10(p, "utf8");
4842
5049
  } catch {
4843
5050
  return null;
4844
5051
  }
@@ -4848,6 +5055,14 @@ function findCwdVaultOrAttachment(startDir, stopAt) {
4848
5055
  const boundary = stopAt ? path3.resolve(stopAt) : null;
4849
5056
  let dir = startDir;
4850
5057
  for (let i = 0; i < 256; i += 1) {
5058
+ const marked = readMarkerSatellite(dir);
5059
+ if (marked) {
5060
+ return {
5061
+ repoRootAbs: marked.vaultRootAbs ?? dir,
5062
+ satellite: true,
5063
+ markerVaultId: marked.vaultId
5064
+ };
5065
+ }
4851
5066
  const pinned = readPinnedVaultRoot(dir, deps).find((pin) => !isAtOrUnder(pin.value, dir));
4852
5067
  if (pinned) return { repoRootAbs: path3.resolve(pinned.value), satellite: true };
4853
5068
  if (isVaultRootDir(dir)) return { repoRootAbs: dir, satellite: false };
@@ -4893,6 +5108,9 @@ async function resolveActiveVaultRoot(options = {}) {
4893
5108
  const cwdMatch = findCwdVaultOrAttachment(cwdBase, options.cwdWalkRoot);
4894
5109
  if (cwdMatch?.satellite) {
4895
5110
  onTier({ tier: "satellite-pin", outcome: "resolved", detail: cwdMatch.repoRootAbs });
5111
+ if (cwdMatch.markerVaultId) {
5112
+ return finalize(cwdMatch.markerVaultId, path3.resolve(cwdMatch.repoRootAbs), "satellite-pin");
5113
+ }
4896
5114
  return finalizePathTier(cwdMatch.repoRootAbs, "satellite-pin");
4897
5115
  }
4898
5116
  if (cwdMatch) {
@@ -5923,7 +6141,7 @@ function statusForVaultManageError(code) {
5923
6141
  if (code === "workspace_owned_by_other_account" || code === "vault_owned_by_other_account") {
5924
6142
  return EXIT.CONFLICT;
5925
6143
  }
5926
- if (code === "cloud_write_failed") return EXIT.CLOUD_WRITE_FAILED;
6144
+ if (code === "cloud_write_failed" || code === "neon_unwritten") return EXIT.CLOUD_WRITE_FAILED;
5927
6145
  return EXIT.INVALID_ARG;
5928
6146
  }
5929
6147
  async function manageWithReceipt(verb, vaultId, effectiveAccountId, deps, retry, write, success, expected) {
@@ -6338,7 +6556,7 @@ function runEntry(name, main2, options = {}) {
6338
6556
  // spex-console/bin/hs-vault.ts
6339
6557
  function readFile(p) {
6340
6558
  try {
6341
- return readFileSync10(p, "utf8");
6559
+ return readFileSync11(p, "utf8");
6342
6560
  } catch {
6343
6561
  return null;
6344
6562
  }
@@ -6397,7 +6615,7 @@ async function main() {
6397
6615
  const result = await runVault(
6398
6616
  parsed.args,
6399
6617
  {
6400
- fileExists: (p) => existsSync7(p),
6618
+ fileExists: (p) => existsSync8(p),
6401
6619
  readFile,
6402
6620
  resolvePath: (p) => path4.resolve(invokedCwd, p),
6403
6621
  joinPath: (...s) => path4.join(...s),
@@ -6405,7 +6623,7 @@ async function main() {
6405
6623
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
6406
6624
  cwd,
6407
6625
  conflictScan: {
6408
- fileExists: (p) => existsSync7(p),
6626
+ fileExists: (p) => existsSync8(p),
6409
6627
  readFile,
6410
6628
  joinPath: (...s) => path4.join(...s)
6411
6629
  },