heyspex 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +181 -15
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +177 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +21 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +312 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/install-copilot-hooks.mjs +288 -0
  72. package/scripts/lib/console-url-resolver.mjs +3 -2
  73. package/scripts/lib/customer-kit.mjs +327 -10
  74. package/scripts/lib/dev-mode.mjs +7 -6
  75. package/scripts/lib/directive-obligations.mjs +349 -0
  76. package/scripts/lib/hook-dispositions.mjs +210 -0
  77. package/scripts/lib/hook-vault-header.mjs +30 -0
  78. package/scripts/lib/hs-source-branch.mjs +196 -0
  79. package/scripts/lib/hs-source-log.mjs +2 -1
  80. package/scripts/lib/hs-source-preflight.mjs +18 -4
  81. package/scripts/lib/hs-source-push.mjs +386 -356
  82. package/scripts/lib/window-placement.mjs +109 -0
  83. package/spexharness/src/adapters/claude-code.mjs +11 -3
  84. package/spexharness/src/adapters/copilot.mjs +236 -0
  85. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  86. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  87. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  88. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  89. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  90. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  91. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  93. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  94. package/spexharness/src/intent-classifier.mjs +4 -2
  95. package/spexharness/src/intent-directive.mjs +626 -541
  96. package/spexharness/src/rule-table.mjs +31 -1
  97. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  98. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  99. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  100. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  101. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  102. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  103. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  104. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  105. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  106. package/vendor/heyspex-mcp/cli.mjs +7 -1
  107. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  108. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  109. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  110. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  111. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  112. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  113. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  114. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  115. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  116. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  117. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  118. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  119. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  120. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  121. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  122. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  123. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  124. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  125. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  126. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  127. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  128. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  129. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  130. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  131. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  132. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  133. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  134. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  135. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  136. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  137. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  138. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  139. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  140. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  141. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  142. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  143. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  144. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  145. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  146. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -35,11 +35,26 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
35
35
  mod
36
36
  ));
37
37
 
38
+ // scripts/lib/heyspex-home.mjs
39
+ import { homedir } from "node:os";
40
+ function heyspexHome(env = process.env) {
41
+ const override = env?.HEYSPEX_HOME;
42
+ if (typeof override === "string" && override.trim().length > 0) {
43
+ return override.trim();
44
+ }
45
+ return homedir();
46
+ }
47
+ var init_heyspex_home = __esm({
48
+ "scripts/lib/heyspex-home.mjs"() {
49
+ "use strict";
50
+ }
51
+ });
52
+
38
53
  // vendor/heyspex-mcp/credentials.mjs
39
54
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
40
- import os, { homedir } from "node:os";
55
+ import os, { homedir as homedir2 } from "node:os";
41
56
  import { dirname, join, resolve } from "node:path";
42
- function credentialsFilePath(homeDir = homedir()) {
57
+ function credentialsFilePath(homeDir = heyspexHome()) {
43
58
  return join(homeDir, ".heyspex", "credentials.yaml");
44
59
  }
45
60
  function parseMcpToken(raw) {
@@ -152,14 +167,14 @@ function readLegacyConfigAccountId(read, homeDir) {
152
167
  const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
153
168
  return value || null;
154
169
  }
155
- function readBearer({ env = process.env, homeDir = homedir(), readFile: readFile2, accountId } = {}) {
170
+ function readBearer({ env = process.env, homeDir = heyspexHome(), readFile: readFile2, accountId } = {}) {
156
171
  const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
157
172
  const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
158
173
  const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
159
174
  const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
160
175
  return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
161
176
  }
162
- function readCurrentAccountId({ homeDir = homedir(), readFile: readFile2 } = {}) {
177
+ function readCurrentAccountId({ homeDir = heyspexHome(), readFile: readFile2 } = {}) {
163
178
  const read = readFile2 ?? ((p) => readFileSync(p, "utf8"));
164
179
  const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
165
180
  if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
@@ -177,6 +192,7 @@ var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT
177
192
  var init_credentials = __esm({
178
193
  "vendor/heyspex-mcp/credentials.mjs"() {
179
194
  "use strict";
195
+ init_heyspex_home();
180
196
  MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
181
197
  MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
182
198
  CREDENTIALS_SCHEMA_VERSION = 2;
@@ -189,9 +205,9 @@ var init_credentials = __esm({
189
205
 
190
206
  // scripts/lib/dev-mode.mjs
191
207
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
192
- import { homedir as homedir2 } from "node:os";
208
+ import { homedir as homedir3 } from "node:os";
193
209
  import { join as join2 } from "node:path";
194
- function devModeFilePath(homeDir = homedir2()) {
210
+ function devModeFilePath(homeDir = heyspexHome()) {
195
211
  return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
196
212
  }
197
213
  function normalizeMode(raw) {
@@ -216,7 +232,7 @@ function deriveSemantics(mode, reliance) {
216
232
  vaultWriteMode: local ? "local" : "cloud"
217
233
  };
218
234
  }
219
- function readLocalDevModeRecord({ homeDir = homedir2(), readFile: readFile2 = defaultReadFile } = {}) {
235
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile } = {}) {
220
236
  let raw;
221
237
  try {
222
238
  raw = readFile2(devModeFilePath(homeDir));
@@ -246,7 +262,7 @@ function readLocalDevModeRecord({ homeDir = homedir2(), readFile: readFile2 = de
246
262
  }
247
263
  function resolveDevMode({
248
264
  env = process.env,
249
- homeDir = homedir2(),
265
+ homeDir = heyspexHome(),
250
266
  readFile: readFile2 = defaultReadFile,
251
267
  cloudSetting = void 0
252
268
  } = {}) {
@@ -309,6 +325,7 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
309
325
  var init_dev_mode = __esm({
310
326
  "scripts/lib/dev-mode.mjs"() {
311
327
  "use strict";
328
+ init_heyspex_home();
312
329
  init_credentials();
313
330
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
314
331
  LOCAL_CONSOLE_URL = "http://localhost:3000";
@@ -982,21 +999,22 @@ var require_gray_matter = __commonJS({
982
999
  import { spawnSync as spawnSync3 } from "node:child_process";
983
1000
  import {
984
1001
  copyFileSync,
985
- existsSync as existsSync5,
986
- mkdirSync as mkdirSync5,
987
- readdirSync as readdirSync2,
988
- readFileSync as readFileSync10,
1002
+ existsSync as existsSync6,
1003
+ mkdirSync as mkdirSync6,
1004
+ readdirSync as readdirSync3,
1005
+ readFileSync as readFileSync11,
989
1006
  statSync,
990
- writeFileSync as writeFileSync5
1007
+ writeFileSync as writeFileSync6
991
1008
  } from "node:fs";
992
1009
  import path6 from "node:path";
993
1010
  import process4 from "node:process";
994
1011
  import { fileURLToPath as fileURLToPath4 } from "node:url";
995
1012
 
996
1013
  // scripts/lib/console-url-resolver.mjs
1014
+ init_heyspex_home();
997
1015
  init_dev_mode();
998
1016
  import { readFileSync as readFileSync3 } from "node:fs";
999
- import { homedir as homedir3 } from "node:os";
1017
+ import { homedir as homedir4 } from "node:os";
1000
1018
  import { join as join3 } from "node:path";
1001
1019
  var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
1002
1020
  function trimTrailingSlash(url) {
@@ -1023,7 +1041,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
1023
1041
  } catch {
1024
1042
  }
1025
1043
  }
1026
- function readCredentialsConsoleUrl({ homeDir = homedir3(), readFile: readFile2 = defaultReadFile2 } = {}) {
1044
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile: readFile2 = defaultReadFile2 } = {}) {
1027
1045
  try {
1028
1046
  const raw = readFile2(join3(homeDir, ".heyspex", "credentials.yaml"));
1029
1047
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
@@ -1067,7 +1085,7 @@ function resolveModeEnvelope({ env, homeDir }) {
1067
1085
  function resolveConsoleUrl({
1068
1086
  repoRoot,
1069
1087
  env = process.env,
1070
- homeDir = homedir3(),
1088
+ homeDir = heyspexHome(),
1071
1089
  strict = false,
1072
1090
  devMode,
1073
1091
  log = (line) => process.stderr.write(line),
@@ -1116,7 +1134,7 @@ function resolveCodeRoot({
1116
1134
  }
1117
1135
 
1118
1136
  // spex-console/src/lib/cli/connect.ts
1119
- import { readdirSync } from "node:fs";
1137
+ import { readdirSync as readdirSync2 } from "node:fs";
1120
1138
 
1121
1139
  // dist/cli/._sql-orm-inert.cli-shim.mjs
1122
1140
  var inert = new Proxy(function inert2() {
@@ -1153,14 +1171,24 @@ var uniqueIndex = inert;
1153
1171
 
1154
1172
  // spex-console/src/lib/cli/vault-workspace-shared.ts
1155
1173
  import { readFileSync as readFileSync5 } from "node:fs";
1156
- import os2 from "node:os";
1174
+
1175
+ // spex-console/src/lib/heyspex-home.ts
1176
+ import { homedir as homedir5 } from "node:os";
1177
+ function heyspexHome2(env = process.env) {
1178
+ const override = env?.HEYSPEX_HOME;
1179
+ if (typeof override === "string" && override.trim().length > 0) {
1180
+ return override.trim();
1181
+ }
1182
+ return homedir5();
1183
+ }
1157
1184
 
1158
1185
  // vendor/heyspex-mcp/session-owner.mjs
1186
+ init_heyspex_home();
1159
1187
  init_credentials();
1160
1188
  import { readFileSync as readFileSync4 } from "node:fs";
1161
- import { homedir as homedir4 } from "node:os";
1189
+ import { homedir as homedir6 } from "node:os";
1162
1190
  import { join as join4 } from "node:path";
1163
- function resolveSessionOwnerUncached({ env = process.env, homeDir = homedir4(), readFile: readFile2 } = {}) {
1191
+ function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile: readFile2 } = {}) {
1164
1192
  const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
1165
1193
  if (fromEnv) return fromEnv;
1166
1194
  return readCurrentAccountId({ homeDir, readFile: readFile2 });
@@ -1189,7 +1217,7 @@ function resolveOfflineDefaultWorkspace(accountId, localWorkspaceIds) {
1189
1217
  function resolveConfiguredAccountId(opts = {}) {
1190
1218
  return resolveSessionOwnerUncached({
1191
1219
  env: opts.env ?? process.env,
1192
- homeDir: opts.homeDir ?? os2.homedir(),
1220
+ homeDir: opts.homeDir ?? heyspexHome2(),
1193
1221
  readFile: opts.readFile ?? ((p) => readFileSync5(p, "utf-8"))
1194
1222
  });
1195
1223
  }
@@ -1741,7 +1769,13 @@ var specEventKindEnum = pgEnum("spec_event_kind", [
1741
1769
  "priority_change",
1742
1770
  "parent_change",
1743
1771
  "claim_acquired",
1744
- "claim_released"
1772
+ "claim_released",
1773
+ // Collaborator write audit (HS-FWB6, migration 0077).
1774
+ "spec_created",
1775
+ "spec_edited",
1776
+ "spec_deleted",
1777
+ "capture_written",
1778
+ "wip_recorded"
1745
1779
  ]);
1746
1780
  var focusPin = pgTable(
1747
1781
  "focus_pin",
@@ -2934,11 +2968,43 @@ var specSettlements = pgTable(
2934
2968
  // `owner/name`
2935
2969
  branch: text("branch").notNull(),
2936
2970
  commitSha: text("commit_sha").notNull(),
2937
- settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`)
2971
+ settledAt: timestamp("settled_at", { withTimezone: true }).notNull().default(sql`now()`),
2972
+ // The pull request that carries this settlement
2973
+ // ([[HS-9D25-CWPX-EPIC-branch-lifecycle-open-a-pull-request]]). NULL until
2974
+ // the branch has gone quiet and a PR was opened or found; a write made
2975
+ // after its PR merged stays NULL, which is what triggers the next PR.
2976
+ pullRequestNumber: integer("pull_request_number"),
2977
+ pullRequestUrl: text("pull_request_url")
2938
2978
  },
2939
2979
  (table) => [
2940
2980
  // "latest settlement for this spec" — the read path's only query.
2941
- index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc())
2981
+ index("spec_settlements_spec_settled_idx").on(table.specId, table.settledAt.desc()),
2982
+ // The PR sweep's only query: branches with settlements not yet in a PR.
2983
+ index("spec_settlements_unproposed_idx").on(table.vaultId, table.repo, table.branch).where(sql`${table.pullRequestNumber} IS NULL`)
2984
+ ]
2985
+ );
2986
+ var sessionBranchDeclarations = pgTable(
2987
+ "session_branch_declarations",
2988
+ {
2989
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
2990
+ vaultId: text("vault_id").notNull().default(DEFAULT_VAULT_ID),
2991
+ actorKey: text("actor_key").notNull(),
2992
+ sessionId: text("session_id").notNull(),
2993
+ branch: text("branch").notNull(),
2994
+ declaredAt: timestamp("declared_at", { withTimezone: true }).notNull().default(sql`now()`),
2995
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
2996
+ },
2997
+ (table) => [
2998
+ uniqueIndex("session_branch_declarations_uq").on(
2999
+ table.vaultId,
3000
+ table.actorKey,
3001
+ table.sessionId
3002
+ ),
3003
+ index("session_branch_declarations_lookup_idx").on(
3004
+ table.vaultId,
3005
+ table.actorKey,
3006
+ table.declaredAt.desc()
3007
+ )
2942
3008
  ]
2943
3009
  );
2944
3010
 
@@ -3031,7 +3097,6 @@ async function runNeonReachabilityProbe(attempt, options2 = {}, label = "Neon re
3031
3097
 
3032
3098
  // spex-console/src/lib/vault-registry/registry-file.ts
3033
3099
  import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
3034
- import { homedir as homedir5 } from "node:os";
3035
3100
  import { dirname as dirname2, join as join5, resolve as resolve3 } from "node:path";
3036
3101
 
3037
3102
  // spex-console/src/lib/vault-registry/types.ts
@@ -3049,7 +3114,7 @@ function emptyVaultRegistry() {
3049
3114
 
3050
3115
  // spex-console/src/lib/vault-registry/registry-file.ts
3051
3116
  function vaultRegistryDir(opts = {}) {
3052
- return opts.registryDir ?? join5(homedir5(), ".heyspex");
3117
+ return opts.registryDir ?? join5(heyspexHome2(), ".heyspex");
3053
3118
  }
3054
3119
  function vaultRegistryFilePath(opts = {}) {
3055
3120
  return join5(vaultRegistryDir(opts), "registry.json");
@@ -3219,7 +3284,7 @@ function samePathLoose(a, b) {
3219
3284
  }
3220
3285
  function assertNotRealHomeRegistryUnderTest(filePath) {
3221
3286
  if (!underTestRunner()) return;
3222
- if (samePathLoose(dirname2(filePath), join5(homedir5(), ".heyspex"))) {
3287
+ if (samePathLoose(dirname2(filePath), join5(heyspexHome2(), ".heyspex"))) {
3223
3288
  throw new Error(
3224
3289
  `registry-file: refusing to write ${filePath} under a test runner \u2014 that is the real ~/.heyspex registry. Pass opts.registryDir (a temp dir).`
3225
3290
  );
@@ -3669,12 +3734,17 @@ async function listWorkspacesNeon(accountId, deps = {}) {
3669
3734
  id: workspaces.id,
3670
3735
  displayName: workspaces.displayName,
3671
3736
  githubOrg: workspaces.githubOrg,
3672
- createdAt: workspaces.createdAt
3737
+ createdAt: workspaces.createdAt,
3738
+ archived: workspaces.archived
3673
3739
  }).from(workspaces).where(eq(workspaces.accountId, accountId));
3674
3740
  return rows.map((r) => ({
3675
3741
  id: r.id,
3676
3742
  display_name: r.displayName,
3677
3743
  created_at: (r.createdAt instanceof Date ? r.createdAt : /* @__PURE__ */ new Date()).toISOString(),
3744
+ // The flag `setWorkspaceArchivedNeon` writes; `list.ts` filters on it.
3745
+ // On a cloud host this row IS the roster (no registry.json to carry the
3746
+ // flag), so leaving it out made an archived workspace list forever.
3747
+ archived: r.archived,
3678
3748
  ...r.githubOrg ? { github_org: r.githubOrg } : {}
3679
3749
  }));
3680
3750
  } catch {
@@ -3961,7 +4031,134 @@ async function createWorkspace(input, deps = {}) {
3961
4031
  import { createHash } from "node:crypto";
3962
4032
  import fs from "node:fs";
3963
4033
  import path2 from "node:path";
4034
+ import os2 from "node:os";
3964
4035
  import { fileURLToPath, pathToFileURL } from "node:url";
4036
+
4037
+ // scripts/lib/hook-dispositions.mjs
4038
+ var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
4039
+ var DISPOSITIONS = Object.freeze(["mapped", "advisory-degraded", "unportable", "not-shipped"]);
4040
+ var mapped = (note) => Object.freeze({ disposition: "mapped", ...note ? { note } : {} });
4041
+ var degraded = (note) => Object.freeze({ disposition: "advisory-degraded", note });
4042
+ var unportable = (control) => Object.freeze({ disposition: "unportable", control });
4043
+ var notShipped = Object.freeze({ disposition: "not-shipped" });
4044
+ var CC = mapped();
4045
+ 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.";
4046
+ 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.";
4047
+ 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";
4048
+ var HOOK_DISPOSITIONS = Object.freeze({
4049
+ // ── customer: session start / compaction ──────────────────────────────────
4050
+ "wip-checkpoint": {
4051
+ "claude-code": CC,
4052
+ cursor: mapped("preCompact is relayed as SessionStart(compact)."),
4053
+ copilot: mapped("sessionStart additionalContext is delivered; the preCompact leg writes WIP only (no output channel), re-injected at the next sessionStart.")
4054
+ },
4055
+ "inject-doctrine": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
4056
+ "check-auto-memory-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
4057
+ "check-claude-md-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
4058
+ "git-reflog-audit": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext and agentStop.") },
4059
+ "vault-mirror-catchup": { "claude-code": CC, cursor: mapped(), copilot: mapped("Detached side effect on sessionStart.") },
4060
+ // ── customer: every user turn ─────────────────────────────────────────────
4061
+ "preflight-reminder": {
4062
+ "claude-code": CC,
4063
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
4064
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Real enforcement is the MCP preflight refusal.`)
4065
+ },
4066
+ "intent-classifier-trigger": {
4067
+ "claude-code": CC,
4068
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
4069
+ copilot: degraded(
4070
+ `${COPILOT_PROMPT_DROPPED} The hook still runs and arms obligation state; the HS-20SW directive-obligation-gate turns that state into a hard deny.`
4071
+ )
4072
+ },
4073
+ "inject-communication-style": {
4074
+ "claude-code": CC,
4075
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
4076
+ copilot: degraded(COPILOT_PROMPT_DROPPED)
4077
+ },
4078
+ "inject-vault-context": {
4079
+ "claude-code": CC,
4080
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
4081
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Per-prompt vault retrieval has no Copilot channel at all.`)
4082
+ },
4083
+ // ── customer: before a tool call (hard gates) ─────────────────────────────
4084
+ "git-identity-guard": { "claude-code": CC, cursor: mapped("beforeShellExecution. Always on."), copilot: mapped("preToolUse on bash/powershell. Always on.") },
4085
+ "vault-write-tier-guard": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell.") },
4086
+ "settings-write-gate": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell and the MCP settings tool.") },
4087
+ "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.") },
4088
+ "enforce-vault-naming": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
4089
+ "guard-auto-memory-writes": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
4090
+ "directive-obligation-gate": {
4091
+ "claude-code": CC,
4092
+ cursor: mapped(),
4093
+ 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.")
4094
+ },
4095
+ "claim-before-code-gate": {
4096
+ "claude-code": CC,
4097
+ 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."),
4098
+ 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.")
4099
+ },
4100
+ "capture-workflow-gate": {
4101
+ "claude-code": CC,
4102
+ cursor: mapped(),
4103
+ copilot: mapped("preToolUse on the MCP heyspex_file_spec tool; MCP tool naming in hook payloads is unverified live (matched by suffix).")
4104
+ },
4105
+ "diagnose-capture-reminder": {
4106
+ "claude-code": CC,
4107
+ cursor: mapped("preToolUse agent_message delivers the reminder."),
4108
+ copilot: degraded("Copilot preToolUse has no context channel for an inject, so this advisory is silent (lost).")
4109
+ },
4110
+ "git-stash-gate": {
4111
+ "claude-code": CC,
4112
+ cursor: mapped(),
4113
+ copilot: mapped('The Claude "ask" becomes a deny where no user is present (cloud agent, CLI autopilot).')
4114
+ },
4115
+ "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.") },
4116
+ "research-method-guard": {
4117
+ "claude-code": CC,
4118
+ cursor: mapped("preToolUse with a Skill|Workflow tool filter. Always on."),
4119
+ 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.")
4120
+ },
4121
+ "deep-research-cost-gate": {
4122
+ "claude-code": CC,
4123
+ cursor: mapped("preToolUse with a Workflow|Skill tool filter."),
4124
+ 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.")
4125
+ },
4126
+ "plan-review-gate": {
4127
+ "claude-code": CC,
4128
+ cursor: unportable(".cursor/rules/plan-review-required.mdc (always-on rule)"),
4129
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
4130
+ },
4131
+ "plan-exit-ingest-gate": {
4132
+ "claude-code": CC,
4133
+ cursor: unportable(".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"),
4134
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
4135
+ },
4136
+ // ── customer: end of turn / session ───────────────────────────────────────
4137
+ "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.') },
4138
+ "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).") },
4139
+ "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).") },
4140
+ "enforce-session-sequence": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive, capped at 8 consecutive blocks.") },
4141
+ "notify-status-flip": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
4142
+ "close-terminal-on-ship": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse / sessionEnd.") },
4143
+ "scripts/stamp-provenance-on-edit.mjs": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
4144
+ "session-end": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionEnd.") },
4145
+ // ── platform: heyspex-app development machinery (never installed in a customer repo) ──
4146
+ "inject-dev-server-awareness": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
4147
+ "inject-coo-seat": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
4148
+ "coo-guard-check": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
4149
+ "coo-denylist-gate": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
4150
+ "agent-spawn-delegation-gate": { "claude-code": CC, cursor: mapped("subagentStart."), copilot: notShipped },
4151
+ "capture-execution-gate": { "claude-code": CC, cursor: mapped("beforeShellExecution + subagentStart."), copilot: notShipped },
4152
+ "check-mcp-freshness-hook": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
4153
+ "dev-server-task-reminder": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
4154
+ "env-vault-guard": { "claude-code": CC, cursor: mapped("Always on."), copilot: notShipped },
4155
+ "log-session-activity": { "claude-code": CC, cursor: mapped(), copilot: notShipped }
4156
+ });
4157
+ function dispositionFor(hook, host) {
4158
+ return HOOK_DISPOSITIONS[hook]?.[host] ?? null;
4159
+ }
4160
+
4161
+ // scripts/lib/customer-kit.mjs
3965
4162
  var HOOK_SCOPES = Object.freeze({
3966
4163
  // ── customer: vault / workflow / safety gates ─────────────────────────────
3967
4164
  "wip-checkpoint": { scope: "customer", why: "Previous-session WIP handoff, read from the active vault." },
@@ -3977,6 +4174,7 @@ var HOOK_SCOPES = Object.freeze({
3977
4174
  "inject-communication-style": { scope: "customer", why: "Plain-language communication rules each turn." },
3978
4175
  "inject-vault-context": { scope: "customer", why: "Lexical vault retrieval for the prompt." },
3979
4176
  "git-identity-guard": { scope: "customer", why: "Agents never write git identity (owner ruling 2026-09-19)." },
4177
+ "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)." },
3980
4178
  "settings-write-gate": { scope: "customer", why: "Approval before the vault's .heyspex/config.yaml changes." },
3981
4179
  "emit-claim-status": { scope: "customer", why: "Claim liveness heartbeat to the console." },
3982
4180
  "enforce-vault-naming": { scope: "customer", why: "Naming V3 filename guard (HS-RY2N)." },
@@ -3984,7 +4182,10 @@ var HOOK_SCOPES = Object.freeze({
3984
4182
  "plan-review-gate": { scope: "customer", why: "Plan review + QC markers before ExitPlanMode." },
3985
4183
  "plan-exit-ingest-gate": { scope: "customer", why: "Plan ingest on ExitPlanMode; arms claim-before-code." },
3986
4184
  "claim-before-code-gate": { scope: "customer", why: "First code edit after plan exit needs a claim." },
4185
+ "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)." },
3987
4186
  "capture-workflow-gate": { scope: "customer", why: "No implementation spec straight out of an open capture." },
4187
+ "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)." },
4188
+ "diagnose-capture-reminder": { scope: "customer", why: "Warn when a diagnose is filed with no capture holding the owner's words (HS-IK2D)." },
3988
4189
  "git-stash-gate": { scope: "customer", why: "Always ask before a mutating git stash." },
3989
4190
  "research-method-guard": { scope: "customer", why: "Research method guard (HS-EN4F)." },
3990
4191
  "deep-research-cost-gate": { scope: "customer", why: "Cost confirmation before a paid deep-research launch." },
@@ -4003,6 +4204,7 @@ var HOOK_SCOPES = Object.freeze({
4003
4204
  "coo-guard-check": { scope: "platform", why: "dev-factory breakdown-delegation guard." },
4004
4205
  "coo-denylist-gate": { scope: "platform", why: "dev-factory COO window deny list." },
4005
4206
  "agent-spawn-delegation-gate": { scope: "platform", why: "dev-factory seat routing for subagents." },
4207
+ "capture-execution-gate": { scope: "platform", why: "Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8)." },
4006
4208
  "check-mcp-freshness-hook": { scope: "platform", why: "Freshness of the vendored MCP build \u2014 a platform-dev concern." },
4007
4209
  "dev-server-task-reminder": { scope: "platform", why: "spex-console dev server + Neon DB safety." },
4008
4210
  "env-vault-guard": { scope: "platform", why: "Guards heyspex-app's encrypted env vault paths." },
@@ -4021,6 +4223,7 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
4021
4223
  { event: "SessionStart", hook: "check-claude-md-budget", timeout: 5 },
4022
4224
  { event: "SessionStart", hook: "check-mcp-freshness-hook", timeout: 5 },
4023
4225
  { event: "SessionStart", hook: "git-reflog-audit", timeout: 5 },
4226
+ { event: "SessionStart", hook: "vault-mirror-catchup", timeout: 5 },
4024
4227
  { event: "PreCompact", hook: "wip-checkpoint", timeout: 5 },
4025
4228
  { event: "UserPromptSubmit", hook: "preflight-reminder", timeout: 5 },
4026
4229
  { event: "UserPromptSubmit", hook: "intent-classifier-trigger", timeout: 5 },
@@ -4028,10 +4231,13 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
4028
4231
  { event: "UserPromptSubmit", hook: "inject-vault-context", timeout: 10 },
4029
4232
  { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "env-vault-guard", timeout: 5 },
4030
4233
  { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-identity-guard", timeout: 5 },
4234
+ { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "vault-write-tier-guard", timeout: 5 },
4031
4235
  { event: "PreToolUse", matcher: "Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set", hook: "settings-write-gate", timeout: 5 },
4032
4236
  { event: "PreToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], timeout: 5 },
4033
4237
  { event: "PreToolUse", matcher: ".*", hook: "coo-denylist-gate", timeout: 5 },
4034
4238
  { event: "PreToolUse", matcher: "Agent|Task", hook: "agent-spawn-delegation-gate", timeout: 5 },
4239
+ { event: "PreToolUse", matcher: "Bash|PowerShell|Agent|Task", hook: "capture-execution-gate", timeout: 5 },
4240
+ { event: "PreToolUse", matcher: "Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "directive-obligation-gate", timeout: 5 },
4035
4241
  { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_claim_spec", hook: "coo-guard-check", timeout: 5 },
4036
4242
  { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "enforce-vault-naming", timeout: 5 },
4037
4243
  { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "guard-auto-memory-writes", timeout: 5 },
@@ -4040,6 +4246,7 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
4040
4246
  { event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-exit-ingest-gate", timeout: 5 },
4041
4247
  { event: "PreToolUse", matcher: "Edit|MultiEdit|Write", hook: "claim-before-code-gate", timeout: 5 },
4042
4248
  { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "capture-workflow-gate", timeout: 8 },
4249
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "diagnose-capture-reminder", timeout: 5 },
4043
4250
  { 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 },
4044
4251
  { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-stash-gate", timeout: 10 },
4045
4252
  { event: "PreToolUse", matcher: "Skill|Workflow", hook: "research-method-guard", timeout: 5 },
@@ -4052,7 +4259,9 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
4052
4259
  { event: "Stop", hook: "epic-milestone-phrasing-gate", timeout: 5 },
4053
4260
  { event: "Stop", hook: "git-reflog-audit", timeout: 5 },
4054
4261
  { event: "Stop", hook: "enforce-session-sequence", timeout: 5 },
4262
+ { event: "Stop", hook: "directive-obligation-gate", timeout: 5 },
4055
4263
  { event: "PostToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], timeout: 5 },
4264
+ { 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 },
4056
4265
  { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "notify-status-flip", timeout: 5 },
4057
4266
  { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "close-terminal-on-ship", timeout: 5 },
4058
4267
  { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "scripts/stamp-provenance-on-edit.mjs", timeout: 5 },
@@ -4137,9 +4346,22 @@ var CUSTOMER_LAUNCHERS = Object.freeze({
4137
4346
  });
4138
4347
  function customerRunnerPrefix(_platformRoot, surface = "claude") {
4139
4348
  const l = CUSTOMER_LAUNCHERS[surface];
4349
+ if (surface === "claude") return CUSTOMER_RUNNER_PREFIX;
4140
4350
  return `node "$${l.projectDirEnv}/${l.rel.join("/")}"`;
4141
4351
  }
4142
- var PLUGIN_RUNNER_PREFIX = 'node "${CLAUDE_PLUGIN_ROOT}/bin/run-hook.mjs"';
4352
+ 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}}";
4353
+ function buildBootstrap({ plugin = false } = {}) {
4354
+ const walk = BOOTSTRAP_WALK;
4355
+ 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)}";
4356
+ 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");
4357
+ }
4358
+ var PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
4359
+ var PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
4360
+ function buildCustomerBootstrap() {
4361
+ const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(",");
4362
+ 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("/");
4363
+ }
4364
+ var CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
4143
4365
  var LAUNCHER_TEMPLATE = [
4144
4366
  "#!/usr/bin/env node",
4145
4367
  "// HeySpex hook launcher -- GENERATED by hs install / hs connect. Commit it with this repo.",
@@ -4217,7 +4439,12 @@ var LAUNCHER_TEMPLATE = [
4217
4439
  "// (the Cursor one) runs exactly as if Cursor had invoked it directly.",
4218
4440
  "const shim = resolve(root, ...SHIM);",
4219
4441
  "process.argv[1] = shim;",
4220
- "await import(pathToFileURL(shim).href);",
4442
+ "try {",
4443
+ " await import(pathToFileURL(shim).href);",
4444
+ "} catch (err) {",
4445
+ " process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
4446
+ " process.exit(0);",
4447
+ "}",
4221
4448
  ""
4222
4449
  ].join("\n");
4223
4450
  function renderCustomerLauncher(surface = "claude") {
@@ -4324,7 +4551,6 @@ function renderPluginHooksDocument() {
4324
4551
  });
4325
4552
  }
4326
4553
  return {
4327
- _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.",
4328
4554
  hooks
4329
4555
  };
4330
4556
  }
@@ -4332,6 +4558,75 @@ function renderPluginHooksText() {
4332
4558
  return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}
4333
4559
  `;
4334
4560
  }
4561
+ var COPILOT_EVENT_NAMES = Object.freeze({
4562
+ SessionStart: "sessionStart",
4563
+ SessionEnd: "sessionEnd",
4564
+ UserPromptSubmit: "userPromptSubmitted",
4565
+ PreToolUse: "preToolUse",
4566
+ PostToolUse: "postToolUse",
4567
+ Stop: "agentStop",
4568
+ PreCompact: "preCompact"
4569
+ });
4570
+ var GITHUB_HOOKS_STATE_ARMING = Object.freeze(["intent-classifier-trigger"]);
4571
+ var COPILOT_TOOL_NAMES = Object.freeze({
4572
+ Write: "create",
4573
+ Edit: "edit",
4574
+ MultiEdit: "edit",
4575
+ NotebookEdit: "edit",
4576
+ Bash: "bash",
4577
+ PowerShell: "powershell",
4578
+ Agent: "task",
4579
+ Task: "task",
4580
+ WebFetch: "web_fetch",
4581
+ WebSearch: "web_search"
4582
+ });
4583
+ function toCopilotMatcher(matcher) {
4584
+ if (!matcher) return void 0;
4585
+ const out = [];
4586
+ for (const part of matcher.split("|")) {
4587
+ let t = null;
4588
+ if (part === ".*") t = ".*";
4589
+ else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
4590
+ else if (part.startsWith(MCP_PROJECT_PREFIX)) t = ".*" + part.slice(MCP_PROJECT_PREFIX.length);
4591
+ else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = ".*" + part.slice(MCP_PLUGIN_PREFIX.length);
4592
+ if (t && !out.includes(t)) out.push(t);
4593
+ }
4594
+ if (out.includes(".*")) return ".*";
4595
+ return out.length ? out.join("|") : null;
4596
+ }
4597
+ function githubHookNames() {
4598
+ const names = /* @__PURE__ */ new Set();
4599
+ for (const r of customerHookRows()) {
4600
+ if (dispositionFor(r.hook, "copilot")?.disposition === "mapped" || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
4601
+ }
4602
+ return names;
4603
+ }
4604
+ function githubHookCommand(row2) {
4605
+ const tail = hookTail(row2);
4606
+ 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}`;
4607
+ }
4608
+ function renderGithubHooksDocument() {
4609
+ const allowed = githubHookNames();
4610
+ const hooks = {};
4611
+ for (const row2 of customerHookRows()) {
4612
+ if (!allowed.has(row2.hook)) continue;
4613
+ const event = COPILOT_EVENT_NAMES[row2.event];
4614
+ if (!event) continue;
4615
+ const matcher = toCopilotMatcher(row2.matcher);
4616
+ if (matcher === null) continue;
4617
+ (hooks[event] ??= []).push({
4618
+ type: "command",
4619
+ ...matcher ? { matcher } : {},
4620
+ bash: githubHookCommand(row2),
4621
+ timeoutSec: row2.timeout
4622
+ });
4623
+ }
4624
+ return { version: 1, hooks };
4625
+ }
4626
+ function renderGithubHooksText() {
4627
+ return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}
4628
+ `;
4629
+ }
4335
4630
  var HARNESS_COMMAND_SURFACES = Object.freeze([
4336
4631
  {
4337
4632
  installedDir: Object.freeze([".claude", "commands"]),
@@ -4464,21 +4759,35 @@ if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
4464
4759
  process.exit(1);
4465
4760
  }
4466
4761
  console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
4762
+ } else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
4763
+ const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
4764
+ const ghWant = renderGithubHooksText();
4765
+ const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
4766
+ if (process.argv.includes("--write-github-hooks")) {
4767
+ if (!ghSame) {
4768
+ fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
4769
+ fs.writeFileSync(ghTarget, ghWant, "utf8");
4770
+ }
4771
+ console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
4772
+ } else if (!ghSame) {
4773
+ console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
4774
+ process.exit(1);
4775
+ } else {
4776
+ console.log("customer-kit: .github/hooks/heyspex.json up to date");
4777
+ }
4467
4778
  } else {
4468
4779
  console.log("usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks");
4469
4780
  }
4470
4781
  }
4471
4782
 
4472
4783
  // spex-console/src/lib/cli/connect-checks.ts
4473
- import { homedir as homedir9 } from "node:os";
4474
4784
  import { dirname as dirname5 } from "node:path";
4475
4785
  import { pathToFileURL as pathToFileURL2 } from "node:url";
4476
4786
 
4477
4787
  // spex-console/src/lib/dev-mode.ts
4478
- init_credentials();
4479
4788
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
4480
- import { homedir as homedir6 } from "node:os";
4481
4789
  import { join as join6 } from "node:path";
4790
+ init_credentials();
4482
4791
  var CLOUD_CONSOLE_URL2 = "https://app.heyspex.com";
4483
4792
  var LOCAL_CONSOLE_URL2 = "http://localhost:3000";
4484
4793
  function normalizeMode2(raw) {
@@ -4511,12 +4820,12 @@ function deriveSemantics2(mode, reliance) {
4511
4820
  vaultWriteMode: local ? "local" : "cloud"
4512
4821
  };
4513
4822
  }
4514
- function devModeFilePath2(homeDir = homedir6()) {
4823
+ function devModeFilePath2(homeDir = heyspexHome2()) {
4515
4824
  return join6(homeDir, ".heyspex", "dev-mode.json");
4516
4825
  }
4517
4826
  var defaultReadFile3 = (p) => readFileSync7(p, "utf8");
4518
4827
  function readLocalDevModeRecord2({
4519
- homeDir = homedir6(),
4828
+ homeDir = heyspexHome2(),
4520
4829
  readFile: readFile2 = defaultReadFile3
4521
4830
  } = {}) {
4522
4831
  let raw;
@@ -4551,7 +4860,7 @@ function readLocalDevModeRecord2({
4551
4860
  }
4552
4861
  function resolveDevMode2({
4553
4862
  env = process.env,
4554
- homeDir = homedir6(),
4863
+ homeDir = heyspexHome2(),
4555
4864
  readFile: readFile2 = defaultReadFile3,
4556
4865
  cloudSetting = void 0
4557
4866
  } = {}) {
@@ -4662,10 +4971,11 @@ function shellQuote(p) {
4662
4971
  var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
4663
4972
 
4664
4973
  // vendor/heyspex-mcp/console-fetch.mjs
4974
+ init_heyspex_home();
4665
4975
  init_credentials();
4666
- import { readFileSync as readFileSync9 } from "node:fs";
4976
+ import { readFileSync as readFileSync10 } from "node:fs";
4667
4977
  import { homedir as homedir8 } from "node:os";
4668
- import { join as join9 } from "node:path";
4978
+ import { join as join10 } from "node:path";
4669
4979
 
4670
4980
  // vendor/heyspex-mcp/vault-less-endpoints.mjs
4671
4981
  var VAULT_LESS_ENDPOINTS = Object.freeze([
@@ -4691,10 +5001,23 @@ function isVaultLessEndpoint(method, path7) {
4691
5001
  });
4692
5002
  }
4693
5003
 
5004
+ // vendor/heyspex-mcp/write-outbox.mjs
5005
+ import {
5006
+ readdirSync,
5007
+ readFileSync as readFileSync8,
5008
+ writeFileSync as writeFileSync5,
5009
+ mkdirSync as mkdirSync5,
5010
+ existsSync as existsSync3,
5011
+ renameSync as renameSync3,
5012
+ rmSync
5013
+ } from "node:fs";
5014
+ import { basename, join as join8, isAbsolute as isAbsolute2 } from "node:path";
5015
+
4694
5016
  // vendor/heyspex-mcp/vault-resolve.mjs
4695
- import { existsSync as existsSync3, readFileSync as readFileSync8 } from "node:fs";
5017
+ init_heyspex_home();
5018
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "node:fs";
4696
5019
  import { homedir as homedir7, tmpdir } from "node:os";
4697
- import { dirname as dirname4, join as join8, resolve as resolve4 } from "node:path";
5020
+ import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
4698
5021
 
4699
5022
  // vendor/heyspex-mcp/console-fetch.mjs
4700
5023
  var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
@@ -5002,7 +5325,7 @@ function runInit(args, deps) {
5002
5325
  };
5003
5326
  }
5004
5327
  const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
5005
- const accountId = args.accountId ?? (existingContent ? parseAccountId(existingContent) : null);
5328
+ const accountId = null;
5006
5329
  const content = renderHeyspexYaml({ ...args, vaultId, accountId });
5007
5330
  try {
5008
5331
  deps.writeFile(markerPath, content);
@@ -5268,6 +5591,62 @@ function isAbsolutePathForCompare(p) {
5268
5591
  const s = p.trim();
5269
5592
  return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
5270
5593
  }
5594
+ function expandPinPlaceholders(value, repoRoot) {
5595
+ return value.replace(/\$\{(?:CLAUDE_PROJECT_DIR|workspaceFolder)(?::-[^}]*)?\}/g, repoRoot);
5596
+ }
5597
+ function lexicalJoin(base, rel) {
5598
+ const parts = `${base}/${rel}`.replace(/\\/g, "/").split("/");
5599
+ const out = [];
5600
+ for (const seg of parts) {
5601
+ if (seg === "" && out.length > 0) continue;
5602
+ if (seg === ".") continue;
5603
+ if (seg === ".." && out.length > 1) {
5604
+ out.pop();
5605
+ continue;
5606
+ }
5607
+ out.push(seg);
5608
+ }
5609
+ return out.join("/");
5610
+ }
5611
+ function parseMarkerVaultPath(content) {
5612
+ const m = content.match(/^vault_path:[ \t]*(.+?)[ \t]*$/m);
5613
+ if (!m) return null;
5614
+ const v = m[1].trim().replace(/\s+#.*$/, "").replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
5615
+ if (!v || v.includes("${") || /^[\\/]/.test(v) || /^[a-zA-Z]:[\\/]/.test(v)) return null;
5616
+ return v;
5617
+ }
5618
+ function markerVaultIdOf(content) {
5619
+ const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
5620
+ if (!m) return null;
5621
+ const v = m[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
5622
+ return v.length > 0 ? v : null;
5623
+ }
5624
+ function detectMarkerAttachment(repoRoot, deps) {
5625
+ const markerPath = deps.joinPath(repoRoot, "heyspex.yaml");
5626
+ if (!deps.fileExists(markerPath)) return null;
5627
+ const raw = deps.readFile(markerPath);
5628
+ const vaultPath = raw ? parseMarkerVaultPath(raw) : null;
5629
+ if (!raw || !vaultPath) return null;
5630
+ const vaultRootAbs = lexicalJoin(repoRoot, vaultPath);
5631
+ const markerId = markerVaultIdOf(raw);
5632
+ let cloned = deps.fileExists(deps.joinPath(vaultRootAbs, "specs"));
5633
+ if (cloned && markerId) {
5634
+ const theirs = deps.readFile(deps.joinPath(vaultRootAbs, "heyspex.yaml"));
5635
+ const theirId = theirs ? markerVaultIdOf(theirs) : null;
5636
+ if (theirId && theirId !== markerId) cloned = false;
5637
+ }
5638
+ const registry = loadVaultRegistry(deps.registryOpts);
5639
+ const record = registry.vaults.find((v) => samePath(v.repo_root_abs, vaultRootAbs));
5640
+ return {
5641
+ vaultRootAbs,
5642
+ vaultId: markerId ?? record?.id ?? null,
5643
+ pins: [{ source: "heyspex.yaml", value: vaultRootAbs }],
5644
+ conflicting: false,
5645
+ viaMarker: true,
5646
+ cloned,
5647
+ vaultPath
5648
+ };
5649
+ }
5271
5650
  function readPinnedVaultRoot(repoRoot, deps) {
5272
5651
  const pins = [];
5273
5652
  for (const rel of SATELLITE_MANIFEST_RELS) {
@@ -5284,13 +5663,19 @@ function readPinnedVaultRoot(repoRoot, deps) {
5284
5663
  }
5285
5664
  if (typeof value !== "string") continue;
5286
5665
  const trimmed = value.trim();
5287
- if (!trimmed || trimmed.includes("${")) continue;
5288
- if (!isAbsolutePathForCompare(trimmed)) continue;
5289
- pins.push({ source: rel.join("/"), value: trimmed });
5666
+ if (!trimmed) continue;
5667
+ const expanded = expandPinPlaceholders(trimmed, repoRoot);
5668
+ if (expanded.includes("${")) continue;
5669
+ if (/^[a-zA-Z]:(?![\\/])/.test(expanded)) continue;
5670
+ const abs = isAbsolutePathForCompare(expanded) ? expanded.includes("..") ? lexicalJoin(expanded, ".") : expanded : lexicalJoin(repoRoot, expanded);
5671
+ if (expanded !== trimmed && isAtOrUnder(abs, repoRoot)) continue;
5672
+ pins.push({ source: rel.join("/"), value: abs });
5290
5673
  }
5291
5674
  return pins;
5292
5675
  }
5293
5676
  function detectSatelliteAttachment(repoRoot, deps) {
5677
+ const viaMarker = detectMarkerAttachment(repoRoot, deps);
5678
+ if (viaMarker) return viaMarker;
5294
5679
  const pins = readPinnedVaultRoot(repoRoot, deps).filter(
5295
5680
  (pin) => !isAtOrUnder(pin.value, repoRoot)
5296
5681
  );
@@ -5304,6 +5689,7 @@ function detectSatelliteAttachment(repoRoot, deps) {
5304
5689
  function findSatelliteRepoRoot(startDir, deps) {
5305
5690
  let dir = deps.resolveDir(startDir);
5306
5691
  for (let i = 0; i < 256; i += 1) {
5692
+ if (detectMarkerAttachment(dir, deps)) return dir;
5307
5693
  const pins = readPinnedVaultRoot(dir, deps);
5308
5694
  if (pins.some((pin) => !isAtOrUnder(deps.resolveDir(pin.value), dir))) {
5309
5695
  return dir;
@@ -5419,7 +5805,7 @@ function workspaceAgreementRow(vaultRecord, cloudWorkspaceId) {
5419
5805
  function checksHomeDir(deps) {
5420
5806
  if (deps.homeDir) return deps.homeDir;
5421
5807
  const registryDir = deps.registryOpts?.registryDir;
5422
- return registryDir ? dirname5(registryDir) : homedir9();
5808
+ return registryDir ? dirname5(registryDir) : heyspexHome2();
5423
5809
  }
5424
5810
  function accountAgreementRow(yamlContent, vaultRecord, deps, opts) {
5425
5811
  const id = "account_agreement";
@@ -5427,14 +5813,14 @@ function accountAgreementRow(yamlContent, vaultRecord, deps, opts) {
5427
5813
  if (yamlContent === null) {
5428
5814
  return { id, label, status: "not_applicable", detail: "no heyspex.yaml yet" };
5429
5815
  }
5430
- const recorded = parseAccountId(yamlContent);
5816
+ const legacyMarkerAccount = parseAccountId(yamlContent);
5817
+ const recorded = vaultRecord && vaultRecord.account_id && vaultRecord.account_id !== LEGACY_REGISTRY_ACCOUNT_ID ? vaultRecord.account_id : null;
5431
5818
  if (!recorded) {
5432
5819
  return {
5433
5820
  id,
5434
5821
  label,
5435
- status: "stale",
5436
- detail: "heyspex.yaml records no account: (written before accounts were recorded) -- run `hs connect` to record the account this repo is connected under",
5437
- remedy: "hs connect"
5822
+ status: "not_applicable",
5823
+ 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)` : "")
5438
5824
  };
5439
5825
  }
5440
5826
  const cloud = opts.cloudVaultAccountId ?? null;
@@ -5462,28 +5848,25 @@ function accountAgreementRow(yamlContent, vaultRecord, deps, opts) {
5462
5848
  remedy
5463
5849
  };
5464
5850
  }
5465
- if (vaultRecord && vaultRecord.account_id !== recorded) {
5466
- const legacy = vaultRecord.account_id === LEGACY_REGISTRY_ACCOUNT_ID;
5467
- return {
5468
- id,
5469
- label,
5470
- status: "stale",
5471
- 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`,
5472
- remedy: "hs connect"
5473
- };
5474
- }
5475
5851
  const current = resolveSessionOwnerUncached({
5476
5852
  env,
5477
5853
  homeDir,
5478
5854
  readFile: (p) => readFile2(p) ?? ""
5479
5855
  });
5480
5856
  const signedIn = current && current !== recorded ? `; signed in as ${current} -- sessions here still use ${recorded}'s sign-in` : "";
5481
- return { id, label, status: "present", detail: `account: ${recorded}${signedIn}` };
5857
+ const legacyNote = legacyMarkerAccount && legacyMarkerAccount !== recorded ? `; legacy account: ${legacyMarkerAccount} in heyspex.yaml is ignored` : "";
5858
+ return {
5859
+ id,
5860
+ label,
5861
+ status: "present",
5862
+ detail: `account: ${recorded} (machine-local)${signedIn}${legacyNote}`
5863
+ };
5482
5864
  }
5483
5865
  function runConnectChecks(repoRoot, deps, opts = {}) {
5484
5866
  const checks = [];
5485
5867
  const satellite = opts.satellite !== void 0 ? opts.satellite : detectSatelliteAttachment(repoRoot, deps);
5486
- const vaultSubject = satellite ? satellite.vaultRootAbs : repoRoot;
5868
+ const cloudOnly = Boolean(satellite?.viaMarker) && satellite?.cloned === false;
5869
+ const vaultSubject = satellite && !cloudOnly ? satellite.vaultRootAbs : repoRoot;
5487
5870
  const yamlPath = deps.joinPath(vaultSubject, "heyspex.yaml");
5488
5871
  const yamlContent = readIfExists(deps, yamlPath);
5489
5872
  const vaultId = yamlContent ? parseVaultId(yamlContent) : null;
@@ -5511,11 +5894,12 @@ function runConnectChecks(repoRoot, deps, opts = {}) {
5511
5894
  )
5512
5895
  );
5513
5896
  const workspaceRecord = vaultRecord ? registry.workspaces.find((w) => w.id === vaultRecord.workspace_id) : void 0;
5897
+ const ownVaultRegistered = Boolean(vaultId) && Boolean(vaultRecord) && vaultRecord?.id === vaultId;
5514
5898
  checks.push(
5515
5899
  row(
5516
5900
  "workspace_registered",
5517
5901
  "workspace registered in ~/.heyspex/registry.json",
5518
- Boolean(workspaceRecord),
5902
+ Boolean(workspaceRecord) || ownVaultRegistered,
5519
5903
  satelliteDetail(
5520
5904
  satellite,
5521
5905
  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"
@@ -5636,9 +6020,38 @@ function runConnectChecks(repoRoot, deps, opts = {}) {
5636
6020
  id: "satellite_attachment",
5637
6021
  label: "attached vault (satellite repo)",
5638
6022
  status: satellite.conflicting ? "stale" : "present",
5639
- 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})`
6023
+ 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})`
5640
6024
  });
5641
6025
  }
6026
+ if (satellite?.viaMarker) {
6027
+ const na = (id, detail) => {
6028
+ const r = checks.find((c) => c.id === id);
6029
+ if (r && (r.status === "missing" || r.status === "stale")) {
6030
+ r.status = "not_applicable";
6031
+ r.detail = detail;
6032
+ delete r.remedy;
6033
+ }
6034
+ };
6035
+ 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)";
6036
+ na("vault_registered", noRow);
6037
+ na("workspace_registered", noRow);
6038
+ na("id_agreement", noRow);
6039
+ na("account_agreement", "portable marker records no account; the signed-in account is used");
6040
+ if (cloudOnly) {
6041
+ na(
6042
+ "specs_dir",
6043
+ `satellite, vault not cloned locally -- cloud mode (clone the vault repo at ${satellite.vaultPath ?? "its vault_path"} for local reads and \`hs pull\`)`
6044
+ );
6045
+ }
6046
+ if (!vaultId) {
6047
+ const y = checks.find((c) => c.id === "heyspex_yaml");
6048
+ if (y) {
6049
+ y.status = "stale";
6050
+ y.detail = "heyspex.yaml has a vault_path but no vault_id; the satellite cannot name its vault";
6051
+ y.remedy = "add `vault_id: <the vault's uuid>` to heyspex.yaml (find it with `hs vault list`), then re-run `hs connect --check`";
6052
+ }
6053
+ }
6054
+ }
5642
6055
  const connected = checks.every((c) => c.status === "present" || c.status === "not_applicable");
5643
6056
  return {
5644
6057
  repo_root: repoRoot,
@@ -5647,7 +6060,8 @@ function runConnectChecks(repoRoot, deps, opts = {}) {
5647
6060
  ...satellite ? {
5648
6061
  satellite: {
5649
6062
  attached_vault_root: satellite.vaultRootAbs,
5650
- attached_vault_id: satellite.vaultId
6063
+ attached_vault_id: satellite.vaultId,
6064
+ ...satellite.viaMarker ? { via_marker: true, cloned: satellite.cloned === true } : {}
5651
6065
  }
5652
6066
  } : {}
5653
6067
  };
@@ -5773,6 +6187,22 @@ function pointerVsRepoVerdict(input) {
5773
6187
  repair: `hs vault switch ${repoVault}`,
5774
6188
  detail: `the cloud pointer names ${cloudPointer ?? "(none)"} but the registry pointer (its local cache) names ${registryPointer}: the cache is stale`
5775
6189
  });
6190
+ if (markerVaultId && input.markerBoundSatellite && !registryRowId) {
6191
+ return {
6192
+ branch: "explicit_vault",
6193
+ agree: true,
6194
+ repair: null,
6195
+ 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`
6196
+ };
6197
+ }
6198
+ if (markerVaultId && registryRowId === markerVaultId) {
6199
+ return {
6200
+ branch: "explicit_vault",
6201
+ agree: true,
6202
+ repair: null,
6203
+ 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}\`)`
6204
+ };
6205
+ }
5776
6206
  if (markerVaultId) {
5777
6207
  if (registryRowId !== markerVaultId) {
5778
6208
  const rekey = ROUTING_FACTS.find((f) => f.id === "C")?.caches.find((c) => c.id === "registry-vault-id")?.repair ?? "hs vault migrate-registry";
@@ -5988,7 +6418,7 @@ async function readCloudView(http, tokens) {
5988
6418
  }
5989
6419
  async function runCloudViewChecks(repoRoot, deps, opts = {}) {
5990
6420
  const satellite = opts.satellite !== void 0 ? opts.satellite : detectSatelliteAttachment(repoRoot, deps);
5991
- const vaultSubject = satellite ? satellite.vaultRootAbs : repoRoot;
6421
+ const vaultSubject = satellite && !(satellite.viaMarker && satellite.cloned === false) ? satellite.vaultRootAbs : repoRoot;
5992
6422
  const marker = readIfExists(deps, deps.joinPath(vaultSubject, "heyspex.yaml"));
5993
6423
  const vaultId = marker ? parseVaultId(marker) : null;
5994
6424
  const env = deps.env ?? process.env;
@@ -5999,7 +6429,11 @@ async function runCloudViewChecks(repoRoot, deps, opts = {}) {
5999
6429
  homeDir,
6000
6430
  readFile: (p) => readFile2(p) ?? ""
6001
6431
  });
6002
- const repoAccount = (marker ? parseAccountId(marker) : null) ?? signedInLocal ?? null;
6432
+ const rowForAccount = loadVaultRegistry(deps.registryOpts).vaults.find(
6433
+ (v) => samePath(v.repo_root_abs, vaultSubject)
6434
+ );
6435
+ const boundAccount = rowForAccount?.account_id && rowForAccount.account_id !== LEGACY_REGISTRY_ACCOUNT_ID ? rowForAccount.account_id : null;
6436
+ const repoAccount = boundAccount ?? signedInLocal ?? null;
6003
6437
  const http = deps.http;
6004
6438
  let tokens = { current: null, repo: null };
6005
6439
  if (http && http.token !== void 0) tokens = { current: http.token, repo: http.token };
@@ -6033,7 +6467,8 @@ async function runCloudViewChecks(repoRoot, deps, opts = {}) {
6033
6467
  markerVaultId: vaultId,
6034
6468
  registryRowId: vaultRecord?.id ?? null,
6035
6469
  cloudPointer: view.cloudPointer,
6036
- registryPointer: localPointer
6470
+ registryPointer: localPointer,
6471
+ markerBoundSatellite: Boolean(satellite?.viaMarker)
6037
6472
  });
6038
6473
  rows.push(
6039
6474
  compared(
@@ -6106,6 +6541,19 @@ async function runCloudViewChecks(repoRoot, deps, opts = {}) {
6106
6541
  rows.push(notApplicableRow(id, "no heyspex.yaml and no signed-in account"));
6107
6542
  } else if (view.sessionFailure) {
6108
6543
  rows.push(unknownRow(id, repoAccount, view.sessionFailure));
6544
+ } else if (!repoAccount && satellite?.viaMarker) {
6545
+ rows.push(
6546
+ compared(
6547
+ id,
6548
+ {
6549
+ local: "(portable marker: no account recorded)",
6550
+ cloud: view.email ? `${view.userId} (${view.email})` : view.userId
6551
+ },
6552
+ true,
6553
+ "hs login",
6554
+ `portable marker; signed in as ${view.email ?? view.userId}`
6555
+ )
6556
+ );
6109
6557
  } else {
6110
6558
  const agree = repoAccount !== null && (repoAccount === view.userId || repoAccount === view.email);
6111
6559
  rows.push(
@@ -6243,7 +6691,7 @@ function writeProjectContract(ctx, options2 = {}) {
6243
6691
  }
6244
6692
 
6245
6693
  // spex-console/src/lib/cli/init.customer-vault.ts
6246
- import { basename } from "node:path";
6694
+ import { basename as basename2 } from "node:path";
6247
6695
 
6248
6696
  // spex-console/src/lib/cli/template-manifest.sources.mjs
6249
6697
  var UNIVERSAL_TEMPLATE_ROWS = [
@@ -6754,7 +7202,7 @@ function findManagedSpecs(deps, targetDir, manifestDests) {
6754
7202
  return out;
6755
7203
  }
6756
7204
  function defaultVaultName(targetDir) {
6757
- return basename(targetDir) || targetDir;
7205
+ return basename2(targetDir) || targetDir;
6758
7206
  }
6759
7207
  function runCustomerVaultInit(args, deps, options2 = {}) {
6760
7208
  const { targetDir } = args;
@@ -6902,14 +7350,14 @@ function templatePinHook() {
6902
7350
  }
6903
7351
 
6904
7352
  // spex-console/src/lib/cli/principle-selection.ts
6905
- import { basename as basename2 } from "node:path";
7353
+ import { basename as basename3 } from "node:path";
6906
7354
  var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
6907
7355
  function principleKey(row2) {
6908
- const base = basename2(row2.dest, ".md");
7356
+ const base = basename3(row2.dest, ".md");
6909
7357
  return base.startsWith("principle-") ? base.slice("principle-".length) : base;
6910
7358
  }
6911
7359
  function principleWikilink(row2) {
6912
- return basename2(row2.dest, ".md");
7360
+ return basename3(row2.dest, ".md");
6913
7361
  }
6914
7362
  function parsePrinciplesFlag(value, rows) {
6915
7363
  const principleRows = rows.filter((row2) => row2.group === "principle");
@@ -7020,7 +7468,7 @@ function applyCustomerTemplate(args) {
7020
7468
  }
7021
7469
 
7022
7470
  // spex-console/src/lib/cli/install.ts
7023
- import { homedir as homedir10 } from "node:os";
7471
+ import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
7024
7472
 
7025
7473
  // scripts/install-cursor-hooks.mjs
7026
7474
  import fs2 from "node:fs";
@@ -7037,10 +7485,13 @@ var HOOKS = [
7037
7485
  { event: "sessionStart", hook: "wip-checkpoint", why: "Surface the previous session WIP handoff before work resumes." },
7038
7486
  { event: "sessionStart", hook: "inject-dev-server-awareness", why: "Once-per-session reminder that dev-server status is a tool call, not a guess." },
7039
7487
  { event: "sessionStart", hook: "inject-coo-seat", why: "Inject the dev-factory COO seat mandate when the COO window is open." },
7488
+ { event: "sessionStart", hook: "inject-doctrine", why: "Inject the baseline SpexCode Doctrine edition at session start." },
7040
7489
  { event: "sessionStart", hook: "coo-guard-check", why: "Warn at session start when a breakdown has not been delegated." },
7041
7490
  { event: "sessionStart", hook: "check-auto-memory-budget", why: "Self-heal and budget-check the auto-memory index." },
7042
7491
  { event: "sessionStart", hook: "check-claude-md-budget", why: "Advisory budget check on the always-loaded agent contract." },
7043
7492
  { event: "sessionStart", hook: "check-mcp-freshness-hook", why: "Warn when the running MCP server is stale versus vendor." },
7493
+ { event: "sessionStart", hook: "git-reflog-audit", why: "Session-start git audit (stash/reset/rebase witness)." },
7494
+ { event: "sessionStart", hook: "vault-mirror-catchup", why: "Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ)." },
7044
7495
  // ── Compaction ──────────────────────────────────────────────────────────
7045
7496
  // Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
7046
7497
  // preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
@@ -7054,6 +7505,7 @@ var HOOKS = [
7054
7505
  { event: "beforeSubmitPrompt", hook: "preflight-reminder", why: "Name the required procedure before a filing/destructive/protocol turn." },
7055
7506
  { event: "beforeSubmitPrompt", hook: "intent-classifier-trigger", why: "Route capture / research / settings / defer intents to the right skill." },
7056
7507
  { event: "beforeSubmitPrompt", hook: "inject-communication-style", why: "Re-assert the plain-language communication rules each turn." },
7508
+ { event: "beforeSubmitPrompt", hook: "inject-vault-context", timeout: 10, why: "Lexical vault retrieval for the prompt (surfaced as a user message)." },
7057
7509
  // ── Before every tool call (any tool) ───────────────────────────────────
7058
7510
  { event: "preToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], why: "Claim liveness heartbeat to the console." },
7059
7511
  { event: "preToolUse", matcher: ".*", hook: "coo-denylist-gate", why: "Hard deny the COO-window deny list." },
@@ -7064,21 +7516,27 @@ var HOOKS = [
7064
7516
  // the vault guard because removing a profile file is the loss it exists to
7065
7517
  // prevent, and Claude had no separate delete tool to list.
7066
7518
  { event: "preToolUse", matcher: "Write|Delete", hook: "env-vault-guard", why: "Block destructive writes to the encrypted env vault (ALWAYS ON)." },
7519
+ { event: "preToolUse", matcher: "Write", hook: "vault-write-tier-guard", why: "Deny disk writes to vault specs in cloud write mode (HS-FCNH)." },
7067
7520
  { event: "preToolUse", matcher: "Write", hook: "settings-write-gate", why: "Require approval before .heyspex/config.yaml changes." },
7068
7521
  { event: "preToolUse", matcher: "Write", hook: "enforce-vault-naming", why: "Block vault filenames that violate the naming registry." },
7069
7522
  { event: "preToolUse", matcher: "Write", hook: "guard-auto-memory-writes", why: "Block new topic files in the auto-memory directory." },
7070
7523
  { event: "preToolUse", matcher: "Write", hook: "preflight-reminder", why: "Preflight reminder on the edit path itself." },
7071
7524
  { event: "preToolUse", matcher: "Write", hook: "claim-before-code-gate", why: "Deny the first code edit after plan exit with no claim." },
7525
+ { 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)." },
7072
7526
  // ── Before a shell command ──────────────────────────────────────────────
7073
7527
  // beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
7074
7528
  // to carry the command string, and Cursor honors allow/deny/ask on it.
7075
7529
  { event: "beforeShellExecution", hook: "env-vault-guard", why: "Block shell writes/exfiltration targeting the env vault (ALWAYS ON)." },
7530
+ { event: "beforeShellExecution", hook: "vault-write-tier-guard", why: "Deny shell writes to vault specs in cloud write mode (HS-FCNH)." },
7076
7531
  { event: "beforeShellExecution", hook: "settings-write-gate", why: "Catch shell writes to .heyspex/config.yaml." },
7077
7532
  { event: "beforeShellExecution", hook: "enforce-vault-naming", why: "Catch vault files created via the shell." },
7078
7533
  { event: "beforeShellExecution", hook: "guard-auto-memory-writes", why: "Catch auto-memory topic files created via the shell." },
7079
7534
  { event: "beforeShellExecution", hook: "preflight-reminder", why: "Preflight reminder on rm -rf / force-push shaped commands." },
7080
7535
  { event: "beforeShellExecution", hook: "dev-server-task-reminder", timeout: 10, why: "Dev-server start/stop hard gate plus DB-safety reminder." },
7081
7536
  { event: "beforeShellExecution", hook: "git-stash-gate", timeout: 10, why: "Always ask before a mutating git stash." },
7537
+ { event: "beforeShellExecution", hook: "git-identity-guard", why: "Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON)." },
7538
+ { event: "beforeShellExecution", hook: "capture-execution-gate", why: "Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform)." },
7539
+ { event: "beforeShellExecution", hook: "directive-obligation-gate", why: "Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW)." },
7082
7540
  // ── Before specific MCP tools ───────────────────────────────────────────
7083
7541
  // Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
7084
7542
  // stay on preToolUse rather than beforeMCPExecution — registering both would
@@ -7088,15 +7546,20 @@ var HOOKS = [
7088
7546
  { 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." },
7089
7547
  { event: "preToolUse", matcher: ".*", tool: "heyspex_settings_set$", hook: "settings-write-gate", why: "Require approval for settings changes made through MCP." },
7090
7548
  { 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." },
7549
+ { 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)." },
7550
+ { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "research-method-guard", why: "Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON)." },
7091
7551
  { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "deep-research-cost-gate", why: "Cost confirmation before a paid deep-research harness launch." },
7092
7552
  { 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)." },
7093
7553
  // ── Before a subagent spawn ─────────────────────────────────────────────
7094
7554
  // subagentStart over preToolUse/Task: it carries subagent_type and the model,
7095
7555
  // which is exactly what the routing gate decides on, and it can deny.
7096
7556
  { event: "subagentStart", hook: "agent-spawn-delegation-gate", why: "Block a build/breakdown/critic subagent on an off-routing model." },
7557
+ { event: "subagentStart", hook: "capture-execution-gate", why: "Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform)." },
7558
+ { 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)." },
7097
7559
  // ── After every tool call ───────────────────────────────────────────────
7098
7560
  { event: "postToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], why: "Claim liveness heartbeat after the call resolves." },
7099
7561
  { event: "postToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PostToolUse"], why: "Append the tool result row to the session log." },
7562
+ { event: "postToolUse", matcher: ".*", hook: "directive-obligation-gate", why: "Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW)." },
7100
7563
  { event: "postToolUse", matcher: ".*", hook: "close-terminal-on-ship", why: "Close the Launch terminal once the spec ships." },
7101
7564
  { event: "postToolUse", matcher: "Write", hook: "notify-status-flip", why: "Sound on an in_review status flip." },
7102
7565
  { event: "postToolUse", matcher: "Write", hook: "scripts/stamp-provenance-on-edit.mjs", why: "Stamp assisted_by provenance on direct spec edits." },
@@ -7109,7 +7572,11 @@ var HOOKS = [
7109
7572
  // hook re-drives the agent — the same effect as Claude's exit-2 block.
7110
7573
  { event: "stop", hook: "emit-claim-status", args: ["--event=Stop"], why: "Mark the claim pending-on-response." },
7111
7574
  { event: "stop", hook: "naughty-list-gate", why: "Re-drive the turn when the final message uses banned jargon." },
7575
+ { event: "stop", hook: "id-display-format-gate", why: "Re-drive the turn when a reply breaks the Naming V3 display-id format." },
7576
+ { event: "stop", hook: "epic-milestone-phrasing-gate", why: "Re-drive the turn when a reply breaks the epic/milestone phrasing rules." },
7577
+ { event: "stop", hook: "git-reflog-audit", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
7112
7578
  { event: "stop", hook: "enforce-session-sequence", why: "Re-drive the turn when work shipped without record_wip." },
7579
+ { event: "stop", hook: "directive-obligation-gate", why: "Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW)." },
7113
7580
  // ── Session end ─────────────────────────────────────────────────────────
7114
7581
  { event: "sessionEnd", hook: "session-end", why: "Release the claim and notify the console." },
7115
7582
  { event: "sessionEnd", hook: "close-terminal-on-ship", why: "Final terminal cleanup." }
@@ -8747,6 +9214,7 @@ var INSTALL_HELP = [
8747
9214
  " hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
8748
9215
  " [--no-cursor] [--no-claude] [--claude-apply]",
8749
9216
  " [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
9217
+ " [--copilot-hooks | --copilot-plugin | --copilot-plugin-apply]",
8750
9218
  " [--no-statusline]",
8751
9219
  " [--cc | --command-code] [--register-vault [--workspace <id>]]",
8752
9220
  " [--remote[=<url>]]",
@@ -8805,6 +9273,15 @@ var INSTALL_HELP = [
8805
9273
  ` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
8806
9274
  " --plugin-scope <s> Plugin install scope: user, project (default), or local.",
8807
9275
  " Implies --plugin.",
9276
+ " --copilot-hooks Write .github/hooks/heyspex.json (GitHub Copilot hooks) into the",
9277
+ " target. Commands resolve node_modules/heyspex first, so an npm-only",
9278
+ " install (no heyspex repo checkout) works. Pick this OR the plugin.",
9279
+ " --copilot-plugin Print `copilot plugin marketplace add '<platform>'` +",
9280
+ " `copilot plugin install <plugin>@<marketplace>` (the platform root is",
9281
+ " node_modules/heyspex for a published install).",
9282
+ " --copilot-plugin-apply Run those two commands when `copilot` is on PATH.",
9283
+ " Requesting the Copilot plugin AND --copilot-hooks warns and skips the",
9284
+ " hooks file (both would fire every hook twice).",
8808
9285
  " --remote[=<url>] Write .mcp.json for the REMOTE (cloud) MCP endpoint instead of",
8809
9286
  ` the local stdio server \u2014 the Streamable-HTTP form against`,
8810
9287
  ` ${DEFAULT_REMOTE_MCP_URL} (or <url>), authenticated by the`,
@@ -8853,15 +9330,15 @@ var INSTALL_HELP = [
8853
9330
 
8854
9331
  // spex-console/src/lib/cli/install-identity.ts
8855
9332
  import { spawnSync as spawnSync2 } from "node:child_process";
8856
- import { existsSync as existsSync4 } from "node:fs";
8857
- import { homedir as homedir12 } from "node:os";
8858
- import { join as join10 } from "node:path";
9333
+ import { existsSync as existsSync5 } from "node:fs";
9334
+ import { join as join11 } from "node:path";
8859
9335
  import { createInterface } from "node:readline/promises";
8860
9336
  init_credentials();
8861
9337
 
8862
9338
  // vendor/heyspex-mcp/whoami.mjs
9339
+ init_heyspex_home();
8863
9340
  init_credentials();
8864
- import { homedir as homedir11 } from "node:os";
9341
+ import { homedir as homedir9 } from "node:os";
8865
9342
  var SESSION_PATH = "/api/mcp/session";
8866
9343
  async function probeCredentialIdentity({ consoleUrl, token, fetchImpl = fetch } = {}) {
8867
9344
  if (!token) return { kind: "no_credential" };
@@ -9116,7 +9593,7 @@ async function runIdentityGate(io, flags, verb, wording = ATTACH_WORDING) {
9116
9593
  "identity_unconfirmed"
9117
9594
  );
9118
9595
  }
9119
- function readConfiguredAccountId(env = process.env, homeDir = homedir12()) {
9596
+ function readConfiguredAccountId(env = process.env, homeDir = heyspexHome2()) {
9120
9597
  return resolveSessionOwnerUncached({ env, homeDir });
9121
9598
  }
9122
9599
  function accountMismatchRepair(repoAccount) {
@@ -9145,7 +9622,7 @@ function isInteractiveTerminal() {
9145
9622
  }
9146
9623
  function defaultIdentityGateIo(opts) {
9147
9624
  const consoleUrl = opts.consoleUrl.trim().replace(/\/+$/, "");
9148
- const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join10(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync4(p));
9625
+ const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join11(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync5(p));
9149
9626
  return {
9150
9627
  consoleUrl,
9151
9628
  interactive: opts.interactive ?? isInteractiveTerminal(),
@@ -9393,7 +9870,7 @@ function remedyFor(row2, satellite) {
9393
9870
  case "heyspex_yaml":
9394
9871
  case "vault_registered":
9395
9872
  case "workspace_registered":
9396
- return satellite ? null : "hs connect";
9873
+ return satellite ? row2.remedy ?? null : "hs connect";
9397
9874
  case "mcp_entry":
9398
9875
  return satellite ? "hs install --attach-vault <vault-id> --force" : "hs connect";
9399
9876
  case "plugin_surface":
@@ -9401,7 +9878,7 @@ function remedyFor(row2, satellite) {
9401
9878
  case "git_hooks":
9402
9879
  return "node scripts/install-git-hooks.mjs";
9403
9880
  case "satellite_attachment":
9404
- return "hs install --attach-vault <vault-id> --force";
9881
+ return row2.remedy ?? "hs install --attach-vault <vault-id> --force";
9405
9882
  case "active_vault":
9406
9883
  case "account_agreement":
9407
9884
  case "workspace_agreement":
@@ -9424,7 +9901,10 @@ function formatReport(report) {
9424
9901
  const lines = [
9425
9902
  `hs connect --check \u2014 ${report.repo_root}`,
9426
9903
  ...report.satellite ? [
9427
- `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.`
9904
+ `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.`,
9905
+ ...report.satellite.via_marker ? [
9906
+ 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)."
9907
+ ] : []
9428
9908
  ] : [],
9429
9909
  "",
9430
9910
  ...report.checks.flatMap((c) => [
@@ -9504,7 +9984,7 @@ var CANONICAL_VAULT_SUBFOLDERS = [
9504
9984
  function bareSpecsDirWarning(specsDir, deps) {
9505
9985
  let entries = [];
9506
9986
  try {
9507
- entries = readdirSync(specsDir);
9987
+ entries = readdirSync2(specsDir);
9508
9988
  } catch {
9509
9989
  return null;
9510
9990
  }
@@ -9626,9 +10106,10 @@ async function runConnectFullRun(args, deps) {
9626
10106
  };
9627
10107
  }
9628
10108
  if (!satellite && strictAccount.ok) {
9629
- const markerPath = deps.joinPath(repoRoot, "heyspex.yaml");
9630
- const markerText = deps.fileExists(markerPath) ? deps.readFile(markerPath) : null;
9631
- const repoAccount = markerText ? parseAccountId(markerText) : null;
10109
+ const boundRow = loadVaultRegistry(deps.registryOpts).vaults.find(
10110
+ (v) => samePath(v.repo_root_abs, repoRoot)
10111
+ );
10112
+ const repoAccount = boundRow?.account_id && boundRow.account_id !== "legacy" ? boundRow.account_id : null;
9632
10113
  if (repoAccount && repoAccount !== strictAccount.accountId) {
9633
10114
  return accountMismatchRefusal("hs connect", repoAccount, strictAccount.accountId);
9634
10115
  }
@@ -9731,37 +10212,14 @@ async function runConnectFullRun(args, deps) {
9731
10212
  if (isPresent("heyspex_yaml")) {
9732
10213
  const content = deps.fileExists(yamlPath) ? deps.readFile(yamlPath) : null;
9733
10214
  vaultId = content ? parseVaultId(content) ?? void 0 : void 0;
9734
- const accountUpdate = content !== null && accountId ? recordMarkerAccount(content, accountId) : null;
9735
- if (accountUpdate?.action !== "added") {
9736
- steps.push({
9737
- id: "heyspex_yaml",
9738
- label: "heyspex.yaml vault marker",
9739
- status: "skipped",
9740
- detail: "already initialized",
9741
- confirmed_by: "read heyspex.yaml"
9742
- });
9743
- } else if (args.dryRun) {
9744
- steps.push({
9745
- id: "heyspex_yaml",
9746
- label: "heyspex.yaml vault marker",
9747
- status: "run",
9748
- detail: `would record account: ${accountId} in ${yamlPath}`
9749
- });
9750
- } else {
9751
- try {
9752
- deps.writeFile(yamlPath, accountUpdate.content);
9753
- assertMarker(deps.readFile(yamlPath), yamlPath, vaultId, accountId);
9754
- steps.push({
9755
- id: "heyspex_yaml",
9756
- label: "heyspex.yaml vault marker",
9757
- status: "run",
9758
- detail: `recorded account: ${accountId} in ${yamlPath}`,
9759
- confirmed_by: "read heyspex.yaml"
9760
- });
9761
- } catch (err) {
9762
- failStep("heyspex_yaml", "heyspex.yaml vault marker", err);
9763
- }
9764
- }
10215
+ const legacyAccount = content ? parseAccountId(content) : null;
10216
+ steps.push({
10217
+ id: "heyspex_yaml",
10218
+ label: "heyspex.yaml vault marker",
10219
+ status: "skipped",
10220
+ detail: legacyAccount ? `already initialized (legacy account: ${legacyAccount} in the marker is ignored -- the account binding is machine-local)` : "already initialized",
10221
+ confirmed_by: "read heyspex.yaml"
10222
+ });
9765
10223
  } else if (args.dryRun) {
9766
10224
  steps.push({
9767
10225
  id: "heyspex_yaml",
@@ -9799,8 +10257,7 @@ async function runConnectFullRun(args, deps) {
9799
10257
  vaultRoot: "specs/",
9800
10258
  force: false,
9801
10259
  json: false,
9802
- adoptedVaultId,
9803
- ...accountId ? { accountId } : {}
10260
+ adoptedVaultId
9804
10261
  },
9805
10262
  initDeps
9806
10263
  );
@@ -10828,9 +11285,6 @@ function assertMarker(content, path7, vaultId, accountId) {
10828
11285
  if (vaultId && back !== vaultId) {
10829
11286
  throw new Error(`${path7} reads back vault_id ${back ?? "(none)"}, not ${vaultId}`);
10830
11287
  }
10831
- if (accountId && parseAccountId(content) !== accountId) {
10832
- throw new Error(`${path7} does not record account: ${accountId} after the write`);
10833
- }
10834
11288
  }
10835
11289
  var MIRRORED_SKILL_DIRS = [
10836
11290
  [".claude", "skills"],
@@ -11201,21 +11655,21 @@ function runEntry(name, main2, options2 = {}) {
11201
11655
  // spex-console/bin/hs-connect.ts
11202
11656
  function readFile(p) {
11203
11657
  try {
11204
- return readFileSync10(p, "utf8");
11658
+ return readFileSync11(p, "utf8");
11205
11659
  } catch {
11206
11660
  return null;
11207
11661
  }
11208
11662
  }
11209
11663
  function listDir(p) {
11210
11664
  try {
11211
- return readdirSync2(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
11665
+ return readdirSync3(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
11212
11666
  } catch {
11213
11667
  return [];
11214
11668
  }
11215
11669
  }
11216
11670
  var packageRoot = path6.resolve(path6.dirname(fileURLToPath4(import.meta.url)), "..", "..");
11217
11671
  var satelliteWalkDeps = {
11218
- fileExists: (p) => existsSync5(p),
11672
+ fileExists: (p) => existsSync6(p),
11219
11673
  readFile,
11220
11674
  joinPath: (...segments) => path6.join(...segments),
11221
11675
  parentDir: (p) => path6.dirname(p),
@@ -11272,11 +11726,11 @@ async function main() {
11272
11726
  repoRoot,
11273
11727
  cwd,
11274
11728
  platformRoot: path6.resolve(platformRoot),
11275
- fileExists: (p) => existsSync5(p),
11729
+ fileExists: (p) => existsSync6(p),
11276
11730
  readFile,
11277
11731
  listDir,
11278
- writeFile: (p, c) => writeFileSync5(p, c, "utf8"),
11279
- mkdir: (p) => mkdirSync5(p, { recursive: true }),
11732
+ writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
11733
+ mkdir: (p) => mkdirSync6(p, { recursive: true }),
11280
11734
  copyFile: (src, dest) => copyFileSync(src, dest),
11281
11735
  isDirectory: (p) => {
11282
11736
  try {