@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.
package/dist/index.js CHANGED
@@ -3,24 +3,12 @@
3
3
  // src/index.ts
4
4
  import { spawnSync as spawnSync6 } from "node:child_process";
5
5
  import { existsSync as existsSync9, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
6
- import { basename as basename4, join as join11, resolve as resolve3 } from "node:path";
6
+ import { basename as basename4, join as join12, resolve as resolve3 } from "node:path";
7
7
  import { Command as Command3 } from "commander";
8
8
 
9
9
  // src/commands/hermes/types.ts
10
10
  var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
11
11
  var SOUL_TONES = ["direct", "playful", "formal", "terse"];
12
- var ROLE_CHOICES = [
13
- { value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
14
- { value: "dev", label: "Developer (dev)", hint: "implements tickets" },
15
- { value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
16
- { value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
17
- { value: "qa", label: "QA (qa)", hint: "test authorship + verification" }
18
- ];
19
- var TICKET_PROVIDERS = [
20
- { value: "plane", label: "Plane", hint: "self-hosted at plane.delo.sh (default)" },
21
- { value: "linear", label: "Linear", hint: "team board (created in Linear UI)" },
22
- { value: "trello", label: "Trello", hint: "board = project" }
23
- ];
24
12
  function deriveAgentId(repo, role) {
25
13
  return `${repo}-${role}`.toLowerCase();
26
14
  }
@@ -159,6 +147,78 @@ var EnsureTemplateConfig = class extends Command {
159
147
  }
160
148
  };
161
149
 
150
+ // src/utils/style.ts
151
+ var env = process.env;
152
+ function detectColor() {
153
+ if ("NO_COLOR" in env && env.NO_COLOR !== "") return false;
154
+ const force = env.FORCE_COLOR;
155
+ if (force === "0" || force === "false") return false;
156
+ if (force !== void 0 && force !== "") return true;
157
+ if (env.TERM === "dumb") return false;
158
+ return Boolean(process.stdout.isTTY);
159
+ }
160
+ var colorEnabled = detectColor();
161
+ function sgr(open, close) {
162
+ const prefix = `\x1B[${open}m`;
163
+ const suffix = `\x1B[${close}m`;
164
+ return (value) => colorEnabled ? `${prefix}${value}${suffix}` : String(value);
165
+ }
166
+ var bold = sgr(1, 22);
167
+ var dim = sgr(2, 22);
168
+ var italic = sgr(3, 23);
169
+ var underline = sgr(4, 24);
170
+ var red = sgr(31, 39);
171
+ var green = sgr(32, 39);
172
+ var yellow = sgr(33, 39);
173
+ var blue = sgr(34, 39);
174
+ var magenta = sgr(35, 39);
175
+ var cyan = sgr(36, 39);
176
+ var gray = sgr(90, 39);
177
+ var glyph = {
178
+ pass: "\u2714",
179
+ fail: "\u2716",
180
+ warn: "\u26A0",
181
+ skip: "\u25CB",
182
+ info: "\u2139",
183
+ arrow: "\u21B3",
184
+ bullet: "\u2022",
185
+ dot: "\xB7",
186
+ add: "+",
187
+ chevron: "\u25B8",
188
+ pointer: "\u276F"
189
+ };
190
+ var STATUS_STYLES = {
191
+ pass: { glyph: glyph.pass, color: green, label: "pass" },
192
+ fail: { glyph: glyph.fail, color: red, label: "fail" },
193
+ warn: { glyph: glyph.warn, color: yellow, label: "warn" },
194
+ skip: { glyph: glyph.skip, color: gray, label: "skip" },
195
+ applied: { glyph: glyph.pass, color: green, label: "applied" },
196
+ noop: { glyph: glyph.skip, color: gray, label: "noop" },
197
+ blocked: { glyph: glyph.fail, color: red, label: "blocked" },
198
+ skipped: { glyph: glyph.skip, color: gray, label: "skipped" }
199
+ };
200
+ function statusStyle(status) {
201
+ return STATUS_STYLES[status] ?? { glyph: glyph.dot, color: dim, label: status };
202
+ }
203
+ function projectStatusColor(status) {
204
+ switch (status) {
205
+ case "active":
206
+ return green;
207
+ case "planned":
208
+ return yellow;
209
+ case "archived":
210
+ return gray;
211
+ default:
212
+ return cyan;
213
+ }
214
+ }
215
+ function heading(title, marker = glyph.chevron) {
216
+ return `${cyan(bold(marker))} ${bold(title)}`;
217
+ }
218
+ function joinDot(fragments) {
219
+ return fragments.join(dim(` ${glyph.dot} `));
220
+ }
221
+
162
222
  // src/recipes/Recipe.ts
163
223
  var Recipe = class {
164
224
  context;
@@ -171,26 +231,22 @@ var Recipe = class {
171
231
  return this;
172
232
  }
173
233
  async execute() {
174
- const dryRunPrefix = this.context.dryRun ? "[DRY RUN] " : "";
175
- console.log(`${dryRunPrefix}\u{1F680} Initializing ${this.constructor.name.replace("Recipe", "").toLowerCase()} subsystem...`);
176
- if (this.context.dryRun) {
177
- console.log("\u26A0\uFE0F Dry-run mode: No files will be modified");
178
- console.log("");
179
- }
234
+ const subsystem = this.constructor.name.replace("Recipe", "").toLowerCase();
235
+ const dryRun = this.context.dryRun;
236
+ console.log("");
237
+ console.log(` ${cyan(bold(glyph.chevron))} ${bold(`Initializing ${subsystem} subsystem`)}${dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
238
+ console.log("");
180
239
  for (const command of this.ingredients) {
181
240
  const result = await command.invoke();
182
- if (result.success) {
183
- console.log(result.message);
184
- } else {
185
- console.log(result.message);
186
- }
241
+ console.log(result.message.split("\n").map((line) => line ? ` ${line}` : line).join("\n"));
187
242
  }
188
- if (!this.context.dryRun) {
243
+ if (!dryRun) {
189
244
  this.printNextSteps();
190
245
  } else {
191
246
  console.log("");
192
- console.log("\u2713 Dry-run complete - no files were modified");
193
- console.log(" Remove --dry-run flag to apply changes");
247
+ console.log(` ${green(glyph.pass)} ${dim("Dry-run complete \u2014 no files were modified.")}`);
248
+ console.log(` ${dim("Remove --dry-run to apply changes.")}`);
249
+ console.log("");
194
250
  }
195
251
  }
196
252
  };
@@ -333,11 +389,111 @@ if __name__ == "__main__":
333
389
  }
334
390
  };
335
391
 
392
+ // src/commands/AddMiseCodegraphScript.ts
393
+ import { chmodSync } from "fs";
394
+ import { join as join3 } from "path";
395
+ var AddMiseCodegraphScript = class extends Command {
396
+ async invoke() {
397
+ const filePath = ".mise/scripts/codegraph.sh";
398
+ if (this.fileExists(filePath) && !this.context.force) {
399
+ return {
400
+ success: false,
401
+ message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph.sh already exists"),
402
+ filePath
403
+ };
404
+ }
405
+ const content = `#!/usr/bin/env bash
406
+ # Mise enter hook: ensure the CodeGraph CLI is available and initialize the
407
+ # project index. If \`codegraph\` is not installed, this script installs it
408
+ # non-interactively into the project-local .mise/bin directory and retries.
409
+ #
410
+ # This is intended to run from a mise enter hook so onboarding a new host is
411
+ # fully automatic.
412
+
413
+ set -euo pipefail
414
+
415
+ REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
416
+ PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
417
+ mkdir -p "$PROJECT_BIN_DIR"
418
+
419
+ # Install the CodeGraph CLI into the project-local bin directory.
420
+ install_codegraph() {
421
+ echo "[mise] codegraph not found. Installing non-interactively..."
422
+ export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
423
+ curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
424
+
425
+ if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
426
+ export PATH="$PROJECT_BIN_DIR:$PATH"
427
+ else
428
+ echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
429
+ return 1
430
+ fi
431
+ }
432
+
433
+ # Ensure a codegraph binary is available on PATH.
434
+ ensure_codegraph() {
435
+ if command -v codegraph >/dev/null 2>&1; then
436
+ return 0
437
+ fi
438
+
439
+ # Check the project-local bin dir first (previous install from this hook).
440
+ if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
441
+ export PATH="$PROJECT_BIN_DIR:$PATH"
442
+ return 0
443
+ fi
444
+
445
+ # Check typical user-level install locations before fetching anything.
446
+ for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
447
+ if [ -x "$d/codegraph" ]; then
448
+ export PATH="$d:$PATH"
449
+ return 0
450
+ fi
451
+ done
452
+
453
+ install_codegraph
454
+ }
455
+
456
+ # Attempt to initialize the project graph. If the command is missing, install
457
+ # it and retry once.
458
+ init_project() {
459
+ local err_file
460
+ err_file="$(mktemp)"
461
+ trap 'rm -f "$err_file"' RETURN
462
+
463
+ if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
464
+ return 0
465
+ fi
466
+
467
+ # If the failure looks like a missing binary, install and retry.
468
+ if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
469
+ ensure_codegraph
470
+ codegraph init -i "$REPO_ROOT"
471
+ return 0
472
+ fi
473
+
474
+ cat "$err_file" >&2
475
+ return 1
476
+ }
477
+
478
+ init_project
479
+ `;
480
+ this.writeFile(filePath, content);
481
+ if (!this.context.dryRun) {
482
+ chmodSync(join3(this.context.targetDir, filePath), 493);
483
+ }
484
+ return {
485
+ success: true,
486
+ message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
487
+ filePath
488
+ };
489
+ }
490
+ };
491
+
336
492
  // src/recipes/MiseRecipe.ts
337
493
  var MiseRecipe = class extends Recipe {
338
494
  constructor(context) {
339
495
  super(context);
340
- this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript);
496
+ this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
341
497
  }
342
498
  printNextSteps() {
343
499
  console.log("\u{1F389} Mise subsystem initialized successfully!");
@@ -565,12 +721,12 @@ var NodeRecipe = class extends Recipe {
565
721
  };
566
722
 
567
723
  // src/commands/hermes/PromptForAgentConfig.ts
568
- import { basename, join as join3 } from "node:path";
724
+ import { basename, join as join4 } from "node:path";
569
725
  import { readFileSync } from "node:fs";
570
726
  import * as p from "@clack/prompts";
571
727
  function detectTicketProvider(targetDir) {
572
728
  try {
573
- const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
729
+ const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
574
730
  return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
575
731
  } catch {
576
732
  return void 0;
@@ -580,19 +736,18 @@ var PromptForAgentConfig = class extends Command {
580
736
  async invoke() {
581
737
  const ctx = this.context;
582
738
  const defaultRepo = basename(ctx.targetDir).toLowerCase();
583
- const defaultRole = "pm";
739
+ ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
740
+ ctx.role ??= "pm";
741
+ ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
742
+ ctx.soulTone ??= "direct";
743
+ ctx.modelProvider ??= "";
744
+ ctx.modelName ??= "";
745
+ ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
746
+ ctx.skipEmail ??= true;
747
+ ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
748
+ ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
584
749
  if (ctx.yes) {
585
- ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
586
- ctx.role ??= defaultRole;
587
- ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
588
- ctx.soulTone ??= "direct";
589
- ctx.modelProvider ??= "";
590
- ctx.modelName ??= "";
591
- ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
592
750
  ctx.skipTelegram ??= true;
593
- ctx.skipEmail ??= true;
594
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
595
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
596
751
  return {
597
752
  success: true,
598
753
  message: this.formatMessage(
@@ -600,96 +755,19 @@ var PromptForAgentConfig = class extends Command {
600
755
  )
601
756
  };
602
757
  }
603
- p.intro("\u2695 hermes-agent \xB7 add a new agent role to this repo");
604
- if (!ctx.targetRepo) {
605
- const answer = await p.text({
606
- message: "Target repo name",
607
- placeholder: defaultRepo,
608
- initialValue: defaultRepo,
609
- validate: (v) => v && v.trim() ? void 0 : "required"
610
- });
611
- if (p.isCancel(answer)) return this.cancelled();
612
- ctx.targetRepo = String(answer).trim().toLowerCase();
613
- }
614
- if (!ctx.role) {
615
- const answer = await p.select({
616
- message: "Role",
617
- options: ROLE_CHOICES.map((r) => ({ value: r.value, label: r.label, hint: r.hint })),
618
- initialValue: defaultRole
619
- });
620
- if (p.isCancel(answer)) return this.cancelled();
621
- ctx.role = String(answer).trim();
622
- }
623
- if (ctx.ticketProvider === void 0) {
624
- const detected = detectTicketProvider(ctx.targetDir);
625
- const answer = await p.select({
626
- message: "Ticket board provider",
627
- options: TICKET_PROVIDERS.map((t) => ({
628
- value: t.value,
629
- label: t.label,
630
- hint: t.value === detected ? `${t.hint} \u2014 current .project.json` : t.hint
631
- })),
632
- initialValue: detected ?? "plane"
633
- });
634
- if (p.isCancel(answer)) return this.cancelled();
635
- ctx.ticketProvider = answer;
636
- }
637
- if (!ctx.agentPurpose) {
638
- const answer = await p.text({
639
- message: "One-line purpose",
640
- placeholder: `${ctx.role} agent for ${ctx.targetRepo}`,
641
- initialValue: `${ctx.role} agent for ${ctx.targetRepo}`
642
- });
643
- if (p.isCancel(answer)) return this.cancelled();
644
- ctx.agentPurpose = String(answer).trim();
645
- }
646
- if (!ctx.soulTone) {
647
- const answer = await p.select({
648
- message: "Personality tone",
649
- options: SOUL_TONES.map((t) => ({
650
- value: t,
651
- label: t,
652
- hint: t === "direct" ? "decision-forward, no preamble (default)" : t === "terse" ? "minimum words, conclusion-first" : t === "playful" ? "warm, mildly funny" : "precise, structured"
653
- })),
654
- initialValue: "direct"
655
- });
656
- if (p.isCancel(answer)) return this.cancelled();
657
- ctx.soulTone = answer;
658
- }
659
- if (ctx.modelProvider === void 0) {
660
- const answer = await p.text({
661
- message: "Provider override (empty = inherit shared default profile)",
662
- placeholder: ""
663
- });
664
- if (p.isCancel(answer)) return this.cancelled();
665
- ctx.modelProvider = String(answer).trim();
666
- }
667
- if (ctx.modelName === void 0) {
668
- const answer = await p.text({
669
- message: "Model name override (empty = inherit shared default profile)",
670
- placeholder: ""
671
- });
672
- if (p.isCancel(answer)) return this.cancelled();
673
- ctx.modelName = String(answer).trim();
674
- }
758
+ p.intro("\u2695 hermes-agent \xB7 provision the PM agent for this repo");
759
+ p.log.info(
760
+ `agent ${ctx.agentId} \xB7 board ${ctx.ticketProvider} \xB7 tone ${ctx.soulTone}`
761
+ );
675
762
  if (ctx.skipTelegram === void 0) {
763
+ const botHandle = `${ctx.targetRepo.replace(/-/g, "_")}_${ctx.role}_bot`;
676
764
  const wire = await p.confirm({
677
- message: `Wire up the Telegram bot (@${ctx.targetRepo}_${ctx.role}_bot) now?`,
765
+ message: `Wire up the Telegram bot (@${botHandle}) now?`,
678
766
  initialValue: true
679
767
  });
680
768
  if (p.isCancel(wire)) return this.cancelled();
681
769
  ctx.skipTelegram = !wire;
682
770
  }
683
- if (ctx.skipEmail === void 0) {
684
- const wire = await p.confirm({
685
- message: `Provision the delo.sh email address (${ctx.targetRepo}-${ctx.role}@delo.sh) now?`,
686
- initialValue: true
687
- });
688
- if (p.isCancel(wire)) return this.cancelled();
689
- ctx.skipEmail = !wire;
690
- }
691
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
692
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
693
771
  return {
694
772
  success: true,
695
773
  message: this.formatMessage(
@@ -706,7 +784,7 @@ var PromptForAgentConfig = class extends Command {
706
784
  // src/commands/hermes/RunCopierTemplate.ts
707
785
  import { spawnSync } from "node:child_process";
708
786
  import { homedir as homedir2 } from "node:os";
709
- import { join as join4, dirname as dirname3 } from "node:path";
787
+ import { join as join5, dirname as dirname3 } from "node:path";
710
788
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
711
789
  import { fileURLToPath } from "node:url";
712
790
  import * as p2 from "@clack/prompts";
@@ -718,8 +796,8 @@ function resolveVendoredTemplate(name) {
718
796
  return void 0;
719
797
  }
720
798
  for (let i = 0; i < 8; i++) {
721
- const candidate = join4(dir, "templates", name);
722
- if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
799
+ const candidate = join5(dir, "templates", name);
800
+ if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
723
801
  const parent = dirname3(dir);
724
802
  if (parent === dir) break;
725
803
  dir = parent;
@@ -738,7 +816,7 @@ var RunCopierTemplate = class extends Command {
738
816
  message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
739
817
  };
740
818
  }
741
- const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
819
+ const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
742
820
  ctx.roleDir = roleDir;
743
821
  ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
744
822
  const which = spawnSync("which", ["copier"], { encoding: "utf8" });
@@ -748,7 +826,7 @@ var RunCopierTemplate = class extends Command {
748
826
  message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
749
827
  };
750
828
  }
751
- if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
829
+ if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
752
830
  if (ctx.yes) {
753
831
  ctx.force = true;
754
832
  } else {
@@ -765,7 +843,7 @@ var RunCopierTemplate = class extends Command {
765
843
  ctx.force = true;
766
844
  }
767
845
  }
768
- const env = {
846
+ const env2 = {
769
847
  ...process.env,
770
848
  SKIP_TELEGRAM: "1",
771
849
  SKIP_EMAIL: "1",
@@ -775,9 +853,9 @@ var RunCopierTemplate = class extends Command {
775
853
  SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
776
854
  SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
777
855
  };
778
- const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
856
+ const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
779
857
  const vendored = resolveVendoredTemplate("hermes-agent");
780
- const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join4(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
858
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
781
859
  const args = [
782
860
  "copy",
783
861
  templateSrc,
@@ -808,13 +886,13 @@ var RunCopierTemplate = class extends Command {
808
886
  message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
809
887
  };
810
888
  }
811
- mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
889
+ mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
812
890
  const spinner4 = p2.spinner();
813
891
  spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
814
892
  const result = spawnSync("copier", args, {
815
893
  stdio: "inherit",
816
894
  // pass the interactive output through; copier prints its own progress
817
- env,
895
+ env: env2,
818
896
  cwd: ctx.targetDir
819
897
  });
820
898
  spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
@@ -833,7 +911,7 @@ var RunCopierTemplate = class extends Command {
833
911
 
834
912
  // src/commands/hermes/WireTelegram.ts
835
913
  import { spawnSync as spawnSync2 } from "node:child_process";
836
- import { join as join5 } from "node:path";
914
+ import { join as join6 } from "node:path";
837
915
  import { existsSync as existsSync4, unlinkSync } from "node:fs";
838
916
  import * as p3 from "@clack/prompts";
839
917
  var WireTelegram = class extends Command {
@@ -922,14 +1000,14 @@ var WireTelegram = class extends Command {
922
1000
  if (p3.isCancel(allowedAnswer)) {
923
1001
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
924
1002
  }
925
- const script = join5(roleDir, ".scripts", "30-telegram.sh");
1003
+ const script = join6(roleDir, ".scripts", "30-telegram.sh");
926
1004
  if (!existsSync4(script)) {
927
1005
  return {
928
1006
  success: false,
929
1007
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
930
1008
  };
931
1009
  }
932
- const marker = join5(roleDir, ".scripts", ".done-30-telegram");
1010
+ const marker = join6(roleDir, ".scripts", ".done-30-telegram");
933
1011
  if (existsSync4(marker)) unlinkSync(marker);
934
1012
  const spinner4 = p3.spinner();
935
1013
  spinner4.start("Verifying token + wiring profile");
@@ -957,14 +1035,14 @@ function cap(s) {
957
1035
 
958
1036
  // src/commands/hermes/WireEmail.ts
959
1037
  import { spawnSync as spawnSync3 } from "node:child_process";
960
- import { join as join6 } from "node:path";
1038
+ import { join as join7 } from "node:path";
961
1039
  import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
962
1040
  import * as p4 from "@clack/prompts";
963
1041
  var WireEmail = class extends Command {
964
1042
  async invoke() {
965
1043
  const ctx = this.context;
966
1044
  if (ctx.skipEmail) {
967
- return { success: true, message: "\u2192 Email wire-up skipped" };
1045
+ return { success: true, message: "" };
968
1046
  }
969
1047
  if (ctx.dryRun) {
970
1048
  return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
@@ -973,7 +1051,7 @@ var WireEmail = class extends Command {
973
1051
  if (!targetRepo || !role || !roleDir) {
974
1052
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
975
1053
  }
976
- const script = join6(roleDir, ".scripts", "50-email.sh");
1054
+ const script = join7(roleDir, ".scripts", "50-email.sh");
977
1055
  if (!existsSync5(script)) {
978
1056
  return { success: false, message: `\u2717 ${script} not found` };
979
1057
  }
@@ -1036,7 +1114,7 @@ var WireEmail = class extends Command {
1036
1114
  }
1037
1115
  }
1038
1116
  }
1039
- const marker = join6(roleDir, ".scripts", ".done-50-email");
1117
+ const marker = join7(roleDir, ".scripts", ".done-50-email");
1040
1118
  if (existsSync5(marker)) unlinkSync2(marker);
1041
1119
  const spinner4 = p4.spinner();
1042
1120
  spinner4.start("Creating Cloudflare Email Routing rule");
@@ -1072,7 +1150,7 @@ var PrintHermesSummary = class extends Command {
1072
1150
  lines.push(`role dir ${ctx.roleDir}`);
1073
1151
  lines.push(`runtime gh:${runtimeRepo}`);
1074
1152
  lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
1075
- lines.push(`email ${email}${skipEmail ? " (NOT yet wired)" : ""}`);
1153
+ if (!skipEmail) lines.push(`email ${email}`);
1076
1154
  lines.push("");
1077
1155
  lines.push("Start daemons:");
1078
1156
  lines.push(` systemctl --user start ${csm}`);
@@ -1085,11 +1163,10 @@ var PrintHermesSummary = class extends Command {
1085
1163
  lines.push("");
1086
1164
  lines.push("Talk locally:");
1087
1165
  lines.push(` ${ctx.roleDir}/hermes chat "status"`);
1088
- if (skipTelegram || skipEmail) {
1166
+ if (skipTelegram) {
1089
1167
  lines.push("");
1090
- lines.push("Deferred \u2014 re-run pjangler hermes-agent without --yes (or with explicit flags):");
1091
- if (skipTelegram) lines.push(" pjangler hermes-agent --skip-telegram=false # wire just telegram");
1092
- if (skipEmail) lines.push(" pjangler hermes-agent --skip-email=false # wire just email");
1168
+ lines.push("Wire Telegram later:");
1169
+ lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
1093
1170
  }
1094
1171
  p5.note(lines.join("\n"), `Provisioned ${agentId}`);
1095
1172
  p5.outro("Done.");
@@ -1125,7 +1202,7 @@ var HermesAgentRecipe = class extends Recipe {
1125
1202
 
1126
1203
  // src/commands/AgentHooksCommands.ts
1127
1204
  import { homedir as homedir3 } from "node:os";
1128
- import { join as join7, dirname as dirname4 } from "node:path";
1205
+ import { join as join8, dirname as dirname4 } from "node:path";
1129
1206
  import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
1130
1207
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1131
1208
  function resolveTemplateRoot() {
@@ -1136,16 +1213,16 @@ function resolveTemplateRoot() {
1136
1213
  try {
1137
1214
  let dir = dirname4(fileURLToPath2(import.meta.url));
1138
1215
  for (let i = 0; i < 8; i++) {
1139
- candidates.push(join7(dir, "templates", "commonproject", "template"));
1216
+ candidates.push(join8(dir, "templates", "commonproject", "template"));
1140
1217
  const parent = dirname4(dir);
1141
1218
  if (parent === dir) break;
1142
1219
  dir = parent;
1143
1220
  }
1144
1221
  } catch {
1145
1222
  }
1146
- candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1223
+ candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1147
1224
  for (const c of candidates) {
1148
- if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
1225
+ if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
1149
1226
  }
1150
1227
  throw new Error(
1151
1228
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1169,8 +1246,8 @@ var CopyAgentHooksTree = class extends Command {
1169
1246
  const created = [];
1170
1247
  const skipped = [];
1171
1248
  for (const { rel, dir } of items) {
1172
- const src = join7(templateRoot, rel);
1173
- const dest = join7(this.context.targetDir, rel);
1249
+ const src = join8(templateRoot, rel);
1250
+ const dest = join8(this.context.targetDir, rel);
1174
1251
  if (!existsSync6(src)) continue;
1175
1252
  if (existsSync6(dest) && !this.context.force) {
1176
1253
  skipped.push(rel);
@@ -1195,7 +1272,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1195
1272
  static CR = "{{config_root}}";
1196
1273
  // mise's own runtime var — emitted literally
1197
1274
  async invoke() {
1198
- const misePath = join7(this.context.targetDir, "mise.toml");
1275
+ const misePath = join8(this.context.targetDir, "mise.toml");
1199
1276
  if (!existsSync6(misePath)) {
1200
1277
  return {
1201
1278
  success: false,
@@ -1316,7 +1393,7 @@ var RECIPE_REGISTRY = {
1316
1393
  name: "mise",
1317
1394
  description: "Mise task runner and environment setup",
1318
1395
  class: MiseRecipe,
1319
- commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
1396
+ commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
1320
1397
  },
1321
1398
  docker: {
1322
1399
  name: "docker",
@@ -1406,6 +1483,12 @@ var COMMAND_REGISTRY = {
1406
1483
  group: "mise",
1407
1484
  class: AddMiseBaseScript
1408
1485
  },
1486
+ AddMiseCodegraphScript: {
1487
+ name: "AddMiseCodegraphScript",
1488
+ description: "Create .mise/scripts/codegraph.sh enter hook",
1489
+ group: "mise",
1490
+ class: AddMiseCodegraphScript
1491
+ },
1409
1492
  AddDotenv: {
1410
1493
  name: "AddDotenv",
1411
1494
  description: "Create .env.example file",
@@ -1442,11 +1525,11 @@ function createRecipe(name, context) {
1442
1525
  }
1443
1526
 
1444
1527
  // src/index.ts
1445
- import { cancel as cancel2, multiselect, text as text3, isCancel as isCancel5 } from "@clack/prompts";
1528
+ import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
1446
1529
 
1447
1530
  // src/parity/index.ts
1448
- import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
1449
- import { basename as basename2, dirname as dirname5, join as join8, relative, resolve } from "node:path";
1531
+ import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2, copyFileSync } from "node:fs";
1532
+ import { basename as basename2, dirname as dirname5, join as join9, relative, resolve } from "node:path";
1450
1533
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1451
1534
  import { homedir as homedir4 } from "node:os";
1452
1535
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -1520,7 +1603,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
1520
1603
  function resolvePjanglerRoot() {
1521
1604
  let dir = dirname5(fileURLToPath3(import.meta.url));
1522
1605
  while (dir !== dirname5(dir)) {
1523
- if (existsSync7(join8(dir, "package.json")) && existsSync7(join8(dir, "templates", "commonproject", "copier.yml"))) {
1606
+ if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
1524
1607
  return dir;
1525
1608
  }
1526
1609
  dir = dirname5(dir);
@@ -1543,10 +1626,10 @@ function writeText(path, content) {
1543
1626
  ensureParent(path);
1544
1627
  writeFileSync4(path, content);
1545
1628
  }
1546
- function tryParseJson(text4) {
1547
- if (!text4) return null;
1629
+ function tryParseJson(text3) {
1630
+ if (!text3) return null;
1548
1631
  try {
1549
- return JSON.parse(text4);
1632
+ return JSON.parse(text3);
1550
1633
  } catch {
1551
1634
  return null;
1552
1635
  }
@@ -1583,10 +1666,10 @@ function ensureSymlink(path, target, dryRun) {
1583
1666
  return { changed: true };
1584
1667
  }
1585
1668
  function bootstrapAgentsFile(repoRoot, dryRun) {
1586
- const agentsPath = join8(repoRoot, "AGENTS.md");
1669
+ const agentsPath = join9(repoRoot, "AGENTS.md");
1587
1670
  if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
1588
1671
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1589
- const source = join8(repoRoot, file);
1672
+ const source = join9(repoRoot, file);
1590
1673
  if (!existsSync7(source)) continue;
1591
1674
  const stat = lstatSync(source);
1592
1675
  if (stat.isSymbolicLink()) continue;
@@ -1596,7 +1679,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1596
1679
  }
1597
1680
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
1598
1681
  }
1599
- const readmePath = join8(repoRoot, "README.md");
1682
+ const readmePath = join9(repoRoot, "README.md");
1600
1683
  if (existsSync7(readmePath)) {
1601
1684
  const stat = lstatSync(readmePath);
1602
1685
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
@@ -1605,9 +1688,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1605
1688
  }
1606
1689
  return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
1607
1690
  }
1608
- function yamlGet(text4, keyPath) {
1691
+ function yamlGet(text3, keyPath) {
1609
1692
  const parts = keyPath.split(".");
1610
- const lines = text4.split("\n");
1693
+ const lines = text3.split("\n");
1611
1694
  let start = 0;
1612
1695
  let indent = 0;
1613
1696
  for (let idx = 0; idx < parts.length; idx += 1) {
@@ -1636,36 +1719,36 @@ function yamlGet(text4, keyPath) {
1636
1719
  return "";
1637
1720
  }
1638
1721
  function discoverRoles(repoRoot) {
1639
- const rolesDir = join8(repoRoot, "agents", "hermes");
1722
+ const rolesDir = join9(repoRoot, "agents", "hermes");
1640
1723
  if (!existsSync7(rolesDir)) return [];
1641
1724
  return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
1642
- const roleDir = join8(rolesDir, entry.name);
1643
- const roleYamlPath = join8(roleDir, "role.yaml");
1725
+ const roleDir = join9(rolesDir, entry.name);
1726
+ const roleYamlPath = join9(roleDir, "role.yaml");
1644
1727
  if (!existsSync7(roleYamlPath)) return null;
1645
- const text4 = readText(roleYamlPath);
1646
- const runtimeRepoRaw = yamlGet(text4, "runtime.github_repo");
1728
+ const text3 = readText(roleYamlPath);
1729
+ const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
1647
1730
  return {
1648
- role: yamlGet(text4, "role") || entry.name,
1731
+ role: yamlGet(text3, "role") || entry.name,
1649
1732
  roleDir,
1650
1733
  roleYamlPath,
1651
- repo: yamlGet(text4, "repo"),
1652
- agentId: yamlGet(text4, "agent_id"),
1653
- profileName: yamlGet(text4, "profile") || yamlGet(text4, "agent_id"),
1654
- displayName: yamlGet(text4, "display_name"),
1655
- purpose: yamlGet(text4, "purpose"),
1656
- botHandle: yamlGet(text4, "telegram.bot_username"),
1734
+ repo: yamlGet(text3, "repo"),
1735
+ agentId: yamlGet(text3, "agent_id"),
1736
+ profileName: yamlGet(text3, "profile") || yamlGet(text3, "agent_id"),
1737
+ displayName: yamlGet(text3, "display_name"),
1738
+ purpose: yamlGet(text3, "purpose"),
1739
+ botHandle: yamlGet(text3, "telegram.bot_username"),
1657
1740
  runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
1658
- runtimeOwner: yamlGet(text4, "runtime.github_owner"),
1659
- planeWorkspace: yamlGet(text4, "ticket_provider.workspace") || yamlGet(text4, "plane.workspace"),
1660
- ticketProviderName: yamlGet(text4, "ticket_provider.name"),
1661
- ticketProviderBoardId: yamlGet(text4, "ticket_provider.board_id"),
1662
- ticketProviderBoardUrl: yamlGet(text4, "ticket_provider.board_url"),
1663
- ticketProviderIdentifier: yamlGet(text4, "plane.identifier")
1741
+ runtimeOwner: yamlGet(text3, "runtime.github_owner"),
1742
+ planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
1743
+ ticketProviderName: yamlGet(text3, "ticket_provider.name"),
1744
+ ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
1745
+ ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
1746
+ ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
1664
1747
  };
1665
1748
  }).filter((value) => Boolean(value));
1666
1749
  }
1667
1750
  function registryPath(homeDir) {
1668
- return join8(homeDir, ".hermes", "agents-registry.yaml");
1751
+ return join9(homeDir, ".hermes", "agents-registry.yaml");
1669
1752
  }
1670
1753
  function systemctlUser(args) {
1671
1754
  const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -1676,7 +1759,7 @@ function systemctlUser(args) {
1676
1759
  };
1677
1760
  }
1678
1761
  function templateScript(ctx, name) {
1679
- const source = join8(ctx.pjanglerRoot, ".mise", "scripts", name);
1762
+ const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
1680
1763
  return existsSync7(source) ? readText(source) : void 0;
1681
1764
  }
1682
1765
  function templateVersioningScript(ctx) {
@@ -1691,9 +1774,9 @@ function renderGeneratedProjectMiseToml(ctx, template) {
1691
1774
  return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
1692
1775
  }
1693
1776
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1694
- const targetPath = join8(ctx.repoRoot, "mise.toml");
1777
+ const targetPath = join9(ctx.repoRoot, "mise.toml");
1695
1778
  if (existsSync7(targetPath)) return false;
1696
- const sourcePath = join8(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1779
+ const sourcePath = join9(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1697
1780
  if (!existsSync7(sourcePath)) return false;
1698
1781
  changedFiles.push(targetPath);
1699
1782
  if (!ctx.dryRun) {
@@ -1702,22 +1785,22 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1702
1785
  return true;
1703
1786
  }
1704
1787
  function templateVersionFilesConf(ctx, repoRoot) {
1705
- const packageJson = join8(repoRoot, "package.json");
1788
+ const packageJson = join9(repoRoot, "package.json");
1706
1789
  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";
1707
1790
  }
1708
- function replaceOrAppendManagedBlock(text4, startMarker, block, beforePattern) {
1709
- if (startMarker.test(text4)) {
1710
- return text4.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1791
+ function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
1792
+ if (startMarker.test(text3)) {
1793
+ return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1711
1794
  }
1712
1795
  if (beforePattern) {
1713
- const match = text4.match(beforePattern);
1796
+ const match = text3.match(beforePattern);
1714
1797
  if (match && typeof match.index === "number") {
1715
- return `${text4.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1798
+ return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1716
1799
 
1717
- ${text4.slice(match.index)}`;
1800
+ ${text3.slice(match.index)}`;
1718
1801
  }
1719
1802
  }
1720
- return `${text4.replace(/\s*$/, "")}
1803
+ return `${text3.replace(/\s*$/, "")}
1721
1804
 
1722
1805
  ${block}
1723
1806
  `;
@@ -1727,22 +1810,22 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
1727
1810
  function requiredMisePathEntries(ctx) {
1728
1811
  const required = [...BASE_MISE_PATH_ENTRIES];
1729
1812
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
1730
- if (existsSync7(join8(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1813
+ if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1731
1814
  }
1732
1815
  return required;
1733
1816
  }
1734
- function upsertMisePath(text4, required = BASE_MISE_PATH_ENTRIES) {
1817
+ function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
1735
1818
  const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
1736
- const envMatch = text4.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1819
+ const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1737
1820
  if (!envMatch || typeof envMatch.index !== "number") {
1738
1821
  return `[env]
1739
1822
  ${render(required)}
1740
1823
 
1741
- ${text4.replace(/^\s+/, "")}`;
1824
+ ${text3.replace(/^\s+/, "")}`;
1742
1825
  }
1743
- const prefix = text4.slice(0, envMatch.index + envMatch[1].length);
1826
+ const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
1744
1827
  const section = envMatch[2];
1745
- const suffix = text4.slice(envMatch.index + envMatch[1].length + section.length);
1828
+ const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
1746
1829
  const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
1747
1830
  if (!pathLine) {
1748
1831
  return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
@@ -1753,11 +1836,11 @@ ${text4.replace(/^\s+/, "")}`;
1753
1836
  if (!merged.includes(value)) merged.push(value);
1754
1837
  }
1755
1838
  const nextLine = render(merged);
1756
- if (pathLine[0] === nextLine) return text4;
1839
+ if (pathLine[0] === nextLine) return text3;
1757
1840
  return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
1758
1841
  }
1759
- function removeTomlSection(text4, headerPattern, marker, options) {
1760
- const lines = text4.split("\n");
1842
+ function removeTomlSection(text3, headerPattern, marker, options) {
1843
+ const lines = text3.split("\n");
1761
1844
  let start = -1;
1762
1845
  let end = -1;
1763
1846
  for (let i = 0; i < lines.length; i++) {
@@ -1782,7 +1865,7 @@ function removeTomlSection(text4, headerPattern, marker, options) {
1782
1865
  if (end === -1) end = lines.length;
1783
1866
  break;
1784
1867
  }
1785
- if (start === -1) return text4;
1868
+ if (start === -1) return text3;
1786
1869
  if (options?.includePrecedingComments) {
1787
1870
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
1788
1871
  start--;
@@ -1791,22 +1874,22 @@ function removeTomlSection(text4, headerPattern, marker, options) {
1791
1874
  const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
1792
1875
  return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
1793
1876
  }
1794
- function insertTomlBlockBeforeVersioning(text4, block) {
1795
- const versioningIndex = text4.indexOf("# >>> mise-versioning >>>");
1877
+ function insertTomlBlockBeforeVersioning(text3, block) {
1878
+ const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
1796
1879
  if (versioningIndex >= 0) {
1797
- return `${text4.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1880
+ return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1798
1881
 
1799
- ${text4.slice(versioningIndex)}`;
1882
+ ${text3.slice(versioningIndex)}`;
1800
1883
  }
1801
- return `${text4.replace(/\s*$/, "")}
1884
+ return `${text3.replace(/\s*$/, "")}
1802
1885
 
1803
1886
  ${block}
1804
1887
  `;
1805
1888
  }
1806
- function extractTomlStrings(text4) {
1889
+ function extractTomlStrings(text3) {
1807
1890
  const values = [];
1808
1891
  const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
1809
- for (const match of text4.matchAll(stringPattern)) {
1892
+ for (const match of text3.matchAll(stringPattern)) {
1810
1893
  if (match[1] !== void 0) {
1811
1894
  try {
1812
1895
  values.push(JSON.parse(`"${match[1]}"`));
@@ -1830,10 +1913,10 @@ function renderHookEntries(entries, indent = "") {
1830
1913
  `${indent}]`
1831
1914
  ];
1832
1915
  }
1833
- function upsertLinkAgentfilesHooks(text4) {
1834
- const lines = text4.split("\n");
1916
+ function upsertLinkAgentfilesHooks(text3) {
1917
+ const lines = text3.split("\n");
1835
1918
  const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
1836
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text4, LINK_AGENTFILES_HOOKS_BLOCK);
1919
+ if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
1837
1920
  let hooksEnd = lines.length;
1838
1921
  for (let i = hooksStart + 1; i < lines.length; i++) {
1839
1922
  if (/^\[[^\]]+\]/.test(lines[i].trim())) {
@@ -1867,8 +1950,8 @@ function upsertLinkAgentfilesHooks(text4) {
1867
1950
  }
1868
1951
  return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
1869
1952
  }
1870
- function upsertLinkAgentfilesBlock(text4, ctx) {
1871
- const withPath = upsertMisePath(text4, requiredMisePathEntries(ctx));
1953
+ function upsertLinkAgentfilesBlock(text3, ctx) {
1954
+ const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
1872
1955
  if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
1873
1956
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
1874
1957
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
@@ -1876,7 +1959,7 @@ function upsertLinkAgentfilesBlock(text4, ctx) {
1876
1959
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
1877
1960
  }
1878
1961
  function readProjectJson(ctx) {
1879
- return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
1962
+ return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
1880
1963
  }
1881
1964
  function canonicalProjectJson(ctx) {
1882
1965
  const roles = discoverRoles(ctx.repoRoot);
@@ -1920,8 +2003,8 @@ function canonicalProjectJson(ctx) {
1920
2003
  };
1921
2004
  }
1922
2005
  function projectJsonFinding(ctx) {
1923
- const projectPath = join8(ctx.repoRoot, ".project.json");
1924
- const planeJsonPath = join8(ctx.repoRoot, ".plane.json");
2006
+ const projectPath = join9(ctx.repoRoot, ".project.json");
2007
+ const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
1925
2008
  const details = [];
1926
2009
  const data = readProjectJson(ctx);
1927
2010
  const roles = discoverRoles(ctx.repoRoot);
@@ -2035,9 +2118,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2035
2118
  if (!existsSync7(sourceDir)) return;
2036
2119
  mkdirSync5(targetDir, { recursive: true });
2037
2120
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2038
- const sourcePath = join8(sourceDir, entry.name);
2121
+ const sourcePath = join9(sourceDir, entry.name);
2039
2122
  if (skip?.(sourcePath)) continue;
2040
- const targetPath = join8(targetDir, entry.name);
2123
+ const targetPath = join9(targetDir, entry.name);
2041
2124
  if (entry.isDirectory()) {
2042
2125
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2043
2126
  continue;
@@ -2051,7 +2134,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2051
2134
  }
2052
2135
  }
2053
2136
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2054
- const gitmodulesPath = join8(repoRoot, ".gitmodules");
2137
+ const gitmodulesPath = join9(repoRoot, ".gitmodules");
2055
2138
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2056
2139
  const owner = role.runtimeOwner || "delorenj";
2057
2140
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2095,9 +2178,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
2095
2178
  return path;
2096
2179
  }
2097
2180
  function profileMetaInheritsDefault(path) {
2098
- const text4 = safeReadText(path);
2181
+ const text3 = safeReadText(path);
2099
2182
  return Boolean(
2100
- text4 && /^config:\s*$/m.test(text4) && /^\s+inherit_from:\s*default\s*$/m.test(text4) && /^\s+save_mode:\s*delta\s*$/m.test(text4)
2183
+ text3 && /^config:\s*$/m.test(text3) && /^\s+inherit_from:\s*default\s*$/m.test(text3) && /^\s+save_mode:\s*delta\s*$/m.test(text3)
2101
2184
  );
2102
2185
  }
2103
2186
  function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
@@ -2151,21 +2234,21 @@ var RULES = [
2151
2234
  id: "mise.config-root",
2152
2235
  title: "mise config_root + AGENTS link hooks",
2153
2236
  audit: (ctx) => {
2154
- const misePath = join8(ctx.repoRoot, "mise.toml");
2237
+ const misePath = join9(ctx.repoRoot, "mise.toml");
2155
2238
  if (!existsSync7(misePath)) {
2156
2239
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2157
2240
  }
2158
- const text4 = readText(misePath);
2241
+ const text3 = readText(misePath);
2159
2242
  const details = [];
2160
- const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2243
+ const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2161
2244
  if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2162
- const pathValues = [...(text4.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2245
+ const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2163
2246
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2164
2247
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
2165
- if (!text4.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2166
- if (!text4.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2167
- if (!text4.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2168
- if (!text4.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2248
+ if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2249
+ if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2250
+ if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2251
+ if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2169
2252
  return {
2170
2253
  id: "mise.config-root",
2171
2254
  title: "mise config_root + AGENTS link hooks",
@@ -2176,7 +2259,7 @@ var RULES = [
2176
2259
  };
2177
2260
  },
2178
2261
  migrate: (ctx, finding) => {
2179
- const path = join8(ctx.repoRoot, "mise.toml");
2262
+ const path = join9(ctx.repoRoot, "mise.toml");
2180
2263
  const changedFiles = [];
2181
2264
  const details = [];
2182
2265
  if (!existsSync7(path)) {
@@ -2188,14 +2271,14 @@ var RULES = [
2188
2271
  return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
2189
2272
  }
2190
2273
  }
2191
- let text4 = readText(path);
2192
- const next = upsertLinkAgentfilesBlock(text4, ctx);
2193
- if (next !== text4) {
2274
+ let text3 = readText(path);
2275
+ const next = upsertLinkAgentfilesBlock(text3, ctx);
2276
+ if (next !== text3) {
2194
2277
  if (!changedFiles.includes(path)) changedFiles.push(path);
2195
2278
  if (!ctx.dryRun) writeText(path, next);
2196
- text4 = next;
2279
+ text3 = next;
2197
2280
  }
2198
- const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2281
+ const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2199
2282
  const expectedScript = templateLinkAgentfilesScript(ctx);
2200
2283
  if (expectedScript === void 0) {
2201
2284
  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: [] };
@@ -2204,7 +2287,7 @@ var RULES = [
2204
2287
  changedFiles.push(linkAgentfilesPath);
2205
2288
  if (!ctx.dryRun) {
2206
2289
  writeText(linkAgentfilesPath, expectedScript);
2207
- chmodSync(linkAgentfilesPath, 493);
2290
+ chmodSync2(linkAgentfilesPath, 493);
2208
2291
  }
2209
2292
  }
2210
2293
  return {
@@ -2222,11 +2305,11 @@ var RULES = [
2222
2305
  title: "managed mise versioning block",
2223
2306
  audit: (ctx) => {
2224
2307
  const details = [];
2225
- const misePath = join8(ctx.repoRoot, "mise.toml");
2226
- const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2227
- const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
2228
- const text4 = safeReadText(misePath);
2229
- if (!text4?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2308
+ const misePath = join9(ctx.repoRoot, "mise.toml");
2309
+ const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2310
+ const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2311
+ const text3 = safeReadText(misePath);
2312
+ if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2230
2313
  if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2231
2314
  if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
2232
2315
  return {
@@ -2241,7 +2324,7 @@ var RULES = [
2241
2324
  migrate: (ctx, finding) => {
2242
2325
  const changedFiles = [];
2243
2326
  const details = [];
2244
- const misePath = join8(ctx.repoRoot, "mise.toml");
2327
+ const misePath = join9(ctx.repoRoot, "mise.toml");
2245
2328
  if (!existsSync7(misePath)) {
2246
2329
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2247
2330
  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: [] };
@@ -2257,7 +2340,7 @@ var RULES = [
2257
2340
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
2258
2341
  if (!ctx.dryRun) writeText(misePath, nextMise);
2259
2342
  }
2260
- const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2343
+ const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2261
2344
  const expectedScript = templateVersioningScript(ctx);
2262
2345
  if (expectedScript === void 0) {
2263
2346
  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: [] };
@@ -2266,10 +2349,10 @@ var RULES = [
2266
2349
  changedFiles.push(versioningPath);
2267
2350
  if (!ctx.dryRun) {
2268
2351
  writeText(versioningPath, expectedScript);
2269
- chmodSync(versioningPath, 493);
2352
+ chmodSync2(versioningPath, 493);
2270
2353
  }
2271
2354
  }
2272
- const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
2355
+ const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2273
2356
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
2274
2357
  if (safeReadText(manifestPath) !== expectedManifest) {
2275
2358
  changedFiles.push(manifestPath);
@@ -2289,9 +2372,9 @@ var RULES = [
2289
2372
  id: "sot.agent-symlinks",
2290
2373
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
2291
2374
  audit: (ctx) => {
2292
- const agentsPath = join8(ctx.repoRoot, "AGENTS.md");
2375
+ const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
2293
2376
  if (!existsSync7(agentsPath)) {
2294
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join8(ctx.repoRoot, file)));
2377
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
2295
2378
  if (fallbackSources.length === 0) {
2296
2379
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
2297
2380
  }
@@ -2306,7 +2389,7 @@ var RULES = [
2306
2389
  }
2307
2390
  const details = [];
2308
2391
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2309
- const full = join8(ctx.repoRoot, file);
2392
+ const full = join9(ctx.repoRoot, file);
2310
2393
  const target = readSymlinkTarget(full);
2311
2394
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
2312
2395
  }
@@ -2330,7 +2413,7 @@ var RULES = [
2330
2413
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
2331
2414
  }
2332
2415
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2333
- const full = join8(ctx.repoRoot, file);
2416
+ const full = join9(ctx.repoRoot, file);
2334
2417
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
2335
2418
  if (result.blocked) blockedDetails.push(result.blocked);
2336
2419
  if (result.changed) changedFiles.push(full);
@@ -2352,7 +2435,7 @@ var RULES = [
2352
2435
  migrate: (ctx, finding) => {
2353
2436
  const changedFiles = [];
2354
2437
  const details = [];
2355
- const path = join8(ctx.repoRoot, ".project.json");
2438
+ const path = join9(ctx.repoRoot, ".project.json");
2356
2439
  const existing = readProjectJson(ctx) ?? {};
2357
2440
  const canonical = canonicalProjectJson(ctx);
2358
2441
  const merged = { ...existing, ...canonical };
@@ -2362,7 +2445,7 @@ var RULES = [
2362
2445
  changedFiles.push(path);
2363
2446
  if (!ctx.dryRun) writeText(path, expected);
2364
2447
  }
2365
- const planeJson = join8(ctx.repoRoot, ".plane.json");
2448
+ const planeJson = join9(ctx.repoRoot, ".plane.json");
2366
2449
  if (existsSync7(planeJson)) {
2367
2450
  const backup = `${planeJson}.migrated-backup`;
2368
2451
  if (existsSync7(backup)) {
@@ -2387,8 +2470,8 @@ var RULES = [
2387
2470
  title: ".env.op + gitignore secrets contract",
2388
2471
  audit: (ctx) => {
2389
2472
  const details = [];
2390
- const envOp = safeReadText(join8(ctx.repoRoot, ".env.op"));
2391
- const gitignore = safeReadText(join8(ctx.repoRoot, ".gitignore"));
2473
+ const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
2474
+ const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
2392
2475
  if (!envOp) {
2393
2476
  details.push(".env.op missing");
2394
2477
  } else {
@@ -2414,12 +2497,12 @@ var RULES = [
2414
2497
  migrate: (ctx, finding) => {
2415
2498
  const changedFiles = [];
2416
2499
  const details = [];
2417
- const envOpPath = join8(ctx.repoRoot, ".env.op");
2500
+ const envOpPath = join9(ctx.repoRoot, ".env.op");
2418
2501
  if (!existsSync7(envOpPath)) {
2419
2502
  changedFiles.push(envOpPath);
2420
- if (!ctx.dryRun) writeText(envOpPath, readText(join8(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2503
+ if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2421
2504
  }
2422
- const gitignorePath = join8(ctx.repoRoot, ".gitignore");
2505
+ const gitignorePath = join9(ctx.repoRoot, ".gitignore");
2423
2506
  const gitignore = safeReadText(gitignorePath) ?? "";
2424
2507
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
2425
2508
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -2446,20 +2529,20 @@ var RULES = [
2446
2529
  title: ".copier-answers.yml provenance + drift report",
2447
2530
  audit: (ctx) => {
2448
2531
  const details = [];
2449
- const path = join8(ctx.repoRoot, ".copier-answers.yml");
2450
- const text4 = safeReadText(path);
2532
+ const path = join9(ctx.repoRoot, ".copier-answers.yml");
2533
+ const text3 = safeReadText(path);
2451
2534
  const project = readProjectJson(ctx);
2452
- if (!text4) {
2535
+ if (!text3) {
2453
2536
  details.push(".copier-answers.yml missing");
2454
2537
  } else {
2455
- if (!text4.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2456
- if (!text4.includes("_src_path:")) details.push("_src_path missing");
2538
+ if (!text3.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2539
+ if (!text3.includes("_src_path:")) details.push("_src_path missing");
2457
2540
  if (project?.project_name) {
2458
- const nameMatch = text4.match(/project_name:\s*(.+)/);
2541
+ const nameMatch = text3.match(/project_name:\s*(.+)/);
2459
2542
  if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
2460
2543
  }
2461
2544
  if (project?.project_description) {
2462
- const descMatch = text4.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2545
+ const descMatch = text3.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2463
2546
  const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
2464
2547
  if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
2465
2548
  }
@@ -2476,16 +2559,16 @@ var RULES = [
2476
2559
  migrate: (ctx, finding) => {
2477
2560
  const changedFiles = [];
2478
2561
  const project = canonicalProjectJson(ctx);
2479
- const text4 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2480
- _src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
2562
+ const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2563
+ _src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
2481
2564
  project_description: ${String(project.project_description)}
2482
2565
  project_name: ${String(project.project_name)}
2483
2566
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2484
2567
  `;
2485
- const path = join8(ctx.repoRoot, ".copier-answers.yml");
2486
- if (safeReadText(path) !== text4) {
2568
+ const path = join9(ctx.repoRoot, ".copier-answers.yml");
2569
+ if (safeReadText(path) !== text3) {
2487
2570
  changedFiles.push(path);
2488
- if (!ctx.dryRun) writeText(path, text4);
2571
+ if (!ctx.dryRun) writeText(path, text3);
2489
2572
  }
2490
2573
  return {
2491
2574
  id: finding.id,
@@ -2501,15 +2584,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2501
2584
  id: "bmad.scaffold",
2502
2585
  title: "BMAD modules/docs scaffold",
2503
2586
  audit: (ctx) => {
2504
- const sourceRoot = join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2505
- const targetRoot = join8(ctx.repoRoot, "_bmad");
2587
+ const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2588
+ const targetRoot = join9(ctx.repoRoot, "_bmad");
2506
2589
  const sentinels = [
2507
- join8("core", "config.yaml"),
2508
- join8("custom", "config.yaml"),
2509
- join8("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2510
- join8("bmm", "workflows", "workflow-status", "workflow.yaml")
2590
+ join9("core", "config.yaml"),
2591
+ join9("custom", "config.yaml"),
2592
+ join9("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2593
+ join9("bmm", "workflows", "workflow-status", "workflow.yaml")
2511
2594
  ];
2512
- const missing = sentinels.filter((file) => existsSync7(join8(sourceRoot, file)) && !existsSync7(join8(targetRoot, file)));
2595
+ const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
2513
2596
  return {
2514
2597
  id: "bmad.scaffold",
2515
2598
  title: "BMAD modules/docs scaffold",
@@ -2521,7 +2604,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2521
2604
  },
2522
2605
  migrate: (ctx, finding) => {
2523
2606
  const changedFiles = [];
2524
- copyMissingRecursive(join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join8(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2607
+ copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2525
2608
  return {
2526
2609
  id: finding.id,
2527
2610
  title: finding.title,
@@ -2543,11 +2626,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2543
2626
  }
2544
2627
  const details = [];
2545
2628
  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"]) {
2546
- if (!existsSync7(join8(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join8(role.roleDir, rel))}`);
2629
+ if (!existsSync7(join9(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join9(role.roleDir, rel))}`);
2547
2630
  }
2548
- const gitmodules = safeReadText(join8(ctx.repoRoot, ".gitmodules")) ?? "";
2631
+ const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
2549
2632
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
2550
- if (!profileMetaInheritsDefault(join8(role.roleDir, "runtime", "profile.yaml"))) {
2633
+ if (!profileMetaInheritsDefault(join9(role.roleDir, "runtime", "profile.yaml"))) {
2551
2634
  details.push("runtime/profile.yaml missing inherited default config metadata");
2552
2635
  }
2553
2636
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -2568,21 +2651,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2568
2651
  if (!role) {
2569
2652
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
2570
2653
  }
2571
- const templateRoleDir = join8(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2572
- writeIfDifferent(join8(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2573
- writeIfDifferent(join8(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2574
- writeIfDifferent(join8(role.roleDir, ".gitignore"), readText(join8(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2575
- copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2576
- copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2577
- copyMissingRecursive(join8(templateRoleDir, ".scripts"), join8(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2578
- const promptSource = join8(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2579
- const promptTarget = join8(role.roleDir, ".scripts", "sentinel.prompt.md");
2654
+ const templateRoleDir = join9(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2655
+ writeIfDifferent(join9(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2656
+ writeIfDifferent(join9(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2657
+ writeIfDifferent(join9(role.roleDir, ".gitignore"), readText(join9(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2658
+ copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2659
+ copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2660
+ copyMissingRecursive(join9(templateRoleDir, ".scripts"), join9(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2661
+ const promptSource = join9(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2662
+ const promptTarget = join9(role.roleDir, ".scripts", "sentinel.prompt.md");
2580
2663
  if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
2581
2664
  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);
2582
2665
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
2583
2666
  }
2584
2667
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2585
- const profileMetaUpdated = upsertInheritedProfileMeta(join8(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2668
+ const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2586
2669
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2587
2670
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2588
2671
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -2636,9 +2719,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2636
2719
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
2637
2720
  }
2638
2721
  for (const role of roles) {
2639
- const sysDir = join8(ctx.homeDir, ".config", "systemd", "user");
2722
+ const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
2640
2723
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
2641
- const allUnitsPresent = units.every((unit) => existsSync7(join8(sysDir, unit)));
2724
+ const allUnitsPresent = units.every((unit) => existsSync7(join9(sysDir, unit)));
2642
2725
  if (allUnitsPresent) {
2643
2726
  if (ctx.dryRun) {
2644
2727
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -2650,7 +2733,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2650
2733
  }
2651
2734
  continue;
2652
2735
  }
2653
- for (const script of [join8(role.roleDir, ".scripts", "70-systemd.sh")]) {
2736
+ for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
2654
2737
  if (!script || !existsSync7(script)) continue;
2655
2738
  if (ctx.dryRun) {
2656
2739
  details.push(`would run: bash ${script}`);
@@ -2678,7 +2761,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
2678
2761
  changedFiles.push(path);
2679
2762
  if (!dryRun) {
2680
2763
  writeText(path, normalized);
2681
- if (mode) chmodSync(path, mode);
2764
+ if (mode) chmodSync2(path, mode);
2682
2765
  }
2683
2766
  }
2684
2767
  function getParityRuleIds() {
@@ -2740,35 +2823,60 @@ function runMigration(selector, repoArg, dryRun, all) {
2740
2823
  const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
2741
2824
  return runMigrationForRules(ruleIds, repoArg, dryRun);
2742
2825
  }
2826
+ function prettyTimestamp(iso) {
2827
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
2828
+ return match ? `${match[1]} ${match[2]} UTC` : iso;
2829
+ }
2743
2830
  function formatAuditReport(report) {
2744
- const lines = [`repo: ${report.repo}`, `ok: ${report.ok}`, `audited_at: ${report.auditedAt}`, "rules:"];
2831
+ const counts = {};
2832
+ for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
2833
+ const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
2834
+ const tally = [];
2835
+ if (counts.pass) tally.push(green(`${counts.pass} passed`));
2836
+ if (counts.fail) tally.push(red(`${counts.fail} failed`));
2837
+ if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
2838
+ if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
2839
+ const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
2840
+ const lines = [""];
2841
+ lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
2842
+ lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
2843
+ lines.push("");
2745
2844
  for (const rule of report.rules) {
2746
- lines.push(`- ${rule.id} [${rule.status}] ${rule.summary}`);
2747
- for (const detail of rule.details) lines.push(` - ${detail}`);
2845
+ const style = statusStyle(rule.status);
2846
+ lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
2847
+ for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
2748
2848
  }
2749
- return `${lines.join("\n")}
2750
- `;
2849
+ lines.push("");
2850
+ return lines.join("\n");
2751
2851
  }
2752
2852
  function formatMigrationReport(report) {
2753
- const lines = [`repo: ${report.repo}`, `dry_run: ${report.dryRun}`, `ok: ${report.ok}`, `selected_rules: ${report.selectedRules.join(", ")}`, "results:"];
2853
+ const idWidth = report.results.reduce((width, result) => Math.max(width, result.id.length), 0);
2854
+ const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : `${red(glyph.fail)} ${bold("Migration finished with blockers")}`;
2855
+ const lines = [""];
2856
+ lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
2857
+ lines.push(` ${dim(report.repo)}`);
2858
+ if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
2859
+ lines.push("");
2754
2860
  for (const result of report.results) {
2755
- lines.push(`- ${result.id} [${result.status}] ${result.summary}`);
2756
- for (const detail of result.details) lines.push(` - ${detail}`);
2757
- for (const file of result.changedFiles) lines.push(` - changed: ${file}`);
2861
+ const style = statusStyle(result.status);
2862
+ lines.push(` ${style.color(style.glyph)} ${style.color(result.id.padEnd(idWidth))} ${result.summary} ${dim(`[${style.label}]`)}`);
2863
+ for (const detail of result.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
2864
+ for (const file of result.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
2758
2865
  }
2759
2866
  if (report.changedFiles.length) {
2760
- lines.push("changed_files:");
2761
- for (const file of report.changedFiles) lines.push(`- ${file}`);
2867
+ lines.push("");
2868
+ lines.push(` ${bold(`Changed files (${report.changedFiles.length})`)}`);
2869
+ for (const file of report.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
2762
2870
  }
2763
- return `${lines.join("\n")}
2764
- `;
2871
+ lines.push("");
2872
+ return lines.join("\n");
2765
2873
  }
2766
2874
 
2767
2875
  // src/project/index.ts
2768
2876
  import { spawnSync as spawnSync5 } from "node:child_process";
2769
2877
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
2770
2878
  import { homedir as homedir5 } from "node:os";
2771
- import { basename as basename3, dirname as dirname6, join as join9, resolve as resolve2 } from "node:path";
2879
+ import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve2 } from "node:path";
2772
2880
  import YAML from "yaml";
2773
2881
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
2774
2882
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
@@ -2776,10 +2884,10 @@ var KNOWN_SKILL_ROOTS = [
2776
2884
  "/home/delorenj/code/skillex/all-skills",
2777
2885
  "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
2778
2886
  "/home/delorenj/code/pjangler/.agents/skills",
2779
- join9(homedir5(), ".codex", "skills")
2887
+ join10(homedir5(), ".codex", "skills")
2780
2888
  ];
2781
- function projectRegistryPath(env = process.env) {
2782
- return expandHome(env[PROJECT_REGISTRY_ENV] || join9(homedir5(), ".config", "pjangler", "projects.yaml"));
2889
+ function projectRegistryPath(env2 = process.env) {
2890
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
2783
2891
  }
2784
2892
  function emptyProjectRegistry() {
2785
2893
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
@@ -2863,7 +2971,7 @@ function resolveSourceSkillPath(sourceSkill) {
2863
2971
  if (existsSync8(direct)) return direct;
2864
2972
  const name = basename3(sourceSkill);
2865
2973
  for (const root of KNOWN_SKILL_ROOTS) {
2866
- const candidate = join9(root, name);
2974
+ const candidate = join10(root, name);
2867
2975
  if (existsSync8(candidate)) return candidate;
2868
2976
  }
2869
2977
  const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
@@ -2943,7 +3051,7 @@ function planProjectInit(input) {
2943
3051
  }));
2944
3052
  }
2945
3053
  actions.push(
2946
- { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
3054
+ { kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
2947
3055
  {
2948
3056
  kind: "plane.create-or-link",
2949
3057
  enabled: live,
@@ -3050,24 +3158,40 @@ function projectManifestFromRegistryProject(project) {
3050
3158
  };
3051
3159
  }
3052
3160
  function formatProjectInitPlan(plan) {
3053
- const lines = [
3054
- `${plan.dryRun ? "[DRY RUN] " : ""}Project init plan: ${plan.project.name} (${plan.project.slug})`,
3055
- `Registry: ${plan.registryPath}`,
3056
- `Target: ${plan.project.repo_path}`,
3057
- "Actions:"
3058
- ];
3161
+ const lines = [""];
3162
+ const title = `${bold(plan.project.name)} ${dim(`(${plan.project.slug})`)}`;
3163
+ lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
3164
+ lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
3165
+ lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
3166
+ lines.push("");
3167
+ lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
3168
+ if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
3059
3169
  for (const action of plan.actions) {
3060
- lines.push(` - ${action.kind}`);
3061
- if (action.kind === "copier.copy.commonproject") lines.push(` target: ${action.targetDir}`);
3062
- if (action.kind === "project.write-manifest") lines.push(` path: ${action.path}`);
3063
- if (action.kind === "plane.create-or-link" && action.reason) lines.push(` note: ${action.reason}`);
3170
+ lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
3171
+ if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
3172
+ if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
3173
+ if (action.kind === "plane.create-or-link" && action.reason) lines.push(` ${dim(`note: ${action.reason}`)}`);
3064
3174
  }
3175
+ lines.push("");
3065
3176
  return lines.join("\n");
3066
3177
  }
3067
3178
  function formatProjectList(registry) {
3068
3179
  const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
3069
- if (!projects.length) return "No projects registered.";
3070
- return projects.map((project) => `${project.slug.padEnd(18)} ${String(project.ticket_provider.identifier ?? "").padEnd(6)} ${project.status.padEnd(8)} ${project.repo_path}`).join("\n");
3180
+ if (!projects.length) return `
3181
+ ${dim("No projects registered.")}
3182
+ `;
3183
+ const slugWidth = projects.reduce((width, project) => Math.max(width, project.slug.length), 0);
3184
+ const idWidth = projects.reduce((width, project) => Math.max(width, String(project.ticket_provider.identifier ?? "").length), 0);
3185
+ const statusWidth = projects.reduce((width, project) => Math.max(width, project.status.length), 0);
3186
+ const lines = ["", ` ${bold("Projects")} ${dim(`(${projects.length})`)}`, ""];
3187
+ for (const project of projects) {
3188
+ const slug = bold(project.slug.padEnd(slugWidth));
3189
+ const identifier = cyan(String(project.ticket_provider.identifier ?? "").padEnd(idWidth));
3190
+ const status = projectStatusColor(project.status)(project.status.padEnd(statusWidth));
3191
+ lines.push(` ${slug} ${identifier} ${status} ${dim(project.repo_path)}`);
3192
+ }
3193
+ lines.push("");
3194
+ return lines.join("\n");
3071
3195
  }
3072
3196
  function getProject(registry, slug) {
3073
3197
  const project = registry.projects[slug];
@@ -3084,7 +3208,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
3084
3208
  } else if (!statSync(project.repo_path).isDirectory()) {
3085
3209
  issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
3086
3210
  } else {
3087
- const manifestPath = join9(project.repo_path, ".project.json");
3211
+ const manifestPath = join10(project.repo_path, ".project.json");
3088
3212
  if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
3089
3213
  }
3090
3214
  for (const artifact of project.source_artifacts) {
@@ -3101,7 +3225,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
3101
3225
  };
3102
3226
  }
3103
3227
  function buildCommonProjectCopierAction(input) {
3104
- const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
3228
+ const templateDir = join10(input.pjanglerRoot, "templates", "commonproject");
3105
3229
  const data = {
3106
3230
  project_name: input.projectName,
3107
3231
  project_description: input.projectDescription ?? "",
@@ -3127,7 +3251,7 @@ function buildCommonProjectCopierAction(input) {
3127
3251
  function resolvePjanglerRoot2() {
3128
3252
  let dir = dirname6(new URL(import.meta.url).pathname);
3129
3253
  while (dir !== dirname6(dir)) {
3130
- if (existsSync8(join9(dir, "package.json")) && existsSync8(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
3254
+ if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) return dir;
3131
3255
  dir = dirname6(dir);
3132
3256
  }
3133
3257
  return resolve2(process.cwd());
@@ -3159,7 +3283,7 @@ function validateProjectRecord(project, key) {
3159
3283
  }
3160
3284
  function expandHome(path) {
3161
3285
  if (path === "~") return homedir5();
3162
- if (path.startsWith("~/")) return join9(homedir5(), path.slice(2));
3286
+ if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
3163
3287
  return path;
3164
3288
  }
3165
3289
  function isRecord(value) {
@@ -3168,14 +3292,14 @@ function isRecord(value) {
3168
3292
 
3169
3293
  // src/utils/version.ts
3170
3294
  import { readFileSync as readFileSync5 } from "node:fs";
3171
- import { dirname as dirname7, join as join10 } from "node:path";
3295
+ import { dirname as dirname7, join as join11 } from "node:path";
3172
3296
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3173
3297
  var PJANGLER_VERSION = (() => {
3174
3298
  try {
3175
3299
  let dir = dirname7(fileURLToPath4(import.meta.url));
3176
3300
  for (let i = 0; i < 4; i++) {
3177
3301
  try {
3178
- const raw = readFileSync5(join10(dir, "package.json"), "utf8");
3302
+ const raw = readFileSync5(join11(dir, "package.json"), "utf8");
3179
3303
  return JSON.parse(raw).version ?? "0.0.0";
3180
3304
  } catch {
3181
3305
  const parent = dirname7(dir);
@@ -3189,6 +3313,7 @@ var PJANGLER_VERSION = (() => {
3189
3313
  })();
3190
3314
 
3191
3315
  // src/index.ts
3316
+ var xmark = `${red(glyph.fail)}`;
3192
3317
  function printMigrationReport(report, asJson) {
3193
3318
  if (asJson) {
3194
3319
  console.log(JSON.stringify(report, null, 2));
@@ -3236,8 +3361,8 @@ function packageNameToProjectName(value) {
3236
3361
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
3237
3362
  }
3238
3363
  function deriveProjectDefaults(targetDir) {
3239
- const manifest = readJson(join11(targetDir, ".project.json"));
3240
- const pkg = readJson(join11(targetDir, "package.json"));
3364
+ const manifest = readJson(join12(targetDir, ".project.json"));
3365
+ const pkg = readJson(join12(targetDir, "package.json"));
3241
3366
  const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
3242
3367
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
3243
3368
  return {
@@ -3251,7 +3376,7 @@ function isInteractiveProjectInit(options) {
3251
3376
  return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
3252
3377
  }
3253
3378
  async function promptTextValue(message, initialValue) {
3254
- const value = await text3({
3379
+ const value = await text2({
3255
3380
  message,
3256
3381
  initialValue,
3257
3382
  validate: (input) => input?.trim() ? void 0 : "Required"
@@ -3348,7 +3473,7 @@ async function resolveProjectInitTarget(name, options) {
3348
3473
  if (!targetDir && interactive) {
3349
3474
  const defaultName = name ?? basename4(cwd);
3350
3475
  const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
3351
- const defaultDir = join11(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
3476
+ const defaultDir = join12(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
3352
3477
  targetDir = await promptTextValue("Project directory", defaultDir);
3353
3478
  name = promptedName;
3354
3479
  }
@@ -3384,27 +3509,30 @@ program.command("init").argument("<subsystem>", "Subsystem to initialize").descr
3384
3509
  try {
3385
3510
  const recipe = createRecipe(subsystem, context);
3386
3511
  if (!recipe) {
3387
- console.error(`\u274C Unknown subsystem: ${subsystem}`);
3388
- console.log(`Available subsystems: ${getRecipeNames().join(", ")}`);
3512
+ console.error(`${xmark} Unknown subsystem: ${bold(subsystem)}`);
3513
+ console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
3389
3514
  process.exit(1);
3390
3515
  }
3391
3516
  await recipe.execute();
3392
3517
  } catch (error) {
3393
- console.error(`\u274C Error initializing ${subsystem}:`, error);
3518
+ console.error(`${xmark} Error initializing ${bold(subsystem)}:`, error);
3394
3519
  process.exit(1);
3395
3520
  }
3396
3521
  });
3397
3522
  program.command("list").description("List available subsystems").action(() => {
3398
- console.log("Available subsystems:");
3523
+ const width = Object.keys(RECIPE_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
3524
+ console.log("");
3525
+ console.log(` ${heading("Available subsystems")}`);
3399
3526
  console.log("");
3400
3527
  for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
3401
- console.log(` ${name.padEnd(10)} - ${info.description}`);
3528
+ console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
3529
+ }
3530
+ console.log("");
3531
+ console.log(` ${dim("Examples")}`);
3532
+ for (const example of ["pj init mise", "pj init docker", "pj init node"]) {
3533
+ console.log(` ${dim(glyph.pointer)} ${dim(example)}`);
3402
3534
  }
3403
3535
  console.log("");
3404
- console.log("Usage examples:");
3405
- console.log(" pjangler init mise");
3406
- console.log(" pjangler init docker");
3407
- console.log(" pjangler init node");
3408
3536
  });
3409
3537
  var projectCmd = program.command("project").description("Manage the pjangler project registry");
3410
3538
  projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
@@ -3458,11 +3586,12 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
3458
3586
  else {
3459
3587
  console.log(formatProjectInitPlan(plan));
3460
3588
  if (payload.proposedOperations.length) {
3461
- console.log("Proposed sync operations:");
3462
- for (const operation of payload.proposedOperations) console.log(` - ${operation}`);
3589
+ console.log(` ${bold("Proposed operations")} ${dim(`(${payload.proposedOperations.length})`)}`);
3590
+ for (const operation of payload.proposedOperations) console.log(` ${cyan(glyph.bullet)} ${operation}`);
3463
3591
  } else {
3464
- console.log("Project is already in parity.");
3592
+ console.log(` ${green(glyph.pass)} ${dim("Project is already in parity.")}`);
3465
3593
  }
3594
+ console.log("");
3466
3595
  }
3467
3596
  return;
3468
3597
  }
@@ -3490,17 +3619,19 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
3490
3619
  } else {
3491
3620
  console.log(formatProjectInitPlan(selectedPlan));
3492
3621
  for (const line of result.logs) console.log(line);
3493
- for (const line of result.errors) console.error(line);
3622
+ for (const line of result.errors) console.error(` ${xmark} ${line}`);
3494
3623
  if (migrationReport) console.log(formatMigrationReport(migrationReport));
3495
- if (result.ok && changedFiles.length) console.log(`Project synchronized: ${plan.project.slug}`);
3496
- if (result.ok && changedFiles.length === 0) console.log(`Project already in parity: ${plan.project.slug}`);
3624
+ if (result.ok && changedFiles.length) console.log(` ${green(glyph.pass)} ${bold("Project synchronized")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
3625
+ `);
3626
+ if (result.ok && changedFiles.length === 0) console.log(` ${green(glyph.pass)} ${dim("Already in parity")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
3627
+ `);
3497
3628
  }
3498
3629
  process.exitCode = result.ok ? 0 : 1;
3499
3630
  } catch (err) {
3500
3631
  if (options.json) {
3501
3632
  console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
3502
3633
  } else {
3503
- console.error("\u274C project init failed:", err instanceof Error ? err.message : err);
3634
+ console.error(`${xmark} project init failed:`, err instanceof Error ? err.message : err);
3504
3635
  }
3505
3636
  process.exit(1);
3506
3637
  }
@@ -3511,19 +3642,24 @@ projectCmd.command("list").description("List projects in the pjangler registry")
3511
3642
  if (options.json) console.log(JSON.stringify(registry, null, 2));
3512
3643
  else console.log(formatProjectList(registry));
3513
3644
  } catch (err) {
3514
- console.error("\u274C project list failed:", err instanceof Error ? err.message : err);
3645
+ console.error(`${xmark} project list failed:`, err instanceof Error ? err.message : err);
3515
3646
  process.exit(1);
3516
3647
  }
3517
3648
  });
3518
3649
  projectCmd.command("show").argument("<slug>", "Project slug").description("Show one project from the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
3519
3650
  try {
3520
3651
  const project = getProject(loadProjectRegistry(options.registry ?? projectRegistryPath()), slug);
3521
- if (options.json) console.log(JSON.stringify(project, null, 2));
3522
- else console.log(`${project.name} (${project.slug})
3523
- ${project.repo_path}
3524
- ${project.description}`);
3652
+ if (options.json) {
3653
+ console.log(JSON.stringify(project, null, 2));
3654
+ } else {
3655
+ console.log("");
3656
+ console.log(` ${heading(project.name)} ${dim(`(${project.slug})`)}`);
3657
+ console.log(` ${dim(project.repo_path)}`);
3658
+ if (project.description) console.log(` ${project.description}`);
3659
+ console.log("");
3660
+ }
3525
3661
  } catch (err) {
3526
- console.error("\u274C project show failed:", err instanceof Error ? err.message : err);
3662
+ console.error(`${xmark} project show failed:`, err instanceof Error ? err.message : err);
3527
3663
  process.exit(1);
3528
3664
  }
3529
3665
  });
@@ -3533,50 +3669,59 @@ projectCmd.command("doctor").argument("[slug]", "Optional project slug").descrip
3533
3669
  if (options.json) {
3534
3670
  console.log(JSON.stringify(report, null, 2));
3535
3671
  } else if (!report.issues.length) {
3536
- console.log(`Project registry OK: ${report.registryPath}`);
3672
+ console.log("");
3673
+ console.log(` ${green(glyph.pass)} ${bold("Project registry OK")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
3674
+ console.log("");
3537
3675
  } else {
3538
- console.log(`Project registry issues: ${report.registryPath}`);
3539
- for (const issue of report.issues) console.log(` [${issue.level}] ${issue.slug ?? "registry"}: ${issue.message}`);
3676
+ console.log("");
3677
+ console.log(` ${red(glyph.fail)} ${bold("Project registry issues")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
3678
+ console.log("");
3679
+ for (const issue of report.issues) {
3680
+ const mark = issue.level === "error" ? red(glyph.fail) : yellow(glyph.warn);
3681
+ console.log(` ${mark} ${bold(issue.slug ?? "registry")} ${issue.message}`);
3682
+ }
3683
+ console.log("");
3540
3684
  }
3541
3685
  process.exit(report.ok ? 0 : 1);
3542
3686
  } catch (err) {
3543
- console.error("\u274C project doctor failed:", err instanceof Error ? err.message : err);
3687
+ console.error(`${xmark} project doctor failed:`, err instanceof Error ? err.message : err);
3544
3688
  process.exit(1);
3545
3689
  }
3546
3690
  });
3547
3691
  var recipeCmd = program.command("recipe").description("Manage pjangler recipes");
3548
3692
  recipeCmd.command("list").description("List all available recipes").action(() => {
3549
- console.log("\u{1F4E6} Available Recipes:");
3693
+ console.log("");
3694
+ console.log(` ${heading("Recipes")}`);
3550
3695
  console.log("");
3551
3696
  for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
3552
- console.log(` ${name}`);
3553
- console.log(` ${info.description}`);
3554
- console.log(` Commands: ${info.commands.join(", ")}`);
3697
+ console.log(` ${cyan(bold(name))}`);
3698
+ console.log(` ${dim(info.description)}`);
3699
+ console.log(` ${dim("commands")} ${info.commands.map((command) => cyan(command)).join(dim(", "))}`);
3555
3700
  console.log("");
3556
3701
  }
3557
- console.log("Usage:");
3558
- console.log(" pjangler recipe run <name>");
3559
- console.log(" pjangler recipe describe <name>");
3702
+ console.log(` ${dim("Usage")}`);
3703
+ console.log(` ${dim(glyph.pointer)} ${dim("pj recipe run <name>")}`);
3704
+ console.log(` ${dim(glyph.pointer)} ${dim("pj recipe describe <name>")}`);
3705
+ console.log("");
3560
3706
  });
3561
3707
  recipeCmd.command("describe").argument("<name>", "Recipe name").description("Show detailed information about a recipe").action((name) => {
3562
3708
  const info = getRecipeInfo(name);
3563
3709
  if (!info) {
3564
- console.error(`\u274C Recipe not found: ${name}`);
3565
- console.log(`Available recipes: ${getRecipeNames().join(", ")}`);
3710
+ console.error(`${xmark} Recipe not found: ${bold(name)}`);
3711
+ console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
3566
3712
  process.exit(1);
3567
3713
  }
3568
- console.log(`\u{1F4E6} Recipe: ${info.name}`);
3569
3714
  console.log("");
3570
- console.log(`Description: ${info.description}`);
3715
+ console.log(` ${heading(info.name)}`);
3716
+ console.log(` ${dim(info.description)}`);
3571
3717
  console.log("");
3572
- console.log("Commands:");
3573
- for (const cmd of info.commands) {
3574
- console.log(` - ${cmd}`);
3575
- }
3718
+ console.log(` ${bold("Commands")}`);
3719
+ for (const command of info.commands) console.log(` ${cyan(glyph.bullet)} ${command}`);
3720
+ console.log("");
3721
+ console.log(` ${dim("Usage")}`);
3722
+ console.log(` ${dim(glyph.pointer)} ${dim(`pj recipe run ${name}`)}`);
3723
+ console.log(` ${dim(glyph.pointer)} ${dim(`pj init ${name}`)}`);
3576
3724
  console.log("");
3577
- console.log("Usage:");
3578
- console.log(` pjangler recipe run ${name}`);
3579
- console.log(` pjangler init ${name}`);
3580
3725
  });
3581
3726
  recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute a specific recipe").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (name, options) => {
3582
3727
  const context = {
@@ -3587,80 +3732,73 @@ recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute
3587
3732
  try {
3588
3733
  const recipe = createRecipe(name, context);
3589
3734
  if (!recipe) {
3590
- console.error(`\u274C Recipe not found: ${name}`);
3591
- console.log(`Available recipes: ${getRecipeNames().join(", ")}`);
3735
+ console.error(`${xmark} Recipe not found: ${bold(name)}`);
3736
+ console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
3592
3737
  process.exit(1);
3593
3738
  }
3594
- const dryRunPrefix = context.dryRun ? "[DRY RUN] " : "";
3595
- console.log(`${dryRunPrefix}\u{1F680} Running recipe: ${name}`);
3596
- console.log("");
3597
3739
  await recipe.execute();
3598
3740
  } catch (error) {
3599
- console.error(`\u274C Error running recipe ${name}:`, error);
3741
+ console.error(`${xmark} Error running recipe ${bold(name)}:`, error);
3600
3742
  process.exit(1);
3601
3743
  }
3602
3744
  });
3603
3745
  var commandCmd = program.command("command").alias("cmd").description("Manage pjangler commands");
3604
3746
  commandCmd.command("list").description("List all available commands").option("-g, --group", "Group commands by category").action((options) => {
3747
+ console.log("");
3605
3748
  if (options.group) {
3606
- console.log("\u2699\uFE0F Available Commands (Grouped):");
3607
- console.log("");
3608
- const grouped = getCommandsByGroup();
3609
- for (const [group, commands] of Object.entries(grouped)) {
3610
- console.log(` ${group.toUpperCase()}:`);
3611
- for (const cmd of commands) {
3612
- console.log(` ${cmd.name.padEnd(30)} - ${cmd.description}`);
3613
- }
3749
+ console.log(` ${heading("Commands by category")}`);
3750
+ for (const [group, commands] of Object.entries(getCommandsByGroup())) {
3751
+ const width = commands.reduce((max, command) => Math.max(max, command.name.length), 0);
3614
3752
  console.log("");
3753
+ console.log(` ${bold(group.toUpperCase())}`);
3754
+ for (const command of commands) {
3755
+ console.log(` ${cyan(command.name.padEnd(width))} ${dim(command.description)}`);
3756
+ }
3615
3757
  }
3758
+ console.log("");
3616
3759
  } else {
3617
- console.log("\u2699\uFE0F Available Commands:");
3760
+ const width = Object.keys(COMMAND_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
3761
+ console.log(` ${heading("Commands")}`);
3618
3762
  console.log("");
3619
3763
  for (const [name, info] of Object.entries(COMMAND_REGISTRY)) {
3620
- console.log(` ${name.padEnd(30)} - ${info.description}`);
3764
+ console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
3621
3765
  }
3622
3766
  console.log("");
3623
3767
  }
3624
- console.log("Usage:");
3625
- console.log(" pj command list --group # Group by category");
3626
- console.log(" pj command describe <name> # Show command details");
3768
+ console.log(` ${dim("Usage")}`);
3769
+ console.log(` ${dim(glyph.pointer)} ${dim("pj command list --group")} ${dim("# group by category")}`);
3770
+ console.log(` ${dim(glyph.pointer)} ${dim("pj command describe <name>")} ${dim("# command details")}`);
3771
+ console.log("");
3627
3772
  });
3628
3773
  commandCmd.command("describe").argument("<name>", "Command name").description("Show detailed information about a command").action((name) => {
3629
3774
  const info = getCommandInfo(name);
3630
3775
  if (!info) {
3631
- console.error(`\u274C Command not found: ${name}`);
3632
- console.log(`Available commands: ${getCommandNames().join(", ")}`);
3776
+ console.error(`${xmark} Command not found: ${bold(name)}`);
3777
+ console.error(` ${dim("Available:")} ${getCommandNames().map((available) => cyan(available)).join(dim(", "))}`);
3633
3778
  process.exit(1);
3634
3779
  }
3635
- console.log(`\u2699\uFE0F Command: ${info.name}`);
3780
+ const usedIn = Object.entries(RECIPE_REGISTRY).filter(([, recipeInfo]) => recipeInfo.commands.includes(name)).map(([recipeName]) => recipeName);
3636
3781
  console.log("");
3637
- console.log(`Description: ${info.description}`);
3638
- console.log(`Group: ${info.group}`);
3782
+ console.log(` ${heading(info.name)}`);
3783
+ console.log(` ${dim(info.description)}`);
3639
3784
  console.log("");
3640
- console.log("This command is used in recipes:");
3641
- for (const [recipeName, recipeInfo] of Object.entries(RECIPE_REGISTRY)) {
3642
- if (recipeInfo.commands.includes(name)) {
3643
- console.log(` - ${recipeName}`);
3644
- }
3645
- }
3785
+ console.log(` ${dim("group".padEnd(7))} ${cyan(info.group)}`);
3786
+ console.log(` ${dim("recipes".padEnd(7))} ${usedIn.length ? usedIn.map((recipeName) => cyan(recipeName)).join(dim(", ")) : dim("(none)")}`);
3787
+ console.log("");
3788
+ console.log(` ${dim("Part of recipe execution (not run directly).")}`);
3646
3789
  console.log("");
3647
- console.log("Usage:");
3648
- console.log(` Part of recipe execution (not run directly)`);
3649
3790
  });
3650
3791
  commandCmd.command("create").argument("<name>", "Command name").argument("<prompt>", "Description of what the command should do").description("Create a new command from template (placeholder for STORY-005)").option("-t, --template <type>", "Template type (toml, json, yaml, dockerfile)").option("-m, --model <model>", "LLM model to use (OpenRouter)").action((name, prompt, options) => {
3651
- console.log("\u{1F6A7} Command generation coming in STORY-005!");
3652
3792
  console.log("");
3653
- console.log("Planned features:");
3654
- console.log(` - Generate ${name} from prompt: "${prompt}"`);
3655
- if (options.template) {
3656
- console.log(` - Template type: ${options.template}`);
3657
- }
3658
- if (options.model) {
3659
- console.log(` - LLM model: ${options.model}`);
3660
- }
3793
+ console.log(` ${yellow(glyph.warn)} ${bold("Command generation coming in STORY-005")}`);
3794
+ console.log("");
3795
+ console.log(` ${dim("Planned")}`);
3796
+ console.log(` ${cyan(glyph.bullet)} Generate ${bold(name)} from prompt: ${dim(`"${prompt}"`)}`);
3797
+ if (options.template) console.log(` ${cyan(glyph.bullet)} Template type: ${cyan(options.template)}`);
3798
+ if (options.model) console.log(` ${cyan(glyph.bullet)} LLM model: ${cyan(options.model)}`);
3799
+ console.log("");
3800
+ console.log(` ${dim("For now, manually create commands in src/commands/")}`);
3661
3801
  console.log("");
3662
- console.log("This feature will be implemented in the Template Generation System story.");
3663
- console.log("For now, manually create commands in src/commands/");
3664
3802
  });
3665
3803
  program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd)").description("Deterministic parity audit against 33god project standard").option("--json", "Output machine-parseable JSON").action((repo, options) => {
3666
3804
  try {
@@ -3672,7 +3810,7 @@ program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd
3672
3810
  }
3673
3811
  process.exit(report.ok ? 0 : 1);
3674
3812
  } catch (err) {
3675
- console.error("\u274C audit failed:", err);
3813
+ console.error(`${xmark} audit failed:`, err);
3676
3814
  process.exit(1);
3677
3815
  }
3678
3816
  });
@@ -3691,7 +3829,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
3691
3829
  }
3692
3830
  if (ruleId && repo) {
3693
3831
  if (!getParityRuleIds().includes(ruleId)) {
3694
- console.error(`\u274C Unknown parity rule: ${ruleId}`);
3832
+ console.error(`${xmark} Unknown parity rule: ${bold(ruleId)}`);
3695
3833
  process.exit(1);
3696
3834
  }
3697
3835
  const report2 = runMigration(ruleId, repo, dryRun, false);
@@ -3704,29 +3842,29 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
3704
3842
  process.exit(report2.ok ? 0 : 1);
3705
3843
  }
3706
3844
  if (options.json) {
3707
- console.error("\u274C JSON output requires a rule-id or --all");
3845
+ console.error(`${xmark} JSON output requires a rule-id or --all`);
3708
3846
  process.exit(1);
3709
3847
  }
3710
3848
  if (!process.stdin.isTTY) {
3711
- console.error("\u274C Provide a rule-id, use --all, or run in an interactive terminal");
3849
+ console.error(`${xmark} Provide a rule-id, use --all, or run in an interactive terminal`);
3712
3850
  process.exit(1);
3713
3851
  }
3714
3852
  const targetRepo = ruleId ?? repo;
3715
3853
  const audit = runAudit(targetRepo);
3716
3854
  const ruleIds = await promptForRuleIds(audit.rules);
3717
3855
  if (!ruleIds.length) {
3718
- console.log("No rules selected; nothing to migrate.");
3856
+ console.log(` ${cyan(glyph.info)} ${dim("No rules selected; nothing to migrate.")}`);
3719
3857
  process.exit(0);
3720
3858
  }
3721
3859
  const report = runMigrationForRules(ruleIds, targetRepo, dryRun);
3722
3860
  printMigrationReport(report, false);
3723
3861
  process.exit(report.ok ? 0 : 1);
3724
3862
  } catch (err) {
3725
- console.error("\u274C migrate failed:", err);
3863
+ console.error(`${xmark} migrate failed:`, err);
3726
3864
  process.exit(1);
3727
3865
  }
3728
3866
  });
3729
- program.command("hermes-agent").alias("hermes").description("Provision a Hermes agent role into the current repo (TUI; --yes for non-interactive)").option("-y, --yes", "Non-interactive: accept all defaults (skips Telegram + email)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role (pm | dev | review | ops | qa | ci | ...)").option("--purpose <text>", "One-line agent purpose").option(`--tone <tone>`, `Personality tone (${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip BotFather token capture step").option("--skip-email", "Skip Cloudflare Email Routing step").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating the Plane project").option("--skip-bloodbank", "Skip installing the Bloodbank NATS consumer").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, Plane, Bloodbank, and systemd (safe for laptops/macOS/non-technical operators)").option("--force-config", "Regenerate ~/.config/hermes-agent-template/config.toml even if it exists").option("--dry-run", "Preview what would run; don't execute copier").option("-f, --force", "Re-render even if agents/hermes/<role>/role.yaml already exists").action(async (options) => {
3867
+ program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating the Plane project").option("--skip-bloodbank", "Skip installing the Bloodbank NATS consumer").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, Plane, Bloodbank, and systemd (safe for laptops/macOS/non-technical operators)").option("--force-config", "Regenerate ~/.config/hermes-agent-template/config.toml even if it exists").option("--dry-run", "Preview what would run; don't execute copier").option("-f, --force", "Re-render even if agents/hermes/<role>/role.yaml already exists").action(async (options) => {
3730
3868
  const isDarwin = process.platform === "darwin";
3731
3869
  const local = options.local ?? false;
3732
3870
  const context = {
@@ -3743,7 +3881,8 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
3743
3881
  modelProvider: options.modelProvider,
3744
3882
  modelName: options.modelName,
3745
3883
  skipTelegram: options.skipTelegram,
3746
- skipEmail: options.skipEmail,
3884
+ // Email is opt-in only: `--email` wires it, otherwise it's never done.
3885
+ skipEmail: options.email ? false : void 0,
3747
3886
  // --local (and macOS, for systemd) flip the heavy/irreversible steps off
3748
3887
  // by default so a non-technical operator can't accidentally create cloud
3749
3888
  // resources under the wrong account or hit systemd on a Mac. An explicit
@@ -3757,12 +3896,12 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
3757
3896
  try {
3758
3897
  const recipe = createRecipe("hermes-agent", context);
3759
3898
  if (!recipe) {
3760
- console.error("\u274C hermes-agent recipe not registered");
3899
+ console.error(`${xmark} hermes-agent recipe not registered`);
3761
3900
  process.exit(1);
3762
3901
  }
3763
3902
  await recipe.execute();
3764
3903
  } catch (err) {
3765
- console.error("\u274C hermes-agent failed:", err);
3904
+ console.error(`${xmark} hermes-agent failed:`, err);
3766
3905
  process.exit(1);
3767
3906
  }
3768
3907
  });
@@ -3780,14 +3919,15 @@ configCmd.command("bootstrap").description("Create ~/.config/hermes-agent-templa
3780
3919
  }
3781
3920
  });
3782
3921
  program.command("describe").description("Describe the current project (for AI context)").action(() => {
3783
- console.log("\u{1F50D} Project Description (placeholder for future enhancement)");
3784
3922
  console.log("");
3785
- console.log("This command will analyze the project and provide:");
3786
- console.log(" - Detected project type");
3787
- console.log(" - Installed subsystems");
3788
- console.log(" - Configuration files present");
3789
- console.log(" - Suggested next steps");
3923
+ console.log(` ${heading("Project description")} ${dim("(placeholder)")}`);
3924
+ console.log("");
3925
+ console.log(` ${dim("Will analyze the project and report:")}`);
3926
+ for (const item of ["Detected project type", "Installed subsystems", "Configuration files present", "Suggested next steps"]) {
3927
+ console.log(` ${cyan(glyph.bullet)} ${item}`);
3928
+ }
3929
+ console.log("");
3930
+ console.log(` ${dim("Coming soon.")}`);
3790
3931
  console.log("");
3791
- console.log("Coming soon!");
3792
3932
  });
3793
3933
  program.parse();