@delorenj/pjangler 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
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
@@ -159,6 +159,78 @@ var EnsureTemplateConfig = class extends Command {
159
159
  }
160
160
  };
161
161
 
162
+ // src/utils/style.ts
163
+ var env = process.env;
164
+ function detectColor() {
165
+ if ("NO_COLOR" in env && env.NO_COLOR !== "") return false;
166
+ const force = env.FORCE_COLOR;
167
+ if (force === "0" || force === "false") return false;
168
+ if (force !== void 0 && force !== "") return true;
169
+ if (env.TERM === "dumb") return false;
170
+ return Boolean(process.stdout.isTTY);
171
+ }
172
+ var colorEnabled = detectColor();
173
+ function sgr(open, close) {
174
+ const prefix = `\x1B[${open}m`;
175
+ const suffix = `\x1B[${close}m`;
176
+ return (value) => colorEnabled ? `${prefix}${value}${suffix}` : String(value);
177
+ }
178
+ var bold = sgr(1, 22);
179
+ var dim = sgr(2, 22);
180
+ var italic = sgr(3, 23);
181
+ var underline = sgr(4, 24);
182
+ var red = sgr(31, 39);
183
+ var green = sgr(32, 39);
184
+ var yellow = sgr(33, 39);
185
+ var blue = sgr(34, 39);
186
+ var magenta = sgr(35, 39);
187
+ var cyan = sgr(36, 39);
188
+ var gray = sgr(90, 39);
189
+ var glyph = {
190
+ pass: "\u2714",
191
+ fail: "\u2716",
192
+ warn: "\u26A0",
193
+ skip: "\u25CB",
194
+ info: "\u2139",
195
+ arrow: "\u21B3",
196
+ bullet: "\u2022",
197
+ dot: "\xB7",
198
+ add: "+",
199
+ chevron: "\u25B8",
200
+ pointer: "\u276F"
201
+ };
202
+ var STATUS_STYLES = {
203
+ pass: { glyph: glyph.pass, color: green, label: "pass" },
204
+ fail: { glyph: glyph.fail, color: red, label: "fail" },
205
+ warn: { glyph: glyph.warn, color: yellow, label: "warn" },
206
+ skip: { glyph: glyph.skip, color: gray, label: "skip" },
207
+ applied: { glyph: glyph.pass, color: green, label: "applied" },
208
+ noop: { glyph: glyph.skip, color: gray, label: "noop" },
209
+ blocked: { glyph: glyph.fail, color: red, label: "blocked" },
210
+ skipped: { glyph: glyph.skip, color: gray, label: "skipped" }
211
+ };
212
+ function statusStyle(status) {
213
+ return STATUS_STYLES[status] ?? { glyph: glyph.dot, color: dim, label: status };
214
+ }
215
+ function projectStatusColor(status) {
216
+ switch (status) {
217
+ case "active":
218
+ return green;
219
+ case "planned":
220
+ return yellow;
221
+ case "archived":
222
+ return gray;
223
+ default:
224
+ return cyan;
225
+ }
226
+ }
227
+ function heading(title, marker = glyph.chevron) {
228
+ return `${cyan(bold(marker))} ${bold(title)}`;
229
+ }
230
+ function joinDot(fragments) {
231
+ return fragments.join(dim(` ${glyph.dot} `));
232
+ }
233
+
162
234
  // src/recipes/Recipe.ts
163
235
  var Recipe = class {
164
236
  context;
@@ -171,26 +243,22 @@ var Recipe = class {
171
243
  return this;
172
244
  }
173
245
  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
- }
246
+ const subsystem = this.constructor.name.replace("Recipe", "").toLowerCase();
247
+ const dryRun = this.context.dryRun;
248
+ console.log("");
249
+ console.log(` ${cyan(bold(glyph.chevron))} ${bold(`Initializing ${subsystem} subsystem`)}${dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
250
+ console.log("");
180
251
  for (const command of this.ingredients) {
181
252
  const result = await command.invoke();
182
- if (result.success) {
183
- console.log(result.message);
184
- } else {
185
- console.log(result.message);
186
- }
253
+ console.log(result.message.split("\n").map((line) => line ? ` ${line}` : line).join("\n"));
187
254
  }
188
- if (!this.context.dryRun) {
255
+ if (!dryRun) {
189
256
  this.printNextSteps();
190
257
  } else {
191
258
  console.log("");
192
- console.log("\u2713 Dry-run complete - no files were modified");
193
- console.log(" Remove --dry-run flag to apply changes");
259
+ console.log(` ${green(glyph.pass)} ${dim("Dry-run complete \u2014 no files were modified.")}`);
260
+ console.log(` ${dim("Remove --dry-run to apply changes.")}`);
261
+ console.log("");
194
262
  }
195
263
  }
196
264
  };
@@ -333,11 +401,111 @@ if __name__ == "__main__":
333
401
  }
334
402
  };
335
403
 
404
+ // src/commands/AddMiseCodegraphScript.ts
405
+ import { chmodSync } from "fs";
406
+ import { join as join3 } from "path";
407
+ var AddMiseCodegraphScript = class extends Command {
408
+ async invoke() {
409
+ const filePath = ".mise/scripts/codegraph.sh";
410
+ if (this.fileExists(filePath) && !this.context.force) {
411
+ return {
412
+ success: false,
413
+ message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph.sh already exists"),
414
+ filePath
415
+ };
416
+ }
417
+ const content = `#!/usr/bin/env bash
418
+ # Mise enter hook: ensure the CodeGraph CLI is available and initialize the
419
+ # project index. If \`codegraph\` is not installed, this script installs it
420
+ # non-interactively into the project-local .mise/bin directory and retries.
421
+ #
422
+ # This is intended to run from a mise enter hook so onboarding a new host is
423
+ # fully automatic.
424
+
425
+ set -euo pipefail
426
+
427
+ REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
428
+ PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
429
+ mkdir -p "$PROJECT_BIN_DIR"
430
+
431
+ # Install the CodeGraph CLI into the project-local bin directory.
432
+ install_codegraph() {
433
+ echo "[mise] codegraph not found. Installing non-interactively..."
434
+ export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
435
+ curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
436
+
437
+ if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
438
+ export PATH="$PROJECT_BIN_DIR:$PATH"
439
+ else
440
+ echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
441
+ return 1
442
+ fi
443
+ }
444
+
445
+ # Ensure a codegraph binary is available on PATH.
446
+ ensure_codegraph() {
447
+ if command -v codegraph >/dev/null 2>&1; then
448
+ return 0
449
+ fi
450
+
451
+ # Check the project-local bin dir first (previous install from this hook).
452
+ if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
453
+ export PATH="$PROJECT_BIN_DIR:$PATH"
454
+ return 0
455
+ fi
456
+
457
+ # Check typical user-level install locations before fetching anything.
458
+ for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
459
+ if [ -x "$d/codegraph" ]; then
460
+ export PATH="$d:$PATH"
461
+ return 0
462
+ fi
463
+ done
464
+
465
+ install_codegraph
466
+ }
467
+
468
+ # Attempt to initialize the project graph. If the command is missing, install
469
+ # it and retry once.
470
+ init_project() {
471
+ local err_file
472
+ err_file="$(mktemp)"
473
+ trap 'rm -f "$err_file"' RETURN
474
+
475
+ if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
476
+ return 0
477
+ fi
478
+
479
+ # If the failure looks like a missing binary, install and retry.
480
+ if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
481
+ ensure_codegraph
482
+ codegraph init -i "$REPO_ROOT"
483
+ return 0
484
+ fi
485
+
486
+ cat "$err_file" >&2
487
+ return 1
488
+ }
489
+
490
+ init_project
491
+ `;
492
+ this.writeFile(filePath, content);
493
+ if (!this.context.dryRun) {
494
+ chmodSync(join3(this.context.targetDir, filePath), 493);
495
+ }
496
+ return {
497
+ success: true,
498
+ message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
499
+ filePath
500
+ };
501
+ }
502
+ };
503
+
336
504
  // src/recipes/MiseRecipe.ts
337
505
  var MiseRecipe = class extends Recipe {
338
506
  constructor(context) {
339
507
  super(context);
340
- this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript);
508
+ this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
341
509
  }
342
510
  printNextSteps() {
343
511
  console.log("\u{1F389} Mise subsystem initialized successfully!");
@@ -565,12 +733,12 @@ var NodeRecipe = class extends Recipe {
565
733
  };
566
734
 
567
735
  // src/commands/hermes/PromptForAgentConfig.ts
568
- import { basename, join as join3 } from "node:path";
736
+ import { basename, join as join4 } from "node:path";
569
737
  import { readFileSync } from "node:fs";
570
738
  import * as p from "@clack/prompts";
571
739
  function detectTicketProvider(targetDir) {
572
740
  try {
573
- const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
741
+ const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
574
742
  return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
575
743
  } catch {
576
744
  return void 0;
@@ -706,7 +874,7 @@ var PromptForAgentConfig = class extends Command {
706
874
  // src/commands/hermes/RunCopierTemplate.ts
707
875
  import { spawnSync } from "node:child_process";
708
876
  import { homedir as homedir2 } from "node:os";
709
- import { join as join4, dirname as dirname3 } from "node:path";
877
+ import { join as join5, dirname as dirname3 } from "node:path";
710
878
  import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
711
879
  import { fileURLToPath } from "node:url";
712
880
  import * as p2 from "@clack/prompts";
@@ -718,8 +886,8 @@ function resolveVendoredTemplate(name) {
718
886
  return void 0;
719
887
  }
720
888
  for (let i = 0; i < 8; i++) {
721
- const candidate = join4(dir, "templates", name);
722
- if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
889
+ const candidate = join5(dir, "templates", name);
890
+ if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
723
891
  const parent = dirname3(dir);
724
892
  if (parent === dir) break;
725
893
  dir = parent;
@@ -738,7 +906,7 @@ var RunCopierTemplate = class extends Command {
738
906
  message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
739
907
  };
740
908
  }
741
- const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
909
+ const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
742
910
  ctx.roleDir = roleDir;
743
911
  ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
744
912
  const which = spawnSync("which", ["copier"], { encoding: "utf8" });
@@ -748,7 +916,7 @@ var RunCopierTemplate = class extends Command {
748
916
  message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
749
917
  };
750
918
  }
751
- if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
919
+ if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
752
920
  if (ctx.yes) {
753
921
  ctx.force = true;
754
922
  } else {
@@ -765,7 +933,7 @@ var RunCopierTemplate = class extends Command {
765
933
  ctx.force = true;
766
934
  }
767
935
  }
768
- const env = {
936
+ const env2 = {
769
937
  ...process.env,
770
938
  SKIP_TELEGRAM: "1",
771
939
  SKIP_EMAIL: "1",
@@ -775,9 +943,9 @@ var RunCopierTemplate = class extends Command {
775
943
  SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
776
944
  SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
777
945
  };
778
- const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
946
+ const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
779
947
  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);
948
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
781
949
  const args = [
782
950
  "copy",
783
951
  templateSrc,
@@ -808,13 +976,13 @@ var RunCopierTemplate = class extends Command {
808
976
  message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
809
977
  };
810
978
  }
811
- mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
979
+ mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
812
980
  const spinner4 = p2.spinner();
813
981
  spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
814
982
  const result = spawnSync("copier", args, {
815
983
  stdio: "inherit",
816
984
  // pass the interactive output through; copier prints its own progress
817
- env,
985
+ env: env2,
818
986
  cwd: ctx.targetDir
819
987
  });
820
988
  spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
@@ -833,7 +1001,7 @@ var RunCopierTemplate = class extends Command {
833
1001
 
834
1002
  // src/commands/hermes/WireTelegram.ts
835
1003
  import { spawnSync as spawnSync2 } from "node:child_process";
836
- import { join as join5 } from "node:path";
1004
+ import { join as join6 } from "node:path";
837
1005
  import { existsSync as existsSync4, unlinkSync } from "node:fs";
838
1006
  import * as p3 from "@clack/prompts";
839
1007
  var WireTelegram = class extends Command {
@@ -922,14 +1090,14 @@ var WireTelegram = class extends Command {
922
1090
  if (p3.isCancel(allowedAnswer)) {
923
1091
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
924
1092
  }
925
- const script = join5(roleDir, ".scripts", "30-telegram.sh");
1093
+ const script = join6(roleDir, ".scripts", "30-telegram.sh");
926
1094
  if (!existsSync4(script)) {
927
1095
  return {
928
1096
  success: false,
929
1097
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
930
1098
  };
931
1099
  }
932
- const marker = join5(roleDir, ".scripts", ".done-30-telegram");
1100
+ const marker = join6(roleDir, ".scripts", ".done-30-telegram");
933
1101
  if (existsSync4(marker)) unlinkSync(marker);
934
1102
  const spinner4 = p3.spinner();
935
1103
  spinner4.start("Verifying token + wiring profile");
@@ -957,7 +1125,7 @@ function cap(s) {
957
1125
 
958
1126
  // src/commands/hermes/WireEmail.ts
959
1127
  import { spawnSync as spawnSync3 } from "node:child_process";
960
- import { join as join6 } from "node:path";
1128
+ import { join as join7 } from "node:path";
961
1129
  import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
962
1130
  import * as p4 from "@clack/prompts";
963
1131
  var WireEmail = class extends Command {
@@ -973,7 +1141,7 @@ var WireEmail = class extends Command {
973
1141
  if (!targetRepo || !role || !roleDir) {
974
1142
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
975
1143
  }
976
- const script = join6(roleDir, ".scripts", "50-email.sh");
1144
+ const script = join7(roleDir, ".scripts", "50-email.sh");
977
1145
  if (!existsSync5(script)) {
978
1146
  return { success: false, message: `\u2717 ${script} not found` };
979
1147
  }
@@ -1036,7 +1204,7 @@ var WireEmail = class extends Command {
1036
1204
  }
1037
1205
  }
1038
1206
  }
1039
- const marker = join6(roleDir, ".scripts", ".done-50-email");
1207
+ const marker = join7(roleDir, ".scripts", ".done-50-email");
1040
1208
  if (existsSync5(marker)) unlinkSync2(marker);
1041
1209
  const spinner4 = p4.spinner();
1042
1210
  spinner4.start("Creating Cloudflare Email Routing rule");
@@ -1125,7 +1293,7 @@ var HermesAgentRecipe = class extends Recipe {
1125
1293
 
1126
1294
  // src/commands/AgentHooksCommands.ts
1127
1295
  import { homedir as homedir3 } from "node:os";
1128
- import { join as join7, dirname as dirname4 } from "node:path";
1296
+ import { join as join8, dirname as dirname4 } from "node:path";
1129
1297
  import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
1130
1298
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1131
1299
  function resolveTemplateRoot() {
@@ -1136,16 +1304,16 @@ function resolveTemplateRoot() {
1136
1304
  try {
1137
1305
  let dir = dirname4(fileURLToPath2(import.meta.url));
1138
1306
  for (let i = 0; i < 8; i++) {
1139
- candidates.push(join7(dir, "templates", "commonproject", "template"));
1307
+ candidates.push(join8(dir, "templates", "commonproject", "template"));
1140
1308
  const parent = dirname4(dir);
1141
1309
  if (parent === dir) break;
1142
1310
  dir = parent;
1143
1311
  }
1144
1312
  } catch {
1145
1313
  }
1146
- candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1314
+ candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1147
1315
  for (const c of candidates) {
1148
- if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
1316
+ if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
1149
1317
  }
1150
1318
  throw new Error(
1151
1319
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1169,8 +1337,8 @@ var CopyAgentHooksTree = class extends Command {
1169
1337
  const created = [];
1170
1338
  const skipped = [];
1171
1339
  for (const { rel, dir } of items) {
1172
- const src = join7(templateRoot, rel);
1173
- const dest = join7(this.context.targetDir, rel);
1340
+ const src = join8(templateRoot, rel);
1341
+ const dest = join8(this.context.targetDir, rel);
1174
1342
  if (!existsSync6(src)) continue;
1175
1343
  if (existsSync6(dest) && !this.context.force) {
1176
1344
  skipped.push(rel);
@@ -1195,7 +1363,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1195
1363
  static CR = "{{config_root}}";
1196
1364
  // mise's own runtime var — emitted literally
1197
1365
  async invoke() {
1198
- const misePath = join7(this.context.targetDir, "mise.toml");
1366
+ const misePath = join8(this.context.targetDir, "mise.toml");
1199
1367
  if (!existsSync6(misePath)) {
1200
1368
  return {
1201
1369
  success: false,
@@ -1316,7 +1484,7 @@ var RECIPE_REGISTRY = {
1316
1484
  name: "mise",
1317
1485
  description: "Mise task runner and environment setup",
1318
1486
  class: MiseRecipe,
1319
- commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
1487
+ commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
1320
1488
  },
1321
1489
  docker: {
1322
1490
  name: "docker",
@@ -1406,6 +1574,12 @@ var COMMAND_REGISTRY = {
1406
1574
  group: "mise",
1407
1575
  class: AddMiseBaseScript
1408
1576
  },
1577
+ AddMiseCodegraphScript: {
1578
+ name: "AddMiseCodegraphScript",
1579
+ description: "Create .mise/scripts/codegraph.sh enter hook",
1580
+ group: "mise",
1581
+ class: AddMiseCodegraphScript
1582
+ },
1409
1583
  AddDotenv: {
1410
1584
  name: "AddDotenv",
1411
1585
  description: "Create .env.example file",
@@ -1445,8 +1619,8 @@ function createRecipe(name, context) {
1445
1619
  import { cancel as cancel2, multiselect, text as text3, isCancel as isCancel5 } from "@clack/prompts";
1446
1620
 
1447
1621
  // 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";
1622
+ 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";
1623
+ import { basename as basename2, dirname as dirname5, join as join9, relative, resolve } from "node:path";
1450
1624
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1451
1625
  import { homedir as homedir4 } from "node:os";
1452
1626
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -1520,7 +1694,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
1520
1694
  function resolvePjanglerRoot() {
1521
1695
  let dir = dirname5(fileURLToPath3(import.meta.url));
1522
1696
  while (dir !== dirname5(dir)) {
1523
- if (existsSync7(join8(dir, "package.json")) && existsSync7(join8(dir, "templates", "commonproject", "copier.yml"))) {
1697
+ if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
1524
1698
  return dir;
1525
1699
  }
1526
1700
  dir = dirname5(dir);
@@ -1583,10 +1757,10 @@ function ensureSymlink(path, target, dryRun) {
1583
1757
  return { changed: true };
1584
1758
  }
1585
1759
  function bootstrapAgentsFile(repoRoot, dryRun) {
1586
- const agentsPath = join8(repoRoot, "AGENTS.md");
1760
+ const agentsPath = join9(repoRoot, "AGENTS.md");
1587
1761
  if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
1588
1762
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1589
- const source = join8(repoRoot, file);
1763
+ const source = join9(repoRoot, file);
1590
1764
  if (!existsSync7(source)) continue;
1591
1765
  const stat = lstatSync(source);
1592
1766
  if (stat.isSymbolicLink()) continue;
@@ -1596,7 +1770,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1596
1770
  }
1597
1771
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
1598
1772
  }
1599
- const readmePath = join8(repoRoot, "README.md");
1773
+ const readmePath = join9(repoRoot, "README.md");
1600
1774
  if (existsSync7(readmePath)) {
1601
1775
  const stat = lstatSync(readmePath);
1602
1776
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
@@ -1636,11 +1810,11 @@ function yamlGet(text4, keyPath) {
1636
1810
  return "";
1637
1811
  }
1638
1812
  function discoverRoles(repoRoot) {
1639
- const rolesDir = join8(repoRoot, "agents", "hermes");
1813
+ const rolesDir = join9(repoRoot, "agents", "hermes");
1640
1814
  if (!existsSync7(rolesDir)) return [];
1641
1815
  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");
1816
+ const roleDir = join9(rolesDir, entry.name);
1817
+ const roleYamlPath = join9(roleDir, "role.yaml");
1644
1818
  if (!existsSync7(roleYamlPath)) return null;
1645
1819
  const text4 = readText(roleYamlPath);
1646
1820
  const runtimeRepoRaw = yamlGet(text4, "runtime.github_repo");
@@ -1665,7 +1839,7 @@ function discoverRoles(repoRoot) {
1665
1839
  }).filter((value) => Boolean(value));
1666
1840
  }
1667
1841
  function registryPath(homeDir) {
1668
- return join8(homeDir, ".hermes", "agents-registry.yaml");
1842
+ return join9(homeDir, ".hermes", "agents-registry.yaml");
1669
1843
  }
1670
1844
  function systemctlUser(args) {
1671
1845
  const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -1676,7 +1850,7 @@ function systemctlUser(args) {
1676
1850
  };
1677
1851
  }
1678
1852
  function templateScript(ctx, name) {
1679
- const source = join8(ctx.pjanglerRoot, ".mise", "scripts", name);
1853
+ const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
1680
1854
  return existsSync7(source) ? readText(source) : void 0;
1681
1855
  }
1682
1856
  function templateVersioningScript(ctx) {
@@ -1691,9 +1865,9 @@ function renderGeneratedProjectMiseToml(ctx, template) {
1691
1865
  return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
1692
1866
  }
1693
1867
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1694
- const targetPath = join8(ctx.repoRoot, "mise.toml");
1868
+ const targetPath = join9(ctx.repoRoot, "mise.toml");
1695
1869
  if (existsSync7(targetPath)) return false;
1696
- const sourcePath = join8(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1870
+ const sourcePath = join9(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1697
1871
  if (!existsSync7(sourcePath)) return false;
1698
1872
  changedFiles.push(targetPath);
1699
1873
  if (!ctx.dryRun) {
@@ -1702,7 +1876,7 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1702
1876
  return true;
1703
1877
  }
1704
1878
  function templateVersionFilesConf(ctx, repoRoot) {
1705
- const packageJson = join8(repoRoot, "package.json");
1879
+ const packageJson = join9(repoRoot, "package.json");
1706
1880
  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
1881
  }
1708
1882
  function replaceOrAppendManagedBlock(text4, startMarker, block, beforePattern) {
@@ -1727,7 +1901,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
1727
1901
  function requiredMisePathEntries(ctx) {
1728
1902
  const required = [...BASE_MISE_PATH_ENTRIES];
1729
1903
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
1730
- if (existsSync7(join8(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1904
+ if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1731
1905
  }
1732
1906
  return required;
1733
1907
  }
@@ -1876,7 +2050,7 @@ function upsertLinkAgentfilesBlock(text4, ctx) {
1876
2050
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
1877
2051
  }
1878
2052
  function readProjectJson(ctx) {
1879
- return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
2053
+ return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
1880
2054
  }
1881
2055
  function canonicalProjectJson(ctx) {
1882
2056
  const roles = discoverRoles(ctx.repoRoot);
@@ -1920,8 +2094,8 @@ function canonicalProjectJson(ctx) {
1920
2094
  };
1921
2095
  }
1922
2096
  function projectJsonFinding(ctx) {
1923
- const projectPath = join8(ctx.repoRoot, ".project.json");
1924
- const planeJsonPath = join8(ctx.repoRoot, ".plane.json");
2097
+ const projectPath = join9(ctx.repoRoot, ".project.json");
2098
+ const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
1925
2099
  const details = [];
1926
2100
  const data = readProjectJson(ctx);
1927
2101
  const roles = discoverRoles(ctx.repoRoot);
@@ -2035,9 +2209,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2035
2209
  if (!existsSync7(sourceDir)) return;
2036
2210
  mkdirSync5(targetDir, { recursive: true });
2037
2211
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2038
- const sourcePath = join8(sourceDir, entry.name);
2212
+ const sourcePath = join9(sourceDir, entry.name);
2039
2213
  if (skip?.(sourcePath)) continue;
2040
- const targetPath = join8(targetDir, entry.name);
2214
+ const targetPath = join9(targetDir, entry.name);
2041
2215
  if (entry.isDirectory()) {
2042
2216
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2043
2217
  continue;
@@ -2051,7 +2225,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2051
2225
  }
2052
2226
  }
2053
2227
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2054
- const gitmodulesPath = join8(repoRoot, ".gitmodules");
2228
+ const gitmodulesPath = join9(repoRoot, ".gitmodules");
2055
2229
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2056
2230
  const owner = role.runtimeOwner || "delorenj";
2057
2231
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2151,13 +2325,13 @@ var RULES = [
2151
2325
  id: "mise.config-root",
2152
2326
  title: "mise config_root + AGENTS link hooks",
2153
2327
  audit: (ctx) => {
2154
- const misePath = join8(ctx.repoRoot, "mise.toml");
2328
+ const misePath = join9(ctx.repoRoot, "mise.toml");
2155
2329
  if (!existsSync7(misePath)) {
2156
2330
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2157
2331
  }
2158
2332
  const text4 = readText(misePath);
2159
2333
  const details = [];
2160
- const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2334
+ const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2161
2335
  if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2162
2336
  const pathValues = [...(text4.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2163
2337
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
@@ -2176,7 +2350,7 @@ var RULES = [
2176
2350
  };
2177
2351
  },
2178
2352
  migrate: (ctx, finding) => {
2179
- const path = join8(ctx.repoRoot, "mise.toml");
2353
+ const path = join9(ctx.repoRoot, "mise.toml");
2180
2354
  const changedFiles = [];
2181
2355
  const details = [];
2182
2356
  if (!existsSync7(path)) {
@@ -2195,7 +2369,7 @@ var RULES = [
2195
2369
  if (!ctx.dryRun) writeText(path, next);
2196
2370
  text4 = next;
2197
2371
  }
2198
- const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2372
+ const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2199
2373
  const expectedScript = templateLinkAgentfilesScript(ctx);
2200
2374
  if (expectedScript === void 0) {
2201
2375
  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 +2378,7 @@ var RULES = [
2204
2378
  changedFiles.push(linkAgentfilesPath);
2205
2379
  if (!ctx.dryRun) {
2206
2380
  writeText(linkAgentfilesPath, expectedScript);
2207
- chmodSync(linkAgentfilesPath, 493);
2381
+ chmodSync2(linkAgentfilesPath, 493);
2208
2382
  }
2209
2383
  }
2210
2384
  return {
@@ -2222,9 +2396,9 @@ var RULES = [
2222
2396
  title: "managed mise versioning block",
2223
2397
  audit: (ctx) => {
2224
2398
  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");
2399
+ const misePath = join9(ctx.repoRoot, "mise.toml");
2400
+ const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2401
+ const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2228
2402
  const text4 = safeReadText(misePath);
2229
2403
  if (!text4?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2230
2404
  if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
@@ -2241,7 +2415,7 @@ var RULES = [
2241
2415
  migrate: (ctx, finding) => {
2242
2416
  const changedFiles = [];
2243
2417
  const details = [];
2244
- const misePath = join8(ctx.repoRoot, "mise.toml");
2418
+ const misePath = join9(ctx.repoRoot, "mise.toml");
2245
2419
  if (!existsSync7(misePath)) {
2246
2420
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2247
2421
  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 +2431,7 @@ var RULES = [
2257
2431
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
2258
2432
  if (!ctx.dryRun) writeText(misePath, nextMise);
2259
2433
  }
2260
- const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2434
+ const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2261
2435
  const expectedScript = templateVersioningScript(ctx);
2262
2436
  if (expectedScript === void 0) {
2263
2437
  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 +2440,10 @@ var RULES = [
2266
2440
  changedFiles.push(versioningPath);
2267
2441
  if (!ctx.dryRun) {
2268
2442
  writeText(versioningPath, expectedScript);
2269
- chmodSync(versioningPath, 493);
2443
+ chmodSync2(versioningPath, 493);
2270
2444
  }
2271
2445
  }
2272
- const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
2446
+ const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
2273
2447
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
2274
2448
  if (safeReadText(manifestPath) !== expectedManifest) {
2275
2449
  changedFiles.push(manifestPath);
@@ -2289,9 +2463,9 @@ var RULES = [
2289
2463
  id: "sot.agent-symlinks",
2290
2464
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
2291
2465
  audit: (ctx) => {
2292
- const agentsPath = join8(ctx.repoRoot, "AGENTS.md");
2466
+ const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
2293
2467
  if (!existsSync7(agentsPath)) {
2294
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join8(ctx.repoRoot, file)));
2468
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
2295
2469
  if (fallbackSources.length === 0) {
2296
2470
  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
2471
  }
@@ -2306,7 +2480,7 @@ var RULES = [
2306
2480
  }
2307
2481
  const details = [];
2308
2482
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2309
- const full = join8(ctx.repoRoot, file);
2483
+ const full = join9(ctx.repoRoot, file);
2310
2484
  const target = readSymlinkTarget(full);
2311
2485
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
2312
2486
  }
@@ -2330,7 +2504,7 @@ var RULES = [
2330
2504
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
2331
2505
  }
2332
2506
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2333
- const full = join8(ctx.repoRoot, file);
2507
+ const full = join9(ctx.repoRoot, file);
2334
2508
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
2335
2509
  if (result.blocked) blockedDetails.push(result.blocked);
2336
2510
  if (result.changed) changedFiles.push(full);
@@ -2352,7 +2526,7 @@ var RULES = [
2352
2526
  migrate: (ctx, finding) => {
2353
2527
  const changedFiles = [];
2354
2528
  const details = [];
2355
- const path = join8(ctx.repoRoot, ".project.json");
2529
+ const path = join9(ctx.repoRoot, ".project.json");
2356
2530
  const existing = readProjectJson(ctx) ?? {};
2357
2531
  const canonical = canonicalProjectJson(ctx);
2358
2532
  const merged = { ...existing, ...canonical };
@@ -2362,7 +2536,7 @@ var RULES = [
2362
2536
  changedFiles.push(path);
2363
2537
  if (!ctx.dryRun) writeText(path, expected);
2364
2538
  }
2365
- const planeJson = join8(ctx.repoRoot, ".plane.json");
2539
+ const planeJson = join9(ctx.repoRoot, ".plane.json");
2366
2540
  if (existsSync7(planeJson)) {
2367
2541
  const backup = `${planeJson}.migrated-backup`;
2368
2542
  if (existsSync7(backup)) {
@@ -2387,8 +2561,8 @@ var RULES = [
2387
2561
  title: ".env.op + gitignore secrets contract",
2388
2562
  audit: (ctx) => {
2389
2563
  const details = [];
2390
- const envOp = safeReadText(join8(ctx.repoRoot, ".env.op"));
2391
- const gitignore = safeReadText(join8(ctx.repoRoot, ".gitignore"));
2564
+ const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
2565
+ const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
2392
2566
  if (!envOp) {
2393
2567
  details.push(".env.op missing");
2394
2568
  } else {
@@ -2414,12 +2588,12 @@ var RULES = [
2414
2588
  migrate: (ctx, finding) => {
2415
2589
  const changedFiles = [];
2416
2590
  const details = [];
2417
- const envOpPath = join8(ctx.repoRoot, ".env.op");
2591
+ const envOpPath = join9(ctx.repoRoot, ".env.op");
2418
2592
  if (!existsSync7(envOpPath)) {
2419
2593
  changedFiles.push(envOpPath);
2420
- if (!ctx.dryRun) writeText(envOpPath, readText(join8(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2594
+ if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2421
2595
  }
2422
- const gitignorePath = join8(ctx.repoRoot, ".gitignore");
2596
+ const gitignorePath = join9(ctx.repoRoot, ".gitignore");
2423
2597
  const gitignore = safeReadText(gitignorePath) ?? "";
2424
2598
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
2425
2599
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -2446,7 +2620,7 @@ var RULES = [
2446
2620
  title: ".copier-answers.yml provenance + drift report",
2447
2621
  audit: (ctx) => {
2448
2622
  const details = [];
2449
- const path = join8(ctx.repoRoot, ".copier-answers.yml");
2623
+ const path = join9(ctx.repoRoot, ".copier-answers.yml");
2450
2624
  const text4 = safeReadText(path);
2451
2625
  const project = readProjectJson(ctx);
2452
2626
  if (!text4) {
@@ -2477,12 +2651,12 @@ var RULES = [
2477
2651
  const changedFiles = [];
2478
2652
  const project = canonicalProjectJson(ctx);
2479
2653
  const text4 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2480
- _src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
2654
+ _src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
2481
2655
  project_description: ${String(project.project_description)}
2482
2656
  project_name: ${String(project.project_name)}
2483
2657
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2484
2658
  `;
2485
- const path = join8(ctx.repoRoot, ".copier-answers.yml");
2659
+ const path = join9(ctx.repoRoot, ".copier-answers.yml");
2486
2660
  if (safeReadText(path) !== text4) {
2487
2661
  changedFiles.push(path);
2488
2662
  if (!ctx.dryRun) writeText(path, text4);
@@ -2501,15 +2675,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2501
2675
  id: "bmad.scaffold",
2502
2676
  title: "BMAD modules/docs scaffold",
2503
2677
  audit: (ctx) => {
2504
- const sourceRoot = join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2505
- const targetRoot = join8(ctx.repoRoot, "_bmad");
2678
+ const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2679
+ const targetRoot = join9(ctx.repoRoot, "_bmad");
2506
2680
  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")
2681
+ join9("core", "config.yaml"),
2682
+ join9("custom", "config.yaml"),
2683
+ join9("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2684
+ join9("bmm", "workflows", "workflow-status", "workflow.yaml")
2511
2685
  ];
2512
- const missing = sentinels.filter((file) => existsSync7(join8(sourceRoot, file)) && !existsSync7(join8(targetRoot, file)));
2686
+ const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
2513
2687
  return {
2514
2688
  id: "bmad.scaffold",
2515
2689
  title: "BMAD modules/docs scaffold",
@@ -2521,7 +2695,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2521
2695
  },
2522
2696
  migrate: (ctx, finding) => {
2523
2697
  const changedFiles = [];
2524
- copyMissingRecursive(join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join8(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2698
+ copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2525
2699
  return {
2526
2700
  id: finding.id,
2527
2701
  title: finding.title,
@@ -2543,11 +2717,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2543
2717
  }
2544
2718
  const details = [];
2545
2719
  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))}`);
2720
+ if (!existsSync7(join9(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join9(role.roleDir, rel))}`);
2547
2721
  }
2548
- const gitmodules = safeReadText(join8(ctx.repoRoot, ".gitmodules")) ?? "";
2722
+ const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
2549
2723
  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"))) {
2724
+ if (!profileMetaInheritsDefault(join9(role.roleDir, "runtime", "profile.yaml"))) {
2551
2725
  details.push("runtime/profile.yaml missing inherited default config metadata");
2552
2726
  }
2553
2727
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -2568,21 +2742,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2568
2742
  if (!role) {
2569
2743
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
2570
2744
  }
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");
2745
+ const templateRoleDir = join9(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2746
+ writeIfDifferent(join9(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2747
+ writeIfDifferent(join9(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2748
+ writeIfDifferent(join9(role.roleDir, ".gitignore"), readText(join9(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2749
+ copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2750
+ copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2751
+ copyMissingRecursive(join9(templateRoleDir, ".scripts"), join9(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2752
+ const promptSource = join9(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2753
+ const promptTarget = join9(role.roleDir, ".scripts", "sentinel.prompt.md");
2580
2754
  if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
2581
2755
  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
2756
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
2583
2757
  }
2584
2758
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2585
- const profileMetaUpdated = upsertInheritedProfileMeta(join8(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2759
+ const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2586
2760
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2587
2761
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2588
2762
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -2636,9 +2810,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2636
2810
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
2637
2811
  }
2638
2812
  for (const role of roles) {
2639
- const sysDir = join8(ctx.homeDir, ".config", "systemd", "user");
2813
+ const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
2640
2814
  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)));
2815
+ const allUnitsPresent = units.every((unit) => existsSync7(join9(sysDir, unit)));
2642
2816
  if (allUnitsPresent) {
2643
2817
  if (ctx.dryRun) {
2644
2818
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -2650,7 +2824,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2650
2824
  }
2651
2825
  continue;
2652
2826
  }
2653
- for (const script of [join8(role.roleDir, ".scripts", "70-systemd.sh")]) {
2827
+ for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
2654
2828
  if (!script || !existsSync7(script)) continue;
2655
2829
  if (ctx.dryRun) {
2656
2830
  details.push(`would run: bash ${script}`);
@@ -2678,7 +2852,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
2678
2852
  changedFiles.push(path);
2679
2853
  if (!dryRun) {
2680
2854
  writeText(path, normalized);
2681
- if (mode) chmodSync(path, mode);
2855
+ if (mode) chmodSync2(path, mode);
2682
2856
  }
2683
2857
  }
2684
2858
  function getParityRuleIds() {
@@ -2740,35 +2914,60 @@ function runMigration(selector, repoArg, dryRun, all) {
2740
2914
  const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
2741
2915
  return runMigrationForRules(ruleIds, repoArg, dryRun);
2742
2916
  }
2917
+ function prettyTimestamp(iso) {
2918
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
2919
+ return match ? `${match[1]} ${match[2]} UTC` : iso;
2920
+ }
2743
2921
  function formatAuditReport(report) {
2744
- const lines = [`repo: ${report.repo}`, `ok: ${report.ok}`, `audited_at: ${report.auditedAt}`, "rules:"];
2922
+ const counts = {};
2923
+ for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
2924
+ const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
2925
+ const tally = [];
2926
+ if (counts.pass) tally.push(green(`${counts.pass} passed`));
2927
+ if (counts.fail) tally.push(red(`${counts.fail} failed`));
2928
+ if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
2929
+ if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
2930
+ const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
2931
+ const lines = [""];
2932
+ lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
2933
+ lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
2934
+ lines.push("");
2745
2935
  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}`);
2936
+ const style = statusStyle(rule.status);
2937
+ lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
2938
+ for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
2748
2939
  }
2749
- return `${lines.join("\n")}
2750
- `;
2940
+ lines.push("");
2941
+ return lines.join("\n");
2751
2942
  }
2752
2943
  function formatMigrationReport(report) {
2753
- const lines = [`repo: ${report.repo}`, `dry_run: ${report.dryRun}`, `ok: ${report.ok}`, `selected_rules: ${report.selectedRules.join(", ")}`, "results:"];
2944
+ const idWidth = report.results.reduce((width, result) => Math.max(width, result.id.length), 0);
2945
+ const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : `${red(glyph.fail)} ${bold("Migration finished with blockers")}`;
2946
+ const lines = [""];
2947
+ lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
2948
+ lines.push(` ${dim(report.repo)}`);
2949
+ if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
2950
+ lines.push("");
2754
2951
  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}`);
2952
+ const style = statusStyle(result.status);
2953
+ lines.push(` ${style.color(style.glyph)} ${style.color(result.id.padEnd(idWidth))} ${result.summary} ${dim(`[${style.label}]`)}`);
2954
+ for (const detail of result.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
2955
+ for (const file of result.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
2758
2956
  }
2759
2957
  if (report.changedFiles.length) {
2760
- lines.push("changed_files:");
2761
- for (const file of report.changedFiles) lines.push(`- ${file}`);
2958
+ lines.push("");
2959
+ lines.push(` ${bold(`Changed files (${report.changedFiles.length})`)}`);
2960
+ for (const file of report.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
2762
2961
  }
2763
- return `${lines.join("\n")}
2764
- `;
2962
+ lines.push("");
2963
+ return lines.join("\n");
2765
2964
  }
2766
2965
 
2767
2966
  // src/project/index.ts
2768
2967
  import { spawnSync as spawnSync5 } from "node:child_process";
2769
2968
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
2770
2969
  import { homedir as homedir5 } from "node:os";
2771
- import { basename as basename3, dirname as dirname6, join as join9, resolve as resolve2 } from "node:path";
2970
+ import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve2 } from "node:path";
2772
2971
  import YAML from "yaml";
2773
2972
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
2774
2973
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
@@ -2776,10 +2975,10 @@ var KNOWN_SKILL_ROOTS = [
2776
2975
  "/home/delorenj/code/skillex/all-skills",
2777
2976
  "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
2778
2977
  "/home/delorenj/code/pjangler/.agents/skills",
2779
- join9(homedir5(), ".codex", "skills")
2978
+ join10(homedir5(), ".codex", "skills")
2780
2979
  ];
2781
- function projectRegistryPath(env = process.env) {
2782
- return expandHome(env[PROJECT_REGISTRY_ENV] || join9(homedir5(), ".config", "pjangler", "projects.yaml"));
2980
+ function projectRegistryPath(env2 = process.env) {
2981
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
2783
2982
  }
2784
2983
  function emptyProjectRegistry() {
2785
2984
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
@@ -2863,7 +3062,7 @@ function resolveSourceSkillPath(sourceSkill) {
2863
3062
  if (existsSync8(direct)) return direct;
2864
3063
  const name = basename3(sourceSkill);
2865
3064
  for (const root of KNOWN_SKILL_ROOTS) {
2866
- const candidate = join9(root, name);
3065
+ const candidate = join10(root, name);
2867
3066
  if (existsSync8(candidate)) return candidate;
2868
3067
  }
2869
3068
  const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
@@ -2943,7 +3142,7 @@ function planProjectInit(input) {
2943
3142
  }));
2944
3143
  }
2945
3144
  actions.push(
2946
- { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
3145
+ { kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
2947
3146
  {
2948
3147
  kind: "plane.create-or-link",
2949
3148
  enabled: live,
@@ -3050,24 +3249,40 @@ function projectManifestFromRegistryProject(project) {
3050
3249
  };
3051
3250
  }
3052
3251
  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
- ];
3252
+ const lines = [""];
3253
+ const title = `${bold(plan.project.name)} ${dim(`(${plan.project.slug})`)}`;
3254
+ lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
3255
+ lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
3256
+ lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
3257
+ lines.push("");
3258
+ lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
3259
+ if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
3059
3260
  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}`);
3261
+ lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
3262
+ if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
3263
+ if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
3264
+ if (action.kind === "plane.create-or-link" && action.reason) lines.push(` ${dim(`note: ${action.reason}`)}`);
3064
3265
  }
3266
+ lines.push("");
3065
3267
  return lines.join("\n");
3066
3268
  }
3067
3269
  function formatProjectList(registry) {
3068
3270
  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");
3271
+ if (!projects.length) return `
3272
+ ${dim("No projects registered.")}
3273
+ `;
3274
+ const slugWidth = projects.reduce((width, project) => Math.max(width, project.slug.length), 0);
3275
+ const idWidth = projects.reduce((width, project) => Math.max(width, String(project.ticket_provider.identifier ?? "").length), 0);
3276
+ const statusWidth = projects.reduce((width, project) => Math.max(width, project.status.length), 0);
3277
+ const lines = ["", ` ${bold("Projects")} ${dim(`(${projects.length})`)}`, ""];
3278
+ for (const project of projects) {
3279
+ const slug = bold(project.slug.padEnd(slugWidth));
3280
+ const identifier = cyan(String(project.ticket_provider.identifier ?? "").padEnd(idWidth));
3281
+ const status = projectStatusColor(project.status)(project.status.padEnd(statusWidth));
3282
+ lines.push(` ${slug} ${identifier} ${status} ${dim(project.repo_path)}`);
3283
+ }
3284
+ lines.push("");
3285
+ return lines.join("\n");
3071
3286
  }
3072
3287
  function getProject(registry, slug) {
3073
3288
  const project = registry.projects[slug];
@@ -3084,7 +3299,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
3084
3299
  } else if (!statSync(project.repo_path).isDirectory()) {
3085
3300
  issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
3086
3301
  } else {
3087
- const manifestPath = join9(project.repo_path, ".project.json");
3302
+ const manifestPath = join10(project.repo_path, ".project.json");
3088
3303
  if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
3089
3304
  }
3090
3305
  for (const artifact of project.source_artifacts) {
@@ -3101,7 +3316,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
3101
3316
  };
3102
3317
  }
3103
3318
  function buildCommonProjectCopierAction(input) {
3104
- const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
3319
+ const templateDir = join10(input.pjanglerRoot, "templates", "commonproject");
3105
3320
  const data = {
3106
3321
  project_name: input.projectName,
3107
3322
  project_description: input.projectDescription ?? "",
@@ -3127,7 +3342,7 @@ function buildCommonProjectCopierAction(input) {
3127
3342
  function resolvePjanglerRoot2() {
3128
3343
  let dir = dirname6(new URL(import.meta.url).pathname);
3129
3344
  while (dir !== dirname6(dir)) {
3130
- if (existsSync8(join9(dir, "package.json")) && existsSync8(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
3345
+ if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) return dir;
3131
3346
  dir = dirname6(dir);
3132
3347
  }
3133
3348
  return resolve2(process.cwd());
@@ -3159,7 +3374,7 @@ function validateProjectRecord(project, key) {
3159
3374
  }
3160
3375
  function expandHome(path) {
3161
3376
  if (path === "~") return homedir5();
3162
- if (path.startsWith("~/")) return join9(homedir5(), path.slice(2));
3377
+ if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
3163
3378
  return path;
3164
3379
  }
3165
3380
  function isRecord(value) {
@@ -3168,14 +3383,14 @@ function isRecord(value) {
3168
3383
 
3169
3384
  // src/utils/version.ts
3170
3385
  import { readFileSync as readFileSync5 } from "node:fs";
3171
- import { dirname as dirname7, join as join10 } from "node:path";
3386
+ import { dirname as dirname7, join as join11 } from "node:path";
3172
3387
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3173
3388
  var PJANGLER_VERSION = (() => {
3174
3389
  try {
3175
3390
  let dir = dirname7(fileURLToPath4(import.meta.url));
3176
3391
  for (let i = 0; i < 4; i++) {
3177
3392
  try {
3178
- const raw = readFileSync5(join10(dir, "package.json"), "utf8");
3393
+ const raw = readFileSync5(join11(dir, "package.json"), "utf8");
3179
3394
  return JSON.parse(raw).version ?? "0.0.0";
3180
3395
  } catch {
3181
3396
  const parent = dirname7(dir);
@@ -3189,6 +3404,7 @@ var PJANGLER_VERSION = (() => {
3189
3404
  })();
3190
3405
 
3191
3406
  // src/index.ts
3407
+ var xmark = `${red(glyph.fail)}`;
3192
3408
  function printMigrationReport(report, asJson) {
3193
3409
  if (asJson) {
3194
3410
  console.log(JSON.stringify(report, null, 2));
@@ -3236,8 +3452,8 @@ function packageNameToProjectName(value) {
3236
3452
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
3237
3453
  }
3238
3454
  function deriveProjectDefaults(targetDir) {
3239
- const manifest = readJson(join11(targetDir, ".project.json"));
3240
- const pkg = readJson(join11(targetDir, "package.json"));
3455
+ const manifest = readJson(join12(targetDir, ".project.json"));
3456
+ const pkg = readJson(join12(targetDir, "package.json"));
3241
3457
  const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
3242
3458
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
3243
3459
  return {
@@ -3348,7 +3564,7 @@ async function resolveProjectInitTarget(name, options) {
3348
3564
  if (!targetDir && interactive) {
3349
3565
  const defaultName = name ?? basename4(cwd);
3350
3566
  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, "-"));
3567
+ const defaultDir = join12(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
3352
3568
  targetDir = await promptTextValue("Project directory", defaultDir);
3353
3569
  name = promptedName;
3354
3570
  }
@@ -3384,27 +3600,30 @@ program.command("init").argument("<subsystem>", "Subsystem to initialize").descr
3384
3600
  try {
3385
3601
  const recipe = createRecipe(subsystem, context);
3386
3602
  if (!recipe) {
3387
- console.error(`\u274C Unknown subsystem: ${subsystem}`);
3388
- console.log(`Available subsystems: ${getRecipeNames().join(", ")}`);
3603
+ console.error(`${xmark} Unknown subsystem: ${bold(subsystem)}`);
3604
+ console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
3389
3605
  process.exit(1);
3390
3606
  }
3391
3607
  await recipe.execute();
3392
3608
  } catch (error) {
3393
- console.error(`\u274C Error initializing ${subsystem}:`, error);
3609
+ console.error(`${xmark} Error initializing ${bold(subsystem)}:`, error);
3394
3610
  process.exit(1);
3395
3611
  }
3396
3612
  });
3397
3613
  program.command("list").description("List available subsystems").action(() => {
3398
- console.log("Available subsystems:");
3614
+ const width = Object.keys(RECIPE_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
3615
+ console.log("");
3616
+ console.log(` ${heading("Available subsystems")}`);
3399
3617
  console.log("");
3400
3618
  for (const [name, info] of Object.entries(RECIPE_REGISTRY)) {
3401
- console.log(` ${name.padEnd(10)} - ${info.description}`);
3619
+ console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
3620
+ }
3621
+ console.log("");
3622
+ console.log(` ${dim("Examples")}`);
3623
+ for (const example of ["pj init mise", "pj init docker", "pj init node"]) {
3624
+ console.log(` ${dim(glyph.pointer)} ${dim(example)}`);
3402
3625
  }
3403
3626
  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
3627
  });
3409
3628
  var projectCmd = program.command("project").description("Manage the pjangler project registry");
3410
3629
  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 +3677,12 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
3458
3677
  else {
3459
3678
  console.log(formatProjectInitPlan(plan));
3460
3679
  if (payload.proposedOperations.length) {
3461
- console.log("Proposed sync operations:");
3462
- for (const operation of payload.proposedOperations) console.log(` - ${operation}`);
3680
+ console.log(` ${bold("Proposed operations")} ${dim(`(${payload.proposedOperations.length})`)}`);
3681
+ for (const operation of payload.proposedOperations) console.log(` ${cyan(glyph.bullet)} ${operation}`);
3463
3682
  } else {
3464
- console.log("Project is already in parity.");
3683
+ console.log(` ${green(glyph.pass)} ${dim("Project is already in parity.")}`);
3465
3684
  }
3685
+ console.log("");
3466
3686
  }
3467
3687
  return;
3468
3688
  }
@@ -3490,17 +3710,19 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
3490
3710
  } else {
3491
3711
  console.log(formatProjectInitPlan(selectedPlan));
3492
3712
  for (const line of result.logs) console.log(line);
3493
- for (const line of result.errors) console.error(line);
3713
+ for (const line of result.errors) console.error(` ${xmark} ${line}`);
3494
3714
  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}`);
3715
+ if (result.ok && changedFiles.length) console.log(` ${green(glyph.pass)} ${bold("Project synchronized")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
3716
+ `);
3717
+ if (result.ok && changedFiles.length === 0) console.log(` ${green(glyph.pass)} ${dim("Already in parity")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}
3718
+ `);
3497
3719
  }
3498
3720
  process.exitCode = result.ok ? 0 : 1;
3499
3721
  } catch (err) {
3500
3722
  if (options.json) {
3501
3723
  console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
3502
3724
  } else {
3503
- console.error("\u274C project init failed:", err instanceof Error ? err.message : err);
3725
+ console.error(`${xmark} project init failed:`, err instanceof Error ? err.message : err);
3504
3726
  }
3505
3727
  process.exit(1);
3506
3728
  }
@@ -3511,19 +3733,24 @@ projectCmd.command("list").description("List projects in the pjangler registry")
3511
3733
  if (options.json) console.log(JSON.stringify(registry, null, 2));
3512
3734
  else console.log(formatProjectList(registry));
3513
3735
  } catch (err) {
3514
- console.error("\u274C project list failed:", err instanceof Error ? err.message : err);
3736
+ console.error(`${xmark} project list failed:`, err instanceof Error ? err.message : err);
3515
3737
  process.exit(1);
3516
3738
  }
3517
3739
  });
3518
3740
  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
3741
  try {
3520
3742
  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}`);
3743
+ if (options.json) {
3744
+ console.log(JSON.stringify(project, null, 2));
3745
+ } else {
3746
+ console.log("");
3747
+ console.log(` ${heading(project.name)} ${dim(`(${project.slug})`)}`);
3748
+ console.log(` ${dim(project.repo_path)}`);
3749
+ if (project.description) console.log(` ${project.description}`);
3750
+ console.log("");
3751
+ }
3525
3752
  } catch (err) {
3526
- console.error("\u274C project show failed:", err instanceof Error ? err.message : err);
3753
+ console.error(`${xmark} project show failed:`, err instanceof Error ? err.message : err);
3527
3754
  process.exit(1);
3528
3755
  }
3529
3756
  });
@@ -3533,50 +3760,59 @@ projectCmd.command("doctor").argument("[slug]", "Optional project slug").descrip
3533
3760
  if (options.json) {
3534
3761
  console.log(JSON.stringify(report, null, 2));
3535
3762
  } else if (!report.issues.length) {
3536
- console.log(`Project registry OK: ${report.registryPath}`);
3763
+ console.log("");
3764
+ console.log(` ${green(glyph.pass)} ${bold("Project registry OK")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
3765
+ console.log("");
3537
3766
  } 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}`);
3767
+ console.log("");
3768
+ console.log(` ${red(glyph.fail)} ${bold("Project registry issues")} ${dim(glyph.dot)} ${dim(report.registryPath)}`);
3769
+ console.log("");
3770
+ for (const issue of report.issues) {
3771
+ const mark = issue.level === "error" ? red(glyph.fail) : yellow(glyph.warn);
3772
+ console.log(` ${mark} ${bold(issue.slug ?? "registry")} ${issue.message}`);
3773
+ }
3774
+ console.log("");
3540
3775
  }
3541
3776
  process.exit(report.ok ? 0 : 1);
3542
3777
  } catch (err) {
3543
- console.error("\u274C project doctor failed:", err instanceof Error ? err.message : err);
3778
+ console.error(`${xmark} project doctor failed:`, err instanceof Error ? err.message : err);
3544
3779
  process.exit(1);
3545
3780
  }
3546
3781
  });
3547
3782
  var recipeCmd = program.command("recipe").description("Manage pjangler recipes");
3548
3783
  recipeCmd.command("list").description("List all available recipes").action(() => {
3549
- console.log("\u{1F4E6} Available Recipes:");
3784
+ console.log("");
3785
+ console.log(` ${heading("Recipes")}`);
3550
3786
  console.log("");
3551
3787
  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(", ")}`);
3788
+ console.log(` ${cyan(bold(name))}`);
3789
+ console.log(` ${dim(info.description)}`);
3790
+ console.log(` ${dim("commands")} ${info.commands.map((command) => cyan(command)).join(dim(", "))}`);
3555
3791
  console.log("");
3556
3792
  }
3557
- console.log("Usage:");
3558
- console.log(" pjangler recipe run <name>");
3559
- console.log(" pjangler recipe describe <name>");
3793
+ console.log(` ${dim("Usage")}`);
3794
+ console.log(` ${dim(glyph.pointer)} ${dim("pj recipe run <name>")}`);
3795
+ console.log(` ${dim(glyph.pointer)} ${dim("pj recipe describe <name>")}`);
3796
+ console.log("");
3560
3797
  });
3561
3798
  recipeCmd.command("describe").argument("<name>", "Recipe name").description("Show detailed information about a recipe").action((name) => {
3562
3799
  const info = getRecipeInfo(name);
3563
3800
  if (!info) {
3564
- console.error(`\u274C Recipe not found: ${name}`);
3565
- console.log(`Available recipes: ${getRecipeNames().join(", ")}`);
3801
+ console.error(`${xmark} Recipe not found: ${bold(name)}`);
3802
+ console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
3566
3803
  process.exit(1);
3567
3804
  }
3568
- console.log(`\u{1F4E6} Recipe: ${info.name}`);
3569
3805
  console.log("");
3570
- console.log(`Description: ${info.description}`);
3806
+ console.log(` ${heading(info.name)}`);
3807
+ console.log(` ${dim(info.description)}`);
3571
3808
  console.log("");
3572
- console.log("Commands:");
3573
- for (const cmd of info.commands) {
3574
- console.log(` - ${cmd}`);
3575
- }
3809
+ console.log(` ${bold("Commands")}`);
3810
+ for (const command of info.commands) console.log(` ${cyan(glyph.bullet)} ${command}`);
3811
+ console.log("");
3812
+ console.log(` ${dim("Usage")}`);
3813
+ console.log(` ${dim(glyph.pointer)} ${dim(`pj recipe run ${name}`)}`);
3814
+ console.log(` ${dim(glyph.pointer)} ${dim(`pj init ${name}`)}`);
3576
3815
  console.log("");
3577
- console.log("Usage:");
3578
- console.log(` pjangler recipe run ${name}`);
3579
- console.log(` pjangler init ${name}`);
3580
3816
  });
3581
3817
  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
3818
  const context = {
@@ -3587,80 +3823,73 @@ recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute
3587
3823
  try {
3588
3824
  const recipe = createRecipe(name, context);
3589
3825
  if (!recipe) {
3590
- console.error(`\u274C Recipe not found: ${name}`);
3591
- console.log(`Available recipes: ${getRecipeNames().join(", ")}`);
3826
+ console.error(`${xmark} Recipe not found: ${bold(name)}`);
3827
+ console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
3592
3828
  process.exit(1);
3593
3829
  }
3594
- const dryRunPrefix = context.dryRun ? "[DRY RUN] " : "";
3595
- console.log(`${dryRunPrefix}\u{1F680} Running recipe: ${name}`);
3596
- console.log("");
3597
3830
  await recipe.execute();
3598
3831
  } catch (error) {
3599
- console.error(`\u274C Error running recipe ${name}:`, error);
3832
+ console.error(`${xmark} Error running recipe ${bold(name)}:`, error);
3600
3833
  process.exit(1);
3601
3834
  }
3602
3835
  });
3603
3836
  var commandCmd = program.command("command").alias("cmd").description("Manage pjangler commands");
3604
3837
  commandCmd.command("list").description("List all available commands").option("-g, --group", "Group commands by category").action((options) => {
3838
+ console.log("");
3605
3839
  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
- }
3840
+ console.log(` ${heading("Commands by category")}`);
3841
+ for (const [group, commands] of Object.entries(getCommandsByGroup())) {
3842
+ const width = commands.reduce((max, command) => Math.max(max, command.name.length), 0);
3614
3843
  console.log("");
3844
+ console.log(` ${bold(group.toUpperCase())}`);
3845
+ for (const command of commands) {
3846
+ console.log(` ${cyan(command.name.padEnd(width))} ${dim(command.description)}`);
3847
+ }
3615
3848
  }
3849
+ console.log("");
3616
3850
  } else {
3617
- console.log("\u2699\uFE0F Available Commands:");
3851
+ const width = Object.keys(COMMAND_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
3852
+ console.log(` ${heading("Commands")}`);
3618
3853
  console.log("");
3619
3854
  for (const [name, info] of Object.entries(COMMAND_REGISTRY)) {
3620
- console.log(` ${name.padEnd(30)} - ${info.description}`);
3855
+ console.log(` ${cyan(name.padEnd(width))} ${dim(info.description)}`);
3621
3856
  }
3622
3857
  console.log("");
3623
3858
  }
3624
- console.log("Usage:");
3625
- console.log(" pj command list --group # Group by category");
3626
- console.log(" pj command describe <name> # Show command details");
3859
+ console.log(` ${dim("Usage")}`);
3860
+ console.log(` ${dim(glyph.pointer)} ${dim("pj command list --group")} ${dim("# group by category")}`);
3861
+ console.log(` ${dim(glyph.pointer)} ${dim("pj command describe <name>")} ${dim("# command details")}`);
3862
+ console.log("");
3627
3863
  });
3628
3864
  commandCmd.command("describe").argument("<name>", "Command name").description("Show detailed information about a command").action((name) => {
3629
3865
  const info = getCommandInfo(name);
3630
3866
  if (!info) {
3631
- console.error(`\u274C Command not found: ${name}`);
3632
- console.log(`Available commands: ${getCommandNames().join(", ")}`);
3867
+ console.error(`${xmark} Command not found: ${bold(name)}`);
3868
+ console.error(` ${dim("Available:")} ${getCommandNames().map((available) => cyan(available)).join(dim(", "))}`);
3633
3869
  process.exit(1);
3634
3870
  }
3635
- console.log(`\u2699\uFE0F Command: ${info.name}`);
3871
+ const usedIn = Object.entries(RECIPE_REGISTRY).filter(([, recipeInfo]) => recipeInfo.commands.includes(name)).map(([recipeName]) => recipeName);
3636
3872
  console.log("");
3637
- console.log(`Description: ${info.description}`);
3638
- console.log(`Group: ${info.group}`);
3873
+ console.log(` ${heading(info.name)}`);
3874
+ console.log(` ${dim(info.description)}`);
3639
3875
  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
- }
3876
+ console.log(` ${dim("group".padEnd(7))} ${cyan(info.group)}`);
3877
+ console.log(` ${dim("recipes".padEnd(7))} ${usedIn.length ? usedIn.map((recipeName) => cyan(recipeName)).join(dim(", ")) : dim("(none)")}`);
3878
+ console.log("");
3879
+ console.log(` ${dim("Part of recipe execution (not run directly).")}`);
3646
3880
  console.log("");
3647
- console.log("Usage:");
3648
- console.log(` Part of recipe execution (not run directly)`);
3649
3881
  });
3650
3882
  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
3883
  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
- }
3884
+ console.log(` ${yellow(glyph.warn)} ${bold("Command generation coming in STORY-005")}`);
3885
+ console.log("");
3886
+ console.log(` ${dim("Planned")}`);
3887
+ console.log(` ${cyan(glyph.bullet)} Generate ${bold(name)} from prompt: ${dim(`"${prompt}"`)}`);
3888
+ if (options.template) console.log(` ${cyan(glyph.bullet)} Template type: ${cyan(options.template)}`);
3889
+ if (options.model) console.log(` ${cyan(glyph.bullet)} LLM model: ${cyan(options.model)}`);
3890
+ console.log("");
3891
+ console.log(` ${dim("For now, manually create commands in src/commands/")}`);
3661
3892
  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
3893
  });
3665
3894
  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
3895
  try {
@@ -3672,7 +3901,7 @@ program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd
3672
3901
  }
3673
3902
  process.exit(report.ok ? 0 : 1);
3674
3903
  } catch (err) {
3675
- console.error("\u274C audit failed:", err);
3904
+ console.error(`${xmark} audit failed:`, err);
3676
3905
  process.exit(1);
3677
3906
  }
3678
3907
  });
@@ -3691,7 +3920,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
3691
3920
  }
3692
3921
  if (ruleId && repo) {
3693
3922
  if (!getParityRuleIds().includes(ruleId)) {
3694
- console.error(`\u274C Unknown parity rule: ${ruleId}`);
3923
+ console.error(`${xmark} Unknown parity rule: ${bold(ruleId)}`);
3695
3924
  process.exit(1);
3696
3925
  }
3697
3926
  const report2 = runMigration(ruleId, repo, dryRun, false);
@@ -3704,25 +3933,25 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
3704
3933
  process.exit(report2.ok ? 0 : 1);
3705
3934
  }
3706
3935
  if (options.json) {
3707
- console.error("\u274C JSON output requires a rule-id or --all");
3936
+ console.error(`${xmark} JSON output requires a rule-id or --all`);
3708
3937
  process.exit(1);
3709
3938
  }
3710
3939
  if (!process.stdin.isTTY) {
3711
- console.error("\u274C Provide a rule-id, use --all, or run in an interactive terminal");
3940
+ console.error(`${xmark} Provide a rule-id, use --all, or run in an interactive terminal`);
3712
3941
  process.exit(1);
3713
3942
  }
3714
3943
  const targetRepo = ruleId ?? repo;
3715
3944
  const audit = runAudit(targetRepo);
3716
3945
  const ruleIds = await promptForRuleIds(audit.rules);
3717
3946
  if (!ruleIds.length) {
3718
- console.log("No rules selected; nothing to migrate.");
3947
+ console.log(` ${cyan(glyph.info)} ${dim("No rules selected; nothing to migrate.")}`);
3719
3948
  process.exit(0);
3720
3949
  }
3721
3950
  const report = runMigrationForRules(ruleIds, targetRepo, dryRun);
3722
3951
  printMigrationReport(report, false);
3723
3952
  process.exit(report.ok ? 0 : 1);
3724
3953
  } catch (err) {
3725
- console.error("\u274C migrate failed:", err);
3954
+ console.error(`${xmark} migrate failed:`, err);
3726
3955
  process.exit(1);
3727
3956
  }
3728
3957
  });
@@ -3757,12 +3986,12 @@ program.command("hermes-agent").alias("hermes").description("Provision a Hermes
3757
3986
  try {
3758
3987
  const recipe = createRecipe("hermes-agent", context);
3759
3988
  if (!recipe) {
3760
- console.error("\u274C hermes-agent recipe not registered");
3989
+ console.error(`${xmark} hermes-agent recipe not registered`);
3761
3990
  process.exit(1);
3762
3991
  }
3763
3992
  await recipe.execute();
3764
3993
  } catch (err) {
3765
- console.error("\u274C hermes-agent failed:", err);
3994
+ console.error(`${xmark} hermes-agent failed:`, err);
3766
3995
  process.exit(1);
3767
3996
  }
3768
3997
  });
@@ -3780,14 +4009,15 @@ configCmd.command("bootstrap").description("Create ~/.config/hermes-agent-templa
3780
4009
  }
3781
4010
  });
3782
4011
  program.command("describe").description("Describe the current project (for AI context)").action(() => {
3783
- console.log("\u{1F50D} Project Description (placeholder for future enhancement)");
3784
4012
  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");
4013
+ console.log(` ${heading("Project description")} ${dim("(placeholder)")}`);
4014
+ console.log("");
4015
+ console.log(` ${dim("Will analyze the project and report:")}`);
4016
+ for (const item of ["Detected project type", "Installed subsystems", "Configuration files present", "Suggested next steps"]) {
4017
+ console.log(` ${cyan(glyph.bullet)} ${item}`);
4018
+ }
4019
+ console.log("");
4020
+ console.log(` ${dim("Coming soon.")}`);
3790
4021
  console.log("");
3791
- console.log("Coming soon!");
3792
4022
  });
3793
4023
  program.parse();