heyspex 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +163 -11
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +162 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +20 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +309 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/lib/console-url-resolver.mjs +3 -2
  72. package/scripts/lib/customer-kit.mjs +327 -10
  73. package/scripts/lib/dev-mode.mjs +7 -6
  74. package/scripts/lib/directive-obligations.mjs +349 -0
  75. package/scripts/lib/hook-dispositions.mjs +210 -0
  76. package/scripts/lib/hook-vault-header.mjs +30 -0
  77. package/scripts/lib/hs-source-branch.mjs +196 -0
  78. package/scripts/lib/hs-source-log.mjs +2 -1
  79. package/scripts/lib/hs-source-preflight.mjs +18 -4
  80. package/scripts/lib/hs-source-push.mjs +386 -356
  81. package/scripts/lib/window-placement.mjs +109 -0
  82. package/spexharness/src/adapters/claude-code.mjs +11 -3
  83. package/spexharness/src/adapters/copilot.mjs +236 -0
  84. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  85. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  86. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  87. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  88. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  89. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  90. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  91. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  93. package/spexharness/src/intent-classifier.mjs +4 -2
  94. package/spexharness/src/intent-directive.mjs +626 -541
  95. package/spexharness/src/rule-table.mjs +31 -1
  96. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  97. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  98. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  99. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  100. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  101. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  102. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  103. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  104. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  105. package/vendor/heyspex-mcp/cli.mjs +7 -1
  106. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  107. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  108. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  109. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  110. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  111. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  112. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  113. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  114. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  115. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  116. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  117. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  118. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  119. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  120. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  121. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  122. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  123. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  124. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  125. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  126. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  127. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  128. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  129. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  130. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  131. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  132. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  133. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  134. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  135. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  136. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  137. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  138. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  139. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  140. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  141. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  142. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  143. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  144. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  145. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -1,25 +1,71 @@
1
1
  import { createRequire as __hsCreateRequire } from 'node:module';
2
2
  const require = __hsCreateRequire(import.meta.url);
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
4
15
  var __esm = (fn, res) => function __init() {
5
16
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
17
  };
18
+ var __commonJS = (cb, mod) => function __require2() {
19
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
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
+ });
7
52
 
8
53
  // vendor/heyspex-mcp/credentials.mjs
9
54
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
- import os, { homedir } from "node:os";
55
+ import os, { homedir as homedir2 } from "node:os";
11
56
  import { dirname, join, resolve as resolve2 } from "node:path";
12
57
  var init_credentials = __esm({
13
58
  "vendor/heyspex-mcp/credentials.mjs"() {
14
59
  "use strict";
60
+ init_heyspex_home();
15
61
  }
16
62
  });
17
63
 
18
64
  // scripts/lib/dev-mode.mjs
19
65
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
20
- import { homedir as homedir2 } from "node:os";
66
+ import { homedir as homedir3 } from "node:os";
21
67
  import { join as join2 } from "node:path";
22
- function devModeFilePath(homeDir = homedir2()) {
68
+ function devModeFilePath(homeDir = heyspexHome()) {
23
69
  return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
24
70
  }
25
71
  function normalizeMode(raw) {
@@ -44,7 +90,7 @@ function deriveSemantics(mode, reliance) {
44
90
  vaultWriteMode: local ? "local" : "cloud"
45
91
  };
46
92
  }
47
- function readLocalDevModeRecord({ homeDir = homedir2(), readFile = defaultReadFile } = {}) {
93
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
48
94
  let raw;
49
95
  try {
50
96
  raw = readFile(devModeFilePath(homeDir));
@@ -74,7 +120,7 @@ function readLocalDevModeRecord({ homeDir = homedir2(), readFile = defaultReadFi
74
120
  }
75
121
  function resolveDevMode({
76
122
  env = process.env,
77
- homeDir = homedir2(),
123
+ homeDir = heyspexHome(),
78
124
  readFile = defaultReadFile,
79
125
  cloudSetting = void 0
80
126
  } = {}) {
@@ -137,6 +183,7 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
137
183
  var init_dev_mode = __esm({
138
184
  "scripts/lib/dev-mode.mjs"() {
139
185
  "use strict";
186
+ init_heyspex_home();
140
187
  init_credentials();
141
188
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
142
189
  LOCAL_CONSOLE_URL = "http://localhost:3000";
@@ -145,20 +192,681 @@ var init_dev_mode = __esm({
145
192
  }
146
193
  });
147
194
 
195
+ // node_modules/kind-of/index.js
196
+ var require_kind_of = __commonJS({
197
+ "node_modules/kind-of/index.js"(exports2, module2) {
198
+ "use strict";
199
+ var toString = Object.prototype.toString;
200
+ module2.exports = function kindOf(val) {
201
+ if (val === void 0) return "undefined";
202
+ if (val === null) return "null";
203
+ var type = typeof val;
204
+ if (type === "boolean") return "boolean";
205
+ if (type === "string") return "string";
206
+ if (type === "number") return "number";
207
+ if (type === "symbol") return "symbol";
208
+ if (type === "function") {
209
+ return isGeneratorFn(val) ? "generatorfunction" : "function";
210
+ }
211
+ if (isArray(val)) return "array";
212
+ if (isBuffer(val)) return "buffer";
213
+ if (isArguments(val)) return "arguments";
214
+ if (isDate(val)) return "date";
215
+ if (isError(val)) return "error";
216
+ if (isRegexp(val)) return "regexp";
217
+ switch (ctorName(val)) {
218
+ case "Symbol":
219
+ return "symbol";
220
+ case "Promise":
221
+ return "promise";
222
+ // Set, Map, WeakSet, WeakMap
223
+ case "WeakMap":
224
+ return "weakmap";
225
+ case "WeakSet":
226
+ return "weakset";
227
+ case "Map":
228
+ return "map";
229
+ case "Set":
230
+ return "set";
231
+ // 8-bit typed arrays
232
+ case "Int8Array":
233
+ return "int8array";
234
+ case "Uint8Array":
235
+ return "uint8array";
236
+ case "Uint8ClampedArray":
237
+ return "uint8clampedarray";
238
+ // 16-bit typed arrays
239
+ case "Int16Array":
240
+ return "int16array";
241
+ case "Uint16Array":
242
+ return "uint16array";
243
+ // 32-bit typed arrays
244
+ case "Int32Array":
245
+ return "int32array";
246
+ case "Uint32Array":
247
+ return "uint32array";
248
+ case "Float32Array":
249
+ return "float32array";
250
+ case "Float64Array":
251
+ return "float64array";
252
+ }
253
+ if (isGeneratorObj(val)) {
254
+ return "generator";
255
+ }
256
+ type = toString.call(val);
257
+ switch (type) {
258
+ case "[object Object]":
259
+ return "object";
260
+ // iterators
261
+ case "[object Map Iterator]":
262
+ return "mapiterator";
263
+ case "[object Set Iterator]":
264
+ return "setiterator";
265
+ case "[object String Iterator]":
266
+ return "stringiterator";
267
+ case "[object Array Iterator]":
268
+ return "arrayiterator";
269
+ }
270
+ return type.slice(8, -1).toLowerCase().replace(/\s/g, "");
271
+ };
272
+ function ctorName(val) {
273
+ return typeof val.constructor === "function" ? val.constructor.name : null;
274
+ }
275
+ function isArray(val) {
276
+ if (Array.isArray) return Array.isArray(val);
277
+ return val instanceof Array;
278
+ }
279
+ function isError(val) {
280
+ return val instanceof Error || typeof val.message === "string" && val.constructor && typeof val.constructor.stackTraceLimit === "number";
281
+ }
282
+ function isDate(val) {
283
+ if (val instanceof Date) return true;
284
+ return typeof val.toDateString === "function" && typeof val.getDate === "function" && typeof val.setDate === "function";
285
+ }
286
+ function isRegexp(val) {
287
+ if (val instanceof RegExp) return true;
288
+ return typeof val.flags === "string" && typeof val.ignoreCase === "boolean" && typeof val.multiline === "boolean" && typeof val.global === "boolean";
289
+ }
290
+ function isGeneratorFn(name, val) {
291
+ return ctorName(name) === "GeneratorFunction";
292
+ }
293
+ function isGeneratorObj(val) {
294
+ return typeof val.throw === "function" && typeof val.return === "function" && typeof val.next === "function";
295
+ }
296
+ function isArguments(val) {
297
+ try {
298
+ if (typeof val.length === "number" && typeof val.callee === "function") {
299
+ return true;
300
+ }
301
+ } catch (err) {
302
+ if (err.message.indexOf("callee") !== -1) {
303
+ return true;
304
+ }
305
+ }
306
+ return false;
307
+ }
308
+ function isBuffer(val) {
309
+ if (val.constructor && typeof val.constructor.isBuffer === "function") {
310
+ return val.constructor.isBuffer(val);
311
+ }
312
+ return false;
313
+ }
314
+ }
315
+ });
316
+
317
+ // node_modules/is-extendable/index.js
318
+ var require_is_extendable = __commonJS({
319
+ "node_modules/is-extendable/index.js"(exports2, module2) {
320
+ "use strict";
321
+ module2.exports = function isExtendable(val) {
322
+ return typeof val !== "undefined" && val !== null && (typeof val === "object" || typeof val === "function");
323
+ };
324
+ }
325
+ });
326
+
327
+ // node_modules/extend-shallow/index.js
328
+ var require_extend_shallow = __commonJS({
329
+ "node_modules/extend-shallow/index.js"(exports2, module2) {
330
+ "use strict";
331
+ var isObject = require_is_extendable();
332
+ module2.exports = function extend(o) {
333
+ if (!isObject(o)) {
334
+ o = {};
335
+ }
336
+ var len = arguments.length;
337
+ for (var i = 1; i < len; i++) {
338
+ var obj = arguments[i];
339
+ if (isObject(obj)) {
340
+ assign(o, obj);
341
+ }
342
+ }
343
+ return o;
344
+ };
345
+ function assign(a, b) {
346
+ for (var key in b) {
347
+ if (hasOwn(b, key)) {
348
+ a[key] = b[key];
349
+ }
350
+ }
351
+ }
352
+ function hasOwn(obj, key) {
353
+ return Object.prototype.hasOwnProperty.call(obj, key);
354
+ }
355
+ }
356
+ });
357
+
358
+ // node_modules/section-matter/index.js
359
+ var require_section_matter = __commonJS({
360
+ "node_modules/section-matter/index.js"(exports2, module2) {
361
+ "use strict";
362
+ var typeOf = require_kind_of();
363
+ var extend = require_extend_shallow();
364
+ module2.exports = function(input, options2) {
365
+ if (typeof options2 === "function") {
366
+ options2 = { parse: options2 };
367
+ }
368
+ var file = toObject(input);
369
+ var defaults = { section_delimiter: "---", parse: identity };
370
+ var opts = extend({}, defaults, options2);
371
+ var delim = opts.section_delimiter;
372
+ var lines = file.content.split(/\r?\n/);
373
+ var sections = null;
374
+ var section = createSection();
375
+ var content = [];
376
+ var stack = [];
377
+ function initSections(val) {
378
+ file.content = val;
379
+ sections = [];
380
+ content = [];
381
+ }
382
+ function closeSection(val) {
383
+ if (stack.length) {
384
+ section.key = getKey(stack[0], delim);
385
+ section.content = val;
386
+ opts.parse(section, sections);
387
+ sections.push(section);
388
+ section = createSection();
389
+ content = [];
390
+ stack = [];
391
+ }
392
+ }
393
+ for (var i = 0; i < lines.length; i++) {
394
+ var line = lines[i];
395
+ var len = stack.length;
396
+ var ln = line.trim();
397
+ if (isDelimiter(ln, delim)) {
398
+ if (ln.length === 3 && i !== 0) {
399
+ if (len === 0 || len === 2) {
400
+ content.push(line);
401
+ continue;
402
+ }
403
+ stack.push(ln);
404
+ section.data = content.join("\n");
405
+ content = [];
406
+ continue;
407
+ }
408
+ if (sections === null) {
409
+ initSections(content.join("\n"));
410
+ }
411
+ if (len === 2) {
412
+ closeSection(content.join("\n"));
413
+ }
414
+ stack.push(ln);
415
+ continue;
416
+ }
417
+ content.push(line);
418
+ }
419
+ if (sections === null) {
420
+ initSections(content.join("\n"));
421
+ } else {
422
+ closeSection(content.join("\n"));
423
+ }
424
+ file.sections = sections;
425
+ return file;
426
+ };
427
+ function isDelimiter(line, delim) {
428
+ if (line.slice(0, delim.length) !== delim) {
429
+ return false;
430
+ }
431
+ if (line.charAt(delim.length + 1) === delim.slice(-1)) {
432
+ return false;
433
+ }
434
+ return true;
435
+ }
436
+ function toObject(input) {
437
+ if (typeOf(input) !== "object") {
438
+ input = { content: input };
439
+ }
440
+ if (typeof input.content !== "string" && !isBuffer(input.content)) {
441
+ throw new TypeError("expected a buffer or string");
442
+ }
443
+ input.content = input.content.toString();
444
+ input.sections = [];
445
+ return input;
446
+ }
447
+ function getKey(val, delim) {
448
+ return val ? val.slice(delim.length).trim() : "";
449
+ }
450
+ function createSection() {
451
+ return { key: "", data: "", content: "" };
452
+ }
453
+ function identity(val) {
454
+ return val;
455
+ }
456
+ function isBuffer(val) {
457
+ if (val && val.constructor && typeof val.constructor.isBuffer === "function") {
458
+ return val.constructor.isBuffer(val);
459
+ }
460
+ return false;
461
+ }
462
+ }
463
+ });
464
+
465
+ // node_modules/gray-matter/lib/engines.js
466
+ var require_engines = __commonJS({
467
+ "node_modules/gray-matter/lib/engines.js"(exports, module) {
468
+ "use strict";
469
+ var yaml = __require("js-yaml");
470
+ var engines = exports = module.exports;
471
+ engines.yaml = {
472
+ parse: yaml.safeLoad.bind(yaml),
473
+ stringify: yaml.safeDump.bind(yaml)
474
+ };
475
+ engines.json = {
476
+ parse: JSON.parse.bind(JSON),
477
+ stringify: function(obj, options2) {
478
+ const opts = Object.assign({ replacer: null, space: 2 }, options2);
479
+ return JSON.stringify(obj, opts.replacer, opts.space);
480
+ }
481
+ };
482
+ engines.javascript = {
483
+ parse: function parse(str, options, wrap) {
484
+ try {
485
+ if (wrap !== false) {
486
+ str = "(function() {\nreturn " + str.trim() + ";\n}());";
487
+ }
488
+ return eval(str) || {};
489
+ } catch (err) {
490
+ if (wrap !== false && /(unexpected|identifier)/i.test(err.message)) {
491
+ return parse(str, options, false);
492
+ }
493
+ throw new SyntaxError(err);
494
+ }
495
+ },
496
+ stringify: function() {
497
+ throw new Error("stringifying JavaScript is not supported");
498
+ }
499
+ };
500
+ }
501
+ });
502
+
503
+ // node_modules/strip-bom-string/index.js
504
+ var require_strip_bom_string = __commonJS({
505
+ "node_modules/strip-bom-string/index.js"(exports2, module2) {
506
+ "use strict";
507
+ module2.exports = function(str2) {
508
+ if (typeof str2 === "string" && str2.charAt(0) === "\uFEFF") {
509
+ return str2.slice(1);
510
+ }
511
+ return str2;
512
+ };
513
+ }
514
+ });
515
+
516
+ // node_modules/gray-matter/lib/utils.js
517
+ var require_utils = __commonJS({
518
+ "node_modules/gray-matter/lib/utils.js"(exports2) {
519
+ "use strict";
520
+ var stripBom = require_strip_bom_string();
521
+ var typeOf = require_kind_of();
522
+ exports2.define = function(obj, key, val) {
523
+ Reflect.defineProperty(obj, key, {
524
+ enumerable: false,
525
+ configurable: true,
526
+ writable: true,
527
+ value: val
528
+ });
529
+ };
530
+ exports2.isBuffer = function(val) {
531
+ return typeOf(val) === "buffer";
532
+ };
533
+ exports2.isObject = function(val) {
534
+ return typeOf(val) === "object";
535
+ };
536
+ exports2.toBuffer = function(input) {
537
+ return typeof input === "string" ? Buffer.from(input) : input;
538
+ };
539
+ exports2.toString = function(input) {
540
+ if (exports2.isBuffer(input)) return stripBom(String(input));
541
+ if (typeof input !== "string") {
542
+ throw new TypeError("expected input to be a string or buffer");
543
+ }
544
+ return stripBom(input);
545
+ };
546
+ exports2.arrayify = function(val) {
547
+ return val ? Array.isArray(val) ? val : [val] : [];
548
+ };
549
+ exports2.startsWith = function(str2, substr, len) {
550
+ if (typeof len !== "number") len = substr.length;
551
+ return str2.slice(0, len) === substr;
552
+ };
553
+ }
554
+ });
555
+
556
+ // node_modules/gray-matter/lib/defaults.js
557
+ var require_defaults = __commonJS({
558
+ "node_modules/gray-matter/lib/defaults.js"(exports2, module2) {
559
+ "use strict";
560
+ var engines2 = require_engines();
561
+ var utils = require_utils();
562
+ module2.exports = function(options2) {
563
+ const opts = Object.assign({}, options2);
564
+ opts.delimiters = utils.arrayify(opts.delims || opts.delimiters || "---");
565
+ if (opts.delimiters.length === 1) {
566
+ opts.delimiters.push(opts.delimiters[0]);
567
+ }
568
+ opts.language = (opts.language || opts.lang || "yaml").toLowerCase();
569
+ opts.engines = Object.assign({}, engines2, opts.parsers, opts.engines);
570
+ return opts;
571
+ };
572
+ }
573
+ });
574
+
575
+ // node_modules/gray-matter/lib/engine.js
576
+ var require_engine = __commonJS({
577
+ "node_modules/gray-matter/lib/engine.js"(exports2, module2) {
578
+ "use strict";
579
+ module2.exports = function(name, options2) {
580
+ let engine = options2.engines[name] || options2.engines[aliase(name)];
581
+ if (typeof engine === "undefined") {
582
+ throw new Error('gray-matter engine "' + name + '" is not registered');
583
+ }
584
+ if (typeof engine === "function") {
585
+ engine = { parse: engine };
586
+ }
587
+ return engine;
588
+ };
589
+ function aliase(name) {
590
+ switch (name.toLowerCase()) {
591
+ case "js":
592
+ case "javascript":
593
+ return "javascript";
594
+ case "coffee":
595
+ case "coffeescript":
596
+ case "cson":
597
+ return "coffee";
598
+ case "yaml":
599
+ case "yml":
600
+ return "yaml";
601
+ default: {
602
+ return name;
603
+ }
604
+ }
605
+ }
606
+ }
607
+ });
608
+
609
+ // node_modules/gray-matter/lib/stringify.js
610
+ var require_stringify = __commonJS({
611
+ "node_modules/gray-matter/lib/stringify.js"(exports2, module2) {
612
+ "use strict";
613
+ var typeOf = require_kind_of();
614
+ var getEngine = require_engine();
615
+ var defaults = require_defaults();
616
+ module2.exports = function(file, data, options2) {
617
+ if (data == null && options2 == null) {
618
+ switch (typeOf(file)) {
619
+ case "object":
620
+ data = file.data;
621
+ options2 = {};
622
+ break;
623
+ case "string":
624
+ return file;
625
+ default: {
626
+ throw new TypeError("expected file to be a string or object");
627
+ }
628
+ }
629
+ }
630
+ const str2 = file.content;
631
+ const opts = defaults(options2);
632
+ if (data == null) {
633
+ if (!opts.data) return file;
634
+ data = opts.data;
635
+ }
636
+ const language = file.language || opts.language;
637
+ const engine = getEngine(language, opts);
638
+ if (typeof engine.stringify !== "function") {
639
+ throw new TypeError('expected "' + language + '.stringify" to be a function');
640
+ }
641
+ data = Object.assign({}, file.data, data);
642
+ const open = opts.delimiters[0];
643
+ const close = opts.delimiters[1];
644
+ const matter2 = engine.stringify(data, options2).trim();
645
+ let buf = "";
646
+ if (matter2 !== "{}") {
647
+ buf = newline(open) + newline(matter2) + newline(close);
648
+ }
649
+ if (typeof file.excerpt === "string" && file.excerpt !== "") {
650
+ if (str2.indexOf(file.excerpt.trim()) === -1) {
651
+ buf += newline(file.excerpt) + newline(close);
652
+ }
653
+ }
654
+ return buf + newline(str2);
655
+ };
656
+ function newline(str2) {
657
+ return str2.slice(-1) !== "\n" ? str2 + "\n" : str2;
658
+ }
659
+ }
660
+ });
661
+
662
+ // node_modules/gray-matter/lib/excerpt.js
663
+ var require_excerpt = __commonJS({
664
+ "node_modules/gray-matter/lib/excerpt.js"(exports2, module2) {
665
+ "use strict";
666
+ var defaults = require_defaults();
667
+ module2.exports = function(file, options2) {
668
+ const opts = defaults(options2);
669
+ if (file.data == null) {
670
+ file.data = {};
671
+ }
672
+ if (typeof opts.excerpt === "function") {
673
+ return opts.excerpt(file, opts);
674
+ }
675
+ const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
676
+ if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
677
+ return file;
678
+ }
679
+ const delimiter2 = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
680
+ const idx = file.content.indexOf(delimiter2);
681
+ if (idx !== -1) {
682
+ file.excerpt = file.content.slice(0, idx);
683
+ }
684
+ return file;
685
+ };
686
+ }
687
+ });
688
+
689
+ // node_modules/gray-matter/lib/to-file.js
690
+ var require_to_file = __commonJS({
691
+ "node_modules/gray-matter/lib/to-file.js"(exports2, module2) {
692
+ "use strict";
693
+ var typeOf = require_kind_of();
694
+ var stringify = require_stringify();
695
+ var utils = require_utils();
696
+ module2.exports = function(file) {
697
+ if (typeOf(file) !== "object") {
698
+ file = { content: file };
699
+ }
700
+ if (typeOf(file.data) !== "object") {
701
+ file.data = {};
702
+ }
703
+ if (file.contents && file.content == null) {
704
+ file.content = file.contents;
705
+ }
706
+ utils.define(file, "orig", utils.toBuffer(file.content));
707
+ utils.define(file, "language", file.language || "");
708
+ utils.define(file, "matter", file.matter || "");
709
+ utils.define(file, "stringify", function(data, options2) {
710
+ if (options2 && options2.language) {
711
+ file.language = options2.language;
712
+ }
713
+ return stringify(file, data, options2);
714
+ });
715
+ file.content = utils.toString(file.content);
716
+ file.isEmpty = false;
717
+ file.excerpt = "";
718
+ return file;
719
+ };
720
+ }
721
+ });
722
+
723
+ // node_modules/gray-matter/lib/parse.js
724
+ var require_parse = __commonJS({
725
+ "node_modules/gray-matter/lib/parse.js"(exports2, module2) {
726
+ "use strict";
727
+ var getEngine = require_engine();
728
+ var defaults = require_defaults();
729
+ module2.exports = function(language, str2, options2) {
730
+ const opts = defaults(options2);
731
+ const engine = getEngine(language, opts);
732
+ if (typeof engine.parse !== "function") {
733
+ throw new TypeError('expected "' + language + '.parse" to be a function');
734
+ }
735
+ return engine.parse(str2, opts);
736
+ };
737
+ }
738
+ });
739
+
740
+ // node_modules/gray-matter/index.js
741
+ var require_gray_matter = __commonJS({
742
+ "node_modules/gray-matter/index.js"(exports2, module2) {
743
+ "use strict";
744
+ var fs4 = __require("fs");
745
+ var sections = require_section_matter();
746
+ var defaults = require_defaults();
747
+ var stringify = require_stringify();
748
+ var excerpt = require_excerpt();
749
+ var engines2 = require_engines();
750
+ var toFile = require_to_file();
751
+ var parse2 = require_parse();
752
+ var utils = require_utils();
753
+ function matter2(input, options2) {
754
+ if (input === "") {
755
+ return { data: {}, content: input, excerpt: "", orig: input };
756
+ }
757
+ let file = toFile(input);
758
+ const cached = matter2.cache[file.content];
759
+ if (!options2) {
760
+ if (cached) {
761
+ file = Object.assign({}, cached);
762
+ file.orig = cached.orig;
763
+ return file;
764
+ }
765
+ matter2.cache[file.content] = file;
766
+ }
767
+ return parseMatter(file, options2);
768
+ }
769
+ function parseMatter(file, options2) {
770
+ const opts = defaults(options2);
771
+ const open = opts.delimiters[0];
772
+ const close = "\n" + opts.delimiters[1];
773
+ let str2 = file.content;
774
+ if (opts.language) {
775
+ file.language = opts.language;
776
+ }
777
+ const openLen = open.length;
778
+ if (!utils.startsWith(str2, open, openLen)) {
779
+ excerpt(file, opts);
780
+ return file;
781
+ }
782
+ if (str2.charAt(openLen) === open.slice(-1)) {
783
+ return file;
784
+ }
785
+ str2 = str2.slice(openLen);
786
+ const len = str2.length;
787
+ const language = matter2.language(str2, opts);
788
+ if (language.name) {
789
+ file.language = language.name;
790
+ str2 = str2.slice(language.raw.length);
791
+ }
792
+ let closeIndex = str2.indexOf(close);
793
+ if (closeIndex === -1) {
794
+ closeIndex = len;
795
+ }
796
+ file.matter = str2.slice(0, closeIndex);
797
+ const block = file.matter.replace(/^\s*#[^\n]+/gm, "").trim();
798
+ if (block === "") {
799
+ file.isEmpty = true;
800
+ file.empty = file.content;
801
+ file.data = {};
802
+ } else {
803
+ file.data = parse2(file.language, file.matter, opts);
804
+ }
805
+ if (closeIndex === len) {
806
+ file.content = "";
807
+ } else {
808
+ file.content = str2.slice(closeIndex + close.length);
809
+ if (file.content[0] === "\r") {
810
+ file.content = file.content.slice(1);
811
+ }
812
+ if (file.content[0] === "\n") {
813
+ file.content = file.content.slice(1);
814
+ }
815
+ }
816
+ excerpt(file, opts);
817
+ if (opts.sections === true || typeof opts.section === "function") {
818
+ sections(file, opts.section);
819
+ }
820
+ return file;
821
+ }
822
+ matter2.engines = engines2;
823
+ matter2.stringify = function(file, data, options2) {
824
+ if (typeof file === "string") file = matter2(file, options2);
825
+ return stringify(file, data, options2);
826
+ };
827
+ matter2.read = function(filepath, options2) {
828
+ const str2 = fs4.readFileSync(filepath, "utf8");
829
+ const file = matter2(str2, options2);
830
+ file.path = filepath;
831
+ return file;
832
+ };
833
+ matter2.test = function(str2, options2) {
834
+ return utils.startsWith(str2, defaults(options2).delimiters[0]);
835
+ };
836
+ matter2.language = function(str2, options2) {
837
+ const opts = defaults(options2);
838
+ const open = opts.delimiters[0];
839
+ if (matter2.test(str2)) {
840
+ str2 = str2.slice(open.length);
841
+ }
842
+ const language = str2.slice(0, str2.search(/\r?\n/));
843
+ return {
844
+ raw: language,
845
+ name: language ? language.trim() : ""
846
+ };
847
+ };
848
+ matter2.cache = {};
849
+ matter2.clearCache = function() {
850
+ matter2.cache = {};
851
+ };
852
+ module2.exports = matter2;
853
+ }
854
+ });
855
+
148
856
  // spex-console/bin/hs-init.ts
149
857
  import {
150
858
  copyFileSync,
151
- existsSync as existsSync4,
152
- mkdirSync as mkdirSync4,
153
- readdirSync,
154
- readFileSync as readFileSync8,
859
+ existsSync as existsSync5,
860
+ mkdirSync as mkdirSync6,
861
+ readdirSync as readdirSync2,
862
+ readFileSync as readFileSync11,
155
863
  statSync,
156
- writeFileSync as writeFileSync4
864
+ writeFileSync as writeFileSync6
157
865
  } from "node:fs";
158
- import { dirname as dirname5, join as join9, resolve as resolve5 } from "node:path";
159
- import process3 from "node:process";
866
+ import { dirname as dirname6, join as join11, resolve as resolve5 } from "node:path";
867
+ import process4 from "node:process";
160
868
  import { createInterface } from "node:readline/promises";
161
- import { fileURLToPath as fileURLToPath2 } from "node:url";
869
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
162
870
 
163
871
  // scripts/lib/shared-path-helpers.mjs
164
872
  import { isAbsolute, resolve, sep } from "node:path";
@@ -228,13 +936,13 @@ The vault under \`specs/\` is the only persistent queue. File new work via \`hey
228
936
  This CLAUDE.md was generated for **${vaultName}** from HeySpex template version **${templateVersion}** \u2014 it is yours to edit. Unlike HeySpex's own platform contract (which carries HeySpex's internal build tooling), everything above applies to *your* vault and *your* agents. Edit it freely as your project's conventions evolve; a later template update will not overwrite your changes.
229
937
  `;
230
938
  }
231
- function writeProjectContract(ctx, options = {}) {
939
+ function writeProjectContract(ctx, options2 = {}) {
232
940
  const content = renderCustomerClaudeMd({
233
941
  vaultName: ctx.vaultName,
234
942
  templateVersion: ctx.templateVersion
235
943
  });
236
944
  const results = [];
237
- for (const file of options.files ?? ["CLAUDE.md"]) {
945
+ for (const file of options2.files ?? ["CLAUDE.md"]) {
238
946
  const destPath = ctx.deps.joinPath(ctx.targetDir, file);
239
947
  if (!ctx.deps.exists(destPath)) {
240
948
  ctx.deps.writeFile(destPath, content);
@@ -252,7 +960,7 @@ function writeProjectContract(ctx, options = {}) {
252
960
  ctx.recordWrite?.(templatePath);
253
961
  }
254
962
  const hint = `${file} already exists - left untouched. Generated contract written to ${file}.template; compare with: diff ${file} ${file}.template`;
255
- options.notify?.(hint);
963
+ options2.notify?.(hint);
256
964
  results.push({ file, action: "template", path: templatePath, hint });
257
965
  }
258
966
  return results;
@@ -267,9 +975,10 @@ function customerClaudeMdHook(notify) {
267
975
  import { randomUUID } from "node:crypto";
268
976
 
269
977
  // scripts/lib/console-url-resolver.mjs
978
+ init_heyspex_home();
270
979
  init_dev_mode();
271
980
  import { readFileSync as readFileSync3 } from "node:fs";
272
- import { homedir as homedir3 } from "node:os";
981
+ import { homedir as homedir4 } from "node:os";
273
982
  import { join as join3 } from "node:path";
274
983
  var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
275
984
  function trimTrailingSlash(url) {
@@ -296,7 +1005,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
296
1005
  } catch {
297
1006
  }
298
1007
  }
299
- function readCredentialsConsoleUrl({ homeDir = homedir3(), readFile = defaultReadFile2 } = {}) {
1008
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
300
1009
  try {
301
1010
  const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
302
1011
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
@@ -340,7 +1049,7 @@ function resolveModeEnvelope({ env, homeDir }) {
340
1049
  function resolveConsoleUrl({
341
1050
  repoRoot,
342
1051
  env = process.env,
343
- homeDir = homedir3(),
1052
+ homeDir = heyspexHome(),
344
1053
  strict = false,
345
1054
  devMode,
346
1055
  log = (line) => process.stderr.write(line),
@@ -392,15 +1101,17 @@ function shellQuote(p) {
392
1101
  var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
393
1102
 
394
1103
  // vendor/heyspex-mcp/console-fetch.mjs
1104
+ init_heyspex_home();
395
1105
  init_credentials();
396
- import { readFileSync as readFileSync6 } from "node:fs";
397
- import { homedir as homedir6 } from "node:os";
398
- import { join as join7 } from "node:path";
1106
+ import { readFileSync as readFileSync7 } from "node:fs";
1107
+ import { homedir as homedir7 } from "node:os";
1108
+ import { join as join8 } from "node:path";
399
1109
 
400
1110
  // vendor/heyspex-mcp/session-owner.mjs
1111
+ init_heyspex_home();
401
1112
  init_credentials();
402
1113
  import { readFileSync as readFileSync4 } from "node:fs";
403
- import { homedir as homedir4 } from "node:os";
1114
+ import { homedir as homedir5 } from "node:os";
404
1115
  import { join as join5 } from "node:path";
405
1116
 
406
1117
  // vendor/heyspex-mcp/vault-less-endpoints.mjs
@@ -414,10 +1125,23 @@ var VAULT_LESS_ENDPOINTS = Object.freeze([
414
1125
  "* /api/admin/dev-mode"
415
1126
  ]);
416
1127
 
1128
+ // vendor/heyspex-mcp/write-outbox.mjs
1129
+ import {
1130
+ readdirSync,
1131
+ readFileSync as readFileSync5,
1132
+ writeFileSync as writeFileSync3,
1133
+ mkdirSync as mkdirSync3,
1134
+ existsSync as existsSync2,
1135
+ renameSync as renameSync2,
1136
+ rmSync
1137
+ } from "node:fs";
1138
+ import { basename, join as join6, isAbsolute as isAbsolute2 } from "node:path";
1139
+
417
1140
  // vendor/heyspex-mcp/vault-resolve.mjs
418
- import { existsSync as existsSync2, readFileSync as readFileSync5 } from "node:fs";
419
- import { homedir as homedir5, tmpdir } from "node:os";
420
- import { dirname as dirname3, join as join6, resolve as resolve3 } from "node:path";
1141
+ init_heyspex_home();
1142
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
1143
+ import { homedir as homedir6, tmpdir } from "node:os";
1144
+ import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
421
1145
 
422
1146
  // vendor/heyspex-mcp/console-fetch.mjs
423
1147
  var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
@@ -426,9 +1150,11 @@ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMAT
426
1150
  init_credentials();
427
1151
 
428
1152
  // spex-console/src/lib/vault-registry/registry-file.ts
429
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync7, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
430
- import { homedir as homedir7 } from "node:os";
431
- import { dirname as dirname4, join as join8, resolve as resolve4 } from "node:path";
1153
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
1154
+ import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
1155
+
1156
+ // spex-console/src/lib/heyspex-home.ts
1157
+ import { homedir as homedir8 } from "node:os";
432
1158
 
433
1159
  // spex-console/src/lib/cli/control-shared.ts
434
1160
  var EXIT = {
@@ -626,10 +1352,10 @@ function parseInitArgs(argv) {
626
1352
  }
627
1353
  var HEYSPEX_CONFIG_DIR = ".heyspex";
628
1354
  var HEYSPEX_CONFIG_FILE = "config.yaml";
629
- function seedConfigYaml(root, readSeed, fs) {
630
- const configDir = fs.joinPath(root, HEYSPEX_CONFIG_DIR);
631
- const configPath = fs.joinPath(configDir, HEYSPEX_CONFIG_FILE);
632
- if (fs.exists(configPath)) return { status: "kept", path: configPath };
1355
+ function seedConfigYaml(root, readSeed, fs4) {
1356
+ const configDir = fs4.joinPath(root, HEYSPEX_CONFIG_DIR);
1357
+ const configPath = fs4.joinPath(configDir, HEYSPEX_CONFIG_FILE);
1358
+ if (fs4.exists(configPath)) return { status: "kept", path: configPath };
633
1359
  if (!readSeed) return { status: "no_seed", path: configPath };
634
1360
  let seed;
635
1361
  try {
@@ -638,8 +1364,8 @@ function seedConfigYaml(root, readSeed, fs) {
638
1364
  seed = null;
639
1365
  }
640
1366
  if (seed === null) return { status: "no_seed", path: configPath };
641
- fs.mkdirRecursive(configDir);
642
- fs.writeFile(configPath, seed);
1367
+ fs4.mkdirRecursive(configDir);
1368
+ fs4.writeFile(configPath, seed);
643
1369
  return { status: "seeded", path: configPath };
644
1370
  }
645
1371
  function renderHeyspexYaml(args) {
@@ -765,7 +1491,7 @@ function runInit(args, deps) {
765
1491
  };
766
1492
  }
767
1493
  const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
768
- const accountId = args.accountId ?? (existingContent ? parseAccountId(existingContent) : null);
1494
+ const accountId = null;
769
1495
  const content = renderHeyspexYaml({ ...args, vaultId, accountId });
770
1496
  try {
771
1497
  deps.writeFile(markerPath, content);
@@ -849,7 +1575,7 @@ var HELP = [
849
1575
  ].join("\n");
850
1576
 
851
1577
  // spex-console/src/lib/cli/init.customer-vault.ts
852
- import { basename } from "node:path";
1578
+ import { basename as basename2 } from "node:path";
853
1579
 
854
1580
  // spex-console/src/lib/cli/template-manifest.sources.mjs
855
1581
  var UNIVERSAL_TEMPLATE_ROWS = [
@@ -1335,10 +2061,10 @@ var SCAFFOLD_FOLDERS = [
1335
2061
  "events"
1336
2062
  ];
1337
2063
  var PLACEHOLDER_FILES = /* @__PURE__ */ new Set([".gitkeep"]);
1338
- function isDir(deps, path2, name) {
1339
- if (deps.isDirectory) return deps.isDirectory(path2);
2064
+ function isDir(deps, path5, name) {
2065
+ if (deps.isDirectory) return deps.isDirectory(path5);
1340
2066
  try {
1341
- const kids = deps.readdir(path2);
2067
+ const kids = deps.readdir(path5);
1342
2068
  return kids.length > 0 || !name.includes(".");
1343
2069
  } catch {
1344
2070
  return false;
@@ -1360,12 +2086,12 @@ function findManagedSpecs(deps, targetDir, manifestDests) {
1360
2086
  return out;
1361
2087
  }
1362
2088
  function defaultVaultName(targetDir) {
1363
- return basename(targetDir) || targetDir;
2089
+ return basename2(targetDir) || targetDir;
1364
2090
  }
1365
- function runCustomerVaultInit(args, deps, options = {}) {
2091
+ function runCustomerVaultInit(args, deps, options2 = {}) {
1366
2092
  const { targetDir } = args;
1367
2093
  const written = [];
1368
- const filter = options.manifestFilter ?? (() => true);
2094
+ const filter = options2.manifestFilter ?? (() => true);
1369
2095
  const rows = TEMPLATE_MANIFEST.filter(filter);
1370
2096
  if (deps.exists(targetDir)) {
1371
2097
  const markerPresent = deps.exists(deps.joinPath(targetDir, "heyspex.yaml"));
@@ -1409,7 +2135,7 @@ function runCustomerVaultInit(args, deps, options = {}) {
1409
2135
  deps.writeFile(
1410
2136
  heyspexYamlPath,
1411
2137
  renderHeyspexYaml({
1412
- consoleUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL2,
2138
+ consoleUrl: options2.consoleUrl ?? DEFAULT_CONSOLE_URL2,
1413
2139
  vaultRoot: "specs/",
1414
2140
  vaultId
1415
2141
  })
@@ -1444,19 +2170,19 @@ function runCustomerVaultInit(args, deps, options = {}) {
1444
2170
  written.push(destAbs);
1445
2171
  rowResults.push({ dest: row.dest, action: "created" });
1446
2172
  }
1447
- if (options.afterScaffold && options.afterScaffold.length > 0) {
2173
+ if (options2.afterScaffold && options2.afterScaffold.length > 0) {
1448
2174
  const ctx = {
1449
2175
  targetDir,
1450
- vaultName: options.vaultName ?? defaultVaultName(targetDir),
2176
+ vaultName: options2.vaultName ?? defaultVaultName(targetDir),
1451
2177
  templateVersion: TEMPLATE_VERSION,
1452
2178
  manifestRows: rows,
1453
2179
  rows: rowResults,
1454
2180
  deps,
1455
- recordWrite: (path2) => {
1456
- written.push(path2);
2181
+ recordWrite: (path5) => {
2182
+ written.push(path5);
1457
2183
  }
1458
2184
  };
1459
- for (const hook of options.afterScaffold) hook(ctx);
2185
+ for (const hook of options2.afterScaffold) hook(ctx);
1460
2186
  }
1461
2187
  const createdCount = rowResults.filter((r) => r.action === "created").length;
1462
2188
  const keptCount = rowResults.length - createdCount;
@@ -1544,15 +2270,1102 @@ function gitFinalizeHook(runner, notify) {
1544
2270
  };
1545
2271
  }
1546
2272
 
2273
+ // spex-console/src/lib/cli/install.ts
2274
+ import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
2275
+
2276
+ // scripts/install-cursor-hooks.mjs
2277
+ import fs2 from "node:fs";
2278
+ import path3 from "node:path";
2279
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2280
+
2281
+ // scripts/cursor-hooks/manifest.mjs
2282
+ var SHIM = "scripts/cursor-hooks/_run.mjs";
2283
+ var DEFAULT_TIMEOUT_SECONDS = 5;
2284
+ var HOOKS = [
2285
+ // ── Session start ───────────────────────────────────────────────────────
2286
+ // Cursor's sessionStart honors `additional_context`, so these injections
2287
+ // land in context exactly as they do in Claude Code.
2288
+ { event: "sessionStart", hook: "wip-checkpoint", why: "Surface the previous session WIP handoff before work resumes." },
2289
+ { event: "sessionStart", hook: "inject-dev-server-awareness", why: "Once-per-session reminder that dev-server status is a tool call, not a guess." },
2290
+ { event: "sessionStart", hook: "inject-coo-seat", why: "Inject the dev-factory COO seat mandate when the COO window is open." },
2291
+ { event: "sessionStart", hook: "inject-doctrine", why: "Inject the baseline SpexCode Doctrine edition at session start." },
2292
+ { event: "sessionStart", hook: "coo-guard-check", why: "Warn at session start when a breakdown has not been delegated." },
2293
+ { event: "sessionStart", hook: "check-auto-memory-budget", why: "Self-heal and budget-check the auto-memory index." },
2294
+ { event: "sessionStart", hook: "check-claude-md-budget", why: "Advisory budget check on the always-loaded agent contract." },
2295
+ { event: "sessionStart", hook: "check-mcp-freshness-hook", why: "Warn when the running MCP server is stale versus vendor." },
2296
+ { event: "sessionStart", hook: "git-reflog-audit", why: "Session-start git audit (stash/reset/rebase witness)." },
2297
+ { event: "sessionStart", hook: "vault-mirror-catchup", why: "Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ)." },
2298
+ // ── Compaction ──────────────────────────────────────────────────────────
2299
+ // Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
2300
+ // preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
2301
+ // to keep wip-checkpoint's "rollover, not fresh start" framing.
2302
+ { event: "preCompact", hook: "wip-checkpoint", claudeEvent: "SessionStart", why: "Carry the WIP handoff across a context compaction." },
2303
+ // ── Every user turn ─────────────────────────────────────────────────────
2304
+ // beforeSubmitPrompt is Cursor's UserPromptSubmit. It honors `user_message`
2305
+ // but not `additional_context`, so these three surface in the transcript
2306
+ // rather than as invisible context — the routing still reaches the agent,
2307
+ // and the user can see why it fired.
2308
+ { event: "beforeSubmitPrompt", hook: "preflight-reminder", why: "Name the required procedure before a filing/destructive/protocol turn." },
2309
+ { event: "beforeSubmitPrompt", hook: "intent-classifier-trigger", why: "Route capture / research / settings / defer intents to the right skill." },
2310
+ { event: "beforeSubmitPrompt", hook: "inject-communication-style", why: "Re-assert the plain-language communication rules each turn." },
2311
+ { event: "beforeSubmitPrompt", hook: "inject-vault-context", timeout: 10, why: "Lexical vault retrieval for the prompt (surfaced as a user message)." },
2312
+ // ── Before every tool call (any tool) ───────────────────────────────────
2313
+ { event: "preToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], why: "Claim liveness heartbeat to the console." },
2314
+ { event: "preToolUse", matcher: ".*", hook: "coo-denylist-gate", why: "Hard deny the COO-window deny list." },
2315
+ { event: "preToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PreToolUse"], why: "Append agent.tool_called rows to the session log." },
2316
+ // ── Before a file write / delete ─────────────────────────────────────────
2317
+ // Cursor's `Write` covers create AND patch (Claude's Write|Edit|MultiEdit);
2318
+ // the shim re-derives which one from the input shape. `Delete` is included on
2319
+ // the vault guard because removing a profile file is the loss it exists to
2320
+ // prevent, and Claude had no separate delete tool to list.
2321
+ { event: "preToolUse", matcher: "Write|Delete", hook: "env-vault-guard", why: "Block destructive writes to the encrypted env vault (ALWAYS ON)." },
2322
+ { event: "preToolUse", matcher: "Write", hook: "vault-write-tier-guard", why: "Deny disk writes to vault specs in cloud write mode (HS-FCNH)." },
2323
+ { event: "preToolUse", matcher: "Write", hook: "settings-write-gate", why: "Require approval before .heyspex/config.yaml changes." },
2324
+ { event: "preToolUse", matcher: "Write", hook: "enforce-vault-naming", why: "Block vault filenames that violate the naming registry." },
2325
+ { event: "preToolUse", matcher: "Write", hook: "guard-auto-memory-writes", why: "Block new topic files in the auto-memory directory." },
2326
+ { event: "preToolUse", matcher: "Write", hook: "preflight-reminder", why: "Preflight reminder on the edit path itself." },
2327
+ { event: "preToolUse", matcher: "Write", hook: "claim-before-code-gate", why: "Deny the first code edit after plan exit with no claim." },
2328
+ { 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)." },
2329
+ // ── Before a shell command ──────────────────────────────────────────────
2330
+ // beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
2331
+ // to carry the command string, and Cursor honors allow/deny/ask on it.
2332
+ { event: "beforeShellExecution", hook: "env-vault-guard", why: "Block shell writes/exfiltration targeting the env vault (ALWAYS ON)." },
2333
+ { event: "beforeShellExecution", hook: "vault-write-tier-guard", why: "Deny shell writes to vault specs in cloud write mode (HS-FCNH)." },
2334
+ { event: "beforeShellExecution", hook: "settings-write-gate", why: "Catch shell writes to .heyspex/config.yaml." },
2335
+ { event: "beforeShellExecution", hook: "enforce-vault-naming", why: "Catch vault files created via the shell." },
2336
+ { event: "beforeShellExecution", hook: "guard-auto-memory-writes", why: "Catch auto-memory topic files created via the shell." },
2337
+ { event: "beforeShellExecution", hook: "preflight-reminder", why: "Preflight reminder on rm -rf / force-push shaped commands." },
2338
+ { event: "beforeShellExecution", hook: "dev-server-task-reminder", timeout: 10, why: "Dev-server start/stop hard gate plus DB-safety reminder." },
2339
+ { event: "beforeShellExecution", hook: "git-stash-gate", timeout: 10, why: "Always ask before a mutating git stash." },
2340
+ { event: "beforeShellExecution", hook: "git-identity-guard", why: "Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON)." },
2341
+ { event: "beforeShellExecution", hook: "capture-execution-gate", why: "Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform)." },
2342
+ { event: "beforeShellExecution", hook: "directive-obligation-gate", why: "Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW)." },
2343
+ // ── Before specific MCP tools ───────────────────────────────────────────
2344
+ // Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
2345
+ // stay on preToolUse rather than beforeMCPExecution — registering both would
2346
+ // fire each gate twice. Scoping is the shim's `tool` regex because a Cursor
2347
+ // matcher cannot name one MCP tool per entry portably.
2348
+ { event: "preToolUse", matcher: ".*", tool: "heyspex_claim_spec$", hook: "coo-guard-check", why: "Block a claim whose breakdown was never delegated." },
2349
+ { 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." },
2350
+ { event: "preToolUse", matcher: ".*", tool: "heyspex_settings_set$", hook: "settings-write-gate", why: "Require approval for settings changes made through MCP." },
2351
+ { 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." },
2352
+ { 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)." },
2353
+ { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "research-method-guard", why: "Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON)." },
2354
+ { event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "deep-research-cost-gate", why: "Cost confirmation before a paid deep-research harness launch." },
2355
+ { 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)." },
2356
+ // ── Before a subagent spawn ─────────────────────────────────────────────
2357
+ // subagentStart over preToolUse/Task: it carries subagent_type and the model,
2358
+ // which is exactly what the routing gate decides on, and it can deny.
2359
+ { event: "subagentStart", hook: "agent-spawn-delegation-gate", why: "Block a build/breakdown/critic subagent on an off-routing model." },
2360
+ { event: "subagentStart", hook: "capture-execution-gate", why: "Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform)." },
2361
+ { 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)." },
2362
+ // ── After every tool call ───────────────────────────────────────────────
2363
+ { event: "postToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], why: "Claim liveness heartbeat after the call resolves." },
2364
+ { event: "postToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PostToolUse"], why: "Append the tool result row to the session log." },
2365
+ { event: "postToolUse", matcher: ".*", hook: "directive-obligation-gate", why: "Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW)." },
2366
+ { event: "postToolUse", matcher: ".*", hook: "close-terminal-on-ship", why: "Close the Launch terminal once the spec ships." },
2367
+ { event: "postToolUse", matcher: "Write", hook: "notify-status-flip", why: "Sound on an in_review status flip." },
2368
+ { event: "postToolUse", matcher: "Write", hook: "scripts/stamp-provenance-on-edit.mjs", why: "Stamp assisted_by provenance on direct spec edits." },
2369
+ // afterFileEdit is a distinct signal, not a duplicate: it reports the edit
2370
+ // that actually landed (with old/new strings) even when the write came from a
2371
+ // path postToolUse did not describe as an edit.
2372
+ { event: "afterFileEdit", matcher: ".*", hook: "close-terminal-on-ship", why: "Ship detection from the concrete file edit." },
2373
+ // ── End of the agent turn ───────────────────────────────────────────────
2374
+ // Cursor's `stop` honors `followup_message`, which is how a blocking Stop
2375
+ // hook re-drives the agent — the same effect as Claude's exit-2 block.
2376
+ { event: "stop", hook: "emit-claim-status", args: ["--event=Stop"], why: "Mark the claim pending-on-response." },
2377
+ { event: "stop", hook: "naughty-list-gate", why: "Re-drive the turn when the final message uses banned jargon." },
2378
+ { event: "stop", hook: "id-display-format-gate", why: "Re-drive the turn when a reply breaks the Naming V3 display-id format." },
2379
+ { event: "stop", hook: "epic-milestone-phrasing-gate", why: "Re-drive the turn when a reply breaks the epic/milestone phrasing rules." },
2380
+ { event: "stop", hook: "git-reflog-audit", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
2381
+ { event: "stop", hook: "enforce-session-sequence", why: "Re-drive the turn when work shipped without record_wip." },
2382
+ { event: "stop", hook: "directive-obligation-gate", why: "Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW)." },
2383
+ // ── Session end ─────────────────────────────────────────────────────────
2384
+ { event: "sessionEnd", hook: "session-end", why: "Release the claim and notify the console." },
2385
+ { event: "sessionEnd", hook: "close-terminal-on-ship", why: "Final terminal cleanup." }
2386
+ ];
2387
+ var UNPORTABLE = [
2388
+ {
2389
+ hook: "plan-review-gate",
2390
+ claudeEvent: "PreToolUse/ExitPlanMode",
2391
+ reason: "Cursor exposes no ExitPlanMode tool call to hooks.",
2392
+ coveredBy: ".cursor/rules/plan-review-required.mdc (always-on rule)"
2393
+ },
2394
+ {
2395
+ hook: "plan-exit-ingest-gate",
2396
+ claudeEvent: "PreToolUse/ExitPlanMode",
2397
+ reason: "Same missing trigger; it is also what arms claim-before-code-gate.",
2398
+ coveredBy: ".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"
2399
+ }
2400
+ ];
2401
+ function commandFor(entry) {
2402
+ const parts = ["node", SHIM, entry.hook];
2403
+ if (entry.tool) parts.push(`--hs-tool="${entry.tool}"`);
2404
+ if (entry.claudeEvent) parts.push(`--hs-claude-event=${entry.claudeEvent}`);
2405
+ for (const arg of entry.args ?? []) parts.push(arg);
2406
+ return parts.join(" ");
2407
+ }
2408
+ function commandForPlugin(entry) {
2409
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${SHIM}"`);
2410
+ }
2411
+ var CUSTOMER_LAUNCHER = ".cursor/heyspex/run-hook.mjs";
2412
+ function commandForCustomer(entry) {
2413
+ return commandFor(entry).replace(`node ${SHIM}`, `node "$CURSOR_PROJECT_DIR/${CUSTOMER_LAUNCHER}"`);
2414
+ }
2415
+ function identityFor(entry) {
2416
+ return `${entry.hook}::${entry.tool ?? ""}::${(entry.args ?? []).join(" ")}`;
2417
+ }
2418
+ function timeoutFor(entry) {
2419
+ return entry.timeout ?? DEFAULT_TIMEOUT_SECONDS;
2420
+ }
2421
+
2422
+ // scripts/lib/customer-kit.mjs
2423
+ import { createHash } from "node:crypto";
2424
+ import fs from "node:fs";
2425
+ import path2 from "node:path";
2426
+ import os2 from "node:os";
2427
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
2428
+
2429
+ // scripts/lib/hook-dispositions.mjs
2430
+ var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
2431
+ var DISPOSITIONS = Object.freeze(["mapped", "advisory-degraded", "unportable", "not-shipped"]);
2432
+ var mapped = (note) => Object.freeze({ disposition: "mapped", ...note ? { note } : {} });
2433
+ var degraded = (note) => Object.freeze({ disposition: "advisory-degraded", note });
2434
+ var unportable = (control) => Object.freeze({ disposition: "unportable", control });
2435
+ var notShipped = Object.freeze({ disposition: "not-shipped" });
2436
+ var CC = mapped();
2437
+ 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.";
2438
+ 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.";
2439
+ 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";
2440
+ var HOOK_DISPOSITIONS = Object.freeze({
2441
+ // ── customer: session start / compaction ──────────────────────────────────
2442
+ "wip-checkpoint": {
2443
+ "claude-code": CC,
2444
+ cursor: mapped("preCompact is relayed as SessionStart(compact)."),
2445
+ copilot: mapped("sessionStart additionalContext is delivered; the preCompact leg writes WIP only (no output channel), re-injected at the next sessionStart.")
2446
+ },
2447
+ "inject-doctrine": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
2448
+ "check-auto-memory-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
2449
+ "check-claude-md-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
2450
+ "git-reflog-audit": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext and agentStop.") },
2451
+ "vault-mirror-catchup": { "claude-code": CC, cursor: mapped(), copilot: mapped("Detached side effect on sessionStart.") },
2452
+ // ── customer: every user turn ─────────────────────────────────────────────
2453
+ "preflight-reminder": {
2454
+ "claude-code": CC,
2455
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2456
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Real enforcement is the MCP preflight refusal.`)
2457
+ },
2458
+ "intent-classifier-trigger": {
2459
+ "claude-code": CC,
2460
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2461
+ copilot: degraded(
2462
+ `${COPILOT_PROMPT_DROPPED} The hook still runs and arms obligation state; the HS-20SW directive-obligation-gate turns that state into a hard deny.`
2463
+ )
2464
+ },
2465
+ "inject-communication-style": {
2466
+ "claude-code": CC,
2467
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2468
+ copilot: degraded(COPILOT_PROMPT_DROPPED)
2469
+ },
2470
+ "inject-vault-context": {
2471
+ "claude-code": CC,
2472
+ cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
2473
+ copilot: degraded(`${COPILOT_PROMPT_DROPPED} Per-prompt vault retrieval has no Copilot channel at all.`)
2474
+ },
2475
+ // ── customer: before a tool call (hard gates) ─────────────────────────────
2476
+ "git-identity-guard": { "claude-code": CC, cursor: mapped("beforeShellExecution. Always on."), copilot: mapped("preToolUse on bash/powershell. Always on.") },
2477
+ "vault-write-tier-guard": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell.") },
2478
+ "settings-write-gate": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell and the MCP settings tool.") },
2479
+ "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.") },
2480
+ "enforce-vault-naming": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
2481
+ "guard-auto-memory-writes": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
2482
+ "directive-obligation-gate": {
2483
+ "claude-code": CC,
2484
+ cursor: mapped(),
2485
+ 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.")
2486
+ },
2487
+ "claim-before-code-gate": {
2488
+ "claude-code": CC,
2489
+ 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."),
2490
+ 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.")
2491
+ },
2492
+ "capture-workflow-gate": {
2493
+ "claude-code": CC,
2494
+ cursor: mapped(),
2495
+ copilot: mapped("preToolUse on the MCP heyspex_file_spec tool; MCP tool naming in hook payloads is unverified live (matched by suffix).")
2496
+ },
2497
+ "diagnose-capture-reminder": {
2498
+ "claude-code": CC,
2499
+ cursor: mapped("preToolUse agent_message delivers the reminder."),
2500
+ copilot: degraded("Copilot preToolUse has no context channel for an inject, so this advisory is silent (lost).")
2501
+ },
2502
+ "git-stash-gate": {
2503
+ "claude-code": CC,
2504
+ cursor: mapped(),
2505
+ copilot: mapped('The Claude "ask" becomes a deny where no user is present (cloud agent, CLI autopilot).')
2506
+ },
2507
+ "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.") },
2508
+ "research-method-guard": {
2509
+ "claude-code": CC,
2510
+ cursor: mapped("preToolUse with a Skill|Workflow tool filter. Always on."),
2511
+ 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.")
2512
+ },
2513
+ "deep-research-cost-gate": {
2514
+ "claude-code": CC,
2515
+ cursor: mapped("preToolUse with a Workflow|Skill tool filter."),
2516
+ 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.")
2517
+ },
2518
+ "plan-review-gate": {
2519
+ "claude-code": CC,
2520
+ cursor: unportable(".cursor/rules/plan-review-required.mdc (always-on rule)"),
2521
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
2522
+ },
2523
+ "plan-exit-ingest-gate": {
2524
+ "claude-code": CC,
2525
+ cursor: unportable(".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"),
2526
+ copilot: unportable(COPILOT_NO_PLAN_TOOL)
2527
+ },
2528
+ // ── customer: end of turn / session ───────────────────────────────────────
2529
+ "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.') },
2530
+ "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).") },
2531
+ "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).") },
2532
+ "enforce-session-sequence": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive, capped at 8 consecutive blocks.") },
2533
+ "notify-status-flip": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
2534
+ "close-terminal-on-ship": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse / sessionEnd.") },
2535
+ "scripts/stamp-provenance-on-edit.mjs": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
2536
+ "session-end": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionEnd.") },
2537
+ // ── platform: heyspex-app development machinery (never installed in a customer repo) ──
2538
+ "inject-dev-server-awareness": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2539
+ "inject-coo-seat": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2540
+ "coo-guard-check": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2541
+ "coo-denylist-gate": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2542
+ "agent-spawn-delegation-gate": { "claude-code": CC, cursor: mapped("subagentStart."), copilot: notShipped },
2543
+ "capture-execution-gate": { "claude-code": CC, cursor: mapped("beforeShellExecution + subagentStart."), copilot: notShipped },
2544
+ "check-mcp-freshness-hook": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2545
+ "dev-server-task-reminder": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
2546
+ "env-vault-guard": { "claude-code": CC, cursor: mapped("Always on."), copilot: notShipped },
2547
+ "log-session-activity": { "claude-code": CC, cursor: mapped(), copilot: notShipped }
2548
+ });
2549
+ function dispositionFor(hook, host) {
2550
+ return HOOK_DISPOSITIONS[hook]?.[host] ?? null;
2551
+ }
2552
+
2553
+ // scripts/lib/customer-kit.mjs
2554
+ var HOOK_SCOPES = Object.freeze({
2555
+ // ── customer: vault / workflow / safety gates ─────────────────────────────
2556
+ "wip-checkpoint": { scope: "customer", why: "Previous-session WIP handoff, read from the active vault." },
2557
+ "inject-doctrine": {
2558
+ scope: "customer",
2559
+ why: "Owner ruling 2026-10-03 (HS-A2PO): ships. A customer session gets the baseline edition of SpexCode Doctrine (CUSTOMER_HOOK_DATA), the foundation its own learned layer is built on; never the platform's learned file."
2560
+ },
2561
+ "check-auto-memory-budget": { scope: "customer", why: "Auto-memory index budget for the session project." },
2562
+ "check-claude-md-budget": { scope: "customer", why: "Budget check on the project's always-loaded agent contract." },
2563
+ "git-reflog-audit": { scope: "customer", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
2564
+ "preflight-reminder": { scope: "customer", why: "Names the required procedure before filing/destructive turns." },
2565
+ "intent-classifier-trigger": { scope: "customer", why: "Routes capture/research/settings/defer intents." },
2566
+ "inject-communication-style": { scope: "customer", why: "Plain-language communication rules each turn." },
2567
+ "inject-vault-context": { scope: "customer", why: "Lexical vault retrieval for the prompt." },
2568
+ "git-identity-guard": { scope: "customer", why: "Agents never write git identity (owner ruling 2026-09-19)." },
2569
+ "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)." },
2570
+ "settings-write-gate": { scope: "customer", why: "Approval before the vault's .heyspex/config.yaml changes." },
2571
+ "emit-claim-status": { scope: "customer", why: "Claim liveness heartbeat to the console." },
2572
+ "enforce-vault-naming": { scope: "customer", why: "Naming V3 filename guard (HS-RY2N)." },
2573
+ "guard-auto-memory-writes": { scope: "customer", why: "No new topic files in the auto-memory directory." },
2574
+ "plan-review-gate": { scope: "customer", why: "Plan review + QC markers before ExitPlanMode." },
2575
+ "plan-exit-ingest-gate": { scope: "customer", why: "Plan ingest on ExitPlanMode; arms claim-before-code." },
2576
+ "claim-before-code-gate": { scope: "customer", why: "First code edit after plan exit needs a claim." },
2577
+ "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)." },
2578
+ "capture-workflow-gate": { scope: "customer", why: "No implementation spec straight out of an open capture." },
2579
+ "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)." },
2580
+ "diagnose-capture-reminder": { scope: "customer", why: "Warn when a diagnose is filed with no capture holding the owner's words (HS-IK2D)." },
2581
+ "git-stash-gate": { scope: "customer", why: "Always ask before a mutating git stash." },
2582
+ "research-method-guard": { scope: "customer", why: "Research method guard (HS-EN4F)." },
2583
+ "deep-research-cost-gate": { scope: "customer", why: "Cost confirmation before a paid deep-research launch." },
2584
+ "research-protocol-gate": { scope: "customer", why: "WebSearch/WebFetch refused until research preflight ran (HS-K0ZP, HS-EN4F)." },
2585
+ "naughty-list-gate": { scope: "customer", why: "Re-drive a final message that uses banned jargon." },
2586
+ "id-display-format-gate": { scope: "customer", why: "Naming V3 display-id format in replies." },
2587
+ "epic-milestone-phrasing-gate": { scope: "customer", why: "Epic/milestone phrasing rules in replies." },
2588
+ "enforce-session-sequence": { scope: "customer", why: "Re-drive a turn that shipped without record_wip." },
2589
+ "notify-status-flip": { scope: "customer", why: "Sound on an in_review status flip." },
2590
+ "close-terminal-on-ship": { scope: "customer", why: "Close the launch terminal once the spec ships." },
2591
+ "scripts/stamp-provenance-on-edit.mjs": { scope: "customer", why: "Stamp assisted_by provenance on direct spec edits (active vault)." },
2592
+ "session-end": { scope: "customer", why: "Release the claim and export session events." },
2593
+ // ── platform: heyspex-app's own development machinery ─────────────────────
2594
+ "inject-dev-server-awareness": { scope: "platform", why: "About the spex-console dev server in heyspex-app." },
2595
+ "inject-coo-seat": { scope: "platform", why: "dev-factory COO seat mandate." },
2596
+ "coo-guard-check": { scope: "platform", why: "dev-factory breakdown-delegation guard." },
2597
+ "coo-denylist-gate": { scope: "platform", why: "dev-factory COO window deny list." },
2598
+ "agent-spawn-delegation-gate": { scope: "platform", why: "dev-factory seat routing for subagents." },
2599
+ "capture-execution-gate": { scope: "platform", why: "Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8)." },
2600
+ "check-mcp-freshness-hook": { scope: "platform", why: "Freshness of the vendored MCP build \u2014 a platform-dev concern." },
2601
+ "dev-server-task-reminder": { scope: "platform", why: "spex-console dev server + Neon DB safety." },
2602
+ "env-vault-guard": { scope: "platform", why: "Guards heyspex-app's encrypted env vault paths." },
2603
+ "log-session-activity": { scope: "platform", why: "Resolves .heyspex state from the platform checkout's own location, not the project \u2014 would log customer sessions into heyspex-app." }
2604
+ });
2605
+ var CUSTOMER_HOOK_DATA = Object.freeze({
2606
+ "inject-doctrine": Object.freeze(["spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json"])
2607
+ });
2608
+ var CLAUDE_HOOK_ROWS = Object.freeze([
2609
+ { event: "SessionStart", hook: "wip-checkpoint", timeout: 5 },
2610
+ { event: "SessionStart", hook: "inject-dev-server-awareness", timeout: 5 },
2611
+ { event: "SessionStart", hook: "inject-coo-seat", timeout: 5 },
2612
+ { event: "SessionStart", hook: "inject-doctrine", timeout: 5 },
2613
+ { event: "SessionStart", hook: "coo-guard-check", timeout: 5 },
2614
+ { event: "SessionStart", hook: "check-auto-memory-budget", timeout: 5 },
2615
+ { event: "SessionStart", hook: "check-claude-md-budget", timeout: 5 },
2616
+ { event: "SessionStart", hook: "check-mcp-freshness-hook", timeout: 5 },
2617
+ { event: "SessionStart", hook: "git-reflog-audit", timeout: 5 },
2618
+ { event: "SessionStart", hook: "vault-mirror-catchup", timeout: 5 },
2619
+ { event: "PreCompact", hook: "wip-checkpoint", timeout: 5 },
2620
+ { event: "UserPromptSubmit", hook: "preflight-reminder", timeout: 5 },
2621
+ { event: "UserPromptSubmit", hook: "intent-classifier-trigger", timeout: 5 },
2622
+ { event: "UserPromptSubmit", hook: "inject-communication-style", timeout: 5 },
2623
+ { event: "UserPromptSubmit", hook: "inject-vault-context", timeout: 10 },
2624
+ { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "env-vault-guard", timeout: 5 },
2625
+ { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-identity-guard", timeout: 5 },
2626
+ { event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "vault-write-tier-guard", timeout: 5 },
2627
+ { event: "PreToolUse", matcher: "Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set", hook: "settings-write-gate", timeout: 5 },
2628
+ { event: "PreToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], timeout: 5 },
2629
+ { event: "PreToolUse", matcher: ".*", hook: "coo-denylist-gate", timeout: 5 },
2630
+ { event: "PreToolUse", matcher: "Agent|Task", hook: "agent-spawn-delegation-gate", timeout: 5 },
2631
+ { event: "PreToolUse", matcher: "Bash|PowerShell|Agent|Task", hook: "capture-execution-gate", timeout: 5 },
2632
+ { event: "PreToolUse", matcher: "Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "directive-obligation-gate", timeout: 5 },
2633
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_claim_spec", hook: "coo-guard-check", timeout: 5 },
2634
+ { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "enforce-vault-naming", timeout: 5 },
2635
+ { event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "guard-auto-memory-writes", timeout: 5 },
2636
+ { event: "PreToolUse", matcher: "Edit|MultiEdit|Write|Bash|PowerShell", hook: "preflight-reminder", timeout: 5 },
2637
+ { event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-review-gate", timeout: 8 },
2638
+ { event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-exit-ingest-gate", timeout: 5 },
2639
+ { event: "PreToolUse", matcher: "Edit|MultiEdit|Write", hook: "claim-before-code-gate", timeout: 5 },
2640
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "capture-workflow-gate", timeout: 8 },
2641
+ { event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "diagnose-capture-reminder", timeout: 5 },
2642
+ { 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 },
2643
+ { event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-stash-gate", timeout: 10 },
2644
+ { event: "PreToolUse", matcher: "Skill|Workflow", hook: "research-method-guard", timeout: 5 },
2645
+ { event: "PreToolUse", matcher: "Workflow|Skill", hook: "deep-research-cost-gate", timeout: 5 },
2646
+ { event: "PreToolUse", matcher: "WebSearch|WebFetch|mcp__heyspex__heyspex_save_research|mcp__plugin_heyspex_heyspex__heyspex_save_research", hook: "research-protocol-gate", timeout: 5 },
2647
+ { event: "PreToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
2648
+ { event: "Stop", hook: "emit-claim-status", args: ["--event=Stop"], timeout: 5 },
2649
+ { event: "Stop", hook: "naughty-list-gate", timeout: 5 },
2650
+ { event: "Stop", hook: "id-display-format-gate", timeout: 5 },
2651
+ { event: "Stop", hook: "epic-milestone-phrasing-gate", timeout: 5 },
2652
+ { event: "Stop", hook: "git-reflog-audit", timeout: 5 },
2653
+ { event: "Stop", hook: "enforce-session-sequence", timeout: 5 },
2654
+ { event: "Stop", hook: "directive-obligation-gate", timeout: 5 },
2655
+ { event: "PostToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], timeout: 5 },
2656
+ { 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 },
2657
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "notify-status-flip", timeout: 5 },
2658
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "close-terminal-on-ship", timeout: 5 },
2659
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "scripts/stamp-provenance-on-edit.mjs", timeout: 5 },
2660
+ { event: "PostToolUse", matcher: ".*", hook: "log-session-activity", timeout: 5 },
2661
+ { event: "SessionEnd", hook: "session-end", timeout: 5 },
2662
+ { event: "SessionEnd", hook: "close-terminal-on-ship", timeout: 5 }
2663
+ ]);
2664
+ var COMMAND_SCOPES = Object.freeze({
2665
+ "hs-backfill.md": { scope: "customer", why: "Backfill completed work into the vault." },
2666
+ "hs-capture.md": { scope: "customer", why: "Capture intake." },
2667
+ "hs-defer.md": { scope: "customer", why: "Split + defer remaining work." },
2668
+ "hs-doctor.md": { scope: "customer", why: "Install-surface repair when heyspex_* tools are missing." },
2669
+ "hs-ground.md": { scope: "customer", why: "Pre-plan grounding report." },
2670
+ "hs-handoff.md": { scope: "customer", why: "Short-code lane handoff." },
2671
+ "hs-next.md": { scope: "customer", why: "On-deck prompt + claim handshake." },
2672
+ "hs-promote.md": { scope: "customer", why: "Capture \u2192 research \u2192 milestone promotion." },
2673
+ "hs-push.md": { scope: "customer", why: "Push an epic plan into the vault." },
2674
+ "hs-qc.md": { scope: "customer", why: "Post-plan QC gate." },
2675
+ "hs-res.md": { scope: "customer", why: "Research method entrypoint (SpexCrawl/DeepSeek)." },
2676
+ "hs-resolve.md": { scope: "customer", why: "Resolution lane." },
2677
+ "hs-review.md": { scope: "customer", why: "Review queue." },
2678
+ "hs-view.md": { scope: "customer", why: "Open a console surface." },
2679
+ "hs-auto.md": { scope: "platform", why: "Routes work into dev-factory (platform build system)." },
2680
+ "hs-dev.md": { scope: "platform", why: "Admin/dev-only HeySpex dev/prod mode switch." },
2681
+ "hs-log.md": { scope: "platform", why: "Admin/dev-only dev-server log watcher." },
2682
+ "kill-ports.md": { scope: "platform", why: "Kills local dev-server ports." },
2683
+ "prototype.md": { scope: "platform", why: "Console design-preview prototypes inside spex-console." }
2684
+ });
2685
+ function hookScope(hook) {
2686
+ return HOOK_SCOPES[hook]?.scope ?? null;
2687
+ }
2688
+ function toPosixPath(p) {
2689
+ return String(p).replace(/\\/g, "/").replace(/\/+$/, "");
2690
+ }
2691
+ function samePlatformPath(a, b) {
2692
+ if (!a || !b) return false;
2693
+ const norm = (p) => {
2694
+ const s = toPosixPath(path2.resolve(p));
2695
+ return process.platform === "win32" ? s.toLowerCase() : s;
2696
+ };
2697
+ return norm(a) === norm(b);
2698
+ }
2699
+ var MCP_PROJECT_PREFIX = "mcp__heyspex__";
2700
+ var MCP_PLUGIN_PREFIX = "mcp__plugin_heyspex_heyspex__";
2701
+ function widenMcpMatcher(matcher) {
2702
+ if (!matcher) return matcher;
2703
+ const parts = matcher.split("|");
2704
+ const out = [];
2705
+ for (const part of parts) {
2706
+ if (!out.includes(part)) out.push(part);
2707
+ if (part.startsWith(MCP_PROJECT_PREFIX)) {
2708
+ const plugin = MCP_PLUGIN_PREFIX + part.slice(MCP_PROJECT_PREFIX.length);
2709
+ if (!parts.includes(plugin) && !out.includes(plugin)) out.push(plugin);
2710
+ }
2711
+ }
2712
+ return out.join("|");
2713
+ }
2714
+ function customerHookRows() {
2715
+ return CLAUDE_HOOK_ROWS.filter((r) => hookScope(r.hook) === "customer");
2716
+ }
2717
+ function hookTail(row) {
2718
+ return [row.hook, ...row.args ?? []].join(" ");
2719
+ }
2720
+ var CLAUDE_HOOK_SHIM = "scripts/claude-hooks/_run.mjs";
2721
+ var CURSOR_HOOK_SHIM = "scripts/cursor-hooks/_run.mjs";
2722
+ var CUSTOMER_LAUNCHERS = Object.freeze({
2723
+ claude: Object.freeze({
2724
+ rel: Object.freeze([".claude", "heyspex", "run-hook.mjs"]),
2725
+ projectDirEnv: "CLAUDE_PROJECT_DIR",
2726
+ shim: CLAUDE_HOOK_SHIM,
2727
+ registry: ".claude/settings.json"
2728
+ }),
2729
+ cursor: Object.freeze({
2730
+ rel: Object.freeze([".cursor", "heyspex", "run-hook.mjs"]),
2731
+ projectDirEnv: "CURSOR_PROJECT_DIR",
2732
+ shim: CURSOR_HOOK_SHIM,
2733
+ registry: ".cursor/hooks.json"
2734
+ })
2735
+ });
2736
+ 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}}";
2737
+ function buildBootstrap({ plugin = false } = {}) {
2738
+ const walk = BOOTSTRAP_WALK;
2739
+ 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)}";
2740
+ 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");
2741
+ }
2742
+ var PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
2743
+ var PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
2744
+ function buildCustomerBootstrap() {
2745
+ const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(",");
2746
+ 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("/");
2747
+ }
2748
+ var CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
2749
+ var LAUNCHER_TEMPLATE = [
2750
+ "#!/usr/bin/env node",
2751
+ "// HeySpex hook launcher -- GENERATED by hs install / hs connect. Commit it with this repo.",
2752
+ "//",
2753
+ "// Every HeySpex hook registered in __REGISTRY__ runs",
2754
+ '// node "$__ENV__/__LAUNCHER__" <hook> [args]',
2755
+ "// This file carries NO machine-specific path. It finds the heyspex package this",
2756
+ "// repo depends on (node_modules/heyspex) and hands off in-process to the",
2757
+ "// package's hook shim (__SHIM__), so the same committed command works on every",
2758
+ "// machine the repo is cloned to.",
2759
+ "//",
2760
+ "// Resolution order (the first root that carries the shim wins):",
2761
+ "// 1. HEYSPEX_PLATFORM_ROOT explicit machine-local override",
2762
+ "// 2. HEYSPEX_MCP_ENTRY -> its package root machine-local override",
2763
+ "// 3. node_modules/heyspex, walking up from this file (the repo-local dependency)",
2764
+ "// 4. node_modules/heyspex up from $__ENV__, $CLAUDE_PROJECT_DIR, $CURSOR_PROJECT_DIR, cwd",
2765
+ "// 5. <npm root -g>/heyspex a global install, last resort",
2766
+ "// Nothing found -> one stderr line and exit 0 (fail-open, like the shim itself).",
2767
+ "import { spawnSync } from 'node:child_process';",
2768
+ "import { existsSync } from 'node:fs';",
2769
+ "import { dirname, resolve } from 'node:path';",
2770
+ "import { fileURLToPath, pathToFileURL } from 'node:url';",
2771
+ "",
2772
+ "const SHIM = '__SHIM__'.split('/');",
2773
+ "const here = dirname(fileURLToPath(import.meta.url));",
2774
+ "const env = process.env;",
2775
+ "const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);",
2776
+ "const hasShim = (root) => Boolean(root) && existsSync(resolve(root, ...SHIM));",
2777
+ "",
2778
+ "function packageUpFrom(start) {",
2779
+ " let dir = resolve(start);",
2780
+ " for (;;) {",
2781
+ " const candidate = resolve(dir, 'node_modules', 'heyspex');",
2782
+ " if (hasShim(candidate)) return candidate;",
2783
+ " const parent = dirname(dir);",
2784
+ " if (parent === dir) return null;",
2785
+ " dir = parent;",
2786
+ " }",
2787
+ "}",
2788
+ "",
2789
+ "function globalPackage() {",
2790
+ " try {",
2791
+ " // One command string through the shell: npm is a .cmd on Windows, and a",
2792
+ " // shell + args array is what node deprecates (DEP0190).",
2793
+ " const res = spawnSync('npm root -g', { encoding: 'utf8', timeout: 5000, shell: true });",
2794
+ " const root = String(res.stdout ?? '').trim();",
2795
+ " return root ? resolve(root, 'heyspex') : null;",
2796
+ " } catch {",
2797
+ " return null;",
2798
+ " }",
2799
+ "}",
2800
+ "",
2801
+ "const mcpEntry = text(env.HEYSPEX_MCP_ENTRY);",
2802
+ "const starts = [...new Set([here, text(env.__ENV__), text(env.CLAUDE_PROJECT_DIR), text(env.CURSOR_PROJECT_DIR), process.cwd()].filter(Boolean))];",
2803
+ "let root =",
2804
+ " [text(env.HEYSPEX_PLATFORM_ROOT), mcpEntry && resolve(mcpEntry, '..', '..', '..')].find(hasShim) ??",
2805
+ " starts.map(packageUpFrom).find(Boolean) ??",
2806
+ " null;",
2807
+ "if (!root) {",
2808
+ " const global = globalPackage();",
2809
+ " if (hasShim(global)) root = global;",
2810
+ "}",
2811
+ "",
2812
+ "if (!root) {",
2813
+ " process.stderr.write(",
2814
+ " '[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: no heyspex package found ' +",
2815
+ ` '(run "npm install heyspex" in this repo, or set HEYSPEX_PLATFORM_ROOT). Looked for __SHIM__ under ' +`,
2816
+ " 'HEYSPEX_PLATFORM_ROOT, HEYSPEX_MCP_ENTRY, node_modules/heyspex up from ' + starts.join(', ') + ', and npm root -g.\\n',",
2817
+ " );",
2818
+ " process.exit(0);",
2819
+ "}",
2820
+ "",
2821
+ "// argv is already [node, <this file>, <hook>, ...args] -- the shape the shim",
2822
+ '// reads; the shim path replaces this file so a shim guarded by "am I main?"',
2823
+ "// (the Cursor one) runs exactly as if Cursor had invoked it directly.",
2824
+ "const shim = resolve(root, ...SHIM);",
2825
+ "process.argv[1] = shim;",
2826
+ "try {",
2827
+ " await import(pathToFileURL(shim).href);",
2828
+ "} catch (err) {",
2829
+ " process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
2830
+ " process.exit(0);",
2831
+ "}",
2832
+ ""
2833
+ ].join("\n");
2834
+ function renderCustomerLauncher(surface = "claude") {
2835
+ const l = CUSTOMER_LAUNCHERS[surface];
2836
+ return LAUNCHER_TEMPLATE.replace(
2837
+ /__(REGISTRY|ENV|LAUNCHER|SHIM)__/g,
2838
+ (_m, token) => token === "REGISTRY" ? l.registry : token === "ENV" ? l.projectDirEnv : token === "LAUNCHER" ? l.rel.join("/") : l.shim
2839
+ );
2840
+ }
2841
+ function planCustomerLauncherWrite(existing, surface = "claude") {
2842
+ const text = renderCustomerLauncher(surface);
2843
+ if (existing === null || existing === void 0) return { write: true, reason: "missing", text };
2844
+ if (String(existing).replace(/\r\n/g, "\n") === text) return { write: false, reason: "unchanged", text };
2845
+ return { write: true, reason: "update", text };
2846
+ }
2847
+ function renderCustomerHooks(runnerPrefix) {
2848
+ return customerHookRows().map((row) => ({
2849
+ event: row.event,
2850
+ ...row.matcher ? { matcher: widenMcpMatcher(row.matcher) } : {},
2851
+ command: `${runnerPrefix} ${hookTail(row)}`,
2852
+ timeout: row.timeout,
2853
+ tail: hookTail(row)
2854
+ }));
2855
+ }
2856
+ function renderPluginHooksDocument() {
2857
+ const hooks = {};
2858
+ for (const d of renderCustomerHooks(PLUGIN_RUNNER_PREFIX)) {
2859
+ (hooks[d.event] ??= []).push({
2860
+ ...d.matcher ? { matcher: d.matcher } : {},
2861
+ hooks: [{ type: "command", command: d.command, timeout: d.timeout }]
2862
+ });
2863
+ }
2864
+ return {
2865
+ hooks
2866
+ };
2867
+ }
2868
+ function renderPluginHooksText() {
2869
+ return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}
2870
+ `;
2871
+ }
2872
+ var COPILOT_EVENT_NAMES = Object.freeze({
2873
+ SessionStart: "sessionStart",
2874
+ SessionEnd: "sessionEnd",
2875
+ UserPromptSubmit: "userPromptSubmitted",
2876
+ PreToolUse: "preToolUse",
2877
+ PostToolUse: "postToolUse",
2878
+ Stop: "agentStop",
2879
+ PreCompact: "preCompact"
2880
+ });
2881
+ var GITHUB_HOOKS_STATE_ARMING = Object.freeze(["intent-classifier-trigger"]);
2882
+ var COPILOT_TOOL_NAMES = Object.freeze({
2883
+ Write: "create",
2884
+ Edit: "edit",
2885
+ MultiEdit: "edit",
2886
+ NotebookEdit: "edit",
2887
+ Bash: "bash",
2888
+ PowerShell: "powershell",
2889
+ Agent: "task",
2890
+ Task: "task",
2891
+ WebFetch: "web_fetch",
2892
+ WebSearch: "web_search"
2893
+ });
2894
+ function toCopilotMatcher(matcher) {
2895
+ if (!matcher) return void 0;
2896
+ const out = [];
2897
+ for (const part of matcher.split("|")) {
2898
+ let t = null;
2899
+ if (part === ".*") t = ".*";
2900
+ else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
2901
+ else if (part.startsWith(MCP_PROJECT_PREFIX)) t = ".*" + part.slice(MCP_PROJECT_PREFIX.length);
2902
+ else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = ".*" + part.slice(MCP_PLUGIN_PREFIX.length);
2903
+ if (t && !out.includes(t)) out.push(t);
2904
+ }
2905
+ if (out.includes(".*")) return ".*";
2906
+ return out.length ? out.join("|") : null;
2907
+ }
2908
+ function githubHookNames() {
2909
+ const names = /* @__PURE__ */ new Set();
2910
+ for (const r of customerHookRows()) {
2911
+ if (dispositionFor(r.hook, "copilot")?.disposition === "mapped" || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
2912
+ }
2913
+ return names;
2914
+ }
2915
+ function githubHookCommand(row) {
2916
+ const tail = hookTail(row);
2917
+ 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}`;
2918
+ }
2919
+ function renderGithubHooksDocument() {
2920
+ const allowed = githubHookNames();
2921
+ const hooks = {};
2922
+ for (const row of customerHookRows()) {
2923
+ if (!allowed.has(row.hook)) continue;
2924
+ const event = COPILOT_EVENT_NAMES[row.event];
2925
+ if (!event) continue;
2926
+ const matcher = toCopilotMatcher(row.matcher);
2927
+ if (matcher === null) continue;
2928
+ (hooks[event] ??= []).push({
2929
+ type: "command",
2930
+ ...matcher ? { matcher } : {},
2931
+ bash: githubHookCommand(row),
2932
+ timeoutSec: row.timeout
2933
+ });
2934
+ }
2935
+ return { version: 1, hooks };
2936
+ }
2937
+ function renderGithubHooksText() {
2938
+ return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}
2939
+ `;
2940
+ }
2941
+ var HARNESS_COMMAND_SURFACES = Object.freeze([
2942
+ {
2943
+ installedDir: Object.freeze([".claude", "commands"]),
2944
+ packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "claude"]),
2945
+ devSourceDir: Object.freeze([".claude", "commands"])
2946
+ },
2947
+ {
2948
+ installedDir: Object.freeze([".cursor", "commands"]),
2949
+ packagedDir: Object.freeze(["dist", "cli-templates", "harness-commands", "cursor"]),
2950
+ devSourceDir: Object.freeze([".cursor", "commands"])
2951
+ }
2952
+ ]);
2953
+ var NPM_ALIASES = Object.freeze({ doctor: "doctor", setup: "doctor --fix" });
2954
+ var SCRIPT_VERBS = Object.freeze({
2955
+ "scripts/heyspex-doctor.mjs": "doctor",
2956
+ "scripts/hs-view.mjs": "view",
2957
+ "scripts/hs-capture-cli.mjs": "capture",
2958
+ "scripts/hs-next.mjs": "next",
2959
+ "scripts/hs-mode.mjs": "mode",
2960
+ "scripts/hs-settings.mjs": "settings",
2961
+ "scripts/hs-companion.mjs": "companion",
2962
+ "scripts/wip-ledger-health.mjs": "wip",
2963
+ "scripts/dev-server-status.mjs": "server status",
2964
+ "scripts/dev-server-stop.mjs": "server stop"
2965
+ });
2966
+ var PORTABLE_PACKAGE_DIR = "node_modules/heyspex";
2967
+ var fsDeps = {
2968
+ joinPath: (...s) => path2.join(...s),
2969
+ readFile: (p) => {
2970
+ try {
2971
+ return fs.readFileSync(p, "utf8");
2972
+ } catch {
2973
+ return null;
2974
+ }
2975
+ },
2976
+ listDir: (d) => {
2977
+ try {
2978
+ return fs.readdirSync(d);
2979
+ } catch {
2980
+ return [];
2981
+ }
2982
+ }
2983
+ };
2984
+ function invokedDirectly(importMetaUrl, selfBasename, argv1 = process.argv[1]) {
2985
+ try {
2986
+ if (!argv1) return false;
2987
+ const self = fileURLToPath2(importMetaUrl);
2988
+ if (path2.basename(self) !== selfBasename) return false;
2989
+ return pathToFileURL(path2.resolve(argv1)).href === pathToFileURL(self).href;
2990
+ } catch {
2991
+ return false;
2992
+ }
2993
+ }
2994
+ var SELF = fileURLToPath2(import.meta.url);
2995
+ if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
2996
+ const platformRoot = path2.resolve(path2.dirname(SELF), "..", "..");
2997
+ const target = path2.join(platformRoot, "plugins", "heyspex", "hooks", "hooks.json");
2998
+ const want = renderPluginHooksText();
2999
+ const same = (fsDeps.readFile(target) ?? "").replace(/\r\n/g, "\n") === want;
3000
+ if (process.argv.includes("--write-plugin-hooks")) {
3001
+ if (!same) fs.writeFileSync(target, want, "utf8");
3002
+ console.log(`customer-kit: ${same ? "unchanged" : "wrote"} ${path2.relative(platformRoot, target)}`);
3003
+ } else if (process.argv.includes("--check-plugin-hooks")) {
3004
+ if (!same) {
3005
+ console.error("customer-kit: plugins/heyspex/hooks/hooks.json is stale \u2014 run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`");
3006
+ process.exit(1);
3007
+ }
3008
+ console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
3009
+ } else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
3010
+ const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
3011
+ const ghWant = renderGithubHooksText();
3012
+ const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
3013
+ if (process.argv.includes("--write-github-hooks")) {
3014
+ if (!ghSame) {
3015
+ fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
3016
+ fs.writeFileSync(ghTarget, ghWant, "utf8");
3017
+ }
3018
+ console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
3019
+ } else if (!ghSame) {
3020
+ console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
3021
+ process.exit(1);
3022
+ } else {
3023
+ console.log("customer-kit: .github/hooks/heyspex.json up to date");
3024
+ }
3025
+ } else {
3026
+ console.log("usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks");
3027
+ }
3028
+ }
3029
+
3030
+ // scripts/install-cursor-hooks.mjs
3031
+ var PLATFORM_ROOT = path3.resolve(path3.dirname(fileURLToPath3(import.meta.url)), "..");
3032
+ var REPO_ROOT = process.env.HS_SETUP_TARGET?.trim() ? path3.resolve(process.env.HS_SETUP_TARGET.trim()) : PLATFORM_ROOT;
3033
+ var SETTINGS_PATH = path3.join(REPO_ROOT, ".cursor", "hooks.json");
3034
+ function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
3035
+ const customer = !samePlatformPath(targetRoot, platformRoot);
3036
+ return {
3037
+ customer,
3038
+ hooks: customer ? HOOKS.filter((h) => hookScope(h.hook) === "customer") : HOOKS,
3039
+ commandFor: customer ? commandForCustomer : commandFor,
3040
+ launcher: customer ? { rel: CUSTOMER_LAUNCHERS.cursor.rel, plan: planCustomerLauncherWrite } : null
3041
+ };
3042
+ }
3043
+ var KIT = cursorKitFor(REPO_ROOT);
3044
+ var ACTIVE_HOOKS = KIT.hooks;
3045
+ var LAUNCHER_PATH = path3.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
3046
+ var LEGACY_MARKER = "scripts/claude-hooks/";
3047
+ function readSettings() {
3048
+ try {
3049
+ const raw = fs2.readFileSync(SETTINGS_PATH, "utf8").trim();
3050
+ if (!raw) return { version: 1, hooks: {} };
3051
+ const parsed = JSON.parse(raw);
3052
+ if (parsed && typeof parsed === "object") {
3053
+ parsed.version ??= 1;
3054
+ parsed.hooks ??= {};
3055
+ return parsed;
3056
+ }
3057
+ } catch (err) {
3058
+ if (err && err.code === "ENOENT") return { version: 1, hooks: {} };
3059
+ throw err;
3060
+ }
3061
+ return { version: 1, hooks: {} };
3062
+ }
3063
+ function writeSettings(settings) {
3064
+ fs2.mkdirSync(path3.dirname(SETTINGS_PATH), { recursive: true });
3065
+ fs2.writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}
3066
+ `, "utf8");
3067
+ }
3068
+ var commandOf = (entry) => entry && typeof entry.command === "string" ? entry.command : "";
3069
+ var isOurRunner = (token) => token.includes(SHIM) || token.includes(CUSTOMER_LAUNCHER);
3070
+ var isShimEntry = (raw) => isOurRunner(commandOf(raw));
3071
+ var isLegacyEntry = (raw) => !isShimEntry(raw) && commandOf(raw).includes(LEGACY_MARKER);
3072
+ function identityOfCommand(command) {
3073
+ const tokens = command.trim().split(/\s+/);
3074
+ const shimAt = tokens.findIndex(isOurRunner);
3075
+ if (shimAt === -1) return null;
3076
+ const rest = tokens.slice(shimAt + 1);
3077
+ const hook = rest[0] ?? "";
3078
+ let tool = "";
3079
+ const args = [];
3080
+ for (const token of rest.slice(1)) {
3081
+ if (token.startsWith("--hs-tool=")) {
3082
+ tool = token.slice("--hs-tool=".length).replace(/^"([\s\S]*)"$/, "$1");
3083
+ } else if (token.startsWith("--hs-claude-event=")) continue;
3084
+ else args.push(token);
3085
+ }
3086
+ return `${hook}::${tool}::${args.join(" ")}`;
3087
+ }
3088
+ function desiredEntry(entry, render = KIT.commandFor) {
3089
+ return {
3090
+ command: render(entry),
3091
+ timeout: timeoutFor(entry),
3092
+ ...entry.matcher ? { matcher: entry.matcher } : {}
3093
+ };
3094
+ }
3095
+ function sameEntry(raw, entry, render = KIT.commandFor) {
3096
+ const want = desiredEntry(entry, render);
3097
+ return commandOf(raw) === want.command && raw.timeout === want.timeout && (raw.matcher ?? void 0) === (want.matcher ?? void 0);
3098
+ }
3099
+ var PLUGIN_HOOKS_PATH = path3.join(REPO_ROOT, "plugins", "heyspex", "hooks", "cursor-hooks.json");
3100
+ function pluginHooksDocument() {
3101
+ const hooks = {};
3102
+ for (const entry of HOOKS) {
3103
+ hooks[entry.event] ??= [];
3104
+ hooks[entry.event].push({
3105
+ command: commandForPlugin(entry),
3106
+ timeout: timeoutFor(entry),
3107
+ ...entry.matcher ? { matcher: entry.matcher } : {}
3108
+ });
3109
+ }
3110
+ return {
3111
+ _comment: "GENERATED by scripts/install-cursor-hooks.mjs from scripts/cursor-hooks/manifest.mjs \u2014 do not edit by hand; run `npm run hooks:install-cursor`. Commands resolve through $CURSOR_PROJECT_DIR because the shim and hook bodies live in the host vault checkout, not the plugin cache. WARNING: enabling this plugin in a repo whose .cursor/hooks.json still registers the same hooks runs every hook twice \u2014 validate in a fresh clone, or run `node scripts/install-cursor-hooks.mjs --uninstall` there first.",
3112
+ version: 1,
3113
+ hooks
3114
+ };
3115
+ }
3116
+ function pluginHooksText() {
3117
+ return `${JSON.stringify(pluginHooksDocument(), null, 2)}
3118
+ `;
3119
+ }
3120
+ function syncPluginHooks() {
3121
+ const want = pluginHooksText();
3122
+ let have = null;
3123
+ try {
3124
+ have = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
3125
+ } catch {
3126
+ have = null;
3127
+ }
3128
+ if (have === want) return false;
3129
+ fs2.mkdirSync(path3.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
3130
+ fs2.writeFileSync(PLUGIN_HOOKS_PATH, want, "utf8");
3131
+ return true;
3132
+ }
3133
+ function mergeCursorHooks(settings, { hooks = ACTIVE_HOOKS, commandFor: render = KIT.commandFor } = {}) {
3134
+ settings.version = 1;
3135
+ settings.hooks ??= {};
3136
+ const added = [];
3137
+ const repaired = [];
3138
+ const removed = [];
3139
+ for (const [event, entries] of Object.entries(settings.hooks)) {
3140
+ if (!Array.isArray(entries)) continue;
3141
+ const keep = entries.filter((raw) => {
3142
+ if (!isLegacyEntry(raw)) return true;
3143
+ removed.push(`${event}: ${commandOf(raw)}`);
3144
+ return false;
3145
+ });
3146
+ settings.hooks[event] = keep;
3147
+ }
3148
+ for (const entry of hooks) {
3149
+ settings.hooks[entry.event] ??= [];
3150
+ const entries = settings.hooks[entry.event];
3151
+ const identity = identityFor(entry);
3152
+ const existing = entries.filter(
3153
+ (raw2) => isShimEntry(raw2) && identityOfCommand(commandOf(raw2)) === identity
3154
+ );
3155
+ if (existing.length === 0) {
3156
+ entries.push(desiredEntry(entry, render));
3157
+ added.push(`${entry.event}/${entry.hook}`);
3158
+ continue;
3159
+ }
3160
+ for (const extra of existing.slice(1)) {
3161
+ entries.splice(entries.indexOf(extra), 1);
3162
+ removed.push(`${entry.event}: duplicate ${entry.hook}`);
3163
+ }
3164
+ const raw = existing[0];
3165
+ if (!sameEntry(raw, entry, render)) {
3166
+ Object.assign(raw, desiredEntry(entry, render));
3167
+ if (!entry.matcher && "matcher" in raw) delete raw.matcher;
3168
+ repaired.push(`${entry.event}/${entry.hook}`);
3169
+ }
3170
+ }
3171
+ for (const [event, entries] of Object.entries(settings.hooks)) {
3172
+ if (!Array.isArray(entries)) continue;
3173
+ const wanted = new Set(hooks.filter((h) => h.event === event).map(identityFor));
3174
+ settings.hooks[event] = entries.filter((raw) => {
3175
+ if (!isShimEntry(raw)) return true;
3176
+ if (wanted.has(identityOfCommand(commandOf(raw)))) return true;
3177
+ removed.push(`${event}: retired ${commandOf(raw)}`);
3178
+ return false;
3179
+ });
3180
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
3181
+ }
3182
+ return { added, repaired, removed };
3183
+ }
3184
+ function launcherPlan() {
3185
+ let existing = null;
3186
+ try {
3187
+ existing = fs2.readFileSync(LAUNCHER_PATH, "utf8");
3188
+ } catch {
3189
+ existing = null;
3190
+ }
3191
+ return planCustomerLauncherWrite(existing, "cursor");
3192
+ }
3193
+ function install({ json = false } = {}) {
3194
+ const settings = readSettings();
3195
+ const { added, repaired, removed } = mergeCursorHooks(settings);
3196
+ const settingsChanged = added.length + repaired.length + removed.length > 0;
3197
+ if (settingsChanged) writeSettings(settings);
3198
+ let launcherWritten = false;
3199
+ if (KIT.customer) {
3200
+ const plan = launcherPlan();
3201
+ if (plan.write) {
3202
+ fs2.mkdirSync(path3.dirname(LAUNCHER_PATH), { recursive: true });
3203
+ fs2.writeFileSync(LAUNCHER_PATH, plan.text, "utf8");
3204
+ launcherWritten = true;
3205
+ }
3206
+ }
3207
+ const pluginChanged = KIT.customer ? false : syncPluginHooks();
3208
+ if (json) {
3209
+ console.log(
3210
+ JSON.stringify(
3211
+ {
3212
+ ok: true,
3213
+ changed: settingsChanged || pluginChanged || launcherWritten,
3214
+ added,
3215
+ repaired,
3216
+ removed,
3217
+ pluginChanged,
3218
+ launcherWritten,
3219
+ total: ACTIVE_HOOKS.length,
3220
+ customer: KIT.customer
3221
+ },
3222
+ null,
3223
+ 2
3224
+ )
3225
+ );
3226
+ return;
3227
+ }
3228
+ if (pluginChanged) {
3229
+ console.log(`install-cursor-hooks: regenerated ${path3.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
3230
+ }
3231
+ if (launcherWritten) {
3232
+ console.log(`install-cursor-hooks: wrote ${path3.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
3233
+ }
3234
+ if (!settingsChanged) {
3235
+ console.log(`install-cursor-hooks: already installed (${ACTIVE_HOOKS.length} hooks, no changes).`);
3236
+ return;
3237
+ }
3238
+ const parts = [];
3239
+ if (added.length) parts.push(`added ${added.length}`);
3240
+ if (repaired.length) parts.push(`repaired ${repaired.length}`);
3241
+ if (removed.length) parts.push(`removed ${removed.length}`);
3242
+ console.log(
3243
+ `install-cursor-hooks: ${parts.join("; ")} \u2192 ${path3.relative(REPO_ROOT, SETTINGS_PATH)} (${ACTIVE_HOOKS.length} hooks registered)`
3244
+ );
3245
+ for (const line of removed) console.log(` - ${line}`);
3246
+ console.log(
3247
+ KIT.customer ? ` Launcher: ${CUSTOMER_LAUNCHER} \u2192 node_modules/heyspex/${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)` : ` Shim: ${SHIM} (HeySpex off-switch + Cursor\u2194Claude payload translation)`
3248
+ );
3249
+ for (const gap of UNPORTABLE) {
3250
+ console.log(` Not registerable in Cursor: ${gap.hook} (${gap.reason}) \u2192 covered by ${gap.coveredBy}`);
3251
+ }
3252
+ }
3253
+ function check({ json = false } = {}) {
3254
+ const settings = readSettings();
3255
+ const issues = [];
3256
+ const entriesFor = (event) => Array.isArray(settings?.hooks?.[event]) ? settings.hooks[event] : [];
3257
+ for (const entry of ACTIVE_HOOKS) {
3258
+ const identity = identityFor(entry);
3259
+ const ours = entriesFor(entry.event).filter(
3260
+ (raw) => isShimEntry(raw) && identityOfCommand(commandOf(raw)) === identity
3261
+ );
3262
+ if (ours.length === 0) {
3263
+ issues.push(`${entry.event}: missing ${entry.hook}`);
3264
+ continue;
3265
+ }
3266
+ if (ours.length > 1) issues.push(`${entry.event}: ${entry.hook} registered ${ours.length}\xD7`);
3267
+ for (const raw of ours) {
3268
+ if (!sameEntry(raw, entry)) {
3269
+ issues.push(
3270
+ `${entry.event}/${entry.hook}: drift \u2014 expected \`${KIT.commandFor(entry)}\` (timeout ${timeoutFor(entry)}, matcher ${entry.matcher ?? "none"}), found \`${commandOf(raw)}\` (timeout ${raw.timeout}, matcher ${raw.matcher ?? "none"})`
3271
+ );
3272
+ }
3273
+ }
3274
+ }
3275
+ for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
3276
+ if (!Array.isArray(entries)) continue;
3277
+ for (const raw of entries) {
3278
+ if (isLegacyEntry(raw)) {
3279
+ issues.push(`${event}: superseded direct registration \u2014 ${commandOf(raw)}`);
3280
+ } else if (isShimEntry(raw) && !ACTIVE_HOOKS.some((h) => h.event === event && identityFor(h) === identityOfCommand(commandOf(raw)))) {
3281
+ issues.push(`${event}: not part of this repo's hook set \u2014 ${commandOf(raw)}`);
3282
+ }
3283
+ }
3284
+ }
3285
+ let pluginOnDisk = null;
3286
+ try {
3287
+ pluginOnDisk = fs2.readFileSync(PLUGIN_HOOKS_PATH, "utf8");
3288
+ } catch {
3289
+ pluginOnDisk = null;
3290
+ }
3291
+ if (!KIT.customer && pluginOnDisk !== pluginHooksText()) {
3292
+ issues.push(
3293
+ `plugins/heyspex/hooks/cursor-hooks.json: ${pluginOnDisk === null ? "missing" : "stale"} \u2014 regenerate with \`npm run hooks:install-cursor\``
3294
+ );
3295
+ }
3296
+ if (KIT.customer) {
3297
+ const plan = launcherPlan();
3298
+ if (plan.write) {
3299
+ issues.push(`${CUSTOMER_LAUNCHER}: ${plan.reason === "missing" ? "missing" : "stale"} \u2014 the hook rows point at it`);
3300
+ }
3301
+ }
3302
+ if (json) {
3303
+ console.log(JSON.stringify({ ok: issues.length === 0, issues, total: ACTIVE_HOOKS.length, customer: KIT.customer }, null, 2));
3304
+ process.exit(issues.length === 0 ? 0 : 1);
3305
+ }
3306
+ if (issues.length === 0) {
3307
+ console.log(`install-cursor-hooks --check: OK (${ACTIVE_HOOKS.length} hooks registered)`);
3308
+ process.exit(0);
3309
+ }
3310
+ console.error("install-cursor-hooks --check: drift detected");
3311
+ for (const issue of issues) console.error(` - ${issue}`);
3312
+ console.error(" Run `node scripts/install-cursor-hooks.mjs` to repair.");
3313
+ process.exit(1);
3314
+ }
3315
+ function uninstall() {
3316
+ const settings = readSettings();
3317
+ if (!settings.hooks) {
3318
+ console.log("install-cursor-hooks: nothing to uninstall (no hooks block).");
3319
+ return;
3320
+ }
3321
+ let removed = 0;
3322
+ for (const [event, entries] of Object.entries(settings.hooks)) {
3323
+ if (!Array.isArray(entries)) continue;
3324
+ const keep = entries.filter((raw) => !isShimEntry(raw) && !isLegacyEntry(raw));
3325
+ removed += entries.length - keep.length;
3326
+ if (keep.length === 0) delete settings.hooks[event];
3327
+ else settings.hooks[event] = keep;
3328
+ }
3329
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
3330
+ writeSettings(settings);
3331
+ console.log(`install-cursor-hooks: uninstalled ${removed} hook entr${removed === 1 ? "y" : "ies"}.`);
3332
+ }
3333
+ if (invokedDirectly(import.meta.url, "install-cursor-hooks.mjs")) {
3334
+ const argv = process.argv.slice(2);
3335
+ const json = argv.includes("--json");
3336
+ if (argv.includes("--uninstall")) uninstall();
3337
+ else if (argv.includes("--check")) check({ json });
3338
+ else install({ json });
3339
+ }
3340
+
3341
+ // spex-console/src/lib/cli/install.ts
3342
+ init_credentials();
3343
+
3344
+ // spex-console/src/lib/cli/connect-checks.ts
3345
+ import { dirname as dirname5 } from "node:path";
3346
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
3347
+
3348
+ // spex-console/src/lib/dev-mode.ts
3349
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
3350
+ import { join as join10 } from "node:path";
3351
+ init_credentials();
3352
+
3353
+ // spex-console/src/lib/cli/connect-checks.ts
3354
+ init_credentials();
3355
+
3356
+ // spex-console/src/lib/cli/satellite.ts
3357
+ import { resolve as resolveNodePath } from "node:path";
3358
+ import process2 from "node:process";
3359
+
1547
3360
  // spex-console/src/lib/cli/principle-selection.ts
1548
- import { basename as basename2 } from "node:path";
3361
+ import { basename as basename3 } from "node:path";
1549
3362
  var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
1550
3363
  function principleKey(row) {
1551
- const base = basename2(row.dest, ".md");
3364
+ const base = basename3(row.dest, ".md");
1552
3365
  return base.startsWith("principle-") ? base.slice("principle-".length) : base;
1553
3366
  }
1554
3367
  function principleWikilink(row) {
1555
- return basename2(row.dest, ".md");
3368
+ return basename3(row.dest, ".md");
1556
3369
  }
1557
3370
  function parsePrinciplesFlag(value, rows) {
1558
3371
  const principleRows = rows.filter((row) => row.group === "principle");
@@ -1614,8 +3427,247 @@ function principlesIndexHook(selected, rows) {
1614
3427
  };
1615
3428
  }
1616
3429
 
3430
+ // spex-console/src/lib/cli/receipt.ts
3431
+ var RECEIPT_AGREEMENT_FIELDS = [
3432
+ "vault_id",
3433
+ "workspace_id",
3434
+ "account_id",
3435
+ "active_vault_id"
3436
+ ];
3437
+ var ALL_FIELDS = [
3438
+ ...RECEIPT_AGREEMENT_FIELDS,
3439
+ "display_name",
3440
+ "archived"
3441
+ ];
3442
+
3443
+ // spex-console/src/lib/cli/vault-workspace-shared.ts
3444
+ import { readFileSync as readFileSync10 } from "node:fs";
3445
+
3446
+ // spex-console/src/lib/indexer/parser.ts
3447
+ var import_gray_matter = __toESM(require_gray_matter(), 1);
3448
+ import { createHash as createHash2 } from "node:crypto";
3449
+ import { promises as fs3 } from "node:fs";
3450
+ import path4 from "node:path";
3451
+
3452
+ // spex-console/src/lib/cli/install.ts
3453
+ var MCP_REL = ["vendor", "heyspex-mcp", "cli.mjs"];
3454
+ var PORTABLE_MCP_CLI = Object.freeze({
3455
+ claude: `\${CLAUDE_PROJECT_DIR:-.}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`,
3456
+ cursor: `\${workspaceFolder}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`
3457
+ });
3458
+ function resolveCallerPath(p, env, fallbackCwd) {
3459
+ const trimmed = p?.trim() ?? "";
3460
+ if (!trimmed) return "";
3461
+ if (isAbsolute3(trimmed)) return trimmed;
3462
+ const base = env.HS_INVOKED_CWD?.trim() || fallbackCwd;
3463
+ return resolveNodePath2(base, trimmed);
3464
+ }
3465
+ var DEFAULT_REMOTE_MCP_URL = "https://app.heyspex.com/api/mcp";
3466
+ var REMOTE_MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
3467
+ var PLUGIN_MARKETPLACE_NAME = "heyspex-marketplace";
3468
+ var PLUGIN_INSTALL_SPEC = `heyspex@${PLUGIN_MARKETPLACE_NAME}`;
3469
+ var WRITE_TARGET_KIND = {
3470
+ specsGitkeep: "specs_gitkeep",
3471
+ heyspexYaml: "heyspex_yaml",
3472
+ cursorMcp: "cursor_mcp",
3473
+ claudeMcp: "claude_mcp",
3474
+ claudeMd: "claude_md",
3475
+ agentsMd: "agents_md",
3476
+ statuslineVendor: "statusline_vendor",
3477
+ gitignore: "gitignore",
3478
+ claudeMdTemplate: "claude_md_template",
3479
+ agentsMdTemplate: "agents_md_template"
3480
+ };
3481
+ var INSTALL_WRITE_TARGETS = [
3482
+ {
3483
+ kind: WRITE_TARGET_KIND.specsGitkeep,
3484
+ rel: "specs/.gitkeep",
3485
+ path: (targetDir, joinPath) => joinPath(targetDir, "specs", ".gitkeep"),
3486
+ appliesTo: (_args, opts) => !opts.attached
3487
+ },
3488
+ {
3489
+ kind: WRITE_TARGET_KIND.heyspexYaml,
3490
+ rel: "heyspex.yaml",
3491
+ path: (targetDir, joinPath) => joinPath(targetDir, "heyspex.yaml"),
3492
+ appliesTo: (_args, opts) => !opts.attached
3493
+ },
3494
+ {
3495
+ kind: WRITE_TARGET_KIND.cursorMcp,
3496
+ rel: ".cursor/mcp.json",
3497
+ path: (targetDir, joinPath) => joinPath(targetDir, ".cursor", "mcp.json"),
3498
+ appliesTo: (args) => args.cursor
3499
+ },
3500
+ {
3501
+ kind: WRITE_TARGET_KIND.claudeMcp,
3502
+ rel: ".mcp.json",
3503
+ path: (targetDir, joinPath) => joinPath(targetDir, ".mcp.json"),
3504
+ appliesTo: (args) => args.claude
3505
+ },
3506
+ {
3507
+ kind: WRITE_TARGET_KIND.claudeMd,
3508
+ rel: "CLAUDE.md",
3509
+ path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md"),
3510
+ appliesTo: (args, opts) => args.claude && opts.installContract
3511
+ },
3512
+ {
3513
+ kind: WRITE_TARGET_KIND.agentsMd,
3514
+ rel: "AGENTS.md",
3515
+ path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md"),
3516
+ appliesTo: (_args, opts) => opts.installContract
3517
+ },
3518
+ // The customer-template step (--register-vault) writes the project contract
3519
+ // beside a hand-authored CLAUDE.md/AGENTS.md as <file>.template; a rerun
3520
+ // rewrites it when the generated text changed.
3521
+ {
3522
+ kind: WRITE_TARGET_KIND.claudeMdTemplate,
3523
+ rel: "CLAUDE.md.template",
3524
+ path: (targetDir, joinPath) => joinPath(targetDir, "CLAUDE.md.template"),
3525
+ appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && args.claude && opts.installContract
3526
+ },
3527
+ {
3528
+ kind: WRITE_TARGET_KIND.agentsMdTemplate,
3529
+ rel: "AGENTS.md.template",
3530
+ path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md.template"),
3531
+ appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && opts.installContract
3532
+ },
3533
+ {
3534
+ kind: WRITE_TARGET_KIND.statuslineVendor,
3535
+ rel: "scripts/claude-statusline",
3536
+ path: (targetDir, joinPath) => joinPath(targetDir, "scripts", "claude-statusline"),
3537
+ appliesTo: (args) => (args.statusline ?? true) && args.claude
3538
+ },
3539
+ {
3540
+ kind: WRITE_TARGET_KIND.gitignore,
3541
+ rel: ".gitignore",
3542
+ path: (targetDir, joinPath) => joinPath(targetDir, ".gitignore"),
3543
+ // A satellite owns no vault content and writes no machine-local
3544
+ // manifests of its own (its .mcp.json/.cursor/mcp.json point AT the
3545
+ // attached vault, not a fresh one this repo would need to gitignore) --
3546
+ // only a real vault bootstrap gets one.
3547
+ appliesTo: (_args, opts) => !opts.attached
3548
+ }
3549
+ ];
3550
+ var INSTALL_HELP = [
3551
+ "Usage:",
3552
+ " hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
3553
+ " [--no-cursor] [--no-claude] [--claude-apply]",
3554
+ " [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
3555
+ " [--copilot-hooks | --copilot-plugin | --copilot-plugin-apply]",
3556
+ " [--no-statusline]",
3557
+ " [--cc | --command-code] [--register-vault [--workspace <id>]]",
3558
+ " [--remote[=<url>]]",
3559
+ " hs install --attach-vault <vault-id|vault-path> [--target <dir>] [...]",
3560
+ "",
3561
+ "Bootstrap a project vault for HeySpex agent tooling:",
3562
+ " \u2022 specs/ + heyspex.yaml (via hs init)",
3563
+ " \u2022 .cursor/mcp.json \u2014 Cursor MCP (gitignored, machine-local path to platform MCP)",
3564
+ " \u2022 .mcp.json \u2014 Claude Code project MCP (absolute platform path)",
3565
+ " \u2022 CLAUDE.md / AGENTS.md \u2014 thin agent contract pointing at the HeySpex MCP server",
3566
+ " \u2022 agent kit (customer repos) \u2014 the customer-scoped HeySpex hooks in",
3567
+ " .claude/settings.json + .cursor/hooks.json and the /hs-* commands in",
3568
+ " .claude/commands + .cursor/commands, every platform path absolute.",
3569
+ " Scope table: scripts/lib/customer-kit.mjs. Re-runs are no-ops; a command",
3570
+ " file you edited is left alone (--force overwrites it). `hs doctor` flags",
3571
+ " a missing or stale kit.",
3572
+ "",
3573
+ "Satellite repo (--attach-vault): wire a CODE repo to an EXISTING vault instead",
3574
+ "of making it a vault. Writes the MCP manifests + agent contract only, with",
3575
+ "HEYSPEX_VAULT_ROOT pinned to the attached vault's absolute root -- no specs/,",
3576
+ "no heyspex.yaml, no new registry row. The target is the repo you are standing",
3577
+ "in (or --target), never the vault being attached.",
3578
+ "",
3579
+ "That pin IS the attachment, so it must land somewhere: --attach-vault refuses a",
3580
+ "flag combination that writes no stdio manifest (--no-cursor --no-claude), and",
3581
+ "refuses to re-pin over manifests that name a different vault unless you pass",
3582
+ "--force. Re-attaching to the SAME vault is a no-op and says so.",
3583
+ "",
3584
+ '--attach-vault --remote: the stdio pin under "heyspex" in .mcp.json is preserved',
3585
+ "(never replaced by the HTTP form, even with --force) and the cloud form is",
3586
+ 'written under a second key, "heyspex-cloud" -- Claude Code registers both the',
3587
+ "attached-vault local server and the cloud server. Without --force, a pin that",
3588
+ "already names a DIFFERENT vault still refuses and names the conflicting key,",
3589
+ "same as the non-remote case.",
3590
+ "",
3591
+ "Verify with `hs connect --check` from the satellite -- it is the command that",
3592
+ "audits THIS repo's rows and reports the attachment. `hs doctor` resolves",
3593
+ "THROUGH the pin and audits the ATTACHED VAULT repo, so it reports that vault's",
3594
+ "connection state, not the satellite's.",
3595
+ "",
3596
+ "Conflict scan: every install checks the target repo against the known-conflict",
3597
+ "registry (beads, workflow-pm, bd hooks, \u2026). See:",
3598
+ " specs/protocols/customer-vault-local-install-protocol.md",
3599
+ "",
3600
+ "Run from the HeySpex platform repo, or pass --platform to its checkout.",
3601
+ "Target defaults to the current directory.",
3602
+ "",
3603
+ "Flags:",
3604
+ " --target <dir> Project repo to bootstrap (default: cwd).",
3605
+ " --platform <dir> HeySpex platform checkout (default: HS_INSTALL_PLATFORM or dispatcher root).",
3606
+ " --claude-apply Run `claude mcp add --scope project` after writing files.",
3607
+ " --plugin Print the Claude Code plugin registration block (self-hosted",
3608
+ " marketplace at the platform root \u2014 HS-QTBK prototype).",
3609
+ " --plugin-apply Run `claude plugin marketplace add` + `claude plugin install`",
3610
+ " after writing files. Marketplace + plugin names are read from",
3611
+ ` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
3612
+ " --plugin-scope <s> Plugin install scope: user, project (default), or local.",
3613
+ " Implies --plugin.",
3614
+ " --copilot-hooks Write .github/hooks/heyspex.json (GitHub Copilot hooks) into the",
3615
+ " target. Commands resolve node_modules/heyspex first, so an npm-only",
3616
+ " install (no heyspex repo checkout) works. Pick this OR the plugin.",
3617
+ " --copilot-plugin Print `copilot plugin marketplace add '<platform>'` +",
3618
+ " `copilot plugin install <plugin>@<marketplace>` (the platform root is",
3619
+ " node_modules/heyspex for a published install).",
3620
+ " --copilot-plugin-apply Run those two commands when `copilot` is on PATH.",
3621
+ " Requesting the Copilot plugin AND --copilot-hooks warns and skips the",
3622
+ " hooks file (both would fire every hook twice).",
3623
+ " --remote[=<url>] Write .mcp.json for the REMOTE (cloud) MCP endpoint instead of",
3624
+ ` the local stdio server \u2014 the Streamable-HTTP form against`,
3625
+ ` ${DEFAULT_REMOTE_MCP_URL} (or <url>), authenticated by the`,
3626
+ ` env reference \`\${${REMOTE_MCP_TOKEN_ENV}}\` (never the literal token).`,
3627
+ ' With --attach-vault, the stdio pin survives under "heyspex" and',
3628
+ ' this is written under "heyspex-cloud" instead (see above). If',
3629
+ " this machine already ran `hs login` / `hs mcp pair <code>`, the",
3630
+ " printed guidance resolves the bearer from",
3631
+ " ~/.heyspex/credentials.yaml automatically -- no manual export",
3632
+ " needed on that machine.",
3633
+ " --no-statusline Skip the branded statusline (statusLine entry in the target's",
3634
+ " .claude/settings.json + machine-level glyph install).",
3635
+ " --cc, --command-code Print `cmdc mcp add` PowerShell one-liner for Command Code.",
3636
+ " --attach-vault <v> SATELLITE mode: attach this repo to the EXISTING vault named",
3637
+ " by a registry id (`hs vault list`) or the vault repo's path,",
3638
+ " instead of scaffolding specs/ + heyspex.yaml here. The vault",
3639
+ " must already be registered on this machine. Mutually exclusive",
3640
+ " with --register-vault and --workspace, and with any flag",
3641
+ " combination that would leave no stdio manifest to pin.",
3642
+ " Combines with --remote (see --remote above): the pin and the",
3643
+ " cloud endpoint coexist in .mcp.json under separate keys.",
3644
+ " --register-vault Register this repo's vault into ~/.heyspex/registry.json during install,",
3645
+ " and -- when this machine is signed in -- on the console too; both",
3646
+ " sides are read back (GET /api/vaults) and printed as a receipt.",
3647
+ " Re-running changes nothing and sends no cloud write.",
3648
+ " --workspace <id> With --register-vault: ensure this workspace exists, make it active,",
3649
+ " and associate the vault with it (default: the vault's current",
3650
+ " workspace, else your per-account default ws-<account>).",
3651
+ " --no-cursor Skip .cursor/mcp.json.",
3652
+ " --no-claude Skip .mcp.json and Claude instructions.",
3653
+ " --force Overwrite existing MCP manifests and heyspex.yaml.",
3654
+ " --dry-run Print the plan without writing.",
3655
+ " --skip-conflict-check Skip the known-conflict registry scan.",
3656
+ " --json Machine-readable result.",
3657
+ " --account <email|id> Confirm the account this repo is attached to. An interactive",
3658
+ " install asks (continue / sign in as someone else / create an",
3659
+ " account / abort); a non-interactive one with --register-vault or",
3660
+ " --remote refuses without this or --yes. See `hs whoami`.",
3661
+ " --yes Confirm the signed-in account shown, without typing it.",
3662
+ " --help, -h Show this help.",
3663
+ "",
3664
+ "Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed \xB7 16 sign-in required \xB7",
3665
+ "12 console unreachable (account check) \xB7 17 account not confirmed \xB7",
3666
+ "19 the cloud half did not land (the receipt names the retry) \xB7 21 both sides read back and disagree"
3667
+ ].join("\n");
3668
+
1617
3669
  // spex-console/bin/run-entry.ts
1618
- import process2 from "node:process";
3670
+ import process3 from "node:process";
1619
3671
  var BinExit = class extends Error {
1620
3672
  code;
1621
3673
  constructor(code) {
@@ -1624,35 +3676,35 @@ var BinExit = class extends Error {
1624
3676
  this.code = code;
1625
3677
  }
1626
3678
  };
1627
- function runEntry(name, main2, options = {}) {
1628
- const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
3679
+ function runEntry(name, main2, options2 = {}) {
3680
+ const { fallbackCode = 1, unhandledLabel = "unhandled" } = options2;
1629
3681
  void (async () => {
1630
3682
  try {
1631
3683
  await main2();
1632
3684
  } catch (err) {
1633
3685
  if (err instanceof BinExit) {
1634
- process2.exitCode = err.code;
3686
+ process3.exitCode = err.code;
1635
3687
  return;
1636
3688
  }
1637
3689
  const message = err instanceof Error ? err.message : String(err);
1638
- process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
3690
+ process3.stderr.write(`${name}: ${unhandledLabel}: ${message}
1639
3691
  `);
1640
- process2.exitCode = fallbackCode;
3692
+ process3.exitCode = fallbackCode;
1641
3693
  }
1642
3694
  })();
1643
3695
  }
1644
3696
 
1645
3697
  // spex-console/bin/hs-init.ts
1646
- var INTERACTIVE = process3.stdin.isTTY === true;
3698
+ var INTERACTIVE = process4.stdin.isTTY === true;
1647
3699
  var PKG_ROOT = resolve5(
1648
- resolveCodeRoot({ packageRoot: resolve5(dirname5(fileURLToPath2(import.meta.url)), "..", "..") })
3700
+ resolveCodeRoot({ packageRoot: resolve5(dirname6(fileURLToPath4(import.meta.url)), "..", "..") })
1649
3701
  );
1650
- var CONFIG_SEED_PATH = join9(PKG_ROOT, "vendor", "heyspex-mcp", "config-seed.yaml");
3702
+ var CONFIG_SEED_PATH = join11(PKG_ROOT, "vendor", "heyspex-mcp", "config-seed.yaml");
1651
3703
  function readConfigSeed() {
1652
3704
  try {
1653
- return readFileSync8(CONFIG_SEED_PATH, "utf8");
3705
+ return readFileSync11(CONFIG_SEED_PATH, "utf8");
1654
3706
  } catch {
1655
- process3.stderr.write(
3707
+ process4.stderr.write(
1656
3708
  `hs init: warning \u2014 config seed template not found at ${CONFIG_SEED_PATH}; .heyspex/config.yaml was not seeded (run \`hs settings set <key> <value>\` to create it).
1657
3709
  `
1658
3710
  );
@@ -1660,7 +3712,7 @@ function readConfigSeed() {
1660
3712
  }
1661
3713
  }
1662
3714
  async function askYesNo(question) {
1663
- const rl = createInterface({ input: process3.stdin, output: process3.stdout });
3715
+ const rl = createInterface({ input: process4.stdin, output: process4.stdout });
1664
3716
  try {
1665
3717
  const answer = (await rl.question(`${question} `)).trim().toLowerCase();
1666
3718
  return answer === "y" || answer === "yes" || answer === "";
@@ -1673,9 +3725,9 @@ async function resolvePrincipleSelection(principlesFlag) {
1673
3725
  return parsePrinciplesFlag(principlesFlag, TEMPLATE_MANIFEST);
1674
3726
  }
1675
3727
  if (INTERACTIVE) {
1676
- process3.stdout.write("Select which universal principle docs to ship with this vault:\n");
3728
+ process4.stdout.write("Select which universal principle docs to ship with this vault:\n");
1677
3729
  for (const choice of listPrincipleChoices(TEMPLATE_MANIFEST)) {
1678
- process3.stdout.write(` - ${choice.key}: ${choice.summary}
3730
+ process4.stdout.write(` - ${choice.key}: ${choice.summary}
1679
3731
  `);
1680
3732
  }
1681
3733
  const selected = await promptPrincipleSelection(askYesNo, TEMPLATE_MANIFEST);
@@ -1684,39 +3736,39 @@ async function resolvePrincipleSelection(principlesFlag) {
1684
3736
  return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
1685
3737
  }
1686
3738
  async function main() {
1687
- const repoRoot = process3.env.HS_VAULT_ROOT ? resolve5(process3.env.HS_VAULT_ROOT) : process3.cwd();
1688
- const parsed = parseInitArgs(process3.argv.slice(3));
3739
+ const repoRoot = process4.env.HS_VAULT_ROOT ? resolve5(process4.env.HS_VAULT_ROOT) : process4.cwd();
3740
+ const parsed = parseInitArgs(process4.argv.slice(3));
1689
3741
  if (parsed.help) {
1690
- process3.stdout.write(`${HELP}
3742
+ process4.stdout.write(`${HELP}
1691
3743
  `);
1692
- process3.exitCode = EXIT.OK;
3744
+ process4.exitCode = EXIT.OK;
1693
3745
  return;
1694
3746
  }
1695
3747
  if (parsed.error) {
1696
- process3.stderr.write(`hs init: ${parsed.error.message}
3748
+ process4.stderr.write(`hs init: ${parsed.error.message}
1697
3749
  `);
1698
- process3.exitCode = parsed.error.code;
3750
+ process4.exitCode = parsed.error.code;
1699
3751
  return;
1700
3752
  }
1701
3753
  const args = parsed.args;
1702
3754
  if (args.customerVaultDir) {
1703
- const here = dirname5(fileURLToPath2(import.meta.url));
3755
+ const here = dirname6(fileURLToPath4(import.meta.url));
1704
3756
  const platformRoot = resolve5(here, "..", "..");
1705
- const targetDir = resolve5(args.customerVaultDir);
3757
+ const targetDir = resolve5(resolveCallerPath(args.customerVaultDir, process4.env, process4.cwd()));
1706
3758
  const deps = {
1707
3759
  platformRoot,
1708
- exists: (p) => existsSync4(p),
1709
- readdir: (p) => readdirSync(p),
3760
+ exists: (p) => existsSync5(p),
3761
+ readdir: (p) => readdirSync2(p),
1710
3762
  mkdirRecursive: (p) => {
1711
- mkdirSync4(p, { recursive: true });
3763
+ mkdirSync6(p, { recursive: true });
1712
3764
  },
1713
- writeFile: (p, c) => writeFileSync4(p, c, "utf8"),
3765
+ writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
1714
3766
  copyFile: (src, dest) => copyFileSync(src, dest),
1715
- joinPath: (...s) => join9(...s),
3767
+ joinPath: (...s) => join11(...s),
1716
3768
  readConfigSeed,
1717
3769
  readFile: (p) => {
1718
3770
  try {
1719
- return readFileSync8(p, "utf8");
3771
+ return readFileSync11(p, "utf8");
1720
3772
  } catch {
1721
3773
  return null;
1722
3774
  }
@@ -1725,9 +3777,9 @@ async function main() {
1725
3777
  };
1726
3778
  const principlesResult = await resolvePrincipleSelection(args.principles);
1727
3779
  if (!principlesResult.ok) {
1728
- process3.stderr.write(`hs init: ${principlesResult.message}
3780
+ process4.stderr.write(`hs init: ${principlesResult.message}
1729
3781
  `);
1730
- process3.exitCode = EXIT.INVALID_ARG;
3782
+ process4.exitCode = EXIT.INVALID_ARG;
1731
3783
  return;
1732
3784
  }
1733
3785
  const selected = principlesResult.selected;
@@ -1746,7 +3798,7 @@ async function main() {
1746
3798
  manifestFilter: principleManifestFilter(selected),
1747
3799
  afterScaffold
1748
3800
  });
1749
- const sink2 = result2.isError ? process3.stderr : process3.stdout;
3801
+ const sink2 = result2.isError ? process4.stderr : process4.stdout;
1750
3802
  if (args.json) {
1751
3803
  sink2.write(`${JSON.stringify({ ...result2.payload, notices })}
1752
3804
  `);
@@ -1756,29 +3808,29 @@ async function main() {
1756
3808
  for (const notice of notices) sink2.write(`${notice}
1757
3809
  `);
1758
3810
  }
1759
- process3.exitCode = result2.code;
3811
+ process4.exitCode = result2.code;
1760
3812
  return;
1761
3813
  }
1762
3814
  const result = runInit(args, {
1763
3815
  repoRoot,
1764
- fileExists: (p) => existsSync4(p),
3816
+ fileExists: (p) => existsSync5(p),
1765
3817
  readFile: (p) => {
1766
3818
  try {
1767
- return readFileSync8(p, "utf8");
3819
+ return readFileSync11(p, "utf8");
1768
3820
  } catch {
1769
3821
  return null;
1770
3822
  }
1771
3823
  },
1772
- writeFile: (p, c) => writeFileSync4(p, c, "utf8"),
1773
- joinPath: (...s) => join9(...s),
3824
+ writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
3825
+ joinPath: (...s) => join11(...s),
1774
3826
  mkdirRecursive: (p) => {
1775
- mkdirSync4(p, { recursive: true });
3827
+ mkdirSync6(p, { recursive: true });
1776
3828
  },
1777
3829
  readConfigSeed
1778
3830
  });
1779
- const sink = result.isError ? process3.stderr : process3.stdout;
3831
+ const sink = result.isError ? process4.stderr : process4.stdout;
1780
3832
  sink.write(`${args.json ? JSON.stringify(result.payload) : result.message}
1781
3833
  `);
1782
- process3.exitCode = result.code;
3834
+ process4.exitCode = result.code;
1783
3835
  }
1784
3836
  runEntry("hs init", main, { fallbackCode: EXIT.WRITE_FAILED });