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.
- package/.claude-plugin/marketplace.json +14 -0
- package/bin/hs.mjs +163 -11
- package/dist/cli/hs-check.mjs +88 -46
- package/dist/cli/hs-claim.mjs +84 -39
- package/dist/cli/hs-connect.mjs +583 -129
- package/dist/cli/hs-defer.mjs +251 -30
- package/dist/cli/hs-dev.mjs +20 -8
- package/dist/cli/hs-env.mjs +62 -34
- package/dist/cli/hs-index-rebuild.mjs +288 -83
- package/dist/cli/hs-index-reconcile.mjs +397 -111
- package/dist/cli/hs-init.mjs +2143 -91
- package/dist/cli/hs-install.mjs +628 -106
- package/dist/cli/hs-join.mjs +686 -236
- package/dist/cli/hs-vault-pull.mjs +3189 -179
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +396 -178
- package/dist/cli/hs-workspace.mjs +289 -151
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
- package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
- package/docs/copilot-plugin-install.md +162 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/package.json +20 -2
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
- package/plugins/heyspex/README.md +309 -0
- package/plugins/heyspex/bin/run-hook.mjs +20 -2
- package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
- package/plugins/heyspex/hooks/hooks.json +94 -37
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
- package/scripts/claude-hooks/_run.mjs +316 -23
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
- package/scripts/claude-hooks/session-end.mjs +2 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
- package/scripts/cursor-hooks/manifest.mjs +46 -0
- package/scripts/heyspex-mode.mjs +20 -0
- package/scripts/hs-mode.mjs +1 -1
- package/scripts/hs-view.mjs +180 -17
- package/scripts/install-claude-code-hooks.mjs +3 -1
- package/scripts/lib/console-url-resolver.mjs +3 -2
- package/scripts/lib/customer-kit.mjs +327 -10
- package/scripts/lib/dev-mode.mjs +7 -6
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-log.mjs +2 -1
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -356
- package/scripts/lib/window-placement.mjs +109 -0
- package/spexharness/src/adapters/claude-code.mjs +11 -3
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/intent-classifier.mjs +4 -2
- package/spexharness/src/intent-directive.mjs +626 -541
- package/spexharness/src/rule-table.mjs +31 -1
- package/vendor/heyspex-mcp/account-switch.mjs +6 -5
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
- package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
- package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
- package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
- package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
- package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
- package/vendor/heyspex-mcp/cli.mjs +7 -1
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
- package/vendor/heyspex-mcp/console-transport.mjs +7 -1
- package/vendor/heyspex-mcp/credentials.mjs +9 -8
- package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
- package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
- package/vendor/heyspex-mcp/http-transport.mjs +2 -0
- package/vendor/heyspex-mcp/list-tools.mjs +1 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
- package/vendor/heyspex-mcp/plan-push.mjs +3 -2
- package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
- package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
- package/vendor/heyspex-mcp/promote-research.mjs +6 -0
- package/vendor/heyspex-mcp/read-path.mjs +162 -6
- package/vendor/heyspex-mcp/request-context.mjs +25 -1
- package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/session-owner.mjs +3 -2
- package/vendor/heyspex-mcp/session-tools.mjs +10 -0
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
- package/vendor/heyspex-mcp/split-research.mjs +3 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
- package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
- package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
- package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
- package/vendor/heyspex-mcp/vault-io.mjs +720 -724
- package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
- package/vendor/heyspex-mcp/whoami.mjs +2 -1
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
package/dist/cli/hs-install.mjs
CHANGED
|
@@ -35,11 +35,26 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
35
35
|
mod
|
|
36
36
|
));
|
|
37
37
|
|
|
38
|
+
// scripts/lib/heyspex-home.mjs
|
|
39
|
+
import { homedir } from "node:os";
|
|
40
|
+
function heyspexHome(env = process.env) {
|
|
41
|
+
const override = env?.HEYSPEX_HOME;
|
|
42
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
43
|
+
return override.trim();
|
|
44
|
+
}
|
|
45
|
+
return homedir();
|
|
46
|
+
}
|
|
47
|
+
var init_heyspex_home = __esm({
|
|
48
|
+
"scripts/lib/heyspex-home.mjs"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
38
53
|
// vendor/heyspex-mcp/credentials.mjs
|
|
39
54
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
40
|
-
import os, { homedir } from "node:os";
|
|
55
|
+
import os, { homedir as homedir2 } from "node:os";
|
|
41
56
|
import { dirname, join, resolve } from "node:path";
|
|
42
|
-
function credentialsFilePath(homeDir =
|
|
57
|
+
function credentialsFilePath(homeDir = heyspexHome()) {
|
|
43
58
|
return join(homeDir, ".heyspex", "credentials.yaml");
|
|
44
59
|
}
|
|
45
60
|
function parseMcpToken(raw) {
|
|
@@ -152,14 +167,14 @@ function readLegacyConfigAccountId(read, homeDir) {
|
|
|
152
167
|
const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
|
|
153
168
|
return value || null;
|
|
154
169
|
}
|
|
155
|
-
function readBearer({ env = process.env, homeDir =
|
|
170
|
+
function readBearer({ env = process.env, homeDir = heyspexHome(), readFile, accountId } = {}) {
|
|
156
171
|
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
157
172
|
const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
|
|
158
173
|
const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
|
|
159
174
|
const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
|
|
160
175
|
return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
|
|
161
176
|
}
|
|
162
|
-
function readCurrentAccountId({ homeDir =
|
|
177
|
+
function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
|
|
163
178
|
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
164
179
|
const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
|
|
165
180
|
if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
|
|
@@ -177,6 +192,7 @@ var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT
|
|
|
177
192
|
var init_credentials = __esm({
|
|
178
193
|
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
179
194
|
"use strict";
|
|
195
|
+
init_heyspex_home();
|
|
180
196
|
MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
181
197
|
MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
182
198
|
CREDENTIALS_SCHEMA_VERSION = 2;
|
|
@@ -189,9 +205,9 @@ var init_credentials = __esm({
|
|
|
189
205
|
|
|
190
206
|
// scripts/lib/dev-mode.mjs
|
|
191
207
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
192
|
-
import { homedir as
|
|
208
|
+
import { homedir as homedir3 } from "node:os";
|
|
193
209
|
import { join as join2 } from "node:path";
|
|
194
|
-
function devModeFilePath(homeDir =
|
|
210
|
+
function devModeFilePath(homeDir = heyspexHome()) {
|
|
195
211
|
return join2(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
196
212
|
}
|
|
197
213
|
function normalizeMode(raw) {
|
|
@@ -216,7 +232,7 @@ function deriveSemantics(mode, reliance) {
|
|
|
216
232
|
vaultWriteMode: local ? "local" : "cloud"
|
|
217
233
|
};
|
|
218
234
|
}
|
|
219
|
-
function readLocalDevModeRecord({ homeDir =
|
|
235
|
+
function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
|
|
220
236
|
let raw;
|
|
221
237
|
try {
|
|
222
238
|
raw = readFile(devModeFilePath(homeDir));
|
|
@@ -246,7 +262,7 @@ function readLocalDevModeRecord({ homeDir = homedir2(), readFile = defaultReadFi
|
|
|
246
262
|
}
|
|
247
263
|
function resolveDevMode({
|
|
248
264
|
env = process.env,
|
|
249
|
-
homeDir =
|
|
265
|
+
homeDir = heyspexHome(),
|
|
250
266
|
readFile = defaultReadFile,
|
|
251
267
|
cloudSetting = void 0
|
|
252
268
|
} = {}) {
|
|
@@ -309,6 +325,7 @@ var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
|
309
325
|
var init_dev_mode = __esm({
|
|
310
326
|
"scripts/lib/dev-mode.mjs"() {
|
|
311
327
|
"use strict";
|
|
328
|
+
init_heyspex_home();
|
|
312
329
|
init_credentials();
|
|
313
330
|
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
314
331
|
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
@@ -982,20 +999,21 @@ var require_gray_matter = __commonJS({
|
|
|
982
999
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
983
1000
|
import {
|
|
984
1001
|
copyFileSync,
|
|
985
|
-
existsSync as
|
|
986
|
-
mkdirSync as
|
|
987
|
-
readdirSync,
|
|
988
|
-
readFileSync as
|
|
1002
|
+
existsSync as existsSync6,
|
|
1003
|
+
mkdirSync as mkdirSync6,
|
|
1004
|
+
readdirSync as readdirSync2,
|
|
1005
|
+
readFileSync as readFileSync11,
|
|
989
1006
|
statSync,
|
|
990
|
-
writeFileSync as
|
|
1007
|
+
writeFileSync as writeFileSync6
|
|
991
1008
|
} from "node:fs";
|
|
992
|
-
import { join as
|
|
1009
|
+
import { join as join12, resolve as resolve5 } from "node:path";
|
|
993
1010
|
import process4 from "node:process";
|
|
994
1011
|
|
|
995
1012
|
// scripts/lib/console-url-resolver.mjs
|
|
1013
|
+
init_heyspex_home();
|
|
996
1014
|
init_dev_mode();
|
|
997
1015
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
998
|
-
import { homedir as
|
|
1016
|
+
import { homedir as homedir4 } from "node:os";
|
|
999
1017
|
import { join as join3 } from "node:path";
|
|
1000
1018
|
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
1001
1019
|
function trimTrailingSlash(url) {
|
|
@@ -1022,7 +1040,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
|
1022
1040
|
} catch {
|
|
1023
1041
|
}
|
|
1024
1042
|
}
|
|
1025
|
-
function readCredentialsConsoleUrl({ homeDir =
|
|
1043
|
+
function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
|
|
1026
1044
|
try {
|
|
1027
1045
|
const raw = readFile(join3(homeDir, ".heyspex", "credentials.yaml"));
|
|
1028
1046
|
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
@@ -1066,7 +1084,7 @@ function resolveModeEnvelope({ env, homeDir }) {
|
|
|
1066
1084
|
function resolveConsoleUrl({
|
|
1067
1085
|
repoRoot,
|
|
1068
1086
|
env = process.env,
|
|
1069
|
-
homeDir =
|
|
1087
|
+
homeDir = heyspexHome(),
|
|
1070
1088
|
strict = false,
|
|
1071
1089
|
devMode,
|
|
1072
1090
|
log = (line) => process.stderr.write(line),
|
|
@@ -1136,17 +1154,19 @@ function shellQuote(p) {
|
|
|
1136
1154
|
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
1137
1155
|
|
|
1138
1156
|
// vendor/heyspex-mcp/console-fetch.mjs
|
|
1157
|
+
init_heyspex_home();
|
|
1139
1158
|
init_credentials();
|
|
1140
|
-
import { readFileSync as
|
|
1141
|
-
import { homedir as
|
|
1142
|
-
import { join as
|
|
1159
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1160
|
+
import { homedir as homedir7 } from "node:os";
|
|
1161
|
+
import { join as join8 } from "node:path";
|
|
1143
1162
|
|
|
1144
1163
|
// vendor/heyspex-mcp/session-owner.mjs
|
|
1164
|
+
init_heyspex_home();
|
|
1145
1165
|
init_credentials();
|
|
1146
1166
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1147
|
-
import { homedir as
|
|
1167
|
+
import { homedir as homedir5 } from "node:os";
|
|
1148
1168
|
import { join as join5 } from "node:path";
|
|
1149
|
-
function resolveSessionOwnerUncached({ env = process.env, homeDir =
|
|
1169
|
+
function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
|
|
1150
1170
|
const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
|
|
1151
1171
|
if (fromEnv) return fromEnv;
|
|
1152
1172
|
return readCurrentAccountId({ homeDir, readFile });
|
|
@@ -1176,10 +1196,23 @@ function isVaultLessEndpoint(method, path5) {
|
|
|
1176
1196
|
});
|
|
1177
1197
|
}
|
|
1178
1198
|
|
|
1199
|
+
// vendor/heyspex-mcp/write-outbox.mjs
|
|
1200
|
+
import {
|
|
1201
|
+
readdirSync,
|
|
1202
|
+
readFileSync as readFileSync5,
|
|
1203
|
+
writeFileSync as writeFileSync3,
|
|
1204
|
+
mkdirSync as mkdirSync3,
|
|
1205
|
+
existsSync as existsSync2,
|
|
1206
|
+
renameSync as renameSync2,
|
|
1207
|
+
rmSync
|
|
1208
|
+
} from "node:fs";
|
|
1209
|
+
import { basename, join as join6, isAbsolute as isAbsolute2 } from "node:path";
|
|
1210
|
+
|
|
1179
1211
|
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
1180
|
-
|
|
1181
|
-
import {
|
|
1182
|
-
import {
|
|
1212
|
+
init_heyspex_home();
|
|
1213
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
1214
|
+
import { homedir as homedir6, tmpdir } from "node:os";
|
|
1215
|
+
import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
|
|
1183
1216
|
|
|
1184
1217
|
// vendor/heyspex-mcp/console-fetch.mjs
|
|
1185
1218
|
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
@@ -1188,9 +1221,18 @@ var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMAT
|
|
|
1188
1221
|
init_credentials();
|
|
1189
1222
|
|
|
1190
1223
|
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
1191
|
-
import { existsSync as
|
|
1192
|
-
import {
|
|
1193
|
-
|
|
1224
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1225
|
+
import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
|
|
1226
|
+
|
|
1227
|
+
// spex-console/src/lib/heyspex-home.ts
|
|
1228
|
+
import { homedir as homedir8 } from "node:os";
|
|
1229
|
+
function heyspexHome2(env = process.env) {
|
|
1230
|
+
const override = env?.HEYSPEX_HOME;
|
|
1231
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
1232
|
+
return override.trim();
|
|
1233
|
+
}
|
|
1234
|
+
return homedir8();
|
|
1235
|
+
}
|
|
1194
1236
|
|
|
1195
1237
|
// spex-console/src/lib/vault-registry/types.ts
|
|
1196
1238
|
var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
|
|
@@ -1207,10 +1249,10 @@ function emptyVaultRegistry() {
|
|
|
1207
1249
|
|
|
1208
1250
|
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
1209
1251
|
function vaultRegistryDir(opts = {}) {
|
|
1210
|
-
return opts.registryDir ??
|
|
1252
|
+
return opts.registryDir ?? join9(heyspexHome2(), ".heyspex");
|
|
1211
1253
|
}
|
|
1212
1254
|
function vaultRegistryFilePath(opts = {}) {
|
|
1213
|
-
return
|
|
1255
|
+
return join9(vaultRegistryDir(opts), "registry.json");
|
|
1214
1256
|
}
|
|
1215
1257
|
function isNonEmptyString(v) {
|
|
1216
1258
|
return typeof v === "string" && v.trim().length > 0;
|
|
@@ -1311,9 +1353,9 @@ function parseVaultRegistryFile(json) {
|
|
|
1311
1353
|
}
|
|
1312
1354
|
function readRegistryJson(opts) {
|
|
1313
1355
|
const filePath = vaultRegistryFilePath(opts);
|
|
1314
|
-
if (!
|
|
1356
|
+
if (!existsSync4(filePath)) return { kind: "missing" };
|
|
1315
1357
|
try {
|
|
1316
|
-
const raw =
|
|
1358
|
+
const raw = readFileSync8(filePath, "utf8");
|
|
1317
1359
|
const json = JSON.parse(raw);
|
|
1318
1360
|
if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
|
|
1319
1361
|
return { kind: "ok", raw, json };
|
|
@@ -1374,7 +1416,7 @@ function samePathLoose(a, b) {
|
|
|
1374
1416
|
}
|
|
1375
1417
|
function assertNotRealHomeRegistryUnderTest(filePath) {
|
|
1376
1418
|
if (!underTestRunner()) return;
|
|
1377
|
-
if (samePathLoose(dirname4(filePath),
|
|
1419
|
+
if (samePathLoose(dirname4(filePath), join9(heyspexHome2(), ".heyspex"))) {
|
|
1378
1420
|
throw new Error(
|
|
1379
1421
|
`registry-file: refusing to write ${filePath} under a test runner \u2014 that is the real ~/.heyspex registry. Pass opts.registryDir (a temp dir).`
|
|
1380
1422
|
);
|
|
@@ -1383,10 +1425,10 @@ function assertNotRealHomeRegistryUnderTest(filePath) {
|
|
|
1383
1425
|
function writeVaultRegistry(registry, opts = {}) {
|
|
1384
1426
|
const filePath = vaultRegistryFilePath(opts);
|
|
1385
1427
|
assertNotRealHomeRegistryUnderTest(filePath);
|
|
1386
|
-
|
|
1428
|
+
mkdirSync4(dirname4(filePath), { recursive: true });
|
|
1387
1429
|
const tmpPath = `${filePath}.tmp`;
|
|
1388
|
-
|
|
1389
|
-
|
|
1430
|
+
writeFileSync4(tmpPath, serializeVaultRegistry(registry), "utf8");
|
|
1431
|
+
renameSync3(tmpPath, filePath);
|
|
1390
1432
|
}
|
|
1391
1433
|
function isValidId(id) {
|
|
1392
1434
|
if (!isNonEmptyString(id)) return false;
|
|
@@ -1585,7 +1627,7 @@ function withCloudScope(deps, accountId, vaultId) {
|
|
|
1585
1627
|
}
|
|
1586
1628
|
|
|
1587
1629
|
// spex-console/src/lib/cli/install.ts
|
|
1588
|
-
import {
|
|
1630
|
+
import { isAbsolute as isAbsolute3, resolve as resolveNodePath2 } from "node:path";
|
|
1589
1631
|
|
|
1590
1632
|
// scripts/install-cursor-hooks.mjs
|
|
1591
1633
|
import fs2 from "node:fs";
|
|
@@ -1602,10 +1644,13 @@ var HOOKS = [
|
|
|
1602
1644
|
{ event: "sessionStart", hook: "wip-checkpoint", why: "Surface the previous session WIP handoff before work resumes." },
|
|
1603
1645
|
{ event: "sessionStart", hook: "inject-dev-server-awareness", why: "Once-per-session reminder that dev-server status is a tool call, not a guess." },
|
|
1604
1646
|
{ event: "sessionStart", hook: "inject-coo-seat", why: "Inject the dev-factory COO seat mandate when the COO window is open." },
|
|
1647
|
+
{ event: "sessionStart", hook: "inject-doctrine", why: "Inject the baseline SpexCode Doctrine edition at session start." },
|
|
1605
1648
|
{ event: "sessionStart", hook: "coo-guard-check", why: "Warn at session start when a breakdown has not been delegated." },
|
|
1606
1649
|
{ event: "sessionStart", hook: "check-auto-memory-budget", why: "Self-heal and budget-check the auto-memory index." },
|
|
1607
1650
|
{ event: "sessionStart", hook: "check-claude-md-budget", why: "Advisory budget check on the always-loaded agent contract." },
|
|
1608
1651
|
{ event: "sessionStart", hook: "check-mcp-freshness-hook", why: "Warn when the running MCP server is stale versus vendor." },
|
|
1652
|
+
{ event: "sessionStart", hook: "git-reflog-audit", why: "Session-start git audit (stash/reset/rebase witness)." },
|
|
1653
|
+
{ event: "sessionStart", hook: "vault-mirror-catchup", why: "Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ)." },
|
|
1609
1654
|
// ── Compaction ──────────────────────────────────────────────────────────
|
|
1610
1655
|
// Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
|
|
1611
1656
|
// preCompact BEFORE it, so the shim relays this one as SessionStart(compact)
|
|
@@ -1619,6 +1664,7 @@ var HOOKS = [
|
|
|
1619
1664
|
{ event: "beforeSubmitPrompt", hook: "preflight-reminder", why: "Name the required procedure before a filing/destructive/protocol turn." },
|
|
1620
1665
|
{ event: "beforeSubmitPrompt", hook: "intent-classifier-trigger", why: "Route capture / research / settings / defer intents to the right skill." },
|
|
1621
1666
|
{ event: "beforeSubmitPrompt", hook: "inject-communication-style", why: "Re-assert the plain-language communication rules each turn." },
|
|
1667
|
+
{ event: "beforeSubmitPrompt", hook: "inject-vault-context", timeout: 10, why: "Lexical vault retrieval for the prompt (surfaced as a user message)." },
|
|
1622
1668
|
// ── Before every tool call (any tool) ───────────────────────────────────
|
|
1623
1669
|
{ event: "preToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], why: "Claim liveness heartbeat to the console." },
|
|
1624
1670
|
{ event: "preToolUse", matcher: ".*", hook: "coo-denylist-gate", why: "Hard deny the COO-window deny list." },
|
|
@@ -1629,21 +1675,27 @@ var HOOKS = [
|
|
|
1629
1675
|
// the vault guard because removing a profile file is the loss it exists to
|
|
1630
1676
|
// prevent, and Claude had no separate delete tool to list.
|
|
1631
1677
|
{ event: "preToolUse", matcher: "Write|Delete", hook: "env-vault-guard", why: "Block destructive writes to the encrypted env vault (ALWAYS ON)." },
|
|
1678
|
+
{ event: "preToolUse", matcher: "Write", hook: "vault-write-tier-guard", why: "Deny disk writes to vault specs in cloud write mode (HS-FCNH)." },
|
|
1632
1679
|
{ event: "preToolUse", matcher: "Write", hook: "settings-write-gate", why: "Require approval before .heyspex/config.yaml changes." },
|
|
1633
1680
|
{ event: "preToolUse", matcher: "Write", hook: "enforce-vault-naming", why: "Block vault filenames that violate the naming registry." },
|
|
1634
1681
|
{ event: "preToolUse", matcher: "Write", hook: "guard-auto-memory-writes", why: "Block new topic files in the auto-memory directory." },
|
|
1635
1682
|
{ event: "preToolUse", matcher: "Write", hook: "preflight-reminder", why: "Preflight reminder on the edit path itself." },
|
|
1636
1683
|
{ event: "preToolUse", matcher: "Write", hook: "claim-before-code-gate", why: "Deny the first code edit after plan exit with no claim." },
|
|
1684
|
+
{ 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)." },
|
|
1637
1685
|
// ── Before a shell command ──────────────────────────────────────────────
|
|
1638
1686
|
// beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
|
|
1639
1687
|
// to carry the command string, and Cursor honors allow/deny/ask on it.
|
|
1640
1688
|
{ event: "beforeShellExecution", hook: "env-vault-guard", why: "Block shell writes/exfiltration targeting the env vault (ALWAYS ON)." },
|
|
1689
|
+
{ event: "beforeShellExecution", hook: "vault-write-tier-guard", why: "Deny shell writes to vault specs in cloud write mode (HS-FCNH)." },
|
|
1641
1690
|
{ event: "beforeShellExecution", hook: "settings-write-gate", why: "Catch shell writes to .heyspex/config.yaml." },
|
|
1642
1691
|
{ event: "beforeShellExecution", hook: "enforce-vault-naming", why: "Catch vault files created via the shell." },
|
|
1643
1692
|
{ event: "beforeShellExecution", hook: "guard-auto-memory-writes", why: "Catch auto-memory topic files created via the shell." },
|
|
1644
1693
|
{ event: "beforeShellExecution", hook: "preflight-reminder", why: "Preflight reminder on rm -rf / force-push shaped commands." },
|
|
1645
1694
|
{ event: "beforeShellExecution", hook: "dev-server-task-reminder", timeout: 10, why: "Dev-server start/stop hard gate plus DB-safety reminder." },
|
|
1646
1695
|
{ event: "beforeShellExecution", hook: "git-stash-gate", timeout: 10, why: "Always ask before a mutating git stash." },
|
|
1696
|
+
{ event: "beforeShellExecution", hook: "git-identity-guard", why: "Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON)." },
|
|
1697
|
+
{ event: "beforeShellExecution", hook: "capture-execution-gate", why: "Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform)." },
|
|
1698
|
+
{ event: "beforeShellExecution", hook: "directive-obligation-gate", why: "Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW)." },
|
|
1647
1699
|
// ── Before specific MCP tools ───────────────────────────────────────────
|
|
1648
1700
|
// Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
|
|
1649
1701
|
// stay on preToolUse rather than beforeMCPExecution — registering both would
|
|
@@ -1653,15 +1705,20 @@ var HOOKS = [
|
|
|
1653
1705
|
{ 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." },
|
|
1654
1706
|
{ event: "preToolUse", matcher: ".*", tool: "heyspex_settings_set$", hook: "settings-write-gate", why: "Require approval for settings changes made through MCP." },
|
|
1655
1707
|
{ 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." },
|
|
1708
|
+
{ 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)." },
|
|
1709
|
+
{ event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "research-method-guard", why: "Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON)." },
|
|
1656
1710
|
{ event: "preToolUse", matcher: ".*", tool: "^(Workflow|Skill)$", hook: "deep-research-cost-gate", why: "Cost confirmation before a paid deep-research harness launch." },
|
|
1657
1711
|
{ 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)." },
|
|
1658
1712
|
// ── Before a subagent spawn ─────────────────────────────────────────────
|
|
1659
1713
|
// subagentStart over preToolUse/Task: it carries subagent_type and the model,
|
|
1660
1714
|
// which is exactly what the routing gate decides on, and it can deny.
|
|
1661
1715
|
{ event: "subagentStart", hook: "agent-spawn-delegation-gate", why: "Block a build/breakdown/critic subagent on an off-routing model." },
|
|
1716
|
+
{ event: "subagentStart", hook: "capture-execution-gate", why: "Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform)." },
|
|
1717
|
+
{ 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)." },
|
|
1662
1718
|
// ── After every tool call ───────────────────────────────────────────────
|
|
1663
1719
|
{ event: "postToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], why: "Claim liveness heartbeat after the call resolves." },
|
|
1664
1720
|
{ event: "postToolUse", matcher: ".*", hook: "log-session-activity", args: ["--event=PostToolUse"], why: "Append the tool result row to the session log." },
|
|
1721
|
+
{ event: "postToolUse", matcher: ".*", hook: "directive-obligation-gate", why: "Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW)." },
|
|
1665
1722
|
{ event: "postToolUse", matcher: ".*", hook: "close-terminal-on-ship", why: "Close the Launch terminal once the spec ships." },
|
|
1666
1723
|
{ event: "postToolUse", matcher: "Write", hook: "notify-status-flip", why: "Sound on an in_review status flip." },
|
|
1667
1724
|
{ event: "postToolUse", matcher: "Write", hook: "scripts/stamp-provenance-on-edit.mjs", why: "Stamp assisted_by provenance on direct spec edits." },
|
|
@@ -1674,7 +1731,11 @@ var HOOKS = [
|
|
|
1674
1731
|
// hook re-drives the agent — the same effect as Claude's exit-2 block.
|
|
1675
1732
|
{ event: "stop", hook: "emit-claim-status", args: ["--event=Stop"], why: "Mark the claim pending-on-response." },
|
|
1676
1733
|
{ event: "stop", hook: "naughty-list-gate", why: "Re-drive the turn when the final message uses banned jargon." },
|
|
1734
|
+
{ event: "stop", hook: "id-display-format-gate", why: "Re-drive the turn when a reply breaks the Naming V3 display-id format." },
|
|
1735
|
+
{ event: "stop", hook: "epic-milestone-phrasing-gate", why: "Re-drive the turn when a reply breaks the epic/milestone phrasing rules." },
|
|
1736
|
+
{ event: "stop", hook: "git-reflog-audit", why: "Session-close git audit backstop (stash/reset/rebase witness)." },
|
|
1677
1737
|
{ event: "stop", hook: "enforce-session-sequence", why: "Re-drive the turn when work shipped without record_wip." },
|
|
1738
|
+
{ event: "stop", hook: "directive-obligation-gate", why: "Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW)." },
|
|
1678
1739
|
// ── Session end ─────────────────────────────────────────────────────────
|
|
1679
1740
|
{ event: "sessionEnd", hook: "session-end", why: "Release the claim and notify the console." },
|
|
1680
1741
|
{ event: "sessionEnd", hook: "close-terminal-on-ship", why: "Final terminal cleanup." }
|
|
@@ -1718,7 +1779,134 @@ function timeoutFor(entry) {
|
|
|
1718
1779
|
import { createHash } from "node:crypto";
|
|
1719
1780
|
import fs from "node:fs";
|
|
1720
1781
|
import path2 from "node:path";
|
|
1782
|
+
import os2 from "node:os";
|
|
1721
1783
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
1784
|
+
|
|
1785
|
+
// scripts/lib/hook-dispositions.mjs
|
|
1786
|
+
var HOSTS = Object.freeze(["claude-code", "cursor", "copilot"]);
|
|
1787
|
+
var DISPOSITIONS = Object.freeze(["mapped", "advisory-degraded", "unportable", "not-shipped"]);
|
|
1788
|
+
var mapped = (note) => Object.freeze({ disposition: "mapped", ...note ? { note } : {} });
|
|
1789
|
+
var degraded = (note) => Object.freeze({ disposition: "advisory-degraded", note });
|
|
1790
|
+
var unportable = (control) => Object.freeze({ disposition: "unportable", control });
|
|
1791
|
+
var notShipped = Object.freeze({ disposition: "not-shipped" });
|
|
1792
|
+
var CC = mapped();
|
|
1793
|
+
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.";
|
|
1794
|
+
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.";
|
|
1795
|
+
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";
|
|
1796
|
+
var HOOK_DISPOSITIONS = Object.freeze({
|
|
1797
|
+
// ── customer: session start / compaction ──────────────────────────────────
|
|
1798
|
+
"wip-checkpoint": {
|
|
1799
|
+
"claude-code": CC,
|
|
1800
|
+
cursor: mapped("preCompact is relayed as SessionStart(compact)."),
|
|
1801
|
+
copilot: mapped("sessionStart additionalContext is delivered; the preCompact leg writes WIP only (no output channel), re-injected at the next sessionStart.")
|
|
1802
|
+
},
|
|
1803
|
+
"inject-doctrine": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
1804
|
+
"check-auto-memory-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
1805
|
+
"check-claude-md-budget": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext.") },
|
|
1806
|
+
"git-reflog-audit": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionStart additionalContext and agentStop.") },
|
|
1807
|
+
"vault-mirror-catchup": { "claude-code": CC, cursor: mapped(), copilot: mapped("Detached side effect on sessionStart.") },
|
|
1808
|
+
// ── customer: every user turn ─────────────────────────────────────────────
|
|
1809
|
+
"preflight-reminder": {
|
|
1810
|
+
"claude-code": CC,
|
|
1811
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
1812
|
+
copilot: degraded(`${COPILOT_PROMPT_DROPPED} Real enforcement is the MCP preflight refusal.`)
|
|
1813
|
+
},
|
|
1814
|
+
"intent-classifier-trigger": {
|
|
1815
|
+
"claude-code": CC,
|
|
1816
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
1817
|
+
copilot: degraded(
|
|
1818
|
+
`${COPILOT_PROMPT_DROPPED} The hook still runs and arms obligation state; the HS-20SW directive-obligation-gate turns that state into a hard deny.`
|
|
1819
|
+
)
|
|
1820
|
+
},
|
|
1821
|
+
"inject-communication-style": {
|
|
1822
|
+
"claude-code": CC,
|
|
1823
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
1824
|
+
copilot: degraded(COPILOT_PROMPT_DROPPED)
|
|
1825
|
+
},
|
|
1826
|
+
"inject-vault-context": {
|
|
1827
|
+
"claude-code": CC,
|
|
1828
|
+
cursor: degraded(CURSOR_PROMPT_USER_MESSAGE),
|
|
1829
|
+
copilot: degraded(`${COPILOT_PROMPT_DROPPED} Per-prompt vault retrieval has no Copilot channel at all.`)
|
|
1830
|
+
},
|
|
1831
|
+
// ── customer: before a tool call (hard gates) ─────────────────────────────
|
|
1832
|
+
"git-identity-guard": { "claude-code": CC, cursor: mapped("beforeShellExecution. Always on."), copilot: mapped("preToolUse on bash/powershell. Always on.") },
|
|
1833
|
+
"vault-write-tier-guard": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell.") },
|
|
1834
|
+
"settings-write-gate": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/edit/bash/powershell and the MCP settings tool.") },
|
|
1835
|
+
"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.") },
|
|
1836
|
+
"enforce-vault-naming": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
|
|
1837
|
+
"guard-auto-memory-writes": { "claude-code": CC, cursor: mapped(), copilot: mapped("preToolUse on create/bash/powershell.") },
|
|
1838
|
+
"directive-obligation-gate": {
|
|
1839
|
+
"claude-code": CC,
|
|
1840
|
+
cursor: mapped(),
|
|
1841
|
+
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.")
|
|
1842
|
+
},
|
|
1843
|
+
"claim-before-code-gate": {
|
|
1844
|
+
"claude-code": CC,
|
|
1845
|
+
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."),
|
|
1846
|
+
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.")
|
|
1847
|
+
},
|
|
1848
|
+
"capture-workflow-gate": {
|
|
1849
|
+
"claude-code": CC,
|
|
1850
|
+
cursor: mapped(),
|
|
1851
|
+
copilot: mapped("preToolUse on the MCP heyspex_file_spec tool; MCP tool naming in hook payloads is unverified live (matched by suffix).")
|
|
1852
|
+
},
|
|
1853
|
+
"diagnose-capture-reminder": {
|
|
1854
|
+
"claude-code": CC,
|
|
1855
|
+
cursor: mapped("preToolUse agent_message delivers the reminder."),
|
|
1856
|
+
copilot: degraded("Copilot preToolUse has no context channel for an inject, so this advisory is silent (lost).")
|
|
1857
|
+
},
|
|
1858
|
+
"git-stash-gate": {
|
|
1859
|
+
"claude-code": CC,
|
|
1860
|
+
cursor: mapped(),
|
|
1861
|
+
copilot: mapped('The Claude "ask" becomes a deny where no user is present (cloud agent, CLI autopilot).')
|
|
1862
|
+
},
|
|
1863
|
+
"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.") },
|
|
1864
|
+
"research-method-guard": {
|
|
1865
|
+
"claude-code": CC,
|
|
1866
|
+
cursor: mapped("preToolUse with a Skill|Workflow tool filter. Always on."),
|
|
1867
|
+
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.")
|
|
1868
|
+
},
|
|
1869
|
+
"deep-research-cost-gate": {
|
|
1870
|
+
"claude-code": CC,
|
|
1871
|
+
cursor: mapped("preToolUse with a Workflow|Skill tool filter."),
|
|
1872
|
+
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.")
|
|
1873
|
+
},
|
|
1874
|
+
"plan-review-gate": {
|
|
1875
|
+
"claude-code": CC,
|
|
1876
|
+
cursor: unportable(".cursor/rules/plan-review-required.mdc (always-on rule)"),
|
|
1877
|
+
copilot: unportable(COPILOT_NO_PLAN_TOOL)
|
|
1878
|
+
},
|
|
1879
|
+
"plan-exit-ingest-gate": {
|
|
1880
|
+
"claude-code": CC,
|
|
1881
|
+
cursor: unportable(".cursor/rules/plan-review-required.mdc + spec-creation-linkage.mdc"),
|
|
1882
|
+
copilot: unportable(COPILOT_NO_PLAN_TOOL)
|
|
1883
|
+
},
|
|
1884
|
+
// ── customer: end of turn / session ───────────────────────────────────────
|
|
1885
|
+
"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.') },
|
|
1886
|
+
"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).") },
|
|
1887
|
+
"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).") },
|
|
1888
|
+
"enforce-session-sequence": { "claude-code": CC, cursor: mapped("stop followup_message re-drive."), copilot: mapped("agentStop re-drive, capped at 8 consecutive blocks.") },
|
|
1889
|
+
"notify-status-flip": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
|
|
1890
|
+
"close-terminal-on-ship": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse / sessionEnd.") },
|
|
1891
|
+
"scripts/stamp-provenance-on-edit.mjs": { "claude-code": CC, cursor: mapped(), copilot: mapped("Side effect on postToolUse.") },
|
|
1892
|
+
"session-end": { "claude-code": CC, cursor: mapped(), copilot: mapped("sessionEnd.") },
|
|
1893
|
+
// ── platform: heyspex-app development machinery (never installed in a customer repo) ──
|
|
1894
|
+
"inject-dev-server-awareness": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
1895
|
+
"inject-coo-seat": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
1896
|
+
"coo-guard-check": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
1897
|
+
"coo-denylist-gate": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
1898
|
+
"agent-spawn-delegation-gate": { "claude-code": CC, cursor: mapped("subagentStart."), copilot: notShipped },
|
|
1899
|
+
"capture-execution-gate": { "claude-code": CC, cursor: mapped("beforeShellExecution + subagentStart."), copilot: notShipped },
|
|
1900
|
+
"check-mcp-freshness-hook": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
1901
|
+
"dev-server-task-reminder": { "claude-code": CC, cursor: mapped(), copilot: notShipped },
|
|
1902
|
+
"env-vault-guard": { "claude-code": CC, cursor: mapped("Always on."), copilot: notShipped },
|
|
1903
|
+
"log-session-activity": { "claude-code": CC, cursor: mapped(), copilot: notShipped }
|
|
1904
|
+
});
|
|
1905
|
+
function dispositionFor(hook, host) {
|
|
1906
|
+
return HOOK_DISPOSITIONS[hook]?.[host] ?? null;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// scripts/lib/customer-kit.mjs
|
|
1722
1910
|
var HOOK_SCOPES = Object.freeze({
|
|
1723
1911
|
// ── customer: vault / workflow / safety gates ─────────────────────────────
|
|
1724
1912
|
"wip-checkpoint": { scope: "customer", why: "Previous-session WIP handoff, read from the active vault." },
|
|
@@ -1734,6 +1922,7 @@ var HOOK_SCOPES = Object.freeze({
|
|
|
1734
1922
|
"inject-communication-style": { scope: "customer", why: "Plain-language communication rules each turn." },
|
|
1735
1923
|
"inject-vault-context": { scope: "customer", why: "Lexical vault retrieval for the prompt." },
|
|
1736
1924
|
"git-identity-guard": { scope: "customer", why: "Agents never write git identity (owner ruling 2026-09-19)." },
|
|
1925
|
+
"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)." },
|
|
1737
1926
|
"settings-write-gate": { scope: "customer", why: "Approval before the vault's .heyspex/config.yaml changes." },
|
|
1738
1927
|
"emit-claim-status": { scope: "customer", why: "Claim liveness heartbeat to the console." },
|
|
1739
1928
|
"enforce-vault-naming": { scope: "customer", why: "Naming V3 filename guard (HS-RY2N)." },
|
|
@@ -1741,7 +1930,10 @@ var HOOK_SCOPES = Object.freeze({
|
|
|
1741
1930
|
"plan-review-gate": { scope: "customer", why: "Plan review + QC markers before ExitPlanMode." },
|
|
1742
1931
|
"plan-exit-ingest-gate": { scope: "customer", why: "Plan ingest on ExitPlanMode; arms claim-before-code." },
|
|
1743
1932
|
"claim-before-code-gate": { scope: "customer", why: "First code edit after plan exit needs a claim." },
|
|
1933
|
+
"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)." },
|
|
1744
1934
|
"capture-workflow-gate": { scope: "customer", why: "No implementation spec straight out of an open capture." },
|
|
1935
|
+
"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)." },
|
|
1936
|
+
"diagnose-capture-reminder": { scope: "customer", why: "Warn when a diagnose is filed with no capture holding the owner's words (HS-IK2D)." },
|
|
1745
1937
|
"git-stash-gate": { scope: "customer", why: "Always ask before a mutating git stash." },
|
|
1746
1938
|
"research-method-guard": { scope: "customer", why: "Research method guard (HS-EN4F)." },
|
|
1747
1939
|
"deep-research-cost-gate": { scope: "customer", why: "Cost confirmation before a paid deep-research launch." },
|
|
@@ -1760,6 +1952,7 @@ var HOOK_SCOPES = Object.freeze({
|
|
|
1760
1952
|
"coo-guard-check": { scope: "platform", why: "dev-factory breakdown-delegation guard." },
|
|
1761
1953
|
"coo-denylist-gate": { scope: "platform", why: "dev-factory COO window deny list." },
|
|
1762
1954
|
"agent-spawn-delegation-gate": { scope: "platform", why: "dev-factory seat routing for subagents." },
|
|
1955
|
+
"capture-execution-gate": { scope: "platform", why: "Blocks dev-factory research/paid-model/routing surfaces past an ungrounded capture (HS-KNJ8)." },
|
|
1763
1956
|
"check-mcp-freshness-hook": { scope: "platform", why: "Freshness of the vendored MCP build \u2014 a platform-dev concern." },
|
|
1764
1957
|
"dev-server-task-reminder": { scope: "platform", why: "spex-console dev server + Neon DB safety." },
|
|
1765
1958
|
"env-vault-guard": { scope: "platform", why: "Guards heyspex-app's encrypted env vault paths." },
|
|
@@ -1778,6 +1971,7 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
1778
1971
|
{ event: "SessionStart", hook: "check-claude-md-budget", timeout: 5 },
|
|
1779
1972
|
{ event: "SessionStart", hook: "check-mcp-freshness-hook", timeout: 5 },
|
|
1780
1973
|
{ event: "SessionStart", hook: "git-reflog-audit", timeout: 5 },
|
|
1974
|
+
{ event: "SessionStart", hook: "vault-mirror-catchup", timeout: 5 },
|
|
1781
1975
|
{ event: "PreCompact", hook: "wip-checkpoint", timeout: 5 },
|
|
1782
1976
|
{ event: "UserPromptSubmit", hook: "preflight-reminder", timeout: 5 },
|
|
1783
1977
|
{ event: "UserPromptSubmit", hook: "intent-classifier-trigger", timeout: 5 },
|
|
@@ -1785,10 +1979,13 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
1785
1979
|
{ event: "UserPromptSubmit", hook: "inject-vault-context", timeout: 10 },
|
|
1786
1980
|
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "env-vault-guard", timeout: 5 },
|
|
1787
1981
|
{ event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-identity-guard", timeout: 5 },
|
|
1982
|
+
{ event: "PreToolUse", matcher: "Write|Edit|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "vault-write-tier-guard", timeout: 5 },
|
|
1788
1983
|
{ event: "PreToolUse", matcher: "Write|Edit|NotebookEdit|Bash|PowerShell|mcp__heyspex__heyspex_settings_set", hook: "settings-write-gate", timeout: 5 },
|
|
1789
1984
|
{ event: "PreToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PreToolUse"], timeout: 5 },
|
|
1790
1985
|
{ event: "PreToolUse", matcher: ".*", hook: "coo-denylist-gate", timeout: 5 },
|
|
1791
1986
|
{ event: "PreToolUse", matcher: "Agent|Task", hook: "agent-spawn-delegation-gate", timeout: 5 },
|
|
1987
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell|Agent|Task", hook: "capture-execution-gate", timeout: 5 },
|
|
1988
|
+
{ event: "PreToolUse", matcher: "Agent|Task|Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell", hook: "directive-obligation-gate", timeout: 5 },
|
|
1792
1989
|
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_claim_spec", hook: "coo-guard-check", timeout: 5 },
|
|
1793
1990
|
{ event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "enforce-vault-naming", timeout: 5 },
|
|
1794
1991
|
{ event: "PreToolUse", matcher: "Write|NotebookEdit|Bash|PowerShell", hook: "guard-auto-memory-writes", timeout: 5 },
|
|
@@ -1797,6 +1994,7 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
1797
1994
|
{ event: "PreToolUse", matcher: "ExitPlanMode", hook: "plan-exit-ingest-gate", timeout: 5 },
|
|
1798
1995
|
{ event: "PreToolUse", matcher: "Edit|MultiEdit|Write", hook: "claim-before-code-gate", timeout: 5 },
|
|
1799
1996
|
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "capture-workflow-gate", timeout: 8 },
|
|
1997
|
+
{ event: "PreToolUse", matcher: "mcp__heyspex__heyspex_file_spec", hook: "diagnose-capture-reminder", timeout: 5 },
|
|
1800
1998
|
{ 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 },
|
|
1801
1999
|
{ event: "PreToolUse", matcher: "Bash|PowerShell", hook: "git-stash-gate", timeout: 10 },
|
|
1802
2000
|
{ event: "PreToolUse", matcher: "Skill|Workflow", hook: "research-method-guard", timeout: 5 },
|
|
@@ -1809,7 +2007,9 @@ var CLAUDE_HOOK_ROWS = Object.freeze([
|
|
|
1809
2007
|
{ event: "Stop", hook: "epic-milestone-phrasing-gate", timeout: 5 },
|
|
1810
2008
|
{ event: "Stop", hook: "git-reflog-audit", timeout: 5 },
|
|
1811
2009
|
{ event: "Stop", hook: "enforce-session-sequence", timeout: 5 },
|
|
2010
|
+
{ event: "Stop", hook: "directive-obligation-gate", timeout: 5 },
|
|
1812
2011
|
{ event: "PostToolUse", matcher: ".*", hook: "emit-claim-status", args: ["--event=PostToolUse"], timeout: 5 },
|
|
2012
|
+
{ 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 },
|
|
1813
2013
|
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "notify-status-flip", timeout: 5 },
|
|
1814
2014
|
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "close-terminal-on-ship", timeout: 5 },
|
|
1815
2015
|
{ event: "PostToolUse", matcher: "Edit|Write|MultiEdit", hook: "scripts/stamp-provenance-on-edit.mjs", timeout: 5 },
|
|
@@ -1894,9 +2094,22 @@ var CUSTOMER_LAUNCHERS = Object.freeze({
|
|
|
1894
2094
|
});
|
|
1895
2095
|
function customerRunnerPrefix(_platformRoot, surface = "claude") {
|
|
1896
2096
|
const l = CUSTOMER_LAUNCHERS[surface];
|
|
2097
|
+
if (surface === "claude") return CUSTOMER_RUNNER_PREFIX;
|
|
1897
2098
|
return `node "$${l.projectDirEnv}/${l.rel.join("/")}"`;
|
|
1898
2099
|
}
|
|
1899
|
-
var
|
|
2100
|
+
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}}";
|
|
2101
|
+
function buildBootstrap({ plugin = false } = {}) {
|
|
2102
|
+
const walk = BOOTSTRAP_WALK;
|
|
2103
|
+
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)}";
|
|
2104
|
+
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");
|
|
2105
|
+
}
|
|
2106
|
+
var PLATFORM_RUNNER_PREFIX = 'node -e "' + buildBootstrap() + '"';
|
|
2107
|
+
var PLUGIN_RUNNER_PREFIX = 'node -e "' + buildBootstrap({ plugin: true }) + '"';
|
|
2108
|
+
function buildCustomerBootstrap() {
|
|
2109
|
+
const rel = CUSTOMER_LAUNCHERS.claude.rel.map((s) => "'" + s + "'").join(",");
|
|
2110
|
+
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("/");
|
|
2111
|
+
}
|
|
2112
|
+
var CUSTOMER_RUNNER_PREFIX = 'node -e "' + buildCustomerBootstrap() + '"';
|
|
1900
2113
|
var LAUNCHER_TEMPLATE = [
|
|
1901
2114
|
"#!/usr/bin/env node",
|
|
1902
2115
|
"// HeySpex hook launcher -- GENERATED by hs install / hs connect. Commit it with this repo.",
|
|
@@ -1974,7 +2187,12 @@ var LAUNCHER_TEMPLATE = [
|
|
|
1974
2187
|
"// (the Cursor one) runs exactly as if Cursor had invoked it directly.",
|
|
1975
2188
|
"const shim = resolve(root, ...SHIM);",
|
|
1976
2189
|
"process.argv[1] = shim;",
|
|
1977
|
-
"
|
|
2190
|
+
"try {",
|
|
2191
|
+
" await import(pathToFileURL(shim).href);",
|
|
2192
|
+
"} catch (err) {",
|
|
2193
|
+
" process.stderr.write('[heyspex] hook ' + (process.argv[2] ?? '?') + ' skipped: ' + (err && err.message ? err.message : err) + '\\n');",
|
|
2194
|
+
" process.exit(0);",
|
|
2195
|
+
"}",
|
|
1978
2196
|
""
|
|
1979
2197
|
].join("\n");
|
|
1980
2198
|
function renderCustomerLauncher(surface = "claude") {
|
|
@@ -2081,7 +2299,6 @@ function renderPluginHooksDocument() {
|
|
|
2081
2299
|
});
|
|
2082
2300
|
}
|
|
2083
2301
|
return {
|
|
2084
|
-
_comment: "GENERATED from scripts/lib/customer-kit.mjs (customer-scoped rows only) \u2014 do not edit by hand; run `node scripts/lib/customer-kit.mjs --write-plugin-hooks`. Every command goes through bin/run-hook.mjs, which finds the hook runtime (HEYSPEX_PLATFORM_ROOT / HEYSPEX_MCP_ENTRY when set in the machine's own env, then the plugin's own location, node_modules/heyspex, $CLAUDE_PROJECT_DIR) \u2014 never $CLAUDE_PROJECT_DIR/scripts, which only exists in heyspex-app. WARNING: a repo whose .claude/settings.json already carries the hs install kit hooks runs every hook twice if this plugin is also enabled \u2014 use one or the other.",
|
|
2085
2302
|
hooks
|
|
2086
2303
|
};
|
|
2087
2304
|
}
|
|
@@ -2089,6 +2306,75 @@ function renderPluginHooksText() {
|
|
|
2089
2306
|
return `${JSON.stringify(renderPluginHooksDocument(), null, 2)}
|
|
2090
2307
|
`;
|
|
2091
2308
|
}
|
|
2309
|
+
var COPILOT_EVENT_NAMES = Object.freeze({
|
|
2310
|
+
SessionStart: "sessionStart",
|
|
2311
|
+
SessionEnd: "sessionEnd",
|
|
2312
|
+
UserPromptSubmit: "userPromptSubmitted",
|
|
2313
|
+
PreToolUse: "preToolUse",
|
|
2314
|
+
PostToolUse: "postToolUse",
|
|
2315
|
+
Stop: "agentStop",
|
|
2316
|
+
PreCompact: "preCompact"
|
|
2317
|
+
});
|
|
2318
|
+
var GITHUB_HOOKS_STATE_ARMING = Object.freeze(["intent-classifier-trigger"]);
|
|
2319
|
+
var COPILOT_TOOL_NAMES = Object.freeze({
|
|
2320
|
+
Write: "create",
|
|
2321
|
+
Edit: "edit",
|
|
2322
|
+
MultiEdit: "edit",
|
|
2323
|
+
NotebookEdit: "edit",
|
|
2324
|
+
Bash: "bash",
|
|
2325
|
+
PowerShell: "powershell",
|
|
2326
|
+
Agent: "task",
|
|
2327
|
+
Task: "task",
|
|
2328
|
+
WebFetch: "web_fetch",
|
|
2329
|
+
WebSearch: "web_search"
|
|
2330
|
+
});
|
|
2331
|
+
function toCopilotMatcher(matcher) {
|
|
2332
|
+
if (!matcher) return void 0;
|
|
2333
|
+
const out = [];
|
|
2334
|
+
for (const part of matcher.split("|")) {
|
|
2335
|
+
let t = null;
|
|
2336
|
+
if (part === ".*") t = ".*";
|
|
2337
|
+
else if (COPILOT_TOOL_NAMES[part]) t = COPILOT_TOOL_NAMES[part];
|
|
2338
|
+
else if (part.startsWith(MCP_PROJECT_PREFIX)) t = ".*" + part.slice(MCP_PROJECT_PREFIX.length);
|
|
2339
|
+
else if (part.startsWith(MCP_PLUGIN_PREFIX)) t = ".*" + part.slice(MCP_PLUGIN_PREFIX.length);
|
|
2340
|
+
if (t && !out.includes(t)) out.push(t);
|
|
2341
|
+
}
|
|
2342
|
+
if (out.includes(".*")) return ".*";
|
|
2343
|
+
return out.length ? out.join("|") : null;
|
|
2344
|
+
}
|
|
2345
|
+
function githubHookNames() {
|
|
2346
|
+
const names = /* @__PURE__ */ new Set();
|
|
2347
|
+
for (const r of customerHookRows()) {
|
|
2348
|
+
if (dispositionFor(r.hook, "copilot")?.disposition === "mapped" || GITHUB_HOOKS_STATE_ARMING.includes(r.hook)) names.add(r.hook);
|
|
2349
|
+
}
|
|
2350
|
+
return names;
|
|
2351
|
+
}
|
|
2352
|
+
function githubHookCommand(row) {
|
|
2353
|
+
const tail = hookTail(row);
|
|
2354
|
+
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}`;
|
|
2355
|
+
}
|
|
2356
|
+
function renderGithubHooksDocument() {
|
|
2357
|
+
const allowed = githubHookNames();
|
|
2358
|
+
const hooks = {};
|
|
2359
|
+
for (const row of customerHookRows()) {
|
|
2360
|
+
if (!allowed.has(row.hook)) continue;
|
|
2361
|
+
const event = COPILOT_EVENT_NAMES[row.event];
|
|
2362
|
+
if (!event) continue;
|
|
2363
|
+
const matcher = toCopilotMatcher(row.matcher);
|
|
2364
|
+
if (matcher === null) continue;
|
|
2365
|
+
(hooks[event] ??= []).push({
|
|
2366
|
+
type: "command",
|
|
2367
|
+
...matcher ? { matcher } : {},
|
|
2368
|
+
bash: githubHookCommand(row),
|
|
2369
|
+
timeoutSec: row.timeout
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
return { version: 1, hooks };
|
|
2373
|
+
}
|
|
2374
|
+
function renderGithubHooksText() {
|
|
2375
|
+
return `${JSON.stringify(renderGithubHooksDocument(), null, 2)}
|
|
2376
|
+
`;
|
|
2377
|
+
}
|
|
2092
2378
|
var HARNESS_COMMAND_SURFACES = Object.freeze([
|
|
2093
2379
|
{
|
|
2094
2380
|
installedDir: Object.freeze([".claude", "commands"]),
|
|
@@ -2221,6 +2507,22 @@ if (invokedDirectly(import.meta.url, "customer-kit.mjs")) {
|
|
|
2221
2507
|
process.exit(1);
|
|
2222
2508
|
}
|
|
2223
2509
|
console.log("customer-kit: plugins/heyspex/hooks/hooks.json up to date");
|
|
2510
|
+
} else if (process.argv.includes("--write-github-hooks") || process.argv.includes("--check-github-hooks")) {
|
|
2511
|
+
const ghTarget = path2.join(platformRoot, ".github", "hooks", "heyspex.json");
|
|
2512
|
+
const ghWant = renderGithubHooksText();
|
|
2513
|
+
const ghSame = (fsDeps.readFile(ghTarget) ?? "").replace(/\r\n/g, "\n") === ghWant;
|
|
2514
|
+
if (process.argv.includes("--write-github-hooks")) {
|
|
2515
|
+
if (!ghSame) {
|
|
2516
|
+
fs.mkdirSync(path2.dirname(ghTarget), { recursive: true });
|
|
2517
|
+
fs.writeFileSync(ghTarget, ghWant, "utf8");
|
|
2518
|
+
}
|
|
2519
|
+
console.log(`customer-kit: ${ghSame ? "unchanged" : "wrote"} ${path2.relative(platformRoot, ghTarget).replace(/\\/g, "/")}`);
|
|
2520
|
+
} else if (!ghSame) {
|
|
2521
|
+
console.error("customer-kit: .github/hooks/heyspex.json is stale - run `node scripts/lib/customer-kit.mjs --write-github-hooks`");
|
|
2522
|
+
process.exit(1);
|
|
2523
|
+
} else {
|
|
2524
|
+
console.log("customer-kit: .github/hooks/heyspex.json up to date");
|
|
2525
|
+
}
|
|
2224
2526
|
} else {
|
|
2225
2527
|
console.log("usage: node scripts/lib/customer-kit.mjs --write-plugin-hooks | --check-plugin-hooks");
|
|
2226
2528
|
}
|
|
@@ -2541,15 +2843,13 @@ if (invokedDirectly(import.meta.url, "install-cursor-hooks.mjs")) {
|
|
|
2541
2843
|
init_credentials();
|
|
2542
2844
|
|
|
2543
2845
|
// spex-console/src/lib/cli/connect-checks.ts
|
|
2544
|
-
import { homedir as homedir9 } from "node:os";
|
|
2545
2846
|
import { dirname as dirname5 } from "node:path";
|
|
2546
2847
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
2547
2848
|
|
|
2548
2849
|
// spex-console/src/lib/dev-mode.ts
|
|
2850
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2851
|
+
import { join as join10 } from "node:path";
|
|
2549
2852
|
init_credentials();
|
|
2550
|
-
import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2551
|
-
import { homedir as homedir8 } from "node:os";
|
|
2552
|
-
import { join as join9 } from "node:path";
|
|
2553
2853
|
|
|
2554
2854
|
// spex-console/src/lib/cli/connect-checks.ts
|
|
2555
2855
|
init_credentials();
|
|
@@ -2738,7 +3038,7 @@ function runInit(args, deps) {
|
|
|
2738
3038
|
};
|
|
2739
3039
|
}
|
|
2740
3040
|
const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
|
|
2741
|
-
const accountId =
|
|
3041
|
+
const accountId = null;
|
|
2742
3042
|
const content = renderHeyspexYaml({ ...args, vaultId, accountId });
|
|
2743
3043
|
try {
|
|
2744
3044
|
deps.writeFile(markerPath, content);
|
|
@@ -2835,6 +3135,11 @@ function normalizePathForCompare(p) {
|
|
|
2835
3135
|
function samePath(a, b) {
|
|
2836
3136
|
return normalizePathForCompare(a) === normalizePathForCompare(b);
|
|
2837
3137
|
}
|
|
3138
|
+
function isAtOrUnder(child, parent) {
|
|
3139
|
+
const c = normalizePathForCompare(child);
|
|
3140
|
+
const p = normalizePathForCompare(parent);
|
|
3141
|
+
return c === p || c.startsWith(`${p}/`);
|
|
3142
|
+
}
|
|
2838
3143
|
function isAbsolutePathForCompare(p) {
|
|
2839
3144
|
const s = p.trim();
|
|
2840
3145
|
return /^[\\/]/.test(s) || /^[a-zA-Z]:[\\/]/.test(s);
|
|
@@ -2843,6 +3148,62 @@ function defaultResolveCliPath(p) {
|
|
|
2843
3148
|
const base = process2.env.HS_INVOKED_CWD?.trim() || process2.cwd();
|
|
2844
3149
|
return resolveNodePath(base, p);
|
|
2845
3150
|
}
|
|
3151
|
+
function expandPinPlaceholders(value, repoRoot) {
|
|
3152
|
+
return value.replace(/\$\{(?:CLAUDE_PROJECT_DIR|workspaceFolder)(?::-[^}]*)?\}/g, repoRoot);
|
|
3153
|
+
}
|
|
3154
|
+
function lexicalJoin(base, rel) {
|
|
3155
|
+
const parts = `${base}/${rel}`.replace(/\\/g, "/").split("/");
|
|
3156
|
+
const out = [];
|
|
3157
|
+
for (const seg of parts) {
|
|
3158
|
+
if (seg === "" && out.length > 0) continue;
|
|
3159
|
+
if (seg === ".") continue;
|
|
3160
|
+
if (seg === ".." && out.length > 1) {
|
|
3161
|
+
out.pop();
|
|
3162
|
+
continue;
|
|
3163
|
+
}
|
|
3164
|
+
out.push(seg);
|
|
3165
|
+
}
|
|
3166
|
+
return out.join("/");
|
|
3167
|
+
}
|
|
3168
|
+
function parseMarkerVaultPath(content) {
|
|
3169
|
+
const m = content.match(/^vault_path:[ \t]*(.+?)[ \t]*$/m);
|
|
3170
|
+
if (!m) return null;
|
|
3171
|
+
const v = m[1].trim().replace(/\s+#.*$/, "").replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim();
|
|
3172
|
+
if (!v || v.includes("${") || /^[\\/]/.test(v) || /^[a-zA-Z]:[\\/]/.test(v)) return null;
|
|
3173
|
+
return v;
|
|
3174
|
+
}
|
|
3175
|
+
function markerVaultIdOf(content) {
|
|
3176
|
+
const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
|
|
3177
|
+
if (!m) return null;
|
|
3178
|
+
const v = m[1].trim().replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
3179
|
+
return v.length > 0 ? v : null;
|
|
3180
|
+
}
|
|
3181
|
+
function detectMarkerAttachment(repoRoot, deps) {
|
|
3182
|
+
const markerPath = deps.joinPath(repoRoot, "heyspex.yaml");
|
|
3183
|
+
if (!deps.fileExists(markerPath)) return null;
|
|
3184
|
+
const raw = deps.readFile(markerPath);
|
|
3185
|
+
const vaultPath = raw ? parseMarkerVaultPath(raw) : null;
|
|
3186
|
+
if (!raw || !vaultPath) return null;
|
|
3187
|
+
const vaultRootAbs = lexicalJoin(repoRoot, vaultPath);
|
|
3188
|
+
const markerId = markerVaultIdOf(raw);
|
|
3189
|
+
let cloned = deps.fileExists(deps.joinPath(vaultRootAbs, "specs"));
|
|
3190
|
+
if (cloned && markerId) {
|
|
3191
|
+
const theirs = deps.readFile(deps.joinPath(vaultRootAbs, "heyspex.yaml"));
|
|
3192
|
+
const theirId = theirs ? markerVaultIdOf(theirs) : null;
|
|
3193
|
+
if (theirId && theirId !== markerId) cloned = false;
|
|
3194
|
+
}
|
|
3195
|
+
const registry = loadVaultRegistry(deps.registryOpts);
|
|
3196
|
+
const record = registry.vaults.find((v) => samePath(v.repo_root_abs, vaultRootAbs));
|
|
3197
|
+
return {
|
|
3198
|
+
vaultRootAbs,
|
|
3199
|
+
vaultId: markerId ?? record?.id ?? null,
|
|
3200
|
+
pins: [{ source: "heyspex.yaml", value: vaultRootAbs }],
|
|
3201
|
+
conflicting: false,
|
|
3202
|
+
viaMarker: true,
|
|
3203
|
+
cloned,
|
|
3204
|
+
vaultPath
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
2846
3207
|
function readPinnedVaultRoot(repoRoot, deps) {
|
|
2847
3208
|
const pins = [];
|
|
2848
3209
|
for (const rel of SATELLITE_MANIFEST_RELS) {
|
|
@@ -2859,9 +3220,13 @@ function readPinnedVaultRoot(repoRoot, deps) {
|
|
|
2859
3220
|
}
|
|
2860
3221
|
if (typeof value !== "string") continue;
|
|
2861
3222
|
const trimmed = value.trim();
|
|
2862
|
-
if (!trimmed
|
|
2863
|
-
|
|
2864
|
-
|
|
3223
|
+
if (!trimmed) continue;
|
|
3224
|
+
const expanded = expandPinPlaceholders(trimmed, repoRoot);
|
|
3225
|
+
if (expanded.includes("${")) continue;
|
|
3226
|
+
if (/^[a-zA-Z]:(?![\\/])/.test(expanded)) continue;
|
|
3227
|
+
const abs = isAbsolutePathForCompare(expanded) ? expanded.includes("..") ? lexicalJoin(expanded, ".") : expanded : lexicalJoin(repoRoot, expanded);
|
|
3228
|
+
if (expanded !== trimmed && isAtOrUnder(abs, repoRoot)) continue;
|
|
3229
|
+
pins.push({ source: rel.join("/"), value: abs });
|
|
2865
3230
|
}
|
|
2866
3231
|
return pins;
|
|
2867
3232
|
}
|
|
@@ -3040,7 +3405,7 @@ function writeProjectContract(ctx, options2 = {}) {
|
|
|
3040
3405
|
}
|
|
3041
3406
|
|
|
3042
3407
|
// spex-console/src/lib/cli/init.customer-vault.ts
|
|
3043
|
-
import { basename } from "node:path";
|
|
3408
|
+
import { basename as basename2 } from "node:path";
|
|
3044
3409
|
|
|
3045
3410
|
// spex-console/src/lib/cli/template-manifest.sources.mjs
|
|
3046
3411
|
var UNIVERSAL_TEMPLATE_ROWS = [
|
|
@@ -3551,7 +3916,7 @@ function findManagedSpecs(deps, targetDir, manifestDests) {
|
|
|
3551
3916
|
return out;
|
|
3552
3917
|
}
|
|
3553
3918
|
function defaultVaultName(targetDir) {
|
|
3554
|
-
return
|
|
3919
|
+
return basename2(targetDir) || targetDir;
|
|
3555
3920
|
}
|
|
3556
3921
|
function runCustomerVaultInit(args, deps, options2 = {}) {
|
|
3557
3922
|
const { targetDir } = args;
|
|
@@ -3699,14 +4064,14 @@ function templatePinHook() {
|
|
|
3699
4064
|
}
|
|
3700
4065
|
|
|
3701
4066
|
// spex-console/src/lib/cli/principle-selection.ts
|
|
3702
|
-
import { basename as
|
|
4067
|
+
import { basename as basename3 } from "node:path";
|
|
3703
4068
|
var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
|
|
3704
4069
|
function principleKey(row) {
|
|
3705
|
-
const base =
|
|
4070
|
+
const base = basename3(row.dest, ".md");
|
|
3706
4071
|
return base.startsWith("principle-") ? base.slice("principle-".length) : base;
|
|
3707
4072
|
}
|
|
3708
4073
|
function principleWikilink(row) {
|
|
3709
|
-
return
|
|
4074
|
+
return basename3(row.dest, ".md");
|
|
3710
4075
|
}
|
|
3711
4076
|
function parsePrinciplesFlag(value, rows) {
|
|
3712
4077
|
const principleRows = rows.filter((row) => row.group === "principle");
|
|
@@ -4101,8 +4466,7 @@ function formatReceipt(receipt) {
|
|
|
4101
4466
|
}
|
|
4102
4467
|
|
|
4103
4468
|
// spex-console/src/lib/cli/vault-workspace-shared.ts
|
|
4104
|
-
import { readFileSync as
|
|
4105
|
-
import os2 from "node:os";
|
|
4469
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
4106
4470
|
var VAULT_OWNED_BY_OTHER_ACCOUNT = "vault_owned_by_other_account";
|
|
4107
4471
|
var WORKSPACE_OWNED_BY_OTHER_ACCOUNT = "workspace_owned_by_other_account";
|
|
4108
4472
|
function vaultOwnedByOtherAccountMessage(id) {
|
|
@@ -4981,6 +5345,9 @@ function parseInstallArgs(argv) {
|
|
|
4981
5345
|
let plugin = false;
|
|
4982
5346
|
let pluginApply = false;
|
|
4983
5347
|
let pluginScope = "project";
|
|
5348
|
+
let copilotHooks = false;
|
|
5349
|
+
let copilotPlugin = false;
|
|
5350
|
+
let copilotPluginApply = false;
|
|
4984
5351
|
let statusline = true;
|
|
4985
5352
|
let commandCode = false;
|
|
4986
5353
|
let force = false;
|
|
@@ -5040,6 +5407,11 @@ function parseInstallArgs(argv) {
|
|
|
5040
5407
|
}
|
|
5041
5408
|
pluginScope = v.trim();
|
|
5042
5409
|
plugin = true;
|
|
5410
|
+
} else if (arg === "--copilot-hooks") copilotHooks = true;
|
|
5411
|
+
else if (arg === "--copilot-plugin") copilotPlugin = true;
|
|
5412
|
+
else if (arg === "--copilot-plugin-apply") {
|
|
5413
|
+
copilotPlugin = true;
|
|
5414
|
+
copilotPluginApply = true;
|
|
5043
5415
|
} else if (arg === "--no-statusline") statusline = false;
|
|
5044
5416
|
else if (arg === "--cc" || arg === "--command-code") commandCode = true;
|
|
5045
5417
|
else if (arg === "--cursor-only") {
|
|
@@ -5087,6 +5459,9 @@ function parseInstallArgs(argv) {
|
|
|
5087
5459
|
plugin,
|
|
5088
5460
|
pluginApply,
|
|
5089
5461
|
pluginScope,
|
|
5462
|
+
copilotHooks,
|
|
5463
|
+
copilotPlugin,
|
|
5464
|
+
copilotPluginApply,
|
|
5090
5465
|
statusline,
|
|
5091
5466
|
commandCode,
|
|
5092
5467
|
force,
|
|
@@ -5104,8 +5479,15 @@ function parseInstallArgs(argv) {
|
|
|
5104
5479
|
function mcpCliPath(platformRoot, joinPath) {
|
|
5105
5480
|
return joinPath(platformRoot, ...MCP_REL);
|
|
5106
5481
|
}
|
|
5482
|
+
function resolveCallerPath(p, env, fallbackCwd) {
|
|
5483
|
+
const trimmed = p?.trim() ?? "";
|
|
5484
|
+
if (!trimmed) return "";
|
|
5485
|
+
if (isAbsolute3(trimmed)) return trimmed;
|
|
5486
|
+
const base = env.HS_INVOKED_CWD?.trim() || fallbackCwd;
|
|
5487
|
+
return resolveNodePath2(base, trimmed);
|
|
5488
|
+
}
|
|
5107
5489
|
function resolveInstallTargetDir(argsTargetDir, env, fallbackCwd, opts) {
|
|
5108
|
-
const explicit = argsTargetDir
|
|
5490
|
+
const explicit = resolveCallerPath(argsTargetDir, env, fallbackCwd);
|
|
5109
5491
|
if (opts?.satellite) {
|
|
5110
5492
|
return explicit || env.HS_INVOKED_CWD?.trim() || fallbackCwd;
|
|
5111
5493
|
}
|
|
@@ -5176,7 +5558,7 @@ function mergeAttachedRemoteMcpJson(existingJson, pinEntry, cloudEntry, preserve
|
|
|
5176
5558
|
}
|
|
5177
5559
|
function resolveInstallBearer(deps) {
|
|
5178
5560
|
const env = deps.env ?? process.env;
|
|
5179
|
-
const homeDir = deps.homeDir ??
|
|
5561
|
+
const homeDir = deps.homeDir ?? heyspexHome2();
|
|
5180
5562
|
const { source } = resolveBearer({
|
|
5181
5563
|
envToken: env[REMOTE_MCP_TOKEN_ENV],
|
|
5182
5564
|
credentialsText: deps.readFile(deps.joinPath(homeDir, ".heyspex", "credentials.yaml"))
|
|
@@ -5283,6 +5665,16 @@ function buildClaudePluginInstallCommandPowerShell(targetDir, platformRoot, spec
|
|
|
5283
5665
|
`claude plugin install ${spec} --scope ${scope}`
|
|
5284
5666
|
].join("\n");
|
|
5285
5667
|
}
|
|
5668
|
+
var COPILOT_HOOKS_REL = [".github", "hooks", "heyspex.json"];
|
|
5669
|
+
function buildCopilotPluginInstallCommand(targetDir, platformRoot, spec = PLUGIN_INSTALL_SPEC) {
|
|
5670
|
+
const escapedTarget = targetDir.replace(/'/g, "''");
|
|
5671
|
+
const escapedPlatform = platformRoot.replace(/'/g, "''");
|
|
5672
|
+
return [
|
|
5673
|
+
`Set-Location -LiteralPath '${escapedTarget}'`,
|
|
5674
|
+
`copilot plugin marketplace add '${escapedPlatform}'`,
|
|
5675
|
+
`copilot plugin install ${spec}`
|
|
5676
|
+
].join("\n");
|
|
5677
|
+
}
|
|
5286
5678
|
var STATUSLINE_VENDOR_FILES = [
|
|
5287
5679
|
["scripts", "install-statusline.mjs"],
|
|
5288
5680
|
["scripts", "claude-statusline", "statusline-command.mjs"],
|
|
@@ -5647,11 +6039,11 @@ function slugFromPath(p) {
|
|
|
5647
6039
|
return pathTail(p).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5648
6040
|
}
|
|
5649
6041
|
function registerInstalledVault(args, ctx) {
|
|
5650
|
-
const { targetDir, readFile, joinPath, registryOpts, now, basename:
|
|
6042
|
+
const { targetDir, readFile, joinPath, registryOpts, now, basename: basename4 } = ctx;
|
|
5651
6043
|
let vaultId;
|
|
5652
6044
|
let schemaVersion = HEYSPEX_YAML_SCHEMA_VERSION;
|
|
5653
6045
|
const yamlContent = readFile(joinPath(targetDir, "heyspex.yaml"));
|
|
5654
|
-
const markerAccountId =
|
|
6046
|
+
const markerAccountId = null;
|
|
5655
6047
|
if (yamlContent) {
|
|
5656
6048
|
const existing = parseVaultId(yamlContent);
|
|
5657
6049
|
if (existing) vaultId = existing;
|
|
@@ -5659,7 +6051,7 @@ function registerInstalledVault(args, ctx) {
|
|
|
5659
6051
|
if (versionMatch) schemaVersion = Number(versionMatch[1]);
|
|
5660
6052
|
}
|
|
5661
6053
|
if (!vaultId) vaultId = slugFromPath(targetDir);
|
|
5662
|
-
const displayName =
|
|
6054
|
+
const displayName = basename4(targetDir);
|
|
5663
6055
|
const before = loadVaultRegistry(registryOpts);
|
|
5664
6056
|
const existingVault = before.vaults.find(
|
|
5665
6057
|
(v) => v.id === vaultId || v.repo_root_abs === targetDir
|
|
@@ -5747,7 +6139,7 @@ function guardWritesAgainstLiteralBearer(writeFile, bearerToken) {
|
|
|
5747
6139
|
};
|
|
5748
6140
|
}
|
|
5749
6141
|
function runInstall(args, inputDeps) {
|
|
5750
|
-
const guardHome = inputDeps.homeDir ? inputDeps.homeDir() :
|
|
6142
|
+
const guardHome = inputDeps.homeDir ? inputDeps.homeDir() : heyspexHome2();
|
|
5751
6143
|
const guardBearer = resolveBearer({
|
|
5752
6144
|
envToken: (inputDeps.env ?? process.env)[REMOTE_MCP_TOKEN_ENV],
|
|
5753
6145
|
credentialsText: inputDeps.readFile(
|
|
@@ -5784,16 +6176,27 @@ function runInstall(args, inputDeps) {
|
|
|
5784
6176
|
};
|
|
5785
6177
|
}
|
|
5786
6178
|
let attached;
|
|
6179
|
+
let portableVaultPath = null;
|
|
5787
6180
|
let satellitePins = [];
|
|
5788
6181
|
let satelliteStalePins = [];
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
6182
|
+
const markerSatelliteDeps = {
|
|
6183
|
+
fileExists: deps.fileExists,
|
|
6184
|
+
readFile: deps.readFile,
|
|
6185
|
+
joinPath: deps.joinPath,
|
|
6186
|
+
...deps.registryOpts ? { registryOpts: deps.registryOpts } : {}
|
|
6187
|
+
};
|
|
6188
|
+
const markerAttachment = args.attachVault === void 0 ? detectMarkerAttachment(targetDir, markerSatelliteDeps) : null;
|
|
6189
|
+
if (args.attachVault !== void 0 || markerAttachment && markerAttachment.vaultId) {
|
|
6190
|
+
const satelliteDeps = markerSatelliteDeps;
|
|
6191
|
+
const resolved = args.attachVault === void 0 && markerAttachment ? {
|
|
6192
|
+
ok: true,
|
|
6193
|
+
target: {
|
|
6194
|
+
vaultId: markerAttachment.vaultId ?? "",
|
|
6195
|
+
vaultRootAbs: markerAttachment.vaultRootAbs,
|
|
6196
|
+
workspaceId: "(from heyspex.yaml)",
|
|
6197
|
+
displayName: markerAttachment.vaultId ?? ""
|
|
6198
|
+
}
|
|
6199
|
+
} : resolveAttachTarget(args.attachVault, satelliteDeps);
|
|
5797
6200
|
if (!resolved.ok) {
|
|
5798
6201
|
return {
|
|
5799
6202
|
code: EXIT.INVALID_ARG,
|
|
@@ -5803,6 +6206,12 @@ function runInstall(args, inputDeps) {
|
|
|
5803
6206
|
};
|
|
5804
6207
|
}
|
|
5805
6208
|
attached = resolved.target;
|
|
6209
|
+
{
|
|
6210
|
+
const markerText = deps.readFile(deps.joinPath(targetDir, "heyspex.yaml"));
|
|
6211
|
+
const vp = markerText ? parseMarkerVaultPath(markerText) : null;
|
|
6212
|
+
const markerId = markerText ? parseVaultId(markerText) : null;
|
|
6213
|
+
if (vp && (!markerId || markerId === attached.vaultId)) portableVaultPath = vp;
|
|
6214
|
+
}
|
|
5806
6215
|
if (deps.joinPath(attached.vaultRootAbs) === deps.joinPath(targetDir)) {
|
|
5807
6216
|
const message2 = "--attach-vault: this repo IS that vault -- run a normal `hs install` instead of attaching it to itself.";
|
|
5808
6217
|
return {
|
|
@@ -5993,7 +6402,7 @@ ${detail}
|
|
|
5993
6402
|
if (deps.fileExists(gitkeepPath)) {
|
|
5994
6403
|
staleLocalMarkers.push("specs/.gitkeep");
|
|
5995
6404
|
}
|
|
5996
|
-
if (deps.fileExists(deps.joinPath(targetDir, "heyspex.yaml"))) {
|
|
6405
|
+
if (deps.fileExists(deps.joinPath(targetDir, "heyspex.yaml")) && !portableVaultPath) {
|
|
5997
6406
|
staleLocalMarkers.push("heyspex.yaml");
|
|
5998
6407
|
}
|
|
5999
6408
|
if (staleLocalMarkers.length > 0) {
|
|
@@ -6071,8 +6480,10 @@ ${detail}
|
|
|
6071
6480
|
templateOutcome = outcome;
|
|
6072
6481
|
}
|
|
6073
6482
|
const attachedRoot = attached?.vaultRootAbs;
|
|
6483
|
+
const claudePin = portableVaultPath ? "${CLAUDE_PROJECT_DIR:-.}/" + portableVaultPath : attachedRoot;
|
|
6484
|
+
const cursorPin = portableVaultPath ? "${workspaceFolder}/" + portableVaultPath : attachedRoot;
|
|
6074
6485
|
const satelliteUnchanged = (rel) => pinsUnchanged.find((p) => p.rel === rel) ?? null;
|
|
6075
|
-
const cursorJson = `${JSON.stringify(renderCursorMcpJson(cursorCliArg,
|
|
6486
|
+
const cursorJson = `${JSON.stringify(renderCursorMcpJson(cursorCliArg, cursorPin), null, 2)}
|
|
6076
6487
|
`;
|
|
6077
6488
|
if (args.cursor) {
|
|
6078
6489
|
const cursorUnchanged = satelliteUnchanged(".cursor/mcp.json");
|
|
@@ -6093,7 +6504,7 @@ ${detail}
|
|
|
6093
6504
|
}
|
|
6094
6505
|
const remoteUrl = args.remote ? resolveRemoteMcpUrl(args.remoteUrl) : null;
|
|
6095
6506
|
const claudeJson = remoteUrl ? `${JSON.stringify(renderRemoteClaudeMcpJson(remoteUrl), null, 2)}
|
|
6096
|
-
` : `${JSON.stringify(renderClaudeMcpJson(claudeCliArg,
|
|
6507
|
+
` : `${JSON.stringify(renderClaudeMcpJson(claudeCliArg, claudePin), null, 2)}
|
|
6097
6508
|
`;
|
|
6098
6509
|
const claudePs = remoteUrl ? buildClaudeMcpAddRemoteCommandPowerShell(targetDir, remoteUrl) : buildClaudeMcpAddCommandPowerShell(targetDir, cliPath, attachedRoot);
|
|
6099
6510
|
if (args.claude) {
|
|
@@ -6110,7 +6521,7 @@ ${detail}
|
|
|
6110
6521
|
existingJson = null;
|
|
6111
6522
|
}
|
|
6112
6523
|
}
|
|
6113
|
-
const pinEntry = renderClaudeMcpJson(claudeCliArg,
|
|
6524
|
+
const pinEntry = renderClaudeMcpJson(claudeCliArg, claudePin).mcpServers.heyspex;
|
|
6114
6525
|
const cloudEntry = renderRemoteClaudeMcpJson(remoteUrl).mcpServers.heyspex;
|
|
6115
6526
|
const merged = mergeAttachedRemoteMcpJson(
|
|
6116
6527
|
existingJson,
|
|
@@ -6219,6 +6630,71 @@ ${detail}
|
|
|
6219
6630
|
);
|
|
6220
6631
|
}
|
|
6221
6632
|
}
|
|
6633
|
+
const copilotPluginWanted = args.copilotPlugin === true || args.copilotPluginApply === true;
|
|
6634
|
+
const copilotPluginAvailable = deps.fileExists(marketplaceManifest);
|
|
6635
|
+
let copilotPluginActive = false;
|
|
6636
|
+
if (copilotPluginWanted) {
|
|
6637
|
+
if (!copilotPluginAvailable) {
|
|
6638
|
+
steps.push({
|
|
6639
|
+
kind: "copilot_plugin",
|
|
6640
|
+
path: marketplaceManifest,
|
|
6641
|
+
action: "skipped",
|
|
6642
|
+
detail: "platform root has no .claude-plugin/marketplace.json"
|
|
6643
|
+
});
|
|
6644
|
+
} else {
|
|
6645
|
+
copilotPluginActive = true;
|
|
6646
|
+
steps.push({
|
|
6647
|
+
kind: args.copilotPluginApply ? "copilot_plugin_apply" : "copilot_plugin",
|
|
6648
|
+
path: args.copilotPluginApply ? "copilot plugin install" : pluginSpec.spec,
|
|
6649
|
+
action: args.copilotPluginApply ? args.dryRun ? "would_create" : "created" : "skipped",
|
|
6650
|
+
detail: args.copilotPluginApply ? "needs `copilot` on PATH; see copilot_plugin_commands if apply fails" : "printed below -- run it, or re-run with --copilot-plugin-apply"
|
|
6651
|
+
});
|
|
6652
|
+
if (!args.copilotPluginApply) {
|
|
6653
|
+
nextSteps.push(
|
|
6654
|
+
"GitHub Copilot plugin: run the printed commands, or re-run with --copilot-plugin-apply if `copilot` is on PATH. Then /restart."
|
|
6655
|
+
);
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
if (args.copilotHooks) {
|
|
6660
|
+
const ghPath = deps.joinPath(targetDir, ...COPILOT_HOOKS_REL);
|
|
6661
|
+
if (copilotPluginWanted) {
|
|
6662
|
+
steps.push({
|
|
6663
|
+
kind: "copilot_hooks",
|
|
6664
|
+
path: ghPath,
|
|
6665
|
+
action: "skipped",
|
|
6666
|
+
detail: "WARNING: --copilot-hooks and the Copilot plugin both requested -- the plugin already carries the hooks, so .github/hooks/heyspex.json is not written (they would fire twice)"
|
|
6667
|
+
});
|
|
6668
|
+
nextSteps.push(
|
|
6669
|
+
"Warning: --copilot-hooks skipped because the Copilot plugin is requested (single source: plugin OR .github/hooks, never both)."
|
|
6670
|
+
);
|
|
6671
|
+
} else {
|
|
6672
|
+
const want = renderGithubHooksText();
|
|
6673
|
+
const existing = deps.fileExists(ghPath) ? deps.readFile(ghPath) : null;
|
|
6674
|
+
if (existing !== null && existing.replace(/\r\n/g, "\n") === want) {
|
|
6675
|
+
steps.push({
|
|
6676
|
+
kind: "copilot_hooks",
|
|
6677
|
+
path: ghPath,
|
|
6678
|
+
action: "skipped",
|
|
6679
|
+
detail: "already current"
|
|
6680
|
+
});
|
|
6681
|
+
} else {
|
|
6682
|
+
steps.push({
|
|
6683
|
+
kind: "copilot_hooks",
|
|
6684
|
+
path: ghPath,
|
|
6685
|
+
action: args.dryRun ? "would_create" : "created",
|
|
6686
|
+
detail: `${existing === null ? "new" : "updated"} -- commands resolve ${PORTABLE_PACKAGE_DIR}/scripts/claude-hooks/_run.mjs first, then ./scripts/claude-hooks/_run.mjs, else exit 0`
|
|
6687
|
+
});
|
|
6688
|
+
if (!args.dryRun) {
|
|
6689
|
+
deps.mkdir(deps.joinPath(targetDir, ...COPILOT_HOOKS_REL.slice(0, -1)));
|
|
6690
|
+
deps.writeFile(ghPath, want);
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
nextSteps.push(
|
|
6694
|
+
"GitHub Copilot hooks: commit .github/hooks/heyspex.json so cloud agents and teammates get it."
|
|
6695
|
+
);
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6222
6698
|
let settingsWrittenText = null;
|
|
6223
6699
|
if ((args.statusline ?? true) && args.claude) {
|
|
6224
6700
|
const statuslineShipsInPlatform = deps.fileExists(
|
|
@@ -6455,6 +6931,8 @@ ${detail}
|
|
|
6455
6931
|
claude_plugin_powershell: args.plugin && pluginAvailable ? pluginPs : void 0,
|
|
6456
6932
|
claude_plugin_spec: args.plugin && pluginAvailable ? pluginSpec.spec : void 0,
|
|
6457
6933
|
claude_plugin_scope: args.plugin && pluginAvailable ? pluginScope : void 0,
|
|
6934
|
+
copilot_plugin_commands: copilotPluginActive ? buildCopilotPluginInstallCommand(targetDir, platformRoot, pluginSpec.spec) : void 0,
|
|
6935
|
+
copilot_plugin_spec: copilotPluginActive ? pluginSpec.spec : void 0,
|
|
6458
6936
|
command_code_powershell: args.commandCode ? commandCodePs : void 0,
|
|
6459
6937
|
next_steps: nextSteps,
|
|
6460
6938
|
dry_run: args.dryRun,
|
|
@@ -6548,7 +7026,7 @@ function formatInstallFiles(facts) {
|
|
|
6548
7026
|
}
|
|
6549
7027
|
function predictInstallVaultId(targetDir, deps) {
|
|
6550
7028
|
const yaml2 = deps.readFile(deps.joinPath(targetDir, "heyspex.yaml"));
|
|
6551
|
-
const accountId =
|
|
7029
|
+
const accountId = null;
|
|
6552
7030
|
const fromMarker = yaml2 ? parseVaultId(yaml2) : null;
|
|
6553
7031
|
if (fromMarker) return { vaultId: fromMarker, accountId };
|
|
6554
7032
|
try {
|
|
@@ -6729,6 +7207,7 @@ var INSTALL_HELP = [
|
|
|
6729
7207
|
" hs install [--target <dir>] [--platform <dir>] [--force] [--dry-run] [--json]",
|
|
6730
7208
|
" [--no-cursor] [--no-claude] [--claude-apply]",
|
|
6731
7209
|
" [--plugin | --plugin-apply] [--plugin-scope user|project|local]",
|
|
7210
|
+
" [--copilot-hooks | --copilot-plugin | --copilot-plugin-apply]",
|
|
6732
7211
|
" [--no-statusline]",
|
|
6733
7212
|
" [--cc | --command-code] [--register-vault [--workspace <id>]]",
|
|
6734
7213
|
" [--remote[=<url>]]",
|
|
@@ -6787,6 +7266,15 @@ var INSTALL_HELP = [
|
|
|
6787
7266
|
` the platform's .claude-plugin/marketplace.json (fallback: ${PLUGIN_INSTALL_SPEC}).`,
|
|
6788
7267
|
" --plugin-scope <s> Plugin install scope: user, project (default), or local.",
|
|
6789
7268
|
" Implies --plugin.",
|
|
7269
|
+
" --copilot-hooks Write .github/hooks/heyspex.json (GitHub Copilot hooks) into the",
|
|
7270
|
+
" target. Commands resolve node_modules/heyspex first, so an npm-only",
|
|
7271
|
+
" install (no heyspex repo checkout) works. Pick this OR the plugin.",
|
|
7272
|
+
" --copilot-plugin Print `copilot plugin marketplace add '<platform>'` +",
|
|
7273
|
+
" `copilot plugin install <plugin>@<marketplace>` (the platform root is",
|
|
7274
|
+
" node_modules/heyspex for a published install).",
|
|
7275
|
+
" --copilot-plugin-apply Run those two commands when `copilot` is on PATH.",
|
|
7276
|
+
" Requesting the Copilot plugin AND --copilot-hooks warns and skips the",
|
|
7277
|
+
" hooks file (both would fire every hook twice).",
|
|
6790
7278
|
" --remote[=<url>] Write .mcp.json for the REMOTE (cloud) MCP endpoint instead of",
|
|
6791
7279
|
` the local stdio server \u2014 the Streamable-HTTP form against`,
|
|
6792
7280
|
` ${DEFAULT_REMOTE_MCP_URL} (or <url>), authenticated by the`,
|
|
@@ -6835,15 +7323,15 @@ var INSTALL_HELP = [
|
|
|
6835
7323
|
|
|
6836
7324
|
// spex-console/src/lib/cli/install-identity.ts
|
|
6837
7325
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
6838
|
-
import { existsSync as
|
|
6839
|
-
import {
|
|
6840
|
-
import { join as join10 } from "node:path";
|
|
7326
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
7327
|
+
import { join as join11 } from "node:path";
|
|
6841
7328
|
import { createInterface } from "node:readline/promises";
|
|
6842
7329
|
init_credentials();
|
|
6843
7330
|
|
|
6844
7331
|
// vendor/heyspex-mcp/whoami.mjs
|
|
7332
|
+
init_heyspex_home();
|
|
6845
7333
|
init_credentials();
|
|
6846
|
-
import { homedir as
|
|
7334
|
+
import { homedir as homedir9 } from "node:os";
|
|
6847
7335
|
var SESSION_PATH = "/api/mcp/session";
|
|
6848
7336
|
async function probeCredentialIdentity({ consoleUrl, token, fetchImpl = fetch } = {}) {
|
|
6849
7337
|
if (!token) return { kind: "no_credential" };
|
|
@@ -7098,21 +7586,26 @@ async function runIdentityGate(io, flags, verb, wording = ATTACH_WORDING) {
|
|
|
7098
7586
|
"identity_unconfirmed"
|
|
7099
7587
|
);
|
|
7100
7588
|
}
|
|
7101
|
-
function readConfiguredAccountId(env = process.env, homeDir =
|
|
7589
|
+
function readConfiguredAccountId(env = process.env, homeDir = heyspexHome2()) {
|
|
7102
7590
|
return resolveSessionOwnerUncached({ env, homeDir });
|
|
7103
7591
|
}
|
|
7104
|
-
function parseMarkerAccount(raw) {
|
|
7105
|
-
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
7106
|
-
return parseAccountId(raw);
|
|
7107
|
-
}
|
|
7108
7592
|
function accountMismatchRepair(repoAccount) {
|
|
7109
7593
|
return `hs login --switch-account ${repoAccount}`;
|
|
7110
7594
|
}
|
|
7111
7595
|
function readRecordedRepoAccount(opts) {
|
|
7112
|
-
const
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7596
|
+
const homeDir = opts.homeDir ?? heyspexHome2();
|
|
7597
|
+
let fromRegistry = null;
|
|
7598
|
+
try {
|
|
7599
|
+
const row = loadVaultRegistry({ registryDir: join11(homeDir, ".heyspex") }).vaults.find(
|
|
7600
|
+
(v) => samePath(v.repo_root_abs, opts.repoRoot)
|
|
7601
|
+
);
|
|
7602
|
+
const id = row?.account_id;
|
|
7603
|
+
fromRegistry = id && id !== "legacy" ? id : null;
|
|
7604
|
+
} catch {
|
|
7605
|
+
fromRegistry = null;
|
|
7606
|
+
}
|
|
7607
|
+
if (fromRegistry) return { accountId: fromRegistry, source: "registry" };
|
|
7608
|
+
const configured = readConfiguredAccountId(opts.env ?? process.env, homeDir);
|
|
7116
7609
|
return configured ? { accountId: configured, source: "config.yaml" } : null;
|
|
7117
7610
|
}
|
|
7118
7611
|
function decideRemoteAccount(input) {
|
|
@@ -7131,7 +7624,7 @@ function decideRemoteAccount(input) {
|
|
|
7131
7624
|
const tokenLine = ` token: ${describeBearerSource(identity.tokenSource ?? null)}`;
|
|
7132
7625
|
if (recorded && recorded.accountId !== identity.accountId) {
|
|
7133
7626
|
const message2 = `${verb}: refusing -- this repo is recorded under account ${recorded.accountId} (${recorded.source}), but the token this install would wire belongs to ${formatShownIdentity(identity)}. Sign in as the repo's account with \`hs login --switch-account\`, then re-run with --account ${recorded.accountId}. Nothing was written.`;
|
|
7134
|
-
const fromMarker = recorded.source === "
|
|
7627
|
+
const fromMarker = recorded.source === "registry";
|
|
7135
7628
|
return {
|
|
7136
7629
|
ok: false,
|
|
7137
7630
|
result: {
|
|
@@ -7152,7 +7645,7 @@ function decideRemoteAccount(input) {
|
|
|
7152
7645
|
}
|
|
7153
7646
|
};
|
|
7154
7647
|
}
|
|
7155
|
-
const repoLine = !recorded ? " repo: unrecorded (heyspex.yaml has no account:, no account_id in ~/.heyspex/config.yaml) -- using the signed-in account" : recorded.source === "
|
|
7648
|
+
const repoLine = !recorded ? " repo: unrecorded (heyspex.yaml has no account:, no account_id in ~/.heyspex/config.yaml) -- using the signed-in account" : recorded.source === "registry" ? ` repo: ${recorded.accountId} (registry) -- matches the signed-in account` : ` repo: unrecorded in heyspex.yaml -- compared with this machine's configured account ${recorded.accountId} (~/.heyspex/config.yaml), which matches`;
|
|
7156
7649
|
return {
|
|
7157
7650
|
ok: true,
|
|
7158
7651
|
identity,
|
|
@@ -7192,7 +7685,7 @@ function isInteractiveTerminal() {
|
|
|
7192
7685
|
}
|
|
7193
7686
|
function defaultIdentityGateIo(opts) {
|
|
7194
7687
|
const consoleUrl = opts.consoleUrl.trim().replace(/\/+$/, "");
|
|
7195
|
-
const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) =>
|
|
7688
|
+
const loginCli = [opts.platformRoot, resolveCodeRoot({})].filter((r) => Boolean(r && r.trim())).map((r) => join11(r, "vendor", "heyspex-mcp", "cli.mjs")).find((p) => existsSync5(p));
|
|
7196
7689
|
return {
|
|
7197
7690
|
consoleUrl,
|
|
7198
7691
|
interactive: opts.interactive ?? isInteractiveTerminal(),
|
|
@@ -7299,25 +7792,27 @@ async function main() {
|
|
|
7299
7792
|
satellite: args.attachVault !== void 0
|
|
7300
7793
|
})
|
|
7301
7794
|
);
|
|
7302
|
-
const platformRoot = resolve5(
|
|
7795
|
+
const platformRoot = resolve5(
|
|
7796
|
+
resolveCallerPath(args.platformRoot, process4.env, process4.cwd()) || platformDefault || process4.cwd()
|
|
7797
|
+
);
|
|
7303
7798
|
const deps = {
|
|
7304
7799
|
repoRoot: targetDir,
|
|
7305
7800
|
resolvedTarget: targetDir,
|
|
7306
7801
|
resolvedPlatform: platformRoot,
|
|
7307
|
-
fileExists: (p) =>
|
|
7802
|
+
fileExists: (p) => existsSync6(p),
|
|
7308
7803
|
readFile: (p) => {
|
|
7309
7804
|
try {
|
|
7310
|
-
return
|
|
7805
|
+
return readFileSync11(p, "utf8");
|
|
7311
7806
|
} catch {
|
|
7312
7807
|
return null;
|
|
7313
7808
|
}
|
|
7314
7809
|
},
|
|
7315
|
-
writeFile: (p, c) =>
|
|
7316
|
-
mkdir: (p) =>
|
|
7810
|
+
writeFile: (p, c) => writeFileSync6(p, c, "utf8"),
|
|
7811
|
+
mkdir: (p) => mkdirSync6(p, { recursive: true }),
|
|
7317
7812
|
copyFile: (src, dest) => copyFileSync(src, dest),
|
|
7318
|
-
joinPath: (...s) =>
|
|
7813
|
+
joinPath: (...s) => join12(...s),
|
|
7319
7814
|
// Customer-template step (--register-vault) walks specs/ before applying.
|
|
7320
|
-
readdir: (p) =>
|
|
7815
|
+
readdir: (p) => readdirSync2(p),
|
|
7321
7816
|
isDirectory: (p) => {
|
|
7322
7817
|
try {
|
|
7323
7818
|
return statSync(p).isDirectory();
|
|
@@ -7328,7 +7823,7 @@ async function main() {
|
|
|
7328
7823
|
// The /hs-* harness commands step lists the platform's canonical trees.
|
|
7329
7824
|
listDir: (p) => {
|
|
7330
7825
|
try {
|
|
7331
|
-
return
|
|
7826
|
+
return readdirSync2(p);
|
|
7332
7827
|
} catch {
|
|
7333
7828
|
return [];
|
|
7334
7829
|
}
|
|
@@ -7349,7 +7844,7 @@ async function main() {
|
|
|
7349
7844
|
verb: "hs install",
|
|
7350
7845
|
repoRoot: targetDir,
|
|
7351
7846
|
readFile: deps.readFile,
|
|
7352
|
-
joinPath:
|
|
7847
|
+
joinPath: join12
|
|
7353
7848
|
}) : await runIdentityGate(io, identityArgv.flags, "hs install");
|
|
7354
7849
|
if (!gate.ok) {
|
|
7355
7850
|
process4.stderr.write(
|
|
@@ -7380,7 +7875,7 @@ async function main() {
|
|
|
7380
7875
|
sink.write(`${args.json ? JSON.stringify(payload, null, 2) : result.message}
|
|
7381
7876
|
`);
|
|
7382
7877
|
if (!result.isError && args.claudeApply && !args.dryRun && args.claude) {
|
|
7383
|
-
const cliPath = mcpCliPath(platformRoot,
|
|
7878
|
+
const cliPath = mcpCliPath(platformRoot, join12);
|
|
7384
7879
|
const attachedRoot = result.payload?.attached_vault?.repo_root_abs;
|
|
7385
7880
|
const mcpVaultRoot = attachedRoot ?? targetDir;
|
|
7386
7881
|
const claude = spawnSync3(
|
|
@@ -7413,17 +7908,17 @@ ${buildClaudeMcpAddCommandPowerShell(targetDir, cliPath, attachedRoot)}
|
|
|
7413
7908
|
process4.stdout.write("Claude Code: registered heyspex MCP (--scope project).\n");
|
|
7414
7909
|
}
|
|
7415
7910
|
}
|
|
7416
|
-
if (!result.isError && args.pluginApply && !args.dryRun &&
|
|
7911
|
+
if (!result.isError && args.pluginApply && !args.dryRun && existsSync6(pluginMarketplaceManifestPath(platformRoot, join12))) {
|
|
7417
7912
|
const shell = process4.platform === "win32";
|
|
7418
7913
|
const spec = resolvePluginSpec(platformRoot, {
|
|
7419
7914
|
readFile: (p) => {
|
|
7420
7915
|
try {
|
|
7421
|
-
return
|
|
7916
|
+
return readFileSync11(p, "utf8");
|
|
7422
7917
|
} catch {
|
|
7423
7918
|
return null;
|
|
7424
7919
|
}
|
|
7425
7920
|
},
|
|
7426
|
-
joinPath:
|
|
7921
|
+
joinPath: join12
|
|
7427
7922
|
});
|
|
7428
7923
|
const scope = args.pluginScope ?? "project";
|
|
7429
7924
|
const manualBlock = buildClaudePluginInstallCommandPowerShell(
|
|
@@ -7458,6 +7953,33 @@ ${manualBlock}
|
|
|
7458
7953
|
} else {
|
|
7459
7954
|
process4.stdout.write(
|
|
7460
7955
|
`Claude Code: installed plugin ${spec.spec} (--scope ${scope}). Restart the Claude Code session to load it.
|
|
7956
|
+
`
|
|
7957
|
+
);
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
const copilotCommands = result.payload?.copilot_plugin_commands;
|
|
7962
|
+
if (!result.isError && args.copilotPluginApply && !args.dryRun && copilotCommands) {
|
|
7963
|
+
const shell = process4.platform === "win32";
|
|
7964
|
+
const spec = result.payload.copilot_plugin_spec ?? "";
|
|
7965
|
+
const copilotRun = (cliArgs) => spawnSync3("copilot", cliArgs, { cwd: targetDir, encoding: "utf8", shell });
|
|
7966
|
+
const failed = (what, r) => process4.stderr.write(
|
|
7967
|
+
`hs install: copilot plugin ${what} failed (${r.error ? r.error.message : `exit ${r.status ?? "?"}`}).
|
|
7968
|
+
${r.stderr || r.stdout || ""}
|
|
7969
|
+
Run manually:
|
|
7970
|
+
${copilotCommands}
|
|
7971
|
+
`
|
|
7972
|
+
);
|
|
7973
|
+
const marketplace = copilotRun(["plugin", "marketplace", "add", platformRoot]);
|
|
7974
|
+
if (marketplace.status !== 0 && !/already/i.test(marketplace.stderr + marketplace.stdout)) {
|
|
7975
|
+
failed("marketplace add", marketplace);
|
|
7976
|
+
} else {
|
|
7977
|
+
const install2 = copilotRun(["plugin", "install", spec]);
|
|
7978
|
+
if (install2.status !== 0 && !/already/i.test(install2.stderr + install2.stdout)) {
|
|
7979
|
+
failed("install", install2);
|
|
7980
|
+
} else {
|
|
7981
|
+
process4.stdout.write(
|
|
7982
|
+
`GitHub Copilot: installed plugin ${spec}. Run /restart in the Copilot CLI to load it.
|
|
7461
7983
|
`
|
|
7462
7984
|
);
|
|
7463
7985
|
}
|
|
@@ -7465,9 +7987,9 @@ ${manualBlock}
|
|
|
7465
7987
|
}
|
|
7466
7988
|
if (!result.isError && !args.dryRun && args.claude && (args.statusline ?? true)) {
|
|
7467
7989
|
const glyphScript = [
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
].find((p) =>
|
|
7990
|
+
join12(targetDir, "scripts", "install-statusline.mjs"),
|
|
7991
|
+
join12(platformRoot, "scripts", "install-statusline.mjs")
|
|
7992
|
+
].find((p) => existsSync6(p));
|
|
7471
7993
|
const glyph = glyphScript ? spawnSync3(process4.execPath, [glyphScript, "--glyph-only"], {
|
|
7472
7994
|
cwd: targetDir,
|
|
7473
7995
|
encoding: "utf8"
|