@delorenj/pjangler 1.2.2 → 1.2.3

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/mcp-server.ts
4
4
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
5
- import { basename as basename4, dirname as dirname8, join as join11, resolve as resolve3 } from "node:path";
5
+ import { basename as basename4, dirname as dirname8, join as join12, resolve as resolve3 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -44,6 +44,63 @@ var Command = class {
44
44
  }
45
45
  };
46
46
 
47
+ // src/utils/style.ts
48
+ var env = process.env;
49
+ function detectColor() {
50
+ if ("NO_COLOR" in env && env.NO_COLOR !== "") return false;
51
+ const force = env.FORCE_COLOR;
52
+ if (force === "0" || force === "false") return false;
53
+ if (force !== void 0 && force !== "") return true;
54
+ if (env.TERM === "dumb") return false;
55
+ return Boolean(process.stdout.isTTY);
56
+ }
57
+ var colorEnabled = detectColor();
58
+ function sgr(open, close) {
59
+ const prefix = `\x1B[${open}m`;
60
+ const suffix = `\x1B[${close}m`;
61
+ return (value) => colorEnabled ? `${prefix}${value}${suffix}` : String(value);
62
+ }
63
+ var bold = sgr(1, 22);
64
+ var dim = sgr(2, 22);
65
+ var italic = sgr(3, 23);
66
+ var underline = sgr(4, 24);
67
+ var red = sgr(31, 39);
68
+ var green = sgr(32, 39);
69
+ var yellow = sgr(33, 39);
70
+ var blue = sgr(34, 39);
71
+ var magenta = sgr(35, 39);
72
+ var cyan = sgr(36, 39);
73
+ var gray = sgr(90, 39);
74
+ var glyph = {
75
+ pass: "\u2714",
76
+ fail: "\u2716",
77
+ warn: "\u26A0",
78
+ skip: "\u25CB",
79
+ info: "\u2139",
80
+ arrow: "\u21B3",
81
+ bullet: "\u2022",
82
+ dot: "\xB7",
83
+ add: "+",
84
+ chevron: "\u25B8",
85
+ pointer: "\u276F"
86
+ };
87
+ var STATUS_STYLES = {
88
+ pass: { glyph: glyph.pass, color: green, label: "pass" },
89
+ fail: { glyph: glyph.fail, color: red, label: "fail" },
90
+ warn: { glyph: glyph.warn, color: yellow, label: "warn" },
91
+ skip: { glyph: glyph.skip, color: gray, label: "skip" },
92
+ applied: { glyph: glyph.pass, color: green, label: "applied" },
93
+ noop: { glyph: glyph.skip, color: gray, label: "noop" },
94
+ blocked: { glyph: glyph.fail, color: red, label: "blocked" },
95
+ skipped: { glyph: glyph.skip, color: gray, label: "skipped" }
96
+ };
97
+ function statusStyle(status) {
98
+ return STATUS_STYLES[status] ?? { glyph: glyph.dot, color: dim, label: status };
99
+ }
100
+ function joinDot(fragments) {
101
+ return fragments.join(dim(` ${glyph.dot} `));
102
+ }
103
+
47
104
  // src/recipes/Recipe.ts
48
105
  var Recipe = class {
49
106
  context;
@@ -56,26 +113,22 @@ var Recipe = class {
56
113
  return this;
57
114
  }
58
115
  async execute() {
59
- const dryRunPrefix = this.context.dryRun ? "[DRY RUN] " : "";
60
- console.log(`${dryRunPrefix}\u{1F680} Initializing ${this.constructor.name.replace("Recipe", "").toLowerCase()} subsystem...`);
61
- if (this.context.dryRun) {
62
- console.log("\u26A0\uFE0F Dry-run mode: No files will be modified");
63
- console.log("");
64
- }
116
+ const subsystem = this.constructor.name.replace("Recipe", "").toLowerCase();
117
+ const dryRun = this.context.dryRun;
118
+ console.log("");
119
+ console.log(` ${cyan(bold(glyph.chevron))} ${bold(`Initializing ${subsystem} subsystem`)}${dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
120
+ console.log("");
65
121
  for (const command of this.ingredients) {
66
122
  const result = await command.invoke();
67
- if (result.success) {
68
- console.log(result.message);
69
- } else {
70
- console.log(result.message);
71
- }
123
+ console.log(result.message.split("\n").map((line) => line ? ` ${line}` : line).join("\n"));
72
124
  }
73
- if (!this.context.dryRun) {
125
+ if (!dryRun) {
74
126
  this.printNextSteps();
75
127
  } else {
76
128
  console.log("");
77
- console.log("\u2713 Dry-run complete - no files were modified");
78
- console.log(" Remove --dry-run flag to apply changes");
129
+ console.log(` ${green(glyph.pass)} ${dim("Dry-run complete \u2014 no files were modified.")}`);
130
+ console.log(` ${dim("Remove --dry-run to apply changes.")}`);
131
+ console.log("");
79
132
  }
80
133
  }
81
134
  };
@@ -218,11 +271,111 @@ if __name__ == "__main__":
218
271
  }
219
272
  };
220
273
 
274
+ // src/commands/AddMiseCodegraphScript.ts
275
+ import { chmodSync } from "fs";
276
+ import { join as join2 } from "path";
277
+ var AddMiseCodegraphScript = class extends Command {
278
+ async invoke() {
279
+ const filePath = ".mise/scripts/codegraph.sh";
280
+ if (this.fileExists(filePath) && !this.context.force) {
281
+ return {
282
+ success: false,
283
+ message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph.sh already exists"),
284
+ filePath
285
+ };
286
+ }
287
+ const content = `#!/usr/bin/env bash
288
+ # Mise enter hook: ensure the CodeGraph CLI is available and initialize the
289
+ # project index. If \`codegraph\` is not installed, this script installs it
290
+ # non-interactively into the project-local .mise/bin directory and retries.
291
+ #
292
+ # This is intended to run from a mise enter hook so onboarding a new host is
293
+ # fully automatic.
294
+
295
+ set -euo pipefail
296
+
297
+ REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
298
+ PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
299
+ mkdir -p "$PROJECT_BIN_DIR"
300
+
301
+ # Install the CodeGraph CLI into the project-local bin directory.
302
+ install_codegraph() {
303
+ echo "[mise] codegraph not found. Installing non-interactively..."
304
+ export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
305
+ curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
306
+
307
+ if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
308
+ export PATH="$PROJECT_BIN_DIR:$PATH"
309
+ else
310
+ echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
311
+ return 1
312
+ fi
313
+ }
314
+
315
+ # Ensure a codegraph binary is available on PATH.
316
+ ensure_codegraph() {
317
+ if command -v codegraph >/dev/null 2>&1; then
318
+ return 0
319
+ fi
320
+
321
+ # Check the project-local bin dir first (previous install from this hook).
322
+ if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
323
+ export PATH="$PROJECT_BIN_DIR:$PATH"
324
+ return 0
325
+ fi
326
+
327
+ # Check typical user-level install locations before fetching anything.
328
+ for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
329
+ if [ -x "$d/codegraph" ]; then
330
+ export PATH="$d:$PATH"
331
+ return 0
332
+ fi
333
+ done
334
+
335
+ install_codegraph
336
+ }
337
+
338
+ # Attempt to initialize the project graph. If the command is missing, install
339
+ # it and retry once.
340
+ init_project() {
341
+ local err_file
342
+ err_file="$(mktemp)"
343
+ trap 'rm -f "$err_file"' RETURN
344
+
345
+ if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
346
+ return 0
347
+ fi
348
+
349
+ # If the failure looks like a missing binary, install and retry.
350
+ if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
351
+ ensure_codegraph
352
+ codegraph init -i "$REPO_ROOT"
353
+ return 0
354
+ fi
355
+
356
+ cat "$err_file" >&2
357
+ return 1
358
+ }
359
+
360
+ init_project
361
+ `;
362
+ this.writeFile(filePath, content);
363
+ if (!this.context.dryRun) {
364
+ chmodSync(join2(this.context.targetDir, filePath), 493);
365
+ }
366
+ return {
367
+ success: true,
368
+ message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
369
+ filePath
370
+ };
371
+ }
372
+ };
373
+
221
374
  // src/recipes/MiseRecipe.ts
222
375
  var MiseRecipe = class extends Recipe {
223
376
  constructor(context) {
224
377
  super(context);
225
- this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript);
378
+ this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
226
379
  }
227
380
  printNextSteps() {
228
381
  console.log("\u{1F389} Mise subsystem initialized successfully!");
@@ -452,19 +605,19 @@ var NodeRecipe = class extends Recipe {
452
605
  // src/commands/hermes/EnsureTemplateConfig.ts
453
606
  import { homedir, platform } from "node:os";
454
607
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
455
- import { join as join2, dirname as dirname2 } from "node:path";
608
+ import { join as join3, dirname as dirname2 } from "node:path";
456
609
  function resolveTemplateConfigPath() {
457
610
  const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
458
611
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
459
612
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
460
- const base = xdg && xdg.length ? xdg : join2(homedir(), ".config");
461
- return join2(base, "hermes-agent-template", "config.toml");
613
+ const base = xdg && xdg.length ? xdg : join3(homedir(), ".config");
614
+ return join3(base, "hermes-agent-template", "config.toml");
462
615
  }
463
616
  function detectHermesBin(home) {
464
617
  const candidates = [
465
- join2(home, "code", "hermes-agent", "venv", "bin", "hermes"),
466
- join2(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
467
- join2(home, ".local", "bin", "hermes")
618
+ join3(home, "code", "hermes-agent", "venv", "bin", "hermes"),
619
+ join3(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
620
+ join3(home, ".local", "bin", "hermes")
468
621
  ];
469
622
  for (const c of candidates) {
470
623
  if (existsSync2(c)) return c;
@@ -474,9 +627,9 @@ function detectHermesBin(home) {
474
627
  function renderHostConfig() {
475
628
  const home = homedir();
476
629
  const hermesBin = detectHermesBin(home);
477
- const hermesRepo = join2(home, "code", "hermes-agent");
478
- const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
479
- const skillsDir = join2(home, ".agents", "skills");
630
+ const hermesRepo = join3(home, "code", "hermes-agent");
631
+ const scaffoldDir = join3(home, "code", "hermes-agent-template", "runtime-scaffold");
632
+ const skillsDir = join3(home, ".agents", "skills");
480
633
  return `# hermes-agent-template \u2014 host configuration
481
634
  # Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
482
635
  #
@@ -543,7 +696,7 @@ var EnsureTemplateConfig = class extends Command {
543
696
  };
544
697
 
545
698
  // src/commands/hermes/PromptForAgentConfig.ts
546
- import { basename, join as join3 } from "node:path";
699
+ import { basename, join as join4 } from "node:path";
547
700
  import { readFileSync } from "node:fs";
548
701
  import * as p from "@clack/prompts";
549
702
 
@@ -572,7 +725,7 @@ function deriveProfileName(repo, role) {
572
725
  // src/commands/hermes/PromptForAgentConfig.ts
573
726
  function detectTicketProvider(targetDir) {
574
727
  try {
575
- const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
728
+ const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
576
729
  return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
577
730
  } catch {
578
731
  return void 0;
@@ -708,7 +861,7 @@ var PromptForAgentConfig = class extends Command {
708
861
  // src/commands/hermes/RunCopierTemplate.ts
709
862
  import { spawnSync } from "node:child_process";
710
863
  import { homedir as homedir2 } from "node:os";
711
- import { join as join4, dirname as dirname3 } from "node:path";
864
+ import { join as join5, dirname as dirname3 } from "node:path";
712
865
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
713
866
  import { fileURLToPath } from "node:url";
714
867
  import * as p2 from "@clack/prompts";
@@ -720,8 +873,8 @@ function resolveVendoredTemplate(name) {
720
873
  return void 0;
721
874
  }
722
875
  for (let i = 0; i < 8; i++) {
723
- const candidate = join4(dir, "templates", name);
724
- if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
876
+ const candidate = join5(dir, "templates", name);
877
+ if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
725
878
  const parent = dirname3(dir);
726
879
  if (parent === dir) break;
727
880
  dir = parent;
@@ -740,7 +893,7 @@ var RunCopierTemplate = class extends Command {
740
893
  message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
741
894
  };
742
895
  }
743
- const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
896
+ const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
744
897
  ctx.roleDir = roleDir;
745
898
  ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
746
899
  const which = spawnSync("which", ["copier"], { encoding: "utf8" });
@@ -750,7 +903,7 @@ var RunCopierTemplate = class extends Command {
750
903
  message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
751
904
  };
752
905
  }
753
- if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
906
+ if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
754
907
  if (ctx.yes) {
755
908
  ctx.force = true;
756
909
  } else {
@@ -767,7 +920,7 @@ var RunCopierTemplate = class extends Command {
767
920
  ctx.force = true;
768
921
  }
769
922
  }
770
- const env = {
923
+ const env2 = {
771
924
  ...process.env,
772
925
  SKIP_TELEGRAM: "1",
773
926
  SKIP_EMAIL: "1",
@@ -777,9 +930,9 @@ var RunCopierTemplate = class extends Command {
777
930
  SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
778
931
  SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
779
932
  };
780
- const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
933
+ const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
781
934
  const vendored = resolveVendoredTemplate("hermes-agent");
782
- const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join4(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
935
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
783
936
  const args = [
784
937
  "copy",
785
938
  templateSrc,
@@ -810,13 +963,13 @@ var RunCopierTemplate = class extends Command {
810
963
  message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
811
964
  };
812
965
  }
813
- mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
966
+ mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
814
967
  const spinner4 = p2.spinner();
815
968
  spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
816
969
  const result = spawnSync("copier", args, {
817
970
  stdio: "inherit",
818
971
  // pass the interactive output through; copier prints its own progress
819
- env,
972
+ env: env2,
820
973
  cwd: ctx.targetDir
821
974
  });
822
975
  spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
@@ -835,7 +988,7 @@ var RunCopierTemplate = class extends Command {
835
988
 
836
989
  // src/commands/hermes/WireTelegram.ts
837
990
  import { spawnSync as spawnSync2 } from "node:child_process";
838
- import { join as join5 } from "node:path";
991
+ import { join as join6 } from "node:path";
839
992
  import { existsSync as existsSync4, unlinkSync } from "node:fs";
840
993
  import * as p3 from "@clack/prompts";
841
994
  var WireTelegram = class extends Command {
@@ -924,14 +1077,14 @@ var WireTelegram = class extends Command {
924
1077
  if (p3.isCancel(allowedAnswer)) {
925
1078
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
926
1079
  }
927
- const script = join5(roleDir, ".scripts", "30-telegram.sh");
1080
+ const script = join6(roleDir, ".scripts", "30-telegram.sh");
928
1081
  if (!existsSync4(script)) {
929
1082
  return {
930
1083
  success: false,
931
1084
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
932
1085
  };
933
1086
  }
934
- const marker = join5(roleDir, ".scripts", ".done-30-telegram");
1087
+ const marker = join6(roleDir, ".scripts", ".done-30-telegram");
935
1088
  if (existsSync4(marker)) unlinkSync(marker);
936
1089
  const spinner4 = p3.spinner();
937
1090
  spinner4.start("Verifying token + wiring profile");
@@ -959,7 +1112,7 @@ function cap(s) {
959
1112
 
960
1113
  // src/commands/hermes/WireEmail.ts
961
1114
  import { spawnSync as spawnSync3 } from "node:child_process";
962
- import { join as join6 } from "node:path";
1115
+ import { join as join7 } from "node:path";
963
1116
  import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
964
1117
  import * as p4 from "@clack/prompts";
965
1118
  var WireEmail = class extends Command {
@@ -975,7 +1128,7 @@ var WireEmail = class extends Command {
975
1128
  if (!targetRepo || !role || !roleDir) {
976
1129
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
977
1130
  }
978
- const script = join6(roleDir, ".scripts", "50-email.sh");
1131
+ const script = join7(roleDir, ".scripts", "50-email.sh");
979
1132
  if (!existsSync5(script)) {
980
1133
  return { success: false, message: `\u2717 ${script} not found` };
981
1134
  }
@@ -1038,7 +1191,7 @@ var WireEmail = class extends Command {
1038
1191
  }
1039
1192
  }
1040
1193
  }
1041
- const marker = join6(roleDir, ".scripts", ".done-50-email");
1194
+ const marker = join7(roleDir, ".scripts", ".done-50-email");
1042
1195
  if (existsSync5(marker)) unlinkSync2(marker);
1043
1196
  const spinner4 = p4.spinner();
1044
1197
  spinner4.start("Creating Cloudflare Email Routing rule");
@@ -1127,7 +1280,7 @@ var HermesAgentRecipe = class extends Recipe {
1127
1280
 
1128
1281
  // src/commands/AgentHooksCommands.ts
1129
1282
  import { homedir as homedir3 } from "node:os";
1130
- import { join as join7, dirname as dirname4 } from "node:path";
1283
+ import { join as join8, dirname as dirname4 } from "node:path";
1131
1284
  import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
1132
1285
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1133
1286
  function resolveTemplateRoot() {
@@ -1138,16 +1291,16 @@ function resolveTemplateRoot() {
1138
1291
  try {
1139
1292
  let dir = dirname4(fileURLToPath2(import.meta.url));
1140
1293
  for (let i = 0; i < 8; i++) {
1141
- candidates.push(join7(dir, "templates", "commonproject", "template"));
1294
+ candidates.push(join8(dir, "templates", "commonproject", "template"));
1142
1295
  const parent = dirname4(dir);
1143
1296
  if (parent === dir) break;
1144
1297
  dir = parent;
1145
1298
  }
1146
1299
  } catch {
1147
1300
  }
1148
- candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1301
+ candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1149
1302
  for (const c of candidates) {
1150
- if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
1303
+ if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
1151
1304
  }
1152
1305
  throw new Error(
1153
1306
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1171,8 +1324,8 @@ var CopyAgentHooksTree = class extends Command {
1171
1324
  const created = [];
1172
1325
  const skipped = [];
1173
1326
  for (const { rel, dir } of items) {
1174
- const src = join7(templateRoot, rel);
1175
- const dest = join7(this.context.targetDir, rel);
1327
+ const src = join8(templateRoot, rel);
1328
+ const dest = join8(this.context.targetDir, rel);
1176
1329
  if (!existsSync6(src)) continue;
1177
1330
  if (existsSync6(dest) && !this.context.force) {
1178
1331
  skipped.push(rel);
@@ -1197,7 +1350,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1197
1350
  static CR = "{{config_root}}";
1198
1351
  // mise's own runtime var — emitted literally
1199
1352
  async invoke() {
1200
- const misePath = join7(this.context.targetDir, "mise.toml");
1353
+ const misePath = join8(this.context.targetDir, "mise.toml");
1201
1354
  if (!existsSync6(misePath)) {
1202
1355
  return {
1203
1356
  success: false,
@@ -1318,7 +1471,7 @@ var RECIPE_REGISTRY = {
1318
1471
  name: "mise",
1319
1472
  description: "Mise task runner and environment setup",
1320
1473
  class: MiseRecipe,
1321
- commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
1474
+ commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
1322
1475
  },
1323
1476
  docker: {
1324
1477
  name: "docker",
@@ -1408,6 +1561,12 @@ var COMMAND_REGISTRY = {
1408
1561
  group: "mise",
1409
1562
  class: AddMiseBaseScript
1410
1563
  },
1564
+ AddMiseCodegraphScript: {
1565
+ name: "AddMiseCodegraphScript",
1566
+ description: "Create .mise/scripts/codegraph.sh enter hook",
1567
+ group: "mise",
1568
+ class: AddMiseCodegraphScript
1569
+ },
1411
1570
  AddDotenv: {
1412
1571
  name: "AddDotenv",
1413
1572
  description: "Create .env.example file",
@@ -1429,14 +1588,14 @@ function createRecipe(name, context) {
1429
1588
 
1430
1589
  // src/utils/version.ts
1431
1590
  import { readFileSync as readFileSync3 } from "node:fs";
1432
- import { dirname as dirname5, join as join8 } from "node:path";
1591
+ import { dirname as dirname5, join as join9 } from "node:path";
1433
1592
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1434
1593
  var PJANGLER_VERSION = (() => {
1435
1594
  try {
1436
1595
  let dir = dirname5(fileURLToPath3(import.meta.url));
1437
1596
  for (let i = 0; i < 4; i++) {
1438
1597
  try {
1439
- const raw = readFileSync3(join8(dir, "package.json"), "utf8");
1598
+ const raw = readFileSync3(join9(dir, "package.json"), "utf8");
1440
1599
  return JSON.parse(raw).version ?? "0.0.0";
1441
1600
  } catch {
1442
1601
  const parent = dirname5(dir);
@@ -1450,8 +1609,8 @@ var PJANGLER_VERSION = (() => {
1450
1609
  })();
1451
1610
 
1452
1611
  // src/parity/index.ts
1453
- import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
1454
- import { basename as basename2, dirname as dirname6, join as join9, relative, resolve } from "node:path";
1612
+ import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2, copyFileSync } from "node:fs";
1613
+ import { basename as basename2, dirname as dirname6, join as join10, relative, resolve } from "node:path";
1455
1614
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1456
1615
  import { homedir as homedir4 } from "node:os";
1457
1616
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -1525,7 +1684,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
1525
1684
  function resolvePjanglerRoot() {
1526
1685
  let dir = dirname6(fileURLToPath4(import.meta.url));
1527
1686
  while (dir !== dirname6(dir)) {
1528
- if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
1687
+ if (existsSync7(join10(dir, "package.json")) && existsSync7(join10(dir, "templates", "commonproject", "copier.yml"))) {
1529
1688
  return dir;
1530
1689
  }
1531
1690
  dir = dirname6(dir);
@@ -1588,10 +1747,10 @@ function ensureSymlink(path, target, dryRun) {
1588
1747
  return { changed: true };
1589
1748
  }
1590
1749
  function bootstrapAgentsFile(repoRoot, dryRun) {
1591
- const agentsPath = join9(repoRoot, "AGENTS.md");
1750
+ const agentsPath = join10(repoRoot, "AGENTS.md");
1592
1751
  if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
1593
1752
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1594
- const source = join9(repoRoot, file);
1753
+ const source = join10(repoRoot, file);
1595
1754
  if (!existsSync7(source)) continue;
1596
1755
  const stat = lstatSync(source);
1597
1756
  if (stat.isSymbolicLink()) continue;
@@ -1601,7 +1760,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1601
1760
  }
1602
1761
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
1603
1762
  }
1604
- const readmePath = join9(repoRoot, "README.md");
1763
+ const readmePath = join10(repoRoot, "README.md");
1605
1764
  if (existsSync7(readmePath)) {
1606
1765
  const stat = lstatSync(readmePath);
1607
1766
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
@@ -1641,11 +1800,11 @@ function yamlGet(text3, keyPath) {
1641
1800
  return "";
1642
1801
  }
1643
1802
  function discoverRoles(repoRoot) {
1644
- const rolesDir = join9(repoRoot, "agents", "hermes");
1803
+ const rolesDir = join10(repoRoot, "agents", "hermes");
1645
1804
  if (!existsSync7(rolesDir)) return [];
1646
1805
  return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
1647
- const roleDir = join9(rolesDir, entry.name);
1648
- const roleYamlPath = join9(roleDir, "role.yaml");
1806
+ const roleDir = join10(rolesDir, entry.name);
1807
+ const roleYamlPath = join10(roleDir, "role.yaml");
1649
1808
  if (!existsSync7(roleYamlPath)) return null;
1650
1809
  const text3 = readText(roleYamlPath);
1651
1810
  const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
@@ -1670,7 +1829,7 @@ function discoverRoles(repoRoot) {
1670
1829
  }).filter((value) => Boolean(value));
1671
1830
  }
1672
1831
  function registryPath(homeDir) {
1673
- return join9(homeDir, ".hermes", "agents-registry.yaml");
1832
+ return join10(homeDir, ".hermes", "agents-registry.yaml");
1674
1833
  }
1675
1834
  function systemctlUser(args) {
1676
1835
  const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -1681,7 +1840,7 @@ function systemctlUser(args) {
1681
1840
  };
1682
1841
  }
1683
1842
  function templateScript(ctx, name) {
1684
- const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
1843
+ const source = join10(ctx.pjanglerRoot, ".mise", "scripts", name);
1685
1844
  return existsSync7(source) ? readText(source) : void 0;
1686
1845
  }
1687
1846
  function templateVersioningScript(ctx) {
@@ -1696,9 +1855,9 @@ function renderGeneratedProjectMiseToml(ctx, template) {
1696
1855
  return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
1697
1856
  }
1698
1857
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1699
- const targetPath = join9(ctx.repoRoot, "mise.toml");
1858
+ const targetPath = join10(ctx.repoRoot, "mise.toml");
1700
1859
  if (existsSync7(targetPath)) return false;
1701
- const sourcePath = join9(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1860
+ const sourcePath = join10(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1702
1861
  if (!existsSync7(sourcePath)) return false;
1703
1862
  changedFiles.push(targetPath);
1704
1863
  if (!ctx.dryRun) {
@@ -1707,7 +1866,7 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1707
1866
  return true;
1708
1867
  }
1709
1868
  function templateVersionFilesConf(ctx, repoRoot) {
1710
- const packageJson = join9(repoRoot, "package.json");
1869
+ const packageJson = join10(repoRoot, "package.json");
1711
1870
  return existsSync7(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
1712
1871
  }
1713
1872
  function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
@@ -1732,7 +1891,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
1732
1891
  function requiredMisePathEntries(ctx) {
1733
1892
  const required = [...BASE_MISE_PATH_ENTRIES];
1734
1893
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
1735
- if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1894
+ if (existsSync7(join10(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1736
1895
  }
1737
1896
  return required;
1738
1897
  }
@@ -1881,7 +2040,7 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
1881
2040
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
1882
2041
  }
1883
2042
  function readProjectJson(ctx) {
1884
- return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
2043
+ return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
1885
2044
  }
1886
2045
  function canonicalProjectJson(ctx) {
1887
2046
  const roles = discoverRoles(ctx.repoRoot);
@@ -1925,8 +2084,8 @@ function canonicalProjectJson(ctx) {
1925
2084
  };
1926
2085
  }
1927
2086
  function projectJsonFinding(ctx) {
1928
- const projectPath = join9(ctx.repoRoot, ".project.json");
1929
- const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
2087
+ const projectPath = join10(ctx.repoRoot, ".project.json");
2088
+ const planeJsonPath = join10(ctx.repoRoot, ".plane.json");
1930
2089
  const details = [];
1931
2090
  const data = readProjectJson(ctx);
1932
2091
  const roles = discoverRoles(ctx.repoRoot);
@@ -2040,9 +2199,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2040
2199
  if (!existsSync7(sourceDir)) return;
2041
2200
  mkdirSync5(targetDir, { recursive: true });
2042
2201
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2043
- const sourcePath = join9(sourceDir, entry.name);
2202
+ const sourcePath = join10(sourceDir, entry.name);
2044
2203
  if (skip?.(sourcePath)) continue;
2045
- const targetPath = join9(targetDir, entry.name);
2204
+ const targetPath = join10(targetDir, entry.name);
2046
2205
  if (entry.isDirectory()) {
2047
2206
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2048
2207
  continue;
@@ -2056,7 +2215,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2056
2215
  }
2057
2216
  }
2058
2217
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2059
- const gitmodulesPath = join9(repoRoot, ".gitmodules");
2218
+ const gitmodulesPath = join10(repoRoot, ".gitmodules");
2060
2219
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2061
2220
  const owner = role.runtimeOwner || "delorenj";
2062
2221
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2156,13 +2315,13 @@ var RULES = [
2156
2315
  id: "mise.config-root",
2157
2316
  title: "mise config_root + AGENTS link hooks",
2158
2317
  audit: (ctx) => {
2159
- const misePath = join9(ctx.repoRoot, "mise.toml");
2318
+ const misePath = join10(ctx.repoRoot, "mise.toml");
2160
2319
  if (!existsSync7(misePath)) {
2161
2320
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2162
2321
  }
2163
2322
  const text3 = readText(misePath);
2164
2323
  const details = [];
2165
- const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2324
+ const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2166
2325
  if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2167
2326
  const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2168
2327
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
@@ -2181,7 +2340,7 @@ var RULES = [
2181
2340
  };
2182
2341
  },
2183
2342
  migrate: (ctx, finding) => {
2184
- const path = join9(ctx.repoRoot, "mise.toml");
2343
+ const path = join10(ctx.repoRoot, "mise.toml");
2185
2344
  const changedFiles = [];
2186
2345
  const details = [];
2187
2346
  if (!existsSync7(path)) {
@@ -2200,7 +2359,7 @@ var RULES = [
2200
2359
  if (!ctx.dryRun) writeText(path, next);
2201
2360
  text3 = next;
2202
2361
  }
2203
- const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2362
+ const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2204
2363
  const expectedScript = templateLinkAgentfilesScript(ctx);
2205
2364
  if (expectedScript === void 0) {
2206
2365
  return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
@@ -2209,7 +2368,7 @@ var RULES = [
2209
2368
  changedFiles.push(linkAgentfilesPath);
2210
2369
  if (!ctx.dryRun) {
2211
2370
  writeText(linkAgentfilesPath, expectedScript);
2212
- chmodSync(linkAgentfilesPath, 493);
2371
+ chmodSync2(linkAgentfilesPath, 493);
2213
2372
  }
2214
2373
  }
2215
2374
  return {
@@ -2227,9 +2386,9 @@ var RULES = [
2227
2386
  title: "managed mise versioning block",
2228
2387
  audit: (ctx) => {
2229
2388
  const details = [];
2230
- const misePath = join9(ctx.repoRoot, "mise.toml");
2231
- const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2232
- const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2389
+ const misePath = join10(ctx.repoRoot, "mise.toml");
2390
+ const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2391
+ const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
2233
2392
  const text3 = safeReadText(misePath);
2234
2393
  if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2235
2394
  if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
@@ -2246,7 +2405,7 @@ var RULES = [
2246
2405
  migrate: (ctx, finding) => {
2247
2406
  const changedFiles = [];
2248
2407
  const details = [];
2249
- const misePath = join9(ctx.repoRoot, "mise.toml");
2408
+ const misePath = join10(ctx.repoRoot, "mise.toml");
2250
2409
  if (!existsSync7(misePath)) {
2251
2410
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2252
2411
  return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
@@ -2262,7 +2421,7 @@ var RULES = [
2262
2421
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
2263
2422
  if (!ctx.dryRun) writeText(misePath, nextMise);
2264
2423
  }
2265
- const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2424
+ const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2266
2425
  const expectedScript = templateVersioningScript(ctx);
2267
2426
  if (expectedScript === void 0) {
2268
2427
  return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
@@ -2271,10 +2430,10 @@ var RULES = [
2271
2430
  changedFiles.push(versioningPath);
2272
2431
  if (!ctx.dryRun) {
2273
2432
  writeText(versioningPath, expectedScript);
2274
- chmodSync(versioningPath, 493);
2433
+ chmodSync2(versioningPath, 493);
2275
2434
  }
2276
2435
  }
2277
- const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2436
+ const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
2278
2437
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
2279
2438
  if (safeReadText(manifestPath) !== expectedManifest) {
2280
2439
  changedFiles.push(manifestPath);
@@ -2294,9 +2453,9 @@ var RULES = [
2294
2453
  id: "sot.agent-symlinks",
2295
2454
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
2296
2455
  audit: (ctx) => {
2297
- const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
2456
+ const agentsPath = join10(ctx.repoRoot, "AGENTS.md");
2298
2457
  if (!existsSync7(agentsPath)) {
2299
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
2458
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join10(ctx.repoRoot, file)));
2300
2459
  if (fallbackSources.length === 0) {
2301
2460
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
2302
2461
  }
@@ -2311,7 +2470,7 @@ var RULES = [
2311
2470
  }
2312
2471
  const details = [];
2313
2472
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2314
- const full = join9(ctx.repoRoot, file);
2473
+ const full = join10(ctx.repoRoot, file);
2315
2474
  const target = readSymlinkTarget(full);
2316
2475
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
2317
2476
  }
@@ -2335,7 +2494,7 @@ var RULES = [
2335
2494
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
2336
2495
  }
2337
2496
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2338
- const full = join9(ctx.repoRoot, file);
2497
+ const full = join10(ctx.repoRoot, file);
2339
2498
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
2340
2499
  if (result.blocked) blockedDetails.push(result.blocked);
2341
2500
  if (result.changed) changedFiles.push(full);
@@ -2357,7 +2516,7 @@ var RULES = [
2357
2516
  migrate: (ctx, finding) => {
2358
2517
  const changedFiles = [];
2359
2518
  const details = [];
2360
- const path = join9(ctx.repoRoot, ".project.json");
2519
+ const path = join10(ctx.repoRoot, ".project.json");
2361
2520
  const existing = readProjectJson(ctx) ?? {};
2362
2521
  const canonical = canonicalProjectJson(ctx);
2363
2522
  const merged = { ...existing, ...canonical };
@@ -2367,7 +2526,7 @@ var RULES = [
2367
2526
  changedFiles.push(path);
2368
2527
  if (!ctx.dryRun) writeText(path, expected);
2369
2528
  }
2370
- const planeJson = join9(ctx.repoRoot, ".plane.json");
2529
+ const planeJson = join10(ctx.repoRoot, ".plane.json");
2371
2530
  if (existsSync7(planeJson)) {
2372
2531
  const backup = `${planeJson}.migrated-backup`;
2373
2532
  if (existsSync7(backup)) {
@@ -2392,8 +2551,8 @@ var RULES = [
2392
2551
  title: ".env.op + gitignore secrets contract",
2393
2552
  audit: (ctx) => {
2394
2553
  const details = [];
2395
- const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
2396
- const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
2554
+ const envOp = safeReadText(join10(ctx.repoRoot, ".env.op"));
2555
+ const gitignore = safeReadText(join10(ctx.repoRoot, ".gitignore"));
2397
2556
  if (!envOp) {
2398
2557
  details.push(".env.op missing");
2399
2558
  } else {
@@ -2419,12 +2578,12 @@ var RULES = [
2419
2578
  migrate: (ctx, finding) => {
2420
2579
  const changedFiles = [];
2421
2580
  const details = [];
2422
- const envOpPath = join9(ctx.repoRoot, ".env.op");
2581
+ const envOpPath = join10(ctx.repoRoot, ".env.op");
2423
2582
  if (!existsSync7(envOpPath)) {
2424
2583
  changedFiles.push(envOpPath);
2425
- if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2584
+ if (!ctx.dryRun) writeText(envOpPath, readText(join10(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2426
2585
  }
2427
- const gitignorePath = join9(ctx.repoRoot, ".gitignore");
2586
+ const gitignorePath = join10(ctx.repoRoot, ".gitignore");
2428
2587
  const gitignore = safeReadText(gitignorePath) ?? "";
2429
2588
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
2430
2589
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -2451,7 +2610,7 @@ var RULES = [
2451
2610
  title: ".copier-answers.yml provenance + drift report",
2452
2611
  audit: (ctx) => {
2453
2612
  const details = [];
2454
- const path = join9(ctx.repoRoot, ".copier-answers.yml");
2613
+ const path = join10(ctx.repoRoot, ".copier-answers.yml");
2455
2614
  const text3 = safeReadText(path);
2456
2615
  const project = readProjectJson(ctx);
2457
2616
  if (!text3) {
@@ -2482,12 +2641,12 @@ var RULES = [
2482
2641
  const changedFiles = [];
2483
2642
  const project = canonicalProjectJson(ctx);
2484
2643
  const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2485
- _src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
2644
+ _src_path: ${join10(ctx.pjanglerRoot, "templates", "commonproject")}
2486
2645
  project_description: ${String(project.project_description)}
2487
2646
  project_name: ${String(project.project_name)}
2488
2647
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2489
2648
  `;
2490
- const path = join9(ctx.repoRoot, ".copier-answers.yml");
2649
+ const path = join10(ctx.repoRoot, ".copier-answers.yml");
2491
2650
  if (safeReadText(path) !== text3) {
2492
2651
  changedFiles.push(path);
2493
2652
  if (!ctx.dryRun) writeText(path, text3);
@@ -2506,15 +2665,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2506
2665
  id: "bmad.scaffold",
2507
2666
  title: "BMAD modules/docs scaffold",
2508
2667
  audit: (ctx) => {
2509
- const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2510
- const targetRoot = join9(ctx.repoRoot, "_bmad");
2668
+ const sourceRoot = join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2669
+ const targetRoot = join10(ctx.repoRoot, "_bmad");
2511
2670
  const sentinels = [
2512
- join9("core", "config.yaml"),
2513
- join9("custom", "config.yaml"),
2514
- join9("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2515
- join9("bmm", "workflows", "workflow-status", "workflow.yaml")
2671
+ join10("core", "config.yaml"),
2672
+ join10("custom", "config.yaml"),
2673
+ join10("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2674
+ join10("bmm", "workflows", "workflow-status", "workflow.yaml")
2516
2675
  ];
2517
- const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
2676
+ const missing = sentinels.filter((file) => existsSync7(join10(sourceRoot, file)) && !existsSync7(join10(targetRoot, file)));
2518
2677
  return {
2519
2678
  id: "bmad.scaffold",
2520
2679
  title: "BMAD modules/docs scaffold",
@@ -2526,7 +2685,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2526
2685
  },
2527
2686
  migrate: (ctx, finding) => {
2528
2687
  const changedFiles = [];
2529
- copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2688
+ copyMissingRecursive(join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join10(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2530
2689
  return {
2531
2690
  id: finding.id,
2532
2691
  title: finding.title,
@@ -2548,11 +2707,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2548
2707
  }
2549
2708
  const details = [];
2550
2709
  for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
2551
- if (!existsSync7(join9(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join9(role.roleDir, rel))}`);
2710
+ if (!existsSync7(join10(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join10(role.roleDir, rel))}`);
2552
2711
  }
2553
- const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
2712
+ const gitmodules = safeReadText(join10(ctx.repoRoot, ".gitmodules")) ?? "";
2554
2713
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
2555
- if (!profileMetaInheritsDefault(join9(role.roleDir, "runtime", "profile.yaml"))) {
2714
+ if (!profileMetaInheritsDefault(join10(role.roleDir, "runtime", "profile.yaml"))) {
2556
2715
  details.push("runtime/profile.yaml missing inherited default config metadata");
2557
2716
  }
2558
2717
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -2573,21 +2732,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2573
2732
  if (!role) {
2574
2733
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
2575
2734
  }
2576
- const templateRoleDir = join9(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2577
- writeIfDifferent(join9(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2578
- writeIfDifferent(join9(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2579
- writeIfDifferent(join9(role.roleDir, ".gitignore"), readText(join9(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2580
- copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2581
- copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2582
- copyMissingRecursive(join9(templateRoleDir, ".scripts"), join9(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2583
- const promptSource = join9(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2584
- const promptTarget = join9(role.roleDir, ".scripts", "sentinel.prompt.md");
2735
+ const templateRoleDir = join10(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2736
+ writeIfDifferent(join10(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2737
+ writeIfDifferent(join10(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2738
+ writeIfDifferent(join10(role.roleDir, ".gitignore"), readText(join10(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2739
+ copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2740
+ copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2741
+ copyMissingRecursive(join10(templateRoleDir, ".scripts"), join10(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2742
+ const promptSource = join10(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2743
+ const promptTarget = join10(role.roleDir, ".scripts", "sentinel.prompt.md");
2585
2744
  if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
2586
2745
  const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
2587
2746
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
2588
2747
  }
2589
2748
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2590
- const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2749
+ const profileMetaUpdated = upsertInheritedProfileMeta(join10(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2591
2750
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2592
2751
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2593
2752
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -2641,9 +2800,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2641
2800
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
2642
2801
  }
2643
2802
  for (const role of roles) {
2644
- const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
2803
+ const sysDir = join10(ctx.homeDir, ".config", "systemd", "user");
2645
2804
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
2646
- const allUnitsPresent = units.every((unit) => existsSync7(join9(sysDir, unit)));
2805
+ const allUnitsPresent = units.every((unit) => existsSync7(join10(sysDir, unit)));
2647
2806
  if (allUnitsPresent) {
2648
2807
  if (ctx.dryRun) {
2649
2808
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -2655,7 +2814,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2655
2814
  }
2656
2815
  continue;
2657
2816
  }
2658
- for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
2817
+ for (const script of [join10(role.roleDir, ".scripts", "70-systemd.sh")]) {
2659
2818
  if (!script || !existsSync7(script)) continue;
2660
2819
  if (ctx.dryRun) {
2661
2820
  details.push(`would run: bash ${script}`);
@@ -2683,7 +2842,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
2683
2842
  changedFiles.push(path);
2684
2843
  if (!dryRun) {
2685
2844
  writeText(path, normalized);
2686
- if (mode) chmodSync(path, mode);
2845
+ if (mode) chmodSync2(path, mode);
2687
2846
  }
2688
2847
  }
2689
2848
  function getParityRuleIds() {
@@ -2745,21 +2904,38 @@ function runMigration(selector, repoArg, dryRun, all) {
2745
2904
  const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
2746
2905
  return runMigrationForRules(ruleIds, repoArg, dryRun);
2747
2906
  }
2907
+ function prettyTimestamp(iso) {
2908
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
2909
+ return match ? `${match[1]} ${match[2]} UTC` : iso;
2910
+ }
2748
2911
  function formatAuditReport(report) {
2749
- const lines = [`repo: ${report.repo}`, `ok: ${report.ok}`, `audited_at: ${report.auditedAt}`, "rules:"];
2912
+ const counts = {};
2913
+ for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
2914
+ const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
2915
+ const tally = [];
2916
+ if (counts.pass) tally.push(green(`${counts.pass} passed`));
2917
+ if (counts.fail) tally.push(red(`${counts.fail} failed`));
2918
+ if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
2919
+ if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
2920
+ const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
2921
+ const lines = [""];
2922
+ lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
2923
+ lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
2924
+ lines.push("");
2750
2925
  for (const rule of report.rules) {
2751
- lines.push(`- ${rule.id} [${rule.status}] ${rule.summary}`);
2752
- for (const detail of rule.details) lines.push(` - ${detail}`);
2926
+ const style = statusStyle(rule.status);
2927
+ lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
2928
+ for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
2753
2929
  }
2754
- return `${lines.join("\n")}
2755
- `;
2930
+ lines.push("");
2931
+ return lines.join("\n");
2756
2932
  }
2757
2933
 
2758
2934
  // src/project/index.ts
2759
2935
  import { spawnSync as spawnSync5 } from "node:child_process";
2760
2936
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
2761
2937
  import { homedir as homedir5 } from "node:os";
2762
- import { basename as basename3, dirname as dirname7, join as join10, resolve as resolve2 } from "node:path";
2938
+ import { basename as basename3, dirname as dirname7, join as join11, resolve as resolve2 } from "node:path";
2763
2939
  import YAML from "yaml";
2764
2940
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
2765
2941
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
@@ -2767,10 +2943,10 @@ var KNOWN_SKILL_ROOTS = [
2767
2943
  "/home/delorenj/code/skillex/all-skills",
2768
2944
  "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
2769
2945
  "/home/delorenj/code/pjangler/.agents/skills",
2770
- join10(homedir5(), ".codex", "skills")
2946
+ join11(homedir5(), ".codex", "skills")
2771
2947
  ];
2772
- function projectRegistryPath(env = process.env) {
2773
- return expandHome(env[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
2948
+ function projectRegistryPath(env2 = process.env) {
2949
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir5(), ".config", "pjangler", "projects.yaml"));
2774
2950
  }
2775
2951
  function emptyProjectRegistry() {
2776
2952
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
@@ -2854,7 +3030,7 @@ function resolveSourceSkillPath(sourceSkill) {
2854
3030
  if (existsSync8(direct)) return direct;
2855
3031
  const name = basename3(sourceSkill);
2856
3032
  for (const root of KNOWN_SKILL_ROOTS) {
2857
- const candidate = join10(root, name);
3033
+ const candidate = join11(root, name);
2858
3034
  if (existsSync8(candidate)) return candidate;
2859
3035
  }
2860
3036
  const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
@@ -2934,7 +3110,7 @@ function planProjectInit(input) {
2934
3110
  }));
2935
3111
  }
2936
3112
  actions.push(
2937
- { kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
3113
+ { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
2938
3114
  {
2939
3115
  kind: "plane.create-or-link",
2940
3116
  enabled: live,
@@ -3046,7 +3222,7 @@ function getProject(registry, slug) {
3046
3222
  return project;
3047
3223
  }
3048
3224
  function buildCommonProjectCopierAction(input) {
3049
- const templateDir = join10(input.pjanglerRoot, "templates", "commonproject");
3225
+ const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
3050
3226
  const data = {
3051
3227
  project_name: input.projectName,
3052
3228
  project_description: input.projectDescription ?? "",
@@ -3072,7 +3248,7 @@ function buildCommonProjectCopierAction(input) {
3072
3248
  function resolvePjanglerRoot2() {
3073
3249
  let dir = dirname7(new URL(import.meta.url).pathname);
3074
3250
  while (dir !== dirname7(dir)) {
3075
- if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) return dir;
3251
+ if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
3076
3252
  dir = dirname7(dir);
3077
3253
  }
3078
3254
  return resolve2(process.cwd());
@@ -3104,7 +3280,7 @@ function validateProjectRecord(project, key) {
3104
3280
  }
3105
3281
  function expandHome(path) {
3106
3282
  if (path === "~") return homedir5();
3107
- if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
3283
+ if (path.startsWith("~/")) return join11(homedir5(), path.slice(2));
3108
3284
  return path;
3109
3285
  }
3110
3286
  function isRecord(value) {
@@ -3130,7 +3306,7 @@ function resolveTargetDir(targetDir) {
3130
3306
  function resolvePjanglerRoot3() {
3131
3307
  let dir = dirname8(fileURLToPath5(import.meta.url));
3132
3308
  while (dir !== dirname8(dir)) {
3133
- if (existsSync9(join11(dir, "package.json")) && existsSync9(join11(dir, "templates", "commonproject", "copier.yml"))) {
3309
+ if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
3134
3310
  return dir;
3135
3311
  }
3136
3312
  dir = dirname8(dir);
@@ -3330,7 +3506,7 @@ server.registerTool(
3330
3506
  const projectSlug = input.projectSlug ?? slugify(input.projectName);
3331
3507
  const parentDir = resolve3(input.parentDir ?? process.cwd());
3332
3508
  if (!existsSync9(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
3333
- const targetDir = resolve3(input.targetDir ?? join11(parentDir, projectSlug));
3509
+ const targetDir = resolve3(input.targetDir ?? join12(parentDir, projectSlug));
3334
3510
  const overwrite = input.overwrite ?? input.force ?? false;
3335
3511
  const dryRun = input.dryRun ?? true;
3336
3512
  const local = input.local ?? true;