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
@@ -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 str3 = fs10.readFileSync(filepath, "utf8");
1444
+ const str3 = fs11.readFileSync(filepath, "utf8");
1382
1445
  const file = matter4(str3, options2);
1383
1446
  file.path = filepath;
1384
1447
  return file;
@@ -1853,7 +1916,7 @@ var init_diagnose_skeleton = __esm({
1853
1916
  });
1854
1917
 
1855
1918
  // dist/cli/._sql-orm-inert.cli-shim.mjs
1856
- var inert, customType, sql, and, eq, isNull, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1919
+ var inert, customType, sql, and, eq, isNull, desc, gt, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1857
1920
  var init_sql_orm_inert_cli_shim = __esm({
1858
1921
  "dist/cli/._sql-orm-inert.cli-shim.mjs"() {
1859
1922
  "use strict";
@@ -1876,6 +1939,8 @@ var init_sql_orm_inert_cli_shim = __esm({
1876
1939
  and = inert;
1877
1940
  eq = inert;
1878
1941
  isNull = inert;
1942
+ desc = inert;
1943
+ gt = inert;
1879
1944
  inArray = inert;
1880
1945
  bigint = inert;
1881
1946
  bigserial = inert;
@@ -1904,7 +1969,7 @@ var init_interfaces = __esm({
1904
1969
  });
1905
1970
 
1906
1971
  // spex-console/src/lib/db/schema/postgres.ts
1907
- 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;
1972
+ 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;
1908
1973
  var init_postgres = __esm({
1909
1974
  "spex-console/src/lib/db/schema/postgres.ts"() {
1910
1975
  "use strict";
@@ -2346,7 +2411,13 @@ var init_postgres = __esm({
2346
2411
  "priority_change",
2347
2412
  "parent_change",
2348
2413
  "claim_acquired",
2349
- "claim_released"
2414
+ "claim_released",
2415
+ // Collaborator write audit (HS-FWB6, migration 0077).
2416
+ "spec_created",
2417
+ "spec_edited",
2418
+ "spec_deleted",
2419
+ "capture_written",
2420
+ "wip_recorded"
2350
2421
  ]);
2351
2422
  focusPin = pgTable(
2352
2423
  "focus_pin",
@@ -3539,11 +3610,43 @@ var init_postgres = __esm({
3539
3610
  // `owner/name`
3540
3611
  branch: text("branch").notNull(),
3541
3612
  commitSha: text("commit_sha").notNull(),
3542
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
3613
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
3614
+ // The pull request that carries this settlement
3615
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
3616
+ // the branch has gone quiet and a PR was opened or found; a write made
3617
+ // after its PR merged stays NULL, which is what triggers the next PR.
3618
+ pullRequestNumber: integer("pull_request_number"),
3619
+ pullRequestUrl: text("pull_request_url")
3543
3620
  },
3544
3621
  (table) => [
3545
3622
  // "latest settlement for this spec" — the read path's only query.
3546
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
3623
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
3624
+ // The PR sweep's only query: branches with settlements not yet in a PR.
3625
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
3626
+ ]
3627
+ );
3628
+ sessionBranchDeclarations = pgTable(
3629
+ "session_branch_declarations",
3630
+ {
3631
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
3632
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
3633
+ actorKey: text("actor_key").notNull(),
3634
+ sessionId: text("session_id").notNull(),
3635
+ branch: text("branch").notNull(),
3636
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
3637
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
3638
+ },
3639
+ (table) => [
3640
+ uniqueIndex("session_branch_declarations_uq").on(
3641
+ table.vaultId,
3642
+ table.actorKey,
3643
+ table.sessionId
3644
+ ),
3645
+ index("session_branch_declarations_lookup_idx").on(
3646
+ table.vaultId,
3647
+ table.actorKey,
3648
+ table.declaredAt.desc()
3649
+ )
3547
3650
  ]
3548
3651
  );
3549
3652
  }
@@ -3709,7 +3812,7 @@ var init_satellite = __esm({
3709
3812
  });
3710
3813
 
3711
3814
  // spex-console/src/lib/vault-resolve/index.ts
3712
- import { existsSync as existsSync4, readFileSync as readFileSync10 } from "node:fs";
3815
+ import { existsSync as existsSync5, readFileSync as readFileSync11 } from "node:fs";
3713
3816
  import { tmpdir as tmpdir2 } from "node:os";
3714
3817
  import path5 from "node:path";
3715
3818
  var init_vault_resolve2 = __esm({
@@ -3748,7 +3851,7 @@ var init_resolve_status = __esm({
3748
3851
  });
3749
3852
 
3750
3853
  // spex-console/src/lib/vault-resolve/active.ts
3751
- import { existsSync as existsSync5 } from "node:fs";
3854
+ import { existsSync as existsSync6 } from "node:fs";
3752
3855
  import path6 from "node:path";
3753
3856
  function fallbackSpecsDir() {
3754
3857
  const override = process.env.HEYSPEX_VAULT_DIR;
@@ -3969,8 +4072,8 @@ function singleFlightDebounce(fn, delayMs) {
3969
4072
  })();
3970
4073
  }
3971
4074
  return function debounced() {
3972
- return new Promise((resolve4, reject) => {
3973
- const resolver = { resolve: resolve4, reject };
4075
+ return new Promise((resolve5, reject) => {
4076
+ const resolver = { resolve: resolve5, reject };
3974
4077
  if (inFlight) {
3975
4078
  if (queuedResolvers) {
3976
4079
  queuedResolvers.push(resolver);
@@ -4263,6 +4366,55 @@ var init_branch_policy = __esm({
4263
4366
  }
4264
4367
  });
4265
4368
 
4369
+ // spex-console/src/lib/git-export/branch-resolver.ts
4370
+ function isValidSessionBranch(name, defaultBranch) {
4371
+ const b = name.trim();
4372
+ if (!b) return { ok: false, reason: "branch name is empty" };
4373
+ if (b === defaultBranch) {
4374
+ return { ok: false, reason: `\`${b}\` is the default branch; the drain never writes it` };
4375
+ }
4376
+ if (PROTECTED_SESSION_BRANCHES.includes(b)) {
4377
+ return { ok: false, reason: `\`${b}\` is a protected ref` };
4378
+ }
4379
+ const hasControlChar = [...b].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
4380
+ if (hasControlChar || /[\s~^:?*[\\]/.test(b)) {
4381
+ return { ok: false, reason: "branch name contains characters git refs do not allow" };
4382
+ }
4383
+ if (b.includes("..") || b.includes("@{") || b.includes("//")) {
4384
+ return { ok: false, reason: "branch name contains a forbidden sequence (.. @{ //)" };
4385
+ }
4386
+ if (b.startsWith("/") || b.endsWith("/") || b.startsWith("-") || b.endsWith(".") || b === "@") {
4387
+ return { ok: false, reason: "branch name has a forbidden leading or trailing character" };
4388
+ }
4389
+ if (b.split("/").some((seg) => seg.startsWith(".") || seg.endsWith(".lock"))) {
4390
+ return {
4391
+ ok: false,
4392
+ reason: "branch name has a path segment starting with . or ending in .lock"
4393
+ };
4394
+ }
4395
+ return { ok: true };
4396
+ }
4397
+ function resolveSessionTargetBranch(input) {
4398
+ const dated = resolveTargetBranch({
4399
+ user: input.user,
4400
+ date: input.date,
4401
+ defaultBranch: input.defaultBranch
4402
+ });
4403
+ const declared = input.sessionBranch?.trim();
4404
+ if (!declared) return { branch: dated, source: "dated" };
4405
+ const check = isValidSessionBranch(declared, input.defaultBranch);
4406
+ if (!check.ok) return { branch: dated, source: "dated", reason: check.reason };
4407
+ return { branch: declared, source: "session" };
4408
+ }
4409
+ var PROTECTED_SESSION_BRANCHES;
4410
+ var init_branch_resolver = __esm({
4411
+ "spex-console/src/lib/git-export/branch-resolver.ts"() {
4412
+ "use strict";
4413
+ init_branch_policy();
4414
+ PROTECTED_SESSION_BRANCHES = ["main", "master", "HEAD", "trunk"];
4415
+ }
4416
+ });
4417
+
4266
4418
  // spex-console/src/lib/git-export/config.ts
4267
4419
  function parseGithubRemote(githubRemote) {
4268
4420
  const match = githubRemote.match(/^([^/\s]+)\/([^/\s]+)$/);
@@ -4350,10 +4502,10 @@ function trashPathFor(relPath, now) {
4350
4502
  const normalized = relPath.split("\\").join("/");
4351
4503
  const slashIdx = normalized.lastIndexOf("/");
4352
4504
  const folder = slashIdx === -1 ? "" : normalized.slice(0, slashIdx);
4353
- const basename = slashIdx === -1 ? normalized : normalized.slice(slashIdx + 1);
4505
+ const basename2 = slashIdx === -1 ? normalized : normalized.slice(slashIdx + 1);
4354
4506
  const stamp = now.toISOString().replace(/[:.]/g, "-");
4355
4507
  const trashDir = folder ? `${folder}/.trash` : ".trash";
4356
- return `${trashDir}/${stamp}-${basename}`;
4508
+ return `${trashDir}/${stamp}-${basename2}`;
4357
4509
  }
4358
4510
  function materializeSpecMarkdown(bundle) {
4359
4511
  return stringifySpecMarkdown(bundle.bodyText, bundle.frontmatter);
@@ -4662,6 +4814,39 @@ var init_github_client = __esm({
4662
4814
  }
4663
4815
  });
4664
4816
 
4817
+ // spex-console/src/lib/git-export/session-branch-store.ts
4818
+ async function readActiveDeclaration(db, vaultId, actorKey, now) {
4819
+ const rows = await db.select().from(sessionBranchDeclarations).where(
4820
+ and(
4821
+ eq(sessionBranchDeclarations.vaultId, vaultId),
4822
+ eq(sessionBranchDeclarations.actorKey, actorKey),
4823
+ gt(sessionBranchDeclarations.expiresAt, now)
4824
+ )
4825
+ ).orderBy(desc(sessionBranchDeclarations.declaredAt)).limit(1);
4826
+ const row = rows[0];
4827
+ if (!row) return null;
4828
+ return {
4829
+ vaultId: row.vaultId,
4830
+ actorKey: row.actorKey,
4831
+ sessionId: row.sessionId,
4832
+ branch: row.branch,
4833
+ declaredAt: row.declaredAt.toISOString(),
4834
+ expiresAt: row.expiresAt.toISOString()
4835
+ };
4836
+ }
4837
+ async function readActiveSessionBranch(db, vaultId, actorKey, now) {
4838
+ return (await readActiveDeclaration(db, vaultId, actorKey, now))?.branch ?? null;
4839
+ }
4840
+ var SESSION_BRANCH_TTL_MS;
4841
+ var init_session_branch_store = __esm({
4842
+ "spex-console/src/lib/git-export/session-branch-store.ts"() {
4843
+ "use strict";
4844
+ init_sql_orm_inert_cli_shim();
4845
+ init_postgres();
4846
+ SESSION_BRANCH_TTL_MS = 12 * 60 * 60 * 1e3;
4847
+ }
4848
+ });
4849
+
4665
4850
  // spex-console/src/lib/git-export/settlement-store.ts
4666
4851
  async function recordSettlement(tx, input) {
4667
4852
  await tx.insert(specSettlements).values({
@@ -4892,6 +5077,7 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
4892
5077
  result: { status: "error", message: target.message }
4893
5078
  };
4894
5079
  }
5080
+ const lookupSessionBranch = deps.lookupSessionBranch ?? ((vId, actor) => readActiveSessionBranch(tx, vId, actor, now()));
4895
5081
  const { config, tokenSource, installationId } = target;
4896
5082
  const provider = createPatCommitProvider(() => config.token);
4897
5083
  let pendingSettlement = null;
@@ -4905,11 +5091,18 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
4905
5091
  fetchBundles: (specIds) => fetchDrainBundles(tx, vaultId, specIds),
4906
5092
  resolveActorIdentity,
4907
5093
  commit: async (plan, message, coAuthor) => {
4908
- const targetBranch = resolveTargetBranch({
5094
+ let sessionBranch = null;
5095
+ try {
5096
+ sessionBranch = await lookupSessionBranch(vaultId, coAuthor.name);
5097
+ } catch {
5098
+ sessionBranch = null;
5099
+ }
5100
+ const targetBranch = resolveSessionTargetBranch({
4909
5101
  user: coAuthor.name,
4910
5102
  date: now(),
4911
- defaultBranch: config.branch
4912
- });
5103
+ defaultBranch: config.branch,
5104
+ sessionBranch
5105
+ }).branch;
4913
5106
  const outcome = await commitPlanToGitHub(
4914
5107
  provider,
4915
5108
  config,
@@ -5050,12 +5243,13 @@ var init_drain_runner = __esm({
5050
5243
  init_postgres();
5051
5244
  init_github_app();
5052
5245
  init_advisory_lock();
5053
- init_branch_policy();
5246
+ init_branch_resolver();
5054
5247
  init_config();
5055
5248
  init_drain();
5056
5249
  init_drain_skip();
5057
5250
  init_export_status();
5058
5251
  init_github_client();
5252
+ init_session_branch_store();
5059
5253
  init_settlement_store();
5060
5254
  BOT_NAME = "heyspex-bot";
5061
5255
  BOT_EMAIL = "bot@heyspex.app";
@@ -5780,7 +5974,7 @@ var init_search_body_hit = __esm({
5780
5974
 
5781
5975
  // spex-console/src/lib/vault.ts
5782
5976
  import { spawn } from "node:child_process";
5783
- import { readdirSync, readFileSync as readFileSync11, statSync } from "node:fs";
5977
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync } from "node:fs";
5784
5978
  import { promises as fs7 } from "node:fs";
5785
5979
  import path13 from "node:path";
5786
5980
  import yaml3 from "js-yaml";
@@ -5823,18 +6017,18 @@ function extractBodyLinks(body) {
5823
6017
  return links;
5824
6018
  }
5825
6019
  function toSummary(absPath, root, fm, body) {
5826
- const basename = path13.basename(absPath).replace(/\.md$/i, "");
6020
+ const basename2 = path13.basename(absPath).replace(/\.md$/i, "");
5827
6021
  const relPath = path13.relative(root, absPath).split(path13.sep).join("/");
5828
6022
  return {
5829
- id: typeof fm.id === "string" && fm.id ? fm.id : basename,
5830
- name: typeof fm.name === "string" && fm.name ? fm.name : basename,
6023
+ id: typeof fm.id === "string" && fm.id ? fm.id : basename2,
6024
+ name: typeof fm.name === "string" && fm.name ? fm.name : basename2,
5831
6025
  type: typeof fm.type === "string" ? fm.type : "unknown",
5832
6026
  status: typeof fm.status === "string" ? fm.status : "unknown",
5833
6027
  effort: typeof fm.effort === "number" ? fm.effort : null,
5834
6028
  owner: typeof fm.owner === "string" ? fm.owner : null,
5835
6029
  tags: Array.isArray(fm.tags) ? fm.tags.map(String) : [],
5836
6030
  archived: fm.archived === true,
5837
- basename,
6031
+ basename: basename2,
5838
6032
  relPath,
5839
6033
  links: extractBodyLinks(body),
5840
6034
  created: normalizeDateField(fm.created),
@@ -5920,9 +6114,9 @@ function invalidateVaultCacheForFile(absPath) {
5920
6114
  else evictProcessVaultHotRows();
5921
6115
  }
5922
6116
  function specIdFromAbsPath(absPath) {
5923
- const basename = path13.basename(absPath).replace(/\.md$/i, "");
5924
- if (!basename) return null;
5925
- const parsed = parseSpecId(basename);
6117
+ const basename2 = path13.basename(absPath).replace(/\.md$/i, "");
6118
+ if (!basename2) return null;
6119
+ const parsed = parseSpecId(basename2);
5926
6120
  if (parsed?.era === 3 && parsed.fileKey) return `HS-${parsed.fileKey.toUpperCase()}`;
5927
6121
  return null;
5928
6122
  }
@@ -6178,8 +6372,8 @@ var init_vault = __esm({
6178
6372
 
6179
6373
  // spex-console/bin/hs-index-reconcile.ts
6180
6374
  import { execFileSync } from "node:child_process";
6181
- import { appendFileSync, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "node:fs";
6182
- import { promises as fs9 } from "node:fs";
6375
+ import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "node:fs";
6376
+ import { promises as fs10 } from "node:fs";
6183
6377
  import path14 from "node:path";
6184
6378
  import process4 from "node:process";
6185
6379
 
@@ -6346,7 +6540,7 @@ function resolveSyncVaultTarget(input) {
6346
6540
  detail: `heyspex.yaml vault_id${drift}`
6347
6541
  };
6348
6542
  }
6349
- if (input.pointer.status === "read" && input.pointer.vaultId) {
6543
+ if (input.allowPointer && input.pointer.status === "read" && input.pointer.vaultId) {
6350
6544
  return {
6351
6545
  ok: true,
6352
6546
  vaultId: input.pointer.vaultId,
@@ -6354,9 +6548,9 @@ function resolveSyncVaultTarget(input) {
6354
6548
  detail: "this account's active vault, read from the console (heyspex.yaml names none)"
6355
6549
  };
6356
6550
  }
6357
- 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";
6551
+ 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";
6358
6552
  return refuse(
6359
- `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>\`.`
6553
+ `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).`
6360
6554
  );
6361
6555
  }
6362
6556
  async function resolveSyncVaultTargetFromEnv(deps) {
@@ -6378,8 +6572,11 @@ async function resolveSyncVaultTargetFromEnv(deps) {
6378
6572
  } catch {
6379
6573
  registryRowId = null;
6380
6574
  }
6575
+ const allowPointer = ["1", "true", "yes"].includes(
6576
+ (deps.env.HEYSPEX_ALLOW_POINTER_VAULT ?? "").trim().toLowerCase()
6577
+ );
6381
6578
  let pointer = { status: "unreadable", detail: "not read" };
6382
- if (!override && !markerVaultId) {
6579
+ if (!override && !markerVaultId && allowPointer) {
6383
6580
  const http = {
6384
6581
  fetchImpl: deps.fetchImpl ?? fetch,
6385
6582
  consoleUrl: deps.consoleUrl,
@@ -6401,7 +6598,8 @@ async function resolveSyncVaultTargetFromEnv(deps) {
6401
6598
  markerVaultId,
6402
6599
  registryRowId,
6403
6600
  pointer,
6404
- verb: deps.verb
6601
+ verb: deps.verb,
6602
+ allowPointer
6405
6603
  });
6406
6604
  }
6407
6605
 
@@ -6504,9 +6702,9 @@ function isRoutableStatus(status, type) {
6504
6702
  if (ROUTE_SETTABLE_STATUSES.has(status)) return true;
6505
6703
  return (type ?? "").toLowerCase() === "adr" && ADR_SETTABLE_STATUSES.has(status);
6506
6704
  }
6507
- function matchesSelector(id, basename, selectors) {
6705
+ function matchesSelector(id, basename2, selectors) {
6508
6706
  const i = id.toLowerCase();
6509
- const b = basename.toLowerCase();
6707
+ const b = basename2.toLowerCase();
6510
6708
  return selectors.some((raw) => {
6511
6709
  const s = raw.trim().toLowerCase();
6512
6710
  return s.length > 0 && (i === s || b === s || i.startsWith(`${s}-`) || b.startsWith(`${s}-`));
@@ -6546,20 +6744,20 @@ function decideReconcilePlan({
6546
6744
  const c = cloudById.get(id) ?? null;
6547
6745
  const primary = d ?? c;
6548
6746
  const relPath = reconcileRelPath(primary.relPath);
6549
- const basename = primary.basename || basenameOfRelPath(relPath);
6747
+ const basename2 = primary.basename || basenameOfRelPath(relPath);
6550
6748
  const type = d?.type ?? c?.type ?? null;
6551
6749
  const base = {
6552
6750
  id,
6553
6751
  type,
6554
- basename,
6752
+ basename: basename2,
6555
6753
  relPath,
6556
6754
  disk: view(d),
6557
6755
  cloud: view(c),
6558
6756
  ...d?.closeReason ? { closeReason: d.closeReason } : {}
6559
6757
  };
6560
6758
  if (d && isCapabilityDoc(d.relPath)) capDocsOnDisk += 1;
6561
- if (only.length > 0 && !matchesSelector(id, basename, only)) continue;
6562
- if (skip.length > 0 && matchesSelector(id, basename, skip)) {
6759
+ if (only.length > 0 && !matchesSelector(id, basename2, only)) continue;
6760
+ if (skip.length > 0 && matchesSelector(id, basename2, skip)) {
6563
6761
  rows.push(hold({ ...base, winner: "none" }, "skipped", "held by --skip"));
6564
6762
  continue;
6565
6763
  }
@@ -6827,10 +7025,10 @@ async function applyReconcilePlan(deps, plan) {
6827
7025
  }
6828
7026
  return summarizeResult(rows, plan, sideEffects);
6829
7027
  }
6830
- function interpretPatch(requested, res, basename, sideEffects) {
7028
+ function interpretPatch(requested, res, basename2, sideEffects) {
6831
7029
  const json = rec(res.json);
6832
7030
  if (res.status >= 200 && res.status < 300) {
6833
- pushSideEffects(sideEffects, basename, json);
7031
+ pushSideEffects(sideEffects, basename2, json);
6834
7032
  const coercedTo = str2(json.coerced_to) ?? str2(json.coercedTo);
6835
7033
  if (coercedTo && coercedTo !== requested) {
6836
7034
  return {
@@ -7109,23 +7307,22 @@ async function postEdgesChunk(input, specs2) {
7109
7307
  }
7110
7308
 
7111
7309
  // spex-console/src/lib/vault-sync/pull-io.ts
7112
- import { promises as fs8 } from "node:fs";
7113
- import { homedir as homedir9 } from "node:os";
7114
- import nodePath from "node:path";
7310
+ import { promises as fs9 } from "node:fs";
7311
+ import nodePath2 from "node:path";
7115
7312
 
7116
7313
  // spex-console/src/lib/dev-mode.ts
7314
+ init_heyspex_home2();
7117
7315
  init_credentials();
7118
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
7119
- import { homedir as homedir8 } from "node:os";
7120
- import { join as join9 } from "node:path";
7316
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
7317
+ import { join as join10 } from "node:path";
7121
7318
  var CLOUD_CONSOLE_URL2 = "https://app.heyspex.com";
7122
- var defaultReadFile3 = (p) => readFileSync9(p, "utf8");
7319
+ var defaultReadFile3 = (p) => readFileSync10(p, "utf8");
7123
7320
  function readCredentialsMcpToken({
7124
- homeDir = homedir8(),
7321
+ homeDir = heyspexHome2(),
7125
7322
  readFile = defaultReadFile3
7126
7323
  } = {}) {
7127
7324
  try {
7128
- const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
7325
+ const raw = readFile(join10(homeDir, ".heyspex", "credentials.yaml"));
7129
7326
  const match = raw.match(/^mcp_token:\s*["']?([^"'\r\n#]+)/m);
7130
7327
  return match?.[1]?.trim() || null;
7131
7328
  } catch {
@@ -7134,8 +7331,19 @@ function readCredentialsMcpToken({
7134
7331
  }
7135
7332
 
7136
7333
  // spex-console/src/lib/vault-sync/pull-io.ts
7334
+ init_heyspex_home2();
7137
7335
  init_vault();
7138
7336
  init_vault_frontmatter();
7337
+
7338
+ // spex-console/src/lib/vault-sync/ledger.ts
7339
+ import { createHash as createHash3 } from "node:crypto";
7340
+ import { promises as fs8 } from "node:fs";
7341
+ import nodePath from "node:path";
7342
+ function hashContent(content) {
7343
+ return createHash3("sha256").update(content, "utf8").digest("hex");
7344
+ }
7345
+
7346
+ // spex-console/src/lib/vault-sync/pull-io.ts
7139
7347
  var LIST_FIELDS = "id,basename,relPath,type,status,updated,updatedAt";
7140
7348
  var PULL_LIST_PAGE_LIMIT = 500;
7141
7349
  var PULL_LIST_MAX_PAGES = 2e3;
@@ -7149,7 +7357,7 @@ var VaultPullSourceError = class extends Error {
7149
7357
  };
7150
7358
  function resolvePullToken({
7151
7359
  env = process.env,
7152
- homeDir = homedir9()
7360
+ homeDir = heyspexHome2()
7153
7361
  } = {}) {
7154
7362
  const fromEnv = env.HEYSPEX_MCP_TOKEN?.trim();
7155
7363
  if (fromEnv) return fromEnv;
@@ -7248,22 +7456,68 @@ async function fetchRemoteSpecContent(http, item) {
7248
7456
  }
7249
7457
  return { body: spec.body, frontmatter };
7250
7458
  }
7459
+ function remoteStamp(spec) {
7460
+ if (!spec) return null;
7461
+ const fm = spec.frontmatter;
7462
+ for (const candidate of [fm?.updated, spec.updatedAt, spec.updated]) {
7463
+ const raw = candidate instanceof Date ? candidate.toISOString() : candidate;
7464
+ if (typeof raw === "string" && raw.trim()) {
7465
+ const ms = Date.parse(raw);
7466
+ if (!Number.isNaN(ms)) return /T/.test(raw) ? new Date(ms).toISOString() : raw.trim();
7467
+ }
7468
+ }
7469
+ return null;
7470
+ }
7471
+ async function fetchRemoteRefAndContent(http, selector) {
7472
+ const { status, json, text: text2 } = await getJson(http, `api/specs/${encodeURIComponent(selector)}`);
7473
+ if (status === 404) return null;
7474
+ if (status !== 200) {
7475
+ throw new VaultPullSourceError(
7476
+ `GET /api/specs/${selector} returned ${status}: ${text2.slice(0, 200)}`,
7477
+ status
7478
+ );
7479
+ }
7480
+ const spec = json?.spec;
7481
+ const fm = spec?.frontmatter && typeof spec.frontmatter === "object" ? spec.frontmatter : null;
7482
+ if (!spec || typeof spec.body !== "string" || !fm || Object.keys(fm).length === 0) {
7483
+ throw new VaultPullSourceError(`GET /api/specs/${selector} returned no usable spec`, status);
7484
+ }
7485
+ return {
7486
+ ref: {
7487
+ id: String(spec.id ?? fm.id ?? ""),
7488
+ basename: typeof spec.basename === "string" ? spec.basename : null,
7489
+ relPath: typeof spec.relPath === "string" ? spec.relPath : null,
7490
+ type: typeof spec.type === "string" ? spec.type : null,
7491
+ status: typeof spec.status === "string" ? spec.status : null,
7492
+ updated: typeof spec.updated === "string" ? spec.updated : null,
7493
+ updatedAt: remoteStamp(spec)
7494
+ },
7495
+ content: { body: spec.body, frontmatter: fm }
7496
+ };
7497
+ }
7498
+ async function hashFile(abs) {
7499
+ try {
7500
+ return hashContent(await fs9.readFile(abs, "utf8"));
7501
+ } catch {
7502
+ return null;
7503
+ }
7504
+ }
7251
7505
  async function listLocalSpecRelPaths(root = vaultRoot()) {
7252
7506
  const out = [];
7253
7507
  async function walk(dir) {
7254
7508
  let entries;
7255
7509
  try {
7256
- entries = await fs8.readdir(dir, { withFileTypes: true });
7510
+ entries = await fs9.readdir(dir, { withFileTypes: true });
7257
7511
  } catch {
7258
7512
  return;
7259
7513
  }
7260
7514
  for (const entry of entries) {
7261
- const full = nodePath.join(dir, entry.name);
7515
+ const full = nodePath2.join(dir, entry.name);
7262
7516
  if (entry.isDirectory()) {
7263
7517
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
7264
7518
  await walk(full);
7265
7519
  } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
7266
- out.push(nodePath.relative(root, full).split(nodePath.sep).join("/"));
7520
+ out.push(nodePath2.relative(root, full).split(nodePath2.sep).join("/"));
7267
7521
  }
7268
7522
  }
7269
7523
  }
@@ -7275,20 +7529,52 @@ function createVaultPullDeps({
7275
7529
  token,
7276
7530
  fetchImpl,
7277
7531
  localRoot,
7278
- scopeHeaders
7532
+ scopeHeaders,
7533
+ ledger,
7534
+ onLocalWrite
7279
7535
  }) {
7280
7536
  const http = { baseUrl, token, fetchImpl, scopeHeaders };
7537
+ const prefetched = /* @__PURE__ */ new Map();
7281
7538
  return {
7282
7539
  listRemote: () => listRemoteSpecs(http),
7283
- fetchRemoteSpec: (item) => fetchRemoteSpecContent(http, item),
7540
+ getRemoteRef: async (selector) => {
7541
+ const hit = await fetchRemoteRefAndContent(http, selector);
7542
+ if (!hit) return null;
7543
+ if (hit.ref.basename) prefetched.set(hit.ref.basename, hit.content);
7544
+ return hit.ref;
7545
+ },
7546
+ fetchRemoteSpec: async (item) => {
7547
+ const cached = prefetched.get(item.basename);
7548
+ if (cached) {
7549
+ prefetched.delete(item.basename);
7550
+ return cached;
7551
+ }
7552
+ return fetchRemoteSpecContent(http, item);
7553
+ },
7554
+ readLedger: ledger ? () => ledger.snapshot() : void 0,
7555
+ hashLocal: (relPath) => hashFile(nodePath2.join(localRoot ?? vaultRoot(), relPath)),
7556
+ finish: ledger ? () => ledger.flush() : void 0,
7284
7557
  listLocal: () => listLocalSpecRelPaths(localRoot ?? vaultRoot()),
7285
7558
  serialize: stringifySpecMarkdown,
7286
- writeSpec: localRoot ? async (relPath, content) => {
7287
- const abs = nodePath.join(localRoot, relPath);
7288
- await fs8.mkdir(nodePath.dirname(abs), { recursive: true });
7289
- await fs8.writeFile(abs, content, "utf8");
7559
+ writeSpec: localRoot ? async (relPath, content, opts) => {
7560
+ const abs = nodePath2.join(localRoot, relPath);
7561
+ await fs9.mkdir(nodePath2.dirname(abs), { recursive: true });
7562
+ onLocalWrite?.(relPath);
7563
+ await fs9.writeFile(abs, content, {
7564
+ encoding: "utf8",
7565
+ flag: opts?.overwrite === true ? "w" : "wx"
7566
+ });
7290
7567
  } : (relPath, content) => writeSpecBytesToVault(relPath, content),
7291
- afterWrite: (relPath) => updateVaultCacheEntry(nodePath.join(localRoot ?? vaultRoot(), relPath))
7568
+ afterWrite: async (relPath, info) => {
7569
+ if (ledger && info) {
7570
+ ledger.record(relPath, {
7571
+ hash: hashContent(info.content),
7572
+ remoteUpdatedAt: info.remoteUpdatedAt,
7573
+ by: "pull"
7574
+ });
7575
+ }
7576
+ await updateVaultCacheEntry(nodePath2.join(localRoot ?? vaultRoot(), relPath));
7577
+ }
7292
7578
  };
7293
7579
  }
7294
7580
 
@@ -7447,7 +7733,7 @@ function say(args, text2) {
7447
7733
  }
7448
7734
  function readDiscovery(p) {
7449
7735
  try {
7450
- return readFileSync12(p, "utf-8");
7736
+ return readFileSync13(p, "utf-8");
7451
7737
  } catch {
7452
7738
  return null;
7453
7739
  }
@@ -7459,7 +7745,7 @@ function resolveConsole(args) {
7459
7745
  }
7460
7746
  function readMarkerVaultId(vaultRoot2) {
7461
7747
  try {
7462
- return parseVaultId(readFileSync12(path14.join(vaultRoot2, "heyspex.yaml"), "utf8"));
7748
+ return parseVaultId(readFileSync13(path14.join(vaultRoot2, "heyspex.yaml"), "utf8"));
7463
7749
  } catch {
7464
7750
  return null;
7465
7751
  }
@@ -7479,7 +7765,7 @@ async function walkLocal(args) {
7479
7765
  async function walk(dir) {
7480
7766
  let entries;
7481
7767
  try {
7482
- entries = await fs9.readdir(dir, { withFileTypes: true });
7768
+ entries = await fs10.readdir(dir, { withFileTypes: true });
7483
7769
  } catch {
7484
7770
  return;
7485
7771
  }
@@ -7501,7 +7787,7 @@ async function walkLocal(args) {
7501
7787
  for (const abs of files) {
7502
7788
  const relPath = path14.relative(specsDir, abs).split(path14.sep).join("/");
7503
7789
  try {
7504
- const content = await fs9.readFile(abs, "utf8");
7790
+ const content = await fs10.readFile(abs, "utf8");
7505
7791
  const parsed = parseSpecFromContent(content, {
7506
7792
  relPath: `specs/${relPath}`,
7507
7793
  basenameFallback: path14.basename(abs).replace(/\.md$/i, "")
@@ -7635,15 +7921,15 @@ function reportStamp() {
7635
7921
  }
7636
7922
  function writeReport(vaultRoot2, payload) {
7637
7923
  const dir = path14.join(vaultRoot2, ".heyspex", "index-reconcile");
7638
- mkdirSync5(dir, { recursive: true });
7924
+ mkdirSync6(dir, { recursive: true });
7639
7925
  const file = path14.join(dir, `${reportStamp()}.json`);
7640
- writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
7926
+ writeFileSync6(file, `${JSON.stringify(payload, null, 2)}
7641
7927
  `, "utf-8");
7642
7928
  return file;
7643
7929
  }
7644
7930
  function appendJournal(vaultRoot2, message) {
7645
7931
  const heyspexDir = path14.join(vaultRoot2, ".heyspex");
7646
- mkdirSync5(heyspexDir, { recursive: true });
7932
+ mkdirSync6(heyspexDir, { recursive: true });
7647
7933
  appendFileSync(
7648
7934
  path14.join(heyspexDir, "index-reconcile.log"),
7649
7935
  `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
@@ -7740,8 +8026,8 @@ async function main() {
7740
8026
  localRoot: path14.join(args.vaultRoot, "specs")
7741
8027
  });
7742
8028
  const deps = {
7743
- patchStatus: async (basename, body) => {
7744
- const route = `/api/specs/${encodeURIComponent(basename)}/status`;
8029
+ patchStatus: async (basename2, body) => {
8030
+ const route = `/api/specs/${encodeURIComponent(basename2)}/status`;
7745
8031
  try {
7746
8032
  const res = await fetch(`${consoleUrl}${route}`, {
7747
8033
  method: "PATCH",
@@ -7775,7 +8061,7 @@ async function main() {
7775
8061
  const content = await pull.fetchRemoteSpec(item);
7776
8062
  if (!content) throw new Error("remote returned no content for this spec");
7777
8063
  const bytes = pull.serialize(content.body, content.frontmatter);
7778
- await pull.writeSpec(item.relPath, bytes);
8064
+ await pull.writeSpec(item.relPath, bytes, { overwrite: item.overwrites });
7779
8065
  try {
7780
8066
  await pull.afterWrite?.(item.relPath);
7781
8067
  } catch {