heyspex 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +181 -15
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +177 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +21 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +312 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/install-copilot-hooks.mjs +288 -0
  72. package/scripts/lib/console-url-resolver.mjs +3 -2
  73. package/scripts/lib/customer-kit.mjs +327 -10
  74. package/scripts/lib/dev-mode.mjs +7 -6
  75. package/scripts/lib/directive-obligations.mjs +349 -0
  76. package/scripts/lib/hook-dispositions.mjs +210 -0
  77. package/scripts/lib/hook-vault-header.mjs +30 -0
  78. package/scripts/lib/hs-source-branch.mjs +196 -0
  79. package/scripts/lib/hs-source-log.mjs +2 -1
  80. package/scripts/lib/hs-source-preflight.mjs +18 -4
  81. package/scripts/lib/hs-source-push.mjs +386 -356
  82. package/scripts/lib/window-placement.mjs +109 -0
  83. package/spexharness/src/adapters/claude-code.mjs +11 -3
  84. package/spexharness/src/adapters/copilot.mjs +236 -0
  85. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  86. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  87. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  88. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  89. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  90. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  91. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  93. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  94. package/spexharness/src/intent-classifier.mjs +4 -2
  95. package/spexharness/src/intent-directive.mjs +626 -541
  96. package/spexharness/src/rule-table.mjs +31 -1
  97. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  98. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  99. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  100. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  101. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  102. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  103. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  104. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  105. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  106. package/vendor/heyspex-mcp/cli.mjs +7 -1
  107. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  108. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  109. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  110. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  111. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  112. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  113. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  114. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  115. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  116. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  117. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  118. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  119. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  120. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  121. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  122. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  123. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  124. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  125. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  126. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  127. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  128. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  129. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  130. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  131. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  132. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  133. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  134. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  135. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  136. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  137. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  138. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  139. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  140. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  141. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  142. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  143. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  144. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  145. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  146. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -59,11 +59,26 @@ var init_hs_command = __esm({
59
59
  }
60
60
  });
61
61
 
62
+ // scripts/lib/heyspex-home.mjs
63
+ import { homedir } from "node:os";
64
+ function heyspexHome(env = process.env) {
65
+ const override = env?.HEYSPEX_HOME;
66
+ if (typeof override === "string" && override.trim().length > 0) {
67
+ return override.trim();
68
+ }
69
+ return homedir();
70
+ }
71
+ var init_heyspex_home = __esm({
72
+ "scripts/lib/heyspex-home.mjs"() {
73
+ "use strict";
74
+ }
75
+ });
76
+
62
77
  // vendor/heyspex-mcp/credentials.mjs
63
78
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
64
- import os, { homedir } from "node:os";
79
+ import os, { homedir as homedir2 } from "node:os";
65
80
  import { dirname as dirname2, join as join2, resolve } from "node:path";
66
- function credentialsFilePath(homeDir = homedir()) {
81
+ function credentialsFilePath(homeDir = heyspexHome()) {
67
82
  return join2(homeDir, ".heyspex", "credentials.yaml");
68
83
  }
69
84
  function parseMcpToken(raw) {
@@ -159,7 +174,7 @@ function readLegacyConfigAccountId(read, homeDir) {
159
174
  const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
160
175
  return value || null;
161
176
  }
162
- function readCurrentAccountId({ homeDir = homedir(), readFile } = {}) {
177
+ function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
163
178
  const read = readFile ?? ((p) => readFileSync(p, "utf8"));
164
179
  const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
165
180
  if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
@@ -172,6 +187,7 @@ var MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FI
172
187
  var init_credentials = __esm({
173
188
  "vendor/heyspex-mcp/credentials.mjs"() {
174
189
  "use strict";
190
+ init_heyspex_home();
175
191
  MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
176
192
  CREDENTIALS_SCHEMA_VERSION = 2;
177
193
  LEGACY_ACCOUNT_ID = "legacy";
@@ -183,9 +199,9 @@ var init_credentials = __esm({
183
199
 
184
200
  // vendor/heyspex-mcp/session-owner.mjs
185
201
  import { readFileSync as readFileSync2 } from "node:fs";
186
- import { homedir as homedir2 } from "node:os";
202
+ import { homedir as homedir3 } from "node:os";
187
203
  import { join as join3 } from "node:path";
188
- function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile } = {}) {
204
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
189
205
  const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
190
206
  if (fromEnv) return fromEnv;
191
207
  return readCurrentAccountId({ homeDir, readFile });
@@ -193,6 +209,7 @@ function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(),
193
209
  var init_session_owner = __esm({
194
210
  "vendor/heyspex-mcp/session-owner.mjs"() {
195
211
  "use strict";
212
+ init_heyspex_home();
196
213
  init_credentials();
197
214
  }
198
215
  });
@@ -227,20 +244,47 @@ var init_vault_less_endpoints = __esm({
227
244
  }
228
245
  });
229
246
 
247
+ // scripts/lib/shared-path-helpers.mjs
248
+ import { isAbsolute, resolve as resolve2, sep } from "node:path";
249
+ var init_shared_path_helpers = __esm({
250
+ "scripts/lib/shared-path-helpers.mjs"() {
251
+ "use strict";
252
+ }
253
+ });
254
+
255
+ // vendor/heyspex-mcp/write-outbox.mjs
256
+ import {
257
+ readdirSync,
258
+ readFileSync as readFileSync3,
259
+ writeFileSync as writeFileSync2,
260
+ mkdirSync as mkdirSync2,
261
+ existsSync as existsSync2,
262
+ renameSync as renameSync2,
263
+ rmSync
264
+ } from "node:fs";
265
+ import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
266
+ var init_write_outbox = __esm({
267
+ "vendor/heyspex-mcp/write-outbox.mjs"() {
268
+ "use strict";
269
+ init_shared_path_helpers();
270
+ }
271
+ });
272
+
230
273
  // vendor/heyspex-mcp/vault-resolve.mjs
231
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
232
- import { homedir as homedir3, tmpdir } from "node:os";
233
- import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
274
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
275
+ import { homedir as homedir4, tmpdir } from "node:os";
276
+ import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
234
277
  var init_vault_resolve = __esm({
235
278
  "vendor/heyspex-mcp/vault-resolve.mjs"() {
236
279
  "use strict";
280
+ init_heyspex_home();
237
281
  }
238
282
  });
239
283
 
240
284
  // vendor/heyspex-mcp/console-fetch.mjs
241
- import { readFileSync as readFileSync4 } from "node:fs";
242
- import { homedir as homedir4 } from "node:os";
243
- import { join as join5 } from "node:path";
285
+ import { readFileSync as readFileSync5 } from "node:fs";
286
+ import { homedir as homedir5 } from "node:os";
287
+ import { join as join6 } from "node:path";
244
288
  function isCloudVaultId(value) {
245
289
  return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
246
290
  }
@@ -248,16 +292,33 @@ var CLOUD_VAULT_ID_RE, SCOPE_REFUSAL_CODES;
248
292
  var init_console_fetch = __esm({
249
293
  "vendor/heyspex-mcp/console-fetch.mjs"() {
250
294
  "use strict";
295
+ init_heyspex_home();
251
296
  init_credentials();
252
297
  init_session_owner();
253
298
  init_vault_less_endpoints();
254
299
  init_hs_command();
300
+ init_write_outbox();
255
301
  init_vault_resolve();
256
302
  CLOUD_VAULT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
257
303
  SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
258
304
  }
259
305
  });
260
306
 
307
+ // spex-console/src/lib/heyspex-home.ts
308
+ import { homedir as homedir6 } from "node:os";
309
+ function heyspexHome2(env = process.env) {
310
+ const override = env?.HEYSPEX_HOME;
311
+ if (typeof override === "string" && override.trim().length > 0) {
312
+ return override.trim();
313
+ }
314
+ return homedir6();
315
+ }
316
+ var init_heyspex_home2 = __esm({
317
+ "spex-console/src/lib/heyspex-home.ts"() {
318
+ "use strict";
319
+ }
320
+ });
321
+
261
322
  // spex-console/src/lib/vault-registry/types.ts
262
323
  function emptyVaultRegistry() {
263
324
  return {
@@ -278,14 +339,13 @@ var init_types = __esm({
278
339
  });
279
340
 
280
341
  // spex-console/src/lib/vault-registry/registry-file.ts
281
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
282
- import { homedir as homedir5 } from "node:os";
283
- import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
342
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
343
+ import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
284
344
  function vaultRegistryDir(opts = {}) {
285
- return opts.registryDir ?? join6(homedir5(), ".heyspex");
345
+ return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
286
346
  }
287
347
  function vaultRegistryFilePath(opts = {}) {
288
- return join6(vaultRegistryDir(opts), "registry.json");
348
+ return join7(vaultRegistryDir(opts), "registry.json");
289
349
  }
290
350
  function isNonEmptyString(v) {
291
351
  return typeof v === "string" && v.trim().length > 0;
@@ -386,9 +446,9 @@ function parseVaultRegistryFile(json) {
386
446
  }
387
447
  function readRegistryJson(opts) {
388
448
  const filePath = vaultRegistryFilePath(opts);
389
- if (!existsSync3(filePath)) return { kind: "missing" };
449
+ if (!existsSync4(filePath)) return { kind: "missing" };
390
450
  try {
391
- const raw = readFileSync5(filePath, "utf8");
451
+ const raw = readFileSync6(filePath, "utf8");
392
452
  const json = JSON.parse(raw);
393
453
  if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
394
454
  return { kind: "ok", raw, json };
@@ -417,17 +477,18 @@ function loadVaultRegistry(opts = {}) {
417
477
  var init_registry_file = __esm({
418
478
  "spex-console/src/lib/vault-registry/registry-file.ts"() {
419
479
  "use strict";
480
+ init_heyspex_home2();
420
481
  init_types();
421
482
  init_types();
422
483
  }
423
484
  });
424
485
 
425
486
  // scripts/lib/dev-mode.mjs
426
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
427
- import { homedir as homedir6 } from "node:os";
428
- import { join as join7 } from "node:path";
429
- function devModeFilePath(homeDir = homedir6()) {
430
- return join7(homeDir, ".heyspex", DEV_MODE_FILENAME);
487
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
488
+ import { homedir as homedir7 } from "node:os";
489
+ import { join as join8 } from "node:path";
490
+ function devModeFilePath(homeDir = heyspexHome()) {
491
+ return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
431
492
  }
432
493
  function normalizeMode(raw) {
433
494
  const v = String(raw ?? "").trim().toLowerCase();
@@ -451,7 +512,7 @@ function deriveSemantics(mode, reliance) {
451
512
  vaultWriteMode: local ? "local" : "cloud"
452
513
  };
453
514
  }
454
- function readLocalDevModeRecord({ homeDir = homedir6(), readFile = defaultReadFile } = {}) {
515
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
455
516
  let raw;
456
517
  try {
457
518
  raw = readFile(devModeFilePath(homeDir));
@@ -481,7 +542,7 @@ function readLocalDevModeRecord({ homeDir = homedir6(), readFile = defaultReadFi
481
542
  }
482
543
  function resolveDevMode({
483
544
  env = process.env,
484
- homeDir = homedir6(),
545
+ homeDir = heyspexHome(),
485
546
  readFile = defaultReadFile,
486
547
  cloudSetting = void 0
487
548
  } = {}) {
@@ -544,18 +605,19 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
544
605
  var init_dev_mode = __esm({
545
606
  "scripts/lib/dev-mode.mjs"() {
546
607
  "use strict";
608
+ init_heyspex_home();
547
609
  init_credentials();
548
610
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
549
611
  LOCAL_CONSOLE_URL = "http://localhost:3000";
550
612
  DEV_MODE_FILENAME = "dev-mode.json";
551
- defaultReadFile = (p) => readFileSync6(p, "utf8");
613
+ defaultReadFile = (p) => readFileSync7(p, "utf8");
552
614
  }
553
615
  });
554
616
 
555
617
  // scripts/lib/console-url-resolver.mjs
556
- import { readFileSync as readFileSync7 } from "node:fs";
557
- import { homedir as homedir7 } from "node:os";
558
- import { join as join8 } from "node:path";
618
+ import { readFileSync as readFileSync8 } from "node:fs";
619
+ import { homedir as homedir8 } from "node:os";
620
+ import { join as join9 } from "node:path";
559
621
  function trimTrailingSlash(url) {
560
622
  return url.replace(/\/+$/, "");
561
623
  }
@@ -578,9 +640,9 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
578
640
  } catch {
579
641
  }
580
642
  }
581
- function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile = defaultReadFile2 } = {}) {
643
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
582
644
  try {
583
- const raw = readFile(join8(homeDir, ".heyspex", "credentials.yaml"));
645
+ const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
584
646
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
585
647
  const value = match?.[1]?.trim();
586
648
  return value && value.length > 0 ? trimTrailingSlash(value) : null;
@@ -589,12 +651,12 @@ function readCredentialsConsoleUrl({ homeDir = homedir7(), readFile = defaultRea
589
651
  }
590
652
  }
591
653
  function defaultReadFile2(p) {
592
- return readFileSync7(p, "utf8");
654
+ return readFileSync8(p, "utf8");
593
655
  }
594
656
  function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery: readDiscovery2 = defaultReadFile2 } = {}) {
595
657
  if (!repoRoot) return null;
596
658
  try {
597
- const discovery = String(readDiscovery2(join8(repoRoot, ".heyspex", "console-url")) ?? "").trim();
659
+ const discovery = String(readDiscovery2(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
598
660
  if (discovery.length > 0) return trimTrailingSlash(discovery);
599
661
  } catch {
600
662
  }
@@ -614,7 +676,7 @@ function resolveModeEnvelope({ env, homeDir }) {
614
676
  function resolveConsoleUrl({
615
677
  repoRoot,
616
678
  env = process.env,
617
- homeDir = homedir7(),
679
+ homeDir = heyspexHome(),
618
680
  strict = false,
619
681
  devMode,
620
682
  log = (line) => process.stderr.write(line),
@@ -647,6 +709,7 @@ var DEFAULT_CONSOLE_URL, LOOPBACK_HOSTS, staleLoopbackCredentialsWarned, Console
647
709
  var init_console_url_resolver = __esm({
648
710
  "scripts/lib/console-url-resolver.mjs"() {
649
711
  "use strict";
712
+ init_heyspex_home();
650
713
  init_dev_mode();
651
714
  DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
652
715
  LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
@@ -700,13 +763,12 @@ var init_default_workspace = __esm({
700
763
  });
701
764
 
702
765
  // spex-console/src/lib/cli/vault-workspace-shared.ts
703
- import { readFileSync as readFileSync8 } from "node:fs";
704
- import os2 from "node:os";
766
+ import { readFileSync as readFileSync9 } from "node:fs";
705
767
  function resolveConfiguredAccountId(opts = {}) {
706
768
  return resolveSessionOwnerUncached({
707
769
  env: opts.env ?? process.env,
708
- homeDir: opts.homeDir ?? os2.homedir(),
709
- readFile: opts.readFile ?? ((p) => readFileSync8(p, "utf-8"))
770
+ homeDir: opts.homeDir ?? heyspexHome2(),
771
+ readFile: opts.readFile ?? ((p) => readFileSync9(p, "utf-8"))
710
772
  });
711
773
  }
712
774
  function resolveCloudAccountIdOrRefuse(opts = {}) {
@@ -739,6 +801,7 @@ var CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE;
739
801
  var init_vault_workspace_shared = __esm({
740
802
  "spex-console/src/lib/cli/vault-workspace-shared.ts"() {
741
803
  "use strict";
804
+ init_heyspex_home2();
742
805
  init_session_owner();
743
806
  init_default_workspace();
744
807
  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.";
@@ -1225,11 +1288,11 @@ var require_excerpt = __commonJS({
1225
1288
  if (typeof opts.excerpt === "function") {
1226
1289
  return opts.excerpt(file, opts);
1227
1290
  }
1228
- const sep = file.data.excerpt_separator || opts.excerpt_separator;
1229
- if (sep == null && (opts.excerpt === false || opts.excerpt == null)) {
1291
+ const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
1292
+ if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
1230
1293
  return file;
1231
1294
  }
1232
- const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep || opts.delimiters[0];
1295
+ const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
1233
1296
  const idx = file.content.indexOf(delimiter2);
1234
1297
  if (idx !== -1) {
1235
1298
  file.excerpt = file.content.slice(0, idx);
@@ -1294,7 +1357,7 @@ var require_parse = __commonJS({
1294
1357
  var require_gray_matter = __commonJS({
1295
1358
  "node_modules/gray-matter/index.js"(exports2, module2) {
1296
1359
  "use strict";
1297
- var fs10 = __require("fs");
1360
+ var fs11 = __require("fs");
1298
1361
  var sections = require_section_matter();
1299
1362
  var defaults = require_defaults();
1300
1363
  var stringify = require_stringify();
@@ -1378,7 +1441,7 @@ var require_gray_matter = __commonJS({
1378
1441
  return stringify(file, data, options2);
1379
1442
  };
1380
1443
  matter4.read = function(filepath, options2) {
1381
- const str2 = fs10.readFileSync(filepath, "utf8");
1444
+ const str2 = fs11.readFileSync(filepath, "utf8");
1382
1445
  const file = matter4(str2, options2);
1383
1446
  file.path = filepath;
1384
1447
  return file;
@@ -1526,13 +1589,13 @@ function bareFileKeyOf(input) {
1526
1589
  if (KNOWN_TYPE_LABELS.has(token)) return null;
1527
1590
  return FILE_KEY_RE.test(token) ? token : null;
1528
1591
  }
1529
- function isEra3SpecId(id, basename) {
1592
+ function isEra3SpecId(id, basename2) {
1530
1593
  const raw = String(id ?? "").trim();
1531
1594
  if (!raw) return false;
1532
1595
  if (parseSpecId(raw)?.era === 3) return true;
1533
1596
  const fileKey = bareFileKeyOf(raw);
1534
1597
  if (!fileKey) return false;
1535
- const parsed = parseSpecId(basename ?? "");
1598
+ const parsed = parseSpecId(basename2 ?? "");
1536
1599
  return parsed?.era === 3 && (parsed.fileKey ?? "").toUpperCase() === fileKey;
1537
1600
  }
1538
1601
  var FILE_KEY_RE, KNOWN_TYPE_LABELS, RULED_FAMILY_SURNAMES, FAMILY_KEYED_TYPE_LABELS, FAMILY_KEY_SHAPE_RE;
@@ -1858,7 +1921,7 @@ var init_parser = __esm({
1858
1921
  });
1859
1922
 
1860
1923
  // dist/cli/._sql-orm-inert.cli-shim.mjs
1861
- var inert, customType, sql, and, eq, isNull, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1924
+ var inert, customType, sql, and, eq, isNull, desc, gt, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1862
1925
  var init_sql_orm_inert_cli_shim = __esm({
1863
1926
  "dist/cli/._sql-orm-inert.cli-shim.mjs"() {
1864
1927
  "use strict";
@@ -1881,6 +1944,8 @@ var init_sql_orm_inert_cli_shim = __esm({
1881
1944
  and = inert;
1882
1945
  eq = inert;
1883
1946
  isNull = inert;
1947
+ desc = inert;
1948
+ gt = inert;
1884
1949
  inArray = inert;
1885
1950
  bigint = inert;
1886
1951
  bigserial = inert;
@@ -1909,7 +1974,7 @@ var init_interfaces = __esm({
1909
1974
  });
1910
1975
 
1911
1976
  // spex-console/src/lib/db/schema/postgres.ts
1912
- 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;
1977
+ 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;
1913
1978
  var init_postgres = __esm({
1914
1979
  "spex-console/src/lib/db/schema/postgres.ts"() {
1915
1980
  "use strict";
@@ -2351,7 +2416,13 @@ var init_postgres = __esm({
2351
2416
  "priority_change",
2352
2417
  "parent_change",
2353
2418
  "claim_acquired",
2354
- "claim_released"
2419
+ "claim_released",
2420
+ // Collaborator write audit (HS-FWB6, migration 0077).
2421
+ "spec_created",
2422
+ "spec_edited",
2423
+ "spec_deleted",
2424
+ "capture_written",
2425
+ "wip_recorded"
2355
2426
  ]);
2356
2427
  focusPin = pgTable(
2357
2428
  "focus_pin",
@@ -3544,11 +3615,43 @@ var init_postgres = __esm({
3544
3615
  // `owner/name`
3545
3616
  branch: text("branch").notNull(),
3546
3617
  commitSha: text("commit_sha").notNull(),
3547
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
3618
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
3619
+ // The pull request that carries this settlement
3620
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
3621
+ // the branch has gone quiet and a PR was opened or found; a write made
3622
+ // after its PR merged stays NULL, which is what triggers the next PR.
3623
+ pullRequestNumber: integer("pull_request_number"),
3624
+ pullRequestUrl: text("pull_request_url")
3548
3625
  },
3549
3626
  (table) => [
3550
3627
  // "latest settlement for this spec" — the read path's only query.
3551
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
3628
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
3629
+ // The PR sweep's only query: branches with settlements not yet in a PR.
3630
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
3631
+ ]
3632
+ );
3633
+ sessionBranchDeclarations = pgTable(
3634
+ "session_branch_declarations",
3635
+ {
3636
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
3637
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
3638
+ actorKey: text("actor_key").notNull(),
3639
+ sessionId: text("session_id").notNull(),
3640
+ branch: text("branch").notNull(),
3641
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
3642
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
3643
+ },
3644
+ (table) => [
3645
+ uniqueIndex("session_branch_declarations_uq").on(
3646
+ table.vaultId,
3647
+ table.actorKey,
3648
+ table.sessionId
3649
+ ),
3650
+ index("session_branch_declarations_lookup_idx").on(
3651
+ table.vaultId,
3652
+ table.actorKey,
3653
+ table.declaredAt.desc()
3654
+ )
3552
3655
  ]
3553
3656
  );
3554
3657
  }
@@ -4465,7 +4568,7 @@ var init_satellite = __esm({
4465
4568
  });
4466
4569
 
4467
4570
  // spex-console/src/lib/vault-resolve/index.ts
4468
- import { existsSync as existsSync4, readFileSync as readFileSync10 } from "node:fs";
4571
+ import { existsSync as existsSync5, readFileSync as readFileSync11 } from "node:fs";
4469
4572
  import { tmpdir as tmpdir2 } from "node:os";
4470
4573
  import path5 from "node:path";
4471
4574
  var init_vault_resolve2 = __esm({
@@ -4504,7 +4607,7 @@ var init_resolve_status = __esm({
4504
4607
  });
4505
4608
 
4506
4609
  // spex-console/src/lib/vault-resolve/active.ts
4507
- import { existsSync as existsSync5 } from "node:fs";
4610
+ import { existsSync as existsSync6 } from "node:fs";
4508
4611
  import path6 from "node:path";
4509
4612
  var ACTIVE_VAULT_CACHE_KEY, cacheStore;
4510
4613
  var init_active = __esm({
@@ -4714,8 +4817,8 @@ function singleFlightDebounce(fn, delayMs) {
4714
4817
  })();
4715
4818
  }
4716
4819
  return function debounced() {
4717
- return new Promise((resolve4, reject) => {
4718
- const resolver = { resolve: resolve4, reject };
4820
+ return new Promise((resolve5, reject) => {
4821
+ const resolver = { resolve: resolve5, reject };
4719
4822
  if (inFlight) {
4720
4823
  if (queuedResolvers) {
4721
4824
  queuedResolvers.push(resolver);
@@ -5008,6 +5111,55 @@ var init_branch_policy = __esm({
5008
5111
  }
5009
5112
  });
5010
5113
 
5114
+ // spex-console/src/lib/git-export/branch-resolver.ts
5115
+ function isValidSessionBranch(name, defaultBranch) {
5116
+ const b = name.trim();
5117
+ if (!b) return { ok: false, reason: "branch name is empty" };
5118
+ if (b === defaultBranch) {
5119
+ return { ok: false, reason: `\`${b}\` is the default branch; the drain never writes it` };
5120
+ }
5121
+ if (PROTECTED_SESSION_BRANCHES.includes(b)) {
5122
+ return { ok: false, reason: `\`${b}\` is a protected ref` };
5123
+ }
5124
+ const hasControlChar = [...b].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
5125
+ if (hasControlChar || /[\s~^:?*[\\]/.test(b)) {
5126
+ return { ok: false, reason: "branch name contains characters git refs do not allow" };
5127
+ }
5128
+ if (b.includes("..") || b.includes("@{") || b.includes("//")) {
5129
+ return { ok: false, reason: "branch name contains a forbidden sequence (.. @{ //)" };
5130
+ }
5131
+ if (b.startsWith("/") || b.endsWith("/") || b.startsWith("-") || b.endsWith(".") || b === "@") {
5132
+ return { ok: false, reason: "branch name has a forbidden leading or trailing character" };
5133
+ }
5134
+ if (b.split("/").some((seg) => seg.startsWith(".") || seg.endsWith(".lock"))) {
5135
+ return {
5136
+ ok: false,
5137
+ reason: "branch name has a path segment starting with . or ending in .lock"
5138
+ };
5139
+ }
5140
+ return { ok: true };
5141
+ }
5142
+ function resolveSessionTargetBranch(input) {
5143
+ const dated = resolveTargetBranch({
5144
+ user: input.user,
5145
+ date: input.date,
5146
+ defaultBranch: input.defaultBranch
5147
+ });
5148
+ const declared = input.sessionBranch?.trim();
5149
+ if (!declared) return { branch: dated, source: "dated" };
5150
+ const check = isValidSessionBranch(declared, input.defaultBranch);
5151
+ if (!check.ok) return { branch: dated, source: "dated", reason: check.reason };
5152
+ return { branch: declared, source: "session" };
5153
+ }
5154
+ var PROTECTED_SESSION_BRANCHES;
5155
+ var init_branch_resolver = __esm({
5156
+ "spex-console/src/lib/git-export/branch-resolver.ts"() {
5157
+ "use strict";
5158
+ init_branch_policy();
5159
+ PROTECTED_SESSION_BRANCHES = ["main", "master", "HEAD", "trunk"];
5160
+ }
5161
+ });
5162
+
5011
5163
  // spex-console/src/lib/git-export/config.ts
5012
5164
  function parseGithubRemote(githubRemote) {
5013
5165
  const match = githubRemote.match(/^([^/\s]+)\/([^/\s]+)$/);
@@ -5095,10 +5247,10 @@ function trashPathFor(relPath, now) {
5095
5247
  const normalized = relPath.split("\\").join("/");
5096
5248
  const slashIdx = normalized.lastIndexOf("/");
5097
5249
  const folder = slashIdx === -1 ? "" : normalized.slice(0, slashIdx);
5098
- const basename = slashIdx === -1 ? normalized : normalized.slice(slashIdx + 1);
5250
+ const basename2 = slashIdx === -1 ? normalized : normalized.slice(slashIdx + 1);
5099
5251
  const stamp = now.toISOString().replace(/[:.]/g, "-");
5100
5252
  const trashDir = folder ? `${folder}/.trash` : ".trash";
5101
- return `${trashDir}/${stamp}-${basename}`;
5253
+ return `${trashDir}/${stamp}-${basename2}`;
5102
5254
  }
5103
5255
  function materializeSpecMarkdown(bundle) {
5104
5256
  return stringifySpecMarkdown(bundle.bodyText, bundle.frontmatter);
@@ -5407,6 +5559,39 @@ var init_github_client = __esm({
5407
5559
  }
5408
5560
  });
5409
5561
 
5562
+ // spex-console/src/lib/git-export/session-branch-store.ts
5563
+ async function readActiveDeclaration(db, vaultId, actorKey, now) {
5564
+ const rows = await db.select().from(sessionBranchDeclarations).where(
5565
+ and(
5566
+ eq(sessionBranchDeclarations.vaultId, vaultId),
5567
+ eq(sessionBranchDeclarations.actorKey, actorKey),
5568
+ gt(sessionBranchDeclarations.expiresAt, now)
5569
+ )
5570
+ ).orderBy(desc(sessionBranchDeclarations.declaredAt)).limit(1);
5571
+ const row = rows[0];
5572
+ if (!row) return null;
5573
+ return {
5574
+ vaultId: row.vaultId,
5575
+ actorKey: row.actorKey,
5576
+ sessionId: row.sessionId,
5577
+ branch: row.branch,
5578
+ declaredAt: row.declaredAt.toISOString(),
5579
+ expiresAt: row.expiresAt.toISOString()
5580
+ };
5581
+ }
5582
+ async function readActiveSessionBranch(db, vaultId, actorKey, now) {
5583
+ return (await readActiveDeclaration(db, vaultId, actorKey, now))?.branch ?? null;
5584
+ }
5585
+ var SESSION_BRANCH_TTL_MS;
5586
+ var init_session_branch_store = __esm({
5587
+ "spex-console/src/lib/git-export/session-branch-store.ts"() {
5588
+ "use strict";
5589
+ init_sql_orm_inert_cli_shim();
5590
+ init_postgres();
5591
+ SESSION_BRANCH_TTL_MS = 12 * 60 * 60 * 1e3;
5592
+ }
5593
+ });
5594
+
5410
5595
  // spex-console/src/lib/git-export/settlement-store.ts
5411
5596
  async function recordSettlement(tx, input) {
5412
5597
  await tx.insert(specSettlements).values({
@@ -5637,6 +5822,7 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
5637
5822
  result: { status: "error", message: target.message }
5638
5823
  };
5639
5824
  }
5825
+ const lookupSessionBranch = deps.lookupSessionBranch ?? ((vId, actor) => readActiveSessionBranch(tx, vId, actor, now()));
5640
5826
  const { config, tokenSource, installationId } = target;
5641
5827
  const provider = createPatCommitProvider(() => config.token);
5642
5828
  let pendingSettlement = null;
@@ -5650,11 +5836,18 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
5650
5836
  fetchBundles: (specIds) => fetchDrainBundles(tx, vaultId, specIds),
5651
5837
  resolveActorIdentity,
5652
5838
  commit: async (plan, message, coAuthor) => {
5653
- const targetBranch = resolveTargetBranch({
5839
+ let sessionBranch = null;
5840
+ try {
5841
+ sessionBranch = await lookupSessionBranch(vaultId, coAuthor.name);
5842
+ } catch {
5843
+ sessionBranch = null;
5844
+ }
5845
+ const targetBranch = resolveSessionTargetBranch({
5654
5846
  user: coAuthor.name,
5655
5847
  date: now(),
5656
- defaultBranch: config.branch
5657
- });
5848
+ defaultBranch: config.branch,
5849
+ sessionBranch
5850
+ }).branch;
5658
5851
  const outcome = await commitPlanToGitHub(
5659
5852
  provider,
5660
5853
  config,
@@ -5795,12 +5988,13 @@ var init_drain_runner = __esm({
5795
5988
  init_postgres();
5796
5989
  init_github_app();
5797
5990
  init_advisory_lock();
5798
- init_branch_policy();
5991
+ init_branch_resolver();
5799
5992
  init_config();
5800
5993
  init_drain();
5801
5994
  init_drain_skip();
5802
5995
  init_export_status();
5803
5996
  init_github_client();
5997
+ init_session_branch_store();
5804
5998
  init_settlement_store();
5805
5999
  BOT_NAME = "heyspex-bot";
5806
6000
  BOT_EMAIL = "bot@heyspex.app";
@@ -6237,7 +6431,7 @@ var init_search_body_hit = __esm({
6237
6431
 
6238
6432
  // spex-console/src/lib/vault.ts
6239
6433
  import { spawn } from "node:child_process";
6240
- import { readdirSync, readFileSync as readFileSync11, statSync } from "node:fs";
6434
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync } from "node:fs";
6241
6435
  import { promises as fs7 } from "node:fs";
6242
6436
  import path13 from "node:path";
6243
6437
  import yaml3 from "js-yaml";
@@ -6357,8 +6551,8 @@ var init_vault = __esm({
6357
6551
 
6358
6552
  // spex-console/bin/hs-index-rebuild.ts
6359
6553
  import { execSync } from "node:child_process";
6360
- import { appendFileSync, mkdirSync as mkdirSync5, readFileSync as readFileSync12 } from "node:fs";
6361
- import { promises as fs9 } from "node:fs";
6554
+ import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync13 } from "node:fs";
6555
+ import { promises as fs10 } from "node:fs";
6362
6556
  import path14 from "node:path";
6363
6557
  import process4 from "node:process";
6364
6558
 
@@ -6527,7 +6721,7 @@ function resolveSyncVaultTarget(input) {
6527
6721
  detail: `heyspex.yaml vault_id${drift}`
6528
6722
  };
6529
6723
  }
6530
- if (input.pointer.status === "read" && input.pointer.vaultId) {
6724
+ if (input.allowPointer && input.pointer.status === "read" && input.pointer.vaultId) {
6531
6725
  return {
6532
6726
  ok: true,
6533
6727
  vaultId: input.pointer.vaultId,
@@ -6535,9 +6729,9 @@ function resolveSyncVaultTarget(input) {
6535
6729
  detail: "this account's active vault, read from the console (heyspex.yaml names none)"
6536
6730
  };
6537
6731
  }
6538
- const why = input.pointer.status === "unreadable" ? `the console could not answer for the active vault (${input.pointer.detail})` : "the console reports no active vault for this account";
6732
+ const why = !input.allowPointer ? "the account-wide active vault is not a binding and was not consulted (set HEYSPEX_ALLOW_POINTER_VAULT=1 to follow it on purpose)" : input.pointer.status === "unreadable" ? `the console could not answer for the active vault (${input.pointer.detail})` : "the console reports no active vault for this account";
6539
6733
  return refuse(
6540
- `cannot tell which vault to ${verb === "pull" ? "pull into" : verb === "index reconcile" ? "reconcile" : "rebuild"}: heyspex.yaml names no vault_id and ${why}. Nothing was changed. The registry.json pointer copy was deliberately not used (it can be stale and target the wrong vault). Name the vault: \`hs --vault <id> ${verb}\`; check this repo's wiring with \`hs connect --check\`; or set the account pointer with \`hs vault switch <id>\`.`
6734
+ `cannot tell which vault to ${verb === "pull" ? "pull into" : verb === "push" ? "push from" : verb === "import" ? "import into" : verb === "index reconcile" ? "reconcile" : "rebuild"}: heyspex.yaml names no vault_id and ${why}. Nothing was changed. The registry.json pointer copy was deliberately not used (it can be stale and target the wrong vault). Name the vault: \`hs --vault <id> ${verb}\`; check this repo's wiring with \`hs connect --check\`; or bind this repo with a heyspex.yaml vault_id (a committed satellite marker is portable).`
6541
6735
  );
6542
6736
  }
6543
6737
  async function resolveSyncVaultTargetFromEnv(deps) {
@@ -6559,8 +6753,11 @@ async function resolveSyncVaultTargetFromEnv(deps) {
6559
6753
  } catch {
6560
6754
  registryRowId = null;
6561
6755
  }
6756
+ const allowPointer = ["1", "true", "yes"].includes(
6757
+ (deps.env.HEYSPEX_ALLOW_POINTER_VAULT ?? "").trim().toLowerCase()
6758
+ );
6562
6759
  let pointer = { status: "unreadable", detail: "not read" };
6563
- if (!override && !markerVaultId) {
6760
+ if (!override && !markerVaultId && allowPointer) {
6564
6761
  const http = {
6565
6762
  fetchImpl: deps.fetchImpl ?? fetch,
6566
6763
  consoleUrl: deps.consoleUrl,
@@ -6582,7 +6779,8 @@ async function resolveSyncVaultTargetFromEnv(deps) {
6582
6779
  markerVaultId,
6583
6780
  registryRowId,
6584
6781
  pointer,
6585
- verb: deps.verb
6782
+ verb: deps.verb,
6783
+ allowPointer
6586
6784
  });
6587
6785
  }
6588
6786
 
@@ -6743,23 +6941,22 @@ async function runReindex(input) {
6743
6941
  init_validator();
6744
6942
 
6745
6943
  // spex-console/src/lib/vault-sync/pull-io.ts
6746
- import { promises as fs8 } from "node:fs";
6747
- import { homedir as homedir9 } from "node:os";
6748
- import nodePath from "node:path";
6944
+ import { promises as fs9 } from "node:fs";
6945
+ import nodePath2 from "node:path";
6749
6946
 
6750
6947
  // spex-console/src/lib/dev-mode.ts
6948
+ init_heyspex_home2();
6751
6949
  init_credentials();
6752
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
6753
- import { homedir as homedir8 } from "node:os";
6754
- import { join as join9 } from "node:path";
6950
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
6951
+ import { join as join10 } from "node:path";
6755
6952
  var CLOUD_CONSOLE_URL2 = "https://app.heyspex.com";
6756
- var defaultReadFile3 = (p) => readFileSync9(p, "utf8");
6953
+ var defaultReadFile3 = (p) => readFileSync10(p, "utf8");
6757
6954
  function readCredentialsMcpToken({
6758
- homeDir = homedir8(),
6955
+ homeDir = heyspexHome2(),
6759
6956
  readFile = defaultReadFile3
6760
6957
  } = {}) {
6761
6958
  try {
6762
- const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
6959
+ const raw = readFile(join10(homeDir, ".heyspex", "credentials.yaml"));
6763
6960
  const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
6764
6961
  return match?.[1]?.trim() || null;
6765
6962
  } catch {
@@ -6768,11 +6965,19 @@ function readCredentialsMcpToken({
6768
6965
  }
6769
6966
 
6770
6967
  // spex-console/src/lib/vault-sync/pull-io.ts
6968
+ init_heyspex_home2();
6771
6969
  init_vault();
6772
6970
  init_vault_frontmatter();
6971
+
6972
+ // spex-console/src/lib/vault-sync/ledger.ts
6973
+ import { createHash as createHash3 } from "node:crypto";
6974
+ import { promises as fs8 } from "node:fs";
6975
+ import nodePath from "node:path";
6976
+
6977
+ // spex-console/src/lib/vault-sync/pull-io.ts
6773
6978
  function resolvePullToken({
6774
6979
  env = process.env,
6775
- homeDir = homedir9()
6980
+ homeDir = heyspexHome2()
6776
6981
  } = {}) {
6777
6982
  const fromEnv = env.HEYSPEX_MCP_TOKEN?.trim();
6778
6983
  if (fromEnv) return fromEnv;
@@ -6896,7 +7101,7 @@ function currentGitHead(vaultRoot2) {
6896
7101
  }
6897
7102
  function appendJournal(vaultRoot2, message) {
6898
7103
  const heyspexDir = path14.join(vaultRoot2, ".heyspex");
6899
- mkdirSync5(heyspexDir, { recursive: true });
7104
+ mkdirSync6(heyspexDir, { recursive: true });
6900
7105
  const logPath = path14.join(heyspexDir, "index-rebuild.log");
6901
7106
  const ts = (/* @__PURE__ */ new Date()).toISOString();
6902
7107
  appendFileSync(logPath, `[${ts}] ${message}
@@ -6904,7 +7109,7 @@ function appendJournal(vaultRoot2, message) {
6904
7109
  }
6905
7110
  function readDiscovery(p) {
6906
7111
  try {
6907
- return readFileSync12(p, "utf-8");
7112
+ return readFileSync13(p, "utf-8");
6908
7113
  } catch {
6909
7114
  return null;
6910
7115
  }
@@ -6916,7 +7121,7 @@ function resolveConsole(args) {
6916
7121
  }
6917
7122
  async function loadCrosswalk(vaultRoot2) {
6918
7123
  try {
6919
- const raw = await fs9.readFile(path14.join(vaultRoot2, ".heyspex", "id-crosswalk.json"), "utf8");
7124
+ const raw = await fs10.readFile(path14.join(vaultRoot2, ".heyspex", "id-crosswalk.json"), "utf8");
6920
7125
  const parsed = JSON.parse(raw);
6921
7126
  return Array.isArray(parsed) ? parsed : [];
6922
7127
  } catch {
@@ -6926,7 +7131,7 @@ async function loadCrosswalk(vaultRoot2) {
6926
7131
  async function walkLocal(args) {
6927
7132
  const specsDir = path14.join(args.vaultRoot, "specs");
6928
7133
  const files = [];
6929
- for await (const file of fs9.glob("**/*.md", { cwd: specsDir })) {
7134
+ for await (const file of fs10.glob("**/*.md", { cwd: specsDir })) {
6930
7135
  files.push(path14.join(specsDir, file));
6931
7136
  }
6932
7137
  files.sort();
@@ -6934,7 +7139,7 @@ async function walkLocal(args) {
6934
7139
  let failed = 0;
6935
7140
  for (const abs of files) {
6936
7141
  try {
6937
- const content = await fs9.readFile(abs, "utf8");
7142
+ const content = await fs10.readFile(abs, "utf8");
6938
7143
  const relPath = path14.relative(args.vaultRoot, abs).split(path14.sep).join("/");
6939
7144
  const parsed = parseSpecFromContent(content, {
6940
7145
  relPath,