@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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/mcp-server.ts
4
- import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
5
- import { basename as basename4, dirname as dirname8, join as join12, resolve as resolve3 } from "node:path";
4
+ import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
5
+ import { basename as basename4, dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -902,10 +902,100 @@ var RunCopierTemplate = class extends Command {
902
902
  }
903
903
  };
904
904
 
905
- // src/commands/hermes/WireTelegram.ts
905
+ // src/commands/hermes/UntrackHermesRuntimes.ts
906
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
907
+ import { join as join6 } from "path";
906
908
  import { spawnSync as spawnSync2 } from "node:child_process";
907
- import { join as join6 } from "node:path";
908
- import { existsSync as existsSync4, unlinkSync } from "node:fs";
909
+ var UntrackHermesRuntimes = class extends Command {
910
+ async invoke() {
911
+ const targetDir = this.context.targetDir;
912
+ const rolesDir = join6(targetDir, "agents", "hermes");
913
+ if (!existsSync4(rolesDir)) {
914
+ return {
915
+ success: true,
916
+ message: "No Hermes agents found (no agents/hermes directory)."
917
+ };
918
+ }
919
+ const roles = readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
920
+ if (roles.length === 0) {
921
+ return {
922
+ success: true,
923
+ message: "No Hermes agents found."
924
+ };
925
+ }
926
+ let modifiedAny = false;
927
+ const details = [];
928
+ for (const role of roles) {
929
+ const roleDir = join6("agents", "hermes", role);
930
+ const runtimePath = join6(roleDir, "runtime");
931
+ const gitignorePath = join6(roleDir, ".gitignore");
932
+ let isTracked = false;
933
+ const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
934
+ cwd: targetDir,
935
+ encoding: "utf8"
936
+ });
937
+ if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
938
+ isTracked = true;
939
+ }
940
+ let isIgnored = false;
941
+ const fullGitignorePath = join6(targetDir, gitignorePath);
942
+ if (existsSync4(fullGitignorePath)) {
943
+ const content = readFileSync2(fullGitignorePath, "utf8");
944
+ const lines = content.split(/\r?\n/).map((line) => line.trim());
945
+ isIgnored = lines.includes("runtime/") || lines.includes("runtime");
946
+ }
947
+ if (isTracked || !isIgnored) {
948
+ modifiedAny = true;
949
+ if (isTracked) {
950
+ details.push(`untrack agents/hermes/${role}/runtime`);
951
+ if (!this.context.dryRun) {
952
+ const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
953
+ cwd: targetDir,
954
+ encoding: "utf8"
955
+ });
956
+ if (rmResult.status !== 0) {
957
+ return {
958
+ success: false,
959
+ message: `Failed to untrack agents/hermes/${role}/runtime: ${rmResult.stderr}`
960
+ };
961
+ }
962
+ }
963
+ }
964
+ if (!isIgnored) {
965
+ details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
966
+ if (!this.context.dryRun) {
967
+ let content = "";
968
+ if (existsSync4(fullGitignorePath)) {
969
+ content = readFileSync2(fullGitignorePath, "utf8");
970
+ }
971
+ if (content && !content.endsWith("\n")) {
972
+ content += "\n";
973
+ }
974
+ content += "runtime/\n";
975
+ writeFileSync3(fullGitignorePath, content, "utf8");
976
+ }
977
+ }
978
+ }
979
+ }
980
+ if (!modifiedAny) {
981
+ return {
982
+ success: true,
983
+ message: "\u2705 All Hermes agent runtimes are already untracked and gitignored."
984
+ };
985
+ }
986
+ const actionText = this.context.dryRun ? "Would make" : "Made";
987
+ return {
988
+ success: true,
989
+ message: `${actionText} Hermes agent runtimes untracked and gitignored:
990
+ ${details.map((d) => ` - ${d}`).join("\n")}`
991
+ };
992
+ }
993
+ };
994
+
995
+ // src/commands/hermes/WireTelegram.ts
996
+ import { spawnSync as spawnSync3 } from "node:child_process";
997
+ import { join as join7 } from "node:path";
998
+ import { existsSync as existsSync5, unlinkSync } from "node:fs";
909
999
  import * as p3 from "@clack/prompts";
910
1000
  var WireTelegram = class extends Command {
911
1001
  async invoke() {
@@ -927,7 +1017,7 @@ var WireTelegram = class extends Command {
927
1017
  let token = process.env.TELEGRAM_BOT_TOKEN;
928
1018
  let source = token ? "env" : null;
929
1019
  if (!token) {
930
- const tryOp = spawnSync2("op", ["read", vaultRef], { encoding: "utf8" });
1020
+ const tryOp = spawnSync3("op", ["read", vaultRef], { encoding: "utf8" });
931
1021
  if (tryOp.status === 0) {
932
1022
  token = tryOp.stdout.trim();
933
1023
  source = "op";
@@ -966,7 +1056,7 @@ var WireTelegram = class extends Command {
966
1056
  initialValue: true
967
1057
  });
968
1058
  if (!p3.isCancel(persist) && persist) {
969
- const create = spawnSync2(
1059
+ const create = spawnSync3(
970
1060
  "op",
971
1061
  [
972
1062
  "item",
@@ -993,18 +1083,18 @@ var WireTelegram = class extends Command {
993
1083
  if (p3.isCancel(allowedAnswer)) {
994
1084
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
995
1085
  }
996
- const script = join6(roleDir, ".scripts", "30-telegram.sh");
997
- if (!existsSync4(script)) {
1086
+ const script = join7(roleDir, ".scripts", "30-telegram.sh");
1087
+ if (!existsSync5(script)) {
998
1088
  return {
999
1089
  success: false,
1000
1090
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
1001
1091
  };
1002
1092
  }
1003
- const marker = join6(roleDir, ".scripts", ".done-30-telegram");
1004
- if (existsSync4(marker)) unlinkSync(marker);
1093
+ const marker = join7(roleDir, ".scripts", ".done-30-telegram");
1094
+ if (existsSync5(marker)) unlinkSync(marker);
1005
1095
  const spinner4 = p3.spinner();
1006
1096
  spinner4.start("Verifying token + wiring profile");
1007
- const result = spawnSync2("bash", [script], {
1097
+ const result = spawnSync3("bash", [script], {
1008
1098
  stdio: "inherit",
1009
1099
  env: {
1010
1100
  ...process.env,
@@ -1027,9 +1117,9 @@ function cap(s) {
1027
1117
  }
1028
1118
 
1029
1119
  // src/commands/hermes/WireEmail.ts
1030
- import { spawnSync as spawnSync3 } from "node:child_process";
1031
- import { join as join7 } from "node:path";
1032
- import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
1120
+ import { spawnSync as spawnSync4 } from "node:child_process";
1121
+ import { join as join8 } from "node:path";
1122
+ import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
1033
1123
  import * as p4 from "@clack/prompts";
1034
1124
  var WireEmail = class extends Command {
1035
1125
  async invoke() {
@@ -1044,13 +1134,13 @@ var WireEmail = class extends Command {
1044
1134
  if (!targetRepo || !role || !roleDir) {
1045
1135
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
1046
1136
  }
1047
- const script = join7(roleDir, ".scripts", "50-email.sh");
1048
- if (!existsSync5(script)) {
1137
+ const script = join8(roleDir, ".scripts", "50-email.sh");
1138
+ if (!existsSync6(script)) {
1049
1139
  return { success: false, message: `\u2717 ${script} not found` };
1050
1140
  }
1051
1141
  let token = process.env.CF_EMAIL_ROUTING_TOKEN;
1052
1142
  if (!token) {
1053
- const tryOp = spawnSync3(
1143
+ const tryOp = spawnSync4(
1054
1144
  "op",
1055
1145
  ["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
1056
1146
  { encoding: "utf8" }
@@ -1090,7 +1180,7 @@ var WireEmail = class extends Command {
1090
1180
  initialValue: true
1091
1181
  });
1092
1182
  if (!p4.isCancel(persist) && persist) {
1093
- const create = spawnSync3(
1183
+ const create = spawnSync4(
1094
1184
  "op",
1095
1185
  [
1096
1186
  "item",
@@ -1107,11 +1197,11 @@ var WireEmail = class extends Command {
1107
1197
  }
1108
1198
  }
1109
1199
  }
1110
- const marker = join7(roleDir, ".scripts", ".done-50-email");
1111
- if (existsSync5(marker)) unlinkSync2(marker);
1200
+ const marker = join8(roleDir, ".scripts", ".done-50-email");
1201
+ if (existsSync6(marker)) unlinkSync2(marker);
1112
1202
  const spinner4 = p4.spinner();
1113
1203
  spinner4.start("Creating Cloudflare Email Routing rule");
1114
- const result = spawnSync3("bash", [script], {
1204
+ const result = spawnSync4("bash", [script], {
1115
1205
  stdio: "inherit",
1116
1206
  env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
1117
1207
  cwd: roleDir
@@ -1171,7 +1261,7 @@ var PrintHermesSummary = class extends Command {
1171
1261
  var HermesAgentRecipe = class extends Recipe {
1172
1262
  constructor(context) {
1173
1263
  super(context);
1174
- this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
1264
+ this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(UntrackHermesRuntimes).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
1175
1265
  }
1176
1266
  // Override execute() to suppress the base class's per-command logging since
1177
1267
  // our commands already render their own UI via @clack/prompts.
@@ -1195,33 +1285,33 @@ var HermesAgentRecipe = class extends Recipe {
1195
1285
 
1196
1286
  // src/commands/AgentHooksCommands.ts
1197
1287
  import { homedir as homedir4 } from "node:os";
1198
- import { join as join9, dirname as dirname5 } from "node:path";
1199
- import { existsSync as existsSync7, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
1288
+ import { join as join10, dirname as dirname5 } from "node:path";
1289
+ import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
1200
1290
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1201
1291
 
1202
1292
  // src/project/index.ts
1203
- import { spawnSync as spawnSync4 } from "node:child_process";
1204
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
1293
+ import { spawnSync as spawnSync5 } from "node:child_process";
1294
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1205
1295
  import { homedir as homedir3 } from "node:os";
1206
- import { basename as basename2, delimiter, dirname as dirname4, join as join8, resolve } from "node:path";
1296
+ import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
1207
1297
  import YAML from "yaml";
1208
1298
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
1209
1299
  var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1210
1300
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1211
1301
  var DEFAULT_SOURCE_SKILL_ROOTS = [
1212
1302
  "/home/delorenj/code/skillex/all-skills",
1213
- join8(homedir3(), ".agents", "skills"),
1214
- join8(homedir3(), ".codex", "skills")
1303
+ join9(homedir3(), ".agents", "skills"),
1304
+ join9(homedir3(), ".codex", "skills")
1215
1305
  ];
1216
1306
  function projectRegistryPath(env2 = process.env) {
1217
- return expandHome(env2[PROJECT_REGISTRY_ENV] || join8(homedir3(), ".config", "pjangler", "projects.yaml"));
1307
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
1218
1308
  }
1219
1309
  function emptyProjectRegistry() {
1220
1310
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
1221
1311
  }
1222
1312
  function loadProjectRegistry(path = projectRegistryPath()) {
1223
- if (!existsSync6(path)) return emptyProjectRegistry();
1224
- const raw = YAML.parse(readFileSync2(path, "utf8"));
1313
+ if (!existsSync7(path)) return emptyProjectRegistry();
1314
+ const raw = YAML.parse(readFileSync3(path, "utf8"));
1225
1315
  if (raw == null) return emptyProjectRegistry();
1226
1316
  if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1227
1317
  const registry = raw;
@@ -1236,7 +1326,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1236
1326
  validateProjectRegistry(registry);
1237
1327
  mkdirSync4(dirname4(path), { recursive: true });
1238
1328
  const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1239
- writeFileSync3(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
1329
+ writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
1240
1330
  renameSync(temp, path);
1241
1331
  }
1242
1332
  function validateProjectRegistry(registry) {
@@ -1318,7 +1408,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
1318
1408
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1319
1409
  if (override === "0" || override === "false") return false;
1320
1410
  if (override === "1" || override === "true") return true;
1321
- return !existsSync6(join8(homedir3(), ".agents", "hooks"));
1411
+ return !existsSync7(join9(homedir3(), ".agents", "hooks"));
1322
1412
  }
1323
1413
  function jsonStable(value) {
1324
1414
  return JSON.stringify(value);
@@ -1349,12 +1439,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1349
1439
  if (!sourceSkill) return void 0;
1350
1440
  const expanded = expandHome(sourceSkill);
1351
1441
  const direct = resolve(expanded);
1352
- if (existsSync6(direct)) return direct;
1442
+ if (existsSync7(direct)) return direct;
1353
1443
  const name = basename2(sourceSkill);
1354
1444
  const roots = sourceSkillRoots(env2);
1355
1445
  for (const root of roots) {
1356
- const candidate = join8(root, name);
1357
- if (existsSync6(candidate)) return candidate;
1446
+ const candidate = join9(root, name);
1447
+ if (existsSync7(candidate)) return candidate;
1358
1448
  }
1359
1449
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1360
1450
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
@@ -1435,7 +1525,7 @@ function planProjectInit(input) {
1435
1525
  }));
1436
1526
  }
1437
1527
  actions.push(
1438
- { kind: "project.write-manifest", path: join8(targetDir, ".project.json"), manifest },
1528
+ { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
1439
1529
  {
1440
1530
  kind: "ticket-provider.create-or-link",
1441
1531
  enabled: live,
@@ -1476,7 +1566,7 @@ function executeProjectInitPlan(plan) {
1476
1566
  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"
1477
1567
  );
1478
1568
  mkdirSync4(dirname4(action.targetDir), { recursive: true });
1479
- const result = spawnSync4(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
1569
+ const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
1480
1570
  if (result.stdout?.trim()) logs.push(result.stdout.trim());
1481
1571
  if (result.stderr?.trim()) logs.push(result.stderr.trim());
1482
1572
  if (result.error) {
@@ -1488,7 +1578,7 @@ function executeProjectInitPlan(plan) {
1488
1578
  }
1489
1579
  if (result.status !== 0) {
1490
1580
  errors.push(`copier exited with status ${result.status ?? "unknown"}`);
1491
- if (existsSync6(action.targetDir)) changedFiles.push(action.targetDir);
1581
+ if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
1492
1582
  break;
1493
1583
  }
1494
1584
  changedFiles.push(action.targetDir);
@@ -1496,9 +1586,9 @@ function executeProjectInitPlan(plan) {
1496
1586
  mkdirSync4(dirname4(action.path), { recursive: true });
1497
1587
  const next = `${JSON.stringify(action.manifest, null, 2)}
1498
1588
  `;
1499
- const current = existsSync6(action.path) ? readFileSync2(action.path, "utf8") : void 0;
1589
+ const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
1500
1590
  if (current !== next) {
1501
- writeFileSync3(action.path, next, "utf8");
1591
+ writeFileSync4(action.path, next, "utf8");
1502
1592
  changedFiles.push(action.path);
1503
1593
  }
1504
1594
  } else if (action.kind === "registry.upsert") {
@@ -1551,7 +1641,7 @@ function getProject(registry, slug) {
1551
1641
  return project;
1552
1642
  }
1553
1643
  function buildCommonProjectCopierAction(input) {
1554
- const templateDir = join8(input.pjanglerRoot, "templates", "commonproject");
1644
+ const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
1555
1645
  const data = {
1556
1646
  project_name: input.projectName,
1557
1647
  project_description: input.projectDescription ?? "",
@@ -1580,7 +1670,7 @@ function buildCommonProjectCopierAction(input) {
1580
1670
  function resolvePjanglerRoot() {
1581
1671
  let dir = dirname4(new URL(import.meta.url).pathname);
1582
1672
  while (dir !== dirname4(dir)) {
1583
- if (existsSync6(join8(dir, "package.json")) && existsSync6(join8(dir, "templates", "commonproject", "copier.yml"))) return dir;
1673
+ if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
1584
1674
  dir = dirname4(dir);
1585
1675
  }
1586
1676
  return resolve(process.cwd());
@@ -1612,7 +1702,7 @@ function validateProjectRecord(project, key) {
1612
1702
  }
1613
1703
  function expandHome(path) {
1614
1704
  if (path === "~") return homedir3();
1615
- if (path.startsWith("~/")) return join8(homedir3(), path.slice(2));
1705
+ if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
1616
1706
  return path;
1617
1707
  }
1618
1708
  function isRecord(value) {
@@ -1629,16 +1719,16 @@ function resolveTemplateRoot() {
1629
1719
  try {
1630
1720
  let dir = dirname5(fileURLToPath2(import.meta.url));
1631
1721
  for (let i = 0; i < 8; i++) {
1632
- candidates.push(join9(dir, "templates", "commonproject", "template"));
1722
+ candidates.push(join10(dir, "templates", "commonproject", "template"));
1633
1723
  const parent = dirname5(dir);
1634
1724
  if (parent === dir) break;
1635
1725
  dir = parent;
1636
1726
  }
1637
1727
  } catch {
1638
1728
  }
1639
- candidates.push(join9(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1729
+ candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1640
1730
  for (const c of candidates) {
1641
- if (existsSync7(join9(c, ".agents", "hooks", "hooks.master.json"))) return c;
1731
+ if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
1642
1732
  }
1643
1733
  throw new Error(
1644
1734
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1665,10 +1755,10 @@ var CopyAgentHooksTree = class extends Command {
1665
1755
  const created = [];
1666
1756
  const skipped = [];
1667
1757
  for (const { rel, dir } of items) {
1668
- const src = join9(templateRoot, rel);
1669
- const dest = join9(this.context.targetDir, rel);
1670
- if (!existsSync7(src)) continue;
1671
- if (existsSync7(dest) && !this.context.force) {
1758
+ const src = join10(templateRoot, rel);
1759
+ const dest = join10(this.context.targetDir, rel);
1760
+ if (!existsSync8(src)) continue;
1761
+ if (existsSync8(dest) && !this.context.force) {
1672
1762
  skipped.push(rel);
1673
1763
  continue;
1674
1764
  }
@@ -1694,14 +1784,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1694
1784
  if (!resolveAgentHooksLayer()) {
1695
1785
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
1696
1786
  }
1697
- const misePath = join9(this.context.targetDir, "mise.toml");
1698
- if (!existsSync7(misePath)) {
1787
+ const misePath = join10(this.context.targetDir, "mise.toml");
1788
+ if (!existsSync8(misePath)) {
1699
1789
  return {
1700
1790
  success: false,
1701
1791
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
1702
1792
  };
1703
1793
  }
1704
- let content = readFileSync3(misePath, "utf8");
1794
+ let content = readFileSync4(misePath, "utf8");
1705
1795
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
1706
1796
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
1707
1797
  }
@@ -1777,7 +1867,7 @@ ${leaveBlock}`);
1777
1867
  ""
1778
1868
  ].join("\n");
1779
1869
  content = content.replace(/\n*$/, "\n") + appended;
1780
- if (!this.context.dryRun) writeFileSync4(misePath, content);
1870
+ if (!this.context.dryRun) writeFileSync5(misePath, content);
1781
1871
  if (wiredHooks) {
1782
1872
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
1783
1873
  }
@@ -1931,15 +2021,15 @@ function createRecipe(name, context) {
1931
2021
  }
1932
2022
 
1933
2023
  // src/utils/version.ts
1934
- import { readFileSync as readFileSync4 } from "node:fs";
1935
- import { dirname as dirname6, join as join10 } from "node:path";
2024
+ import { readFileSync as readFileSync5 } from "node:fs";
2025
+ import { dirname as dirname6, join as join11 } from "node:path";
1936
2026
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1937
2027
  var PJANGLER_VERSION = (() => {
1938
2028
  try {
1939
2029
  let dir = dirname6(fileURLToPath3(import.meta.url));
1940
2030
  for (let i = 0; i < 4; i++) {
1941
2031
  try {
1942
- const raw = readFileSync4(join10(dir, "package.json"), "utf8");
2032
+ const raw = readFileSync5(join11(dir, "package.json"), "utf8");
1943
2033
  return JSON.parse(raw).version ?? "0.0.0";
1944
2034
  } catch {
1945
2035
  const parent = dirname6(dir);
@@ -1953,11 +2043,12 @@ var PJANGLER_VERSION = (() => {
1953
2043
  })();
1954
2044
 
1955
2045
  // src/parity/index.ts
1956
- import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readlinkSync, readdirSync, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync as chmodSync2, copyFileSync } from "node:fs";
1957
- import { basename as basename3, dirname as dirname7, join as join11, relative, resolve as resolve2 } from "node:path";
2046
+ import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
2047
+ import { basename as basename3, dirname as dirname7, join as join12, relative, resolve as resolve2 } from "node:path";
1958
2048
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1959
2049
  import { homedir as homedir5 } from "node:os";
1960
- import { spawnSync as spawnSync5 } from "node:child_process";
2050
+ import { spawnSync as spawnSync6 } from "node:child_process";
2051
+ import YAML2 from "yaml";
1961
2052
  var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
1962
2053
  # agent files to the main AGENTS.md file.
1963
2054
  #
@@ -2028,7 +2119,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
2028
2119
  function resolvePjanglerRoot2() {
2029
2120
  let dir = dirname7(fileURLToPath4(import.meta.url));
2030
2121
  while (dir !== dirname7(dir)) {
2031
- if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) {
2122
+ if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
2032
2123
  return dir;
2033
2124
  }
2034
2125
  dir = dirname7(dir);
@@ -2039,17 +2130,17 @@ function normalizeNewlines(value) {
2039
2130
  return value.replace(/\r\n/g, "\n");
2040
2131
  }
2041
2132
  function readText(path) {
2042
- return normalizeNewlines(readFileSync5(path, "utf8"));
2133
+ return normalizeNewlines(readFileSync6(path, "utf8"));
2043
2134
  }
2044
2135
  function safeReadText(path) {
2045
- return existsSync8(path) ? readText(path) : null;
2136
+ return existsSync9(path) ? readText(path) : null;
2046
2137
  }
2047
2138
  function ensureParent(path) {
2048
2139
  mkdirSync6(dirname7(path), { recursive: true });
2049
2140
  }
2050
2141
  function writeText(path, content) {
2051
2142
  ensureParent(path);
2052
- writeFileSync5(path, content);
2143
+ writeFileSync6(path, content);
2053
2144
  }
2054
2145
  function tryParseJson(text2) {
2055
2146
  if (!text2) return null;
@@ -2066,7 +2157,7 @@ function titleCaseSlug(slug) {
2066
2157
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2067
2158
  }
2068
2159
  function readSymlinkTarget(path) {
2069
- if (!existsSync8(path)) return null;
2160
+ if (!existsSync9(path)) return null;
2070
2161
  try {
2071
2162
  return readlinkSync(path);
2072
2163
  } catch {
@@ -2074,7 +2165,7 @@ function readSymlinkTarget(path) {
2074
2165
  }
2075
2166
  }
2076
2167
  function ensureSymlink(path, target, dryRun) {
2077
- if (existsSync8(path)) {
2168
+ if (existsSync9(path)) {
2078
2169
  const stat = lstatSync(path);
2079
2170
  if (stat.isSymbolicLink()) {
2080
2171
  const current = readSymlinkTarget(path);
@@ -2091,11 +2182,11 @@ function ensureSymlink(path, target, dryRun) {
2091
2182
  return { changed: true };
2092
2183
  }
2093
2184
  function bootstrapAgentsFile(repoRoot, dryRun) {
2094
- const agentsPath = join11(repoRoot, "AGENTS.md");
2095
- if (existsSync8(agentsPath)) return { changedFiles: [], details: [] };
2185
+ const agentsPath = join12(repoRoot, "AGENTS.md");
2186
+ if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
2096
2187
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2097
- const source = join11(repoRoot, file);
2098
- if (!existsSync8(source)) continue;
2188
+ const source = join12(repoRoot, file);
2189
+ if (!existsSync9(source)) continue;
2099
2190
  const stat = lstatSync(source);
2100
2191
  if (stat.isSymbolicLink()) continue;
2101
2192
  if (stat.isFile()) {
@@ -2104,8 +2195,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
2104
2195
  }
2105
2196
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
2106
2197
  }
2107
- const readmePath = join11(repoRoot, "README.md");
2108
- if (existsSync8(readmePath)) {
2198
+ const readmePath = join12(repoRoot, "README.md");
2199
+ if (existsSync9(readmePath)) {
2109
2200
  const stat = lstatSync(readmePath);
2110
2201
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
2111
2202
  if (!dryRun) copyFileSync(readmePath, agentsPath);
@@ -2144,12 +2235,12 @@ function yamlGet(text2, keyPath) {
2144
2235
  return "";
2145
2236
  }
2146
2237
  function discoverRoles(repoRoot) {
2147
- const rolesDir = join11(repoRoot, "agents", "hermes");
2148
- if (!existsSync8(rolesDir)) return [];
2149
- return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2150
- const roleDir = join11(rolesDir, entry.name);
2151
- const roleYamlPath = join11(roleDir, "role.yaml");
2152
- if (!existsSync8(roleYamlPath)) return null;
2238
+ const rolesDir = join12(repoRoot, "agents", "hermes");
2239
+ if (!existsSync9(rolesDir)) return [];
2240
+ return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2241
+ const roleDir = join12(rolesDir, entry.name);
2242
+ const roleYamlPath = join12(roleDir, "role.yaml");
2243
+ if (!existsSync9(roleYamlPath)) return null;
2153
2244
  const text2 = readText(roleYamlPath);
2154
2245
  const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
2155
2246
  return {
@@ -2177,10 +2268,10 @@ function discoverRoles(repoRoot) {
2177
2268
  }).filter((value) => Boolean(value));
2178
2269
  }
2179
2270
  function registryPath(homeDir) {
2180
- return join11(homeDir, ".hermes", "agents-registry.yaml");
2271
+ return join12(homeDir, ".hermes", "agents-registry.yaml");
2181
2272
  }
2182
2273
  function systemctlUser(args) {
2183
- const result = spawnSync5("systemctl", ["--user", ...args], { encoding: "utf8" });
2274
+ const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
2184
2275
  return {
2185
2276
  ok: result.status === 0,
2186
2277
  stdout: result.stdout.trim(),
@@ -2188,8 +2279,8 @@ function systemctlUser(args) {
2188
2279
  };
2189
2280
  }
2190
2281
  function templateScript(ctx, name) {
2191
- const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
2192
- return existsSync8(source) ? readText(source) : void 0;
2282
+ const source = join12(ctx.pjanglerRoot, ".mise", "scripts", name);
2283
+ return existsSync9(source) ? readText(source) : void 0;
2193
2284
  }
2194
2285
  function templateVersioningScript(ctx) {
2195
2286
  return templateScript(ctx, "versioning.sh");
@@ -2201,8 +2292,8 @@ function resolveAgentHooksLayer2(ctx) {
2201
2292
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
2202
2293
  if (override === "0" || override === "false") return false;
2203
2294
  if (override === "1" || override === "true") return true;
2204
- if (existsSync8(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2205
- return !existsSync8(join11(ctx.homeDir, ".agents", "hooks"));
2295
+ if (existsSync9(join12(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2296
+ return !existsSync9(join12(ctx.homeDir, ".agents", "hooks"));
2206
2297
  }
2207
2298
  function evaluateMiseConditionals(template, agentHooksLayer) {
2208
2299
  const out = [];
@@ -2232,10 +2323,10 @@ function renderGeneratedProjectMiseToml(ctx, template) {
2232
2323
  return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2233
2324
  }
2234
2325
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2235
- const targetPath = join11(ctx.repoRoot, "mise.toml");
2236
- if (existsSync8(targetPath)) return false;
2237
- const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2238
- if (!existsSync8(sourcePath)) return false;
2326
+ const targetPath = join12(ctx.repoRoot, "mise.toml");
2327
+ if (existsSync9(targetPath)) return false;
2328
+ const sourcePath = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2329
+ if (!existsSync9(sourcePath)) return false;
2239
2330
  changedFiles.push(targetPath);
2240
2331
  if (!ctx.dryRun) {
2241
2332
  writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
@@ -2243,8 +2334,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2243
2334
  return true;
2244
2335
  }
2245
2336
  function templateVersionFilesConf(ctx, repoRoot) {
2246
- const packageJson = join11(repoRoot, "package.json");
2247
- 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";
2337
+ const packageJson = join12(repoRoot, "package.json");
2338
+ 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";
2248
2339
  }
2249
2340
  function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
2250
2341
  if (startMarker.test(text2)) {
@@ -2268,7 +2359,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
2268
2359
  function requiredMisePathEntries(ctx) {
2269
2360
  const required = [...BASE_MISE_PATH_ENTRIES];
2270
2361
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
2271
- if (existsSync8(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2362
+ if (existsSync9(join12(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2272
2363
  }
2273
2364
  return required;
2274
2365
  }
@@ -2417,7 +2508,7 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
2417
2508
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
2418
2509
  }
2419
2510
  function readProjectJson(ctx) {
2420
- return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
2511
+ return tryParseJson(safeReadText(join12(ctx.repoRoot, ".project.json")));
2421
2512
  }
2422
2513
  function boolSetting(value, fallback) {
2423
2514
  if (typeof value === "boolean") return value;
@@ -2492,12 +2583,12 @@ function canonicalProjectJson(ctx) {
2492
2583
  };
2493
2584
  }
2494
2585
  function projectJsonFinding(ctx) {
2495
- const projectPath = join11(ctx.repoRoot, ".project.json");
2496
- const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
2586
+ const projectPath = join12(ctx.repoRoot, ".project.json");
2587
+ const planeJsonPath = join12(ctx.repoRoot, ".plane.json");
2497
2588
  const details = [];
2498
2589
  const data = readProjectJson(ctx);
2499
2590
  const roles = discoverRoles(ctx.repoRoot);
2500
- if (!existsSync8(projectPath)) {
2591
+ if (!existsSync9(projectPath)) {
2501
2592
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
2502
2593
  }
2503
2594
  if (!data) {
@@ -2532,7 +2623,7 @@ function projectJsonFinding(ctx) {
2532
2623
  for (const key of ["enabled", "grace_hours", "auto_review"]) {
2533
2624
  if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2534
2625
  }
2535
- if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2626
+ if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2536
2627
  return {
2537
2628
  id: "sot.project-json",
2538
2629
  title: "Canonical .project.json",
@@ -2613,17 +2704,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
2613
2704
  `.replace(/\u0010/g, "$");
2614
2705
  }
2615
2706
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2616
- if (!existsSync8(sourceDir)) return;
2707
+ if (!existsSync9(sourceDir)) return;
2617
2708
  mkdirSync6(targetDir, { recursive: true });
2618
- for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2619
- const sourcePath = join11(sourceDir, entry.name);
2709
+ for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
2710
+ const sourcePath = join12(sourceDir, entry.name);
2620
2711
  if (skip?.(sourcePath)) continue;
2621
- const targetPath = join11(targetDir, entry.name);
2712
+ const targetPath = join12(targetDir, entry.name);
2622
2713
  if (entry.isDirectory()) {
2623
2714
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2624
2715
  continue;
2625
2716
  }
2626
- if (existsSync8(targetPath)) continue;
2717
+ if (existsSync9(targetPath)) continue;
2627
2718
  changedFiles.push(targetPath);
2628
2719
  if (!dryRun) {
2629
2720
  ensureParent(targetPath);
@@ -2632,7 +2723,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2632
2723
  }
2633
2724
  }
2634
2725
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2635
- const gitmodulesPath = join11(repoRoot, ".gitmodules");
2726
+ const gitmodulesPath = join12(repoRoot, ".gitmodules");
2636
2727
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2637
2728
  const owner = role.runtimeOwner || "delorenj";
2638
2729
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2727,19 +2818,186 @@ function checkUnit(unit) {
2727
2818
  const active = systemctlUser(["is-active", unit]).ok;
2728
2819
  return { enabled, active };
2729
2820
  }
2821
+ var BMAD_NPM_PACKAGE = "bmad-method";
2822
+ var BMAD_TARGET_CHANNEL = "next";
2823
+ var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
2824
+ var BMAD_INSTALL_TOOLS = [
2825
+ "claude-code",
2826
+ "codex",
2827
+ "cursor",
2828
+ "github-copilot",
2829
+ "adal",
2830
+ "antigravity-cli",
2831
+ "auggie",
2832
+ "goose",
2833
+ "cline",
2834
+ "codebuddy",
2835
+ "codewhale",
2836
+ "command-code",
2837
+ "crush",
2838
+ "droid",
2839
+ "firebender",
2840
+ "gemini",
2841
+ "antigravity",
2842
+ "hermes",
2843
+ "bob",
2844
+ "iflow",
2845
+ "junie",
2846
+ "kilo",
2847
+ "kimi-code",
2848
+ "kiro",
2849
+ "kode",
2850
+ "mistral-vibe",
2851
+ "mux",
2852
+ "neovate",
2853
+ "ona",
2854
+ "openclaw",
2855
+ "opencode",
2856
+ "openhands",
2857
+ "pi",
2858
+ "pochi",
2859
+ "qoder",
2860
+ "qwen",
2861
+ "replit",
2862
+ "roo",
2863
+ "rovo-dev",
2864
+ "cortex",
2865
+ "amp",
2866
+ "trae",
2867
+ "warp",
2868
+ "windsurf",
2869
+ "zencoder"
2870
+ ];
2871
+ function bmadInstallArgs(repoRoot) {
2872
+ return [
2873
+ "-y",
2874
+ `${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
2875
+ "install",
2876
+ "--yes",
2877
+ "--directory",
2878
+ repoRoot,
2879
+ "--modules",
2880
+ "bmm,bmb,cis",
2881
+ "--tools",
2882
+ BMAD_INSTALL_TOOLS.join(",")
2883
+ ];
2884
+ }
2885
+ function runBmadInstall(repoRoot) {
2886
+ const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
2887
+ if (result.status !== 0) {
2888
+ return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
2889
+ }
2890
+ return { ok: true };
2891
+ }
2892
+ function readInstalledBmadVersion(repoRoot) {
2893
+ const raw = safeReadText(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
2894
+ if (!raw) return void 0;
2895
+ try {
2896
+ const parsed = YAML2.parse(raw);
2897
+ const version = parsed?.installation?.version;
2898
+ return typeof version === "string" && version.trim() ? version.trim() : void 0;
2899
+ } catch {
2900
+ return void 0;
2901
+ }
2902
+ }
2903
+ function bmadCachePath(homeDir) {
2904
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join12(homeDir, ".cache");
2905
+ return join12(cacheRoot, "pjangler", "bmad-dist-tags.json");
2906
+ }
2907
+ function readBmadDistTagsCache(homeDir) {
2908
+ const raw = safeReadText(bmadCachePath(homeDir));
2909
+ if (!raw) return void 0;
2910
+ try {
2911
+ const parsed = JSON.parse(raw);
2912
+ if (parsed && typeof parsed.fetchedAt === "number" && parsed.distTags && typeof parsed.distTags === "object") {
2913
+ return parsed;
2914
+ }
2915
+ } catch {
2916
+ }
2917
+ return void 0;
2918
+ }
2919
+ function fetchBmadDistTags() {
2920
+ const result = spawnSync6("npm", ["view", BMAD_NPM_PACKAGE, "dist-tags", "--json"], {
2921
+ encoding: "utf8",
2922
+ timeout: 8e3
2923
+ });
2924
+ if (result.status !== 0 || !result.stdout.trim()) return void 0;
2925
+ try {
2926
+ const parsed = JSON.parse(result.stdout);
2927
+ const obj = Array.isArray(parsed) ? parsed[0] : parsed;
2928
+ if (!obj || typeof obj !== "object") return void 0;
2929
+ const tags = {};
2930
+ for (const [key, value] of Object.entries(obj)) {
2931
+ if (typeof value === "string") tags[key] = value;
2932
+ }
2933
+ return Object.keys(tags).length ? tags : void 0;
2934
+ } catch {
2935
+ return void 0;
2936
+ }
2937
+ }
2938
+ function resolveBmadDistTags(homeDir) {
2939
+ const cached = readBmadDistTagsCache(homeDir);
2940
+ if (cached && Date.now() - cached.fetchedAt < BMAD_DIST_TAGS_TTL_MS) {
2941
+ return { distTags: cached.distTags, stale: false };
2942
+ }
2943
+ const fetched = fetchBmadDistTags();
2944
+ if (fetched) {
2945
+ try {
2946
+ const path = bmadCachePath(homeDir);
2947
+ mkdirSync6(dirname7(path), { recursive: true });
2948
+ writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
2949
+ } catch {
2950
+ }
2951
+ return { distTags: fetched, stale: false };
2952
+ }
2953
+ if (cached) return { distTags: cached.distTags, stale: true };
2954
+ return void 0;
2955
+ }
2956
+ function compareBmadVersions(a, b) {
2957
+ const parse = (v) => {
2958
+ const [core = "0", pre = ""] = v.replace(/^v/, "").split("-", 2);
2959
+ const parts = core.split(".");
2960
+ const n = (i) => parseInt(parts[i] ?? "0", 10) || 0;
2961
+ return { nums: [n(0), n(1), n(2)], pre };
2962
+ };
2963
+ const pa = parse(a);
2964
+ const pb = parse(b);
2965
+ for (let i = 0; i < 3; i++) {
2966
+ if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] - pb.nums[i];
2967
+ }
2968
+ if (pa.pre === pb.pre) return 0;
2969
+ if (!pa.pre) return 1;
2970
+ if (!pb.pre) return -1;
2971
+ const ida = pa.pre.split(".");
2972
+ const idb = pb.pre.split(".");
2973
+ for (let i = 0; i < Math.max(ida.length, idb.length); i++) {
2974
+ const xa = ida[i];
2975
+ const xb = idb[i];
2976
+ if (xa === void 0) return -1;
2977
+ if (xb === void 0) return 1;
2978
+ const na = Number(xa);
2979
+ const nb = Number(xb);
2980
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
2981
+ if (na !== nb) return na - nb;
2982
+ } else if (xa !== xb) {
2983
+ return xa < xb ? -1 : 1;
2984
+ }
2985
+ }
2986
+ return 0;
2987
+ }
2730
2988
  var RULES = [
2731
2989
  {
2732
2990
  id: "mise.config-root",
2733
2991
  title: "mise config_root + AGENTS link hooks",
2734
2992
  audit: (ctx) => {
2735
- const misePath = join11(ctx.repoRoot, "mise.toml");
2736
- if (!existsSync8(misePath)) {
2993
+ const misePath = join12(ctx.repoRoot, "mise.toml");
2994
+ if (!existsSync9(misePath)) {
2737
2995
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2738
2996
  }
2739
2997
  const text2 = readText(misePath);
2740
2998
  const details = [];
2741
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2742
- if (!existsSync8(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2999
+ const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3000
+ if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2743
3001
  const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2744
3002
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2745
3003
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
@@ -2757,10 +3015,10 @@ var RULES = [
2757
3015
  };
2758
3016
  },
2759
3017
  migrate: (ctx, finding) => {
2760
- const path = join11(ctx.repoRoot, "mise.toml");
3018
+ const path = join12(ctx.repoRoot, "mise.toml");
2761
3019
  const changedFiles = [];
2762
3020
  const details = [];
2763
- if (!existsSync8(path)) {
3021
+ if (!existsSync9(path)) {
2764
3022
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2765
3023
  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: [] };
2766
3024
  }
@@ -2776,7 +3034,7 @@ var RULES = [
2776
3034
  if (!ctx.dryRun) writeText(path, next);
2777
3035
  text2 = next;
2778
3036
  }
2779
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3037
+ const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2780
3038
  const expectedScript = templateLinkAgentfilesScript(ctx);
2781
3039
  if (expectedScript === void 0) {
2782
3040
  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: [] };
@@ -2803,13 +3061,13 @@ var RULES = [
2803
3061
  title: "managed mise versioning block",
2804
3062
  audit: (ctx) => {
2805
3063
  const details = [];
2806
- const misePath = join11(ctx.repoRoot, "mise.toml");
2807
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2808
- const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
3064
+ const misePath = join12(ctx.repoRoot, "mise.toml");
3065
+ const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3066
+ const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
2809
3067
  const text2 = safeReadText(misePath);
2810
3068
  if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2811
- if (!existsSync8(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2812
- if (!existsSync8(manifestPath)) details.push(".mise/version-files.conf missing");
3069
+ if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3070
+ if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
2813
3071
  return {
2814
3072
  id: "mise.versioning",
2815
3073
  title: "managed mise versioning block",
@@ -2822,8 +3080,8 @@ var RULES = [
2822
3080
  migrate: (ctx, finding) => {
2823
3081
  const changedFiles = [];
2824
3082
  const details = [];
2825
- const misePath = join11(ctx.repoRoot, "mise.toml");
2826
- if (!existsSync8(misePath)) {
3083
+ const misePath = join12(ctx.repoRoot, "mise.toml");
3084
+ if (!existsSync9(misePath)) {
2827
3085
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2828
3086
  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: [] };
2829
3087
  }
@@ -2833,12 +3091,21 @@ var RULES = [
2833
3091
  }
2834
3092
  }
2835
3093
  const currentMise = readText(misePath);
2836
- const nextMise = replaceOrAppendManagedBlock(currentMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
3094
+ let cleanedMise = currentMise;
3095
+ if (!currentMise.includes("# >>> mise-versioning >>>")) {
3096
+ const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
3097
+ for (const taskName of taskNames) {
3098
+ const escaped = taskName.replace(/:/g, "\\:");
3099
+ const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
3100
+ cleanedMise = removeTomlSection(cleanedMise, headerPattern);
3101
+ }
3102
+ }
3103
+ const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
2837
3104
  if (nextMise !== currentMise) {
2838
3105
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
2839
3106
  if (!ctx.dryRun) writeText(misePath, nextMise);
2840
3107
  }
2841
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3108
+ const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2842
3109
  const expectedScript = templateVersioningScript(ctx);
2843
3110
  if (expectedScript === void 0) {
2844
3111
  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: [] };
@@ -2850,7 +3117,7 @@ var RULES = [
2850
3117
  chmodSync2(versioningPath, 493);
2851
3118
  }
2852
3119
  }
2853
- const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
3120
+ const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
2854
3121
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
2855
3122
  if (safeReadText(manifestPath) !== expectedManifest) {
2856
3123
  changedFiles.push(manifestPath);
@@ -2870,9 +3137,9 @@ var RULES = [
2870
3137
  id: "sot.agent-symlinks",
2871
3138
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
2872
3139
  audit: (ctx) => {
2873
- const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
2874
- if (!existsSync8(agentsPath)) {
2875
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync8(join11(ctx.repoRoot, file)));
3140
+ const agentsPath = join12(ctx.repoRoot, "AGENTS.md");
3141
+ if (!existsSync9(agentsPath)) {
3142
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join12(ctx.repoRoot, file)));
2876
3143
  if (fallbackSources.length === 0) {
2877
3144
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
2878
3145
  }
@@ -2887,7 +3154,7 @@ var RULES = [
2887
3154
  }
2888
3155
  const details = [];
2889
3156
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2890
- const full = join11(ctx.repoRoot, file);
3157
+ const full = join12(ctx.repoRoot, file);
2891
3158
  const target = readSymlinkTarget(full);
2892
3159
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
2893
3160
  }
@@ -2911,7 +3178,7 @@ var RULES = [
2911
3178
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
2912
3179
  }
2913
3180
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2914
- const full = join11(ctx.repoRoot, file);
3181
+ const full = join12(ctx.repoRoot, file);
2915
3182
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
2916
3183
  if (result.blocked) blockedDetails.push(result.blocked);
2917
3184
  if (result.changed) changedFiles.push(full);
@@ -2933,7 +3200,7 @@ var RULES = [
2933
3200
  migrate: (ctx, finding) => {
2934
3201
  const changedFiles = [];
2935
3202
  const details = [];
2936
- const path = join11(ctx.repoRoot, ".project.json");
3203
+ const path = join12(ctx.repoRoot, ".project.json");
2937
3204
  const existing = readProjectJson(ctx) ?? {};
2938
3205
  const canonical = canonicalProjectJson(ctx);
2939
3206
  const merged = { ...existing, ...canonical };
@@ -2943,10 +3210,10 @@ var RULES = [
2943
3210
  changedFiles.push(path);
2944
3211
  if (!ctx.dryRun) writeText(path, expected);
2945
3212
  }
2946
- const planeJson = join11(ctx.repoRoot, ".plane.json");
2947
- if (existsSync8(planeJson)) {
3213
+ const planeJson = join12(ctx.repoRoot, ".plane.json");
3214
+ if (existsSync9(planeJson)) {
2948
3215
  const backup = `${planeJson}.migrated-backup`;
2949
- if (existsSync8(backup)) {
3216
+ if (existsSync9(backup)) {
2950
3217
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
2951
3218
  } else {
2952
3219
  changedFiles.push(backup);
@@ -2968,8 +3235,8 @@ var RULES = [
2968
3235
  title: ".env.op + gitignore secrets contract",
2969
3236
  audit: (ctx) => {
2970
3237
  const details = [];
2971
- const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
2972
- const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
3238
+ const envOp = safeReadText(join12(ctx.repoRoot, ".env.op"));
3239
+ const gitignore = safeReadText(join12(ctx.repoRoot, ".gitignore"));
2973
3240
  if (!envOp) {
2974
3241
  details.push(".env.op missing");
2975
3242
  } else {
@@ -2995,12 +3262,12 @@ var RULES = [
2995
3262
  migrate: (ctx, finding) => {
2996
3263
  const changedFiles = [];
2997
3264
  const details = [];
2998
- const envOpPath = join11(ctx.repoRoot, ".env.op");
2999
- if (!existsSync8(envOpPath)) {
3265
+ const envOpPath = join12(ctx.repoRoot, ".env.op");
3266
+ if (!existsSync9(envOpPath)) {
3000
3267
  changedFiles.push(envOpPath);
3001
- if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3268
+ if (!ctx.dryRun) writeText(envOpPath, readText(join12(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3002
3269
  }
3003
- const gitignorePath = join11(ctx.repoRoot, ".gitignore");
3270
+ const gitignorePath = join12(ctx.repoRoot, ".gitignore");
3004
3271
  const gitignore = safeReadText(gitignorePath) ?? "";
3005
3272
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
3006
3273
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -3027,7 +3294,7 @@ var RULES = [
3027
3294
  title: ".copier-answers.yml provenance + drift report",
3028
3295
  audit: (ctx) => {
3029
3296
  const details = [];
3030
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
3297
+ const path = join12(ctx.repoRoot, ".copier-answers.yml");
3031
3298
  const text2 = safeReadText(path);
3032
3299
  const project = readProjectJson(ctx);
3033
3300
  if (!text2) {
@@ -3058,12 +3325,12 @@ var RULES = [
3058
3325
  const changedFiles = [];
3059
3326
  const project = canonicalProjectJson(ctx);
3060
3327
  const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
3061
- _src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
3328
+ _src_path: ${join12(ctx.pjanglerRoot, "templates", "commonproject")}
3062
3329
  project_description: ${String(project.project_description)}
3063
3330
  project_name: ${String(project.project_name)}
3064
3331
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3065
3332
  `;
3066
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
3333
+ const path = join12(ctx.repoRoot, ".copier-answers.yml");
3067
3334
  if (safeReadText(path) !== text2) {
3068
3335
  changedFiles.push(path);
3069
3336
  if (!ctx.dryRun) writeText(path, text2);
@@ -3082,15 +3349,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3082
3349
  id: "bmad.scaffold",
3083
3350
  title: "BMAD modules/docs scaffold",
3084
3351
  audit: (ctx) => {
3085
- const sourceRoot = join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
3086
- const targetRoot = join11(ctx.repoRoot, "_bmad");
3352
+ const targetRoot = join12(ctx.repoRoot, "_bmad");
3087
3353
  const sentinels = [
3088
- join11("core", "config.yaml"),
3089
- join11("custom", "config.yaml"),
3090
- join11("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
3091
- join11("bmm", "workflows", "workflow-status", "workflow.yaml")
3354
+ join12("core", "config.yaml"),
3355
+ join12("config.toml"),
3356
+ join12("_config", "manifest.yaml"),
3357
+ join12("bmm", "config.yaml")
3092
3358
  ];
3093
- const missing = sentinels.filter((file) => existsSync8(join11(sourceRoot, file)) && !existsSync8(join11(targetRoot, file)));
3359
+ const missing = sentinels.filter((file) => !existsSync9(join12(targetRoot, file)));
3094
3360
  return {
3095
3361
  id: "bmad.scaffold",
3096
3362
  title: "BMAD modules/docs scaffold",
@@ -3102,17 +3368,148 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3102
3368
  },
3103
3369
  migrate: (ctx, finding) => {
3104
3370
  const changedFiles = [];
3105
- copyMissingRecursive(join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join11(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
3371
+ if (ctx.dryRun) {
3372
+ for (const detail of finding.details) {
3373
+ changedFiles.push(join12(ctx.repoRoot, detail));
3374
+ }
3375
+ return {
3376
+ id: finding.id,
3377
+ title: finding.title,
3378
+ status: changedFiles.length ? "applied" : "noop",
3379
+ summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
3380
+ changedFiles,
3381
+ details: [
3382
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3383
+ ]
3384
+ };
3385
+ }
3386
+ const install = runBmadInstall(ctx.repoRoot);
3387
+ if (!install.ok) {
3388
+ return {
3389
+ id: finding.id,
3390
+ title: finding.title,
3391
+ status: "blocked",
3392
+ summary: `Failed to run bmad-method install`,
3393
+ changedFiles: [],
3394
+ details: [install.error ?? "Unknown error"]
3395
+ };
3396
+ }
3397
+ for (const detail of finding.details) {
3398
+ if (existsSync9(join12(ctx.repoRoot, detail))) {
3399
+ changedFiles.push(join12(ctx.repoRoot, detail));
3400
+ }
3401
+ }
3106
3402
  return {
3107
3403
  id: finding.id,
3108
3404
  title: finding.title,
3109
3405
  status: changedFiles.length ? "applied" : "noop",
3110
- summary: changedFiles.length ? "Copied missing BMAD scaffold files" : "No changes required",
3406
+ summary: changedFiles.length ? "Installed BMAD scaffold via non-interactive installer" : "No changes required",
3111
3407
  changedFiles,
3112
3408
  details: []
3113
3409
  };
3114
3410
  }
3115
3411
  },
3412
+ {
3413
+ id: "bmad.version",
3414
+ title: "BMAD version currency",
3415
+ audit: (ctx) => {
3416
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3417
+ if (!installed) {
3418
+ return {
3419
+ id: "bmad.version",
3420
+ title: "BMAD version currency",
3421
+ status: "skip",
3422
+ summary: existsSync9(join12(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
3423
+ details: [],
3424
+ fixable: false
3425
+ };
3426
+ }
3427
+ const resolved = resolveBmadDistTags(ctx.homeDir);
3428
+ const available = resolved?.distTags?.[BMAD_TARGET_CHANNEL];
3429
+ if (!available) {
3430
+ return {
3431
+ id: "bmad.version",
3432
+ title: "BMAD version currency",
3433
+ status: "skip",
3434
+ summary: `BMAD ${installed} installed; latest ${BMAD_TARGET_CHANNEL} version unknown (npm unreachable)`,
3435
+ details: [`Could not resolve ${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL} from npm`],
3436
+ fixable: false
3437
+ };
3438
+ }
3439
+ const staleNote = resolved.stale ? ` ${glyph.dot} cached` : "";
3440
+ if (compareBmadVersions(installed, available) >= 0) {
3441
+ return {
3442
+ id: "bmad.version",
3443
+ title: "BMAD version currency",
3444
+ status: "pass",
3445
+ summary: `BMAD ${installed} is current (${BMAD_TARGET_CHANNEL} ${available})${staleNote}`,
3446
+ details: [],
3447
+ fixable: false
3448
+ };
3449
+ }
3450
+ return {
3451
+ id: "bmad.version",
3452
+ title: "BMAD version currency",
3453
+ status: "warn",
3454
+ summary: `BMAD ${installed} is behind ${BMAD_TARGET_CHANNEL} ${available} \u2014 upgrade available`,
3455
+ details: [
3456
+ `installed: ${installed}`,
3457
+ `available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
3458
+ resolved.distTags.latest ? `stable latest: ${resolved.distTags.latest}` : "",
3459
+ "run `pj migrate bmad.version` to upgrade"
3460
+ ].filter(Boolean),
3461
+ fixable: true
3462
+ };
3463
+ },
3464
+ migrate: (ctx, finding) => {
3465
+ if (finding.status !== "warn") {
3466
+ return {
3467
+ id: finding.id,
3468
+ title: finding.title,
3469
+ status: "noop",
3470
+ summary: finding.status === "skip" ? finding.summary : "BMAD already current",
3471
+ changedFiles: [],
3472
+ details: []
3473
+ };
3474
+ }
3475
+ const installed = readInstalledBmadVersion(ctx.repoRoot);
3476
+ const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3477
+ const manifestPath = join12(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3478
+ if (ctx.dryRun) {
3479
+ return {
3480
+ id: finding.id,
3481
+ title: finding.title,
3482
+ status: "applied",
3483
+ summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
3484
+ changedFiles: [manifestPath],
3485
+ details: [
3486
+ `Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
3487
+ ]
3488
+ };
3489
+ }
3490
+ const install = runBmadInstall(ctx.repoRoot);
3491
+ if (!install.ok) {
3492
+ return {
3493
+ id: finding.id,
3494
+ title: finding.title,
3495
+ status: "blocked",
3496
+ summary: "Failed to upgrade BMAD via installer",
3497
+ changedFiles: [],
3498
+ details: [install.error ?? "Unknown error"]
3499
+ };
3500
+ }
3501
+ const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
3502
+ const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
3503
+ return {
3504
+ id: finding.id,
3505
+ title: finding.title,
3506
+ status: upgraded ? "applied" : "noop",
3507
+ summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
3508
+ changedFiles: upgraded ? [manifestPath] : [],
3509
+ details: []
3510
+ };
3511
+ }
3512
+ },
3116
3513
  {
3117
3514
  id: "hermes.pm-scaffold",
3118
3515
  title: "Hermes PM scaffold parity",
@@ -3124,11 +3521,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3124
3521
  }
3125
3522
  const details = [];
3126
3523
  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"]) {
3127
- if (!existsSync8(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
3524
+ if (!existsSync9(join12(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join12(role.roleDir, rel))}`);
3128
3525
  }
3129
- const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
3526
+ const gitmodules = safeReadText(join12(ctx.repoRoot, ".gitmodules")) ?? "";
3130
3527
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
3131
- if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
3528
+ if (!profileMetaInheritsDefault(join12(role.roleDir, "runtime", "profile.yaml"))) {
3132
3529
  details.push("runtime/profile.yaml missing inherited default config metadata");
3133
3530
  }
3134
3531
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -3149,21 +3546,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3149
3546
  if (!role) {
3150
3547
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
3151
3548
  }
3152
- const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3153
- writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3154
- writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3155
- writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3156
- copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3157
- copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3158
- copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3159
- const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3160
- const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
3161
- if (existsSync8(promptSource) && !existsSync8(promptTarget)) {
3549
+ const templateRoleDir = join12(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3550
+ writeIfDifferent(join12(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3551
+ writeIfDifferent(join12(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3552
+ writeIfDifferent(join12(role.roleDir, ".gitignore"), readText(join12(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3553
+ copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3554
+ copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3555
+ copyMissingRecursive(join12(templateRoleDir, ".scripts"), join12(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3556
+ const promptSource = join12(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3557
+ const promptTarget = join12(role.roleDir, ".scripts", "sentinel.prompt.md");
3558
+ if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
3162
3559
  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);
3163
3560
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
3164
3561
  }
3165
3562
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
3166
- const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
3563
+ const profileMetaUpdated = upsertInheritedProfileMeta(join12(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
3167
3564
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
3168
3565
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
3169
3566
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -3177,6 +3574,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3177
3574
  };
3178
3575
  }
3179
3576
  },
3577
+ {
3578
+ id: "hermes.untracked-runtimes",
3579
+ title: "Hermes agent runtimes untracked + gitignored",
3580
+ audit: (ctx) => {
3581
+ const roles = discoverRoles(ctx.repoRoot);
3582
+ if (roles.length === 0) {
3583
+ return {
3584
+ id: "hermes.untracked-runtimes",
3585
+ title: "Hermes agent runtimes untracked + gitignored",
3586
+ status: "skip",
3587
+ summary: "No Hermes roles present",
3588
+ details: [],
3589
+ fixable: false
3590
+ };
3591
+ }
3592
+ const details = [];
3593
+ for (const role of roles) {
3594
+ const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3595
+ const runtimeRelPath = join12(roleRelDir, "runtime");
3596
+ const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3597
+ cwd: ctx.repoRoot,
3598
+ encoding: "utf8"
3599
+ });
3600
+ if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3601
+ details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
3602
+ }
3603
+ const gitignorePath = join12(role.roleDir, ".gitignore");
3604
+ if (existsSync9(gitignorePath)) {
3605
+ const content = safeReadText(gitignorePath) ?? "";
3606
+ const lines = content.split(/\r?\n/).map((line) => line.trim());
3607
+ if (!lines.includes("runtime/") && !lines.includes("runtime")) {
3608
+ details.push(`.gitignore missing runtime/ ignore entry in ${relative(ctx.repoRoot, gitignorePath)}`);
3609
+ }
3610
+ } else {
3611
+ details.push(`.gitignore is missing in ${relative(ctx.repoRoot, gitignorePath)}`);
3612
+ }
3613
+ }
3614
+ return {
3615
+ id: "hermes.untracked-runtimes",
3616
+ title: "Hermes agent runtimes untracked + gitignored",
3617
+ status: details.length === 0 ? "pass" : "fail",
3618
+ summary: details.length === 0 ? "All Hermes agent runtimes are untracked and gitignored" : `${details.length} issue(s) with untracked/ignored runtimes detected`,
3619
+ details,
3620
+ fixable: true
3621
+ };
3622
+ },
3623
+ migrate: (ctx, finding) => {
3624
+ const roles = discoverRoles(ctx.repoRoot);
3625
+ const changedFiles = [];
3626
+ const details = [];
3627
+ for (const role of roles) {
3628
+ const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3629
+ const runtimeRelPath = join12(roleRelDir, "runtime");
3630
+ const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3631
+ cwd: ctx.repoRoot,
3632
+ encoding: "utf8"
3633
+ });
3634
+ if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3635
+ details.push(`untrack ${runtimeRelPath}`);
3636
+ changedFiles.push(runtimeRelPath);
3637
+ if (!ctx.dryRun) {
3638
+ spawnSync6("git", ["rm", "--cached", "-r", runtimeRelPath], {
3639
+ cwd: ctx.repoRoot,
3640
+ encoding: "utf8"
3641
+ });
3642
+ }
3643
+ }
3644
+ const gitignorePath = join12(role.roleDir, ".gitignore");
3645
+ let content = "";
3646
+ let isIgnored = false;
3647
+ if (existsSync9(gitignorePath)) {
3648
+ content = safeReadText(gitignorePath) ?? "";
3649
+ const lines = content.split(/\r?\n/).map((line) => line.trim());
3650
+ isIgnored = lines.includes("runtime/") || lines.includes("runtime");
3651
+ }
3652
+ if (!isIgnored) {
3653
+ details.push(`ignore runtime/ in ${relative(ctx.repoRoot, gitignorePath)}`);
3654
+ changedFiles.push(gitignorePath);
3655
+ if (!ctx.dryRun) {
3656
+ if (content && !content.endsWith("\n")) {
3657
+ content += "\n";
3658
+ }
3659
+ content += "runtime/\n";
3660
+ writeText(gitignorePath, content);
3661
+ }
3662
+ }
3663
+ }
3664
+ return {
3665
+ id: finding.id,
3666
+ title: finding.title,
3667
+ status: changedFiles.length ? "applied" : "noop",
3668
+ summary: changedFiles.length ? "Hermes agent runtimes made untracked and ignored" : "No changes required",
3669
+ changedFiles,
3670
+ details
3671
+ };
3672
+ }
3673
+ },
3180
3674
  {
3181
3675
  id: "systemd.sentinel",
3182
3676
  title: "Hermes systemd/sentinel units enabled + active",
@@ -3217,9 +3711,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3217
3711
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
3218
3712
  }
3219
3713
  for (const role of roles) {
3220
- const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
3714
+ const sysDir = join12(ctx.homeDir, ".config", "systemd", "user");
3221
3715
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
3222
- const allUnitsPresent = units.every((unit) => existsSync8(join11(sysDir, unit)));
3716
+ const allUnitsPresent = units.every((unit) => existsSync9(join12(sysDir, unit)));
3223
3717
  if (allUnitsPresent) {
3224
3718
  if (ctx.dryRun) {
3225
3719
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -3231,12 +3725,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3231
3725
  }
3232
3726
  continue;
3233
3727
  }
3234
- for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
3235
- if (!script || !existsSync8(script)) continue;
3728
+ for (const script of [join12(role.roleDir, ".scripts", "70-systemd.sh")]) {
3729
+ if (!script || !existsSync9(script)) continue;
3236
3730
  if (ctx.dryRun) {
3237
3731
  details.push(`would run: bash ${script}`);
3238
3732
  } else {
3239
- const result = spawnSync5("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
3733
+ const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
3240
3734
  if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
3241
3735
  }
3242
3736
  }
@@ -3356,7 +3850,7 @@ var server = new McpServer({
3356
3850
  var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
3357
3851
  function resolveTargetDir(targetDir) {
3358
3852
  const dir = resolve3(targetDir ?? process.cwd());
3359
- if (!existsSync9(dir)) {
3853
+ if (!existsSync10(dir)) {
3360
3854
  throw new Error(`Target directory does not exist: ${dir}`);
3361
3855
  }
3362
3856
  if (!statSync2(dir).isDirectory()) {
@@ -3367,7 +3861,7 @@ function resolveTargetDir(targetDir) {
3367
3861
  function resolvePjanglerRoot3() {
3368
3862
  let dir = dirname8(fileURLToPath5(import.meta.url));
3369
3863
  while (dir !== dirname8(dir)) {
3370
- if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
3864
+ if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
3371
3865
  return dir;
3372
3866
  }
3373
3867
  dir = dirname8(dir);
@@ -3569,8 +4063,8 @@ server.registerTool(
3569
4063
  const pjanglerRoot = resolvePjanglerRoot3();
3570
4064
  const projectSlug = input.projectSlug ?? slugify(input.projectName);
3571
4065
  const parentDir = resolve3(input.parentDir ?? process.cwd());
3572
- if (!existsSync9(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
3573
- const targetDir = resolve3(input.targetDir ?? join12(parentDir, projectSlug));
4066
+ if (!existsSync10(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
4067
+ const targetDir = resolve3(input.targetDir ?? join13(parentDir, projectSlug));
3574
4068
  const overwrite = input.overwrite ?? input.force ?? false;
3575
4069
  const dryRun = input.dryRun ?? true;
3576
4070
  const local = input.local ?? true;
@@ -3580,7 +4074,7 @@ server.registerTool(
3580
4074
  if (!skipPlane && ticketProvider === "plane" && !boardId) {
3581
4075
  throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
3582
4076
  }
3583
- if (!dryRun && existsSync9(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
4077
+ if (!dryRun && existsSync10(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
3584
4078
  const plan = planProjectInit({
3585
4079
  name: input.projectName,
3586
4080
  description: input.projectDescription,