@delorenj/pjangler 1.2.33 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +4754 -2564
  3. package/dist/mcp-server.js +4516 -2141
  4. package/dist/prompt.js +404 -0
  5. package/package.json +7 -5
  6. package/templates/commonproject/copier.yml +6 -1
  7. package/templates/commonproject/template/.agents/hooks/README.md +10 -10
  8. package/templates/commonproject/template/.agents/hooks/hooks.master.json +1 -1
  9. package/templates/commonproject/template/.agents/hooks/sync.py +4 -4
  10. package/templates/commonproject/template/.agents/local.example.json +1 -1
  11. package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +1 -1
  12. package/templates/commonproject/template/.mise/scripts/provision-packs.py +14 -50
  13. package/templates/commonproject/template/mise.toml.jinja +11 -11
  14. package/templates/hermes-agent/copier.yml +34 -14
  15. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
  16. package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
  17. package/templates/hermes-agent/template/.scripts/01-config.sh +13 -4
  18. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
  19. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +73 -14
  20. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +37 -32
  21. package/templates/hermes-agent/template/.scripts/30-telegram.sh +18 -4
  22. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
  23. package/templates/hermes-agent/template/.scripts/70-systemd.sh +114 -24
  24. package/templates/hermes-agent/template/.scripts/80-registry.sh +19 -3
  25. package/templates/hermes-agent/template/.scripts/_lib.sh +74 -11
  26. package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
  27. package/templates/hermes-agent/template/.scripts/config.example.toml +7 -4
  28. package/templates/hermes-agent/template/.scripts/credential-launch.sh +81 -0
  29. package/templates/hermes-agent/template/.scripts/heartbeat.sh +15 -2
  30. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
  31. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +734 -0
  32. package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
  33. package/templates/hermes-agent/template/.scripts/providers/plane.sh +31 -5
  34. package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
  35. package/templates/hermes-agent/template/SOUL.md.jinja +48 -12
  36. package/templates/hermes-agent/template/hermes.jinja +51 -10
  37. package/templates/hermes-agent/template/momo.jinja +177 -0
  38. package/templates/hermes-agent/template/role.yaml.jinja +31 -21
package/dist/prompt.js ADDED
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/prompt.ts
4
+ import { readFileSync as readFileSync2, realpathSync } from "node:fs";
5
+ import { basename, join as join3 } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ // src/describe/activity.ts
9
+ import { spawn, spawnSync } from "node:child_process";
10
+ import { statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
13
+ var MAX_DIRTY_STATS = 500;
14
+ var GIT_TIMEOUT_MS = 5e3;
15
+ var GIT_MAX_BUFFER = 16 * 1024 * 1024;
16
+ function git(repo, args) {
17
+ const result = spawnSync("git", ["-C", repo, ...args], {
18
+ encoding: "utf8",
19
+ timeout: GIT_TIMEOUT_MS,
20
+ maxBuffer: GIT_MAX_BUFFER
21
+ });
22
+ if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
23
+ return result.stdout;
24
+ }
25
+ function trimmed(raw) {
26
+ if (raw === void 0) return void 0;
27
+ const value = raw.trim();
28
+ return value === "" ? void 0 : value;
29
+ }
30
+ function gitLine(repo, args) {
31
+ return trimmed(git(repo, args));
32
+ }
33
+ function isGitRepo(repo) {
34
+ return gitLine(repo, ["rev-parse", "--is-inside-work-tree"]) === "true";
35
+ }
36
+ var MINUTE = 60;
37
+ var HOUR = 60 * MINUTE;
38
+ var DAY = 24 * HOUR;
39
+ var WEEK = 7 * DAY;
40
+ var MONTH = 30 * DAY;
41
+ var YEAR = 365 * DAY;
42
+ function plural(count, unit) {
43
+ return `${count} ${unit}${count === 1 ? "" : "s"} ago`;
44
+ }
45
+ function formatRelativeAge(deltaSeconds) {
46
+ const delta = Math.max(0, Math.floor(deltaSeconds));
47
+ if (delta < MINUTE) return "just now";
48
+ if (delta < HOUR) return plural(Math.floor(delta / MINUTE), "minute");
49
+ if (delta < DAY) return plural(Math.floor(delta / HOUR), "hour");
50
+ if (delta < WEEK) return plural(Math.floor(delta / DAY), "day");
51
+ if (delta < MONTH) return plural(Math.floor(delta / WEEK), "week");
52
+ if (delta < YEAR) return plural(Math.floor(delta / MONTH), "month");
53
+ return plural(Math.floor(delta / YEAR), "year");
54
+ }
55
+ function formatCompactAge(deltaSeconds) {
56
+ const delta = Math.max(0, Math.floor(deltaSeconds));
57
+ if (delta < MINUTE) return "now";
58
+ if (delta < HOUR) return `${Math.floor(delta / MINUTE)}m`;
59
+ if (delta < DAY) return `${Math.floor(delta / HOUR)}h`;
60
+ if (delta < WEEK) return `${Math.floor(delta / DAY)}d`;
61
+ if (delta < MONTH) return `${Math.floor(delta / WEEK)}w`;
62
+ if (delta < YEAR) return `${Math.floor(delta / MONTH)}mo`;
63
+ return `${Math.floor(delta / YEAR)}y`;
64
+ }
65
+ var REF_ARGS = [
66
+ "for-each-ref",
67
+ "--sort=-committerdate",
68
+ "--format=%(committerdate:unix)%09%(refname:short)",
69
+ "refs/heads",
70
+ "refs/remotes",
71
+ "refs/tags"
72
+ ];
73
+ var WORKTREE_ARGS = ["worktree", "list", "--porcelain"];
74
+ var STATUS_ARGS = ["status", "--porcelain", "-z", "--ignore-submodules=dirty"];
75
+ function parseRefs(raw) {
76
+ if (raw === void 0) return { count: 0 };
77
+ const lines = raw.split("\n").filter((line) => line.trim() !== "");
78
+ if (!lines.length) return { count: 0 };
79
+ const [stamp, name] = lines[0].split(" ");
80
+ const unix = Number(stamp);
81
+ if (!Number.isFinite(unix) || unix <= 0) return { count: lines.length };
82
+ return { source: { kind: "ref", label: name ?? "(unnamed ref)", unix }, count: lines.length };
83
+ }
84
+ function parseWorktrees(raw) {
85
+ if (raw === void 0) return [];
86
+ const entries = [];
87
+ let current = { detached: false };
88
+ const flush = () => {
89
+ if (current.path && current.sha) entries.push({ path: current.path, sha: current.sha, detached: current.detached });
90
+ current = { detached: false };
91
+ };
92
+ for (const line of raw.split("\n")) {
93
+ if (line.startsWith("worktree ")) {
94
+ flush();
95
+ current.path = line.slice("worktree ".length);
96
+ } else if (line.startsWith("HEAD ")) {
97
+ current.sha = line.slice("HEAD ".length).trim();
98
+ } else if (line === "detached") {
99
+ current.detached = true;
100
+ }
101
+ }
102
+ flush();
103
+ return entries;
104
+ }
105
+ function parseWorktreeStamps(raw, entries) {
106
+ if (raw === void 0) return void 0;
107
+ let best;
108
+ for (const line of raw.split("\n")) {
109
+ const [stamp, sha] = line.trim().split(" ");
110
+ const unix = Number(stamp);
111
+ if (!Number.isFinite(unix) || unix <= 0 || !sha) continue;
112
+ if (best && unix <= best.unix) continue;
113
+ const owner = entries.find((entry) => entry.sha === sha);
114
+ const name = owner ? basenameOf(owner.path) : sha.slice(0, 7);
115
+ best = { kind: "worktree", label: owner?.detached ? `${name} (detached)` : name, unix };
116
+ }
117
+ return best;
118
+ }
119
+ function parseStatusPaths(raw) {
120
+ if (raw === void 0) return [];
121
+ const parts = raw.split("\0").filter((part) => part !== "");
122
+ const paths = [];
123
+ for (let index = 0; index < parts.length; index++) {
124
+ const entry = parts[index];
125
+ if (entry.length < 4 || entry[2] !== " ") continue;
126
+ paths.push(entry.slice(3));
127
+ if (entry[0] === "R" || entry[0] === "C") index += 1;
128
+ }
129
+ return paths;
130
+ }
131
+ function basenameOf(path) {
132
+ const parts = path.split("/").filter(Boolean);
133
+ return parts[parts.length - 1] ?? path;
134
+ }
135
+ function uncommittedSource(repo, paths) {
136
+ if (!paths.length) return void 0;
137
+ let newest = 0;
138
+ for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
139
+ try {
140
+ const mtime = Math.floor(statSync(join(repo, path)).mtimeMs / 1e3);
141
+ if (mtime > newest) newest = mtime;
142
+ } catch {
143
+ }
144
+ }
145
+ if (newest <= 0) return void 0;
146
+ const label = paths.length === 1 ? "1 uncommitted file" : `${paths.length} uncommitted files`;
147
+ return { kind: "uncommitted", label, unix: newest };
148
+ }
149
+ var NO_ACTIVITY = {
150
+ updated: null,
151
+ updatedUnix: null,
152
+ relative: "never",
153
+ compact: "\u2014",
154
+ active: false,
155
+ source: null,
156
+ scanned: { refs: 0, worktrees: 0, dirtyFiles: 0 }
157
+ };
158
+ function emptyActivity() {
159
+ return { ...NO_ACTIVITY, scanned: { refs: 0, worktrees: 0, dirtyFiles: 0 } };
160
+ }
161
+ function assembleActivity(candidates, scanned, now) {
162
+ let winner = null;
163
+ for (const candidate of candidates) {
164
+ if (!candidate) continue;
165
+ if (!winner || candidate.unix >= winner.unix) winner = candidate;
166
+ }
167
+ if (!winner) return { ...NO_ACTIVITY, scanned };
168
+ const nowUnix = Math.floor((now?.getTime() ?? Date.now()) / 1e3);
169
+ const delta = nowUnix - winner.unix;
170
+ return {
171
+ updated: new Date(winner.unix * 1e3).toISOString(),
172
+ updatedUnix: winner.unix,
173
+ relative: formatRelativeAge(delta),
174
+ compact: formatCompactAge(delta),
175
+ active: delta < ACTIVE_WINDOW_SECONDS,
176
+ source: winner,
177
+ scanned
178
+ };
179
+ }
180
+ function computeRepoActivity(repo, options = {}) {
181
+ if (!isGitRepo(repo)) return emptyActivity();
182
+ const refs = parseRefs(git(repo, REF_ARGS));
183
+ const worktrees = parseWorktrees(git(repo, WORKTREE_ARGS));
184
+ const shas = [...new Set(worktrees.map((entry) => entry.sha))];
185
+ const worktreeSource = shas.length ? parseWorktreeStamps(git(repo, ["show", "-s", "--format=%ct %H", ...shas]), worktrees) : void 0;
186
+ const paths = parseStatusPaths(git(repo, STATUS_ARGS));
187
+ return assembleActivity(
188
+ [refs.source, worktreeSource, uncommittedSource(repo, paths)],
189
+ { refs: refs.count, worktrees: worktrees.length, dirtyFiles: paths.length },
190
+ options.now
191
+ );
192
+ }
193
+
194
+ // src/project/boardUrl.ts
195
+ import { existsSync, readFileSync, statSync as statSync2 } from "node:fs";
196
+ import { homedir } from "node:os";
197
+ import { dirname, isAbsolute, join as join2, resolve } from "node:path";
198
+ var DEFAULT_PLANE_BASE = "https://plane.delo.sh";
199
+ var DEFAULT_PLANE_WORKSPACE = "33god";
200
+ function resolveTemplateConfigPath(env = process.env, home = homedir()) {
201
+ const fromEnv = env.HERMES_TEMPLATE_CONFIG;
202
+ if (fromEnv && fromEnv.trim()) return fromEnv.trim();
203
+ const xdg = env.XDG_CONFIG_HOME?.trim();
204
+ const base = xdg && xdg.length ? xdg : join2(home, ".config");
205
+ return join2(base, "hermes-agent-template", "config.toml");
206
+ }
207
+ function readTomlScalar(text, section, key) {
208
+ let inSection = false;
209
+ for (const raw of text.split("\n")) {
210
+ const line = raw.trim();
211
+ if (!line || line.startsWith("#")) continue;
212
+ if (line.startsWith("[")) {
213
+ inSection = line === `[${section}]`;
214
+ continue;
215
+ }
216
+ if (!inSection) continue;
217
+ const eq = line.indexOf("=");
218
+ if (eq === -1) continue;
219
+ if (line.slice(0, eq).trim() !== key) continue;
220
+ const value = line.slice(eq + 1).trim();
221
+ const quoted = /^"([^"]*)"|^'([^']*)'/.exec(value);
222
+ if (quoted) return quoted[1] ?? quoted[2];
223
+ const bare = (value.split("#")[0] ?? "").trim();
224
+ return bare || void 0;
225
+ }
226
+ return void 0;
227
+ }
228
+ function readTemplateConfig(env, home) {
229
+ try {
230
+ const path = resolveTemplateConfigPath(env, home);
231
+ return existsSync(path) ? readFileSync(path, "utf8") : void 0;
232
+ } catch {
233
+ return void 0;
234
+ }
235
+ }
236
+ function planeBase(env = process.env, home = homedir()) {
237
+ const fromEnv = env.PLANE_BASE?.trim();
238
+ if (fromEnv) return fromEnv.replace(/\/+$/, "");
239
+ const config = readTemplateConfig(env, home);
240
+ const fromConfig = config ? readTomlScalar(config, "plane", "base")?.trim() : void 0;
241
+ if (fromConfig) return fromConfig.replace(/\/+$/, "");
242
+ return DEFAULT_PLANE_BASE;
243
+ }
244
+ function planeWorkspace(provider, env, home) {
245
+ const fromManifest = provider.workspace?.trim();
246
+ if (fromManifest) return fromManifest;
247
+ const config = readTemplateConfig(env, home);
248
+ const fromConfig = config ? readTomlScalar(config, "plane", "workspace")?.trim() : void 0;
249
+ return fromConfig || DEFAULT_PLANE_WORKSPACE;
250
+ }
251
+ function escapeRegExp(value) {
252
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
253
+ }
254
+ function extractTicketRef(branch, identifier) {
255
+ if (!branch || !identifier) return void 0;
256
+ const ident = identifier.trim();
257
+ if (!ident) return void 0;
258
+ const match = new RegExp(`\\b${escapeRegExp(ident)}-(\\d+)\\b`, "i").exec(branch);
259
+ return match ? `${ident.toUpperCase()}-${match[1]}` : void 0;
260
+ }
261
+ function normalizeTicketRef(input, identifier) {
262
+ const value = input?.trim();
263
+ if (!value) return void 0;
264
+ if (/^\d+$/.test(value)) {
265
+ const ident = identifier?.trim();
266
+ return ident ? `${ident.toUpperCase()}-${value}` : void 0;
267
+ }
268
+ const qualified = /^([A-Za-z][A-Za-z0-9]*)-(\d+)$/.exec(value);
269
+ if (!qualified) return void 0;
270
+ return `${qualified[1].toUpperCase()}-${qualified[2]}`;
271
+ }
272
+ function resolveTicketRef(provider, options) {
273
+ return normalizeTicketRef(options.ref, provider.identifier) ?? extractTicketRef(options.branch, provider.identifier);
274
+ }
275
+ function boardUrl(provider, options = {}) {
276
+ if (!provider) return void 0;
277
+ const env = options.env ?? process.env;
278
+ const home = options.home ?? homedir();
279
+ const type = (provider.type || "plane").trim().toLowerCase();
280
+ const boardId = provider.board_id?.trim();
281
+ if (!boardId) return void 0;
282
+ if (type === "trello") {
283
+ return `https://trello.com/b/${boardId}`;
284
+ }
285
+ if (type !== "plane") return void 0;
286
+ const workspace = planeWorkspace(provider, env, home);
287
+ if (!workspace) return void 0;
288
+ const base = planeBase(env, home);
289
+ const ref = resolveTicketRef(provider, options);
290
+ return ref ? `${base}/${workspace}/browse/${ref}` : `${base}/${workspace}/projects/${boardId}/issues`;
291
+ }
292
+ function findProjectRoot(from) {
293
+ let dir = resolve(from);
294
+ for (; ; ) {
295
+ if (existsSync(join2(dir, ".project.json"))) return dir;
296
+ const parent = dirname(dir);
297
+ if (parent === dir) return void 0;
298
+ dir = parent;
299
+ }
300
+ }
301
+ function readTicketProvider(root) {
302
+ try {
303
+ const manifest = JSON.parse(readFileSync(join2(root, ".project.json"), "utf8"));
304
+ const provider = manifest.ticket_provider;
305
+ if (!provider || typeof provider !== "object") return void 0;
306
+ return provider;
307
+ } catch {
308
+ return void 0;
309
+ }
310
+ }
311
+ function currentBranch(from) {
312
+ try {
313
+ let dir = resolve(from);
314
+ for (; ; ) {
315
+ const dotgit = join2(dir, ".git");
316
+ if (existsSync(dotgit)) {
317
+ let gitDir = dotgit;
318
+ if (statSync2(dotgit).isFile()) {
319
+ const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotgit, "utf8"));
320
+ if (!pointer) return void 0;
321
+ const target = pointer[1].trim();
322
+ gitDir = isAbsolute(target) ? target : resolve(dir, target);
323
+ }
324
+ const head = readFileSync(join2(gitDir, "HEAD"), "utf8").trim();
325
+ const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
326
+ return ref ? ref[1].trim() : void 0;
327
+ }
328
+ const parent = dirname(dir);
329
+ if (parent === dir) return void 0;
330
+ dir = parent;
331
+ }
332
+ } catch {
333
+ return void 0;
334
+ }
335
+ }
336
+ function resolveBoardUrl(cwd, ref, env = process.env) {
337
+ const root = findProjectRoot(cwd);
338
+ if (!root) return void 0;
339
+ const provider = readTicketProvider(root);
340
+ if (!provider) return void 0;
341
+ return boardUrl(provider, { ref, branch: currentBranch(root), env });
342
+ }
343
+
344
+ // src/prompt.ts
345
+ function readPromptFacts(root, now) {
346
+ let slug = basename(root);
347
+ let identifier;
348
+ try {
349
+ const manifest = JSON.parse(readFileSync2(join3(root, ".project.json"), "utf8"));
350
+ if (typeof manifest.project_slug === "string" && manifest.project_slug) slug = manifest.project_slug;
351
+ const provider = manifest.ticket_provider;
352
+ if (provider && typeof provider.identifier === "string" && provider.identifier) identifier = provider.identifier;
353
+ } catch {
354
+ }
355
+ const activity = computeRepoActivity(root, { now });
356
+ return {
357
+ root,
358
+ slug,
359
+ identifier,
360
+ age: activity.updatedUnix ? activity.compact : void 0,
361
+ active: activity.active
362
+ };
363
+ }
364
+ function formatPromptLine(facts) {
365
+ const parts = [facts.slug];
366
+ if (facts.identifier) parts.push(`(${facts.identifier})`);
367
+ const head = parts.join(" ");
368
+ return facts.age ? `${head} \xB7 ${facts.age}` : head;
369
+ }
370
+ function promptLine(cwd, now) {
371
+ const root = findProjectRoot(cwd);
372
+ if (!root) return void 0;
373
+ return formatPromptLine(readPromptFacts(root, now));
374
+ }
375
+ function main() {
376
+ try {
377
+ const args = process.argv.slice(2);
378
+ if (args[0] === "--url") {
379
+ const url = resolveBoardUrl(process.cwd(), args[1]);
380
+ if (url) process.stdout.write(`${url}
381
+ `);
382
+ else process.exitCode = 1;
383
+ return;
384
+ }
385
+ const line = promptLine(process.cwd());
386
+ if (line) process.stdout.write(line);
387
+ } catch {
388
+ }
389
+ }
390
+ function isMainModule() {
391
+ if (!process.argv[1]) return false;
392
+ try {
393
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
394
+ } catch {
395
+ return false;
396
+ }
397
+ }
398
+ if (isMainModule()) main();
399
+ export {
400
+ findProjectRoot,
401
+ formatPromptLine,
402
+ promptLine,
403
+ readPromptFacts
404
+ };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.33",
3
+ "version": "1.3.7",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "pjangler": "dist/index.js",
9
9
  "pj": "dist/index.js",
10
- "pjangler-mcp": "dist/mcp-server.js"
10
+ "pjangler-mcp": "dist/mcp-server.js",
11
+ "pjangler-prompt": "dist/prompt.js"
11
12
  },
12
13
  "files": [
13
14
  "dist",
@@ -22,7 +23,7 @@
22
23
  "node": ">=20"
23
24
  },
24
25
  "scripts": {
25
- "build": "esbuild src/index.ts src/mcp-server.ts --bundle --packages=external --platform=node --format=esm --outdir=dist",
26
+ "build": "esbuild src/index.ts src/mcp-server.ts src/prompt.ts --bundle --packages=external --platform=node --format=esm --outdir=dist",
26
27
  "check:audit:prod": "npm audit --omit=dev",
27
28
  "check:lock": "node scripts/check-package-lock-parity.mjs",
28
29
  "check:submodules": "node scripts/check-submodule-contract.mjs",
@@ -31,11 +32,12 @@
31
32
  "mcp": "node dist/mcp-server.js",
32
33
  "typecheck": "tsc --noEmit",
33
34
  "test:bmad-installer-contract": "node tests/bmad-installer-contract-regressions.mjs",
34
- "test": "npm run check:lock && npm run check:submodules && npm run check:tracked-secrets && node tests/portable-test-paths-regressions.mjs && node tests/release-regressions.mjs && node tests/submodule-contract-regressions.mjs && node tests/secret-publication-gate-regressions.mjs && node tests/bmad-version-surface-regressions.mjs && node tests/bmad-transaction-regressions.mjs && node tests/parity-migrate-regressions.mjs && node tests/pjan-57-lifecycle-recipes-regressions.mjs && node tests/pjan-57-dogfood-regressions.mjs && node tests/generated-project-lifecycle-regressions.mjs && node tests/pack-flatten-regressions.mjs && node tests/pack-flatten-cross-engine-regressions.mjs && node tests/registry-cache-parity-regressions.mjs && node tests/registry-root-ladder-regressions.mjs && node tests/pjan-23-regressions.mjs && node tests/pjan-24-regressions.mjs && node tests/pjan-28-regressions.mjs && node tests/pjan-30-regressions.mjs && node tests/pjan-31a-regressions.mjs && node tests/pjan-36-regressions.mjs && node tests/pjan-43-regressions.mjs && node tests/pjan-48-regressions.mjs && node tests/pjan-49-regressions.mjs && node tests/pjan-50-regressions.mjs && node tests/skillex-init-regressions.mjs && node tests/fleet-shared-bloodbank-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs && node tests/pg-registry-regressions.mjs && node tests/momo-lifecycle-plane-regressions.mjs",
35
+ "test": "npm run check:lock && node tests/package-lock-parity-regressions.mjs && npm run check:submodules && npm run check:tracked-secrets && node tests/portable-test-paths-regressions.mjs && node tests/release-regressions.mjs && node tests/submodule-contract-regressions.mjs && node tests/secret-publication-gate-regressions.mjs && node tests/bmad-version-surface-regressions.mjs && node tests/bmad-transaction-regressions.mjs && node tests/parity-migrate-regressions.mjs && node tests/hermes-profile-inheritance-regressions.mjs && node tests/pjan-57-lifecycle-recipes-regressions.mjs && node tests/pjan-57-dogfood-regressions.mjs && node tests/generated-project-lifecycle-regressions.mjs && node tests/pack-flatten-regressions.mjs && node tests/pack-flatten-cross-engine-regressions.mjs && node tests/registry-cache-parity-regressions.mjs && node tests/registry-root-ladder-regressions.mjs && node tests/pjan-23-regressions.mjs && node tests/pjan-24-regressions.mjs && node tests/pjan-28-regressions.mjs && node tests/pjan-30-regressions.mjs && node tests/pjan-31a-regressions.mjs && node tests/pjan-36-regressions.mjs && node tests/pjan-43-regressions.mjs && node tests/pjan-48-regressions.mjs && node tests/pjan-49-regressions.mjs && node tests/pjan-50-regressions.mjs && node tests/pjan-65-regressions.mjs && node tests/pjan-67-lifecycle-preflight-regressions.mjs && node tests/pjan-67-regressions.mjs && node tests/pjan-67-trusted-lifecycle-regressions.mjs && node tests/pjan-71-regressions.mjs && node tests/pjan-72-regressions.mjs && node tests/pjan-75-regressions.mjs && node tests/pjan-76-regressions.mjs && node tests/skillex-init-regressions.mjs && node tests/fleet-shared-bloodbank-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs && node tests/pg-registry-regressions.mjs && node tests/momo-lifecycle-plane-regressions.mjs",
35
36
  "migrate:up": "node-pg-migrate --migrations-dir migrations up",
36
37
  "migrate:down": "node-pg-migrate --migrations-dir migrations down",
37
38
  "migrate:create": "node-pg-migrate --migrations-dir migrations create",
38
- "prepublishOnly": "npm run check:lock && npm run check:submodules -- --remote --recursive --archive --npm && npm run build && npm run check:tracked-secrets"
39
+ "prepublishOnly": "npm run check:lock && npm run check:submodules -- --remote --recursive --archive --npm && npm run build && npm run check:tracked-secrets",
40
+ "test:hermes-profile-inheritance": "node tests/hermes-profile-inheritance-regressions.mjs"
39
41
  },
40
42
  "keywords": [
41
43
  "cli",
@@ -116,7 +116,12 @@ _exclude:
116
116
  _tasks:
117
117
  - "cp ~/.config/git/ignore .gitignore 2>/dev/null || echo '# Add ignores here' > .gitignore"
118
118
  # Ensure secrets + per-dev agent-hook/skill overrides are ignored (appended after the base ignore).
119
- - "printf '\\n# Secrets + per-dev agent-hook/skill overrides (see .agents/local.example.json)\\n.env\\n.env.*\\n!.env.op\\n.agents/local.json\\n.lastagent\\n**/.claude/settings.local.json\\n\\n# PJAN-57: all six generated CLI configurations are durable project state.\\n!.claude/\\n!.claude/**\\n!.codex/\\n!.codex/**\\n!.gemini/\\n!.gemini/**\\n!.copilot/\\n!.copilot/**\\n!.opencode/\\n!.opencode/**\\n!.kimi-code/\\n!.kimi-code/**\\n' >> .gitignore"
119
+ # Each CLI root's hand-owned config is durable project state; the skill
120
+ # projections inside it are not — bmad-method and skills:sync rewrite them
121
+ # on every run, so they are re-ignored after the un-ignore lines (last
122
+ # match wins). _bmad/_config/manifest.yaml pins the version that
123
+ # reproduces them exactly.
124
+ - "printf '\\n# Secrets + per-dev agent-hook/skill overrides (see .agents/local.example.json)\\n.env\\n.env.*\\n!.env.op\\n.agents/local.json\\n.lastagent\\n**/.claude/settings.local.json\\n\\n# Generated CLI configurations are durable project state...\\n!.claude/\\n!.claude/**\\n!.codex/\\n!.codex/**\\n!.gemini/\\n!.gemini/**\\n!.copilot/\\n!.copilot/**\\n!.opencode/\\n!.opencode/**\\n!.kimi-code/\\n!.kimi-code/**\\n# ...but their skill projections are regenerated, so they stay out of the tree.\\n/.agents/skills/\\n.claude/skills/\\n.codex/skills/\\n.gemini/skills/\\n.copilot/skills/\\n.opencode/skills/\\n.kimi-code/skills/\\n' >> .gitignore"
120
125
  - "ln -sf AGENTS.md CLAUDE.md"
121
126
  - "ln -sf AGENTS.md GEMINI.md"
122
127
  # Materialize the declared Skillex packs' manifest + symlink topology
@@ -12,11 +12,11 @@ master writes zero bytes).
12
12
  ## TL;DR
13
13
 
14
14
  ```bash
15
- mise run hooks-sync # fan out the master -> claude + codex + hermes
16
- mise run hooks-check # drift gate (read-only; used in CI)
17
- mise run hooks-uninstall # remove codex + hermes injections
18
- mise run skills-sync # sync .agents/skills.json -> local CLI skill dirs
19
- mise run hindsight-setup # one-time: pull the shared CAF Hindsight key into .env
15
+ mise run hooks:sync # fan out the master -> claude + codex + hermes
16
+ mise run hooks:check # drift gate (read-only; used in CI)
17
+ mise run hooks:uninstall # remove codex + hermes injections
18
+ mise run skills:sync # sync .agents/skills.json -> local CLI skill dirs
19
+ mise run hindsight:setup # one-time: pull the shared CAF Hindsight key into .env
20
20
  ```
21
21
 
22
22
  You normally run nothing — `mise` does it on directory enter/leave (see
@@ -93,10 +93,10 @@ merged in automatically.
93
93
 
94
94
  - Project-local skills with the same name shadow the global ones.
95
95
  - No `defer_to_global` flag or bash cleanup scripts are needed anymore.
96
- - The manifest is the only hand-edited skill SSOT; run `mise run skills-sync`
96
+ - The manifest is the only hand-edited skill SSOT; run `mise run skills:sync`
97
97
  after changing it, or just re-enter the repo.
98
98
 
99
- ## Hindsight credentials — `mise run hindsight-setup`
99
+ ## Hindsight credentials — `mise run hindsight:setup`
100
100
 
101
101
  The hooks shell out to `hindsight`, which needs an API key for the self-hosted
102
102
  instance. Strategy: **one shared CAF-scoped key in 1Password**. `hindsight-setup`
@@ -105,7 +105,7 @@ gitignored `.env` (mise loads it, env vars outrank `~/.hindsight/config`). Point
105
105
  it at the right item if the default ref is wrong:
106
106
 
107
107
  ```bash
108
- HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight-setup
108
+ HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight:setup
109
109
  ```
110
110
 
111
111
  Without a key the hooks **no-op gracefully** — recall/retain just do nothing.
@@ -113,10 +113,10 @@ Without a key the hooks **no-op gracefully** — recall/retain just do nothing.
113
113
  ## Adding / changing a hook
114
114
 
115
115
  1. Edit `hooks.master.json`.
116
- 2. `mise run hooks-sync`.
116
+ 2. `mise run hooks:sync`.
117
117
  3. Commit the master **and** the regenerated `.claude/settings.json` together
118
118
  (codex/hermes targets are per-dev/per-deployment, not committed).
119
- 4. CI runs `mise run hooks-check` — fails if the committed Claude settings drift.
119
+ 4. CI runs `mise run hooks:check` — fails if the committed Claude settings drift.
120
120
 
121
121
  ## Files
122
122
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "SINGLE SOURCE OF TRUTH for this repo's project-scoped agent hooks. Hand-edit ONLY this file, then run `mise run hooks-sync` (or re-cd into the repo) to regenerate every per-agent config. Generated files are NOT hand-edited. Pattern: the /ssot-fanout skill. See .agents/hooks/README.md.",
2
+ "$comment": "SINGLE SOURCE OF TRUTH for this repo's project-scoped agent hooks. Hand-edit ONLY this file, then run `mise run hooks:sync` (or re-cd into the repo) to regenerate every per-agent config. Generated files are NOT hand-edited. Pattern: the /ssot-fanout skill. See .agents/hooks/README.md.",
3
3
  "version": 1,
4
4
  "marker": "project-agent-hooks",
5
5
  "hooks": [
@@ -515,7 +515,7 @@ def cmd_check(master: dict) -> int:
515
515
  current = target.read_text() if target.exists() else None
516
516
  if current != desired:
517
517
  warn(f"DRIFT: {target.relative_to(REPO_ROOT)} differs from hooks.master.json "
518
- f"(run `mise run hooks-sync`)")
518
+ f"(run `mise run hooks:sync`)")
519
519
  rc = 1
520
520
  else:
521
521
  log(f"claude: in sync ({target.relative_to(REPO_ROOT)})")
@@ -538,7 +538,7 @@ def cmd_check(master: dict) -> int:
538
538
  ]
539
539
  if missing:
540
540
  warn(f"DRIFT: codex hooks.json missing {len(missing)} project hook(s) "
541
- f"(run `mise run hooks-sync`)")
541
+ f"(run `mise run hooks:sync`)")
542
542
  rc = 1
543
543
  else:
544
544
  log(f"codex: in sync ({ct})")
@@ -558,7 +558,7 @@ def cmd_check(master: dict) -> int:
558
558
  missing = [c for c in want_cmds if c not in present]
559
559
  if begin not in present or missing:
560
560
  warn(f"DRIFT: kimi config.toml missing the project hooks block "
561
- f"(run `mise run hooks-sync`)")
561
+ f"(run `mise run hooks:sync`)")
562
562
  rc = 1
563
563
  else:
564
564
  log(f"kimi: in sync ({kt})")
@@ -574,7 +574,7 @@ def cmd_check(master: dict) -> int:
574
574
  missing = [c for _ev, c, _t in hermes_commands(master) if c not in present]
575
575
  if missing:
576
576
  warn(f"DRIFT: hermes config.yaml missing {len(missing)} adapter hook(s) "
577
- f"(run `mise run hooks-sync`)")
577
+ f"(run `mise run hooks:sync`)")
578
578
  rc = 1
579
579
  else:
580
580
  log(f"hermes: in sync ({hcfg.relative_to(REPO_ROOT)})")
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "PER-DEV LOCAL OVERRIDES — copy this file to .agents/local.json (gitignored) and edit. Lets you opt out of individual hooks or whole injected agents without touching committed config. Hooks self-skip at RUNTIME via .agents/hooks/lib/hook-guard.sh (so even Claude's committed hooks honor it); codex/hermes injections additionally skip at INSTALL time. Re-run `mise run hooks-sync` (or re-cd into the repo) after editing.",
2
+ "$comment": "PER-DEV LOCAL OVERRIDES — copy this file to .agents/local.json (gitignored) and edit. Lets you opt out of individual hooks or whole injected agents without touching committed config. Hooks self-skip at RUNTIME via .agents/hooks/lib/hook-guard.sh (so even Claude's committed hooks honor it); codex/hermes injections additionally skip at INSTALL time. Re-run `mise run hooks:sync` (or re-cd into the repo) after editing.",
3
3
  "hooks": {
4
4
  "$disabled_help": "Hook ids from .agents/hooks/hooks.master.json: skill-check-reminder, hindsight-recall, hindsight-retain, hindsight-session-end. Listed ids are skipped at runtime across ALL agents.",
5
5
  "disabled": [],
@@ -11,7 +11,7 @@
11
11
  # .env untouched. Never prints the secret.
12
12
  #
13
13
  # Override the 1Password reference if the item path differs:
14
- # HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight-setup
14
+ # HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight:setup
15
15
  set -euo pipefail
16
16
 
17
17
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"