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
@@ -39,11 +39,26 @@ var __toESM = (mod2, isNodeMode, target) => (target = mod2 != null ? __create(__
39
39
  mod2
40
40
  ));
41
41
 
42
+ // scripts/lib/heyspex-home.mjs
43
+ import { homedir as homedir2 } from "node:os";
44
+ function heyspexHome2(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 homedir2();
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 homedir3 } from "node:os";
45
60
  import { dirname, join, resolve } from "node:path";
46
- function credentialsFilePath(homeDir = homedir()) {
61
+ function credentialsFilePath(homeDir = heyspexHome2()) {
47
62
  return join(homeDir, ".heyspex", "credentials.yaml");
48
63
  }
49
64
  function parseMcpToken(raw) {
@@ -156,14 +171,14 @@ function readLegacyConfigAccountId(read2, homeDir) {
156
171
  const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
157
172
  return value || null;
158
173
  }
159
- function readBearer({ env = process.env, homeDir = homedir(), readFile: readFile3, accountId } = {}) {
174
+ function readBearer({ env = process.env, homeDir = heyspexHome2(), readFile: readFile3, accountId } = {}) {
160
175
  const read2 = readFile3 ?? ((p) => readFileSync(p, "utf8"));
161
176
  const credentialsText = readTextOrNull(read2, credentialsFilePath(homeDir));
162
177
  const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
163
178
  const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read2, homeDir) : null;
164
179
  return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
165
180
  }
166
- function readCurrentAccountId({ homeDir = homedir(), readFile: readFile3 } = {}) {
181
+ function readCurrentAccountId({ homeDir = heyspexHome2(), readFile: readFile3 } = {}) {
167
182
  const read2 = readFile3 ?? ((p) => readFileSync(p, "utf8"));
168
183
  const store = parseCredentialsStore(readTextOrNull(read2, credentialsFilePath(homeDir)));
169
184
  if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
@@ -181,6 +196,7 @@ var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT
181
196
  var init_credentials = __esm({
182
197
  "vendor/heyspex-mcp/credentials.mjs"() {
183
198
  "use strict";
199
+ init_heyspex_home();
184
200
  MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
185
201
  MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
186
202
  CREDENTIALS_SCHEMA_VERSION = 2;
@@ -193,9 +209,9 @@ var init_credentials = __esm({
193
209
 
194
210
  // scripts/lib/dev-mode.mjs
195
211
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
196
- import { homedir as homedir4 } from "node:os";
212
+ import { homedir as homedir5 } from "node:os";
197
213
  import { join as join4 } from "node:path";
198
- function devModeFilePath(homeDir = homedir4()) {
214
+ function devModeFilePath(homeDir = heyspexHome2()) {
199
215
  return join4(homeDir, ".heyspex", DEV_MODE_FILENAME);
200
216
  }
201
217
  function normalizeMode(raw) {
@@ -220,7 +236,7 @@ function deriveSemantics(mode, reliance) {
220
236
  vaultWriteMode: local ? "local" : "cloud"
221
237
  };
222
238
  }
223
- function readLocalDevModeRecord({ homeDir = homedir4(), readFile: readFile3 = defaultReadFile } = {}) {
239
+ function readLocalDevModeRecord({ homeDir = heyspexHome2(), readFile: readFile3 = defaultReadFile } = {}) {
224
240
  let raw;
225
241
  try {
226
242
  raw = readFile3(devModeFilePath(homeDir));
@@ -250,7 +266,7 @@ function readLocalDevModeRecord({ homeDir = homedir4(), readFile: readFile3 = de
250
266
  }
251
267
  function resolveDevMode({
252
268
  env = process.env,
253
- homeDir = homedir4(),
269
+ homeDir = heyspexHome2(),
254
270
  readFile: readFile3 = defaultReadFile,
255
271
  cloudSetting = void 0
256
272
  } = {}) {
@@ -313,6 +329,7 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
313
329
  var init_dev_mode = __esm({
314
330
  "scripts/lib/dev-mode.mjs"() {
315
331
  "use strict";
332
+ init_heyspex_home();
316
333
  init_credentials();
317
334
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
318
335
  LOCAL_CONSOLE_URL = "http://localhost:3000";
@@ -22722,14 +22739,14 @@ var require_lib2 = __commonJS({
22722
22739
 
22723
22740
  // spex-console/bin/hs-join.ts
22724
22741
  import { spawn, spawnSync as spawnSync5 } from "node:child_process";
22725
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "node:fs";
22742
+ import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "node:fs";
22726
22743
  import { userInfo } from "node:os";
22727
- import { basename as basename3, dirname as dirname6, join as join15, resolve as resolve5 } from "node:path";
22744
+ import { basename as basename4, dirname as dirname6, join as join16, resolve as resolve5 } from "node:path";
22728
22745
  import process4 from "node:process";
22729
22746
  import { createInterface as createInterface2 } from "node:readline/promises";
22730
22747
 
22731
22748
  // spex-console/src/lib/cli/connect.ts
22732
- import { readdirSync } from "node:fs";
22749
+ import { readdirSync as readdirSync2 } from "node:fs";
22733
22750
 
22734
22751
  // dist/cli/._sql-orm-inert.cli-shim.mjs
22735
22752
  var inert = new Proxy(function inert2() {
@@ -22767,14 +22784,24 @@ var uniqueIndex = inert;
22767
22784
 
22768
22785
  // spex-console/src/lib/cli/vault-workspace-shared.ts
22769
22786
  import { readFileSync as readFileSync3 } from "node:fs";
22770
- import os2 from "node:os";
22787
+
22788
+ // spex-console/src/lib/heyspex-home.ts
22789
+ import { homedir } from "node:os";
22790
+ function heyspexHome(env = process.env) {
22791
+ const override = env?.HEYSPEX_HOME;
22792
+ if (typeof override === "string" && override.trim().length > 0) {
22793
+ return override.trim();
22794
+ }
22795
+ return homedir();
22796
+ }
22771
22797
 
22772
22798
  // vendor/heyspex-mcp/session-owner.mjs
22799
+ init_heyspex_home();
22773
22800
  init_credentials();
22774
22801
  import { readFileSync as readFileSync2 } from "node:fs";
22775
- import { homedir as homedir2 } from "node:os";
22802
+ import { homedir as homedir4 } from "node:os";
22776
22803
  import { join as join2 } from "node:path";
22777
- function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir2(), readFile: readFile3 } = {}) {
22804
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome2(), readFile: readFile3 } = {}) {
22778
22805
  const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
22779
22806
  if (fromEnv) return fromEnv;
22780
22807
  return readCurrentAccountId({ homeDir, readFile: readFile3 });
@@ -22803,7 +22830,7 @@ function resolveOfflineDefaultWorkspace(accountId, localWorkspaceIds) {
22803
22830
  function resolveConfiguredAccountId(opts = {}) {
22804
22831
  return resolveSessionOwnerUncached({
22805
22832
  env: opts.env ?? process.env,
22806
- homeDir: opts.homeDir ?? os2.homedir(),
22833
+ homeDir: opts.homeDir ?? heyspexHome(),
22807
22834
  readFile: opts.readFile ?? ((p) => readFileSync3(p, "utf-8"))
22808
22835
  });
22809
22836
  }
@@ -23367,7 +23394,13 @@ var specEventKindEnum = pgEnum("spec_event_kind", [
23367
23394
  "priority_change",
23368
23395
  "parent_change",
23369
23396
  "claim_acquired",
23370
- "claim_released"
23397
+ "claim_released",
23398
+ // Collaborator write audit (HS-FWB6, migration 0077).
23399
+ "spec_created",
23400
+ "spec_edited",
23401
+ "spec_deleted",
23402
+ "capture_written",
23403
+ "wip_recorded"
23371
23404
  ]);
23372
23405
  var focusPin = pgTable(
23373
23406
  "focus_pin",
@@ -24560,11 +24593,43 @@ var specSettlements = pgTable(
24560
24593
  // `owner/name`
24561
24594
  branch: text("branch").notNull(),
24562
24595
  commitSha: text("commit_sha").notNull(),
24563
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
24596
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
24597
+ // The pull request that carries this settlement
24598
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
24599
+ // the branch has gone quiet and a PR was opened or found; a write made
24600
+ // after its PR merged stays NULL, which is what triggers the next PR.
24601
+ pullRequestNumber: integer("pull_request_number"),
24602
+ pullRequestUrl: text("pull_request_url")
24564
24603
  },
24565
24604
  (table) => [
24566
24605
  // "latest settlement for this spec" — the read path's only query.
24567
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
24606
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
24607
+ // The PR sweep's only query: branches with settlements not yet in a PR.
24608
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
24609
+ ]
24610
+ );
24611
+ var sessionBranchDeclarations = pgTable(
24612
+ "session_branch_declarations",
24613
+ {
24614
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
24615
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
24616
+ actorKey: text("actor_key").notNull(),
24617
+ sessionId: text("session_id").notNull(),
24618
+ branch: text("branch").notNull(),
24619
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
24620
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
24621
+ },
24622
+ (table) => [
24623
+ uniqueIndex("session_branch_declarations_uq").on(
24624
+ table.vaultId,
24625
+ table.actorKey,
24626
+ table.sessionId
24627
+ ),
24628
+ index("session_branch_declarations_lookup_idx").on(
24629
+ table.vaultId,
24630
+ table.actorKey,
24631
+ table.declaredAt.desc()
24632
+ )
24568
24633
  ]
24569
24634
  );
24570
24635
 
@@ -24657,7 +24722,6 @@ async function runNeonReachabilityProbe(attempt, options2 = {}, label = "Neon re
24657
24722
 
24658
24723
  // spex-console/src/lib/vault-registry/registry-file.ts
24659
24724
  import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
24660
- import { homedir as homedir3 } from "node:os";
24661
24725
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
24662
24726
 
24663
24727
  // spex-console/src/lib/vault-registry/types.ts
@@ -24675,7 +24739,7 @@ function emptyVaultRegistry() {
24675
24739
 
24676
24740
  // spex-console/src/lib/vault-registry/registry-file.ts
24677
24741
  function vaultRegistryDir(opts = {}) {
24678
- return opts.registryDir ?? join3(homedir3(), ".heyspex");
24742
+ return opts.registryDir ?? join3(heyspexHome(), ".heyspex");
24679
24743
  }
24680
24744
  function vaultRegistryFilePath(opts = {}) {
24681
24745
  return join3(vaultRegistryDir(opts), "registry.json");
@@ -24845,7 +24909,7 @@ function samePathLoose(a, b) {
24845
24909
  }
24846
24910
  function assertNotRealHomeRegistryUnderTest(filePath) {
24847
24911
  if (!underTestRunner()) return;
24848
- if (samePathLoose(dirname2(filePath), join3(homedir3(), ".heyspex"))) {
24912
+ if (samePathLoose(dirname2(filePath), join3(heyspexHome(), ".heyspex"))) {
24849
24913
  throw new Error(
24850
24914
  `registry-file: refusing to write ${filePath} under a test runner \u2014 that is the real ~/.heyspex registry. Pass opts.registryDir (a temp dir).`
24851
24915
  );
@@ -25312,12 +25376,17 @@ async function listWorkspacesNeon(accountId, deps = {}) {
25312
25376
  id: workspaces.id,
25313
25377
  displayName: workspaces.displayName,
25314
25378
  githubOrg: workspaces.githubOrg,
25315
- createdAt: workspaces.createdAt
25379
+ createdAt: workspaces.createdAt,
25380
+ archived: workspaces.archived
25316
25381
  }).from(workspaces).where(eq(workspaces.accountId, accountId));
25317
25382
  return rows.map((r) => ({
25318
25383
  id: r.id,
25319
25384
  display_name: r.displayName,
25320
25385
  created_at: (r.createdAt instanceof Date ? r.createdAt : /* @__PURE__ */ new Date()).toISOString(),
25386
+ // The flag `setWorkspaceArchivedNeon` writes; `list.ts` filters on it.
25387
+ // On a cloud host this row IS the roster (no registry.json to carry the
25388
+ // flag), so leaving it out made an archived workspace list forever.
25389
+ archived: r.archived,
25321
25390
  ...r.githubOrg ? { github_org: r.githubOrg } : {}
25322
25391
  }));
25323
25392
  } catch {
@@ -25601,9 +25670,10 @@ async function createWorkspace(input, deps = {}) {
25601
25670
  }
25602
25671
 
25603
25672
  // scripts/lib/console-url-resolver.mjs
25673
+ init_heyspex_home();
25604
25674
  init_dev_mode();
25605
25675
  import { readFileSync as readFileSync6 } from "node:fs";
25606
- import { homedir as homedir5 } from "node:os";
25676
+ import { homedir as homedir6 } from "node:os";
25607
25677
  import { join as join5 } from "node:path";
25608
25678
  var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
25609
25679
  function trimTrailingSlash(url) {
@@ -25630,7 +25700,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
25630
25700
  } catch {
25631
25701
  }
25632
25702
  }
25633
- function readCredentialsConsoleUrl({ homeDir = homedir5(), readFile: readFile3 = defaultReadFile2 } = {}) {
25703
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome2(), readFile: readFile3 = defaultReadFile2 } = {}) {
25634
25704
  try {
25635
25705
  const raw = readFile3(join5(homeDir, ".heyspex", "credentials.yaml"));
25636
25706
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
@@ -25674,7 +25744,7 @@ function resolveModeEnvelope({ env, homeDir }) {
25674
25744
  function resolveConsoleUrl({
25675
25745
  repoRoot,
25676
25746
  env = process.env,
25677
- homeDir = homedir5(),
25747
+ homeDir = heyspexHome2(),
25678
25748
  strict = false,
25679
25749
  devMode,
25680
25750
  log = (line) => process.stderr.write(line),
@@ -25708,7 +25778,134 @@ function resolveConsoleUrl({
25708
25778
  import { createHash } from "node:crypto";
25709
25779
  import fs from "node:fs";
25710
25780
  import path2 from "node:path";
25781
+ import os2 from "node:os";
25711
25782
  import { fileURLToPath, pathToFileURL } from "node:url";
25783
+
25784
+ // scripts/lib/hook-dispositions.mjs
25785
+ var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
25786
+ var DISPOSITIONS = Object.freeze(["mapped", "advisory-degraded", "unportable", "not-shipped"]);
25787
+ var mapped = (note) => Object.freeze({ disposition: "mapped", ...note ? { note } : {} });
25788
+ var degraded = (note) => Object.freeze({ disposition: "advisory-degraded", note });
25789
+ var unportable = (control) => Object.freeze({ disposition: "unportable", control });
25790
+ var notShipped = Object.freeze({ disposition: "not-shipped" });
25791
+ var CC = mapped();
25792
+ var COPILOT_PROMPT_DROPPED = "Copilot drops command-hook output on userPromptSubmitted (no prompt-time context channel); the text moves to sessionStart / .github/copilot-instructions.md.";
25793
+ var CURSOR_PROMPT_USER_MESSAGE = "Cursor beforeSubmitPrompt honors user_message only (no additional_context): the text surfaces in the transcript instead of as invisible context.";
25794
+ var COPILOT_NO_PLAN_TOOL = "MCP preflight refusals (heyspex_claim_spec / heyspex_submit_for_review / heyspex_save_research) + .github/copilot-instructions.md plan rule (state the plan in chat, claim the spec before editing) + HS-20SW directive-obligation-gate claim-before-build deny";
25795
+ var HOOK_DISPOSITIONS = Object.freeze({
25796
+ // ── customer: session start / compaction ──────────────────────────────────
25797
+ "wip-checkpoint": {
25798
+ "claude-code": CC,
25799
+ cursor: mapped("preCompact is relayed as SessionStart(compact)."),
25800
+ copilot: mapped("sessionStart additionalContext is delivered; the preCompact leg writes WIP only (no output channel), re-injected at the next sessionStart.")
25801
+ },
25802
+ "inject-doctrine": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
25803
+ "check-auto-memory-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
25804
+ "check-claude-md-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
25805
+ "git-reflog-audit": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext and agentStop.") },
25806
+ "vault-mirror-catchup": { "claude-code": CC, cursor: mapped(), copilot: mapped("Detached side effect on sessionStart.") },
25807
+ // ── customer: every user turn ─────────────────────────────────────────────
25808
+ "preflight-reminder": {
25809
+ "claude-code": CC,
25810
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
25811
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Real enforcement is the MCP preflight refusal.`)
25812
+ },
25813
+ "intent-classifier-trigger": {
25814
+ "claude-code": CC,
25815
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
25816
+ copilot: degraded(
25817
+ `${COPILOT_PROMPT_DROPPED} The hook still runs and arms obligation state; the HS-20SW directive-obligation-gate turns that state into a hard deny.`
25818
+ )
25819
+ },
25820
+ "inject-communication-style": {
25821
+ "claude-code": CC,
25822
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
25823
+ copilot: degraded(COPILOT_PROMPT_DROPPED)
25824
+ },
25825
+ "inject-vault-context": {
25826
+ "claude-code": CC,
25827
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
25828
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Per-prompt vault retrieval has no Copilot channel at all.`)
25829
+ },
25830
+ // ── customer: before a tool call (hard gates) ─────────────────────────────
25831
+ "git-identity-guard": { "claude-code": CC, cursor: mapped("beforeShellExecution. Always on."), copilot: mapped("preToolUse on bash/powershell. Always on.") },
25832
+ "vault-write-tier-guard": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell.") },
25833
+ "settings-write-gate": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell and the MCP settings tool.") },
25834
+ "emit-claim-status": { "claude-code": CC, cursor: mapped(), copilot: mapped("Telemetry on preToolUse/postToolUse/agentStop; the cloud agent needs a network allow rule for the console.") },
25835
+ "enforce-vault-naming": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
25836
+ "guard-auto-memory-writes": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
25837
+ "directive-obligation-gate": {
25838
+ "claude-code": CC,
25839
+ cursor: mapped(),
25840
+ copilot: mapped("HS-20SW: hard deny on task/create/edit/write-shaped shell while a capture obligation is unmet; the only hard capture-on-idea guarantee on Copilot.")
25841
+ },
25842
+ "claim-before-code-gate": {
25843
+ "claude-code": CC,
25844
+ cursor: degraded("Armed only by plan-exit-ingest-gate, which has no Cursor trigger, so it stays dormant; covered by the HS-20SW claim-before-build rule."),
25845
+ copilot: degraded("Armed only by plan-exit-ingest-gate, which has no Copilot trigger (no ExitPlanMode tool), so it stays dormant; covered by the HS-20SW claim-before-build rule.")
25846
+ },
25847
+ "capture-workflow-gate": {
25848
+ "claude-code": CC,
25849
+ cursor: mapped(),
25850
+ copilot: mapped("preToolUse on the MCP heyspex_file_spec tool; MCP tool naming in hook payloads is unverified live (matched by suffix).")
25851
+ },
25852
+ "diagnose-capture-reminder": {
25853
+ "claude-code": CC,
25854
+ cursor: mapped("preToolUse agent_message delivers the reminder."),
25855
+ copilot: degraded("Copilot preToolUse has no context channel for an inject, so this advisory is silent (lost).")
25856
+ },
25857
+ "git-stash-gate": {
25858
+ "claude-code": CC,
25859
+ cursor: mapped(),
25860
+ copilot: mapped('The Claude "ask" becomes a deny where no user is present (cloud agent, CLI autopilot).')
25861
+ },
25862
+ "research-protocol-gate": { "claude-code": CC, cursor: mapped("Always on."), copilot: mapped("preToolUse on web_fetch/web_search and the MCP save_research tool. Always on; the MCP refusal duplicates it.") },
25863
+ "research-method-guard": {
25864
+ "claude-code": CC,
25865
+ cursor: mapped("preToolUse with a Skill|Workflow tool filter. Always on."),
25866
+ copilot: unportable("MCP preflight refusal on heyspex_save_research (hard floor, hook-independent) + .github/copilot-instructions.md research rule; Copilot has no Skill or Workflow tool to hook.")
25867
+ },
25868
+ "deep-research-cost-gate": {
25869
+ "claude-code": CC,
25870
+ cursor: mapped("preToolUse with a Workflow|Skill tool filter."),
25871
+ copilot: unportable("MCP preflight refusal on heyspex_save_research + .github/copilot-instructions.md cost-confirmation rule; Copilot has no Skill or Workflow tool to hook.")
25872
+ },
25873
+ "plan-review-gate": {
25874
+ "claude-code": CC,
25875
+ cursor: unportable(".cursor/rules/plan-review-required.mdc (always-on rule)"),
25876
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
25877
+ },
25878
+ "plan-exit-ingest-gate": {
25879
+ "claude-code": CC,
25880
+ cursor: unportable(".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"),
25881
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
25882
+ },
25883
+ // ── customer: end of turn / session ───────────────────────────────────────
25884
+ "naughty-list-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped('agentStop {decision:"block"} re-drive, capped at 8 consecutive blocks.') },
25885
+ "id-display-format-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
25886
+ "epic-milestone-phrasing-gate": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive; reads the transcript (format is not a stable API).") },
25887
+ "enforce-session-sequence": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive, capped at 8 consecutive blocks.") },
25888
+ "notify-status-flip": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
25889
+ "close-terminal-on-ship": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse / sessionEnd.") },
25890
+ "scripts/stamp-provenance-on-edit.mjs": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
25891
+ "session-end": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionEnd.") },
25892
+ // ── platform: heyspex-app development machinery (never installed in a customer repo) ──
25893
+ "inject-dev-server-awareness": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
25894
+ "inject-coo-seat": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
25895
+ "coo-guard-check": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
25896
+ "coo-denylist-gate": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
25897
+ "agent-spawn-delegation-gate": { "claude-code": CC, cursor: mapped("subagentStart."), copilot: notShipped },
25898
+ "capture-execution-gate": { "claude-code": CC, cursor: mapped("beforeShellExecution + subagentStart."), copilot: notShipped },
25899
+ "check-mcp-freshness-hook": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
25900
+ "dev-server-task-reminder": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
25901
+ "env-vault-guard": { "claude-code": CC, cursor: mapped("Always on."), copilot: notShipped },
25902
+ "log-session-activity": { "claude-code": CC, cursor: mapped(), copilot: notShipped }
25903
+ });
25904
+ function dispositionFor(hook, host) {
25905
+ return HOOK_DISPOSITIONS[hook]?.[host] ?? null;
25906
+ }
25907
+
25908
+ // scripts/lib/customer-kit.mjs
25712
25909
  var HOOK_SCOPES = Object.freeze({
25713
25910
  // ── customer: vault / workflow / safety gates ─────────────────────────────
25714
25911
  "wip-checkpoint": { scope: "customer", why: "Previous-session WIP handoff, read from the active vault." },
@@ -25724,6 +25921,7 @@ var HOOK_SCOPES = Object.freeze({
25724
25921
  "inject-communication-style": { scope: "customer", why: "Plain-language communication rules each turn." },
25725
25922
  "inject-vault-context": { scope: "customer", why: "Lexical vault retrieval for the prompt." },
25726
25923
  "git-identity-guard": { scope: "customer", why: "Agents never write git identity (owner ruling 2026-09-19)." },
25924
+ "vault-write-tier-guard": { scope: "customer", why: "Cloud write mode: disk edits to vault specs are a mirror the console never reads back (HS-FCNH)." },
25727
25925
  "settings-write-gate": { scope: "customer", why: "Approval before the vault's .heyspex/config.yaml changes." },
25728
25926
  "emit-claim-status": { scope: "customer", why: "Claim liveness heartbeat to the console." },
25729
25927
  "enforce-vault-naming": { scope: "customer", why: "Naming V3 filename guard (HS-RY2N)." },
@@ -25731,7 +25929,10 @@ var HOOK_SCOPES = Object.freeze({
25731
25929
  "plan-review-gate": { scope: "customer", why: "Plan review + QC markers before ExitPlanMode." },
25732
25930
  "plan-exit-ingest-gate": { scope: "customer", why: "Plan ingest on ExitPlanMode; arms claim-before-code." },
25733
25931
  "claim-before-code-gate": { scope: "customer", why: "First code edit after plan exit needs a claim." },
25932
+ "directive-obligation-gate": { scope: "customer", why: "A capture_on_idea directive leaves a durable obligation; an unmet one denies sub-agent spawns, product-code writes and builds, and re-drives Stop once. Also claim-before-build (HS-20SW)." },
25734
25933
  "capture-workflow-gate": { scope: "customer", why: "No implementation spec straight out of an open capture." },
25934
+ "vault-mirror-catchup": { scope: "customer", why: "Cloud write mode: a SessionStart detached `hs pull --apply` closes a stale disk mirror before the first tool call (HS-XJFQ)." },
25935
+ "diagnose-capture-reminder": { scope: "customer", why: "Warn when a diagnose is filed with no capture holding the owner's words (HS-IK2D)." },
25735
25936
  "git-stash-gate": { scope: "customer", why: "Always ask before a mutating git stash." },
25736
25937
  "research-method-guard": { scope: "customer", why: "Research method guard (HS-EN4F)." },
25737
25938
  "deep-research-cost-gate": { scope: "customer", why: "Cost confirmation before a paid deep-research launch." },
@@ -25750,6 +25951,7 @@ var HOOK_SCOPES = Object.freeze({
25750
25951
  "coo-guard-check": { scope: "platform", why: "dev-factory breakdown-delegation guard." },
25751
25952
  "coo-denylist-gate": { scope: "platform", why: "dev-factory COO window deny list." },
25752
25953
  "agent-spawn-delegation-gate": { scope: "platform", why: "dev-factory seat routing for subagents." },
25954
+ "capture-execution-gate": { scope: "platform", why: "Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8)." },
25753
25955
  "check-mcp-freshness-hook": { scope: "platform", why: "Freshness of the vendored MCP build \u2014 a platform-dev concern." },
25754
25956
  "dev-server-task-reminder": { scope: "platform", why: "spex-console dev server + Neon DB safety." },
25755
25957
  "env-vault-guard": { scope: "platform", why: "Guards heyspex-app's encrypted env vault paths." },
@@ -25768,6 +25970,7 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
25768
25970
  { event: "SessionStart", hook: "check-claude-md-budget", timeout: 5 },
25769
25971
  { event: "SessionStart", hook: "check-mcp-freshness-hook", timeout: 5 },
25770
25972
  { event: "SessionStart", hook: "git-reflog-audit", timeout: 5 },
25973
+ { event: "SessionStart", hook: "vault-mirror-catchup", timeout: 5 },
25771
25974
  { event: "PreCompact", hook: "wip-checkpoint", timeout: 5 },
25772
25975
  { event: "UserPromptSubmit", hook: "preflight-reminder", timeout: 5 },
25773
25976
  { event: "UserPromptSubmit", hook: "intent-classifier-trigger", timeout: 5 },
@@ -25775,10 +25978,13 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
25775
25978
  { event: "UserPromptSubmit", hook: "inject-vault-context", timeout: 10 },
25776
25979
  { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "env-vault-guard", timeout: 5 },
25777
25980
  { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-identity-guard", timeout: 5 },
25981
+ { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "vault-write-tier-guard", timeout: 5 },
25778
25982
  { event: "PreToolUse", matcher: "Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set", hook: "settings-write-gate", timeout: 5 },
25779
25983
  { event: "PreToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], timeout: 5 },
25780
25984
  { event: "PreToolUse", matcher: ".*", hook: "coo-denylist-gate", timeout: 5 },
25781
25985
  { event: "PreToolUse", matcher: "Agent|Task", hook: "agent-spawn-delegation-gate", timeout: 5 },
25986
+ { event: "PreToolUse", matcher: "Bash|PowerShell|Agent|Task", hook: "capture-execution-gate", timeout: 5 },
25987
+ { event: "PreToolUse", matcher: "Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "directive-obligation-gate", timeout: 5 },
25782
25988
  { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_claim_spec", hook: "coo-guard-check", timeout: 5 },
25783
25989
  { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "enforce-vault-naming", timeout: 5 },
25784
25990
  { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "guard-auto-memory-writes", timeout: 5 },
@@ -25787,6 +25993,7 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
25787
25993
  { event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-exit-ingest-gate", timeout: 5 },
25788
25994
  { event: "PreToolUse", matcher: "Edit|MultiEdit|Write", hook: "claim-before-code-gate", timeout: 5 },
25789
25995
  { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "capture-workflow-gate", timeout: 8 },
25996
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "diagnose-capture-reminder", timeout: 5 },
25790
25997
  { event: "PreToolUse", matcher: "Bash|PowerShell|mcp__neon__prepare_database_migration|mcp__neon__complete_database_migration|mcp__neon__reset_from_parent|mcp__neon__delete_branch|mcp__Neon__prepare_database_migration|mcp__Neon__complete_database_migration|mcp__Neon__reset_from_parent|mcp__Neon__delete_branch", hook: "dev-server-task-reminder", timeout: 10 },
25791
25998
  { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-stash-gate", timeout: 10 },
25792
25999
  { event: "PreToolUse", matcher: "Skill|Workflow", hook: "research-method-guard", timeout: 5 },
@@ -25799,7 +26006,9 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
25799
26006
  { event: "Stop", hook: "epic-milestone-phrasing-gate", timeout: 5 },
25800
26007
  { event: "Stop", hook: "git-reflog-audit", timeout: 5 },
25801
26008
  { event: "Stop", hook: "enforce-session-sequence", timeout: 5 },
26009
+ { event: "Stop", hook: "directive-obligation-gate", timeout: 5 },
25802
26010
  { event: "PostToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], timeout: 5 },
26011
+ { event: "PostToolUse", matcher: "Skill|mcp__heyspex__heyspex_update_capture|mcp__heyspex__heyspex_promote_capture.*|mcp__heyspex__heyspex_file_spec|mcp__heyspex__heyspex_claim_spec|mcp__heyspex__heyspex_release_spec|mcp__heyspex__heyspex_defer_spec", hook: "directive-obligation-gate", timeout: 5 },
25803
26012
  { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "notify-status-flip", timeout: 5 },
25804
26013
  { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "close-terminal-on-ship", timeout: 5 },
25805
26014
  { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "scripts/stamp-provenance-on-edit.mjs", timeout: 5 },
@@ -25884,9 +26093,22 @@ var CUSTOMER_LAUNCHERS = Object.freeze({
25884
26093
  });
25885
26094
  function customerRunnerPrefix(_platformRoot, surface = "claude") {
25886
26095
  const l = CUSTOMER_LAUNCHERS[surface];
26096
+ if (surface === "claude") return CUSTOMER_RUNNER_PREFIX;
25887
26097
  return `node "$${l.projectDirEnv}/${l.rel.join("/")}"`;
25888
26098
  }
25889
- var PLUGIN_RUNNER_PREFIX = 'node "${CLAUDE_PLUGIN_ROOT}/bin/run-hook.mjs"';
26099
+ var BOOTSTRAP_WALK = "var d=process.cwd(),r=e.CLAUDE_PROJECT_DIR;if(!r||!f.existsSync(p.join(r,...n))){r='';for(;;){if(f.existsSync(p.join(d,...n))){r=d;break}var q=p.dirname(d);if(q===d)break;d=q}}";
26100
+ function buildBootstrap({ plugin = false } = {}) {
26101
+ const walk = BOOTSTRAP_WALK;
26102
+ const body = plugin ? "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='',m=e.CLAUDE_PLUGIN_ROOT||e.COPILOT_PLUGIN_ROOT||e.PLUGIN_ROOT;if(m&&f.existsSync(p.join(m,'bin','run-hook.mjs')))s=p.join(m,'bin','run-hook.mjs');if(!s){" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}}" : "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=['scripts','claude-hooks','_run.mjs'],s='';" + walk + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}";
26103
+ return body + "if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});" + (plugin ? "//bin/run-hook.mjs" : "//scripts/claude-hooks/_run.mjs");
26104
+ }
26105
+ var PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
26106
+ var PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
26107
+ function buildCustomerBootstrap() {
26108
+ const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(",");
26109
+ return "var f=require('fs'),p=require('path'),u=require('url'),e=process.env,n=[" + rel + "],s='';" + BOOTSTRAP_WALK + "if(r){e.CLAUDE_PROJECT_DIR=r;s=p.join(r,...n)}if(!s)process.exit(0);process.argv=[process.argv[0],s].concat(process.argv.slice(1));import(u.pathToFileURL(s).href).catch(function(){process.exit(0)});//" + CUSTOMER_LAUNCHERS.claude.rel.join("/");
26110
+ }
26111
+ var CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
25890
26112
  var LAUNCHER_TEMPLATE = [
25891
26113
  "#!/usr/bin/env node",
25892
26114
  "// HeySpex hook launcher -- GENERATED by hs install / hs connect. Commit it with this repo.",
@@ -25964,7 +26186,12 @@ var LAUNCHER_TEMPLATE = [
25964
26186
  "// (the Cursor one) runs exactly as if Cursor had invoked it directly.",
25965
26187
  "const shim = resolve(root, ...SHIM);",
25966
26188
  "process.argv[1] = shim;",
25967
- "await import(pathToFileURL(shim).href);",
26189
+ "try {",
26190
+ " await import(pathToFileURL(shim).href);",
26191
+ "} catch (err) {",
26192
+ " process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
26193
+ " process.exit(0);",
26194
+ "}",
25968
26195
  ""
25969
26196
  ].join("\n");
25970
26197
  function renderCustomerLauncher(surface = "claude") {
@@ -26071,7 +26298,6 @@ function renderPluginHooksDocument() {
26071
26298
  });
26072
26299
  }
26073
26300
  return {
26074
- _comment: "GENERATED from scripts/lib/customer-kit.mjs (customer-scoped rows only) \u2014 do not edit by hand; run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`. Every command goes through bin/run-hook.mjs, which finds the hook runtime (HEYSPEX_PLATFORM_ROOT / HEYSPEX_MCP_ENTRY when set in the machine's own env, then the plugin's own location, node_modules/heyspex, $CLAUDE_PROJECT_DIR) \u2014 never $CLAUDE_PROJECT_DIR/scripts, which only exists in heyspex-app. WARNING: a repo whose .claude/settings.json already carries the hs install kit hooks runs every hook twice if this plugin is also enabled \u2014 use one or the other.",
26075
26301
  hooks
26076
26302
  };
26077
26303
  }
@@ -26079,6 +26305,75 @@ function renderPluginHooksText() {
26079
26305
  return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}
26080
26306
  `;
26081
26307
  }
26308
+ var COPILOT_EVENT_NAMES = Object.freeze({
26309
+ SessionStart: "sessionStart",
26310
+ SessionEnd: "sessionEnd",
26311
+ UserPromptSubmit: "userPromptSubmitted",
26312
+ PreToolUse: "preToolUse",
26313
+ PostToolUse: "postToolUse",
26314
+ Stop: "agentStop",
26315
+ PreCompact: "preCompact"
26316
+ });
26317
+ var GITHUB_HOOKS_STATE_ARMING = Object.freeze(["intent-classifier-trigger"]);
26318
+ var COPILOT_TOOL_NAMES = Object.freeze({
26319
+ Write: "create",
26320
+ Edit: "edit",
26321
+ MultiEdit: "edit",
26322
+ NotebookEdit: "edit",
26323
+ Bash: "bash",
26324
+ PowerShell: "powershell",
26325
+ Agent: "task",
26326
+ Task: "task",
26327
+ WebFetch: "web_fetch",
26328
+ WebSearch: "web_search"
26329
+ });
26330
+ function toCopilotMatcher(matcher) {
26331
+ if (!matcher) return void 0;
26332
+ const out = [];
26333
+ for (const part of matcher.split("|")) {
26334
+ let t = null;
26335
+ if (part === ".*") t = ".*";
26336
+ else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
26337
+ else if (part.startsWith(MCP_PROJECT_PREFIX)) t = ".*" + part.slice(MCP_PROJECT_PREFIX.length);
26338
+ else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = ".*" + part.slice(MCP_PLUGIN_PREFIX.length);
26339
+ if (t && !out.includes(t)) out.push(t);
26340
+ }
26341
+ if (out.includes(".*")) return ".*";
26342
+ return out.length ? out.join("|") : null;
26343
+ }
26344
+ function githubHookNames() {
26345
+ const names = /* @__PURE__ */ new Set();
26346
+ for (const r of customerHookRows()) {
26347
+ if (dispositionFor(r.hook, "copilot")?.disposition === "mapped" || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
26348
+ }
26349
+ return names;
26350
+ }
26351
+ function githubHookCommand(row2) {
26352
+ const tail = hookTail(row2);
26353
+ return `R=node_modules/heyspex/scripts/claude-hooks/_run.mjs; [ -f "$R" ] || R=./scripts/claude-hooks/_run.mjs; [ -f "$R" ] || exit 0; exec node "$R" ${tail}`;
26354
+ }
26355
+ function renderGithubHooksDocument() {
26356
+ const allowed = githubHookNames();
26357
+ const hooks = {};
26358
+ for (const row2 of customerHookRows()) {
26359
+ if (!allowed.has(row2.hook)) continue;
26360
+ const event = COPILOT_EVENT_NAMES[row2.event];
26361
+ if (!event) continue;
26362
+ const matcher = toCopilotMatcher(row2.matcher);
26363
+ if (matcher === null) continue;
26364
+ (hooks[event] ??= []).push({
26365
+ type: "command",
26366
+ ...matcher ? { matcher } : {},
26367
+ bash: githubHookCommand(row2),
26368
+ timeoutSec: row2.timeout
26369
+ });
26370
+ }
26371
+ return { version: 1, hooks };
26372
+ }
26373
+ function renderGithubHooksText() {
26374
+ return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}
26375
+ `;
26376
+ }
26082
26377
  var HARNESS_COMMAND_SURFACES = Object.freeze([
26083
26378
  {
26084
26379
  installedDir: Object.freeze([".claude", "commands"]),
@@ -26211,21 +26506,35 @@ if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
26211
26506
  process.exit(1);
26212
26507
  }
26213
26508
  console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
26509
+ } else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
26510
+ const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
26511
+ const ghWant = renderGithubHooksText();
26512
+ const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
26513
+ if (process.argv.includes("--write-github-hooks")) {
26514
+ if (!ghSame) {
26515
+ fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
26516
+ fs.writeFileSync(ghTarget, ghWant, "utf8");
26517
+ }
26518
+ console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
26519
+ } else if (!ghSame) {
26520
+ console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
26521
+ process.exit(1);
26522
+ } else {
26523
+ console.log("customer-kit: .github/hooks/heyspex.json up to date");
26524
+ }
26214
26525
  } else {
26215
26526
  console.log("usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks");
26216
26527
  }
26217
26528
  }
26218
26529
 
26219
26530
  // spex-console/src/lib/cli/connect-checks.ts
26220
- import { homedir as homedir9 } from "node:os";
26221
26531
  import { dirname as dirname5 } from "node:path";
26222
26532
  import { pathToFileURL as pathToFileURL2 } from "node:url";
26223
26533
 
26224
26534
  // spex-console/src/lib/dev-mode.ts
26225
- init_credentials();
26226
26535
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
26227
- import { homedir as homedir6 } from "node:os";
26228
26536
  import { join as join6 } from "node:path";
26537
+ init_credentials();
26229
26538
  var CLOUD_CONSOLE_URL2 = "https://app.heyspex.com";
26230
26539
  var LOCAL_CONSOLE_URL2 = "http://localhost:3000";
26231
26540
  function normalizeMode2(raw) {
@@ -26258,12 +26567,12 @@ function deriveSemantics2(mode, reliance) {
26258
26567
  vaultWriteMode: local ? "local" : "cloud"
26259
26568
  };
26260
26569
  }
26261
- function devModeFilePath2(homeDir = homedir6()) {
26570
+ function devModeFilePath2(homeDir = heyspexHome()) {
26262
26571
  return join6(homeDir, ".heyspex", "dev-mode.json");
26263
26572
  }
26264
26573
  var defaultReadFile3 = (p) => readFileSync7(p, "utf8");
26265
26574
  function readLocalDevModeRecord2({
26266
- homeDir = homedir6(),
26575
+ homeDir = heyspexHome(),
26267
26576
  readFile: readFile3 = defaultReadFile3
26268
26577
  } = {}) {
26269
26578
  let raw;
@@ -26298,7 +26607,7 @@ function readLocalDevModeRecord2({
26298
26607
  }
26299
26608
  function resolveDevMode2({
26300
26609
  env = process.env,
26301
- homeDir = homedir6(),
26610
+ homeDir = heyspexHome(),
26302
26611
  readFile: readFile3 = defaultReadFile3,
26303
26612
  cloudSetting = void 0
26304
26613
  } = {}) {
@@ -26409,10 +26718,11 @@ function shellQuote(p) {
26409
26718
  var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
26410
26719
 
26411
26720
  // vendor/heyspex-mcp/console-fetch.mjs
26721
+ init_heyspex_home();
26412
26722
  init_credentials();
26413
- import { readFileSync as readFileSync9 } from "node:fs";
26723
+ import { readFileSync as readFileSync10 } from "node:fs";
26414
26724
  import { homedir as homedir8 } from "node:os";
26415
- import { join as join9 } from "node:path";
26725
+ import { join as join10 } from "node:path";
26416
26726
 
26417
26727
  // vendor/heyspex-mcp/vault-less-endpoints.mjs
26418
26728
  var VAULT_LESS_ENDPOINTS = Object.freeze([
@@ -26438,10 +26748,41 @@ function isVaultLessEndpoint(method, path9) {
26438
26748
  });
26439
26749
  }
26440
26750
 
26751
+ // vendor/heyspex-mcp/write-outbox.mjs
26752
+ import {
26753
+ readdirSync,
26754
+ readFileSync as readFileSync8,
26755
+ writeFileSync as writeFileSync5,
26756
+ mkdirSync as mkdirSync5,
26757
+ existsSync as existsSync3,
26758
+ renameSync as renameSync3,
26759
+ rmSync
26760
+ } from "node:fs";
26761
+ import { basename, join as join8, isAbsolute as isAbsolute2 } from "node:path";
26762
+
26763
+ // scripts/lib/shared-path-helpers.mjs
26764
+ import { isAbsolute, resolve as resolve3, sep } from "node:path";
26765
+ function resolveCodeRoot({
26766
+ explicit,
26767
+ packageRoot,
26768
+ vaultRoot,
26769
+ env = process.env
26770
+ } = {}) {
26771
+ const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
26772
+ if (fromExplicit) return fromExplicit;
26773
+ const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
26774
+ if (fromEnv) return fromEnv;
26775
+ const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
26776
+ if (fromPackage) return fromPackage;
26777
+ const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
26778
+ return fromVault;
26779
+ }
26780
+
26441
26781
  // vendor/heyspex-mcp/vault-resolve.mjs
26442
- import { existsSync as existsSync3, readFileSync as readFileSync8 } from "node:fs";
26782
+ init_heyspex_home();
26783
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
26443
26784
  import { homedir as homedir7, tmpdir } from "node:os";
26444
- import { dirname as dirname4, join as join8, resolve as resolve3 } from "node:path";
26785
+ import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
26445
26786
 
26446
26787
  // vendor/heyspex-mcp/console-fetch.mjs
26447
26788
  var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
@@ -26749,7 +27090,7 @@ function runInit(args, deps) {
26749
27090
  };
26750
27091
  }
26751
27092
  const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
26752
- const accountId = args.accountId ?? (existingContent ? parseAccountId(existingContent) : null);
27093
+ const accountId = null;
26753
27094
  const content = renderHeyspexYaml({ ...args, vaultId, accountId });
26754
27095
  try {
26755
27096
  deps.writeFile(markerPath, content);
@@ -27015,6 +27356,62 @@ function isAbsolutePathForCompare(p) {
27015
27356
  const s = p.trim();
27016
27357
  return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
27017
27358
  }
27359
+ function expandPinPlaceholders(value, repoRoot) {
27360
+ return value.replace(/\$\{(?:CLAUDE_PROJECT_DIR|workspaceFolder)(?::-[^}]*)?\}/g, repoRoot);
27361
+ }
27362
+ function lexicalJoin(base, rel) {
27363
+ const parts = `${base}/${rel}`.replace(/\\/g, "/").split("/");
27364
+ const out = [];
27365
+ for (const seg of parts) {
27366
+ if (seg === "" && out.length > 0) continue;
27367
+ if (seg === ".") continue;
27368
+ if (seg === ".." && out.length > 1) {
27369
+ out.pop();
27370
+ continue;
27371
+ }
27372
+ out.push(seg);
27373
+ }
27374
+ return out.join("/");
27375
+ }
27376
+ function parseMarkerVaultPath(content) {
27377
+ const m = content.match(/^vault_path:[ \t]*(.+?)[ \t]*$/m);
27378
+ if (!m) return null;
27379
+ const v = m[1].trim().replace(/\s+#.*$/, "").replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
27380
+ if (!v || v.includes("${") || /^[\\/]/.test(v) || /^[a-zA-Z]:[\\/]/.test(v)) return null;
27381
+ return v;
27382
+ }
27383
+ function markerVaultIdOf(content) {
27384
+ const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
27385
+ if (!m) return null;
27386
+ const v = m[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
27387
+ return v.length > 0 ? v : null;
27388
+ }
27389
+ function detectMarkerAttachment(repoRoot, deps) {
27390
+ const markerPath = deps.joinPath(repoRoot, "heyspex.yaml");
27391
+ if (!deps.fileExists(markerPath)) return null;
27392
+ const raw = deps.readFile(markerPath);
27393
+ const vaultPath = raw ? parseMarkerVaultPath(raw) : null;
27394
+ if (!raw || !vaultPath) return null;
27395
+ const vaultRootAbs = lexicalJoin(repoRoot, vaultPath);
27396
+ const markerId = markerVaultIdOf(raw);
27397
+ let cloned = deps.fileExists(deps.joinPath(vaultRootAbs, "specs"));
27398
+ if (cloned && markerId) {
27399
+ const theirs = deps.readFile(deps.joinPath(vaultRootAbs, "heyspex.yaml"));
27400
+ const theirId = theirs ? markerVaultIdOf(theirs) : null;
27401
+ if (theirId && theirId !== markerId) cloned = false;
27402
+ }
27403
+ const registry = loadVaultRegistry(deps.registryOpts);
27404
+ const record = registry.vaults.find((v) => samePath(v.repo_root_abs, vaultRootAbs));
27405
+ return {
27406
+ vaultRootAbs,
27407
+ vaultId: markerId ?? record?.id ?? null,
27408
+ pins: [{ source: "heyspex.yaml", value: vaultRootAbs }],
27409
+ conflicting: false,
27410
+ viaMarker: true,
27411
+ cloned,
27412
+ vaultPath
27413
+ };
27414
+ }
27018
27415
  function readPinnedVaultRoot(repoRoot, deps) {
27019
27416
  const pins = [];
27020
27417
  for (const rel of SATELLITE_MANIFEST_RELS) {
@@ -27031,13 +27428,19 @@ function readPinnedVaultRoot(repoRoot, deps) {
27031
27428
  }
27032
27429
  if (typeof value !== "string") continue;
27033
27430
  const trimmed = value.trim();
27034
- if (!trimmed || trimmed.includes("${")) continue;
27035
- if (!isAbsolutePathForCompare(trimmed)) continue;
27036
- pins.push({ source: rel.join("/"), value: trimmed });
27431
+ if (!trimmed) continue;
27432
+ const expanded = expandPinPlaceholders(trimmed, repoRoot);
27433
+ if (expanded.includes("${")) continue;
27434
+ if (/^[a-zA-Z]:(?![\\/])/.test(expanded)) continue;
27435
+ const abs = isAbsolutePathForCompare(expanded) ? expanded.includes("..") ? lexicalJoin(expanded, ".") : expanded : lexicalJoin(repoRoot, expanded);
27436
+ if (expanded !== trimmed && isAtOrUnder(abs, repoRoot)) continue;
27437
+ pins.push({ source: rel.join("/"), value: abs });
27037
27438
  }
27038
27439
  return pins;
27039
27440
  }
27040
27441
  function detectSatelliteAttachment(repoRoot, deps) {
27442
+ const viaMarker = detectMarkerAttachment(repoRoot, deps);
27443
+ if (viaMarker) return viaMarker;
27041
27444
  const pins = readPinnedVaultRoot(repoRoot, deps).filter(
27042
27445
  (pin) => !isAtOrUnder(pin.value, repoRoot)
27043
27446
  );
@@ -27153,7 +27556,7 @@ function workspaceAgreementRow(vaultRecord, cloudWorkspaceId) {
27153
27556
  function checksHomeDir(deps) {
27154
27557
  if (deps.homeDir) return deps.homeDir;
27155
27558
  const registryDir = deps.registryOpts?.registryDir;
27156
- return registryDir ? dirname5(registryDir) : homedir9();
27559
+ return registryDir ? dirname5(registryDir) : heyspexHome();
27157
27560
  }
27158
27561
  function accountAgreementRow(yamlContent, vaultRecord, deps, opts) {
27159
27562
  const id = "account_agreement";
@@ -27161,14 +27564,14 @@ function accountAgreementRow(yamlContent, vaultRecord, deps, opts) {
27161
27564
  if (yamlContent === null) {
27162
27565
  return { id, label, status: "not_applicable", detail: "no heyspex.yaml yet" };
27163
27566
  }
27164
- const recorded = parseAccountId(yamlContent);
27567
+ const legacyMarkerAccount = parseAccountId(yamlContent);
27568
+ const recorded = vaultRecord && vaultRecord.account_id && vaultRecord.account_id !== LEGACY_REGISTRY_ACCOUNT_ID ? vaultRecord.account_id : null;
27165
27569
  if (!recorded) {
27166
27570
  return {
27167
27571
  id,
27168
27572
  label,
27169
- status: "stale",
27170
- detail: "heyspex.yaml records no account: (written before accounts were recorded) -- run `hs connect` to record the account this repo is connected under",
27171
- remedy: "hs connect"
27573
+ status: "not_applicable",
27574
+ detail: "the account binding is machine-local (the registry row), and none exists on this machine yet; the signed-in account is used" + (legacyMarkerAccount ? ` (legacy account: ${legacyMarkerAccount} in heyspex.yaml is ignored)` : "")
27172
27575
  };
27173
27576
  }
27174
27577
  const cloud = opts.cloudVaultAccountId ?? null;
@@ -27196,28 +27599,25 @@ function accountAgreementRow(yamlContent, vaultRecord, deps, opts) {
27196
27599
  remedy
27197
27600
  };
27198
27601
  }
27199
- if (vaultRecord && vaultRecord.account_id !== recorded) {
27200
- const legacy = vaultRecord.account_id === LEGACY_REGISTRY_ACCOUNT_ID;
27201
- return {
27202
- id,
27203
- label,
27204
- status: "stale",
27205
- detail: `the registry row for this repo is keyed to ${legacy ? "no account (legacy)" : vaultRecord.account_id}, heyspex.yaml says ${recorded} -- run \`hs connect\` to backfill it`,
27206
- remedy: "hs connect"
27207
- };
27208
- }
27209
27602
  const current = resolveSessionOwnerUncached({
27210
27603
  env,
27211
27604
  homeDir,
27212
27605
  readFile: (p) => readFile3(p) ?? ""
27213
27606
  });
27214
27607
  const signedIn = current && current !== recorded ? `; signed in as ${current} -- sessions here still use ${recorded}'s sign-in` : "";
27215
- return { id, label, status: "present", detail: `account: ${recorded}${signedIn}` };
27608
+ const legacyNote = legacyMarkerAccount && legacyMarkerAccount !== recorded ? `; legacy account: ${legacyMarkerAccount} in heyspex.yaml is ignored` : "";
27609
+ return {
27610
+ id,
27611
+ label,
27612
+ status: "present",
27613
+ detail: `account: ${recorded} (machine-local)${signedIn}${legacyNote}`
27614
+ };
27216
27615
  }
27217
27616
  function runConnectChecks(repoRoot, deps, opts = {}) {
27218
27617
  const checks = [];
27219
27618
  const satellite = opts.satellite !== void 0 ? opts.satellite : detectSatelliteAttachment(repoRoot, deps);
27220
- const vaultSubject = satellite ? satellite.vaultRootAbs : repoRoot;
27619
+ const cloudOnly = Boolean(satellite?.viaMarker) && satellite?.cloned === false;
27620
+ const vaultSubject = satellite && !cloudOnly ? satellite.vaultRootAbs : repoRoot;
27221
27621
  const yamlPath = deps.joinPath(vaultSubject, "heyspex.yaml");
27222
27622
  const yamlContent = readIfExists(deps, yamlPath);
27223
27623
  const vaultId = yamlContent ? parseVaultId(yamlContent) : null;
@@ -27245,11 +27645,12 @@ function runConnectChecks(repoRoot, deps, opts = {}) {
27245
27645
  )
27246
27646
  );
27247
27647
  const workspaceRecord = vaultRecord ? registry.workspaces.find((w) => w.id === vaultRecord.workspace_id) : void 0;
27648
+ const ownVaultRegistered = Boolean(vaultId) && Boolean(vaultRecord) && vaultRecord?.id === vaultId;
27248
27649
  checks.push(
27249
27650
  row(
27250
27651
  "workspace_registered",
27251
27652
  "workspace registered in ~/.heyspex/registry.json",
27252
- Boolean(workspaceRecord),
27653
+ Boolean(workspaceRecord) || ownVaultRegistered,
27253
27654
  satelliteDetail(
27254
27655
  satellite,
27255
27656
  workspaceRecord ? `id: ${workspaceRecord.id}` : vaultRecord ? `vault's workspace "${vaultRecord.workspace_id}" not found in registry` : satellite ? "the attached vault is not registered on this machine" : "no vault registered for this repo"
@@ -27370,9 +27771,38 @@ function runConnectChecks(repoRoot, deps, opts = {}) {
27370
27771
  id: "satellite_attachment",
27371
27772
  label: "attached vault (satellite repo)",
27372
27773
  status: satellite.conflicting ? "stale" : "present",
27373
- detail: satellite.conflicting ? `manifests pin different vaults (${pinned}) -- re-run \`hs install --attach-vault <id> --force\` to make them agree` : `${satellite.vaultId ?? "(unregistered)"} at ${satellite.vaultRootAbs} (${pinned})`
27774
+ detail: satellite.viaMarker ? `${satellite.vaultId ?? "(no vault_id)"} via heyspex.yaml vault_path ${satellite.vaultPath} -> ${satellite.vaultRootAbs} (${satellite.cloned ? "cloned here" : "NOT cloned here: cloud-only mode"})` : satellite.conflicting ? `manifests pin different vaults (${pinned}) -- re-run \`hs install --attach-vault <id> --force\` to make them agree` : `${satellite.vaultId ?? "(unregistered)"} at ${satellite.vaultRootAbs} (${pinned})`
27374
27775
  });
27375
27776
  }
27777
+ if (satellite?.viaMarker) {
27778
+ const na = (id, detail) => {
27779
+ const r = checks.find((c) => c.id === id);
27780
+ if (r && (r.status === "missing" || r.status === "stale")) {
27781
+ r.status = "not_applicable";
27782
+ r.detail = detail;
27783
+ delete r.remedy;
27784
+ }
27785
+ };
27786
+ const noRow = "not required: this repo's committed heyspex.yaml binds the vault by id (a registry row appears after `hs connect` in the vault clone)";
27787
+ na("vault_registered", noRow);
27788
+ na("workspace_registered", noRow);
27789
+ na("id_agreement", noRow);
27790
+ na("account_agreement", "portable marker records no account; the signed-in account is used");
27791
+ if (cloudOnly) {
27792
+ na(
27793
+ "specs_dir",
27794
+ `satellite, vault not cloned locally -- cloud mode (clone the vault repo at ${satellite.vaultPath ?? "its vault_path"} for local reads and \`hs pull\`)`
27795
+ );
27796
+ }
27797
+ if (!vaultId) {
27798
+ const y = checks.find((c) => c.id === "heyspex_yaml");
27799
+ if (y) {
27800
+ y.status = "stale";
27801
+ y.detail = "heyspex.yaml has a vault_path but no vault_id; the satellite cannot name its vault";
27802
+ y.remedy = "add `vault_id: <the vault's uuid>` to heyspex.yaml (find it with `hs vault list`), then re-run `hs connect --check`";
27803
+ }
27804
+ }
27805
+ }
27376
27806
  const connected = checks.every((c) => c.status === "present" || c.status === "not_applicable");
27377
27807
  return {
27378
27808
  repo_root: repoRoot,
@@ -27381,7 +27811,8 @@ function runConnectChecks(repoRoot, deps, opts = {}) {
27381
27811
  ...satellite ? {
27382
27812
  satellite: {
27383
27813
  attached_vault_root: satellite.vaultRootAbs,
27384
- attached_vault_id: satellite.vaultId
27814
+ attached_vault_id: satellite.vaultId,
27815
+ ...satellite.viaMarker ? { via_marker: true, cloned: satellite.cloned === true } : {}
27385
27816
  }
27386
27817
  } : {}
27387
27818
  };
@@ -27507,6 +27938,22 @@ function pointerVsRepoVerdict(input) {
27507
27938
  repair: `hs vault switch ${repoVault}`,
27508
27939
  detail: `the cloud pointer names ${cloudPointer ?? "(none)"} but the registry pointer (its local cache) names ${registryPointer}: the cache is stale`
27509
27940
  });
27941
+ if (markerVaultId && input.markerBoundSatellite && !registryRowId) {
27942
+ return {
27943
+ branch: "explicit_vault",
27944
+ agree: true,
27945
+ repair: null,
27946
+ detail: `this marker-bound satellite names ${markerVaultId} in its committed heyspex.yaml, so CLI and MCP requests route to it by id; no registry row is needed and the account pointer (${cloudPointer ?? "(none)"}) is informational`
27947
+ };
27948
+ }
27949
+ if (markerVaultId && registryRowId === markerVaultId) {
27950
+ return {
27951
+ branch: "explicit_vault",
27952
+ agree: true,
27953
+ repair: null,
27954
+ detail: cloudPointer === markerVaultId && (registryPointer === null || registryPointer === markerVaultId) ? `both pointers name ${markerVaultId}` : `the account's active pointer names ${cloudPointer ?? "(none)"} (the browser console's choice${registryPointer !== null && registryPointer !== cloudPointer ? `; the local cache names ${registryPointer}` : ""}); this repo names its own vault ${markerVaultId} explicitly in heyspex.yaml, so CLI and MCP requests route to it and the difference is informational (optional: \`hs vault switch ${markerVaultId}\`)`
27955
+ };
27956
+ }
27510
27957
  if (markerVaultId) {
27511
27958
  if (registryRowId !== markerVaultId) {
27512
27959
  const rekey = ROUTING_FACTS.find((f) => f.id === "C")?.caches.find((c) => c.id === "registry-vault-id")?.repair ?? "hs vault migrate-registry";
@@ -27722,7 +28169,7 @@ async function readCloudView(http, tokens) {
27722
28169
  }
27723
28170
  async function runCloudViewChecks(repoRoot, deps, opts = {}) {
27724
28171
  const satellite = opts.satellite !== void 0 ? opts.satellite : detectSatelliteAttachment(repoRoot, deps);
27725
- const vaultSubject = satellite ? satellite.vaultRootAbs : repoRoot;
28172
+ const vaultSubject = satellite && !(satellite.viaMarker && satellite.cloned === false) ? satellite.vaultRootAbs : repoRoot;
27726
28173
  const marker = readIfExists(deps, deps.joinPath(vaultSubject, "heyspex.yaml"));
27727
28174
  const vaultId = marker ? parseVaultId(marker) : null;
27728
28175
  const env = deps.env ?? process.env;
@@ -27733,7 +28180,11 @@ async function runCloudViewChecks(repoRoot, deps, opts = {}) {
27733
28180
  homeDir,
27734
28181
  readFile: (p) => readFile3(p) ?? ""
27735
28182
  });
27736
- const repoAccount = (marker ? parseAccountId(marker) : null) ?? signedInLocal ?? null;
28183
+ const rowForAccount = loadVaultRegistry(deps.registryOpts).vaults.find(
28184
+ (v) => samePath(v.repo_root_abs, vaultSubject)
28185
+ );
28186
+ const boundAccount = rowForAccount?.account_id && rowForAccount.account_id !== LEGACY_REGISTRY_ACCOUNT_ID ? rowForAccount.account_id : null;
28187
+ const repoAccount = boundAccount ?? signedInLocal ?? null;
27737
28188
  const http = deps.http;
27738
28189
  let tokens = { current: null, repo: null };
27739
28190
  if (http && http.token !== void 0) tokens = { current: http.token, repo: http.token };
@@ -27767,7 +28218,8 @@ async function runCloudViewChecks(repoRoot, deps, opts = {}) {
27767
28218
  markerVaultId: vaultId,
27768
28219
  registryRowId: vaultRecord?.id ?? null,
27769
28220
  cloudPointer: view.cloudPointer,
27770
- registryPointer: localPointer
28221
+ registryPointer: localPointer,
28222
+ markerBoundSatellite: Boolean(satellite?.viaMarker)
27771
28223
  });
27772
28224
  rows.push(
27773
28225
  compared(
@@ -27840,6 +28292,19 @@ async function runCloudViewChecks(repoRoot, deps, opts = {}) {
27840
28292
  rows.push(notApplicableRow(id, "no heyspex.yaml and no signed-in account"));
27841
28293
  } else if (view.sessionFailure) {
27842
28294
  rows.push(unknownRow(id, repoAccount, view.sessionFailure));
28295
+ } else if (!repoAccount && satellite?.viaMarker) {
28296
+ rows.push(
28297
+ compared(
28298
+ id,
28299
+ {
28300
+ local: "(portable marker: no account recorded)",
28301
+ cloud: view.email ? `${view.userId} (${view.email})` : view.userId
28302
+ },
28303
+ true,
28304
+ "hs login",
28305
+ `portable marker; signed in as ${view.email ?? view.userId}`
28306
+ )
28307
+ );
27843
28308
  } else {
27844
28309
  const agree = repoAccount !== null && (repoAccount === view.userId || repoAccount === view.email);
27845
28310
  rows.push(
@@ -27977,7 +28442,7 @@ function writeProjectContract(ctx, options2 = {}) {
27977
28442
  }
27978
28443
 
27979
28444
  // spex-console/src/lib/cli/init.customer-vault.ts
27980
- import { basename } from "node:path";
28445
+ import { basename as basename2 } from "node:path";
27981
28446
 
27982
28447
  // spex-console/src/lib/cli/template-manifest.sources.mjs
27983
28448
  var UNIVERSAL_TEMPLATE_ROWS = [
@@ -28488,7 +28953,7 @@ function findManagedSpecs(deps, targetDir, manifestDests) {
28488
28953
  return out;
28489
28954
  }
28490
28955
  function defaultVaultName(targetDir) {
28491
- return basename(targetDir) || targetDir;
28956
+ return basename2(targetDir) || targetDir;
28492
28957
  }
28493
28958
  function runCustomerVaultInit(args, deps, options2 = {}) {
28494
28959
  const { targetDir } = args;
@@ -28636,14 +29101,14 @@ function templatePinHook() {
28636
29101
  }
28637
29102
 
28638
29103
  // spex-console/src/lib/cli/principle-selection.ts
28639
- import { basename as basename2 } from "node:path";
29104
+ import { basename as basename3 } from "node:path";
28640
29105
  var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
28641
29106
  function principleKey(row2) {
28642
- const base = basename2(row2.dest, ".md");
29107
+ const base = basename3(row2.dest, ".md");
28643
29108
  return base.startsWith("principle-") ? base.slice("principle-".length) : base;
28644
29109
  }
28645
29110
  function principleWikilink(row2) {
28646
- return basename2(row2.dest, ".md");
29111
+ return basename3(row2.dest, ".md");
28647
29112
  }
28648
29113
  function parsePrinciplesFlag(value, rows) {
28649
29114
  const principleRows = rows.filter((row2) => row2.group === "principle");
@@ -28754,7 +29219,7 @@ function applyCustomerTemplate(args) {
28754
29219
  }
28755
29220
 
28756
29221
  // spex-console/src/lib/cli/install.ts
28757
- import { homedir as homedir10 } from "node:os";
29222
+ import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
28758
29223
 
28759
29224
  // scripts/install-cursor-hooks.mjs
28760
29225
  import fs2 from "node:fs";
@@ -28771,10 +29236,13 @@ var HOOKS = [
28771
29236
  { event: "sessionStart", hook: "wip-checkpoint", why: "Surface the previous session WIP handoff before work resumes." },
28772
29237
  { event: "sessionStart", hook: "inject-dev-server-awareness", why: "Once-per-session reminder that dev-server status is a tool call, not a guess." },
28773
29238
  { event: "sessionStart", hook: "inject-coo-seat", why: "Inject the dev-factory COO seat mandate when the COO window is open." },
29239
+ { event: "sessionStart", hook: "inject-doctrine", why: "Inject the baseline SpexCode Doctrine edition at session start." },
28774
29240
  { event: "sessionStart", hook: "coo-guard-check", why: "Warn at session start when a breakdown has not been delegated." },
28775
29241
  { event: "sessionStart", hook: "check-auto-memory-budget", why: "Self-heal and budget-check the auto-memory index." },
28776
29242
  { event: "sessionStart", hook: "check-claude-md-budget", why: "Advisory budget check on the always-loaded agent contract." },
28777
29243
  { event: "sessionStart", hook: "check-mcp-freshness-hook", why: "Warn when the running MCP server is stale versus vendor." },
29244
+ { event: "sessionStart", hook: "git-reflog-audit", why: "Session-start git audit (stash/reset/rebase witness)." },
29245
+ { event: "sessionStart", hook: "vault-mirror-catchup", why: "Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ)." },
28778
29246
  // ── Compaction ──────────────────────────────────────────────────────────
28779
29247
  // Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
28780
29248
  // preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
@@ -28788,6 +29256,7 @@ var HOOKS = [
28788
29256
  { event: "beforeSubmitPrompt", hook: "preflight-reminder", why: "Name the required procedure before a filing/destructive/protocol turn." },
28789
29257
  { event: "beforeSubmitPrompt", hook: "intent-classifier-trigger", why: "Route capture / research / settings / defer intents to the right skill." },
28790
29258
  { event: "beforeSubmitPrompt", hook: "inject-communication-style", why: "Re-assert the plain-language communication rules each turn." },
29259
+ { event: "beforeSubmitPrompt", hook: "inject-vault-context", timeout: 10, why: "Lexical vault retrieval for the prompt (surfaced as a user message)." },
28791
29260
  // ── Before every tool call (any tool) ───────────────────────────────────
28792
29261
  { event: "preToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], why: "Claim liveness heartbeat to the console." },
28793
29262
  { event: "preToolUse", matcher: ".*", hook: "coo-denylist-gate", why: "Hard deny the COO-window deny list." },
@@ -28798,21 +29267,27 @@ var HOOKS = [
28798
29267
  // the vault guard because removing a profile file is the loss it exists to
28799
29268
  // prevent, and Claude had no separate delete tool to list.
28800
29269
  { event: "preToolUse", matcher: "Write|Delete", hook: "env-vault-guard", why: "Block destructive writes to the encrypted env vault (ALWAYS ON)." },
29270
+ { event: "preToolUse", matcher: "Write", hook: "vault-write-tier-guard", why: "Deny disk writes to vault specs in cloud write mode (HS-FCNH)." },
28801
29271
  { event: "preToolUse", matcher: "Write", hook: "settings-write-gate", why: "Require approval before .heyspex/config.yaml changes." },
28802
29272
  { event: "preToolUse", matcher: "Write", hook: "enforce-vault-naming", why: "Block vault filenames that violate the naming registry." },
28803
29273
  { event: "preToolUse", matcher: "Write", hook: "guard-auto-memory-writes", why: "Block new topic files in the auto-memory directory." },
28804
29274
  { event: "preToolUse", matcher: "Write", hook: "preflight-reminder", why: "Preflight reminder on the edit path itself." },
28805
29275
  { event: "preToolUse", matcher: "Write", hook: "claim-before-code-gate", why: "Deny the first code edit after plan exit with no claim." },
29276
+ { event: "preToolUse", matcher: "Write", hook: "directive-obligation-gate", why: "Deny product-code writes while a capture_on_idea obligation is unmet, and (claim-before-build) with no claim/capture (HS-20SW)." },
28806
29277
  // ── Before a shell command ──────────────────────────────────────────────
28807
29278
  // beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
28808
29279
  // to carry the command string, and Cursor honors allow/deny/ask on it.
28809
29280
  { event: "beforeShellExecution", hook: "env-vault-guard", why: "Block shell writes/exfiltration targeting the env vault (ALWAYS ON)." },
29281
+ { event: "beforeShellExecution", hook: "vault-write-tier-guard", why: "Deny shell writes to vault specs in cloud write mode (HS-FCNH)." },
28810
29282
  { event: "beforeShellExecution", hook: "settings-write-gate", why: "Catch shell writes to .heyspex/config.yaml." },
28811
29283
  { event: "beforeShellExecution", hook: "enforce-vault-naming", why: "Catch vault files created via the shell." },
28812
29284
  { event: "beforeShellExecution", hook: "guard-auto-memory-writes", why: "Catch auto-memory topic files created via the shell." },
28813
29285
  { event: "beforeShellExecution", hook: "preflight-reminder", why: "Preflight reminder on rm -rf / force-push shaped commands." },
28814
29286
  { event: "beforeShellExecution", hook: "dev-server-task-reminder", timeout: 10, why: "Dev-server start/stop hard gate plus DB-safety reminder." },
28815
29287
  { event: "beforeShellExecution", hook: "git-stash-gate", timeout: 10, why: "Always ask before a mutating git stash." },
29288
+ { event: "beforeShellExecution", hook: "git-identity-guard", why: "Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON)." },
29289
+ { event: "beforeShellExecution", hook: "capture-execution-gate", why: "Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform)." },
29290
+ { event: "beforeShellExecution", hook: "directive-obligation-gate", why: "Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW)." },
28816
29291
  // ── Before specific MCP tools ───────────────────────────────────────────
28817
29292
  // Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
28818
29293
  // stay on preToolUse rather than beforeMCPExecution — registering both would
@@ -28822,15 +29297,20 @@ var HOOKS = [
28822
29297
  { event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "capture-workflow-gate", timeout: 8, why: "Deny filing an implementation spec straight out of an open capture." },
28823
29298
  { event: "preToolUse", matcher: ".*", tool: "heyspex_settings_set$", hook: "settings-write-gate", why: "Require approval for settings changes made through MCP." },
28824
29299
  { event: "preToolUse", matcher: ".*", tool: "[Nn]eon__(prepare_database_migration|complete_database_migration|reset_from_parent|delete_branch)$", hook: "dev-server-task-reminder", timeout: 10, why: "DB-safety gate on Neon migration/reset/delete tools." },
29300
+ { event: "preToolUse", matcher: ".*", tool: "heyspex_file_spec$", hook: "diagnose-capture-reminder", why: "Warn when a diagnose is filed with no capture holding the owner words (HS-IK2D)." },
29301
+ { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "research-method-guard", why: "Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON)." },
28825
29302
  { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "deep-research-cost-gate", why: "Cost confirmation before a paid deep-research harness launch." },
28826
29303
  { event: "preToolUse", matcher: ".*", tool: "^(WebSearch|WebFetch|web_search|web_fetch)$|heyspex_save_research$", hook: "research-protocol-gate", why: "WebSearch/WebFetch/save_research refused until the research preflight ran (HS-K0ZP, HS-EN4F)." },
28827
29304
  // ── Before a subagent spawn ─────────────────────────────────────────────
28828
29305
  // subagentStart over preToolUse/Task: it carries subagent_type and the model,
28829
29306
  // which is exactly what the routing gate decides on, and it can deny.
28830
29307
  { event: "subagentStart", hook: "agent-spawn-delegation-gate", why: "Block a build/breakdown/critic subagent on an off-routing model." },
29308
+ { event: "subagentStart", hook: "capture-execution-gate", why: "Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform)." },
29309
+ { event: "subagentStart", hook: "directive-obligation-gate", why: "Deny a build subagent while a capture obligation is unmet or the session has no claim/capture (HS-20SW)." },
28831
29310
  // ── After every tool call ───────────────────────────────────────────────
28832
29311
  { event: "postToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], why: "Claim liveness heartbeat after the call resolves." },
28833
29312
  { event: "postToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PostToolUse"], why: "Append the tool result row to the session log." },
29313
+ { event: "postToolUse", matcher: ".*", hook: "directive-obligation-gate", why: "Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW)." },
28834
29314
  { event: "postToolUse", matcher: ".*", hook: "close-terminal-on-ship", why: "Close the Launch terminal once the spec ships." },
28835
29315
  { event: "postToolUse", matcher: "Write", hook: "notify-status-flip", why: "Sound on an in_review status flip." },
28836
29316
  { event: "postToolUse", matcher: "Write", hook: "scripts/stamp-provenance-on-edit.mjs", why: "Stamp assisted_by provenance on direct spec edits." },
@@ -28843,7 +29323,11 @@ var HOOKS = [
28843
29323
  // hook re-drives the agent — the same effect as Claude's exit-2 block.
28844
29324
  { event: "stop", hook: "emit-claim-status", args: ["--event=Stop"], why: "Mark the claim pending-on-response." },
28845
29325
  { event: "stop", hook: "naughty-list-gate", why: "Re-drive the turn when the final message uses banned jargon." },
29326
+ { event: "stop", hook: "id-display-format-gate", why: "Re-drive the turn when a reply breaks the Naming V3 display-id format." },
29327
+ { event: "stop", hook: "epic-milestone-phrasing-gate", why: "Re-drive the turn when a reply breaks the epic/milestone phrasing rules." },
29328
+ { event: "stop", hook: "git-reflog-audit", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
28846
29329
  { event: "stop", hook: "enforce-session-sequence", why: "Re-drive the turn when work shipped without record_wip." },
29330
+ { event: "stop", hook: "directive-obligation-gate", why: "Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW)." },
28847
29331
  // ── Session end ─────────────────────────────────────────────────────────
28848
29332
  { event: "sessionEnd", hook: "session-end", why: "Release the claim and notify the console." },
28849
29333
  { event: "sessionEnd", hook: "close-terminal-on-ship", why: "Final terminal cleanup." }
@@ -30481,6 +30965,7 @@ var INSTALL_HELP = [
30481
30965
  " hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
30482
30966
  " [--no-cursor] [--no-claude] [--claude-apply]",
30483
30967
  " [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
30968
+ " [--copilot-hooks | --copilot-plugin | --copilot-plugin-apply]",
30484
30969
  " [--no-statusline]",
30485
30970
  " [--cc | --command-code] [--register-vault [--workspace <id>]]",
30486
30971
  " [--remote[=<url>]]",
@@ -30539,6 +31024,15 @@ var INSTALL_HELP = [
30539
31024
  ` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
30540
31025
  " --plugin-scope <s> Plugin install scope: user, project (default), or local.",
30541
31026
  " Implies --plugin.",
31027
+ " --copilot-hooks Write .github/hooks/heyspex.json (GitHub Copilot hooks) into the",
31028
+ " target. Commands resolve node_modules/heyspex first, so an npm-only",
31029
+ " install (no heyspex repo checkout) works. Pick this OR the plugin.",
31030
+ " --copilot-plugin Print `copilot plugin marketplace add '<platform>'` +",
31031
+ " `copilot plugin install <plugin>@<marketplace>` (the platform root is",
31032
+ " node_modules/heyspex for a published install).",
31033
+ " --copilot-plugin-apply Run those two commands when `copilot` is on PATH.",
31034
+ " Requesting the Copilot plugin AND --copilot-hooks warns and skips the",
31035
+ " hooks file (both would fire every hook twice).",
30542
31036
  " --remote[=<url>] Write .mcp.json for the REMOTE (cloud) MCP endpoint instead of",
30543
31037
  ` the local stdio server \u2014 the Streamable-HTTP form against`,
30544
31038
  ` ${DEFAULT_REMOTE_MCP_URL} (or <url>), authenticated by the`,
@@ -30587,35 +31081,15 @@ var INSTALL_HELP = [
30587
31081
 
30588
31082
  // spex-console/src/lib/cli/install-identity.ts
30589
31083
  import { spawnSync as spawnSync2 } from "node:child_process";
30590
- import { existsSync as existsSync4 } from "node:fs";
30591
- import { homedir as homedir12 } from "node:os";
30592
- import { join as join10 } from "node:path";
31084
+ import { existsSync as existsSync5 } from "node:fs";
31085
+ import { join as join11 } from "node:path";
30593
31086
  import { createInterface } from "node:readline/promises";
30594
-
30595
- // scripts/lib/shared-path-helpers.mjs
30596
- import { isAbsolute, resolve as resolve4, sep } from "node:path";
30597
- function resolveCodeRoot({
30598
- explicit,
30599
- packageRoot,
30600
- vaultRoot,
30601
- env = process.env
30602
- } = {}) {
30603
- const fromExplicit = typeof explicit === "string" ? explicit.trim() : "";
30604
- if (fromExplicit) return fromExplicit;
30605
- const fromEnv = typeof env?.HS_PKG_ROOT === "string" ? env.HS_PKG_ROOT.trim() : "";
30606
- if (fromEnv) return fromEnv;
30607
- const fromPackage = typeof packageRoot === "string" ? packageRoot.trim() : "";
30608
- if (fromPackage) return fromPackage;
30609
- const fromVault = typeof vaultRoot === "string" ? vaultRoot.trim() : "";
30610
- return fromVault;
30611
- }
30612
-
30613
- // spex-console/src/lib/cli/install-identity.ts
30614
31087
  init_credentials();
30615
31088
 
30616
31089
  // vendor/heyspex-mcp/whoami.mjs
31090
+ init_heyspex_home();
30617
31091
  init_credentials();
30618
- import { homedir as homedir11 } from "node:os";
31092
+ import { homedir as homedir9 } from "node:os";
30619
31093
  var SESSION_PATH = "/api/mcp/session";
30620
31094
  async function probeCredentialIdentity({ consoleUrl, token, fetchImpl = fetch } = {}) {
30621
31095
  if (!token) return { kind: "no_credential" };
@@ -30870,7 +31344,7 @@ async function runIdentityGate(io, flags, verb, wording = ATTACH_WORDING) {
30870
31344
  "identity_unconfirmed"
30871
31345
  );
30872
31346
  }
30873
- function readConfiguredAccountId(env = process.env, homeDir = homedir12()) {
31347
+ function readConfiguredAccountId(env = process.env, homeDir = heyspexHome()) {
30874
31348
  return resolveSessionOwnerUncached({ env, homeDir });
30875
31349
  }
30876
31350
  function accountMismatchRepair(repoAccount) {
@@ -30899,7 +31373,7 @@ function isInteractiveTerminal() {
30899
31373
  }
30900
31374
  function defaultIdentityGateIo(opts) {
30901
31375
  const consoleUrl = opts.consoleUrl.trim().replace(/\/+$/, "");
30902
- const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join10(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync4(p));
31376
+ const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join11(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync5(p));
30903
31377
  return {
30904
31378
  consoleUrl,
30905
31379
  interactive: opts.interactive ?? isInteractiveTerminal(),
@@ -31030,7 +31504,7 @@ function remedyFor(row2, satellite) {
31030
31504
  case "heyspex_yaml":
31031
31505
  case "vault_registered":
31032
31506
  case "workspace_registered":
31033
- return satellite ? null : "hs connect";
31507
+ return satellite ? row2.remedy ?? null : "hs connect";
31034
31508
  case "mcp_entry":
31035
31509
  return satellite ? "hs install --attach-vault <vault-id> --force" : "hs connect";
31036
31510
  case "plugin_surface":
@@ -31038,7 +31512,7 @@ function remedyFor(row2, satellite) {
31038
31512
  case "git_hooks":
31039
31513
  return "node scripts/install-git-hooks.mjs";
31040
31514
  case "satellite_attachment":
31041
- return "hs install --attach-vault <vault-id> --force";
31515
+ return row2.remedy ?? "hs install --attach-vault <vault-id> --force";
31042
31516
  case "active_vault":
31043
31517
  case "account_agreement":
31044
31518
  case "workspace_agreement":
@@ -31061,7 +31535,10 @@ function formatReport(report) {
31061
31535
  const lines = [
31062
31536
  `hs connect --check \u2014 ${report.repo_root}`,
31063
31537
  ...report.satellite ? [
31064
- `satellite repo \u2014 attached to vault ${report.satellite.attached_vault_id ?? "(unregistered)"} at ${report.satellite.attached_vault_root}; the vault rows below describe THAT repo.`
31538
+ `satellite repo \u2014 attached to vault ${report.satellite.attached_vault_id ?? "(unregistered)"} at ${report.satellite.attached_vault_root}; the vault rows below describe THAT repo.`,
31539
+ ...report.satellite.via_marker ? [
31540
+ report.satellite.cloned ? "bound by this repo's committed heyspex.yaml (vault_id + relative vault_path)." : "bound by this repo's committed heyspex.yaml; vault not cloned locally -- cloud mode (cloud reads and writes work; clone the vault at its vault_path for local reads)."
31541
+ ] : []
31065
31542
  ] : [],
31066
31543
  "",
31067
31544
  ...report.checks.flatMap((c) => [
@@ -31137,7 +31614,7 @@ var CANONICAL_VAULT_SUBFOLDERS = [
31137
31614
  function bareSpecsDirWarning(specsDir, deps) {
31138
31615
  let entries = [];
31139
31616
  try {
31140
- entries = readdirSync(specsDir);
31617
+ entries = readdirSync2(specsDir);
31141
31618
  } catch {
31142
31619
  return null;
31143
31620
  }
@@ -31259,9 +31736,10 @@ async function runConnectFullRun(args, deps) {
31259
31736
  };
31260
31737
  }
31261
31738
  if (!satellite && strictAccount.ok) {
31262
- const markerPath = deps.joinPath(repoRoot, "heyspex.yaml");
31263
- const markerText = deps.fileExists(markerPath) ? deps.readFile(markerPath) : null;
31264
- const repoAccount = markerText ? parseAccountId(markerText) : null;
31739
+ const boundRow = loadVaultRegistry(deps.registryOpts).vaults.find(
31740
+ (v) => samePath(v.repo_root_abs, repoRoot)
31741
+ );
31742
+ const repoAccount = boundRow?.account_id && boundRow.account_id !== "legacy" ? boundRow.account_id : null;
31265
31743
  if (repoAccount && repoAccount !== strictAccount.accountId) {
31266
31744
  return accountMismatchRefusal("hs connect", repoAccount, strictAccount.accountId);
31267
31745
  }
@@ -31364,37 +31842,14 @@ async function runConnectFullRun(args, deps) {
31364
31842
  if (isPresent("heyspex_yaml")) {
31365
31843
  const content = deps.fileExists(yamlPath) ? deps.readFile(yamlPath) : null;
31366
31844
  vaultId = content ? parseVaultId(content) ?? void 0 : void 0;
31367
- const accountUpdate = content !== null && accountId ? recordMarkerAccount(content, accountId) : null;
31368
- if (accountUpdate?.action !== "added") {
31369
- steps.push({
31370
- id: "heyspex_yaml",
31371
- label: "heyspex.yaml vault marker",
31372
- status: "skipped",
31373
- detail: "already initialized",
31374
- confirmed_by: "read heyspex.yaml"
31375
- });
31376
- } else if (args.dryRun) {
31377
- steps.push({
31378
- id: "heyspex_yaml",
31379
- label: "heyspex.yaml vault marker",
31380
- status: "run",
31381
- detail: `would record account: ${accountId} in ${yamlPath}`
31382
- });
31383
- } else {
31384
- try {
31385
- deps.writeFile(yamlPath, accountUpdate.content);
31386
- assertMarker(deps.readFile(yamlPath), yamlPath, vaultId, accountId);
31387
- steps.push({
31388
- id: "heyspex_yaml",
31389
- label: "heyspex.yaml vault marker",
31390
- status: "run",
31391
- detail: `recorded account: ${accountId} in ${yamlPath}`,
31392
- confirmed_by: "read heyspex.yaml"
31393
- });
31394
- } catch (err) {
31395
- failStep("heyspex_yaml", "heyspex.yaml vault marker", err);
31396
- }
31397
- }
31845
+ const legacyAccount = content ? parseAccountId(content) : null;
31846
+ steps.push({
31847
+ id: "heyspex_yaml",
31848
+ label: "heyspex.yaml vault marker",
31849
+ status: "skipped",
31850
+ detail: legacyAccount ? `already initialized (legacy account: ${legacyAccount} in the marker is ignored -- the account binding is machine-local)` : "already initialized",
31851
+ confirmed_by: "read heyspex.yaml"
31852
+ });
31398
31853
  } else if (args.dryRun) {
31399
31854
  steps.push({
31400
31855
  id: "heyspex_yaml",
@@ -31432,8 +31887,7 @@ async function runConnectFullRun(args, deps) {
31432
31887
  vaultRoot: "specs/",
31433
31888
  force: false,
31434
31889
  json: false,
31435
- adoptedVaultId,
31436
- ...accountId ? { accountId } : {}
31890
+ adoptedVaultId
31437
31891
  },
31438
31892
  initDeps
31439
31893
  );
@@ -32452,9 +32906,6 @@ function assertMarker(content, path9, vaultId, accountId) {
32452
32906
  if (vaultId && back !== vaultId) {
32453
32907
  throw new Error(`${path9} reads back vault_id ${back ?? "(none)"}, not ${vaultId}`);
32454
32908
  }
32455
- if (accountId && parseAccountId(content) !== accountId) {
32456
- throw new Error(`${path9} does not record account: ${accountId} after the write`);
32457
- }
32458
32909
  }
32459
32910
  var MIRRORED_SKILL_DIRS = [
32460
32911
  [".claude", "skills"],
@@ -32796,7 +33247,6 @@ var HELP3 = [
32796
33247
 
32797
33248
  // spex-console/src/lib/cli/join.ts
32798
33249
  init_credentials();
32799
- import { homedir as homedir14 } from "node:os";
32800
33250
 
32801
33251
  // spex-console/src/lib/env-vault/crypto.ts
32802
33252
  import {
@@ -33113,17 +33563,17 @@ import {
33113
33563
  import {
33114
33564
  appendFileSync,
33115
33565
  closeSync,
33116
- existsSync as existsSync5,
33117
- mkdirSync as mkdirSync5,
33566
+ existsSync as existsSync6,
33567
+ mkdirSync as mkdirSync6,
33118
33568
  openSync,
33119
- readFileSync as readFileSync10,
33120
- renameSync as renameSync3,
33569
+ readFileSync as readFileSync11,
33570
+ renameSync as renameSync4,
33121
33571
  statSync,
33122
33572
  unlinkSync,
33123
- writeFileSync as writeFileSync5,
33573
+ writeFileSync as writeFileSync6,
33124
33574
  writeSync
33125
33575
  } from "node:fs";
33126
- import { join as join11 } from "node:path";
33576
+ import { join as join12 } from "node:path";
33127
33577
  var STORE_FILE = "env-tokens.json";
33128
33578
  var LOCK_FILE = "env-tokens.lock";
33129
33579
  var FILE_MODE = 384;
@@ -33159,15 +33609,15 @@ function unwrapPrivateKey(token, wrapSaltHex, wrapped) {
33159
33609
  return plaintext.toString("utf8");
33160
33610
  }
33161
33611
  function heyspexDir(vaultRoot) {
33162
- const dir = join11(vaultRoot, ".heyspex");
33163
- mkdirSync5(dir, { recursive: true });
33612
+ const dir = join12(vaultRoot, ".heyspex");
33613
+ mkdirSync6(dir, { recursive: true });
33164
33614
  return dir;
33165
33615
  }
33166
33616
  function memberTokenStorePath(vaultRoot) {
33167
- return join11(vaultRoot, ".heyspex", STORE_FILE);
33617
+ return join12(vaultRoot, ".heyspex", STORE_FILE);
33168
33618
  }
33169
33619
  function memberTokenStorePresent(vaultRoot) {
33170
- return existsSync5(memberTokenStorePath(vaultRoot));
33620
+ return existsSync6(memberTokenStorePath(vaultRoot));
33171
33621
  }
33172
33622
  function sleepSync(ms) {
33173
33623
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -33198,7 +33648,7 @@ function acquireLock(lockPath) {
33198
33648
  }
33199
33649
  }
33200
33650
  function withTokenLock(vaultRoot, fn) {
33201
- const lockPath = join11(heyspexDir(vaultRoot), LOCK_FILE);
33651
+ const lockPath = join12(heyspexDir(vaultRoot), LOCK_FILE);
33202
33652
  const fd = acquireLock(lockPath);
33203
33653
  try {
33204
33654
  return fn();
@@ -33215,19 +33665,19 @@ function withTokenLock(vaultRoot, fn) {
33215
33665
  }
33216
33666
  function readStore(vaultRoot) {
33217
33667
  const p = memberTokenStorePath(vaultRoot);
33218
- if (!existsSync5(p)) return [];
33668
+ if (!existsSync6(p)) return [];
33219
33669
  try {
33220
- const parsed = JSON.parse(readFileSync10(p, "utf8"));
33670
+ const parsed = JSON.parse(readFileSync11(p, "utf8"));
33221
33671
  return Array.isArray(parsed) ? parsed : [];
33222
33672
  } catch {
33223
33673
  return [];
33224
33674
  }
33225
33675
  }
33226
33676
  function writeStore(vaultRoot, records) {
33227
- const p = join11(heyspexDir(vaultRoot), STORE_FILE);
33677
+ const p = join12(heyspexDir(vaultRoot), STORE_FILE);
33228
33678
  const tmp = `${p}.tmp`;
33229
- writeFileSync5(tmp, JSON.stringify(records, null, 2) + "\n", { mode: FILE_MODE });
33230
- renameSync3(tmp, p);
33679
+ writeFileSync6(tmp, JSON.stringify(records, null, 2) + "\n", { mode: FILE_MODE });
33680
+ renameSync4(tmp, p);
33231
33681
  }
33232
33682
  function isLive(r, nowMs) {
33233
33683
  if (r.revokedAt) return false;
@@ -33282,23 +33732,23 @@ import { randomBytes as randomBytes3, scryptSync as scryptSync2, timingSafeEqual
33282
33732
  import {
33283
33733
  appendFileSync as appendFileSync2,
33284
33734
  closeSync as closeSync2,
33285
- existsSync as existsSync7,
33286
- mkdirSync as mkdirSync6,
33735
+ existsSync as existsSync8,
33736
+ mkdirSync as mkdirSync7,
33287
33737
  openSync as openSync2,
33288
- readFileSync as readFileSync11,
33289
- renameSync as renameSync4,
33738
+ readFileSync as readFileSync12,
33739
+ renameSync as renameSync5,
33290
33740
  statSync as statSync2,
33291
33741
  unlinkSync as unlinkSync2,
33292
- writeFileSync as writeFileSync6,
33742
+ writeFileSync as writeFileSync7,
33293
33743
  writeSync as writeSync2
33294
33744
  } from "node:fs";
33295
- import { join as join12 } from "node:path";
33745
+ import { join as join13 } from "node:path";
33296
33746
 
33297
33747
  // spex-console/src/lib/env-vault/sops.ts
33298
33748
  import { spawnSync as spawnSync3 } from "node:child_process";
33299
33749
  import { createHash as createHash5 } from "node:crypto";
33300
- import { existsSync as existsSync6 } from "node:fs";
33301
- import { homedir as homedir13 } from "node:os";
33750
+ import { existsSync as existsSync7 } from "node:fs";
33751
+ import { homedir as homedir10 } from "node:os";
33302
33752
  import path6 from "node:path";
33303
33753
  function ageRecipientFingerprint(recipient, length = 12) {
33304
33754
  return createHash5("sha256").update(recipient, "utf8").digest("hex").slice(0, length);
@@ -33332,14 +33782,14 @@ function candidateInvocations(tool) {
33332
33782
  "mise",
33333
33783
  "bin"
33334
33784
  );
33335
- const extra = existsSync6(miseBin) ? [miseBin] : [];
33785
+ const extra = existsSync7(miseBin) ? [miseBin] : [];
33336
33786
  candidates.push({
33337
33787
  command: path6.join(local, "mise", "shims", `${tool}.exe`),
33338
33788
  env: withExtendedPath(extra)
33339
33789
  });
33340
33790
  }
33341
33791
  candidates.push({
33342
- command: path6.join(homedir13(), ".local", "share", "mise", "shims", tool),
33792
+ command: path6.join(homedir10(), ".local", "share", "mise", "shims", tool),
33343
33793
  env: process.env
33344
33794
  });
33345
33795
  return candidates;
@@ -33647,9 +34097,9 @@ function formatCode(bytes) {
33647
34097
  function normalizeEnvPairingCode(raw) {
33648
34098
  const compact = raw.replace(/[\s-]+/g, "").toUpperCase();
33649
34099
  if (LEGACY_CANONICAL_CODE.test(compact)) return group(compact);
33650
- const mapped = compact.replace(/O/g, "0").replace(/[IL]/g, "1");
33651
- if (!CANONICAL_CODE.test(mapped)) return null;
33652
- return group(mapped);
34100
+ const mapped2 = compact.replace(/O/g, "0").replace(/[IL]/g, "1");
34101
+ if (!CANONICAL_CODE.test(mapped2)) return null;
34102
+ return group(mapped2);
33653
34103
  }
33654
34104
  var SCRYPT2 = { N: 1 << 14, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
33655
34105
  var SCRYPT_KEYLEN2 = 32;
@@ -33663,18 +34113,18 @@ function hashEquals2(a, b) {
33663
34113
  return timingSafeEqual2(bufA, bufB);
33664
34114
  }
33665
34115
  function heyspexDir2(vaultRoot) {
33666
- const dir = join12(vaultRoot, ".heyspex");
33667
- mkdirSync6(dir, { recursive: true });
34116
+ const dir = join13(vaultRoot, ".heyspex");
34117
+ mkdirSync7(dir, { recursive: true });
33668
34118
  return dir;
33669
34119
  }
33670
34120
  function envPairingStorePath(vaultRoot) {
33671
- return join12(vaultRoot, ".heyspex", STORE_FILE2);
34121
+ return join13(vaultRoot, ".heyspex", STORE_FILE2);
33672
34122
  }
33673
34123
  function envPairingStorePresent(vaultRoot) {
33674
- return existsSync7(envPairingStorePath(vaultRoot));
34124
+ return existsSync8(envPairingStorePath(vaultRoot));
33675
34125
  }
33676
34126
  function auditPath(vaultRoot) {
33677
- return join12(heyspexDir2(vaultRoot), AUDIT_FILE);
34127
+ return join13(heyspexDir2(vaultRoot), AUDIT_FILE);
33678
34128
  }
33679
34129
  function sleepSync2(ms) {
33680
34130
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
@@ -33705,7 +34155,7 @@ function acquireLock2(lockPath) {
33705
34155
  }
33706
34156
  }
33707
34157
  function withPairingLock(vaultRoot, fn) {
33708
- const lockPath = join12(heyspexDir2(vaultRoot), LOCK_FILE2);
34158
+ const lockPath = join13(heyspexDir2(vaultRoot), LOCK_FILE2);
33709
34159
  const fd = acquireLock2(lockPath);
33710
34160
  try {
33711
34161
  return fn();
@@ -33722,9 +34172,9 @@ function withPairingLock(vaultRoot, fn) {
33722
34172
  }
33723
34173
  function readStore2(vaultRoot) {
33724
34174
  const p = envPairingStorePath(vaultRoot);
33725
- if (!existsSync7(p)) return [];
34175
+ if (!existsSync8(p)) return [];
33726
34176
  try {
33727
- const parsed = JSON.parse(readFileSync11(p, "utf8"));
34177
+ const parsed = JSON.parse(readFileSync12(p, "utf8"));
33728
34178
  return Array.isArray(parsed) ? parsed : [];
33729
34179
  } catch {
33730
34180
  return [];
@@ -33738,10 +34188,10 @@ function prune(records) {
33738
34188
  });
33739
34189
  }
33740
34190
  function writeStore2(vaultRoot, records) {
33741
- const p = join12(heyspexDir2(vaultRoot), STORE_FILE2);
34191
+ const p = join13(heyspexDir2(vaultRoot), STORE_FILE2);
33742
34192
  const tmp = `${p}.tmp`;
33743
- writeFileSync6(tmp, JSON.stringify(prune(records), null, 2) + "\n", { mode: FILE_MODE2 });
33744
- renameSync4(tmp, p);
34193
+ writeFileSync7(tmp, JSON.stringify(prune(records), null, 2) + "\n", { mode: FILE_MODE2 });
34194
+ renameSync5(tmp, p);
33745
34195
  }
33746
34196
  function appendEnvPairingAudit(vaultRoot, event) {
33747
34197
  const line = {
@@ -33755,18 +34205,18 @@ function appendEnvPairingAudit(vaultRoot, event) {
33755
34205
  appendFileSync2(auditPath(vaultRoot), JSON.stringify(line) + "\n", { mode: FILE_MODE2 });
33756
34206
  }
33757
34207
  function strikesPath(vaultRoot) {
33758
- return join12(heyspexDir2(vaultRoot), STRIKES_FILE);
34208
+ return join13(heyspexDir2(vaultRoot), STRIKES_FILE);
33759
34209
  }
33760
34210
  function readStrikes(vaultRoot) {
33761
34211
  try {
33762
- const parsed = JSON.parse(readFileSync11(strikesPath(vaultRoot), "utf8"));
34212
+ const parsed = JSON.parse(readFileSync12(strikesPath(vaultRoot), "utf8"));
33763
34213
  return typeof parsed.count === "number" && Number.isFinite(parsed.count) ? Math.max(0, Math.floor(parsed.count)) : 0;
33764
34214
  } catch {
33765
34215
  return 0;
33766
34216
  }
33767
34217
  }
33768
34218
  function writeStrikes(vaultRoot, count) {
33769
- writeFileSync6(
34219
+ writeFileSync7(
33770
34220
  strikesPath(vaultRoot),
33771
34221
  JSON.stringify({ count, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }) + "\n",
33772
34222
  { mode: FILE_MODE2 }
@@ -33975,7 +34425,7 @@ function markEnvInviteApplied(vaultRoot, id) {
33975
34425
  }
33976
34426
 
33977
34427
  // spex-console/src/lib/env-vault/policy.ts
33978
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync12, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "node:fs";
34428
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "node:fs";
33979
34429
  import path7 from "node:path";
33980
34430
  var POLICY_REL_PATH = ".heyspex/env/policy.json";
33981
34431
  var POLICY_VERSION = 1;
@@ -34008,7 +34458,7 @@ function readPolicy(vaultRoot, profile) {
34008
34458
  const file = policyPath(vaultRoot);
34009
34459
  let text2;
34010
34460
  try {
34011
- text2 = readFileSync12(file, "utf-8");
34461
+ text2 = readFileSync13(file, "utf-8");
34012
34462
  } catch {
34013
34463
  return { mode: "allow", source: "default" };
34014
34464
  }
@@ -34028,8 +34478,8 @@ function serializePolicyFile(file) {
34028
34478
  }
34029
34479
  function loadForWrite(vaultRoot) {
34030
34480
  const file = policyPath(vaultRoot);
34031
- if (!existsSync8(file)) return { version: POLICY_VERSION, profiles: {} };
34032
- const parsed = parsePolicyFile(readFileSync12(file, "utf-8"));
34481
+ if (!existsSync9(file)) return { version: POLICY_VERSION, profiles: {} };
34482
+ const parsed = parsePolicyFile(readFileSync13(file, "utf-8"));
34033
34483
  if (parsed === null) {
34034
34484
  throw new Error(
34035
34485
  `${POLICY_REL_PATH} exists but is not a valid v${POLICY_VERSION} policy file \u2014 fix or remove it before changing policy (refusing to overwrite what it meant to say).`
@@ -34039,10 +34489,10 @@ function loadForWrite(vaultRoot) {
34039
34489
  }
34040
34490
  function writePolicyFile(vaultRoot, file) {
34041
34491
  const target = policyPath(vaultRoot);
34042
- mkdirSync7(path7.dirname(target), { recursive: true });
34492
+ mkdirSync8(path7.dirname(target), { recursive: true });
34043
34493
  const tmp = `${target}.tmp`;
34044
- writeFileSync7(tmp, serializePolicyFile(file), "utf-8");
34045
- renameSync5(tmp, target);
34494
+ writeFileSync8(tmp, serializePolicyFile(file), "utf-8");
34495
+ renameSync6(tmp, target);
34046
34496
  }
34047
34497
  function writePolicy(vaultRoot, profile, mode) {
34048
34498
  if (!isPolicyMode(mode)) {
@@ -34054,7 +34504,7 @@ function writePolicy(vaultRoot, profile, mode) {
34054
34504
  }
34055
34505
  function clearPolicy(vaultRoot, profile) {
34056
34506
  const file = policyPath(vaultRoot);
34057
- if (!existsSync8(file)) return;
34507
+ if (!existsSync9(file)) return;
34058
34508
  const parsed = loadForWrite(vaultRoot);
34059
34509
  if (!(profile in parsed.profiles)) return;
34060
34510
  delete parsed.profiles[profile];
@@ -34645,7 +35095,7 @@ function readJoinManifest(deps, repoRoot, profile) {
34645
35095
  }
34646
35096
  }
34647
35097
  function hasLoginCredentials(deps) {
34648
- const raw = deps.readFile(deps.joinPath(homedir14(), ".heyspex", "credentials.yaml"));
35098
+ const raw = deps.readFile(deps.joinPath(heyspexHome(), ".heyspex", "credentials.yaml"));
34649
35099
  return parseMcpToken(raw) !== null;
34650
35100
  }
34651
35101
  function repoTargetName(repo) {
@@ -35661,16 +36111,16 @@ import { spawnSync as spawnSync4 } from "node:child_process";
35661
36111
  import { randomBytes as randomBytes5 } from "node:crypto";
35662
36112
  import {
35663
36113
  chmodSync,
35664
- existsSync as existsSync9,
35665
- mkdirSync as mkdirSync8,
35666
- readdirSync as readdirSync2,
35667
- readFileSync as readFileSync13,
35668
- renameSync as renameSync6,
36114
+ existsSync as existsSync10,
36115
+ mkdirSync as mkdirSync9,
36116
+ readdirSync as readdirSync3,
36117
+ readFileSync as readFileSync14,
36118
+ renameSync as renameSync7,
35669
36119
  statSync as statSync3,
35670
36120
  unlinkSync as unlinkSync3,
35671
- writeFileSync as writeFileSync8
36121
+ writeFileSync as writeFileSync9
35672
36122
  } from "node:fs";
35673
- import { homedir as homedir16 } from "node:os";
36123
+ import { homedir as homedir12 } from "node:os";
35674
36124
  import path8 from "node:path";
35675
36125
 
35676
36126
  // node_modules/sops-age/dist/index.js
@@ -40592,7 +41042,7 @@ function alphabet(letters) {
40592
41042
  };
40593
41043
  }
40594
41044
  // @__NO_SIDE_EFFECTS__
40595
- function join13(separator = "") {
41045
+ function join14(separator = "") {
40596
41046
  astr("join", separator);
40597
41047
  return {
40598
41048
  encode: (from) => {
@@ -40722,9 +41172,9 @@ var base64 = hasBase64Builtin ? {
40722
41172
  decode(s) {
40723
41173
  return decodeBase64Builtin(s, false);
40724
41174
  }
40725
- } : /* @__PURE__ */ chain(/* @__PURE__ */ radix2(6), /* @__PURE__ */ alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), /* @__PURE__ */ padding(6), /* @__PURE__ */ join13(""));
40726
- var base64nopad = /* @__PURE__ */ chain(/* @__PURE__ */ radix2(6), /* @__PURE__ */ alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), /* @__PURE__ */ join13(""));
40727
- var BECH_ALPHABET = /* @__PURE__ */ chain(/* @__PURE__ */ alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), /* @__PURE__ */ join13(""));
41175
+ } : /* @__PURE__ */ chain(/* @__PURE__ */ radix2(6), /* @__PURE__ */ alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), /* @__PURE__ */ padding(6), /* @__PURE__ */ join14(""));
41176
+ var base64nopad = /* @__PURE__ */ chain(/* @__PURE__ */ radix2(6), /* @__PURE__ */ alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), /* @__PURE__ */ join14(""));
41177
+ var BECH_ALPHABET = /* @__PURE__ */ chain(/* @__PURE__ */ alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), /* @__PURE__ */ join14(""));
40728
41178
  var POLYMOD_GENERATORS = [996825010, 642813549, 513874426, 1027748829, 705979059];
40729
41179
  function bech32Polymod(pre) {
40730
41180
  const b = pre >> 25;
@@ -44340,8 +44790,8 @@ var gcm = /* @__PURE__ */ wrapCipher({ blockSize: 16, nonceLength: 12, tagLength
44340
44790
 
44341
44791
  // node_modules/sops-age/dist/index.js
44342
44792
  import { readFile } from "node:fs/promises";
44343
- import { join as join14 } from "node:path";
44344
- import { homedir as homedir15, platform } from "node:os";
44793
+ import { join as join15 } from "node:path";
44794
+ import { homedir as homedir11, platform } from "node:os";
44345
44795
  import { exec } from "node:child_process";
44346
44796
  import { promisify } from "node:util";
44347
44797
  var import_sshpk = __toESM(require_lib2(), 1);
@@ -44682,11 +45132,11 @@ async function getUserConfigDir() {
44682
45132
  if (!appData) throw new Error("APPDATA env var not set");
44683
45133
  return appData;
44684
45134
  case "darwin":
44685
- return join14(homedir15(), "Library", "Application Support");
45135
+ return join15(homedir11(), "Library", "Application Support");
44686
45136
  default:
44687
45137
  const xdgConfigHome = process.env.XDG_CONFIG_HOME;
44688
45138
  if (xdgConfigHome && xdgConfigHome.trim() !== "") return xdgConfigHome;
44689
- return join14(homedir15(), ".config");
45139
+ return join15(homedir11(), ".config");
44690
45140
  }
44691
45141
  }
44692
45142
  async function findAllAgeKeys() {
@@ -44707,11 +45157,11 @@ async function findAllAgeKeys() {
44707
45157
  );
44708
45158
  }
44709
45159
  } else {
44710
- const userHomeDir = homedir15();
45160
+ const userHomeDir = homedir11();
44711
45161
  if (userHomeDir) {
44712
45162
  const defaultSshPaths = [
44713
- join14(userHomeDir, ".ssh", "id_ed25519"),
44714
- join14(userHomeDir, ".ssh", "id_rsa")
45163
+ join15(userHomeDir, ".ssh", "id_ed25519"),
45164
+ join15(userHomeDir, ".ssh", "id_rsa")
44715
45165
  ];
44716
45166
  for (const sshPath of defaultSshPaths) {
44717
45167
  if (foundKeySource && convertedSshKeys.length > 0) {
@@ -44788,7 +45238,7 @@ async function findAllAgeKeys() {
44788
45238
  }
44789
45239
  }
44790
45240
  if (userConfigDirPath) {
44791
- const sopsKeysFilePath = join14(
45241
+ const sopsKeysFilePath = join15(
44792
45242
  userConfigDirPath,
44793
45243
  SOPS_AGE_KEY_USER_CONFIG_PATH
44794
45244
  );
@@ -45281,14 +45731,14 @@ function keysPath(vaultRoot) {
45281
45731
  return path8.join(vaultRoot, ".heyspex", "env.keys");
45282
45732
  }
45283
45733
  function homeKeysPath() {
45284
- return path8.join(process.env.HEYSPEX_ENV_KEYS_HOME || homedir16(), ".heyspex", "env.keys");
45734
+ return path8.join(process.env.HEYSPEX_ENV_KEYS_HOME || homedir12(), ".heyspex", "env.keys");
45285
45735
  }
45286
45736
  function dotenvPath(vaultRoot) {
45287
45737
  return path8.join(vaultRoot, ".env");
45288
45738
  }
45289
45739
  function readOrNull(file) {
45290
45740
  try {
45291
- return readFileSync13(file, "utf-8");
45741
+ return readFileSync14(file, "utf-8");
45292
45742
  } catch {
45293
45743
  return null;
45294
45744
  }
@@ -45302,7 +45752,7 @@ function renameWithRetry(tmp, file) {
45302
45752
  const retryable = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
45303
45753
  for (let attempt = 0; ; attempt++) {
45304
45754
  try {
45305
- renameSync6(tmp, file);
45755
+ renameSync7(tmp, file);
45306
45756
  return;
45307
45757
  } catch (err) {
45308
45758
  const code = err.code ?? "";
@@ -45320,14 +45770,14 @@ function unlinkQuiet(file) {
45320
45770
  function atomicWrite(file, content, mode = 420) {
45321
45771
  const secret = mode === SECRET_MODE;
45322
45772
  const dir = path8.dirname(file);
45323
- mkdirSync8(dir, { recursive: true, ...secret ? { mode: SECRET_DIR_MODE } : {} });
45773
+ mkdirSync9(dir, { recursive: true, ...secret ? { mode: SECRET_DIR_MODE } : {} });
45324
45774
  const tmp = path8.join(
45325
45775
  dir,
45326
45776
  `.${path8.basename(file)}.${process.pid}.${randomBytes5(4).toString("hex")}.tmp`
45327
45777
  );
45328
45778
  let renamed = false;
45329
45779
  try {
45330
- writeFileSync8(tmp, content, { encoding: "utf-8", mode });
45780
+ writeFileSync9(tmp, content, { encoding: "utf-8", mode });
45331
45781
  if (secret) chmodOwnerOnly(tmp);
45332
45782
  renameWithRetry(tmp, file);
45333
45783
  renamed = true;
@@ -45341,11 +45791,11 @@ var LOCK_RETRY_MS3 = 50;
45341
45791
  var LOCK_STALE_MS3 = 5e3;
45342
45792
  function withFileLock(file, fn) {
45343
45793
  const lock = `${file}.lock`;
45344
- mkdirSync8(path8.dirname(file), { recursive: true, mode: SECRET_DIR_MODE });
45794
+ mkdirSync9(path8.dirname(file), { recursive: true, mode: SECRET_DIR_MODE });
45345
45795
  let held = false;
45346
45796
  for (let attempt = 0; attempt < LOCK_RETRIES && !held; attempt++) {
45347
45797
  try {
45348
- writeFileSync8(lock, `${process.pid}
45798
+ writeFileSync9(lock, `${process.pid}
45349
45799
  `, { flag: "wx" });
45350
45800
  held = true;
45351
45801
  } catch (err) {
@@ -45537,7 +45987,7 @@ function legacyDecryptAllSync(vaultRoot, name) {
45537
45987
  }
45538
45988
  function initProfile(vaultRoot, name = DEFAULT_PROFILE2, opts = {}) {
45539
45989
  const pFile = profilePath(vaultRoot, name);
45540
- if (existsSync9(pFile) && !opts.force) {
45990
+ if (existsSync10(pFile) && !opts.force) {
45541
45991
  throw new EnvVaultConfigError(
45542
45992
  `Env profile "${name}" already exists at ${pFile}. Pass --force to re-key it (this discards existing values and mints a new keypair).`
45543
45993
  );
@@ -45583,7 +46033,7 @@ function listProfiles(vaultRoot) {
45583
46033
  } catch {
45584
46034
  return [];
45585
46035
  }
45586
- return readdirSync2(dir).filter((f) => f.endsWith(".env")).map((f) => f.slice(0, -".env".length)).sort();
46036
+ return readdirSync3(dir).filter((f) => f.endsWith(".env")).map((f) => f.slice(0, -".env".length)).sort();
45587
46037
  }
45588
46038
  function listProfilesStatus(vaultRoot) {
45589
46039
  return listProfiles(vaultRoot).map((name) => {
@@ -45923,7 +46373,7 @@ function removeGrant(vaultRoot, profileName, name, opts = {}) {
45923
46373
  }
45924
46374
  async function exportProfile(vaultRoot, name = DEFAULT_PROFILE2, opts = {}) {
45925
46375
  const target = dotenvPath(vaultRoot);
45926
- if (existsSync9(target) && !opts.force) {
46376
+ if (existsSync10(target) && !opts.force) {
45927
46377
  throw new EnvVaultConfigError(
45928
46378
  `${target} already exists. Pass --force to overwrite it (it is disposable \u2014 the encrypted profile is the system of record).`
45929
46379
  );
@@ -45952,7 +46402,7 @@ function writeTextFile(vaultRoot, filePath, content) {
45952
46402
  atomicWrite(resolveVaultPath(vaultRoot, filePath), content, SECRET_MODE);
45953
46403
  }
45954
46404
  function pathExists(vaultRoot, filePath) {
45955
- return existsSync9(resolveVaultPath(vaultRoot, filePath));
46405
+ return existsSync10(resolveVaultPath(vaultRoot, filePath));
45956
46406
  }
45957
46407
  function fileMtime(vaultRoot, filePath) {
45958
46408
  const resolved = resolveVaultPath(vaultRoot, filePath);
@@ -45979,7 +46429,7 @@ function listRepoSourceFiles(vaultRoot) {
45979
46429
  const walk = (dir, rel) => {
45980
46430
  let entries;
45981
46431
  try {
45982
- entries = readdirSync2(dir, { withFileTypes: true });
46432
+ entries = readdirSync3(dir, { withFileTypes: true });
45983
46433
  } catch {
45984
46434
  return;
45985
46435
  }
@@ -46134,7 +46584,7 @@ async function confirm(question) {
46134
46584
  function findRepoRoot(start) {
46135
46585
  let dir = resolve5(start);
46136
46586
  for (; ; ) {
46137
- if (existsSync10(join15(dir, ".git"))) return dir;
46587
+ if (existsSync11(join16(dir, ".git"))) return dir;
46138
46588
  const parent = dirname6(dir);
46139
46589
  if (parent === dir) return null;
46140
46590
  dir = parent;
@@ -46142,7 +46592,7 @@ function findRepoRoot(start) {
46142
46592
  }
46143
46593
  function readFile2(p) {
46144
46594
  try {
46145
- return readFileSync14(p, "utf-8");
46595
+ return readFileSync15(p, "utf-8");
46146
46596
  } catch {
46147
46597
  return null;
46148
46598
  }
@@ -46150,12 +46600,12 @@ function readFile2(p) {
46150
46600
  function connectCliDeps(repoRoot) {
46151
46601
  return {
46152
46602
  repoRoot,
46153
- fileExists: (p) => existsSync10(p),
46603
+ fileExists: (p) => existsSync11(p),
46154
46604
  readFile: readFile2,
46155
- writeFile: (p, c) => writeFileSync9(p, c, "utf-8"),
46156
- mkdir: (p) => mkdirSync9(p, { recursive: true }),
46157
- joinPath: (...s) => join15(...s),
46158
- basename: (p) => basename3(p),
46605
+ writeFile: (p, c) => writeFileSync10(p, c, "utf-8"),
46606
+ mkdir: (p) => mkdirSync10(p, { recursive: true }),
46607
+ joinPath: (...s) => join16(...s),
46608
+ basename: (p) => basename4(p),
46159
46609
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
46160
46610
  spawn: (command, spawnArgs, opts) => {
46161
46611
  const res = spawnSync5(command, spawnArgs, {
@@ -46204,7 +46654,7 @@ async function main() {
46204
46654
  const launchCwd = process4.env.HS_JOIN_CWD ? resolve5(process4.env.HS_JOIN_CWD) : process4.cwd();
46205
46655
  const walked = findRepoRoot(launchCwd);
46206
46656
  const rawHint = walked ?? (process4.env.HS_VAULT_ROOT ? resolve5(process4.env.HS_VAULT_ROOT) : null);
46207
- const repoRootHint = rawHint && existsSync10(join15(rawHint, "package.json")) ? rawHint : null;
46657
+ const repoRootHint = rawHint && existsSync11(join16(rawHint, "package.json")) ? rawHint : null;
46208
46658
  let userName = null;
46209
46659
  try {
46210
46660
  userName = userInfo().username || null;
@@ -46217,9 +46667,9 @@ async function main() {
46217
46667
  platform: process4.platform,
46218
46668
  interactive: INTERACTIVE,
46219
46669
  userName,
46220
- pathExists: (p) => existsSync10(p),
46670
+ pathExists: (p) => existsSync11(p),
46221
46671
  readFile: readFile2,
46222
- joinPath: (...s) => join15(...s),
46672
+ joinPath: (...s) => join16(...s),
46223
46673
  resolvePath: (...s) => resolve5(...s),
46224
46674
  commandExists,
46225
46675
  // npm/mise/git resolve through the shell on Windows (.cmd shims).
@@ -46266,9 +46716,9 @@ async function main() {
46266
46716
  connect: {
46267
46717
  isConnected: (repoRoot) => Promise.resolve(
46268
46718
  runConnectChecks(repoRoot, {
46269
- fileExists: (p) => existsSync10(p),
46719
+ fileExists: (p) => existsSync11(p),
46270
46720
  readFile: readFile2,
46271
- joinPath: (...s) => join15(...s)
46721
+ joinPath: (...s) => join16(...s)
46272
46722
  }).connected
46273
46723
  ),
46274
46724
  findUnconnectedVaultForRemote: async (githubRemote) => {