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
@@ -39,11 +39,26 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  mod
40
40
  ));
41
41
 
42
+ // scripts/lib/heyspex-home.mjs
43
+ import { homedir } from "node:os";
44
+ function heyspexHome(env = process.env) {
45
+ const override = env?.HEYSPEX_HOME;
46
+ if (typeof override === "string" && override.trim().length > 0) {
47
+ return override.trim();
48
+ }
49
+ return homedir();
50
+ }
51
+ var init_heyspex_home = __esm({
52
+ "scripts/lib/heyspex-home.mjs"() {
53
+ "use strict";
54
+ }
55
+ });
56
+
42
57
  // vendor/heyspex-mcp/credentials.mjs
43
58
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
44
- import os, { homedir } from "node:os";
59
+ import os, { homedir as homedir2 } from "node:os";
45
60
  import { dirname, join, resolve } from "node:path";
46
- function credentialsFilePath(homeDir = homedir()) {
61
+ function credentialsFilePath(homeDir = heyspexHome()) {
47
62
  return join(homeDir, ".heyspex", "credentials.yaml");
48
63
  }
49
64
  function parseMcpToken(raw) {
@@ -139,7 +154,7 @@ function readLegacyConfigAccountId(read, homeDir) {
139
154
  const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
140
155
  return value || null;
141
156
  }
142
- function readCurrentAccountId({ homeDir = homedir(), readFile } = {}) {
157
+ function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
143
158
  const read = readFile ?? ((p) => readFileSync(p, "utf8"));
144
159
  const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
145
160
  if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
@@ -152,6 +167,7 @@ var MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FI
152
167
  var init_credentials = __esm({
153
168
  "vendor/heyspex-mcp/credentials.mjs"() {
154
169
  "use strict";
170
+ init_heyspex_home();
155
171
  MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
156
172
  CREDENTIALS_SCHEMA_VERSION = 2;
157
173
  LEGACY_ACCOUNT_ID = "legacy";
@@ -163,9 +179,9 @@ var init_credentials = __esm({
163
179
 
164
180
  // scripts/lib/dev-mode.mjs
165
181
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
166
- import { homedir as homedir2 } from "node:os";
182
+ import { homedir as homedir3 } from "node:os";
167
183
  import { join as join2 } from "node:path";
168
- function devModeFilePath(homeDir = homedir2()) {
184
+ function devModeFilePath(homeDir = heyspexHome()) {
169
185
  return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
170
186
  }
171
187
  function normalizeMode(raw) {
@@ -190,7 +206,7 @@ function deriveSemantics(mode, reliance) {
190
206
  vaultWriteMode: local ? "local" : "cloud"
191
207
  };
192
208
  }
193
- function readLocalDevModeRecord({ homeDir = homedir2(), readFile = defaultReadFile } = {}) {
209
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
194
210
  let raw;
195
211
  try {
196
212
  raw = readFile(devModeFilePath(homeDir));
@@ -220,7 +236,7 @@ function readLocalDevModeRecord({ homeDir = homedir2(), readFile = defaultReadFi
220
236
  }
221
237
  function resolveDevMode({
222
238
  env = process.env,
223
- homeDir = homedir2(),
239
+ homeDir = heyspexHome(),
224
240
  readFile = defaultReadFile,
225
241
  cloudSetting = void 0
226
242
  } = {}) {
@@ -283,6 +299,7 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
283
299
  var init_dev_mode = __esm({
284
300
  "scripts/lib/dev-mode.mjs"() {
285
301
  "use strict";
302
+ init_heyspex_home();
286
303
  init_credentials();
287
304
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
288
305
  LOCAL_CONSOLE_URL = "http://localhost:3000";
@@ -293,7 +310,7 @@ var init_dev_mode = __esm({
293
310
 
294
311
  // scripts/lib/console-url-resolver.mjs
295
312
  import { readFileSync as readFileSync3 } from "node:fs";
296
- import { homedir as homedir3 } from "node:os";
313
+ import { homedir as homedir4 } from "node:os";
297
314
  import { join as join3 } from "node:path";
298
315
  function trimTrailingSlash(url) {
299
316
  return url.replace(/\/+$/, "");
@@ -317,7 +334,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
317
334
  } catch {
318
335
  }
319
336
  }
320
- function readCredentialsConsoleUrl({ homeDir = homedir3(), readFile = defaultReadFile2 } = {}) {
337
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
321
338
  try {
322
339
  const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
323
340
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
@@ -353,7 +370,7 @@ function resolveModeEnvelope({ env, homeDir }) {
353
370
  function resolveConsoleUrl({
354
371
  repoRoot: repoRoot2,
355
372
  env = process.env,
356
- homeDir = homedir3(),
373
+ homeDir = heyspexHome(),
357
374
  strict = false,
358
375
  devMode,
359
376
  log = (line) => process.stderr.write(line),
@@ -386,6 +403,7 @@ var DEFAULT_CONSOLE_URL, LOOPBACK_HOSTS, staleLoopbackCredentialsWarned, Console
386
403
  var init_console_url_resolver = __esm({
387
404
  "scripts/lib/console-url-resolver.mjs"() {
388
405
  "use strict";
406
+ init_heyspex_home();
389
407
  init_dev_mode();
390
408
  DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
391
409
  LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
@@ -401,11 +419,26 @@ var init_console_url_resolver = __esm({
401
419
  }
402
420
  });
403
421
 
422
+ // spex-console/src/lib/heyspex-home.ts
423
+ import { homedir as homedir5 } from "node:os";
424
+ function heyspexHome2(env = process.env) {
425
+ const override = env?.HEYSPEX_HOME;
426
+ if (typeof override === "string" && override.trim().length > 0) {
427
+ return override.trim();
428
+ }
429
+ return homedir5();
430
+ }
431
+ var init_heyspex_home2 = __esm({
432
+ "spex-console/src/lib/heyspex-home.ts"() {
433
+ "use strict";
434
+ }
435
+ });
436
+
404
437
  // vendor/heyspex-mcp/session-owner.mjs
405
438
  import { readFileSync as readFileSync4 } from "node:fs";
406
- import { homedir as homedir4 } from "node:os";
439
+ import { homedir as homedir6 } from "node:os";
407
440
  import { join as join4 } from "node:path";
408
- function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir4(), readFile } = {}) {
441
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
409
442
  const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
410
443
  if (fromEnv) return fromEnv;
411
444
  return readCurrentAccountId({ homeDir, readFile });
@@ -413,6 +446,7 @@ function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir4(),
413
446
  var init_session_owner = __esm({
414
447
  "vendor/heyspex-mcp/session-owner.mjs"() {
415
448
  "use strict";
449
+ init_heyspex_home();
416
450
  init_credentials();
417
451
  }
418
452
  });
@@ -426,11 +460,10 @@ var init_default_workspace = __esm({
426
460
 
427
461
  // spex-console/src/lib/cli/vault-workspace-shared.ts
428
462
  import { readFileSync as readFileSync5 } from "node:fs";
429
- import os2 from "node:os";
430
463
  function resolveConfiguredAccountId(opts = {}) {
431
464
  return resolveSessionOwnerUncached({
432
465
  env: opts.env ?? process.env,
433
- homeDir: opts.homeDir ?? os2.homedir(),
466
+ homeDir: opts.homeDir ?? heyspexHome2(),
434
467
  readFile: opts.readFile ?? ((p) => readFileSync5(p, "utf-8"))
435
468
  });
436
469
  }
@@ -464,6 +497,7 @@ var CLOUD_ACCOUNT_ID_REQUIRED_MESSAGE;
464
497
  var init_vault_workspace_shared = __esm({
465
498
  "spex-console/src/lib/cli/vault-workspace-shared.ts"() {
466
499
  "use strict";
500
+ init_heyspex_home2();
467
501
  init_session_owner();
468
502
  init_default_workspace();
469
503
  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.";
@@ -1157,7 +1191,7 @@ var init_diagnose_skeleton = __esm({
1157
1191
  });
1158
1192
 
1159
1193
  // dist/cli/._sql-orm-inert.cli-shim.mjs
1160
- var inert, customType, drizzle, sql, and, or, eq, ne, isNull, asc, gt, count, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1194
+ var inert, customType, drizzle, sql, and, or, eq, ne, isNull, desc, asc, gt, count, inArray, bigint, bigserial, boolean, foreignKey, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex;
1161
1195
  var init_sql_orm_inert_cli_shim = __esm({
1162
1196
  "dist/cli/._sql-orm-inert.cli-shim.mjs"() {
1163
1197
  "use strict";
@@ -1183,6 +1217,7 @@ var init_sql_orm_inert_cli_shim = __esm({
1183
1217
  eq = inert;
1184
1218
  ne = inert;
1185
1219
  isNull = inert;
1220
+ desc = inert;
1186
1221
  asc = inert;
1187
1222
  gt = inert;
1188
1223
  count = inert;
@@ -1214,7 +1249,7 @@ var init_interfaces = __esm({
1214
1249
  });
1215
1250
 
1216
1251
  // spex-console/src/lib/db/schema/postgres.ts
1217
- 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;
1252
+ 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;
1218
1253
  var init_postgres = __esm({
1219
1254
  "spex-console/src/lib/db/schema/postgres.ts"() {
1220
1255
  "use strict";
@@ -1656,7 +1691,13 @@ var init_postgres = __esm({
1656
1691
  "priority_change",
1657
1692
  "parent_change",
1658
1693
  "claim_acquired",
1659
- "claim_released"
1694
+ "claim_released",
1695
+ // Collaborator write audit (HS-FWB6, migration 0077).
1696
+ "spec_created",
1697
+ "spec_edited",
1698
+ "spec_deleted",
1699
+ "capture_written",
1700
+ "wip_recorded"
1660
1701
  ]);
1661
1702
  focusPin = pgTable(
1662
1703
  "focus_pin",
@@ -2849,11 +2890,43 @@ var init_postgres = __esm({
2849
2890
  // `owner/name`
2850
2891
  branch: text("branch").notNull(),
2851
2892
  commitSha: text("commit_sha").notNull(),
2852
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
2893
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
2894
+ // The pull request that carries this settlement
2895
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
2896
+ // the branch has gone quiet and a PR was opened or found; a write made
2897
+ // after its PR merged stays NULL, which is what triggers the next PR.
2898
+ pullRequestNumber: integer("pull_request_number"),
2899
+ pullRequestUrl: text("pull_request_url")
2853
2900
  },
2854
2901
  (table) => [
2855
2902
  // "latest settlement for this spec" — the read path's only query.
2856
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
2903
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
2904
+ // The PR sweep's only query: branches with settlements not yet in a PR.
2905
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
2906
+ ]
2907
+ );
2908
+ sessionBranchDeclarations = pgTable(
2909
+ "session_branch_declarations",
2910
+ {
2911
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
2912
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
2913
+ actorKey: text("actor_key").notNull(),
2914
+ sessionId: text("session_id").notNull(),
2915
+ branch: text("branch").notNull(),
2916
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
2917
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
2918
+ },
2919
+ (table) => [
2920
+ uniqueIndex("session_branch_declarations_uq").on(
2921
+ table.vaultId,
2922
+ table.actorKey,
2923
+ table.sessionId
2924
+ ),
2925
+ index("session_branch_declarations_lookup_idx").on(
2926
+ table.vaultId,
2927
+ table.actorKey,
2928
+ table.declaredAt.desc()
2929
+ )
2857
2930
  ]
2858
2931
  );
2859
2932
  }
@@ -3143,10 +3216,9 @@ var init_types = __esm({
3143
3216
 
3144
3217
  // spex-console/src/lib/vault-registry/registry-file.ts
3145
3218
  import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
3146
- import { homedir as homedir5 } from "node:os";
3147
3219
  import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
3148
3220
  function vaultRegistryDir(opts = {}) {
3149
- return opts.registryDir ?? join5(homedir5(), ".heyspex");
3221
+ return opts.registryDir ?? join5(heyspexHome2(), ".heyspex");
3150
3222
  }
3151
3223
  function vaultRegistryFilePath(opts = {}) {
3152
3224
  return join5(vaultRegistryDir(opts), "registry.json");
@@ -3300,6 +3372,7 @@ function accountPointersOf(registry, accountId) {
3300
3372
  var init_registry_file = __esm({
3301
3373
  "spex-console/src/lib/vault-registry/registry-file.ts"() {
3302
3374
  "use strict";
3375
+ init_heyspex_home2();
3303
3376
  init_types();
3304
3377
  init_types();
3305
3378
  }
@@ -3478,6 +3551,30 @@ function isAbsolutePathForCompare(p) {
3478
3551
  const s = p.trim();
3479
3552
  return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
3480
3553
  }
3554
+ function expandPinPlaceholders(value, repoRoot2) {
3555
+ return value.replace(/\$\{(?:CLAUDE_PROJECT_DIR|workspaceFolder)(?::-[^}]*)?\}/g, repoRoot2);
3556
+ }
3557
+ function lexicalJoin(base, rel) {
3558
+ const parts = `${base}/${rel}`.replace(/\\/g, "/").split("/");
3559
+ const out = [];
3560
+ for (const seg of parts) {
3561
+ if (seg === "" && out.length > 0) continue;
3562
+ if (seg === ".") continue;
3563
+ if (seg === ".." && out.length > 1) {
3564
+ out.pop();
3565
+ continue;
3566
+ }
3567
+ out.push(seg);
3568
+ }
3569
+ return out.join("/");
3570
+ }
3571
+ function parseMarkerVaultPath(content) {
3572
+ const m = content.match(/^vault_path:[ \t]*(.+?)[ \t]*$/m);
3573
+ if (!m) return null;
3574
+ const v = m[1].trim().replace(/\s+#.*$/, "").replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
3575
+ if (!v || v.includes("${") || /^[\\/]/.test(v) || /^[a-zA-Z]:[\\/]/.test(v)) return null;
3576
+ return v;
3577
+ }
3481
3578
  function readPinnedVaultRoot(repoRoot2, deps) {
3482
3579
  const pins = [];
3483
3580
  for (const rel of SATELLITE_MANIFEST_RELS) {
@@ -3494,9 +3591,13 @@ function readPinnedVaultRoot(repoRoot2, deps) {
3494
3591
  }
3495
3592
  if (typeof value !== "string") continue;
3496
3593
  const trimmed = value.trim();
3497
- if (!trimmed || trimmed.includes("${")) continue;
3498
- if (!isAbsolutePathForCompare(trimmed)) continue;
3499
- pins.push({ source: rel.join("/"), value: trimmed });
3594
+ if (!trimmed) continue;
3595
+ const expanded = expandPinPlaceholders(trimmed, repoRoot2);
3596
+ if (expanded.includes("${")) continue;
3597
+ if (/^[a-zA-Z]:(?![\\/])/.test(expanded)) continue;
3598
+ const abs = isAbsolutePathForCompare(expanded) ? expanded.includes("..") ? lexicalJoin(expanded, ".") : expanded : lexicalJoin(repoRoot2, expanded);
3599
+ if (expanded !== trimmed && isAtOrUnder(abs, repoRoot2)) continue;
3600
+ pins.push({ source: rel.join("/"), value: abs });
3500
3601
  }
3501
3602
  return pins;
3502
3603
  }
@@ -3547,6 +3648,24 @@ function readMarkerVaultId(dir) {
3547
3648
  return null;
3548
3649
  }
3549
3650
  }
3651
+ function readMarkerSatellite(dir) {
3652
+ let raw;
3653
+ try {
3654
+ raw = readFileSync7(path2.join(dir, "heyspex.yaml"), "utf8");
3655
+ } catch {
3656
+ return null;
3657
+ }
3658
+ const vaultPath = parseMarkerVaultPath(raw);
3659
+ if (!vaultPath) return null;
3660
+ const vaultId = parseMarkerVaultId(raw);
3661
+ const candidate = path2.resolve(dir, vaultPath);
3662
+ let vaultRootAbs = null;
3663
+ if (candidate.toLowerCase() !== path2.resolve(dir).toLowerCase() && isVaultRootDir(candidate)) {
3664
+ const theirs = readMarkerVaultId(candidate);
3665
+ if (!vaultId || !theirs || theirs === vaultId) vaultRootAbs = candidate;
3666
+ }
3667
+ return { vaultId, vaultRootAbs };
3668
+ }
3550
3669
  function isVaultRootDir(dir) {
3551
3670
  const norm = (p) => path2.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
3552
3671
  if (norm(dir) === norm(tmpdir())) return false;
@@ -3567,6 +3686,14 @@ function findCwdVaultOrAttachment(startDir, stopAt) {
3567
3686
  const boundary = stopAt ? path2.resolve(stopAt) : null;
3568
3687
  let dir = startDir;
3569
3688
  for (let i = 0; i < 256; i += 1) {
3689
+ const marked = readMarkerSatellite(dir);
3690
+ if (marked) {
3691
+ return {
3692
+ repoRootAbs: marked.vaultRootAbs ?? dir,
3693
+ satellite: true,
3694
+ markerVaultId: marked.vaultId
3695
+ };
3696
+ }
3570
3697
  const pinned = readPinnedVaultRoot(dir, deps).find((pin) => !isAtOrUnder(pin.value, dir));
3571
3698
  if (pinned) return { repoRootAbs: path2.resolve(pinned.value), satellite: true };
3572
3699
  if (isVaultRootDir(dir)) return { repoRootAbs: dir, satellite: false };
@@ -3612,6 +3739,9 @@ async function resolveActiveVaultRoot(options2 = {}) {
3612
3739
  const cwdMatch = findCwdVaultOrAttachment(cwdBase, options2.cwdWalkRoot);
3613
3740
  if (cwdMatch?.satellite) {
3614
3741
  onTier({ tier: "satellite-pin", outcome: "resolved", detail: cwdMatch.repoRootAbs });
3742
+ if (cwdMatch.markerVaultId) {
3743
+ return finalize(cwdMatch.markerVaultId, path2.resolve(cwdMatch.repoRootAbs), "satellite-pin");
3744
+ }
3615
3745
  return finalizePathTier(cwdMatch.repoRootAbs, "satellite-pin");
3616
3746
  }
3617
3747
  if (cwdMatch) {
@@ -4795,6 +4925,55 @@ var init_branch_policy = __esm({
4795
4925
  }
4796
4926
  });
4797
4927
 
4928
+ // spex-console/src/lib/git-export/branch-resolver.ts
4929
+ function isValidSessionBranch(name, defaultBranch) {
4930
+ const b = name.trim();
4931
+ if (!b) return { ok: false, reason: "branch name is empty" };
4932
+ if (b === defaultBranch) {
4933
+ return { ok: false, reason: `\`${b}\` is the default branch; the drain never writes it` };
4934
+ }
4935
+ if (PROTECTED_SESSION_BRANCHES.includes(b)) {
4936
+ return { ok: false, reason: `\`${b}\` is a protected ref` };
4937
+ }
4938
+ const hasControlChar = [...b].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
4939
+ if (hasControlChar || /[\s~^:?*[\\]/.test(b)) {
4940
+ return { ok: false, reason: "branch name contains characters git refs do not allow" };
4941
+ }
4942
+ if (b.includes("..") || b.includes("@{") || b.includes("//")) {
4943
+ return { ok: false, reason: "branch name contains a forbidden sequence (.. @{ //)" };
4944
+ }
4945
+ if (b.startsWith("/") || b.endsWith("/") || b.startsWith("-") || b.endsWith(".") || b === "@") {
4946
+ return { ok: false, reason: "branch name has a forbidden leading or trailing character" };
4947
+ }
4948
+ if (b.split("/").some((seg) => seg.startsWith(".") || seg.endsWith(".lock"))) {
4949
+ return {
4950
+ ok: false,
4951
+ reason: "branch name has a path segment starting with . or ending in .lock"
4952
+ };
4953
+ }
4954
+ return { ok: true };
4955
+ }
4956
+ function resolveSessionTargetBranch(input) {
4957
+ const dated = resolveTargetBranch({
4958
+ user: input.user,
4959
+ date: input.date,
4960
+ defaultBranch: input.defaultBranch
4961
+ });
4962
+ const declared = input.sessionBranch?.trim();
4963
+ if (!declared) return { branch: dated, source: "dated" };
4964
+ const check = isValidSessionBranch(declared, input.defaultBranch);
4965
+ if (!check.ok) return { branch: dated, source: "dated", reason: check.reason };
4966
+ return { branch: declared, source: "session" };
4967
+ }
4968
+ var PROTECTED_SESSION_BRANCHES;
4969
+ var init_branch_resolver = __esm({
4970
+ "spex-console/src/lib/git-export/branch-resolver.ts"() {
4971
+ "use strict";
4972
+ init_branch_policy();
4973
+ PROTECTED_SESSION_BRANCHES = ["main", "master", "HEAD", "trunk"];
4974
+ }
4975
+ });
4976
+
4798
4977
  // spex-console/src/lib/git-export/config.ts
4799
4978
  function parseGithubRemote(githubRemote) {
4800
4979
  const match = githubRemote.match(/^([^/\s]+)\/([^/\s]+)$/);
@@ -5197,6 +5376,39 @@ var init_github_client = __esm({
5197
5376
  }
5198
5377
  });
5199
5378
 
5379
+ // spex-console/src/lib/git-export/session-branch-store.ts
5380
+ async function readActiveDeclaration(db, vaultId, actorKey, now) {
5381
+ const rows = await db.select().from(sessionBranchDeclarations).where(
5382
+ and(
5383
+ eq(sessionBranchDeclarations.vaultId, vaultId),
5384
+ eq(sessionBranchDeclarations.actorKey, actorKey),
5385
+ gt(sessionBranchDeclarations.expiresAt, now)
5386
+ )
5387
+ ).orderBy(desc(sessionBranchDeclarations.declaredAt)).limit(1);
5388
+ const row = rows[0];
5389
+ if (!row) return null;
5390
+ return {
5391
+ vaultId: row.vaultId,
5392
+ actorKey: row.actorKey,
5393
+ sessionId: row.sessionId,
5394
+ branch: row.branch,
5395
+ declaredAt: row.declaredAt.toISOString(),
5396
+ expiresAt: row.expiresAt.toISOString()
5397
+ };
5398
+ }
5399
+ async function readActiveSessionBranch(db, vaultId, actorKey, now) {
5400
+ return (await readActiveDeclaration(db, vaultId, actorKey, now))?.branch ?? null;
5401
+ }
5402
+ var SESSION_BRANCH_TTL_MS;
5403
+ var init_session_branch_store = __esm({
5404
+ "spex-console/src/lib/git-export/session-branch-store.ts"() {
5405
+ "use strict";
5406
+ init_sql_orm_inert_cli_shim();
5407
+ init_postgres();
5408
+ SESSION_BRANCH_TTL_MS = 12 * 60 * 60 * 1e3;
5409
+ }
5410
+ });
5411
+
5200
5412
  // spex-console/src/lib/git-export/settlement-store.ts
5201
5413
  async function recordSettlement(tx, input) {
5202
5414
  await tx.insert(specSettlements).values({
@@ -5427,6 +5639,7 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
5427
5639
  result: { status: "error", message: target.message }
5428
5640
  };
5429
5641
  }
5642
+ const lookupSessionBranch = deps.lookupSessionBranch ?? ((vId, actor) => readActiveSessionBranch(tx, vId, actor, now()));
5430
5643
  const { config, tokenSource, installationId } = target;
5431
5644
  const provider = createPatCommitProvider(() => config.token);
5432
5645
  let pendingSettlement = null;
@@ -5440,11 +5653,18 @@ async function processVault(db, vaultId, env, now, deps, batchLimit, jobTimeoutM
5440
5653
  fetchBundles: (specIds) => fetchDrainBundles(tx, vaultId, specIds),
5441
5654
  resolveActorIdentity,
5442
5655
  commit: async (plan, message, coAuthor) => {
5443
- const targetBranch = resolveTargetBranch({
5656
+ let sessionBranch = null;
5657
+ try {
5658
+ sessionBranch = await lookupSessionBranch(vaultId, coAuthor.name);
5659
+ } catch {
5660
+ sessionBranch = null;
5661
+ }
5662
+ const targetBranch = resolveSessionTargetBranch({
5444
5663
  user: coAuthor.name,
5445
5664
  date: now(),
5446
- defaultBranch: config.branch
5447
- });
5665
+ defaultBranch: config.branch,
5666
+ sessionBranch
5667
+ }).branch;
5448
5668
  const outcome = await commitPlanToGitHub(
5449
5669
  provider,
5450
5670
  config,
@@ -5585,12 +5805,13 @@ var init_drain_runner = __esm({
5585
5805
  init_postgres();
5586
5806
  init_github_app();
5587
5807
  init_advisory_lock();
5588
- init_branch_policy();
5808
+ init_branch_resolver();
5589
5809
  init_config();
5590
5810
  init_drain();
5591
5811
  init_drain_skip();
5592
5812
  init_export_status();
5593
5813
  init_github_client();
5814
+ init_session_branch_store();
5594
5815
  init_settlement_store();
5595
5816
  BOT_NAME = "heyspex-bot";
5596
5817
  BOT_EMAIL = "bot@heyspex.app";
@@ -8154,7 +8375,7 @@ var init_plan_tier = __esm({
8154
8375
 
8155
8376
  // spex-console/src/lib/db/clients/vault-router.ts
8156
8377
  import { existsSync as existsSync4, readFileSync as readFileSync8 } from "node:fs";
8157
- import { homedir as homedir6 } from "node:os";
8378
+ import { homedir as homedir7 } from "node:os";
8158
8379
  import { join as join6 } from "node:path";
8159
8380
  function normalizeDatabaseRef(raw) {
8160
8381
  if (typeof raw !== "string") return null;
@@ -8287,7 +8508,7 @@ var init_vault_router = __esm({
8287
8508
  init_postgres_client_cli_shim();
8288
8509
  DEDICATED_DB_ENV_PREFIX = "HEYSPEX_DEDICATED_DB_";
8289
8510
  DEDICATED_REFS_ENV = "DEDICATED_DATABASE_REFS";
8290
- CREDENTIALS_PATH = join6(homedir6(), ".heyspex", "credentials.yaml");
8511
+ CREDENTIALS_PATH = join6(homedir7(), ".heyspex", "credentials.yaml");
8291
8512
  REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
8292
8513
  ROUTER_KEY = Symbol.for("heyspex.vaultRouter");
8293
8514
  globalRouter = globalThis;
@@ -6,12 +6,24 @@ import process3 from "node:process";
6
6
 
7
7
  // spex-console/src/lib/dev-mode.ts
8
8
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
9
- import { homedir as homedir2 } from "node:os";
10
9
  import { join as join2 } from "node:path";
11
10
 
11
+ // spex-console/src/lib/heyspex-home.ts
12
+ import { homedir } from "node:os";
13
+ function heyspexHome(env = process.env) {
14
+ const override = env?.HEYSPEX_HOME;
15
+ if (typeof override === "string" && override.trim().length > 0) {
16
+ return override.trim();
17
+ }
18
+ return homedir();
19
+ }
20
+
21
+ // scripts/lib/heyspex-home.mjs
22
+ import { homedir as homedir2 } from "node:os";
23
+
12
24
  // vendor/heyspex-mcp/credentials.mjs
13
25
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
14
- import os, { homedir } from "node:os";
26
+ import os, { homedir as homedir3 } from "node:os";
15
27
  import { dirname, join, resolve } from "node:path";
16
28
  function underTestRunner(env = process.env) {
17
29
  return Boolean(env.VITEST) || Boolean(env.NODE_TEST_CONTEXT);
@@ -74,12 +86,12 @@ function deriveSemantics(mode, reliance) {
74
86
  vaultWriteMode: local ? "local" : "cloud"
75
87
  };
76
88
  }
77
- function devModeFilePath(homeDir = homedir2()) {
89
+ function devModeFilePath(homeDir = heyspexHome()) {
78
90
  return join2(homeDir, ".heyspex", "dev-mode.json");
79
91
  }
80
92
  var defaultReadFile = (p) => readFileSync2(p, "utf8");
81
93
  function readLocalDevModeRecord({
82
- homeDir = homedir2(),
94
+ homeDir = heyspexHome(),
83
95
  readFile = defaultReadFile
84
96
  } = {}) {
85
97
  let raw;
@@ -114,7 +126,7 @@ function readLocalDevModeRecord({
114
126
  }
115
127
  function resolveDevMode({
116
128
  env = process.env,
117
- homeDir = homedir2(),
129
+ homeDir = heyspexHome(),
118
130
  readFile = defaultReadFile,
119
131
  cloudSetting = void 0
120
132
  } = {}) {
@@ -228,7 +240,7 @@ function upsertCredentialsYamlKeys(homeDir, updates, deps = {}) {
228
240
  return filePath;
229
241
  }
230
242
  function readCredentialsMcpToken({
231
- homeDir = homedir2(),
243
+ homeDir = heyspexHome(),
232
244
  readFile = defaultReadFile
233
245
  } = {}) {
234
246
  try {
@@ -265,7 +277,7 @@ async function discardResponseBody(res) {
265
277
  async function fetchCloudDevMode({
266
278
  consoleUrl,
267
279
  fetchImpl = fetch,
268
- homeDir = homedir2(),
280
+ homeDir = heyspexHome(),
269
281
  readFile = defaultReadFile,
270
282
  timeoutMs = 4e3
271
283
  }) {
@@ -305,7 +317,7 @@ var DevModeNotAdminError = class extends Error {
305
317
  };
306
318
  async function applyDevMode(targetMode, targetReliance, deps = {}) {
307
319
  const {
308
- homeDir = homedir2(),
320
+ homeDir = heyspexHome(),
309
321
  fetchImpl = fetch,
310
322
  source = "cli",
311
323
  now = () => (/* @__PURE__ */ new Date()).toISOString(),