@delorenj/pjangler 1.2.12 → 1.2.14

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
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { spawnSync as spawnSync6 } from "node:child_process";
5
- import { existsSync as existsSync9, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
6
- import { basename as basename4, join as join12, resolve as resolve3 } from "node:path";
4
+ import { spawnSync as spawnSync7 } from "node:child_process";
5
+ import { existsSync as existsSync10, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
6
+ import { basename as basename4, join as join13, resolve as resolve3 } from "node:path";
7
7
  import { Command as Command3 } from "commander";
8
8
 
9
9
  // src/commands/hermes/types.ts
@@ -916,10 +916,100 @@ var RunCopierTemplate = class extends Command {
916
916
  }
917
917
  };
918
918
 
919
- // src/commands/hermes/WireTelegram.ts
919
+ // src/commands/hermes/UntrackHermesRuntimes.ts
920
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
921
+ import { join as join6 } from "path";
920
922
  import { spawnSync as spawnSync2 } from "node:child_process";
921
- import { join as join6 } from "node:path";
922
- import { existsSync as existsSync4, unlinkSync } from "node:fs";
923
+ var UntrackHermesRuntimes = class extends Command {
924
+ async invoke() {
925
+ const targetDir = this.context.targetDir;
926
+ const rolesDir = join6(targetDir, "agents", "hermes");
927
+ if (!existsSync4(rolesDir)) {
928
+ return {
929
+ success: true,
930
+ message: "No Hermes agents found (no agents/hermes directory)."
931
+ };
932
+ }
933
+ const roles = readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
934
+ if (roles.length === 0) {
935
+ return {
936
+ success: true,
937
+ message: "No Hermes agents found."
938
+ };
939
+ }
940
+ let modifiedAny = false;
941
+ const details = [];
942
+ for (const role of roles) {
943
+ const roleDir = join6("agents", "hermes", role);
944
+ const runtimePath = join6(roleDir, "runtime");
945
+ const gitignorePath = join6(roleDir, ".gitignore");
946
+ let isTracked = false;
947
+ const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
948
+ cwd: targetDir,
949
+ encoding: "utf8"
950
+ });
951
+ if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
952
+ isTracked = true;
953
+ }
954
+ let isIgnored = false;
955
+ const fullGitignorePath = join6(targetDir, gitignorePath);
956
+ if (existsSync4(fullGitignorePath)) {
957
+ const content = readFileSync2(fullGitignorePath, "utf8");
958
+ const lines = content.split(/\r?\n/).map((line) => line.trim());
959
+ isIgnored = lines.includes("runtime/") || lines.includes("runtime");
960
+ }
961
+ if (isTracked || !isIgnored) {
962
+ modifiedAny = true;
963
+ if (isTracked) {
964
+ details.push(`untrack agents/hermes/${role}/runtime`);
965
+ if (!this.context.dryRun) {
966
+ const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
967
+ cwd: targetDir,
968
+ encoding: "utf8"
969
+ });
970
+ if (rmResult.status !== 0) {
971
+ return {
972
+ success: false,
973
+ message: `Failed to untrack agents/hermes/${role}/runtime: ${rmResult.stderr}`
974
+ };
975
+ }
976
+ }
977
+ }
978
+ if (!isIgnored) {
979
+ details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
980
+ if (!this.context.dryRun) {
981
+ let content = "";
982
+ if (existsSync4(fullGitignorePath)) {
983
+ content = readFileSync2(fullGitignorePath, "utf8");
984
+ }
985
+ if (content && !content.endsWith("\n")) {
986
+ content += "\n";
987
+ }
988
+ content += "runtime/\n";
989
+ writeFileSync3(fullGitignorePath, content, "utf8");
990
+ }
991
+ }
992
+ }
993
+ }
994
+ if (!modifiedAny) {
995
+ return {
996
+ success: true,
997
+ message: "\u2705 All Hermes agent runtimes are already untracked and gitignored."
998
+ };
999
+ }
1000
+ const actionText = this.context.dryRun ? "Would make" : "Made";
1001
+ return {
1002
+ success: true,
1003
+ message: `${actionText} Hermes agent runtimes untracked and gitignored:
1004
+ ${details.map((d) => ` - ${d}`).join("\n")}`
1005
+ };
1006
+ }
1007
+ };
1008
+
1009
+ // src/commands/hermes/WireTelegram.ts
1010
+ import { spawnSync as spawnSync3 } from "node:child_process";
1011
+ import { join as join7 } from "node:path";
1012
+ import { existsSync as existsSync5, unlinkSync } from "node:fs";
923
1013
  import * as p3 from "@clack/prompts";
924
1014
  var WireTelegram = class extends Command {
925
1015
  async invoke() {
@@ -941,7 +1031,7 @@ var WireTelegram = class extends Command {
941
1031
  let token = process.env.TELEGRAM_BOT_TOKEN;
942
1032
  let source = token ? "env" : null;
943
1033
  if (!token) {
944
- const tryOp = spawnSync2("op", ["read", vaultRef], { encoding: "utf8" });
1034
+ const tryOp = spawnSync3("op", ["read", vaultRef], { encoding: "utf8" });
945
1035
  if (tryOp.status === 0) {
946
1036
  token = tryOp.stdout.trim();
947
1037
  source = "op";
@@ -980,7 +1070,7 @@ var WireTelegram = class extends Command {
980
1070
  initialValue: true
981
1071
  });
982
1072
  if (!p3.isCancel(persist) && persist) {
983
- const create = spawnSync2(
1073
+ const create = spawnSync3(
984
1074
  "op",
985
1075
  [
986
1076
  "item",
@@ -1007,18 +1097,18 @@ var WireTelegram = class extends Command {
1007
1097
  if (p3.isCancel(allowedAnswer)) {
1008
1098
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
1009
1099
  }
1010
- const script = join6(roleDir, ".scripts", "30-telegram.sh");
1011
- if (!existsSync4(script)) {
1100
+ const script = join7(roleDir, ".scripts", "30-telegram.sh");
1101
+ if (!existsSync5(script)) {
1012
1102
  return {
1013
1103
  success: false,
1014
1104
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
1015
1105
  };
1016
1106
  }
1017
- const marker = join6(roleDir, ".scripts", ".done-30-telegram");
1018
- if (existsSync4(marker)) unlinkSync(marker);
1107
+ const marker = join7(roleDir, ".scripts", ".done-30-telegram");
1108
+ if (existsSync5(marker)) unlinkSync(marker);
1019
1109
  const spinner4 = p3.spinner();
1020
1110
  spinner4.start("Verifying token + wiring profile");
1021
- const result = spawnSync2("bash", [script], {
1111
+ const result = spawnSync3("bash", [script], {
1022
1112
  stdio: "inherit",
1023
1113
  env: {
1024
1114
  ...process.env,
@@ -1041,9 +1131,9 @@ function cap(s) {
1041
1131
  }
1042
1132
 
1043
1133
  // src/commands/hermes/WireEmail.ts
1044
- import { spawnSync as spawnSync3 } from "node:child_process";
1045
- import { join as join7 } from "node:path";
1046
- import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
1134
+ import { spawnSync as spawnSync4 } from "node:child_process";
1135
+ import { join as join8 } from "node:path";
1136
+ import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
1047
1137
  import * as p4 from "@clack/prompts";
1048
1138
  var WireEmail = class extends Command {
1049
1139
  async invoke() {
@@ -1058,13 +1148,13 @@ var WireEmail = class extends Command {
1058
1148
  if (!targetRepo || !role || !roleDir) {
1059
1149
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
1060
1150
  }
1061
- const script = join7(roleDir, ".scripts", "50-email.sh");
1062
- if (!existsSync5(script)) {
1151
+ const script = join8(roleDir, ".scripts", "50-email.sh");
1152
+ if (!existsSync6(script)) {
1063
1153
  return { success: false, message: `\u2717 ${script} not found` };
1064
1154
  }
1065
1155
  let token = process.env.CF_EMAIL_ROUTING_TOKEN;
1066
1156
  if (!token) {
1067
- const tryOp = spawnSync3(
1157
+ const tryOp = spawnSync4(
1068
1158
  "op",
1069
1159
  ["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
1070
1160
  { encoding: "utf8" }
@@ -1104,7 +1194,7 @@ var WireEmail = class extends Command {
1104
1194
  initialValue: true
1105
1195
  });
1106
1196
  if (!p4.isCancel(persist) && persist) {
1107
- const create = spawnSync3(
1197
+ const create = spawnSync4(
1108
1198
  "op",
1109
1199
  [
1110
1200
  "item",
@@ -1121,11 +1211,11 @@ var WireEmail = class extends Command {
1121
1211
  }
1122
1212
  }
1123
1213
  }
1124
- const marker = join7(roleDir, ".scripts", ".done-50-email");
1125
- if (existsSync5(marker)) unlinkSync2(marker);
1214
+ const marker = join8(roleDir, ".scripts", ".done-50-email");
1215
+ if (existsSync6(marker)) unlinkSync2(marker);
1126
1216
  const spinner4 = p4.spinner();
1127
1217
  spinner4.start("Creating Cloudflare Email Routing rule");
1128
- const result = spawnSync3("bash", [script], {
1218
+ const result = spawnSync4("bash", [script], {
1129
1219
  stdio: "inherit",
1130
1220
  env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
1131
1221
  cwd: roleDir
@@ -1185,7 +1275,7 @@ var PrintHermesSummary = class extends Command {
1185
1275
  var HermesAgentRecipe = class extends Recipe {
1186
1276
  constructor(context) {
1187
1277
  super(context);
1188
- this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
1278
+ this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(UntrackHermesRuntimes).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
1189
1279
  }
1190
1280
  // Override execute() to suppress the base class's per-command logging since
1191
1281
  // our commands already render their own UI via @clack/prompts.
@@ -1209,33 +1299,33 @@ var HermesAgentRecipe = class extends Recipe {
1209
1299
 
1210
1300
  // src/commands/AgentHooksCommands.ts
1211
1301
  import { homedir as homedir4 } from "node:os";
1212
- import { join as join9, dirname as dirname5 } from "node:path";
1213
- import { existsSync as existsSync7, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
1302
+ import { join as join10, dirname as dirname5 } from "node:path";
1303
+ import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
1214
1304
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1215
1305
 
1216
1306
  // src/project/index.ts
1217
- import { spawnSync as spawnSync4 } from "node:child_process";
1218
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
1307
+ import { spawnSync as spawnSync5 } from "node:child_process";
1308
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1219
1309
  import { homedir as homedir3 } from "node:os";
1220
- import { basename as basename2, delimiter, dirname as dirname4, join as join8, resolve } from "node:path";
1310
+ import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
1221
1311
  import YAML from "yaml";
1222
1312
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
1223
1313
  var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1224
1314
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1225
1315
  var DEFAULT_SOURCE_SKILL_ROOTS = [
1226
1316
  "/home/delorenj/code/skillex/all-skills",
1227
- join8(homedir3(), ".agents", "skills"),
1228
- join8(homedir3(), ".codex", "skills")
1317
+ join9(homedir3(), ".agents", "skills"),
1318
+ join9(homedir3(), ".codex", "skills")
1229
1319
  ];
1230
1320
  function projectRegistryPath(env2 = process.env) {
1231
- return expandHome(env2[PROJECT_REGISTRY_ENV] || join8(homedir3(), ".config", "pjangler", "projects.yaml"));
1321
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
1232
1322
  }
1233
1323
  function emptyProjectRegistry() {
1234
1324
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
1235
1325
  }
1236
1326
  function loadProjectRegistry(path = projectRegistryPath()) {
1237
- if (!existsSync6(path)) return emptyProjectRegistry();
1238
- const raw = YAML.parse(readFileSync2(path, "utf8"));
1327
+ if (!existsSync7(path)) return emptyProjectRegistry();
1328
+ const raw = YAML.parse(readFileSync3(path, "utf8"));
1239
1329
  if (raw == null) return emptyProjectRegistry();
1240
1330
  if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1241
1331
  const registry = raw;
@@ -1250,7 +1340,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1250
1340
  validateProjectRegistry(registry);
1251
1341
  mkdirSync4(dirname4(path), { recursive: true });
1252
1342
  const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1253
- writeFileSync3(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
1343
+ writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
1254
1344
  renameSync(temp, path);
1255
1345
  }
1256
1346
  function validateProjectRegistry(registry) {
@@ -1332,7 +1422,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
1332
1422
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1333
1423
  if (override === "0" || override === "false") return false;
1334
1424
  if (override === "1" || override === "true") return true;
1335
- return !existsSync6(join8(homedir3(), ".agents", "hooks"));
1425
+ return !existsSync7(join9(homedir3(), ".agents", "hooks"));
1336
1426
  }
1337
1427
  function jsonStable(value) {
1338
1428
  return JSON.stringify(value);
@@ -1363,12 +1453,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1363
1453
  if (!sourceSkill) return void 0;
1364
1454
  const expanded = expandHome(sourceSkill);
1365
1455
  const direct = resolve(expanded);
1366
- if (existsSync6(direct)) return direct;
1456
+ if (existsSync7(direct)) return direct;
1367
1457
  const name = basename2(sourceSkill);
1368
1458
  const roots = sourceSkillRoots(env2);
1369
1459
  for (const root of roots) {
1370
- const candidate = join8(root, name);
1371
- if (existsSync6(candidate)) return candidate;
1460
+ const candidate = join9(root, name);
1461
+ if (existsSync7(candidate)) return candidate;
1372
1462
  }
1373
1463
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1374
1464
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
@@ -1449,7 +1539,7 @@ function planProjectInit(input) {
1449
1539
  }));
1450
1540
  }
1451
1541
  actions.push(
1452
- { kind: "project.write-manifest", path: join8(targetDir, ".project.json"), manifest },
1542
+ { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
1453
1543
  {
1454
1544
  kind: "ticket-provider.create-or-link",
1455
1545
  enabled: live,
@@ -1490,7 +1580,7 @@ function executeProjectInitPlan(plan) {
1490
1580
  action.data.agent_hooks_layer === "false" ? "commonproject: agent-hooks layer skipped (global ~/.agents/hooks detected \u2014 no per-user CLI injection)" : "commonproject: agent-hooks layer included"
1491
1581
  );
1492
1582
  mkdirSync4(dirname4(action.targetDir), { recursive: true });
1493
- const result = spawnSync4(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
1583
+ const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
1494
1584
  if (result.stdout?.trim()) logs.push(result.stdout.trim());
1495
1585
  if (result.stderr?.trim()) logs.push(result.stderr.trim());
1496
1586
  if (result.error) {
@@ -1502,7 +1592,7 @@ function executeProjectInitPlan(plan) {
1502
1592
  }
1503
1593
  if (result.status !== 0) {
1504
1594
  errors.push(`copier exited with status ${result.status ?? "unknown"}`);
1505
- if (existsSync6(action.targetDir)) changedFiles.push(action.targetDir);
1595
+ if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
1506
1596
  break;
1507
1597
  }
1508
1598
  changedFiles.push(action.targetDir);
@@ -1510,9 +1600,9 @@ function executeProjectInitPlan(plan) {
1510
1600
  mkdirSync4(dirname4(action.path), { recursive: true });
1511
1601
  const next = `${JSON.stringify(action.manifest, null, 2)}
1512
1602
  `;
1513
- const current = existsSync6(action.path) ? readFileSync2(action.path, "utf8") : void 0;
1603
+ const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
1514
1604
  if (current !== next) {
1515
- writeFileSync3(action.path, next, "utf8");
1605
+ writeFileSync4(action.path, next, "utf8");
1516
1606
  changedFiles.push(action.path);
1517
1607
  }
1518
1608
  } else if (action.kind === "registry.upsert") {
@@ -1605,16 +1695,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
1605
1695
  const registry = loadProjectRegistry(registryPath2);
1606
1696
  const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
1607
1697
  for (const [projectSlug, project] of projects) {
1608
- if (!existsSync6(project.repo_path)) {
1698
+ if (!existsSync7(project.repo_path)) {
1609
1699
  issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
1610
1700
  } else if (!statSync(project.repo_path).isDirectory()) {
1611
1701
  issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
1612
1702
  } else {
1613
- const manifestPath = join8(project.repo_path, ".project.json");
1614
- if (!existsSync6(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
1703
+ const manifestPath = join9(project.repo_path, ".project.json");
1704
+ if (!existsSync7(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
1615
1705
  }
1616
1706
  for (const artifact of project.source_artifacts) {
1617
- if (artifact.path && !existsSync6(artifact.path)) {
1707
+ if (artifact.path && !existsSync7(artifact.path)) {
1618
1708
  issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
1619
1709
  }
1620
1710
  }
@@ -1627,7 +1717,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
1627
1717
  };
1628
1718
  }
1629
1719
  function buildCommonProjectCopierAction(input) {
1630
- const templateDir = join8(input.pjanglerRoot, "templates", "commonproject");
1720
+ const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
1631
1721
  const data = {
1632
1722
  project_name: input.projectName,
1633
1723
  project_description: input.projectDescription ?? "",
@@ -1656,7 +1746,7 @@ function buildCommonProjectCopierAction(input) {
1656
1746
  function resolvePjanglerRoot() {
1657
1747
  let dir = dirname4(new URL(import.meta.url).pathname);
1658
1748
  while (dir !== dirname4(dir)) {
1659
- if (existsSync6(join8(dir, "package.json")) && existsSync6(join8(dir, "templates", "commonproject", "copier.yml"))) return dir;
1749
+ if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
1660
1750
  dir = dirname4(dir);
1661
1751
  }
1662
1752
  return resolve(process.cwd());
@@ -1688,7 +1778,7 @@ function validateProjectRecord(project, key) {
1688
1778
  }
1689
1779
  function expandHome(path) {
1690
1780
  if (path === "~") return homedir3();
1691
- if (path.startsWith("~/")) return join8(homedir3(), path.slice(2));
1781
+ if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
1692
1782
  return path;
1693
1783
  }
1694
1784
  function isRecord(value) {
@@ -1705,16 +1795,16 @@ function resolveTemplateRoot() {
1705
1795
  try {
1706
1796
  let dir = dirname5(fileURLToPath2(import.meta.url));
1707
1797
  for (let i = 0; i < 8; i++) {
1708
- candidates.push(join9(dir, "templates", "commonproject", "template"));
1798
+ candidates.push(join10(dir, "templates", "commonproject", "template"));
1709
1799
  const parent = dirname5(dir);
1710
1800
  if (parent === dir) break;
1711
1801
  dir = parent;
1712
1802
  }
1713
1803
  } catch {
1714
1804
  }
1715
- candidates.push(join9(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1805
+ candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1716
1806
  for (const c of candidates) {
1717
- if (existsSync7(join9(c, ".agents", "hooks", "hooks.master.json"))) return c;
1807
+ if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
1718
1808
  }
1719
1809
  throw new Error(
1720
1810
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1741,10 +1831,10 @@ var CopyAgentHooksTree = class extends Command {
1741
1831
  const created = [];
1742
1832
  const skipped = [];
1743
1833
  for (const { rel, dir } of items) {
1744
- const src = join9(templateRoot, rel);
1745
- const dest = join9(this.context.targetDir, rel);
1746
- if (!existsSync7(src)) continue;
1747
- if (existsSync7(dest) && !this.context.force) {
1834
+ const src = join10(templateRoot, rel);
1835
+ const dest = join10(this.context.targetDir, rel);
1836
+ if (!existsSync8(src)) continue;
1837
+ if (existsSync8(dest) && !this.context.force) {
1748
1838
  skipped.push(rel);
1749
1839
  continue;
1750
1840
  }
@@ -1770,14 +1860,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1770
1860
  if (!resolveAgentHooksLayer()) {
1771
1861
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
1772
1862
  }
1773
- const misePath = join9(this.context.targetDir, "mise.toml");
1774
- if (!existsSync7(misePath)) {
1863
+ const misePath = join10(this.context.targetDir, "mise.toml");
1864
+ if (!existsSync8(misePath)) {
1775
1865
  return {
1776
1866
  success: false,
1777
1867
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
1778
1868
  };
1779
1869
  }
1780
- let content = readFileSync3(misePath, "utf8");
1870
+ let content = readFileSync4(misePath, "utf8");
1781
1871
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
1782
1872
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
1783
1873
  }
@@ -1853,7 +1943,7 @@ ${leaveBlock}`);
1853
1943
  ""
1854
1944
  ].join("\n");
1855
1945
  content = content.replace(/\n*$/, "\n") + appended;
1856
- if (!this.context.dryRun) writeFileSync4(misePath, content);
1946
+ if (!this.context.dryRun) writeFileSync5(misePath, content);
1857
1947
  if (wiredHooks) {
1858
1948
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
1859
1949
  }
@@ -2026,11 +2116,12 @@ function createRecipe(name, context) {
2026
2116
  import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
2027
2117
 
2028
2118
  // src/parity/index.ts
2029
- import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync as chmodSync2, copyFileSync } from "node:fs";
2030
- import { basename as basename3, dirname as dirname6, join as join10, relative, resolve as resolve2 } from "node:path";
2119
+ import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
2120
+ import { basename as basename3, dirname as dirname6, join as join11, relative, resolve as resolve2 } from "node:path";
2031
2121
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2032
2122
  import { homedir as homedir5 } from "node:os";
2033
- import { spawnSync as spawnSync5 } from "node:child_process";
2123
+ import { spawnSync as spawnSync6 } from "node:child_process";
2124
+ import YAML2 from "yaml";
2034
2125
  var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
2035
2126
  # agent files to the main AGENTS.md file.
2036
2127
  #
@@ -2101,7 +2192,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
2101
2192
  function resolvePjanglerRoot2() {
2102
2193
  let dir = dirname6(fileURLToPath3(import.meta.url));
2103
2194
  while (dir !== dirname6(dir)) {
2104
- if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) {
2195
+ if (existsSync9(join11(dir, "package.json")) && existsSync9(join11(dir, "templates", "commonproject", "copier.yml"))) {
2105
2196
  return dir;
2106
2197
  }
2107
2198
  dir = dirname6(dir);
@@ -2112,17 +2203,17 @@ function normalizeNewlines(value) {
2112
2203
  return value.replace(/\r\n/g, "\n");
2113
2204
  }
2114
2205
  function readText(path) {
2115
- return normalizeNewlines(readFileSync4(path, "utf8"));
2206
+ return normalizeNewlines(readFileSync5(path, "utf8"));
2116
2207
  }
2117
2208
  function safeReadText(path) {
2118
- return existsSync8(path) ? readText(path) : null;
2209
+ return existsSync9(path) ? readText(path) : null;
2119
2210
  }
2120
2211
  function ensureParent(path) {
2121
2212
  mkdirSync6(dirname6(path), { recursive: true });
2122
2213
  }
2123
2214
  function writeText(path, content) {
2124
2215
  ensureParent(path);
2125
- writeFileSync5(path, content);
2216
+ writeFileSync6(path, content);
2126
2217
  }
2127
2218
  function tryParseJson(text3) {
2128
2219
  if (!text3) return null;
@@ -2139,7 +2230,7 @@ function titleCaseSlug(slug) {
2139
2230
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2140
2231
  }
2141
2232
  function readSymlinkTarget(path) {
2142
- if (!existsSync8(path)) return null;
2233
+ if (!existsSync9(path)) return null;
2143
2234
  try {
2144
2235
  return readlinkSync(path);
2145
2236
  } catch {
@@ -2147,7 +2238,7 @@ function readSymlinkTarget(path) {
2147
2238
  }
2148
2239
  }
2149
2240
  function ensureSymlink(path, target, dryRun) {
2150
- if (existsSync8(path)) {
2241
+ if (existsSync9(path)) {
2151
2242
  const stat = lstatSync(path);
2152
2243
  if (stat.isSymbolicLink()) {
2153
2244
  const current = readSymlinkTarget(path);
@@ -2164,11 +2255,11 @@ function ensureSymlink(path, target, dryRun) {
2164
2255
  return { changed: true };
2165
2256
  }
2166
2257
  function bootstrapAgentsFile(repoRoot, dryRun) {
2167
- const agentsPath = join10(repoRoot, "AGENTS.md");
2168
- if (existsSync8(agentsPath)) return { changedFiles: [], details: [] };
2258
+ const agentsPath = join11(repoRoot, "AGENTS.md");
2259
+ if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
2169
2260
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2170
- const source = join10(repoRoot, file);
2171
- if (!existsSync8(source)) continue;
2261
+ const source = join11(repoRoot, file);
2262
+ if (!existsSync9(source)) continue;
2172
2263
  const stat = lstatSync(source);
2173
2264
  if (stat.isSymbolicLink()) continue;
2174
2265
  if (stat.isFile()) {
@@ -2177,8 +2268,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
2177
2268
  }
2178
2269
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
2179
2270
  }
2180
- const readmePath = join10(repoRoot, "README.md");
2181
- if (existsSync8(readmePath)) {
2271
+ const readmePath = join11(repoRoot, "README.md");
2272
+ if (existsSync9(readmePath)) {
2182
2273
  const stat = lstatSync(readmePath);
2183
2274
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
2184
2275
  if (!dryRun) copyFileSync(readmePath, agentsPath);
@@ -2217,12 +2308,12 @@ function yamlGet(text3, keyPath) {
2217
2308
  return "";
2218
2309
  }
2219
2310
  function discoverRoles(repoRoot) {
2220
- const rolesDir = join10(repoRoot, "agents", "hermes");
2221
- if (!existsSync8(rolesDir)) return [];
2222
- return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2223
- const roleDir = join10(rolesDir, entry.name);
2224
- const roleYamlPath = join10(roleDir, "role.yaml");
2225
- if (!existsSync8(roleYamlPath)) return null;
2311
+ const rolesDir = join11(repoRoot, "agents", "hermes");
2312
+ if (!existsSync9(rolesDir)) return [];
2313
+ return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2314
+ const roleDir = join11(rolesDir, entry.name);
2315
+ const roleYamlPath = join11(roleDir, "role.yaml");
2316
+ if (!existsSync9(roleYamlPath)) return null;
2226
2317
  const text3 = readText(roleYamlPath);
2227
2318
  const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
2228
2319
  return {
@@ -2250,10 +2341,10 @@ function discoverRoles(repoRoot) {
2250
2341
  }).filter((value) => Boolean(value));
2251
2342
  }
2252
2343
  function registryPath(homeDir) {
2253
- return join10(homeDir, ".hermes", "agents-registry.yaml");
2344
+ return join11(homeDir, ".hermes", "agents-registry.yaml");
2254
2345
  }
2255
2346
  function systemctlUser(args) {
2256
- const result = spawnSync5("systemctl", ["--user", ...args], { encoding: "utf8" });
2347
+ const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
2257
2348
  return {
2258
2349
  ok: result.status === 0,
2259
2350
  stdout: result.stdout.trim(),
@@ -2261,8 +2352,8 @@ function systemctlUser(args) {
2261
2352
  };
2262
2353
  }
2263
2354
  function templateScript(ctx, name) {
2264
- const source = join10(ctx.pjanglerRoot, ".mise", "scripts", name);
2265
- return existsSync8(source) ? readText(source) : void 0;
2355
+ const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
2356
+ return existsSync9(source) ? readText(source) : void 0;
2266
2357
  }
2267
2358
  function templateVersioningScript(ctx) {
2268
2359
  return templateScript(ctx, "versioning.sh");
@@ -2274,8 +2365,8 @@ function resolveAgentHooksLayer2(ctx) {
2274
2365
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
2275
2366
  if (override === "0" || override === "false") return false;
2276
2367
  if (override === "1" || override === "true") return true;
2277
- if (existsSync8(join10(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2278
- return !existsSync8(join10(ctx.homeDir, ".agents", "hooks"));
2368
+ if (existsSync9(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2369
+ return !existsSync9(join11(ctx.homeDir, ".agents", "hooks"));
2279
2370
  }
2280
2371
  function evaluateMiseConditionals(template, agentHooksLayer) {
2281
2372
  const out = [];
@@ -2305,10 +2396,10 @@ function renderGeneratedProjectMiseToml(ctx, template) {
2305
2396
  return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2306
2397
  }
2307
2398
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2308
- const targetPath = join10(ctx.repoRoot, "mise.toml");
2309
- if (existsSync8(targetPath)) return false;
2310
- const sourcePath = join10(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2311
- if (!existsSync8(sourcePath)) return false;
2399
+ const targetPath = join11(ctx.repoRoot, "mise.toml");
2400
+ if (existsSync9(targetPath)) return false;
2401
+ const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2402
+ if (!existsSync9(sourcePath)) return false;
2312
2403
  changedFiles.push(targetPath);
2313
2404
  if (!ctx.dryRun) {
2314
2405
  writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
@@ -2316,8 +2407,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2316
2407
  return true;
2317
2408
  }
2318
2409
  function templateVersionFilesConf(ctx, repoRoot) {
2319
- const packageJson = join10(repoRoot, "package.json");
2320
- return existsSync8(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";
2410
+ const packageJson = join11(repoRoot, "package.json");
2411
+ return existsSync9(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";
2321
2412
  }
2322
2413
  function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
2323
2414
  if (startMarker.test(text3)) {
@@ -2341,7 +2432,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
2341
2432
  function requiredMisePathEntries(ctx) {
2342
2433
  const required = [...BASE_MISE_PATH_ENTRIES];
2343
2434
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
2344
- if (existsSync8(join10(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2435
+ if (existsSync9(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2345
2436
  }
2346
2437
  return required;
2347
2438
  }
@@ -2490,7 +2581,7 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
2490
2581
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
2491
2582
  }
2492
2583
  function readProjectJson(ctx) {
2493
- return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
2584
+ return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
2494
2585
  }
2495
2586
  function boolSetting(value, fallback) {
2496
2587
  if (typeof value === "boolean") return value;
@@ -2565,12 +2656,12 @@ function canonicalProjectJson(ctx) {
2565
2656
  };
2566
2657
  }
2567
2658
  function projectJsonFinding(ctx) {
2568
- const projectPath = join10(ctx.repoRoot, ".project.json");
2569
- const planeJsonPath = join10(ctx.repoRoot, ".plane.json");
2659
+ const projectPath = join11(ctx.repoRoot, ".project.json");
2660
+ const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
2570
2661
  const details = [];
2571
2662
  const data = readProjectJson(ctx);
2572
2663
  const roles = discoverRoles(ctx.repoRoot);
2573
- if (!existsSync8(projectPath)) {
2664
+ if (!existsSync9(projectPath)) {
2574
2665
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
2575
2666
  }
2576
2667
  if (!data) {
@@ -2605,7 +2696,7 @@ function projectJsonFinding(ctx) {
2605
2696
  for (const key of ["enabled", "grace_hours", "auto_review"]) {
2606
2697
  if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2607
2698
  }
2608
- if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2699
+ if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2609
2700
  return {
2610
2701
  id: "sot.project-json",
2611
2702
  title: "Canonical .project.json",
@@ -2686,17 +2777,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
2686
2777
  `.replace(/\u0010/g, "$");
2687
2778
  }
2688
2779
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2689
- if (!existsSync8(sourceDir)) return;
2780
+ if (!existsSync9(sourceDir)) return;
2690
2781
  mkdirSync6(targetDir, { recursive: true });
2691
- for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2692
- const sourcePath = join10(sourceDir, entry.name);
2782
+ for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
2783
+ const sourcePath = join11(sourceDir, entry.name);
2693
2784
  if (skip?.(sourcePath)) continue;
2694
- const targetPath = join10(targetDir, entry.name);
2785
+ const targetPath = join11(targetDir, entry.name);
2695
2786
  if (entry.isDirectory()) {
2696
2787
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2697
2788
  continue;
2698
2789
  }
2699
- if (existsSync8(targetPath)) continue;
2790
+ if (existsSync9(targetPath)) continue;
2700
2791
  changedFiles.push(targetPath);
2701
2792
  if (!dryRun) {
2702
2793
  ensureParent(targetPath);
@@ -2705,7 +2796,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2705
2796
  }
2706
2797
  }
2707
2798
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2708
- const gitmodulesPath = join10(repoRoot, ".gitmodules");
2799
+ const gitmodulesPath = join11(repoRoot, ".gitmodules");
2709
2800
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2710
2801
  const owner = role.runtimeOwner || "delorenj";
2711
2802
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2800,19 +2891,186 @@ function checkUnit(unit) {
2800
2891
  const active = systemctlUser(["is-active", unit]).ok;
2801
2892
  return { enabled, active };
2802
2893
  }
2894
+ var BMAD_NPM_PACKAGE = "bmad-method";
2895
+ var BMAD_TARGET_CHANNEL = "next";
2896
+ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
2897
+ var BMAD_INSTALL_TOOLS = [
2898
+ "claude-code",
2899
+ "codex",
2900
+ "cursor",
2901
+ "github-copilot",
2902
+ "adal",
2903
+ "antigravity-cli",
2904
+ "auggie",
2905
+ "goose",
2906
+ "cline",
2907
+ "codebuddy",
2908
+ "codewhale",
2909
+ "command-code",
2910
+ "crush",
2911
+ "droid",
2912
+ "firebender",
2913
+ "gemini",
2914
+ "antigravity",
2915
+ "hermes",
2916
+ "bob",
2917
+ "iflow",
2918
+ "junie",
2919
+ "kilo",
2920
+ "kimi-code",
2921
+ "kiro",
2922
+ "kode",
2923
+ "mistral-vibe",
2924
+ "mux",
2925
+ "neovate",
2926
+ "ona",
2927
+ "openclaw",
2928
+ "opencode",
2929
+ "openhands",
2930
+ "pi",
2931
+ "pochi",
2932
+ "qoder",
2933
+ "qwen",
2934
+ "replit",
2935
+ "roo",
2936
+ "rovo-dev",
2937
+ "cortex",
2938
+ "amp",
2939
+ "trae",
2940
+ "warp",
2941
+ "windsurf",
2942
+ "zencoder"
2943
+ ];
2944
+ function bmadInstallArgs(repoRoot) {
2945
+ return [
2946
+ "-y",
2947
+ `${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
2948
+ "install",
2949
+ "--yes",
2950
+ "--directory",
2951
+ repoRoot,
2952
+ "--modules",
2953
+ "bmm,bmb,cis",
2954
+ "--tools",
2955
+ BMAD_INSTALL_TOOLS.join(",")
2956
+ ];
2957
+ }
2958
+ function runBmadInstall(repoRoot) {
2959
+ const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
2960
+ if (result.status !== 0) {
2961
+ return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
2962
+ }
2963
+ return { ok: true };
2964
+ }
2965
+ function readInstalledBmadVersion(repoRoot) {
2966
+ const raw = safeReadText(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
2967
+ if (!raw) return void 0;
2968
+ try {
2969
+ const parsed = YAML2.parse(raw);
2970
+ const version = parsed?.installation?.version;
2971
+ return typeof version === "string" && version.trim() ? version.trim() : void 0;
2972
+ } catch {
2973
+ return void 0;
2974
+ }
2975
+ }
2976
+ function bmadCachePath(homeDir) {
2977
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join11(homeDir, ".cache");
2978
+ return join11(cacheRoot, "pjangler", "bmad-dist-tags.json");
2979
+ }
2980
+ function readBmadDistTagsCache(homeDir) {
2981
+ const raw = safeReadText(bmadCachePath(homeDir));
2982
+ if (!raw) return void 0;
2983
+ try {
2984
+ const parsed = JSON.parse(raw);
2985
+ if (parsed && typeof parsed.fetchedAt === "number" && parsed.distTags && typeof parsed.distTags === "object") {
2986
+ return parsed;
2987
+ }
2988
+ } catch {
2989
+ }
2990
+ return void 0;
2991
+ }
2992
+ function fetchBmadDistTags() {
2993
+ const result = spawnSync6("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
2994
+ encoding: "utf8",
2995
+ timeout: 8e3
2996
+ });
2997
+ if (result.status !== 0 || !result.stdout.trim()) return void 0;
2998
+ try {
2999
+ const parsed = JSON.parse(result.stdout);
3000
+ const obj = Array.isArray(parsed) ? parsed[0] : parsed;
3001
+ if (!obj || typeof obj !== "object") return void 0;
3002
+ const tags = {};
3003
+ for (const [key, value] of Object.entries(obj)) {
3004
+ if (typeof value === "string") tags[key] = value;
3005
+ }
3006
+ return Object.keys(tags).length ? tags : void 0;
3007
+ } catch {
3008
+ return void 0;
3009
+ }
3010
+ }
3011
+ function resolveBmadDistTags(homeDir) {
3012
+ const cached = readBmadDistTagsCache(homeDir);
3013
+ if (cached && Date.now() - cached.fetchedAt < BMAD_DIST_TAGS_TTL_MS) {
3014
+ return { distTags: cached.distTags, stale: false };
3015
+ }
3016
+ const fetched = fetchBmadDistTags();
3017
+ if (fetched) {
3018
+ try {
3019
+ const path = bmadCachePath(homeDir);
3020
+ mkdirSync6(dirname6(path), { recursive: true });
3021
+ writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3022
+ } catch {
3023
+ }
3024
+ return { distTags: fetched, stale: false };
3025
+ }
3026
+ if (cached) return { distTags: cached.distTags, stale: true };
3027
+ return void 0;
3028
+ }
3029
+ function compareBmadVersions(a, b) {
3030
+ const parse = (v) => {
3031
+ const [core = "0", pre = ""] = v.replace(/^v/, "").split("-", 2);
3032
+ const parts = core.split(".");
3033
+ const n = (i) => parseInt(parts[i] ?? "0", 10) || 0;
3034
+ return { nums: [n(0), n(1), n(2)], pre };
3035
+ };
3036
+ const pa = parse(a);
3037
+ const pb = parse(b);
3038
+ for (let i = 0; i < 3; i++) {
3039
+ if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] - pb.nums[i];
3040
+ }
3041
+ if (pa.pre === pb.pre) return 0;
3042
+ if (!pa.pre) return 1;
3043
+ if (!pb.pre) return -1;
3044
+ const ida = pa.pre.split(".");
3045
+ const idb = pb.pre.split(".");
3046
+ for (let i = 0; i < Math.max(ida.length, idb.length); i++) {
3047
+ const xa = ida[i];
3048
+ const xb = idb[i];
3049
+ if (xa === void 0) return -1;
3050
+ if (xb === void 0) return 1;
3051
+ const na = Number(xa);
3052
+ const nb = Number(xb);
3053
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
3054
+ if (na !== nb) return na - nb;
3055
+ } else if (xa !== xb) {
3056
+ return xa < xb ? -1 : 1;
3057
+ }
3058
+ }
3059
+ return 0;
3060
+ }
2803
3061
  var RULES = [
2804
3062
  {
2805
3063
  id: "mise.config-root",
2806
3064
  title: "mise config_root + AGENTS link hooks",
2807
3065
  audit: (ctx) => {
2808
- const misePath = join10(ctx.repoRoot, "mise.toml");
2809
- if (!existsSync8(misePath)) {
3066
+ const misePath = join11(ctx.repoRoot, "mise.toml");
3067
+ if (!existsSync9(misePath)) {
2810
3068
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2811
3069
  }
2812
3070
  const text3 = readText(misePath);
2813
3071
  const details = [];
2814
- const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2815
- if (!existsSync8(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
3072
+ const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3073
+ if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2816
3074
  const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2817
3075
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2818
3076
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
@@ -2830,10 +3088,10 @@ var RULES = [
2830
3088
  };
2831
3089
  },
2832
3090
  migrate: (ctx, finding) => {
2833
- const path = join10(ctx.repoRoot, "mise.toml");
3091
+ const path = join11(ctx.repoRoot, "mise.toml");
2834
3092
  const changedFiles = [];
2835
3093
  const details = [];
2836
- if (!existsSync8(path)) {
3094
+ if (!existsSync9(path)) {
2837
3095
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2838
3096
  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: [] };
2839
3097
  }
@@ -2849,7 +3107,7 @@ var RULES = [
2849
3107
  if (!ctx.dryRun) writeText(path, next);
2850
3108
  text3 = next;
2851
3109
  }
2852
- const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3110
+ const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2853
3111
  const expectedScript = templateLinkAgentfilesScript(ctx);
2854
3112
  if (expectedScript === void 0) {
2855
3113
  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: [] };
@@ -2876,13 +3134,13 @@ var RULES = [
2876
3134
  title: "managed mise versioning block",
2877
3135
  audit: (ctx) => {
2878
3136
  const details = [];
2879
- const misePath = join10(ctx.repoRoot, "mise.toml");
2880
- const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2881
- const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
3137
+ const misePath = join11(ctx.repoRoot, "mise.toml");
3138
+ const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3139
+ const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
2882
3140
  const text3 = safeReadText(misePath);
2883
3141
  if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2884
- if (!existsSync8(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2885
- if (!existsSync8(manifestPath)) details.push(".mise/version-files.conf missing");
3142
+ if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3143
+ if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
2886
3144
  return {
2887
3145
  id: "mise.versioning",
2888
3146
  title: "managed mise versioning block",
@@ -2895,8 +3153,8 @@ var RULES = [
2895
3153
  migrate: (ctx, finding) => {
2896
3154
  const changedFiles = [];
2897
3155
  const details = [];
2898
- const misePath = join10(ctx.repoRoot, "mise.toml");
2899
- if (!existsSync8(misePath)) {
3156
+ const misePath = join11(ctx.repoRoot, "mise.toml");
3157
+ if (!existsSync9(misePath)) {
2900
3158
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2901
3159
  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: [] };
2902
3160
  }
@@ -2906,12 +3164,21 @@ var RULES = [
2906
3164
  }
2907
3165
  }
2908
3166
  const currentMise = readText(misePath);
2909
- const nextMise = replaceOrAppendManagedBlock(currentMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
3167
+ let cleanedMise = currentMise;
3168
+ if (!currentMise.includes("# >>> mise-versioning >>>")) {
3169
+ const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
3170
+ for (const taskName of taskNames) {
3171
+ const escaped = taskName.replace(/:/g, "\\:");
3172
+ const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
3173
+ cleanedMise = removeTomlSection(cleanedMise, headerPattern);
3174
+ }
3175
+ }
3176
+ const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
2910
3177
  if (nextMise !== currentMise) {
2911
3178
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
2912
3179
  if (!ctx.dryRun) writeText(misePath, nextMise);
2913
3180
  }
2914
- const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3181
+ const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2915
3182
  const expectedScript = templateVersioningScript(ctx);
2916
3183
  if (expectedScript === void 0) {
2917
3184
  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: [] };
@@ -2923,7 +3190,7 @@ var RULES = [
2923
3190
  chmodSync2(versioningPath, 493);
2924
3191
  }
2925
3192
  }
2926
- const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
3193
+ const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
2927
3194
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
2928
3195
  if (safeReadText(manifestPath) !== expectedManifest) {
2929
3196
  changedFiles.push(manifestPath);
@@ -2943,9 +3210,9 @@ var RULES = [
2943
3210
  id: "sot.agent-symlinks",
2944
3211
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
2945
3212
  audit: (ctx) => {
2946
- const agentsPath = join10(ctx.repoRoot, "AGENTS.md");
2947
- if (!existsSync8(agentsPath)) {
2948
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync8(join10(ctx.repoRoot, file)));
3213
+ const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
3214
+ if (!existsSync9(agentsPath)) {
3215
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join11(ctx.repoRoot, file)));
2949
3216
  if (fallbackSources.length === 0) {
2950
3217
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
2951
3218
  }
@@ -2960,7 +3227,7 @@ var RULES = [
2960
3227
  }
2961
3228
  const details = [];
2962
3229
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2963
- const full = join10(ctx.repoRoot, file);
3230
+ const full = join11(ctx.repoRoot, file);
2964
3231
  const target = readSymlinkTarget(full);
2965
3232
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
2966
3233
  }
@@ -2984,7 +3251,7 @@ var RULES = [
2984
3251
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
2985
3252
  }
2986
3253
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2987
- const full = join10(ctx.repoRoot, file);
3254
+ const full = join11(ctx.repoRoot, file);
2988
3255
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
2989
3256
  if (result.blocked) blockedDetails.push(result.blocked);
2990
3257
  if (result.changed) changedFiles.push(full);
@@ -3006,7 +3273,7 @@ var RULES = [
3006
3273
  migrate: (ctx, finding) => {
3007
3274
  const changedFiles = [];
3008
3275
  const details = [];
3009
- const path = join10(ctx.repoRoot, ".project.json");
3276
+ const path = join11(ctx.repoRoot, ".project.json");
3010
3277
  const existing = readProjectJson(ctx) ?? {};
3011
3278
  const canonical = canonicalProjectJson(ctx);
3012
3279
  const merged = { ...existing, ...canonical };
@@ -3016,10 +3283,10 @@ var RULES = [
3016
3283
  changedFiles.push(path);
3017
3284
  if (!ctx.dryRun) writeText(path, expected);
3018
3285
  }
3019
- const planeJson = join10(ctx.repoRoot, ".plane.json");
3020
- if (existsSync8(planeJson)) {
3286
+ const planeJson = join11(ctx.repoRoot, ".plane.json");
3287
+ if (existsSync9(planeJson)) {
3021
3288
  const backup = `${planeJson}.migrated-backup`;
3022
- if (existsSync8(backup)) {
3289
+ if (existsSync9(backup)) {
3023
3290
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
3024
3291
  } else {
3025
3292
  changedFiles.push(backup);
@@ -3041,8 +3308,8 @@ var RULES = [
3041
3308
  title: ".env.op + gitignore secrets contract",
3042
3309
  audit: (ctx) => {
3043
3310
  const details = [];
3044
- const envOp = safeReadText(join10(ctx.repoRoot, ".env.op"));
3045
- const gitignore = safeReadText(join10(ctx.repoRoot, ".gitignore"));
3311
+ const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
3312
+ const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
3046
3313
  if (!envOp) {
3047
3314
  details.push(".env.op missing");
3048
3315
  } else {
@@ -3068,12 +3335,12 @@ var RULES = [
3068
3335
  migrate: (ctx, finding) => {
3069
3336
  const changedFiles = [];
3070
3337
  const details = [];
3071
- const envOpPath = join10(ctx.repoRoot, ".env.op");
3072
- if (!existsSync8(envOpPath)) {
3338
+ const envOpPath = join11(ctx.repoRoot, ".env.op");
3339
+ if (!existsSync9(envOpPath)) {
3073
3340
  changedFiles.push(envOpPath);
3074
- if (!ctx.dryRun) writeText(envOpPath, readText(join10(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3341
+ if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3075
3342
  }
3076
- const gitignorePath = join10(ctx.repoRoot, ".gitignore");
3343
+ const gitignorePath = join11(ctx.repoRoot, ".gitignore");
3077
3344
  const gitignore = safeReadText(gitignorePath) ?? "";
3078
3345
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
3079
3346
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -3100,7 +3367,7 @@ var RULES = [
3100
3367
  title: ".copier-answers.yml provenance + drift report",
3101
3368
  audit: (ctx) => {
3102
3369
  const details = [];
3103
- const path = join10(ctx.repoRoot, ".copier-answers.yml");
3370
+ const path = join11(ctx.repoRoot, ".copier-answers.yml");
3104
3371
  const text3 = safeReadText(path);
3105
3372
  const project = readProjectJson(ctx);
3106
3373
  if (!text3) {
@@ -3131,12 +3398,12 @@ var RULES = [
3131
3398
  const changedFiles = [];
3132
3399
  const project = canonicalProjectJson(ctx);
3133
3400
  const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
3134
- _src_path: ${join10(ctx.pjanglerRoot, "templates", "commonproject")}
3401
+ _src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
3135
3402
  project_description: ${String(project.project_description)}
3136
3403
  project_name: ${String(project.project_name)}
3137
3404
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3138
3405
  `;
3139
- const path = join10(ctx.repoRoot, ".copier-answers.yml");
3406
+ const path = join11(ctx.repoRoot, ".copier-answers.yml");
3140
3407
  if (safeReadText(path) !== text3) {
3141
3408
  changedFiles.push(path);
3142
3409
  if (!ctx.dryRun) writeText(path, text3);
@@ -3155,15 +3422,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3155
3422
  id: "bmad.scaffold",
3156
3423
  title: "BMAD modules/docs scaffold",
3157
3424
  audit: (ctx) => {
3158
- const sourceRoot = join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
3159
- const targetRoot = join10(ctx.repoRoot, "_bmad");
3425
+ const targetRoot = join11(ctx.repoRoot, "_bmad");
3160
3426
  const sentinels = [
3161
- join10("core", "config.yaml"),
3162
- join10("custom", "config.yaml"),
3163
- join10("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
3164
- join10("bmm", "workflows", "workflow-status", "workflow.yaml")
3427
+ join11("core", "config.yaml"),
3428
+ join11("config.toml"),
3429
+ join11("_config", "manifest.yaml"),
3430
+ join11("bmm", "config.yaml")
3165
3431
  ];
3166
- const missing = sentinels.filter((file) => existsSync8(join10(sourceRoot, file)) && !existsSync8(join10(targetRoot, file)));
3432
+ const missing = sentinels.filter((file) => !existsSync9(join11(targetRoot, file)));
3167
3433
  return {
3168
3434
  id: "bmad.scaffold",
3169
3435
  title: "BMAD modules/docs scaffold",
@@ -3175,17 +3441,148 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3175
3441
  },
3176
3442
  migrate: (ctx, finding) => {
3177
3443
  const changedFiles = [];
3178
- copyMissingRecursive(join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join10(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
3444
+ if (ctx.dryRun) {
3445
+ for (const detail of finding.details) {
3446
+ changedFiles.push(join11(ctx.repoRoot, detail));
3447
+ }
3448
+ return {
3449
+ id: finding.id,
3450
+ title: finding.title,
3451
+ status: changedFiles.length ? "applied" : "noop",
3452
+ summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
3453
+ changedFiles,
3454
+ details: [
3455
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3456
+ ]
3457
+ };
3458
+ }
3459
+ const install = runBmadInstall(ctx.repoRoot);
3460
+ if (!install.ok) {
3461
+ return {
3462
+ id: finding.id,
3463
+ title: finding.title,
3464
+ status: "blocked",
3465
+ summary: `Failed to run bmad-method install`,
3466
+ changedFiles: [],
3467
+ details: [install.error ?? "Unknown error"]
3468
+ };
3469
+ }
3470
+ for (const detail of finding.details) {
3471
+ if (existsSync9(join11(ctx.repoRoot, detail))) {
3472
+ changedFiles.push(join11(ctx.repoRoot, detail));
3473
+ }
3474
+ }
3179
3475
  return {
3180
3476
  id: finding.id,
3181
3477
  title: finding.title,
3182
3478
  status: changedFiles.length ? "applied" : "noop",
3183
- summary: changedFiles.length ? "Copied missing BMAD scaffold files" : "No changes required",
3479
+ summary: changedFiles.length ? "Installed BMAD scaffold via non-interactive installer" : "No changes required",
3184
3480
  changedFiles,
3185
3481
  details: []
3186
3482
  };
3187
3483
  }
3188
3484
  },
3485
+ {
3486
+ id: "bmad.version",
3487
+ title: "BMAD version currency",
3488
+ audit: (ctx) => {
3489
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3490
+ if (!installed) {
3491
+ return {
3492
+ id: "bmad.version",
3493
+ title: "BMAD version currency",
3494
+ status: "skip",
3495
+ summary: existsSync9(join11(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
3496
+ details: [],
3497
+ fixable: false
3498
+ };
3499
+ }
3500
+ const resolved = resolveBmadDistTags(ctx.homeDir);
3501
+ const available = resolved?.distTags?.[BMAD_TARGET_CHANNEL];
3502
+ if (!available) {
3503
+ return {
3504
+ id: "bmad.version",
3505
+ title: "BMAD version currency",
3506
+ status: "skip",
3507
+ summary: `BMAD ${installed} installed; latest ${BMAD_TARGET_CHANNEL} version unknown (npm unreachable)`,
3508
+ details: [`Could not resolve ${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL} from npm`],
3509
+ fixable: false
3510
+ };
3511
+ }
3512
+ const staleNote = resolved.stale ? ` ${glyph.dot} cached` : "";
3513
+ if (compareBmadVersions(installed, available) >= 0) {
3514
+ return {
3515
+ id: "bmad.version",
3516
+ title: "BMAD version currency",
3517
+ status: "pass",
3518
+ summary: `BMAD ${installed} is current (${BMAD_TARGET_CHANNEL} ${available})${staleNote}`,
3519
+ details: [],
3520
+ fixable: false
3521
+ };
3522
+ }
3523
+ return {
3524
+ id: "bmad.version",
3525
+ title: "BMAD version currency",
3526
+ status: "warn",
3527
+ summary: `BMAD ${installed} is behind ${BMAD_TARGET_CHANNEL} ${available} \u2014 upgrade available`,
3528
+ details: [
3529
+ `installed: ${installed}`,
3530
+ `available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
3531
+ resolved.distTags.latest ? `stable latest: ${resolved.distTags.latest}` : "",
3532
+ "run `pj migrate bmad.version` to upgrade"
3533
+ ].filter(Boolean),
3534
+ fixable: true
3535
+ };
3536
+ },
3537
+ migrate: (ctx, finding) => {
3538
+ if (finding.status !== "warn") {
3539
+ return {
3540
+ id: finding.id,
3541
+ title: finding.title,
3542
+ status: "noop",
3543
+ summary: finding.status === "skip" ? finding.summary : "BMAD already current",
3544
+ changedFiles: [],
3545
+ details: []
3546
+ };
3547
+ }
3548
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3549
+ const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3550
+ const manifestPath = join11(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3551
+ if (ctx.dryRun) {
3552
+ return {
3553
+ id: finding.id,
3554
+ title: finding.title,
3555
+ status: "applied",
3556
+ summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
3557
+ changedFiles: [manifestPath],
3558
+ details: [
3559
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3560
+ ]
3561
+ };
3562
+ }
3563
+ const install = runBmadInstall(ctx.repoRoot);
3564
+ if (!install.ok) {
3565
+ return {
3566
+ id: finding.id,
3567
+ title: finding.title,
3568
+ status: "blocked",
3569
+ summary: "Failed to upgrade BMAD via installer",
3570
+ changedFiles: [],
3571
+ details: [install.error ?? "Unknown error"]
3572
+ };
3573
+ }
3574
+ const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
3575
+ const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
3576
+ return {
3577
+ id: finding.id,
3578
+ title: finding.title,
3579
+ status: upgraded ? "applied" : "noop",
3580
+ summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
3581
+ changedFiles: upgraded ? [manifestPath] : [],
3582
+ details: []
3583
+ };
3584
+ }
3585
+ },
3189
3586
  {
3190
3587
  id: "hermes.pm-scaffold",
3191
3588
  title: "Hermes PM scaffold parity",
@@ -3197,11 +3594,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3197
3594
  }
3198
3595
  const details = [];
3199
3596
  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"]) {
3200
- if (!existsSync8(join10(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join10(role.roleDir, rel))}`);
3597
+ if (!existsSync9(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
3201
3598
  }
3202
- const gitmodules = safeReadText(join10(ctx.repoRoot, ".gitmodules")) ?? "";
3599
+ const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
3203
3600
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
3204
- if (!profileMetaInheritsDefault(join10(role.roleDir, "runtime", "profile.yaml"))) {
3601
+ if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
3205
3602
  details.push("runtime/profile.yaml missing inherited default config metadata");
3206
3603
  }
3207
3604
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -3222,21 +3619,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3222
3619
  if (!role) {
3223
3620
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
3224
3621
  }
3225
- const templateRoleDir = join10(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3226
- writeIfDifferent(join10(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3227
- writeIfDifferent(join10(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3228
- writeIfDifferent(join10(role.roleDir, ".gitignore"), readText(join10(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3229
- copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3230
- copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3231
- copyMissingRecursive(join10(templateRoleDir, ".scripts"), join10(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3232
- const promptSource = join10(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3233
- const promptTarget = join10(role.roleDir, ".scripts", "sentinel.prompt.md");
3234
- if (existsSync8(promptSource) && !existsSync8(promptTarget)) {
3622
+ const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3623
+ writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3624
+ writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3625
+ writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3626
+ copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3627
+ copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3628
+ copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3629
+ const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3630
+ const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
3631
+ if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
3235
3632
  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);
3236
3633
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
3237
3634
  }
3238
3635
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
3239
- const profileMetaUpdated = upsertInheritedProfileMeta(join10(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
3636
+ const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
3240
3637
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
3241
3638
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
3242
3639
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -3250,6 +3647,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3250
3647
  };
3251
3648
  }
3252
3649
  },
3650
+ {
3651
+ id: "hermes.untracked-runtimes",
3652
+ title: "Hermes agent runtimes untracked + gitignored",
3653
+ audit: (ctx) => {
3654
+ const roles = discoverRoles(ctx.repoRoot);
3655
+ if (roles.length === 0) {
3656
+ return {
3657
+ id: "hermes.untracked-runtimes",
3658
+ title: "Hermes agent runtimes untracked + gitignored",
3659
+ status: "skip",
3660
+ summary: "No Hermes roles present",
3661
+ details: [],
3662
+ fixable: false
3663
+ };
3664
+ }
3665
+ const details = [];
3666
+ for (const role of roles) {
3667
+ const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3668
+ const runtimeRelPath = join11(roleRelDir, "runtime");
3669
+ const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3670
+ cwd: ctx.repoRoot,
3671
+ encoding: "utf8"
3672
+ });
3673
+ if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3674
+ details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
3675
+ }
3676
+ const gitignorePath = join11(role.roleDir, ".gitignore");
3677
+ if (existsSync9(gitignorePath)) {
3678
+ const content = safeReadText(gitignorePath) ?? "";
3679
+ const lines = content.split(/\r?\n/).map((line) => line.trim());
3680
+ if (!lines.includes("runtime/") && !lines.includes("runtime")) {
3681
+ details.push(`.gitignore missing runtime/ ignore entry in ${relative(ctx.repoRoot, gitignorePath)}`);
3682
+ }
3683
+ } else {
3684
+ details.push(`.gitignore is missing in ${relative(ctx.repoRoot, gitignorePath)}`);
3685
+ }
3686
+ }
3687
+ return {
3688
+ id: "hermes.untracked-runtimes",
3689
+ title: "Hermes agent runtimes untracked + gitignored",
3690
+ status: details.length === 0 ? "pass" : "fail",
3691
+ summary: details.length === 0 ? "All Hermes agent runtimes are untracked and gitignored" : `${details.length} issue(s) with untracked/ignored runtimes detected`,
3692
+ details,
3693
+ fixable: true
3694
+ };
3695
+ },
3696
+ migrate: (ctx, finding) => {
3697
+ const roles = discoverRoles(ctx.repoRoot);
3698
+ const changedFiles = [];
3699
+ const details = [];
3700
+ for (const role of roles) {
3701
+ const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3702
+ const runtimeRelPath = join11(roleRelDir, "runtime");
3703
+ const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3704
+ cwd: ctx.repoRoot,
3705
+ encoding: "utf8"
3706
+ });
3707
+ if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3708
+ details.push(`untrack ${runtimeRelPath}`);
3709
+ changedFiles.push(runtimeRelPath);
3710
+ if (!ctx.dryRun) {
3711
+ spawnSync6("git", ["rm", "--cached", "-r", runtimeRelPath], {
3712
+ cwd: ctx.repoRoot,
3713
+ encoding: "utf8"
3714
+ });
3715
+ }
3716
+ }
3717
+ const gitignorePath = join11(role.roleDir, ".gitignore");
3718
+ let content = "";
3719
+ let isIgnored = false;
3720
+ if (existsSync9(gitignorePath)) {
3721
+ content = safeReadText(gitignorePath) ?? "";
3722
+ const lines = content.split(/\r?\n/).map((line) => line.trim());
3723
+ isIgnored = lines.includes("runtime/") || lines.includes("runtime");
3724
+ }
3725
+ if (!isIgnored) {
3726
+ details.push(`ignore runtime/ in ${relative(ctx.repoRoot, gitignorePath)}`);
3727
+ changedFiles.push(gitignorePath);
3728
+ if (!ctx.dryRun) {
3729
+ if (content && !content.endsWith("\n")) {
3730
+ content += "\n";
3731
+ }
3732
+ content += "runtime/\n";
3733
+ writeText(gitignorePath, content);
3734
+ }
3735
+ }
3736
+ }
3737
+ return {
3738
+ id: finding.id,
3739
+ title: finding.title,
3740
+ status: changedFiles.length ? "applied" : "noop",
3741
+ summary: changedFiles.length ? "Hermes agent runtimes made untracked and ignored" : "No changes required",
3742
+ changedFiles,
3743
+ details
3744
+ };
3745
+ }
3746
+ },
3253
3747
  {
3254
3748
  id: "systemd.sentinel",
3255
3749
  title: "Hermes systemd/sentinel units enabled + active",
@@ -3290,9 +3784,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3290
3784
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
3291
3785
  }
3292
3786
  for (const role of roles) {
3293
- const sysDir = join10(ctx.homeDir, ".config", "systemd", "user");
3787
+ const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
3294
3788
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
3295
- const allUnitsPresent = units.every((unit) => existsSync8(join10(sysDir, unit)));
3789
+ const allUnitsPresent = units.every((unit) => existsSync9(join11(sysDir, unit)));
3296
3790
  if (allUnitsPresent) {
3297
3791
  if (ctx.dryRun) {
3298
3792
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -3304,12 +3798,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3304
3798
  }
3305
3799
  continue;
3306
3800
  }
3307
- for (const script of [join10(role.roleDir, ".scripts", "70-systemd.sh")]) {
3308
- if (!script || !existsSync8(script)) continue;
3801
+ for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
3802
+ if (!script || !existsSync9(script)) continue;
3309
3803
  if (ctx.dryRun) {
3310
3804
  details.push(`would run: bash ${script}`);
3311
3805
  } else {
3312
- const result = spawnSync5("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
3806
+ const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
3313
3807
  if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
3314
3808
  }
3315
3809
  }
@@ -3444,15 +3938,15 @@ function formatMigrationReport(report) {
3444
3938
  }
3445
3939
 
3446
3940
  // src/utils/version.ts
3447
- import { readFileSync as readFileSync5 } from "node:fs";
3448
- import { dirname as dirname7, join as join11 } from "node:path";
3941
+ import { readFileSync as readFileSync6 } from "node:fs";
3942
+ import { dirname as dirname7, join as join12 } from "node:path";
3449
3943
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3450
3944
  var PJANGLER_VERSION = (() => {
3451
3945
  try {
3452
3946
  let dir = dirname7(fileURLToPath4(import.meta.url));
3453
3947
  for (let i = 0; i < 4; i++) {
3454
3948
  try {
3455
- const raw = readFileSync5(join11(dir, "package.json"), "utf8");
3949
+ const raw = readFileSync6(join12(dir, "package.json"), "utf8");
3456
3950
  return JSON.parse(raw).version ?? "0.0.0";
3457
3951
  } catch {
3458
3952
  const parent = dirname7(dir);
@@ -3495,16 +3989,16 @@ async function promptForRuleIds(rules) {
3495
3989
  return selected;
3496
3990
  }
3497
3991
  function readJson(path) {
3498
- if (!existsSync9(path)) return void 0;
3992
+ if (!existsSync10(path)) return void 0;
3499
3993
  try {
3500
- const parsed = JSON.parse(readFileSync6(path, "utf8"));
3994
+ const parsed = JSON.parse(readFileSync7(path, "utf8"));
3501
3995
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
3502
3996
  } catch {
3503
3997
  return void 0;
3504
3998
  }
3505
3999
  }
3506
4000
  function findGitRoot(cwd) {
3507
- const result = spawnSync6("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
4001
+ const result = spawnSync7("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
3508
4002
  if (result.status !== 0) return void 0;
3509
4003
  return resolve3(result.stdout.trim());
3510
4004
  }
@@ -3514,8 +4008,8 @@ function packageNameToProjectName(value) {
3514
4008
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
3515
4009
  }
3516
4010
  function deriveProjectDefaults(targetDir) {
3517
- const manifest = readJson(join12(targetDir, ".project.json"));
3518
- const pkg = readJson(join12(targetDir, "package.json"));
4011
+ const manifest = readJson(join13(targetDir, ".project.json"));
4012
+ const pkg = readJson(join13(targetDir, "package.json"));
3519
4013
  const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
3520
4014
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
3521
4015
  return {
@@ -3571,7 +4065,7 @@ function actionNeedsRun(plan, kind, syncMode) {
3571
4065
  if (!action || action.kind !== "project.write-manifest") return false;
3572
4066
  const next = `${JSON.stringify(action.manifest, null, 2)}
3573
4067
  `;
3574
- return !existsSync9(action.path) || readFileSync6(action.path, "utf8") !== next;
4068
+ return !existsSync10(action.path) || readFileSync7(action.path, "utf8") !== next;
3575
4069
  }
3576
4070
  if (kind === "copier.copy.commonproject") return true;
3577
4071
  if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
@@ -3626,7 +4120,7 @@ async function resolveProjectInitTarget(name, options) {
3626
4120
  if (!targetDir && interactive) {
3627
4121
  const defaultName = name ?? basename4(cwd);
3628
4122
  const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
3629
- const defaultDir = join12(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
4123
+ const defaultDir = join13(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
3630
4124
  targetDir = await promptTextValue("Project directory", defaultDir);
3631
4125
  name = promptedName;
3632
4126
  }
@@ -3634,7 +4128,7 @@ async function resolveProjectInitTarget(name, options) {
3634
4128
  if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
3635
4129
  targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
3636
4130
  }
3637
- const targetExists = existsSync9(targetDir);
4131
+ const targetExists = existsSync10(targetDir);
3638
4132
  if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
3639
4133
  const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
3640
4134
  const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));