@delorenj/pjangler 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,25 +696,12 @@ 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
 
550
703
  // src/commands/hermes/types.ts
551
704
  var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
552
- var SOUL_TONES = ["direct", "playful", "formal", "terse"];
553
- var ROLE_CHOICES = [
554
- { value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
555
- { value: "dev", label: "Developer (dev)", hint: "implements tickets" },
556
- { value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
557
- { value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
558
- { value: "qa", label: "QA (qa)", hint: "test authorship + verification" }
559
- ];
560
- var TICKET_PROVIDERS = [
561
- { value: "plane", label: "Plane", hint: "self-hosted at plane.delo.sh (default)" },
562
- { value: "linear", label: "Linear", hint: "team board (created in Linear UI)" },
563
- { value: "trello", label: "Trello", hint: "board = project" }
564
- ];
565
705
  function deriveAgentId(repo, role) {
566
706
  return `${repo}-${role}`.toLowerCase();
567
707
  }
@@ -572,7 +712,7 @@ function deriveProfileName(repo, role) {
572
712
  // src/commands/hermes/PromptForAgentConfig.ts
573
713
  function detectTicketProvider(targetDir) {
574
714
  try {
575
- const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
715
+ const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
576
716
  return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
577
717
  } catch {
578
718
  return void 0;
@@ -582,19 +722,18 @@ var PromptForAgentConfig = class extends Command {
582
722
  async invoke() {
583
723
  const ctx = this.context;
584
724
  const defaultRepo = basename(ctx.targetDir).toLowerCase();
585
- const defaultRole = "pm";
725
+ ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
726
+ ctx.role ??= "pm";
727
+ ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
728
+ ctx.soulTone ??= "direct";
729
+ ctx.modelProvider ??= "";
730
+ ctx.modelName ??= "";
731
+ ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
732
+ ctx.skipEmail ??= true;
733
+ ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
734
+ ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
586
735
  if (ctx.yes) {
587
- ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
588
- ctx.role ??= defaultRole;
589
- ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
590
- ctx.soulTone ??= "direct";
591
- ctx.modelProvider ??= "";
592
- ctx.modelName ??= "";
593
- ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
594
736
  ctx.skipTelegram ??= true;
595
- ctx.skipEmail ??= true;
596
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
597
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
598
737
  return {
599
738
  success: true,
600
739
  message: this.formatMessage(
@@ -602,96 +741,19 @@ var PromptForAgentConfig = class extends Command {
602
741
  )
603
742
  };
604
743
  }
605
- p.intro("\u2695 hermes-agent \xB7 add a new agent role to this repo");
606
- if (!ctx.targetRepo) {
607
- const answer = await p.text({
608
- message: "Target repo name",
609
- placeholder: defaultRepo,
610
- initialValue: defaultRepo,
611
- validate: (v) => v && v.trim() ? void 0 : "required"
612
- });
613
- if (p.isCancel(answer)) return this.cancelled();
614
- ctx.targetRepo = String(answer).trim().toLowerCase();
615
- }
616
- if (!ctx.role) {
617
- const answer = await p.select({
618
- message: "Role",
619
- options: ROLE_CHOICES.map((r) => ({ value: r.value, label: r.label, hint: r.hint })),
620
- initialValue: defaultRole
621
- });
622
- if (p.isCancel(answer)) return this.cancelled();
623
- ctx.role = String(answer).trim();
624
- }
625
- if (ctx.ticketProvider === void 0) {
626
- const detected = detectTicketProvider(ctx.targetDir);
627
- const answer = await p.select({
628
- message: "Ticket board provider",
629
- options: TICKET_PROVIDERS.map((t) => ({
630
- value: t.value,
631
- label: t.label,
632
- hint: t.value === detected ? `${t.hint} \u2014 current .project.json` : t.hint
633
- })),
634
- initialValue: detected ?? "plane"
635
- });
636
- if (p.isCancel(answer)) return this.cancelled();
637
- ctx.ticketProvider = answer;
638
- }
639
- if (!ctx.agentPurpose) {
640
- const answer = await p.text({
641
- message: "One-line purpose",
642
- placeholder: `${ctx.role} agent for ${ctx.targetRepo}`,
643
- initialValue: `${ctx.role} agent for ${ctx.targetRepo}`
644
- });
645
- if (p.isCancel(answer)) return this.cancelled();
646
- ctx.agentPurpose = String(answer).trim();
647
- }
648
- if (!ctx.soulTone) {
649
- const answer = await p.select({
650
- message: "Personality tone",
651
- options: SOUL_TONES.map((t) => ({
652
- value: t,
653
- label: t,
654
- hint: t === "direct" ? "decision-forward, no preamble (default)" : t === "terse" ? "minimum words, conclusion-first" : t === "playful" ? "warm, mildly funny" : "precise, structured"
655
- })),
656
- initialValue: "direct"
657
- });
658
- if (p.isCancel(answer)) return this.cancelled();
659
- ctx.soulTone = answer;
660
- }
661
- if (ctx.modelProvider === void 0) {
662
- const answer = await p.text({
663
- message: "Provider override (empty = inherit shared default profile)",
664
- placeholder: ""
665
- });
666
- if (p.isCancel(answer)) return this.cancelled();
667
- ctx.modelProvider = String(answer).trim();
668
- }
669
- if (ctx.modelName === void 0) {
670
- const answer = await p.text({
671
- message: "Model name override (empty = inherit shared default profile)",
672
- placeholder: ""
673
- });
674
- if (p.isCancel(answer)) return this.cancelled();
675
- ctx.modelName = String(answer).trim();
676
- }
744
+ p.intro("\u2695 hermes-agent \xB7 provision the PM agent for this repo");
745
+ p.log.info(
746
+ `agent ${ctx.agentId} \xB7 board ${ctx.ticketProvider} \xB7 tone ${ctx.soulTone}`
747
+ );
677
748
  if (ctx.skipTelegram === void 0) {
749
+ const botHandle = `${ctx.targetRepo.replace(/-/g, "_")}_${ctx.role}_bot`;
678
750
  const wire = await p.confirm({
679
- message: `Wire up the Telegram bot (@${ctx.targetRepo}_${ctx.role}_bot) now?`,
751
+ message: `Wire up the Telegram bot (@${botHandle}) now?`,
680
752
  initialValue: true
681
753
  });
682
754
  if (p.isCancel(wire)) return this.cancelled();
683
755
  ctx.skipTelegram = !wire;
684
756
  }
685
- if (ctx.skipEmail === void 0) {
686
- const wire = await p.confirm({
687
- message: `Provision the delo.sh email address (${ctx.targetRepo}-${ctx.role}@delo.sh) now?`,
688
- initialValue: true
689
- });
690
- if (p.isCancel(wire)) return this.cancelled();
691
- ctx.skipEmail = !wire;
692
- }
693
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
694
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
695
757
  return {
696
758
  success: true,
697
759
  message: this.formatMessage(
@@ -708,7 +770,7 @@ var PromptForAgentConfig = class extends Command {
708
770
  // src/commands/hermes/RunCopierTemplate.ts
709
771
  import { spawnSync } from "node:child_process";
710
772
  import { homedir as homedir2 } from "node:os";
711
- import { join as join4, dirname as dirname3 } from "node:path";
773
+ import { join as join5, dirname as dirname3 } from "node:path";
712
774
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
713
775
  import { fileURLToPath } from "node:url";
714
776
  import * as p2 from "@clack/prompts";
@@ -720,8 +782,8 @@ function resolveVendoredTemplate(name) {
720
782
  return void 0;
721
783
  }
722
784
  for (let i = 0; i < 8; i++) {
723
- const candidate = join4(dir, "templates", name);
724
- if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
785
+ const candidate = join5(dir, "templates", name);
786
+ if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
725
787
  const parent = dirname3(dir);
726
788
  if (parent === dir) break;
727
789
  dir = parent;
@@ -740,7 +802,7 @@ var RunCopierTemplate = class extends Command {
740
802
  message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
741
803
  };
742
804
  }
743
- const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
805
+ const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
744
806
  ctx.roleDir = roleDir;
745
807
  ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
746
808
  const which = spawnSync("which", ["copier"], { encoding: "utf8" });
@@ -750,7 +812,7 @@ var RunCopierTemplate = class extends Command {
750
812
  message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
751
813
  };
752
814
  }
753
- if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
815
+ if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
754
816
  if (ctx.yes) {
755
817
  ctx.force = true;
756
818
  } else {
@@ -767,7 +829,7 @@ var RunCopierTemplate = class extends Command {
767
829
  ctx.force = true;
768
830
  }
769
831
  }
770
- const env = {
832
+ const env2 = {
771
833
  ...process.env,
772
834
  SKIP_TELEGRAM: "1",
773
835
  SKIP_EMAIL: "1",
@@ -777,9 +839,9 @@ var RunCopierTemplate = class extends Command {
777
839
  SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
778
840
  SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
779
841
  };
780
- const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
842
+ const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
781
843
  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);
844
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
783
845
  const args = [
784
846
  "copy",
785
847
  templateSrc,
@@ -810,13 +872,13 @@ var RunCopierTemplate = class extends Command {
810
872
  message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
811
873
  };
812
874
  }
813
- mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
875
+ mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
814
876
  const spinner4 = p2.spinner();
815
877
  spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
816
878
  const result = spawnSync("copier", args, {
817
879
  stdio: "inherit",
818
880
  // pass the interactive output through; copier prints its own progress
819
- env,
881
+ env: env2,
820
882
  cwd: ctx.targetDir
821
883
  });
822
884
  spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
@@ -835,7 +897,7 @@ var RunCopierTemplate = class extends Command {
835
897
 
836
898
  // src/commands/hermes/WireTelegram.ts
837
899
  import { spawnSync as spawnSync2 } from "node:child_process";
838
- import { join as join5 } from "node:path";
900
+ import { join as join6 } from "node:path";
839
901
  import { existsSync as existsSync4, unlinkSync } from "node:fs";
840
902
  import * as p3 from "@clack/prompts";
841
903
  var WireTelegram = class extends Command {
@@ -924,14 +986,14 @@ var WireTelegram = class extends Command {
924
986
  if (p3.isCancel(allowedAnswer)) {
925
987
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
926
988
  }
927
- const script = join5(roleDir, ".scripts", "30-telegram.sh");
989
+ const script = join6(roleDir, ".scripts", "30-telegram.sh");
928
990
  if (!existsSync4(script)) {
929
991
  return {
930
992
  success: false,
931
993
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
932
994
  };
933
995
  }
934
- const marker = join5(roleDir, ".scripts", ".done-30-telegram");
996
+ const marker = join6(roleDir, ".scripts", ".done-30-telegram");
935
997
  if (existsSync4(marker)) unlinkSync(marker);
936
998
  const spinner4 = p3.spinner();
937
999
  spinner4.start("Verifying token + wiring profile");
@@ -959,14 +1021,14 @@ function cap(s) {
959
1021
 
960
1022
  // src/commands/hermes/WireEmail.ts
961
1023
  import { spawnSync as spawnSync3 } from "node:child_process";
962
- import { join as join6 } from "node:path";
1024
+ import { join as join7 } from "node:path";
963
1025
  import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
964
1026
  import * as p4 from "@clack/prompts";
965
1027
  var WireEmail = class extends Command {
966
1028
  async invoke() {
967
1029
  const ctx = this.context;
968
1030
  if (ctx.skipEmail) {
969
- return { success: true, message: "\u2192 Email wire-up skipped" };
1031
+ return { success: true, message: "" };
970
1032
  }
971
1033
  if (ctx.dryRun) {
972
1034
  return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
@@ -975,7 +1037,7 @@ var WireEmail = class extends Command {
975
1037
  if (!targetRepo || !role || !roleDir) {
976
1038
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
977
1039
  }
978
- const script = join6(roleDir, ".scripts", "50-email.sh");
1040
+ const script = join7(roleDir, ".scripts", "50-email.sh");
979
1041
  if (!existsSync5(script)) {
980
1042
  return { success: false, message: `\u2717 ${script} not found` };
981
1043
  }
@@ -1038,7 +1100,7 @@ var WireEmail = class extends Command {
1038
1100
  }
1039
1101
  }
1040
1102
  }
1041
- const marker = join6(roleDir, ".scripts", ".done-50-email");
1103
+ const marker = join7(roleDir, ".scripts", ".done-50-email");
1042
1104
  if (existsSync5(marker)) unlinkSync2(marker);
1043
1105
  const spinner4 = p4.spinner();
1044
1106
  spinner4.start("Creating Cloudflare Email Routing rule");
@@ -1074,7 +1136,7 @@ var PrintHermesSummary = class extends Command {
1074
1136
  lines.push(`role dir ${ctx.roleDir}`);
1075
1137
  lines.push(`runtime gh:${runtimeRepo}`);
1076
1138
  lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
1077
- lines.push(`email ${email}${skipEmail ? " (NOT yet wired)" : ""}`);
1139
+ if (!skipEmail) lines.push(`email ${email}`);
1078
1140
  lines.push("");
1079
1141
  lines.push("Start daemons:");
1080
1142
  lines.push(` systemctl --user start ${csm}`);
@@ -1087,11 +1149,10 @@ var PrintHermesSummary = class extends Command {
1087
1149
  lines.push("");
1088
1150
  lines.push("Talk locally:");
1089
1151
  lines.push(` ${ctx.roleDir}/hermes chat "status"`);
1090
- if (skipTelegram || skipEmail) {
1152
+ if (skipTelegram) {
1091
1153
  lines.push("");
1092
- lines.push("Deferred \u2014 re-run pjangler hermes-agent without --yes (or with explicit flags):");
1093
- if (skipTelegram) lines.push(" pjangler hermes-agent --skip-telegram=false # wire just telegram");
1094
- if (skipEmail) lines.push(" pjangler hermes-agent --skip-email=false # wire just email");
1154
+ lines.push("Wire Telegram later:");
1155
+ lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
1095
1156
  }
1096
1157
  p5.note(lines.join("\n"), `Provisioned ${agentId}`);
1097
1158
  p5.outro("Done.");
@@ -1127,7 +1188,7 @@ var HermesAgentRecipe = class extends Recipe {
1127
1188
 
1128
1189
  // src/commands/AgentHooksCommands.ts
1129
1190
  import { homedir as homedir3 } from "node:os";
1130
- import { join as join7, dirname as dirname4 } from "node:path";
1191
+ import { join as join8, dirname as dirname4 } from "node:path";
1131
1192
  import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
1132
1193
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1133
1194
  function resolveTemplateRoot() {
@@ -1138,16 +1199,16 @@ function resolveTemplateRoot() {
1138
1199
  try {
1139
1200
  let dir = dirname4(fileURLToPath2(import.meta.url));
1140
1201
  for (let i = 0; i < 8; i++) {
1141
- candidates.push(join7(dir, "templates", "commonproject", "template"));
1202
+ candidates.push(join8(dir, "templates", "commonproject", "template"));
1142
1203
  const parent = dirname4(dir);
1143
1204
  if (parent === dir) break;
1144
1205
  dir = parent;
1145
1206
  }
1146
1207
  } catch {
1147
1208
  }
1148
- candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1209
+ candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1149
1210
  for (const c of candidates) {
1150
- if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
1211
+ if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
1151
1212
  }
1152
1213
  throw new Error(
1153
1214
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1171,8 +1232,8 @@ var CopyAgentHooksTree = class extends Command {
1171
1232
  const created = [];
1172
1233
  const skipped = [];
1173
1234
  for (const { rel, dir } of items) {
1174
- const src = join7(templateRoot, rel);
1175
- const dest = join7(this.context.targetDir, rel);
1235
+ const src = join8(templateRoot, rel);
1236
+ const dest = join8(this.context.targetDir, rel);
1176
1237
  if (!existsSync6(src)) continue;
1177
1238
  if (existsSync6(dest) && !this.context.force) {
1178
1239
  skipped.push(rel);
@@ -1197,7 +1258,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1197
1258
  static CR = "{{config_root}}";
1198
1259
  // mise's own runtime var — emitted literally
1199
1260
  async invoke() {
1200
- const misePath = join7(this.context.targetDir, "mise.toml");
1261
+ const misePath = join8(this.context.targetDir, "mise.toml");
1201
1262
  if (!existsSync6(misePath)) {
1202
1263
  return {
1203
1264
  success: false,
@@ -1318,7 +1379,7 @@ var RECIPE_REGISTRY = {
1318
1379
  name: "mise",
1319
1380
  description: "Mise task runner and environment setup",
1320
1381
  class: MiseRecipe,
1321
- commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
1382
+ commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
1322
1383
  },
1323
1384
  docker: {
1324
1385
  name: "docker",
@@ -1408,6 +1469,12 @@ var COMMAND_REGISTRY = {
1408
1469
  group: "mise",
1409
1470
  class: AddMiseBaseScript
1410
1471
  },
1472
+ AddMiseCodegraphScript: {
1473
+ name: "AddMiseCodegraphScript",
1474
+ description: "Create .mise/scripts/codegraph.sh enter hook",
1475
+ group: "mise",
1476
+ class: AddMiseCodegraphScript
1477
+ },
1411
1478
  AddDotenv: {
1412
1479
  name: "AddDotenv",
1413
1480
  description: "Create .env.example file",
@@ -1429,14 +1496,14 @@ function createRecipe(name, context) {
1429
1496
 
1430
1497
  // src/utils/version.ts
1431
1498
  import { readFileSync as readFileSync3 } from "node:fs";
1432
- import { dirname as dirname5, join as join8 } from "node:path";
1499
+ import { dirname as dirname5, join as join9 } from "node:path";
1433
1500
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1434
1501
  var PJANGLER_VERSION = (() => {
1435
1502
  try {
1436
1503
  let dir = dirname5(fileURLToPath3(import.meta.url));
1437
1504
  for (let i = 0; i < 4; i++) {
1438
1505
  try {
1439
- const raw = readFileSync3(join8(dir, "package.json"), "utf8");
1506
+ const raw = readFileSync3(join9(dir, "package.json"), "utf8");
1440
1507
  return JSON.parse(raw).version ?? "0.0.0";
1441
1508
  } catch {
1442
1509
  const parent = dirname5(dir);
@@ -1450,8 +1517,8 @@ var PJANGLER_VERSION = (() => {
1450
1517
  })();
1451
1518
 
1452
1519
  // 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";
1520
+ 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";
1521
+ import { basename as basename2, dirname as dirname6, join as join10, relative, resolve } from "node:path";
1455
1522
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1456
1523
  import { homedir as homedir4 } from "node:os";
1457
1524
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -1525,7 +1592,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
1525
1592
  function resolvePjanglerRoot() {
1526
1593
  let dir = dirname6(fileURLToPath4(import.meta.url));
1527
1594
  while (dir !== dirname6(dir)) {
1528
- if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
1595
+ if (existsSync7(join10(dir, "package.json")) && existsSync7(join10(dir, "templates", "commonproject", "copier.yml"))) {
1529
1596
  return dir;
1530
1597
  }
1531
1598
  dir = dirname6(dir);
@@ -1548,10 +1615,10 @@ function writeText(path, content) {
1548
1615
  ensureParent(path);
1549
1616
  writeFileSync4(path, content);
1550
1617
  }
1551
- function tryParseJson(text3) {
1552
- if (!text3) return null;
1618
+ function tryParseJson(text2) {
1619
+ if (!text2) return null;
1553
1620
  try {
1554
- return JSON.parse(text3);
1621
+ return JSON.parse(text2);
1555
1622
  } catch {
1556
1623
  return null;
1557
1624
  }
@@ -1588,10 +1655,10 @@ function ensureSymlink(path, target, dryRun) {
1588
1655
  return { changed: true };
1589
1656
  }
1590
1657
  function bootstrapAgentsFile(repoRoot, dryRun) {
1591
- const agentsPath = join9(repoRoot, "AGENTS.md");
1658
+ const agentsPath = join10(repoRoot, "AGENTS.md");
1592
1659
  if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
1593
1660
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1594
- const source = join9(repoRoot, file);
1661
+ const source = join10(repoRoot, file);
1595
1662
  if (!existsSync7(source)) continue;
1596
1663
  const stat = lstatSync(source);
1597
1664
  if (stat.isSymbolicLink()) continue;
@@ -1601,7 +1668,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1601
1668
  }
1602
1669
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
1603
1670
  }
1604
- const readmePath = join9(repoRoot, "README.md");
1671
+ const readmePath = join10(repoRoot, "README.md");
1605
1672
  if (existsSync7(readmePath)) {
1606
1673
  const stat = lstatSync(readmePath);
1607
1674
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
@@ -1610,9 +1677,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1610
1677
  }
1611
1678
  return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
1612
1679
  }
1613
- function yamlGet(text3, keyPath) {
1680
+ function yamlGet(text2, keyPath) {
1614
1681
  const parts = keyPath.split(".");
1615
- const lines = text3.split("\n");
1682
+ const lines = text2.split("\n");
1616
1683
  let start = 0;
1617
1684
  let indent = 0;
1618
1685
  for (let idx = 0; idx < parts.length; idx += 1) {
@@ -1641,36 +1708,36 @@ function yamlGet(text3, keyPath) {
1641
1708
  return "";
1642
1709
  }
1643
1710
  function discoverRoles(repoRoot) {
1644
- const rolesDir = join9(repoRoot, "agents", "hermes");
1711
+ const rolesDir = join10(repoRoot, "agents", "hermes");
1645
1712
  if (!existsSync7(rolesDir)) return [];
1646
1713
  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");
1714
+ const roleDir = join10(rolesDir, entry.name);
1715
+ const roleYamlPath = join10(roleDir, "role.yaml");
1649
1716
  if (!existsSync7(roleYamlPath)) return null;
1650
- const text3 = readText(roleYamlPath);
1651
- const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
1717
+ const text2 = readText(roleYamlPath);
1718
+ const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
1652
1719
  return {
1653
- role: yamlGet(text3, "role") || entry.name,
1720
+ role: yamlGet(text2, "role") || entry.name,
1654
1721
  roleDir,
1655
1722
  roleYamlPath,
1656
- repo: yamlGet(text3, "repo"),
1657
- agentId: yamlGet(text3, "agent_id"),
1658
- profileName: yamlGet(text3, "profile") || yamlGet(text3, "agent_id"),
1659
- displayName: yamlGet(text3, "display_name"),
1660
- purpose: yamlGet(text3, "purpose"),
1661
- botHandle: yamlGet(text3, "telegram.bot_username"),
1723
+ repo: yamlGet(text2, "repo"),
1724
+ agentId: yamlGet(text2, "agent_id"),
1725
+ profileName: yamlGet(text2, "profile") || yamlGet(text2, "agent_id"),
1726
+ displayName: yamlGet(text2, "display_name"),
1727
+ purpose: yamlGet(text2, "purpose"),
1728
+ botHandle: yamlGet(text2, "telegram.bot_username"),
1662
1729
  runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
1663
- runtimeOwner: yamlGet(text3, "runtime.github_owner"),
1664
- planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
1665
- ticketProviderName: yamlGet(text3, "ticket_provider.name"),
1666
- ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
1667
- ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
1668
- ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
1730
+ runtimeOwner: yamlGet(text2, "runtime.github_owner"),
1731
+ planeWorkspace: yamlGet(text2, "ticket_provider.workspace") || yamlGet(text2, "plane.workspace"),
1732
+ ticketProviderName: yamlGet(text2, "ticket_provider.name"),
1733
+ ticketProviderBoardId: yamlGet(text2, "ticket_provider.board_id"),
1734
+ ticketProviderBoardUrl: yamlGet(text2, "ticket_provider.board_url"),
1735
+ ticketProviderIdentifier: yamlGet(text2, "plane.identifier")
1669
1736
  };
1670
1737
  }).filter((value) => Boolean(value));
1671
1738
  }
1672
1739
  function registryPath(homeDir) {
1673
- return join9(homeDir, ".hermes", "agents-registry.yaml");
1740
+ return join10(homeDir, ".hermes", "agents-registry.yaml");
1674
1741
  }
1675
1742
  function systemctlUser(args) {
1676
1743
  const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -1681,7 +1748,7 @@ function systemctlUser(args) {
1681
1748
  };
1682
1749
  }
1683
1750
  function templateScript(ctx, name) {
1684
- const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
1751
+ const source = join10(ctx.pjanglerRoot, ".mise", "scripts", name);
1685
1752
  return existsSync7(source) ? readText(source) : void 0;
1686
1753
  }
1687
1754
  function templateVersioningScript(ctx) {
@@ -1696,9 +1763,9 @@ function renderGeneratedProjectMiseToml(ctx, template) {
1696
1763
  return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
1697
1764
  }
1698
1765
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1699
- const targetPath = join9(ctx.repoRoot, "mise.toml");
1766
+ const targetPath = join10(ctx.repoRoot, "mise.toml");
1700
1767
  if (existsSync7(targetPath)) return false;
1701
- const sourcePath = join9(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1768
+ const sourcePath = join10(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1702
1769
  if (!existsSync7(sourcePath)) return false;
1703
1770
  changedFiles.push(targetPath);
1704
1771
  if (!ctx.dryRun) {
@@ -1707,22 +1774,22 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1707
1774
  return true;
1708
1775
  }
1709
1776
  function templateVersionFilesConf(ctx, repoRoot) {
1710
- const packageJson = join9(repoRoot, "package.json");
1777
+ const packageJson = join10(repoRoot, "package.json");
1711
1778
  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
1779
  }
1713
- function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
1714
- if (startMarker.test(text3)) {
1715
- return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1780
+ function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
1781
+ if (startMarker.test(text2)) {
1782
+ return text2.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1716
1783
  }
1717
1784
  if (beforePattern) {
1718
- const match = text3.match(beforePattern);
1785
+ const match = text2.match(beforePattern);
1719
1786
  if (match && typeof match.index === "number") {
1720
- return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1787
+ return `${text2.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1721
1788
 
1722
- ${text3.slice(match.index)}`;
1789
+ ${text2.slice(match.index)}`;
1723
1790
  }
1724
1791
  }
1725
- return `${text3.replace(/\s*$/, "")}
1792
+ return `${text2.replace(/\s*$/, "")}
1726
1793
 
1727
1794
  ${block}
1728
1795
  `;
@@ -1732,22 +1799,22 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
1732
1799
  function requiredMisePathEntries(ctx) {
1733
1800
  const required = [...BASE_MISE_PATH_ENTRIES];
1734
1801
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
1735
- if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1802
+ if (existsSync7(join10(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1736
1803
  }
1737
1804
  return required;
1738
1805
  }
1739
- function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
1806
+ function upsertMisePath(text2, required = BASE_MISE_PATH_ENTRIES) {
1740
1807
  const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
1741
- const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1808
+ const envMatch = text2.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1742
1809
  if (!envMatch || typeof envMatch.index !== "number") {
1743
1810
  return `[env]
1744
1811
  ${render(required)}
1745
1812
 
1746
- ${text3.replace(/^\s+/, "")}`;
1813
+ ${text2.replace(/^\s+/, "")}`;
1747
1814
  }
1748
- const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
1815
+ const prefix = text2.slice(0, envMatch.index + envMatch[1].length);
1749
1816
  const section = envMatch[2];
1750
- const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
1817
+ const suffix = text2.slice(envMatch.index + envMatch[1].length + section.length);
1751
1818
  const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
1752
1819
  if (!pathLine) {
1753
1820
  return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
@@ -1758,11 +1825,11 @@ ${text3.replace(/^\s+/, "")}`;
1758
1825
  if (!merged.includes(value)) merged.push(value);
1759
1826
  }
1760
1827
  const nextLine = render(merged);
1761
- if (pathLine[0] === nextLine) return text3;
1828
+ if (pathLine[0] === nextLine) return text2;
1762
1829
  return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
1763
1830
  }
1764
- function removeTomlSection(text3, headerPattern, marker, options) {
1765
- const lines = text3.split("\n");
1831
+ function removeTomlSection(text2, headerPattern, marker, options) {
1832
+ const lines = text2.split("\n");
1766
1833
  let start = -1;
1767
1834
  let end = -1;
1768
1835
  for (let i = 0; i < lines.length; i++) {
@@ -1787,7 +1854,7 @@ function removeTomlSection(text3, headerPattern, marker, options) {
1787
1854
  if (end === -1) end = lines.length;
1788
1855
  break;
1789
1856
  }
1790
- if (start === -1) return text3;
1857
+ if (start === -1) return text2;
1791
1858
  if (options?.includePrecedingComments) {
1792
1859
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
1793
1860
  start--;
@@ -1796,22 +1863,22 @@ function removeTomlSection(text3, headerPattern, marker, options) {
1796
1863
  const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
1797
1864
  return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
1798
1865
  }
1799
- function insertTomlBlockBeforeVersioning(text3, block) {
1800
- const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
1866
+ function insertTomlBlockBeforeVersioning(text2, block) {
1867
+ const versioningIndex = text2.indexOf("# >>> mise-versioning >>>");
1801
1868
  if (versioningIndex >= 0) {
1802
- return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1869
+ return `${text2.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1803
1870
 
1804
- ${text3.slice(versioningIndex)}`;
1871
+ ${text2.slice(versioningIndex)}`;
1805
1872
  }
1806
- return `${text3.replace(/\s*$/, "")}
1873
+ return `${text2.replace(/\s*$/, "")}
1807
1874
 
1808
1875
  ${block}
1809
1876
  `;
1810
1877
  }
1811
- function extractTomlStrings(text3) {
1878
+ function extractTomlStrings(text2) {
1812
1879
  const values = [];
1813
1880
  const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
1814
- for (const match of text3.matchAll(stringPattern)) {
1881
+ for (const match of text2.matchAll(stringPattern)) {
1815
1882
  if (match[1] !== void 0) {
1816
1883
  try {
1817
1884
  values.push(JSON.parse(`"${match[1]}"`));
@@ -1835,10 +1902,10 @@ function renderHookEntries(entries, indent = "") {
1835
1902
  `${indent}]`
1836
1903
  ];
1837
1904
  }
1838
- function upsertLinkAgentfilesHooks(text3) {
1839
- const lines = text3.split("\n");
1905
+ function upsertLinkAgentfilesHooks(text2) {
1906
+ const lines = text2.split("\n");
1840
1907
  const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
1841
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
1908
+ if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text2, LINK_AGENTFILES_HOOKS_BLOCK);
1842
1909
  let hooksEnd = lines.length;
1843
1910
  for (let i = hooksStart + 1; i < lines.length; i++) {
1844
1911
  if (/^\[[^\]]+\]/.test(lines[i].trim())) {
@@ -1872,8 +1939,8 @@ function upsertLinkAgentfilesHooks(text3) {
1872
1939
  }
1873
1940
  return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
1874
1941
  }
1875
- function upsertLinkAgentfilesBlock(text3, ctx) {
1876
- const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
1942
+ function upsertLinkAgentfilesBlock(text2, ctx) {
1943
+ const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
1877
1944
  if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
1878
1945
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
1879
1946
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
@@ -1881,7 +1948,7 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
1881
1948
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
1882
1949
  }
1883
1950
  function readProjectJson(ctx) {
1884
- return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
1951
+ return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
1885
1952
  }
1886
1953
  function canonicalProjectJson(ctx) {
1887
1954
  const roles = discoverRoles(ctx.repoRoot);
@@ -1925,8 +1992,8 @@ function canonicalProjectJson(ctx) {
1925
1992
  };
1926
1993
  }
1927
1994
  function projectJsonFinding(ctx) {
1928
- const projectPath = join9(ctx.repoRoot, ".project.json");
1929
- const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
1995
+ const projectPath = join10(ctx.repoRoot, ".project.json");
1996
+ const planeJsonPath = join10(ctx.repoRoot, ".plane.json");
1930
1997
  const details = [];
1931
1998
  const data = readProjectJson(ctx);
1932
1999
  const roles = discoverRoles(ctx.repoRoot);
@@ -2040,9 +2107,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2040
2107
  if (!existsSync7(sourceDir)) return;
2041
2108
  mkdirSync5(targetDir, { recursive: true });
2042
2109
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2043
- const sourcePath = join9(sourceDir, entry.name);
2110
+ const sourcePath = join10(sourceDir, entry.name);
2044
2111
  if (skip?.(sourcePath)) continue;
2045
- const targetPath = join9(targetDir, entry.name);
2112
+ const targetPath = join10(targetDir, entry.name);
2046
2113
  if (entry.isDirectory()) {
2047
2114
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2048
2115
  continue;
@@ -2056,7 +2123,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2056
2123
  }
2057
2124
  }
2058
2125
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2059
- const gitmodulesPath = join9(repoRoot, ".gitmodules");
2126
+ const gitmodulesPath = join10(repoRoot, ".gitmodules");
2060
2127
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2061
2128
  const owner = role.runtimeOwner || "delorenj";
2062
2129
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2100,9 +2167,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
2100
2167
  return path;
2101
2168
  }
2102
2169
  function profileMetaInheritsDefault(path) {
2103
- const text3 = safeReadText(path);
2170
+ const text2 = safeReadText(path);
2104
2171
  return Boolean(
2105
- text3 && /^config:\s*$/m.test(text3) && /^\s+inherit_from:\s*default\s*$/m.test(text3) && /^\s+save_mode:\s*delta\s*$/m.test(text3)
2172
+ text2 && /^config:\s*$/m.test(text2) && /^\s+inherit_from:\s*default\s*$/m.test(text2) && /^\s+save_mode:\s*delta\s*$/m.test(text2)
2106
2173
  );
2107
2174
  }
2108
2175
  function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
@@ -2156,21 +2223,21 @@ var RULES = [
2156
2223
  id: "mise.config-root",
2157
2224
  title: "mise config_root + AGENTS link hooks",
2158
2225
  audit: (ctx) => {
2159
- const misePath = join9(ctx.repoRoot, "mise.toml");
2226
+ const misePath = join10(ctx.repoRoot, "mise.toml");
2160
2227
  if (!existsSync7(misePath)) {
2161
2228
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2162
2229
  }
2163
- const text3 = readText(misePath);
2230
+ const text2 = readText(misePath);
2164
2231
  const details = [];
2165
- const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2232
+ const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2166
2233
  if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2167
- const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2234
+ const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2168
2235
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2169
2236
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
2170
- if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2171
- if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2172
- if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2173
- if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2237
+ if (!text2.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2238
+ if (!text2.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2239
+ if (!text2.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2240
+ if (!text2.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2174
2241
  return {
2175
2242
  id: "mise.config-root",
2176
2243
  title: "mise config_root + AGENTS link hooks",
@@ -2181,7 +2248,7 @@ var RULES = [
2181
2248
  };
2182
2249
  },
2183
2250
  migrate: (ctx, finding) => {
2184
- const path = join9(ctx.repoRoot, "mise.toml");
2251
+ const path = join10(ctx.repoRoot, "mise.toml");
2185
2252
  const changedFiles = [];
2186
2253
  const details = [];
2187
2254
  if (!existsSync7(path)) {
@@ -2193,14 +2260,14 @@ var RULES = [
2193
2260
  return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
2194
2261
  }
2195
2262
  }
2196
- let text3 = readText(path);
2197
- const next = upsertLinkAgentfilesBlock(text3, ctx);
2198
- if (next !== text3) {
2263
+ let text2 = readText(path);
2264
+ const next = upsertLinkAgentfilesBlock(text2, ctx);
2265
+ if (next !== text2) {
2199
2266
  if (!changedFiles.includes(path)) changedFiles.push(path);
2200
2267
  if (!ctx.dryRun) writeText(path, next);
2201
- text3 = next;
2268
+ text2 = next;
2202
2269
  }
2203
- const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2270
+ const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2204
2271
  const expectedScript = templateLinkAgentfilesScript(ctx);
2205
2272
  if (expectedScript === void 0) {
2206
2273
  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 +2276,7 @@ var RULES = [
2209
2276
  changedFiles.push(linkAgentfilesPath);
2210
2277
  if (!ctx.dryRun) {
2211
2278
  writeText(linkAgentfilesPath, expectedScript);
2212
- chmodSync(linkAgentfilesPath, 493);
2279
+ chmodSync2(linkAgentfilesPath, 493);
2213
2280
  }
2214
2281
  }
2215
2282
  return {
@@ -2227,11 +2294,11 @@ var RULES = [
2227
2294
  title: "managed mise versioning block",
2228
2295
  audit: (ctx) => {
2229
2296
  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");
2233
- const text3 = safeReadText(misePath);
2234
- if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2297
+ const misePath = join10(ctx.repoRoot, "mise.toml");
2298
+ const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2299
+ const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
2300
+ const text2 = safeReadText(misePath);
2301
+ if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2235
2302
  if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2236
2303
  if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
2237
2304
  return {
@@ -2246,7 +2313,7 @@ var RULES = [
2246
2313
  migrate: (ctx, finding) => {
2247
2314
  const changedFiles = [];
2248
2315
  const details = [];
2249
- const misePath = join9(ctx.repoRoot, "mise.toml");
2316
+ const misePath = join10(ctx.repoRoot, "mise.toml");
2250
2317
  if (!existsSync7(misePath)) {
2251
2318
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2252
2319
  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 +2329,7 @@ var RULES = [
2262
2329
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
2263
2330
  if (!ctx.dryRun) writeText(misePath, nextMise);
2264
2331
  }
2265
- const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2332
+ const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2266
2333
  const expectedScript = templateVersioningScript(ctx);
2267
2334
  if (expectedScript === void 0) {
2268
2335
  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 +2338,10 @@ var RULES = [
2271
2338
  changedFiles.push(versioningPath);
2272
2339
  if (!ctx.dryRun) {
2273
2340
  writeText(versioningPath, expectedScript);
2274
- chmodSync(versioningPath, 493);
2341
+ chmodSync2(versioningPath, 493);
2275
2342
  }
2276
2343
  }
2277
- const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2344
+ const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
2278
2345
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
2279
2346
  if (safeReadText(manifestPath) !== expectedManifest) {
2280
2347
  changedFiles.push(manifestPath);
@@ -2294,9 +2361,9 @@ var RULES = [
2294
2361
  id: "sot.agent-symlinks",
2295
2362
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
2296
2363
  audit: (ctx) => {
2297
- const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
2364
+ const agentsPath = join10(ctx.repoRoot, "AGENTS.md");
2298
2365
  if (!existsSync7(agentsPath)) {
2299
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
2366
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join10(ctx.repoRoot, file)));
2300
2367
  if (fallbackSources.length === 0) {
2301
2368
  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
2369
  }
@@ -2311,7 +2378,7 @@ var RULES = [
2311
2378
  }
2312
2379
  const details = [];
2313
2380
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2314
- const full = join9(ctx.repoRoot, file);
2381
+ const full = join10(ctx.repoRoot, file);
2315
2382
  const target = readSymlinkTarget(full);
2316
2383
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
2317
2384
  }
@@ -2335,7 +2402,7 @@ var RULES = [
2335
2402
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
2336
2403
  }
2337
2404
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2338
- const full = join9(ctx.repoRoot, file);
2405
+ const full = join10(ctx.repoRoot, file);
2339
2406
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
2340
2407
  if (result.blocked) blockedDetails.push(result.blocked);
2341
2408
  if (result.changed) changedFiles.push(full);
@@ -2357,7 +2424,7 @@ var RULES = [
2357
2424
  migrate: (ctx, finding) => {
2358
2425
  const changedFiles = [];
2359
2426
  const details = [];
2360
- const path = join9(ctx.repoRoot, ".project.json");
2427
+ const path = join10(ctx.repoRoot, ".project.json");
2361
2428
  const existing = readProjectJson(ctx) ?? {};
2362
2429
  const canonical = canonicalProjectJson(ctx);
2363
2430
  const merged = { ...existing, ...canonical };
@@ -2367,7 +2434,7 @@ var RULES = [
2367
2434
  changedFiles.push(path);
2368
2435
  if (!ctx.dryRun) writeText(path, expected);
2369
2436
  }
2370
- const planeJson = join9(ctx.repoRoot, ".plane.json");
2437
+ const planeJson = join10(ctx.repoRoot, ".plane.json");
2371
2438
  if (existsSync7(planeJson)) {
2372
2439
  const backup = `${planeJson}.migrated-backup`;
2373
2440
  if (existsSync7(backup)) {
@@ -2392,8 +2459,8 @@ var RULES = [
2392
2459
  title: ".env.op + gitignore secrets contract",
2393
2460
  audit: (ctx) => {
2394
2461
  const details = [];
2395
- const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
2396
- const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
2462
+ const envOp = safeReadText(join10(ctx.repoRoot, ".env.op"));
2463
+ const gitignore = safeReadText(join10(ctx.repoRoot, ".gitignore"));
2397
2464
  if (!envOp) {
2398
2465
  details.push(".env.op missing");
2399
2466
  } else {
@@ -2419,12 +2486,12 @@ var RULES = [
2419
2486
  migrate: (ctx, finding) => {
2420
2487
  const changedFiles = [];
2421
2488
  const details = [];
2422
- const envOpPath = join9(ctx.repoRoot, ".env.op");
2489
+ const envOpPath = join10(ctx.repoRoot, ".env.op");
2423
2490
  if (!existsSync7(envOpPath)) {
2424
2491
  changedFiles.push(envOpPath);
2425
- if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2492
+ if (!ctx.dryRun) writeText(envOpPath, readText(join10(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2426
2493
  }
2427
- const gitignorePath = join9(ctx.repoRoot, ".gitignore");
2494
+ const gitignorePath = join10(ctx.repoRoot, ".gitignore");
2428
2495
  const gitignore = safeReadText(gitignorePath) ?? "";
2429
2496
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
2430
2497
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -2451,20 +2518,20 @@ var RULES = [
2451
2518
  title: ".copier-answers.yml provenance + drift report",
2452
2519
  audit: (ctx) => {
2453
2520
  const details = [];
2454
- const path = join9(ctx.repoRoot, ".copier-answers.yml");
2455
- const text3 = safeReadText(path);
2521
+ const path = join10(ctx.repoRoot, ".copier-answers.yml");
2522
+ const text2 = safeReadText(path);
2456
2523
  const project = readProjectJson(ctx);
2457
- if (!text3) {
2524
+ if (!text2) {
2458
2525
  details.push(".copier-answers.yml missing");
2459
2526
  } else {
2460
- if (!text3.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2461
- if (!text3.includes("_src_path:")) details.push("_src_path missing");
2527
+ if (!text2.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2528
+ if (!text2.includes("_src_path:")) details.push("_src_path missing");
2462
2529
  if (project?.project_name) {
2463
- const nameMatch = text3.match(/project_name:\s*(.+)/);
2530
+ const nameMatch = text2.match(/project_name:\s*(.+)/);
2464
2531
  if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
2465
2532
  }
2466
2533
  if (project?.project_description) {
2467
- const descMatch = text3.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2534
+ const descMatch = text2.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2468
2535
  const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
2469
2536
  if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
2470
2537
  }
@@ -2481,16 +2548,16 @@ var RULES = [
2481
2548
  migrate: (ctx, finding) => {
2482
2549
  const changedFiles = [];
2483
2550
  const project = canonicalProjectJson(ctx);
2484
- const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2485
- _src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
2551
+ const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2552
+ _src_path: ${join10(ctx.pjanglerRoot, "templates", "commonproject")}
2486
2553
  project_description: ${String(project.project_description)}
2487
2554
  project_name: ${String(project.project_name)}
2488
2555
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2489
2556
  `;
2490
- const path = join9(ctx.repoRoot, ".copier-answers.yml");
2491
- if (safeReadText(path) !== text3) {
2557
+ const path = join10(ctx.repoRoot, ".copier-answers.yml");
2558
+ if (safeReadText(path) !== text2) {
2492
2559
  changedFiles.push(path);
2493
- if (!ctx.dryRun) writeText(path, text3);
2560
+ if (!ctx.dryRun) writeText(path, text2);
2494
2561
  }
2495
2562
  return {
2496
2563
  id: finding.id,
@@ -2506,15 +2573,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2506
2573
  id: "bmad.scaffold",
2507
2574
  title: "BMAD modules/docs scaffold",
2508
2575
  audit: (ctx) => {
2509
- const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2510
- const targetRoot = join9(ctx.repoRoot, "_bmad");
2576
+ const sourceRoot = join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2577
+ const targetRoot = join10(ctx.repoRoot, "_bmad");
2511
2578
  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")
2579
+ join10("core", "config.yaml"),
2580
+ join10("custom", "config.yaml"),
2581
+ join10("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2582
+ join10("bmm", "workflows", "workflow-status", "workflow.yaml")
2516
2583
  ];
2517
- const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
2584
+ const missing = sentinels.filter((file) => existsSync7(join10(sourceRoot, file)) && !existsSync7(join10(targetRoot, file)));
2518
2585
  return {
2519
2586
  id: "bmad.scaffold",
2520
2587
  title: "BMAD modules/docs scaffold",
@@ -2526,7 +2593,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2526
2593
  },
2527
2594
  migrate: (ctx, finding) => {
2528
2595
  const changedFiles = [];
2529
- copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2596
+ copyMissingRecursive(join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join10(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2530
2597
  return {
2531
2598
  id: finding.id,
2532
2599
  title: finding.title,
@@ -2548,11 +2615,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2548
2615
  }
2549
2616
  const details = [];
2550
2617
  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))}`);
2618
+ if (!existsSync7(join10(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join10(role.roleDir, rel))}`);
2552
2619
  }
2553
- const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
2620
+ const gitmodules = safeReadText(join10(ctx.repoRoot, ".gitmodules")) ?? "";
2554
2621
  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"))) {
2622
+ if (!profileMetaInheritsDefault(join10(role.roleDir, "runtime", "profile.yaml"))) {
2556
2623
  details.push("runtime/profile.yaml missing inherited default config metadata");
2557
2624
  }
2558
2625
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -2573,21 +2640,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2573
2640
  if (!role) {
2574
2641
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
2575
2642
  }
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");
2643
+ const templateRoleDir = join10(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2644
+ writeIfDifferent(join10(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2645
+ writeIfDifferent(join10(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2646
+ writeIfDifferent(join10(role.roleDir, ".gitignore"), readText(join10(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2647
+ copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2648
+ copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2649
+ copyMissingRecursive(join10(templateRoleDir, ".scripts"), join10(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2650
+ const promptSource = join10(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2651
+ const promptTarget = join10(role.roleDir, ".scripts", "sentinel.prompt.md");
2585
2652
  if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
2586
2653
  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
2654
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
2588
2655
  }
2589
2656
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2590
- const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2657
+ const profileMetaUpdated = upsertInheritedProfileMeta(join10(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2591
2658
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2592
2659
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2593
2660
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -2641,9 +2708,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2641
2708
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
2642
2709
  }
2643
2710
  for (const role of roles) {
2644
- const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
2711
+ const sysDir = join10(ctx.homeDir, ".config", "systemd", "user");
2645
2712
  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)));
2713
+ const allUnitsPresent = units.every((unit) => existsSync7(join10(sysDir, unit)));
2647
2714
  if (allUnitsPresent) {
2648
2715
  if (ctx.dryRun) {
2649
2716
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -2655,7 +2722,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2655
2722
  }
2656
2723
  continue;
2657
2724
  }
2658
- for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
2725
+ for (const script of [join10(role.roleDir, ".scripts", "70-systemd.sh")]) {
2659
2726
  if (!script || !existsSync7(script)) continue;
2660
2727
  if (ctx.dryRun) {
2661
2728
  details.push(`would run: bash ${script}`);
@@ -2683,7 +2750,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
2683
2750
  changedFiles.push(path);
2684
2751
  if (!dryRun) {
2685
2752
  writeText(path, normalized);
2686
- if (mode) chmodSync(path, mode);
2753
+ if (mode) chmodSync2(path, mode);
2687
2754
  }
2688
2755
  }
2689
2756
  function getParityRuleIds() {
@@ -2745,21 +2812,38 @@ function runMigration(selector, repoArg, dryRun, all) {
2745
2812
  const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
2746
2813
  return runMigrationForRules(ruleIds, repoArg, dryRun);
2747
2814
  }
2815
+ function prettyTimestamp(iso) {
2816
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
2817
+ return match ? `${match[1]} ${match[2]} UTC` : iso;
2818
+ }
2748
2819
  function formatAuditReport(report) {
2749
- const lines = [`repo: ${report.repo}`, `ok: ${report.ok}`, `audited_at: ${report.auditedAt}`, "rules:"];
2820
+ const counts = {};
2821
+ for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
2822
+ const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
2823
+ const tally = [];
2824
+ if (counts.pass) tally.push(green(`${counts.pass} passed`));
2825
+ if (counts.fail) tally.push(red(`${counts.fail} failed`));
2826
+ if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
2827
+ if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
2828
+ const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
2829
+ const lines = [""];
2830
+ lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
2831
+ lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
2832
+ lines.push("");
2750
2833
  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}`);
2834
+ const style = statusStyle(rule.status);
2835
+ lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
2836
+ for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
2753
2837
  }
2754
- return `${lines.join("\n")}
2755
- `;
2838
+ lines.push("");
2839
+ return lines.join("\n");
2756
2840
  }
2757
2841
 
2758
2842
  // src/project/index.ts
2759
2843
  import { spawnSync as spawnSync5 } from "node:child_process";
2760
2844
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
2761
2845
  import { homedir as homedir5 } from "node:os";
2762
- import { basename as basename3, dirname as dirname7, join as join10, resolve as resolve2 } from "node:path";
2846
+ import { basename as basename3, dirname as dirname7, join as join11, resolve as resolve2 } from "node:path";
2763
2847
  import YAML from "yaml";
2764
2848
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
2765
2849
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
@@ -2767,10 +2851,10 @@ var KNOWN_SKILL_ROOTS = [
2767
2851
  "/home/delorenj/code/skillex/all-skills",
2768
2852
  "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
2769
2853
  "/home/delorenj/code/pjangler/.agents/skills",
2770
- join10(homedir5(), ".codex", "skills")
2854
+ join11(homedir5(), ".codex", "skills")
2771
2855
  ];
2772
- function projectRegistryPath(env = process.env) {
2773
- return expandHome(env[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
2856
+ function projectRegistryPath(env2 = process.env) {
2857
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir5(), ".config", "pjangler", "projects.yaml"));
2774
2858
  }
2775
2859
  function emptyProjectRegistry() {
2776
2860
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
@@ -2854,7 +2938,7 @@ function resolveSourceSkillPath(sourceSkill) {
2854
2938
  if (existsSync8(direct)) return direct;
2855
2939
  const name = basename3(sourceSkill);
2856
2940
  for (const root of KNOWN_SKILL_ROOTS) {
2857
- const candidate = join10(root, name);
2941
+ const candidate = join11(root, name);
2858
2942
  if (existsSync8(candidate)) return candidate;
2859
2943
  }
2860
2944
  const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
@@ -2934,7 +3018,7 @@ function planProjectInit(input) {
2934
3018
  }));
2935
3019
  }
2936
3020
  actions.push(
2937
- { kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
3021
+ { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
2938
3022
  {
2939
3023
  kind: "plane.create-or-link",
2940
3024
  enabled: live,
@@ -3046,7 +3130,7 @@ function getProject(registry, slug) {
3046
3130
  return project;
3047
3131
  }
3048
3132
  function buildCommonProjectCopierAction(input) {
3049
- const templateDir = join10(input.pjanglerRoot, "templates", "commonproject");
3133
+ const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
3050
3134
  const data = {
3051
3135
  project_name: input.projectName,
3052
3136
  project_description: input.projectDescription ?? "",
@@ -3072,7 +3156,7 @@ function buildCommonProjectCopierAction(input) {
3072
3156
  function resolvePjanglerRoot2() {
3073
3157
  let dir = dirname7(new URL(import.meta.url).pathname);
3074
3158
  while (dir !== dirname7(dir)) {
3075
- if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) return dir;
3159
+ if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
3076
3160
  dir = dirname7(dir);
3077
3161
  }
3078
3162
  return resolve2(process.cwd());
@@ -3104,7 +3188,7 @@ function validateProjectRecord(project, key) {
3104
3188
  }
3105
3189
  function expandHome(path) {
3106
3190
  if (path === "~") return homedir5();
3107
- if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
3191
+ if (path.startsWith("~/")) return join11(homedir5(), path.slice(2));
3108
3192
  return path;
3109
3193
  }
3110
3194
  function isRecord(value) {
@@ -3130,7 +3214,7 @@ function resolveTargetDir(targetDir) {
3130
3214
  function resolvePjanglerRoot3() {
3131
3215
  let dir = dirname8(fileURLToPath5(import.meta.url));
3132
3216
  while (dir !== dirname8(dir)) {
3133
- if (existsSync9(join11(dir, "package.json")) && existsSync9(join11(dir, "templates", "commonproject", "copier.yml"))) {
3217
+ if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
3134
3218
  return dir;
3135
3219
  }
3136
3220
  dir = dirname8(dir);
@@ -3330,7 +3414,7 @@ server.registerTool(
3330
3414
  const projectSlug = input.projectSlug ?? slugify(input.projectName);
3331
3415
  const parentDir = resolve3(input.parentDir ?? process.cwd());
3332
3416
  if (!existsSync9(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
3333
- const targetDir = resolve3(input.targetDir ?? join11(parentDir, projectSlug));
3417
+ const targetDir = resolve3(input.targetDir ?? join12(parentDir, projectSlug));
3334
3418
  const overwrite = input.overwrite ?? input.force ?? false;
3335
3419
  const dryRun = input.dryRun ?? true;
3336
3420
  const local = input.local ?? true;