heyspex 0.2.2 → 0.2.4

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 (79) hide show
  1. package/.claude-plugin/marketplace.json +2 -1
  2. package/README.md +11 -0
  3. package/bin/hs-vault-binding.mjs +157 -0
  4. package/bin/hs.mjs +83 -79
  5. package/bin/spexcode.mjs +91 -0
  6. package/dev-factory/engine/worktree.mjs +36 -7
  7. package/dev-factory/lib/epic-detect.mjs +2 -2
  8. package/dev-factory/lib/fleet-readiness.mjs +3 -5
  9. package/dev-factory/lib/fleet-state.mjs +2 -2
  10. package/dev-factory/lib/sufficiency-score.mjs +3 -5
  11. package/dist/cli/hs-check.mjs +76 -46
  12. package/dist/cli/hs-claim.mjs +63 -33
  13. package/dist/cli/hs-connect.mjs +520 -214
  14. package/dist/cli/hs-defer.mjs +161 -19
  15. package/dist/cli/hs-dev.mjs +11 -5
  16. package/dist/cli/hs-env.mjs +187 -161
  17. package/dist/cli/hs-index-rebuild.mjs +123 -78
  18. package/dist/cli/hs-index-reconcile.mjs +203 -107
  19. package/dist/cli/hs-init.mjs +426 -179
  20. package/dist/cli/hs-install.mjs +759 -437
  21. package/dist/cli/hs-join.mjs +590 -383
  22. package/dist/cli/hs-vault-pull.mjs +359 -254
  23. package/dist/cli/hs-vault-push.mjs +151 -113
  24. package/dist/cli/hs-vault.mjs +271 -104
  25. package/dist/cli/hs-workspace.mjs +90 -57
  26. package/dist/cli-templates/customer-vault/specs/protocols/session-close-protocol.md +178 -91
  27. package/docs/copilot-plugin-install.md +200 -177
  28. package/docs/driving-heyspex-from-copilot.md +174 -165
  29. package/docs/mcp-client-config.md +93 -25
  30. package/package.json +257 -250
  31. package/plugins/heyspex/.claude-plugin/plugin.json +13 -12
  32. package/plugins/heyspex/.cursor-plugin/plugin.json +16 -15
  33. package/plugins/heyspex/.mcp.json +3 -4
  34. package/plugins/heyspex/README.md +323 -312
  35. package/plugins/heyspex/assets/heyspex-glyph-128.png +0 -0
  36. package/plugins/heyspex/assets/heyspex-glyph-256.png +0 -0
  37. package/plugins/heyspex/assets/heyspex-glyph.svg +12 -0
  38. package/plugins/heyspex/bin/launch-mcp.mjs +21 -7
  39. package/plugins/heyspex/mcp.json +12 -13
  40. package/scripts/check-mcp-freshness.mjs +6 -4
  41. package/scripts/claude-hooks/_intent-classifier-router.mjs +518 -456
  42. package/scripts/claude-hooks/intent-classifier-trigger.mjs +305 -279
  43. package/scripts/claude-hooks/research-method-guard.mjs +2 -2
  44. package/scripts/claude-hooks/wip-checkpoint.mjs +33 -9
  45. package/scripts/claude-statusline/fonts/HeySpexIcons.ttf +0 -0
  46. package/scripts/claude-statusline/statusline-command.mjs +1073 -0
  47. package/scripts/copilot-statusline.mjs +130 -0
  48. package/scripts/dev-server-status.mjs +3 -2
  49. package/scripts/dev-server-stop.mjs +3 -2
  50. package/scripts/hs-capture-cli.mjs +2 -2
  51. package/scripts/hs-settings.mjs +2 -2
  52. package/scripts/hs-view.mjs +3 -2
  53. package/scripts/install-copilot-hooks.mjs +152 -36
  54. package/scripts/lib/copilot-cli.mjs +108 -0
  55. package/scripts/lib/customer-kit.mjs +223 -30
  56. package/scripts/lib/dev-mode.mjs +28 -2
  57. package/scripts/lib/shared-path-helpers.mjs +32 -0
  58. package/scripts/lib/standalone-doctor.mjs +131 -32
  59. package/scripts/wip-ledger-health.mjs +3 -2
  60. package/spexharness/src/gates/vault-mirror-catchup.mjs +113 -24
  61. package/spexharness/src/gates/vault-mirror-catchup.runner.mjs +52 -0
  62. package/spexharness/src/intent-directive.mjs +839 -626
  63. package/spexharness/src/rule-table.mjs +1 -1
  64. package/vendor/heyspex-mcp/cli-verbs.mjs +16 -2
  65. package/vendor/heyspex-mcp/cli.mjs +20 -4
  66. package/vendor/heyspex-mcp/console-fetch.mjs +15 -3
  67. package/vendor/heyspex-mcp/dev-tools.mjs +11 -4
  68. package/vendor/heyspex-mcp/events-tools.mjs +1 -1
  69. package/vendor/heyspex-mcp/factory-run-status.mjs +42 -0
  70. package/vendor/heyspex-mcp/http-transport.mjs +6 -1
  71. package/vendor/heyspex-mcp/mcp-config.mjs +4 -2
  72. package/vendor/heyspex-mcp/server-identity.mjs +13 -0
  73. package/vendor/heyspex-mcp/spec-edit-tools.mjs +26 -1
  74. package/vendor/heyspex-mcp/test-helpers.mjs +5 -2
  75. package/vendor/heyspex-mcp/tool-catalog.mjs +12 -0
  76. package/vendor/heyspex-mcp/tool-dispatch.mjs +63 -23
  77. package/vendor/heyspex-mcp/usable-dir-env.mjs +82 -0
  78. package/vendor/heyspex-mcp/vault-binding.mjs +839 -0
  79. package/vendor/heyspex-mcp/vault-resolve.mjs +353 -245
@@ -51,9 +51,9 @@ var init_heyspex_home = __esm({
51
51
  });
52
52
 
53
53
  // vendor/heyspex-mcp/credentials.mjs
54
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
54
+ import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
55
55
  import os, { homedir as homedir2 } from "node:os";
56
- import { dirname, join, resolve as resolve2 } from "node:path";
56
+ import { dirname as dirname2, join, resolve as resolve3 } from "node:path";
57
57
  var init_credentials = __esm({
58
58
  "vendor/heyspex-mcp/credentials.mjs"() {
59
59
  "use strict";
@@ -62,7 +62,7 @@ var init_credentials = __esm({
62
62
  });
63
63
 
64
64
  // scripts/lib/dev-mode.mjs
65
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
65
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
66
66
  import { homedir as homedir3 } from "node:os";
67
67
  import { join as join2 } from "node:path";
68
68
  function devModeFilePath(homeDir = heyspexHome()) {
@@ -90,7 +90,7 @@ function deriveSemantics(mode, reliance) {
90
90
  vaultWriteMode: local ? "local" : "cloud"
91
91
  };
92
92
  }
93
- function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
93
+ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
94
94
  let raw;
95
95
  try {
96
96
  raw = readFile(devModeFilePath(homeDir));
@@ -118,10 +118,15 @@ function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultRea
118
118
  return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
119
119
  }
120
120
  }
121
+ function overrideCloudCheck(source, cloudKnown, staleAgainstCloud) {
122
+ if (source !== "local-override") return "not-applicable";
123
+ if (!cloudKnown) return "unverified";
124
+ return staleAgainstCloud ? "stale" : "in-sync";
125
+ }
121
126
  function resolveDevMode({
122
127
  env = process.env,
123
128
  homeDir = heyspexHome(),
124
- readFile = defaultReadFile,
129
+ readFile = defaultReadFile2,
125
130
  cloudSetting = void 0
126
131
  } = {}) {
127
132
  const local = readLocalDevModeRecord({ homeDir, readFile });
@@ -157,6 +162,7 @@ function resolveDevMode({
157
162
  const semantics = deriveSemantics(mode, reliance);
158
163
  const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
159
164
  const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
165
+ const cloudCheck = overrideCloudCheck(source, cloudKnown, staleAgainstCloud);
160
166
  return {
161
167
  mode,
162
168
  reliance: semantics.reliance,
@@ -174,12 +180,13 @@ function resolveDevMode({
174
180
  present: local.record !== null,
175
181
  record: local.record,
176
182
  parseError: local.parseError,
177
- staleAgainstCloud
183
+ staleAgainstCloud,
184
+ cloudCheck
178
185
  },
179
186
  cloudSetting: { known: cloudKnown, value: cloudValue }
180
187
  };
181
188
  }
182
- var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
189
+ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile2;
183
190
  var init_dev_mode = __esm({
184
191
  "scripts/lib/dev-mode.mjs"() {
185
192
  "use strict";
@@ -188,7 +195,7 @@ var init_dev_mode = __esm({
188
195
  CLOUD_CONSOLE_URL = "https://app.heyspex.com";
189
196
  LOCAL_CONSOLE_URL = "http://localhost:3000";
190
197
  DEV_MODE_FILENAME = "dev-mode.json";
191
- defaultReadFile = (p) => readFileSync2(p, "utf8");
198
+ defaultReadFile2 = (p) => readFileSync3(p, "utf8");
192
199
  }
193
200
  });
194
201
 
@@ -855,21 +862,23 @@ var require_gray_matter = __commonJS({
855
862
 
856
863
  // spex-console/bin/hs-init.ts
857
864
  import {
858
- copyFileSync,
859
- existsSync as existsSync5,
860
- mkdirSync as mkdirSync6,
865
+ copyFileSync as copyFileSync2,
866
+ existsSync as existsSync8,
867
+ mkdirSync as mkdirSync7,
861
868
  readdirSync as readdirSync2,
862
- readFileSync as readFileSync11,
863
- statSync,
864
- writeFileSync as writeFileSync6
869
+ readFileSync as readFileSync14,
870
+ statSync as statSync5,
871
+ writeFileSync as writeFileSync7
865
872
  } from "node:fs";
866
- import { dirname as dirname6, join as join11, resolve as resolve5 } from "node:path";
873
+ import { dirname as dirname7, join as join11, resolve as resolve6 } from "node:path";
867
874
  import process4 from "node:process";
868
875
  import { createInterface } from "node:readline/promises";
869
- import { fileURLToPath as fileURLToPath4 } from "node:url";
876
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
870
877
 
871
878
  // scripts/lib/shared-path-helpers.mjs
879
+ import { realpathSync } from "node:fs";
872
880
  import { isAbsolute, resolve, sep } from "node:path";
881
+ import { fileURLToPath } from "node:url";
873
882
  function resolveCodeRoot({
874
883
  explicit,
875
884
  packageRoot,
@@ -967,17 +976,118 @@ function writeProjectContract(ctx, options2 = {}) {
967
976
  }
968
977
  function customerClaudeMdHook(notify) {
969
978
  return (ctx) => {
979
+ if (ctx.mode === "refresh" && ctx.deps.exists(ctx.deps.joinPath(ctx.targetDir, "CLAUDE.md"))) {
980
+ return;
981
+ }
970
982
  writeProjectContract(ctx, { notify });
971
983
  };
972
984
  }
973
985
 
986
+ // spex-console/src/lib/cli/generated-file-eol.ts
987
+ import { spawnSync } from "node:child_process";
988
+ import { existsSync, readFileSync } from "node:fs";
989
+ import { dirname, relative, resolve as resolve2 } from "node:path";
990
+ function dominantEol(text) {
991
+ const crlf = (text.match(/\r\n/g) ?? []).length;
992
+ const lf = (text.match(/\n/g) ?? []).length - crlf;
993
+ if (crlf === 0 && lf === 0) return null;
994
+ return crlf > lf ? "\r\n" : "\n";
995
+ }
996
+ function applyEol(text, eol) {
997
+ const lf = text.replace(/\r\n/g, "\n");
998
+ return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
999
+ }
1000
+ function defaultRunGit(args, cwd) {
1001
+ try {
1002
+ const res = spawnSync("git", args, {
1003
+ cwd,
1004
+ encoding: "utf8",
1005
+ windowsHide: true,
1006
+ timeout: 1e4
1007
+ });
1008
+ if (res.status !== 0) return null;
1009
+ return String(res.stdout ?? "").trim();
1010
+ } catch {
1011
+ return null;
1012
+ }
1013
+ }
1014
+ function defaultReadFile(path7) {
1015
+ try {
1016
+ return readFileSync(path7, "utf8");
1017
+ } catch {
1018
+ return null;
1019
+ }
1020
+ }
1021
+ function existingAncestor(path7) {
1022
+ let dir = dirname(resolve2(path7));
1023
+ for (; ; ) {
1024
+ if (existsSync(dir)) return dir;
1025
+ const parent = dirname(dir);
1026
+ if (parent === dir) return dir;
1027
+ dir = parent;
1028
+ }
1029
+ }
1030
+ function createGeneratedFileEol(opts) {
1031
+ const readFile = opts.readFile ?? defaultReadFile;
1032
+ const runGit = opts.runGit ?? defaultRunGit;
1033
+ const platform = opts.platform ?? process.platform;
1034
+ const attrCache = /* @__PURE__ */ new Map();
1035
+ let configEol;
1036
+ const configDefault = () => {
1037
+ if (configEol !== void 0) return configEol;
1038
+ if (runGit(["rev-parse", "--is-inside-work-tree"], opts.repoRoot) !== "true") {
1039
+ configEol = null;
1040
+ return configEol;
1041
+ }
1042
+ const get = (key) => runGit(["config", "--get", key], opts.repoRoot);
1043
+ const coreEol = (get("core.eol") ?? "").toLowerCase();
1044
+ if (coreEol === "crlf") configEol = "\r\n";
1045
+ else if (coreEol === "lf") configEol = "\n";
1046
+ else if (platform === "win32" && (get("core.autocrlf") ?? "").toLowerCase() === "true")
1047
+ configEol = "\r\n";
1048
+ else configEol = null;
1049
+ return configEol;
1050
+ };
1051
+ const attrEol = (path7) => {
1052
+ const abs = resolve2(path7);
1053
+ const cwd = existingAncestor(abs);
1054
+ const rel = relative(cwd, abs).replace(/\\/g, "/") || ".";
1055
+ const key = `${cwd}\0${rel}`;
1056
+ let out = attrCache.get(key);
1057
+ if (out === void 0) {
1058
+ out = runGit(["check-attr", "eol", "--", rel], cwd) ?? "";
1059
+ attrCache.set(key, out);
1060
+ }
1061
+ const m = out.match(/:\s*eol:\s*(\S+)\s*$/);
1062
+ const value = (m?.[1] ?? "").toLowerCase();
1063
+ if (value === "crlf") return "\r\n";
1064
+ if (value === "lf") return "\n";
1065
+ return null;
1066
+ };
1067
+ const eolFor = (path7, existing) => {
1068
+ const current = existing === void 0 ? readFile(path7) : existing;
1069
+ if (current !== null) {
1070
+ const have = dominantEol(current);
1071
+ if (have) return have;
1072
+ }
1073
+ return attrEol(path7) ?? configDefault() ?? "\n";
1074
+ };
1075
+ return {
1076
+ eolFor,
1077
+ convert: (path7, content, existing) => {
1078
+ if (!content.includes("\n")) return content;
1079
+ return applyEol(content, eolFor(path7, existing));
1080
+ }
1081
+ };
1082
+ }
1083
+
974
1084
  // spex-console/src/lib/cli/init.ts
975
1085
  import { randomUUID } from "node:crypto";
976
1086
 
977
1087
  // scripts/lib/console-url-resolver.mjs
978
1088
  init_heyspex_home();
979
1089
  init_dev_mode();
980
- import { readFileSync as readFileSync3 } from "node:fs";
1090
+ import { readFileSync as readFileSync4 } from "node:fs";
981
1091
  import { homedir as homedir4 } from "node:os";
982
1092
  import { join as join3 } from "node:path";
983
1093
  var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
@@ -1005,7 +1115,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
1005
1115
  } catch {
1006
1116
  }
1007
1117
  }
1008
- function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
1118
+ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile3 } = {}) {
1009
1119
  try {
1010
1120
  const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
1011
1121
  const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
@@ -1015,10 +1125,10 @@ function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = default
1015
1125
  return null;
1016
1126
  }
1017
1127
  }
1018
- function defaultReadFile2(p) {
1019
- return readFileSync3(p, "utf8");
1128
+ function defaultReadFile3(p) {
1129
+ return readFileSync4(p, "utf8");
1020
1130
  }
1021
- function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile2 } = {}) {
1131
+ function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery = defaultReadFile3 } = {}) {
1022
1132
  if (!repoRoot) return null;
1023
1133
  try {
1024
1134
  const discovery = String(readDiscovery(join3(repoRoot, ".heyspex", "console-url")) ?? "").trim();
@@ -1053,8 +1163,8 @@ function resolveConsoleUrl({
1053
1163
  strict = false,
1054
1164
  devMode,
1055
1165
  log = (line) => process.stderr.write(line),
1056
- readFile = defaultReadFile2,
1057
- readDiscovery = defaultReadFile2
1166
+ readFile = defaultReadFile3,
1167
+ readDiscovery = defaultReadFile3
1058
1168
  } = {}) {
1059
1169
  const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
1060
1170
  if (envUrl) return trimTrailingSlash(envUrl);
@@ -1080,17 +1190,17 @@ function resolveConsoleUrl({
1080
1190
  }
1081
1191
 
1082
1192
  // spex-console/src/lib/cli/control-shared.ts
1083
- import path from "node:path";
1193
+ import path2 from "node:path";
1084
1194
 
1085
1195
  // scripts/lib/hs-command.mjs
1086
- import { existsSync } from "node:fs";
1087
- import { delimiter, dirname as dirname2, join as join4 } from "node:path";
1088
- import { fileURLToPath } from "node:url";
1196
+ import { existsSync as existsSync2 } from "node:fs";
1197
+ import { delimiter, dirname as dirname3, join as join4 } from "node:path";
1198
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1089
1199
  var HS_BIN_NAME = "hs";
1090
1200
  var HS_DISPATCHER_PATH = (() => {
1091
1201
  try {
1092
- const here = dirname2(fileURLToPath(import.meta.url));
1093
- return join4(dirname2(dirname2(here)), "bin", "hs.mjs");
1202
+ const here = dirname3(fileURLToPath2(import.meta.url));
1203
+ return join4(dirname3(dirname3(here)), "bin", "hs.mjs");
1094
1204
  } catch {
1095
1205
  return null;
1096
1206
  }
@@ -1103,14 +1213,14 @@ var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER
1103
1213
  // vendor/heyspex-mcp/console-fetch.mjs
1104
1214
  init_heyspex_home();
1105
1215
  init_credentials();
1106
- import { readFileSync as readFileSync7 } from "node:fs";
1107
- import { homedir as homedir7 } from "node:os";
1216
+ import { readFileSync as readFileSync9 } from "node:fs";
1217
+ import { homedir as homedir8 } from "node:os";
1108
1218
  import { join as join8 } from "node:path";
1109
1219
 
1110
1220
  // vendor/heyspex-mcp/session-owner.mjs
1111
1221
  init_heyspex_home();
1112
1222
  init_credentials();
1113
- import { readFileSync as readFileSync4 } from "node:fs";
1223
+ import { readFileSync as readFileSync5 } from "node:fs";
1114
1224
  import { homedir as homedir5 } from "node:os";
1115
1225
  import { join as join5 } from "node:path";
1116
1226
 
@@ -1128,10 +1238,10 @@ var VAULT_LESS_ENDPOINTS = Object.freeze([
1128
1238
  // vendor/heyspex-mcp/write-outbox.mjs
1129
1239
  import {
1130
1240
  readdirSync,
1131
- readFileSync as readFileSync5,
1241
+ readFileSync as readFileSync6,
1132
1242
  writeFileSync as writeFileSync3,
1133
1243
  mkdirSync as mkdirSync3,
1134
- existsSync as existsSync2,
1244
+ existsSync as existsSync3,
1135
1245
  renameSync as renameSync2,
1136
1246
  rmSync
1137
1247
  } from "node:fs";
@@ -1139,9 +1249,30 @@ import { basename, join as join6, isAbsolute as isAbsolute2 } from "node:path";
1139
1249
 
1140
1250
  // vendor/heyspex-mcp/vault-resolve.mjs
1141
1251
  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";
1252
+ import { existsSync as existsSync5, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
1253
+ import { homedir as homedir7, tmpdir } from "node:os";
1254
+
1255
+ // vendor/heyspex-mcp/usable-dir-env.mjs
1256
+ import { statSync } from "node:fs";
1257
+ import { isAbsolute as isAbsolute3 } from "node:path";
1258
+
1259
+ // vendor/heyspex-mcp/vault-resolve.mjs
1260
+ import { basename as basename2, dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
1261
+
1262
+ // vendor/heyspex-mcp/vault-binding.mjs
1263
+ import {
1264
+ copyFileSync,
1265
+ existsSync as existsSync4,
1266
+ mkdirSync as mkdirSync4,
1267
+ readFileSync as readFileSync7,
1268
+ realpathSync as realpathSync2,
1269
+ renameSync as renameSync3,
1270
+ statSync as statSync2,
1271
+ unlinkSync,
1272
+ writeFileSync as writeFileSync4
1273
+ } from "node:fs";
1274
+ import { homedir as homedir6 } from "node:os";
1275
+ import path from "node:path";
1145
1276
 
1146
1277
  // vendor/heyspex-mcp/console-fetch.mjs
1147
1278
  var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
@@ -1150,11 +1281,11 @@ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMAT
1150
1281
  init_credentials();
1151
1282
 
1152
1283
  // spex-console/src/lib/vault-registry/registry-file.ts
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";
1284
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
1285
+ import { dirname as dirname5, join as join9, resolve as resolve5 } from "node:path";
1155
1286
 
1156
1287
  // spex-console/src/lib/heyspex-home.ts
1157
- import { homedir as homedir8 } from "node:os";
1288
+ import { homedir as homedir9 } from "node:os";
1158
1289
 
1159
1290
  // spex-console/src/lib/cli/control-shared.ts
1160
1291
  var EXIT = {
@@ -1547,12 +1678,15 @@ var HELP = [
1547
1678
  "(spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
1548
1679
  "copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
1549
1680
  "plus a generated CLAUDE.md, the kept-principles index, and the template_version",
1550
- "pin; with --git, one initial `git init` + commit. Rerunnable: a <dir> that already",
1551
- "holds heyspex.yaml and an empty/placeholder-only specs/ is accepted, existing",
1552
- "files are never overwritten (an existing CLAUDE.md gets a CLAUDE.md.template",
1553
- "beside it), and the result lists each manifest row as created or kept. Refuses a",
1554
- "non-empty <dir> without heyspex.yaml, or a specs/ holding managed specs.",
1555
- "See init.customer-vault.ts.",
1681
+ "pin; with --git, one initial `git init` + commit. On an EXISTING vault (<dir>",
1682
+ "holds heyspex.yaml with a vault_id) it is a REFRESH \u2014 the repair `hs doctor`'s",
1683
+ "vault-template row names: only missing shipped files are added, template_version",
1684
+ "is written/updated in heyspex.yaml (every other byte and the vault_id kept),",
1685
+ "nothing existing is ever overwritten (an existing CLAUDE.md gets a",
1686
+ "CLAUDE.md.template beside it), the result lists each manifest row as created or",
1687
+ "kept, and a second run writes nothing. <dir> may be any path (a satellite repo",
1688
+ "names its vault, e.g. `hs init --customer-vault ../my-specs`). Refuses only a",
1689
+ "non-empty <dir> that has no heyspex.yaml. See init.customer-vault.ts.",
1556
1690
  "",
1557
1691
  "Flags:",
1558
1692
  " --console-url <url> console_url value (default: the mode-aware console --",
@@ -1575,7 +1709,7 @@ var HELP = [
1575
1709
  ].join("\n");
1576
1710
 
1577
1711
  // spex-console/src/lib/cli/init.customer-vault.ts
1578
- import { basename as basename2 } from "node:path";
1712
+ import { basename as basename3 } from "node:path";
1579
1713
 
1580
1714
  // spex-console/src/lib/cli/template-manifest.sources.mjs
1581
1715
  var UNIVERSAL_TEMPLATE_ROWS = [
@@ -2060,41 +2194,42 @@ var SCAFFOLD_FOLDERS = [
2060
2194
  "checks",
2061
2195
  "events"
2062
2196
  ];
2063
- var PLACEHOLDER_FILES = /* @__PURE__ */ new Set([".gitkeep"]);
2064
- function isDir(deps, path5, name) {
2065
- if (deps.isDirectory) return deps.isDirectory(path5);
2066
- try {
2067
- const kids = deps.readdir(path5);
2068
- return kids.length > 0 || !name.includes(".");
2069
- } catch {
2070
- return false;
2071
- }
2197
+ function defaultVaultName(targetDir) {
2198
+ return basename3(targetDir) || targetDir;
2072
2199
  }
2073
- function findManagedSpecs(deps, targetDir, manifestDests) {
2074
- const out = [];
2075
- const walk = (abs, rel) => {
2076
- for (const name of deps.readdir(abs)) {
2077
- const childAbs = deps.joinPath(abs, name);
2078
- const childRel = `${rel}/${name}`;
2079
- if (isDir(deps, childAbs, name)) walk(childAbs, childRel);
2080
- else if (!PLACEHOLDER_FILES.has(name) && name.endsWith(".md") && !manifestDests.has(childRel))
2081
- out.push(childRel);
2082
- }
2083
- };
2084
- const specsRoot = deps.joinPath(targetDir, "specs");
2085
- if (deps.exists(specsRoot)) walk(specsRoot, "specs");
2086
- return out;
2200
+ function readTemplateVersionPin(yamlText) {
2201
+ const m = yamlText.match(/^template_version:[ \t]*(.*?)[ \t]*$/m);
2202
+ if (!m) return null;
2203
+ const raw = m[1].trim();
2204
+ const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
2205
+ return unquoted.length > 0 ? unquoted : null;
2087
2206
  }
2088
- function defaultVaultName(targetDir) {
2089
- return basename2(targetDir) || targetDir;
2207
+ function stampTemplateVersion(content, version) {
2208
+ if (readTemplateVersionPin(content) === version) return content;
2209
+ const eol = content.includes("\r\n") ? "\r\n" : "\n";
2210
+ const line = `template_version: ${version}`;
2211
+ if (/^template_version:[^\r\n]*$/m.test(content)) {
2212
+ return content.replace(/^template_version:[^\r\n]*$/m, line);
2213
+ }
2214
+ const body = content.replace(/(?:\r?\n)+$/, "");
2215
+ return body.length === 0 ? `${line}${eol}` : `${body}${eol}${line}${eol}`;
2216
+ }
2217
+ function parentDirOf(deps, targetDir, dest) {
2218
+ const segments = dest.split("/");
2219
+ if (segments.length < 2) return null;
2220
+ return deps.joinPath(targetDir, ...segments.slice(0, -1));
2090
2221
  }
2091
2222
  function runCustomerVaultInit(args, deps, options2 = {}) {
2092
2223
  const { targetDir } = args;
2093
2224
  const written = [];
2094
2225
  const filter = options2.manifestFilter ?? (() => true);
2095
2226
  const rows = TEMPLATE_MANIFEST.filter(filter);
2227
+ const heyspexYamlPath = deps.joinPath(targetDir, "heyspex.yaml");
2228
+ const markerPresent = deps.exists(targetDir) && deps.exists(heyspexYamlPath);
2229
+ const existingYaml = markerPresent ? deps.readFile?.(heyspexYamlPath) ?? null : null;
2230
+ const existingVaultId = existingYaml ? parseVaultId(existingYaml) : null;
2231
+ const mode = markerPresent && existingVaultId ? "refresh" : "scaffold";
2096
2232
  if (deps.exists(targetDir)) {
2097
- const markerPresent = deps.exists(deps.joinPath(targetDir, "heyspex.yaml"));
2098
2233
  if (!markerPresent && deps.readdir(targetDir).length > 0) {
2099
2234
  return {
2100
2235
  code: EXIT.INVALID_ARG,
@@ -2104,43 +2239,43 @@ function runCustomerVaultInit(args, deps, options2 = {}) {
2104
2239
  written: []
2105
2240
  };
2106
2241
  }
2107
- if (markerPresent) {
2108
- const manifestDests = new Set(TEMPLATE_MANIFEST.map((r) => r.dest));
2109
- const managed = findManagedSpecs(deps, targetDir, manifestDests);
2110
- if (managed.length > 0) {
2111
- return {
2112
- code: EXIT.INVALID_ARG,
2113
- message: `${targetDir}/specs already holds managed spec(s) (${managed.slice(0, 3).join(", ")}${managed.length > 3 ? ", ..." : ""}) \u2014 customer-vault init refuses to write over an existing vault.`,
2114
- payload: { ok: false, error: "specs_not_empty", path: targetDir, specs: managed },
2115
- isError: true,
2116
- written: []
2117
- };
2118
- }
2119
- }
2120
2242
  } else {
2121
2243
  deps.mkdirRecursive(targetDir);
2122
2244
  }
2123
- const specsRoot = deps.joinPath(targetDir, "specs");
2124
- deps.mkdirRecursive(specsRoot);
2125
- for (const folder of SCAFFOLD_FOLDERS) {
2126
- deps.mkdirRecursive(deps.joinPath(specsRoot, folder));
2245
+ if (mode === "scaffold") {
2246
+ const specsRoot = deps.joinPath(targetDir, "specs");
2247
+ deps.mkdirRecursive(specsRoot);
2248
+ for (const folder of SCAFFOLD_FOLDERS) {
2249
+ deps.mkdirRecursive(deps.joinPath(specsRoot, folder));
2250
+ }
2127
2251
  }
2128
- const heyspexYamlPath = deps.joinPath(targetDir, "heyspex.yaml");
2129
2252
  let vaultId;
2130
- if (deps.exists(heyspexYamlPath)) {
2131
- const existingYaml = deps.readFile?.(heyspexYamlPath) ?? null;
2132
- vaultId = (existingYaml ? parseVaultId(existingYaml) : null) ?? "";
2253
+ let pin;
2254
+ if (markerPresent) {
2255
+ vaultId = existingVaultId ?? "";
2256
+ const stamped = existingYaml === null ? null : stampTemplateVersion(existingYaml, TEMPLATE_VERSION);
2257
+ if (stamped !== null && stamped !== existingYaml) {
2258
+ deps.writeFile(heyspexYamlPath, stamped);
2259
+ written.push(heyspexYamlPath);
2260
+ pin = "written";
2261
+ } else {
2262
+ pin = "kept";
2263
+ }
2133
2264
  } else {
2134
2265
  vaultId = mintVaultId();
2135
2266
  deps.writeFile(
2136
2267
  heyspexYamlPath,
2137
- renderHeyspexYaml({
2138
- consoleUrl: options2.consoleUrl ?? DEFAULT_CONSOLE_URL2,
2139
- vaultRoot: "specs/",
2140
- vaultId
2141
- })
2268
+ stampTemplateVersion(
2269
+ renderHeyspexYaml({
2270
+ consoleUrl: options2.consoleUrl ?? DEFAULT_CONSOLE_URL2,
2271
+ vaultRoot: "specs/",
2272
+ vaultId
2273
+ }),
2274
+ TEMPLATE_VERSION
2275
+ )
2142
2276
  );
2143
2277
  written.push(heyspexYamlPath);
2278
+ pin = "written";
2144
2279
  }
2145
2280
  const readConfigSeed2 = deps.readConfigSeed;
2146
2281
  const configSeed = seedConfigYaml(
@@ -2156,6 +2291,10 @@ function runCustomerVaultInit(args, deps, options2 = {}) {
2156
2291
  rowResults.push({ dest: row.dest, action: "kept" });
2157
2292
  continue;
2158
2293
  }
2294
+ if (mode === "refresh") {
2295
+ const parent = parentDirOf(deps, targetDir, row.dest);
2296
+ if (parent && !deps.exists(parent)) deps.mkdirRecursive(parent);
2297
+ }
2159
2298
  if (row.classification === "universal-template") {
2160
2299
  const srcAbs = deps.joinPath(deps.platformRoot, ...row.source.split("/"));
2161
2300
  deps.copyFile(srcAbs, destAbs);
@@ -2175,24 +2314,41 @@ function runCustomerVaultInit(args, deps, options2 = {}) {
2175
2314
  targetDir,
2176
2315
  vaultName: options2.vaultName ?? defaultVaultName(targetDir),
2177
2316
  templateVersion: TEMPLATE_VERSION,
2317
+ mode,
2178
2318
  manifestRows: rows,
2179
2319
  rows: rowResults,
2180
2320
  deps,
2181
- recordWrite: (path5) => {
2182
- written.push(path5);
2321
+ recordWrite: (path7) => {
2322
+ written.push(path7);
2183
2323
  }
2184
2324
  };
2185
2325
  for (const hook of options2.afterScaffold) hook(ctx);
2186
2326
  }
2187
2327
  const createdCount = rowResults.filter((r) => r.action === "created").length;
2188
2328
  const keptCount = rowResults.length - createdCount;
2329
+ const added = rowResults.filter((r) => r.action === "created").map((r) => r.dest);
2330
+ let message;
2331
+ if (mode === "refresh") {
2332
+ const parts = [];
2333
+ if (added.length > 0)
2334
+ parts.push(`added ${added.length} missing template file(s): ${added.join(", ")}`);
2335
+ if (pin === "written")
2336
+ parts.push(`pinned template_version: ${TEMPLATE_VERSION} in heyspex.yaml`);
2337
+ const hookWrites = written.length - added.length - (pin === "written" ? 1 : 0);
2338
+ if (hookWrites > 0) parts.push(`${hookWrites} other file(s) written`);
2339
+ message = parts.length > 0 ? `Refreshed customer vault at ${targetDir}: ${parts.join("; ")} (${keptCount} existing file(s) kept untouched).` : `Customer vault at ${targetDir} is already current (template v${TEMPLATE_VERSION}, ${keptCount} file(s) kept) \u2014 nothing written.`;
2340
+ } else {
2341
+ message = `Initialized customer vault at ${targetDir} (${written.length} files written, ${createdCount} manifest rows created, ${keptCount} kept, template v${TEMPLATE_VERSION}).`;
2342
+ }
2189
2343
  return {
2190
2344
  code: EXIT.OK,
2191
- message: `Initialized customer vault at ${targetDir} (${written.length} files written, ${createdCount} manifest rows created, ${keptCount} kept, template v${TEMPLATE_VERSION}).`,
2345
+ message,
2192
2346
  payload: {
2193
2347
  ok: true,
2348
+ mode,
2194
2349
  targetDir,
2195
2350
  templateVersion: TEMPLATE_VERSION,
2351
+ templatePin: pin,
2196
2352
  vaultId,
2197
2353
  written,
2198
2354
  rows: rowResults,
@@ -2205,10 +2361,10 @@ function runCustomerVaultInit(args, deps, options2 = {}) {
2205
2361
  }
2206
2362
 
2207
2363
  // spex-console/src/lib/cli/init-git-finalize.ts
2208
- import { spawnSync } from "node:child_process";
2364
+ import { spawnSync as spawnSync2 } from "node:child_process";
2209
2365
  var nodeGitRunner = {
2210
2366
  run(args, cwd) {
2211
- const result = spawnSync("git", args, { cwd, encoding: "utf8" });
2367
+ const result = spawnSync2("git", args, { cwd, encoding: "utf8" });
2212
2368
  return {
2213
2369
  ok: result.error === void 0 && result.status === 0,
2214
2370
  stdout: result.stdout ?? "",
@@ -2271,12 +2427,21 @@ function gitFinalizeHook(runner, notify) {
2271
2427
  }
2272
2428
 
2273
2429
  // spex-console/src/lib/cli/install.ts
2274
- import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
2430
+ import { isAbsolute as isAbsolute4, resolve as resolveNodePath2 } from "node:path";
2431
+
2432
+ // spex-console/src/lib/vault-resolve/index.ts
2433
+ import { existsSync as existsSync7, readFileSync as readFileSync11, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
2434
+ import { tmpdir as tmpdir2 } from "node:os";
2435
+ import path3 from "node:path";
2436
+
2437
+ // spex-console/src/lib/cli/satellite.ts
2438
+ import { resolve as resolveNodePath } from "node:path";
2439
+ import process2 from "node:process";
2275
2440
 
2276
2441
  // scripts/install-cursor-hooks.mjs
2277
2442
  import fs2 from "node:fs";
2278
- import path3 from "node:path";
2279
- import { fileURLToPath as fileURLToPath3 } from "node:url";
2443
+ import path5 from "node:path";
2444
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
2280
2445
 
2281
2446
  // scripts/cursor-hooks/manifest.mjs
2282
2447
  var SHIM = "scripts/cursor-hooks/_run.mjs";
@@ -2422,9 +2587,9 @@ function timeoutFor(entry) {
2422
2587
  // scripts/lib/customer-kit.mjs
2423
2588
  import { createHash } from "node:crypto";
2424
2589
  import fs from "node:fs";
2425
- import path2 from "node:path";
2590
+ import path4 from "node:path";
2426
2591
  import os2 from "node:os";
2427
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
2592
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
2428
2593
 
2429
2594
  // scripts/lib/hook-dispositions.mjs
2430
2595
  var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
@@ -2691,7 +2856,7 @@ function toPosixPath(p) {
2691
2856
  function samePlatformPath(a, b) {
2692
2857
  if (!a || !b) return false;
2693
2858
  const norm = (p) => {
2694
- const s = toPosixPath(path2.resolve(p));
2859
+ const s = toPosixPath(path4.resolve(p));
2695
2860
  return process.platform === "win32" ? s.toLowerCase() : s;
2696
2861
  };
2697
2862
  return norm(a) === norm(b);
@@ -2764,8 +2929,16 @@ var LAUNCHER_TEMPLATE = [
2764
2929
  "// 4. node_modules/heyspex up from $__ENV__, $CLAUDE_PROJECT_DIR, $CURSOR_PROJECT_DIR, cwd",
2765
2930
  "// 5. <npm root -g>/heyspex a global install, last resort",
2766
2931
  "// Nothing found -> one stderr line and exit 0 (fail-open, like the shim itself).",
2932
+ "//",
2933
+ "// The same resolution starts the package's other entrypoints, so no committed",
2934
+ "// file names node_modules/heyspex directly and a repo with no node_modules of",
2935
+ "// its own still works off a global install:",
2936
+ "// --mcp the stdio MCP server (.mcp.json, .cursor/mcp.json)",
2937
+ "// --statusline the Claude Code status line (statusLine.command)",
2938
+ "// --resolve print the package root, exit 0; exit 1 when none (hs doctor)",
2767
2939
  "import { spawnSync } from 'node:child_process';",
2768
- "import { existsSync } from 'node:fs';",
2940
+ "import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';",
2941
+ "import { homedir } from 'node:os';",
2769
2942
  "import { dirname, resolve } from 'node:path';",
2770
2943
  "import { fileURLToPath, pathToFileURL } from 'node:url';",
2771
2944
  "",
@@ -2775,6 +2948,7 @@ var LAUNCHER_TEMPLATE = [
2775
2948
  "const text = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);",
2776
2949
  "const hasShim = (root) => Boolean(root) && existsSync(resolve(root, ...SHIM));",
2777
2950
  "",
2951
+ "__YIELD__",
2778
2952
  "function packageUpFrom(start) {",
2779
2953
  " let dir = resolve(start);",
2780
2954
  " for (;;) {",
@@ -2809,31 +2983,109 @@ var LAUNCHER_TEMPLATE = [
2809
2983
  " if (hasShim(global)) root = global;",
2810
2984
  "}",
2811
2985
  "",
2986
+ "const ENTRYPOINTS = {",
2987
+ " '--mcp': 'vendor/heyspex-mcp/cli.mjs',",
2988
+ " '--statusline': 'scripts/claude-statusline/statusline-command.mjs',",
2989
+ "};",
2990
+ "const mode = process.argv[2];",
2991
+ "const entry = Object.hasOwn(ENTRYPOINTS, mode) ? ENTRYPOINTS[mode] : null;",
2992
+ "",
2812
2993
  "if (!root) {",
2994
+ " const what = entry || mode === '--resolve' ? mode : 'hook ' + (mode ?? '?');",
2813
2995
  " 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',",
2996
+ " '[heyspex] ' + what + ' skipped: no heyspex package found ' +",
2997
+ ` '(run "npm install heyspex" or "npm link heyspex" in this repo, install it globally, or set HEYSPEX_PLATFORM_ROOT). ' +`,
2998
+ " 'Looked for __SHIM__ under HEYSPEX_PLATFORM_ROOT, HEYSPEX_MCP_ENTRY, node_modules/heyspex up from ' + starts.join(', ') + ', and npm root -g.\\n',",
2817
2999
  " );",
3000
+ " // Hooks and the status line fail open; the MCP server and --resolve fail",
3001
+ " // loudly, so the client and hs doctor both see it.",
3002
+ " process.exit(mode === '--mcp' || mode === '--resolve' ? 1 : 0);",
3003
+ "}",
3004
+ "if (mode === '--resolve') {",
3005
+ " process.stdout.write(root + '\\n');",
2818
3006
  " process.exit(0);",
2819
3007
  "}",
2820
3008
  "",
2821
3009
  "// 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;",
3010
+ '// reads; the target replaces this file so a script guarded by "am I main?"',
3011
+ "// runs exactly as if it had been invoked directly. Real path, so that guard",
3012
+ "// still matches when node_modules/heyspex is an npm link or a junction (node",
3013
+ "// realpaths import.meta.url but leaves argv[1] alone).",
3014
+ "let target = resolve(root, ...(entry ? entry.split('/') : SHIM));",
3015
+ "try {",
3016
+ " target = realpathSync(target);",
3017
+ "} catch {",
3018
+ " // keep the resolved path",
3019
+ "}",
3020
+ "if (entry) process.argv.splice(2, 1);",
3021
+ "process.argv[1] = target;",
2826
3022
  "try {",
2827
- " await import(pathToFileURL(shim).href);",
3023
+ " await import(pathToFileURL(target).href);",
2828
3024
  "} catch (err) {",
2829
- " process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
3025
+ " process.stderr.write('[heyspex] ' + (entry ? mode : 'hook ' + (process.argv[2] ?? '?')) + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
3026
+ " // Hooks and the status line fail open; the MCP server fails loudly.",
3027
+ " if (mode === '--mcp') process.exit(1);",
2830
3028
  " process.exit(0);",
2831
3029
  "}",
2832
3030
  ""
2833
3031
  ].join("\n");
3032
+ var LAUNCHER_YIELD_MARKER = "// yields-to-plugin: v1";
3033
+ var LAUNCHER_YIELD_BLOCK = [
3034
+ LAUNCHER_YIELD_MARKER + " -- one source of hooks: when the heyspex plugin is enabled on this machine",
3035
+ "// its own launcher (plugins/heyspex/bin/run-hook.mjs, which never calls this file) already runs every",
3036
+ "// hook, so this committed row exits 0 silently before resolving anything and each hook fires ONCE.",
3037
+ "// HEYSPEX_REPO_HOOKS=1 never yield (the repo rows are the chosen source);",
3038
+ `// enabledPlugins["heyspex@..."] === false in ~/.claude/settings.json or this repo's .claude/`,
3039
+ "// settings[.local].json: plugin off here, the row runs;",
3040
+ "// plugin evidence (what `hs doctor` hook-double-fire reads): a heyspex@... key in",
3041
+ "// ~/.claude/plugins/installed_plugins.json, enabledPlugins naming heyspex with a non-false",
3042
+ "// value, or a heyspex directory under ~/.copilot/installed-plugins | ~/.copilot/plugins.",
3043
+ "function readJson(file) {",
3044
+ " try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return undefined; }",
3045
+ "}",
3046
+ "function listDir(dir) {",
3047
+ " try { return readdirSync(dir); } catch { return []; }",
3048
+ "}",
3049
+ "function pluginTakesThisHook() {",
3050
+ " const flag = text(env.HEYSPEX_REPO_HOOKS);",
3051
+ " if (flag && flag !== '0' && flag.toLowerCase() !== 'false') return false;",
3052
+ " const home = homedir();",
3053
+ " const repo = resolve(here, '..', '..');",
3054
+ " const isHeyspex = (k) => /^heyspex(@|$)/i.test(k);",
3055
+ " let enabled = false;",
3056
+ " for (const f of [resolve(home, '.claude', 'settings.json'), resolve(repo, '.claude', 'settings.json'), resolve(repo, '.claude', 'settings.local.json')]) {",
3057
+ " const settings = readJson(f);",
3058
+ " const ep = settings && typeof settings === 'object' ? settings.enabledPlugins : null;",
3059
+ " if (!ep || typeof ep !== 'object') continue;",
3060
+ " for (const [k, v] of Object.entries(ep)) {",
3061
+ " if (!isHeyspex(k)) continue;",
3062
+ " if (v === false) return false;",
3063
+ " enabled = true;",
3064
+ " }",
3065
+ " }",
3066
+ " if (enabled) return true;",
3067
+ " const installed = readJson(resolve(home, '.claude', 'plugins', 'installed_plugins.json'));",
3068
+ " if (installed && typeof installed === 'object' && Object.keys(installed.plugins && typeof installed.plugins === 'object' ? installed.plugins : installed).some(isHeyspex)) return true;",
3069
+ " for (const sub of ['installed-plugins', 'plugins']) {",
3070
+ " const dir = resolve(home, '.copilot', sub);",
3071
+ " const names = listDir(dir);",
3072
+ " if (names.some((n) => /heyspex/i.test(n))) return true;",
3073
+ " if (names.some((n) => listDir(resolve(dir, n)).some((m) => /heyspex/i.test(m)))) return true;",
3074
+ " }",
3075
+ " return false;",
3076
+ "}",
3077
+ "// Only a HOOK EVENT yields: --mcp / --statusline / --resolve and an agent-run",
3078
+ "// `<script> --directive <key>` fetch are not hook events the plugin could run in",
3079
+ "// our place, so they always go on to the package below.",
3080
+ "const yieldMode = process.argv[2];",
3081
+ "const notAHookEvent = ['--mcp', '--statusline', '--resolve'].includes(yieldMode) || process.argv.includes('--directive');",
3082
+ "if (!notAHookEvent && pluginTakesThisHook()) process.exit(0);",
3083
+ "",
3084
+ ""
3085
+ ].join("\n");
2834
3086
  function renderCustomerLauncher(surface = "claude") {
2835
3087
  const l = CUSTOMER_LAUNCHERS[surface];
2836
- return LAUNCHER_TEMPLATE.replace(
3088
+ return LAUNCHER_TEMPLATE.replace("__YIELD__\n", surface === "claude" ? LAUNCHER_YIELD_BLOCK : "").replace(
2837
3089
  /__(REGISTRY|ENV|LAUNCHER|SHIM)__/g,
2838
3090
  (_m, token) => token === "REGISTRY" ? l.registry : token === "ENV" ? l.projectDirEnv : token === "LAUNCHER" ? l.rel.join("/") : l.shim
2839
3091
  );
@@ -2965,7 +3217,7 @@ var SCRIPT_VERBS = Object.freeze({
2965
3217
  });
2966
3218
  var PORTABLE_PACKAGE_DIR = "node_modules/heyspex";
2967
3219
  var fsDeps = {
2968
- joinPath: (...s) => path2.join(...s),
3220
+ joinPath: (...s) => path4.join(...s),
2969
3221
  readFile: (p) => {
2970
3222
  try {
2971
3223
  return fs.readFileSync(p, "utf8");
@@ -2984,22 +3236,22 @@ var fsDeps = {
2984
3236
  function invokedDirectly(importMetaUrl, selfBasename, argv1 = process.argv[1]) {
2985
3237
  try {
2986
3238
  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;
3239
+ const self = fileURLToPath3(importMetaUrl);
3240
+ if (path4.basename(self) !== selfBasename) return false;
3241
+ return pathToFileURL(path4.resolve(argv1)).href === pathToFileURL(self).href;
2990
3242
  } catch {
2991
3243
  return false;
2992
3244
  }
2993
3245
  }
2994
- var SELF = fileURLToPath2(import.meta.url);
3246
+ var SELF = fileURLToPath3(import.meta.url);
2995
3247
  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");
3248
+ const platformRoot = path4.resolve(path4.dirname(SELF), "..", "..");
3249
+ const target = path4.join(platformRoot, "plugins", "heyspex", "hooks", "hooks.json");
2998
3250
  const want = renderPluginHooksText();
2999
3251
  const same = (fsDeps.readFile(target) ?? "").replace(/\r\n/g, "\n") === want;
3000
3252
  if (process.argv.includes("--write-plugin-hooks")) {
3001
3253
  if (!same) fs.writeFileSync(target, want, "utf8");
3002
- console.log(`customer-kit: ${same ? "unchanged" : "wrote"} ${path2.relative(platformRoot, target)}`);
3254
+ console.log(`customer-kit: ${same ? "unchanged" : "wrote"} ${path4.relative(platformRoot, target)}`);
3003
3255
  } else if (process.argv.includes("--check-plugin-hooks")) {
3004
3256
  if (!same) {
3005
3257
  console.error("customer-kit: plugins/heyspex/hooks/hooks.json is stale \u2014 run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`");
@@ -3007,15 +3259,15 @@ if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
3007
3259
  }
3008
3260
  console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
3009
3261
  } else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
3010
- const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
3262
+ const ghTarget = path4.join(platformRoot, ".github", "hooks", "heyspex.json");
3011
3263
  const ghWant = renderGithubHooksText();
3012
3264
  const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
3013
3265
  if (process.argv.includes("--write-github-hooks")) {
3014
3266
  if (!ghSame) {
3015
- fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
3267
+ fs.mkdirSync(path4.dirname(ghTarget), { recursive: true });
3016
3268
  fs.writeFileSync(ghTarget, ghWant, "utf8");
3017
3269
  }
3018
- console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
3270
+ console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path4.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
3019
3271
  } else if (!ghSame) {
3020
3272
  console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
3021
3273
  process.exit(1);
@@ -3028,9 +3280,9 @@ if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
3028
3280
  }
3029
3281
 
3030
3282
  // 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");
3283
+ var PLATFORM_ROOT = path5.resolve(path5.dirname(fileURLToPath4(import.meta.url)), "..");
3284
+ var REPO_ROOT = process.env.HS_SETUP_TARGET?.trim() ? path5.resolve(process.env.HS_SETUP_TARGET.trim()) : PLATFORM_ROOT;
3285
+ var SETTINGS_PATH = path5.join(REPO_ROOT, ".cursor", "hooks.json");
3034
3286
  function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
3035
3287
  const customer = !samePlatformPath(targetRoot, platformRoot);
3036
3288
  return {
@@ -3042,7 +3294,7 @@ function cursorKitFor(targetRoot, platformRoot = PLATFORM_ROOT) {
3042
3294
  }
3043
3295
  var KIT = cursorKitFor(REPO_ROOT);
3044
3296
  var ACTIVE_HOOKS = KIT.hooks;
3045
- var LAUNCHER_PATH = path3.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
3297
+ var LAUNCHER_PATH = path5.join(REPO_ROOT, ...CUSTOMER_LAUNCHERS.cursor.rel);
3046
3298
  var LEGACY_MARKER = "scripts/claude-hooks/";
3047
3299
  function readSettings() {
3048
3300
  try {
@@ -3061,7 +3313,7 @@ function readSettings() {
3061
3313
  return { version: 1, hooks: {} };
3062
3314
  }
3063
3315
  function writeSettings(settings) {
3064
- fs2.mkdirSync(path3.dirname(SETTINGS_PATH), { recursive: true });
3316
+ fs2.mkdirSync(path5.dirname(SETTINGS_PATH), { recursive: true });
3065
3317
  fs2.writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}
3066
3318
  `, "utf8");
3067
3319
  }
@@ -3096,7 +3348,7 @@ function sameEntry(raw, entry, render = KIT.commandFor) {
3096
3348
  const want = desiredEntry(entry, render);
3097
3349
  return commandOf(raw) === want.command && raw.timeout === want.timeout && (raw.matcher ?? void 0) === (want.matcher ?? void 0);
3098
3350
  }
3099
- var PLUGIN_HOOKS_PATH = path3.join(REPO_ROOT, "plugins", "heyspex", "hooks", "cursor-hooks.json");
3351
+ var PLUGIN_HOOKS_PATH = path5.join(REPO_ROOT, "plugins", "heyspex", "hooks", "cursor-hooks.json");
3100
3352
  function pluginHooksDocument() {
3101
3353
  const hooks = {};
3102
3354
  for (const entry of HOOKS) {
@@ -3126,7 +3378,7 @@ function syncPluginHooks() {
3126
3378
  have = null;
3127
3379
  }
3128
3380
  if (have === want) return false;
3129
- fs2.mkdirSync(path3.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
3381
+ fs2.mkdirSync(path5.dirname(PLUGIN_HOOKS_PATH), { recursive: true });
3130
3382
  fs2.writeFileSync(PLUGIN_HOOKS_PATH, want, "utf8");
3131
3383
  return true;
3132
3384
  }
@@ -3199,7 +3451,7 @@ function install({ json = false } = {}) {
3199
3451
  if (KIT.customer) {
3200
3452
  const plan = launcherPlan();
3201
3453
  if (plan.write) {
3202
- fs2.mkdirSync(path3.dirname(LAUNCHER_PATH), { recursive: true });
3454
+ fs2.mkdirSync(path5.dirname(LAUNCHER_PATH), { recursive: true });
3203
3455
  fs2.writeFileSync(LAUNCHER_PATH, plan.text, "utf8");
3204
3456
  launcherWritten = true;
3205
3457
  }
@@ -3226,10 +3478,10 @@ function install({ json = false } = {}) {
3226
3478
  return;
3227
3479
  }
3228
3480
  if (pluginChanged) {
3229
- console.log(`install-cursor-hooks: regenerated ${path3.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
3481
+ console.log(`install-cursor-hooks: regenerated ${path5.relative(REPO_ROOT, PLUGIN_HOOKS_PATH)}`);
3230
3482
  }
3231
3483
  if (launcherWritten) {
3232
- console.log(`install-cursor-hooks: wrote ${path3.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
3484
+ console.log(`install-cursor-hooks: wrote ${path5.relative(REPO_ROOT, LAUNCHER_PATH)} (resolves node_modules/heyspex)`);
3233
3485
  }
3234
3486
  if (!settingsChanged) {
3235
3487
  console.log(`install-cursor-hooks: already installed (${ACTIVE_HOOKS.length} hooks, no changes).`);
@@ -3240,7 +3492,7 @@ function install({ json = false } = {}) {
3240
3492
  if (repaired.length) parts.push(`repaired ${repaired.length}`);
3241
3493
  if (removed.length) parts.push(`removed ${removed.length}`);
3242
3494
  console.log(
3243
- `install-cursor-hooks: ${parts.join("; ")} \u2192 ${path3.relative(REPO_ROOT, SETTINGS_PATH)} (${ACTIVE_HOOKS.length} hooks registered)`
3495
+ `install-cursor-hooks: ${parts.join("; ")} \u2192 ${path5.relative(REPO_ROOT, SETTINGS_PATH)} (${ACTIVE_HOOKS.length} hooks registered)`
3244
3496
  );
3245
3497
  for (const line of removed) console.log(` - ${line}`);
3246
3498
  console.log(
@@ -3342,30 +3594,26 @@ if (invokedDirectly(import.meta.url, "install-cursor-hooks.mjs")) {
3342
3594
  init_credentials();
3343
3595
 
3344
3596
  // spex-console/src/lib/cli/connect-checks.ts
3345
- import { dirname as dirname5 } from "node:path";
3597
+ import { dirname as dirname6 } from "node:path";
3346
3598
  import { pathToFileURL as pathToFileURL2 } from "node:url";
3347
3599
 
3348
3600
  // spex-console/src/lib/dev-mode.ts
3349
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
3601
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "node:fs";
3350
3602
  import { join as join10 } from "node:path";
3351
3603
  init_credentials();
3352
3604
 
3353
3605
  // spex-console/src/lib/cli/connect-checks.ts
3354
3606
  init_credentials();
3355
3607
 
3356
- // spex-console/src/lib/cli/satellite.ts
3357
- import { resolve as resolveNodePath } from "node:path";
3358
- import process2 from "node:process";
3359
-
3360
3608
  // spex-console/src/lib/cli/principle-selection.ts
3361
- import { basename as basename3 } from "node:path";
3609
+ import { basename as basename4 } from "node:path";
3362
3610
  var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
3363
3611
  function principleKey(row) {
3364
- const base = basename3(row.dest, ".md");
3612
+ const base = basename4(row.dest, ".md");
3365
3613
  return base.startsWith("principle-") ? base.slice("principle-".length) : base;
3366
3614
  }
3367
3615
  function principleWikilink(row) {
3368
- return basename3(row.dest, ".md");
3616
+ return basename4(row.dest, ".md");
3369
3617
  }
3370
3618
  function parsePrinciplesFlag(value, rows) {
3371
3619
  const principleRows = rows.filter((row) => row.group === "principle");
@@ -3441,24 +3689,28 @@ var ALL_FIELDS = [
3441
3689
  ];
3442
3690
 
3443
3691
  // spex-console/src/lib/cli/vault-workspace-shared.ts
3444
- import { readFileSync as readFileSync10 } from "node:fs";
3692
+ import { readFileSync as readFileSync13 } from "node:fs";
3445
3693
 
3446
3694
  // spex-console/src/lib/indexer/parser.ts
3447
3695
  var import_gray_matter = __toESM(require_gray_matter(), 1);
3448
3696
  import { createHash as createHash2 } from "node:crypto";
3449
3697
  import { promises as fs3 } from "node:fs";
3450
- import path4 from "node:path";
3698
+ import path6 from "node:path";
3451
3699
 
3452
3700
  // spex-console/src/lib/cli/install.ts
3453
3701
  var MCP_REL = ["vendor", "heyspex-mcp", "cli.mjs"];
3454
3702
  var PORTABLE_MCP_CLI = Object.freeze({
3703
+ claude: `\${CLAUDE_PROJECT_DIR:-.}/${CUSTOMER_LAUNCHERS.claude.rel.join("/")}`,
3704
+ cursor: `\${workspaceFolder}/${CUSTOMER_LAUNCHERS.cursor.rel.join("/")}`
3705
+ });
3706
+ var LEGACY_PORTABLE_MCP_CLI = Object.freeze({
3455
3707
  claude: `\${CLAUDE_PROJECT_DIR:-.}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`,
3456
3708
  cursor: `\${workspaceFolder}/${PORTABLE_PACKAGE_DIR}/${MCP_REL.join("/")}`
3457
3709
  });
3458
3710
  function resolveCallerPath(p, env, fallbackCwd) {
3459
3711
  const trimmed = p?.trim() ?? "";
3460
3712
  if (!trimmed) return "";
3461
- if (isAbsolute3(trimmed)) return trimmed;
3713
+ if (isAbsolute4(trimmed)) return trimmed;
3462
3714
  const base = env.HS_INVOKED_CWD?.trim() || fallbackCwd;
3463
3715
  return resolveNodePath2(base, trimmed);
3464
3716
  }
@@ -3473,7 +3725,6 @@ var WRITE_TARGET_KIND = {
3473
3725
  claudeMcp: "claude_mcp",
3474
3726
  claudeMd: "claude_md",
3475
3727
  agentsMd: "agents_md",
3476
- statuslineVendor: "statusline_vendor",
3477
3728
  gitignore: "gitignore",
3478
3729
  claudeMdTemplate: "claude_md_template",
3479
3730
  agentsMdTemplate: "agents_md_template"
@@ -3530,12 +3781,6 @@ var INSTALL_WRITE_TARGETS = [
3530
3781
  path: (targetDir, joinPath) => joinPath(targetDir, "AGENTS.md.template"),
3531
3782
  appliesTo: (args, opts) => Boolean(args.registerVault) && !opts.attached && opts.installContract
3532
3783
  },
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
3784
  {
3540
3785
  kind: WRITE_TARGET_KIND.gitignore,
3541
3786
  rel: ".gitignore",
@@ -3696,13 +3941,13 @@ function runEntry(name, main2, options2 = {}) {
3696
3941
 
3697
3942
  // spex-console/bin/hs-init.ts
3698
3943
  var INTERACTIVE = process4.stdin.isTTY === true;
3699
- var PKG_ROOT = resolve5(
3700
- resolveCodeRoot({ packageRoot: resolve5(dirname6(fileURLToPath4(import.meta.url)), "..", "..") })
3944
+ var PKG_ROOT = resolve6(
3945
+ resolveCodeRoot({ packageRoot: resolve6(dirname7(fileURLToPath5(import.meta.url)), "..", "..") })
3701
3946
  );
3702
3947
  var CONFIG_SEED_PATH = join11(PKG_ROOT, "vendor", "heyspex-mcp", "config-seed.yaml");
3703
3948
  function readConfigSeed() {
3704
3949
  try {
3705
- return readFileSync11(CONFIG_SEED_PATH, "utf8");
3950
+ return readFileSync14(CONFIG_SEED_PATH, "utf8");
3706
3951
  } catch {
3707
3952
  process4.stderr.write(
3708
3953
  `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).
@@ -3736,7 +3981,7 @@ async function resolvePrincipleSelection(principlesFlag) {
3736
3981
  return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
3737
3982
  }
3738
3983
  async function main() {
3739
- const repoRoot = process4.env.HS_VAULT_ROOT ? resolve5(process4.env.HS_VAULT_ROOT) : process4.cwd();
3984
+ const repoRoot = process4.env.HS_VAULT_ROOT ? resolve6(process4.env.HS_VAULT_ROOT) : process4.cwd();
3740
3985
  const parsed = parseInitArgs(process4.argv.slice(3));
3741
3986
  if (parsed.help) {
3742
3987
  process4.stdout.write(`${HELP}
@@ -3752,28 +3997,29 @@ async function main() {
3752
3997
  }
3753
3998
  const args = parsed.args;
3754
3999
  if (args.customerVaultDir) {
3755
- const here = dirname6(fileURLToPath4(import.meta.url));
3756
- const platformRoot = resolve5(here, "..", "..");
3757
- const targetDir = resolve5(resolveCallerPath(args.customerVaultDir, process4.env, process4.cwd()));
4000
+ const here = dirname7(fileURLToPath5(import.meta.url));
4001
+ const platformRoot = resolve6(here, "..", "..");
4002
+ const targetDir = resolve6(resolveCallerPath(args.customerVaultDir, process4.env, process4.cwd()));
4003
+ const eol = createGeneratedFileEol({ repoRoot: targetDir });
3758
4004
  const deps = {
3759
4005
  platformRoot,
3760
- exists: (p) => existsSync5(p),
4006
+ exists: (p) => existsSync8(p),
3761
4007
  readdir: (p) => readdirSync2(p),
3762
4008
  mkdirRecursive: (p) => {
3763
- mkdirSync6(p, { recursive: true });
4009
+ mkdirSync7(p, { recursive: true });
3764
4010
  },
3765
- writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
3766
- copyFile: (src, dest) => copyFileSync(src, dest),
4011
+ writeFile: (p, c) => writeFileSync7(p, eol.convert(p, c), "utf8"),
4012
+ copyFile: (src, dest) => copyFileSync2(src, dest),
3767
4013
  joinPath: (...s) => join11(...s),
3768
4014
  readConfigSeed,
3769
4015
  readFile: (p) => {
3770
4016
  try {
3771
- return readFileSync11(p, "utf8");
4017
+ return readFileSync14(p, "utf8");
3772
4018
  } catch {
3773
4019
  return null;
3774
4020
  }
3775
4021
  },
3776
- isDirectory: (p) => statSync(p).isDirectory()
4022
+ isDirectory: (p) => statSync5(p).isDirectory()
3777
4023
  };
3778
4024
  const principlesResult = await resolvePrincipleSelection(args.principles);
3779
4025
  if (!principlesResult.ok) {
@@ -3811,20 +4057,21 @@ async function main() {
3811
4057
  process4.exitCode = result2.code;
3812
4058
  return;
3813
4059
  }
4060
+ const repoEol = createGeneratedFileEol({ repoRoot });
3814
4061
  const result = runInit(args, {
3815
4062
  repoRoot,
3816
- fileExists: (p) => existsSync5(p),
4063
+ fileExists: (p) => existsSync8(p),
3817
4064
  readFile: (p) => {
3818
4065
  try {
3819
- return readFileSync11(p, "utf8");
4066
+ return readFileSync14(p, "utf8");
3820
4067
  } catch {
3821
4068
  return null;
3822
4069
  }
3823
4070
  },
3824
- writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
4071
+ writeFile: (p, c) => writeFileSync7(p, repoEol.convert(p, c), "utf8"),
3825
4072
  joinPath: (...s) => join11(...s),
3826
4073
  mkdirRecursive: (p) => {
3827
- mkdirSync6(p, { recursive: true });
4074
+ mkdirSync7(p, { recursive: true });
3828
4075
  },
3829
4076
  readConfigSeed
3830
4077
  });