@delorenj/pjangler 1.2.8 → 1.2.12

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.
Files changed (30) hide show
  1. package/dist/index.js +126 -28
  2. package/dist/mcp-server.js +124 -26
  3. package/package.json +1 -1
  4. package/templates/commonproject/copier.yml +4 -1
  5. package/templates/commonproject/template/.project.json.jinja +9 -12
  6. package/templates/hermes-agent/README.md +1 -1
  7. package/templates/hermes-agent/config.example.toml +66 -0
  8. package/templates/hermes-agent/docs/architecture.md +5 -0
  9. package/templates/hermes-agent/docs/bloodbank-gateway.md +57 -0
  10. package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
  11. package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +60 -0
  12. package/templates/hermes-agent/docs/operations.md +1 -1
  13. package/templates/hermes-agent/docs/sentinel/README.md +2 -2
  14. package/templates/hermes-agent/docs/sentinel/development.md +3 -3
  15. package/templates/hermes-agent/docs/sentinel/providers.md +6 -5
  16. package/templates/hermes-agent/install-local.sh +10 -4
  17. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +5 -3
  18. package/templates/hermes-agent/scripts/fleet-sync.sh +64 -1
  19. package/templates/hermes-agent/template/.scripts/01-config.sh +1 -0
  20. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +9 -0
  21. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +22 -3
  22. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +22 -0
  23. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -33
  24. package/templates/hermes-agent/template/.scripts/70-systemd.sh +8 -1
  25. package/templates/hermes-agent/template/.scripts/_lib.sh +61 -3
  26. package/templates/hermes-agent/template/.scripts/config.example.toml +5 -0
  27. package/templates/hermes-agent/template/.scripts/heartbeat.sh +66 -22
  28. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +3 -3
  29. package/templates/hermes-agent/template/.scripts/providers/plane.sh +29 -9
  30. package/templates/hermes-agent/template/role.yaml.jinja +2 -27
package/dist/index.js CHANGED
@@ -82,6 +82,8 @@ function renderHostConfig() {
82
82
  const hermesRepo = join2(home, "code", "hermes-agent");
83
83
  const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
84
84
  const skillsDir = join2(home, ".agents", "skills");
85
+ const pmExternalSkillGlobalDir = join2(home, "code", "skillex", "skill-sets", "global", ".system");
86
+ const pmExternalSkillBmadDir = join2(home, "code", "skillex", "packs", "bmad", "6.10.2");
85
87
  return `# hermes-agent-template \u2014 host configuration
86
88
  # Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
87
89
  #
@@ -97,9 +99,14 @@ home = "~/.hermes"
97
99
  hermes_bin = "${hermesBin}"
98
100
  hermes_repo = "${hermesRepo}"
99
101
  runtime_scaffold_dir = "${scaffoldDir}"
102
+ # Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
100
103
  fleet_env = "~/.hermes/fleet.env"
101
104
  registry_file = "~/.hermes/agents-registry.yaml"
102
105
  canonical_skills_dir = "${skillsDir}"
106
+ pm_external_skill_dirs = [
107
+ "${pmExternalSkillGlobalDir}",
108
+ "${pmExternalSkillBmadDir}",
109
+ ]
103
110
  symlinked_runtime_skills = []
104
111
 
105
112
  [github]
@@ -1210,14 +1217,14 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
1210
1217
  import { spawnSync as spawnSync4 } from "node:child_process";
1211
1218
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
1212
1219
  import { homedir as homedir3 } from "node:os";
1213
- import { basename as basename2, dirname as dirname4, join as join8, resolve } from "node:path";
1220
+ import { basename as basename2, delimiter, dirname as dirname4, join as join8, resolve } from "node:path";
1214
1221
  import YAML from "yaml";
1215
1222
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
1223
+ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1216
1224
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1217
- var KNOWN_SKILL_ROOTS = [
1225
+ var DEFAULT_SOURCE_SKILL_ROOTS = [
1218
1226
  "/home/delorenj/code/skillex/all-skills",
1219
- "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
1220
- "/home/delorenj/code/pjangler/.agents/skills",
1227
+ join8(homedir3(), ".agents", "skills"),
1221
1228
  join8(homedir3(), ".codex", "skills")
1222
1229
  ];
1223
1230
  function projectRegistryPath(env2 = process.env) {
@@ -1288,8 +1295,7 @@ function buildTicketProviderBlock(input) {
1288
1295
  workspace: input.workspace ?? "",
1289
1296
  identifier: input.identifier,
1290
1297
  board_id: boardId,
1291
- board_url: input.boardUrl ?? (boardId ? `https://trello.com/b/${boardId}` : ""),
1292
- state: "planned"
1298
+ state: boardId ? "linked" : "planned"
1293
1299
  };
1294
1300
  }
1295
1301
  const workspace = input.workspace ?? "33god";
@@ -1298,8 +1304,16 @@ function buildTicketProviderBlock(input) {
1298
1304
  workspace,
1299
1305
  identifier: input.identifier,
1300
1306
  board_id: boardId,
1301
- board_url: input.boardUrl ?? (boardId ? `https://plane.delo.sh/${workspace}/projects/${boardId}/issues/` : ""),
1302
- state: "planned"
1307
+ state: boardId ? "linked" : "planned"
1308
+ };
1309
+ }
1310
+ function defaultProjectAutomation() {
1311
+ return {
1312
+ reconcile: {
1313
+ enabled: false,
1314
+ grace_hours: 0,
1315
+ auto_review: true
1316
+ }
1303
1317
  };
1304
1318
  }
1305
1319
  function slugifyProjectName(value) {
@@ -1333,18 +1347,31 @@ function defaultProjectTargetDir(name, cwd = process.cwd()) {
1333
1347
  const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
1334
1348
  return resolve(dirname4(resolve(cwd)), compactName);
1335
1349
  }
1336
- function resolveSourceSkillPath(sourceSkill) {
1350
+ function sourceSkillRoots(env2 = process.env) {
1351
+ const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
1352
+ const seen = /* @__PURE__ */ new Set();
1353
+ const roots = [];
1354
+ for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
1355
+ const normalized = resolve(expandHome(root));
1356
+ if (seen.has(normalized)) continue;
1357
+ seen.add(normalized);
1358
+ roots.push(normalized);
1359
+ }
1360
+ return roots;
1361
+ }
1362
+ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1337
1363
  if (!sourceSkill) return void 0;
1338
1364
  const expanded = expandHome(sourceSkill);
1339
1365
  const direct = resolve(expanded);
1340
1366
  if (existsSync6(direct)) return direct;
1341
1367
  const name = basename2(sourceSkill);
1342
- for (const root of KNOWN_SKILL_ROOTS) {
1368
+ const roots = sourceSkillRoots(env2);
1369
+ for (const root of roots) {
1343
1370
  const candidate = join8(root, name);
1344
1371
  if (existsSync6(candidate)) return candidate;
1345
1372
  }
1346
- const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
1347
- const hint = existsSync6(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
1373
+ const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1374
+ const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
1348
1375
  throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
1349
1376
  }
1350
1377
  function planProjectInit(input) {
@@ -1384,10 +1411,10 @@ function planProjectInit(input) {
1384
1411
  type: input.ticketProvider ?? "plane",
1385
1412
  identifier,
1386
1413
  boardId: input.boardId ?? input.planeProjectId,
1387
- boardUrl: input.boardUrl,
1388
1414
  workspace: input.boardWorkspace ?? input.planeWorkspace
1389
1415
  }),
1390
1416
  agents,
1417
+ automation: existing?.automation ?? defaultProjectAutomation(),
1391
1418
  created_at: existing?.created_at ?? now,
1392
1419
  updated_at: now
1393
1420
  };
@@ -1415,7 +1442,6 @@ function planProjectInit(input) {
1415
1442
  planeProjectId: project.ticket_provider.board_id ?? "",
1416
1443
  ticketWorkspace: project.ticket_provider.workspace ?? "",
1417
1444
  boardId: project.ticket_provider.board_id ?? "",
1418
- boardUrl: project.ticket_provider.board_url ?? "",
1419
1445
  projectIdentifier: identifier,
1420
1446
  primaryLanguage: project.template.commonproject.primary_language,
1421
1447
  agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
@@ -1527,10 +1553,10 @@ function projectManifestFromRegistryProject(project) {
1527
1553
  workspace: project.ticket_provider.workspace ?? "",
1528
1554
  identifier: project.ticket_provider.identifier ?? "",
1529
1555
  board_id: project.ticket_provider.board_id ?? "",
1530
- board_url: project.ticket_provider.board_url ?? "",
1531
1556
  state: project.ticket_provider.state
1532
1557
  },
1533
- agents
1558
+ agents,
1559
+ automation: project.automation ?? defaultProjectAutomation()
1534
1560
  };
1535
1561
  }
1536
1562
  function formatProjectInitPlan(plan) {
@@ -1611,7 +1637,6 @@ function buildCommonProjectCopierAction(input) {
1611
1637
  plane_project_id: input.planeProjectId ?? "",
1612
1638
  ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
1613
1639
  board_id: input.boardId ?? input.planeProjectId ?? "",
1614
- board_url: input.boardUrl ?? "",
1615
1640
  project_identifier: input.projectIdentifier,
1616
1641
  primary_language: input.primaryLanguage,
1617
1642
  agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
@@ -2215,8 +2240,12 @@ function discoverRoles(repoRoot) {
2215
2240
  planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
2216
2241
  ticketProviderName: yamlGet(text3, "ticket_provider.name"),
2217
2242
  ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
2218
- ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
2219
- ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
2243
+ ticketProviderIdentifier: yamlGet(text3, "plane.identifier"),
2244
+ legacyReconcileEnabled: yamlGet(text3, "reconcile.enabled"),
2245
+ legacyReconcileGraceHours: yamlGet(text3, "reconcile.grace_hours"),
2246
+ legacyReconcileAutoReview: yamlGet(text3, "reconcile.auto_review"),
2247
+ legacyScrumGraceHours: yamlGet(text3, "scrum_master.grace_hours"),
2248
+ legacyScrumAutoReview: yamlGet(text3, "scrum_master.auto_review")
2220
2249
  };
2221
2250
  }).filter((value) => Boolean(value));
2222
2251
  }
@@ -2241,10 +2270,39 @@ function templateVersioningScript(ctx) {
2241
2270
  function templateLinkAgentfilesScript(ctx) {
2242
2271
  return templateScript(ctx, "link-agentfiles.sh");
2243
2272
  }
2273
+ function resolveAgentHooksLayer2(ctx) {
2274
+ const override = process.env.PJ_AGENT_HOOKS_LAYER;
2275
+ if (override === "0" || override === "false") return false;
2276
+ 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"));
2279
+ }
2280
+ function evaluateMiseConditionals(template, agentHooksLayer) {
2281
+ const out = [];
2282
+ let depth = 0;
2283
+ let skipDepth = 0;
2284
+ for (const line of template.split("\n")) {
2285
+ const stmt = line.trim();
2286
+ const ifMatch = /^\{%-?\s*if\s+(\w+)\s*-?%\}$/.exec(stmt);
2287
+ if (ifMatch) {
2288
+ depth += 1;
2289
+ const truthy = ifMatch[1] === "agent_hooks_layer" ? agentHooksLayer : false;
2290
+ if (skipDepth === 0 && !truthy) skipDepth = depth;
2291
+ continue;
2292
+ }
2293
+ if (/^\{%-?\s*endif\s*-?%\}$/.test(stmt)) {
2294
+ if (skipDepth === depth) skipDepth = 0;
2295
+ depth = Math.max(0, depth - 1);
2296
+ continue;
2297
+ }
2298
+ if (skipDepth === 0) out.push(line);
2299
+ }
2300
+ return out.join("\n");
2301
+ }
2244
2302
  function renderGeneratedProjectMiseToml(ctx, template) {
2245
2303
  const project = readProjectJson(ctx);
2246
2304
  const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
2247
- return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2305
+ return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2248
2306
  }
2249
2307
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2250
2308
  const targetPath = join10(ctx.repoRoot, "mise.toml");
@@ -2434,6 +2492,23 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
2434
2492
  function readProjectJson(ctx) {
2435
2493
  return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
2436
2494
  }
2495
+ function boolSetting(value, fallback) {
2496
+ if (typeof value === "boolean") return value;
2497
+ if (typeof value === "string") {
2498
+ const normalized = value.trim().toLowerCase();
2499
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
2500
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
2501
+ }
2502
+ return fallback;
2503
+ }
2504
+ function numberSetting(value, fallback) {
2505
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2506
+ if (typeof value === "string" && value.trim()) {
2507
+ const parsed = Number(value);
2508
+ if (Number.isFinite(parsed)) return parsed;
2509
+ }
2510
+ return fallback;
2511
+ }
2437
2512
  function canonicalProjectJson(ctx) {
2438
2513
  const roles = discoverRoles(ctx.repoRoot);
2439
2514
  const existing = readProjectJson(ctx) ?? {};
@@ -2444,9 +2519,9 @@ function canonicalProjectJson(ctx) {
2444
2519
  workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
2445
2520
  identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
2446
2521
  board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
2447
- board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || ""),
2448
- state: String((existing.ticket_provider?.state ?? "planned") || "planned")
2522
+ state: String((existing.ticket_provider?.state ?? (firstRole?.ticketProviderBoardId ? "linked" : "planned")) || "planned")
2449
2523
  };
2524
+ if (ticketProvider.board_id && ticketProvider.state === "planned") ticketProvider.state = "linked";
2450
2525
  const existingAgents = existing.agents ?? {};
2451
2526
  const discoveredAgents = Object.fromEntries(
2452
2527
  roles.map((role) => [
@@ -2466,13 +2541,27 @@ function canonicalProjectJson(ctx) {
2466
2541
  provisioning_state: existingAgent.provisioning_state
2467
2542
  };
2468
2543
  }
2544
+ const existingAutomation = existing.automation ?? {};
2545
+ const existingReconcile = existingAutomation.reconcile ?? {};
2546
+ const legacyEnabled = roles.find((role) => role.legacyReconcileEnabled)?.legacyReconcileEnabled;
2547
+ const legacyGrace = roles.find((role) => role.legacyReconcileGraceHours || role.legacyScrumGraceHours);
2548
+ const legacyAutoReview = roles.find((role) => role.legacyReconcileAutoReview || role.legacyScrumAutoReview);
2549
+ const automation = {
2550
+ ...existingAutomation,
2551
+ reconcile: {
2552
+ enabled: boolSetting(existingReconcile.enabled, boolSetting(legacyEnabled, false)),
2553
+ grace_hours: numberSetting(existingReconcile.grace_hours, numberSetting(legacyGrace?.legacyReconcileGraceHours || legacyGrace?.legacyScrumGraceHours, 0)),
2554
+ auto_review: boolSetting(existingReconcile.auto_review, boolSetting(legacyAutoReview?.legacyReconcileAutoReview || legacyAutoReview?.legacyScrumAutoReview, true))
2555
+ }
2556
+ };
2469
2557
  return {
2470
2558
  project_name: String(existing.project_name ?? titleCaseSlug(slug)),
2471
2559
  project_description: String(existing.project_description ?? ""),
2472
2560
  project_slug: slug,
2473
2561
  repo_path: ctx.repoRoot,
2474
2562
  ticket_provider: ticketProvider,
2475
- agents
2563
+ agents,
2564
+ automation
2476
2565
  };
2477
2566
  }
2478
2567
  function projectJsonFinding(ctx) {
@@ -2487,7 +2576,7 @@ function projectJsonFinding(ctx) {
2487
2576
  if (!data) {
2488
2577
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json is not valid JSON", details: [], fixable: true };
2489
2578
  }
2490
- for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents"]) {
2579
+ for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents", "automation"]) {
2491
2580
  if (!(key in data)) details.push(`missing key: ${key}`);
2492
2581
  }
2493
2582
  if (data.repo_path !== ctx.repoRoot) details.push(`repo_path should be ${ctx.repoRoot}`);
@@ -2504,9 +2593,18 @@ function projectJsonFinding(ctx) {
2504
2593
  }
2505
2594
  }
2506
2595
  const ticketProvider = data.ticket_provider ?? {};
2507
- for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
2596
+ for (const key of ["type", "workspace", "identifier", "board_id", "state"]) {
2508
2597
  if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
2509
2598
  }
2599
+ if ("board_url" in ticketProvider) details.push("ticket_provider.board_url should be removed; derive it from provider/workspace/board_id");
2600
+ if (!ticketProvider.board_id && roles.some((role) => role.ticketProviderBoardId)) {
2601
+ details.push("ticket_provider.board_id missing even though legacy role.yaml contains a board binding");
2602
+ }
2603
+ const automation = data.automation ?? {};
2604
+ const reconcile = automation.reconcile ?? {};
2605
+ for (const key of ["enabled", "grace_hours", "auto_review"]) {
2606
+ if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2607
+ }
2510
2608
  if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2511
2609
  return {
2512
2610
  id: "sot.project-json",
@@ -2520,7 +2618,7 @@ function projectJsonFinding(ctx) {
2520
2618
  function renderSoul(role) {
2521
2619
  const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
2522
2620
  const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
2523
- const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`reconcile.enabled\` in role.yaml), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
2621
+ const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`automation.reconcile.enabled\` in repo-root \`.project.json\`), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
2524
2622
  const runtimeOwner = role.runtimeOwner || "delorenj";
2525
2623
  return `# ${role.displayName || role.agentId}
2526
2624
 
@@ -3574,7 +3672,7 @@ async function runRecipeSubsystem(name, options) {
3574
3672
  }
3575
3673
  var program = new Command3();
3576
3674
  program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
3577
- program.command("init").argument("[name]", "Project name to bootstrap (omit inside an existing git repo)").description("Bootstrap a project: registry entry + CommonProject scaffold + .project.json").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Board URL override (derived from provider + board-id if omitted)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
3675
+ program.command("init").argument("[name]", "Project name to bootstrap (omit inside an existing git repo)").description("Bootstrap a project: registry entry + CommonProject scaffold + .project.json").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
3578
3676
  if (name && getRecipeNames().includes(name)) {
3579
3677
  if (!options.json) {
3580
3678
  console.error(`${yellow(glyph.warn)} ${dim(`"pjangler init ${name}" is deprecated \u2014 use "pjangler add ${name}". Forwarding\u2026`)}`);
@@ -3603,7 +3701,7 @@ program.command("list").description("List available subsystems").action(() => {
3603
3701
  console.log("");
3604
3702
  });
3605
3703
  var projectCmd = program.command("project").description("Manage the pjangler project registry");
3606
- projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Board URL override (derived from provider + board-id if omitted)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action((name, options) => {
3704
+ projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action((name, options) => {
3607
3705
  if (!options.json) console.error(`${yellow(glyph.warn)} ${dim('"pjangler project init" is deprecated \u2014 use "pjangler init".')}`);
3608
3706
  return runProjectInit(name, options);
3609
3707
  });
@@ -630,6 +630,8 @@ function renderHostConfig() {
630
630
  const hermesRepo = join3(home, "code", "hermes-agent");
631
631
  const scaffoldDir = join3(home, "code", "hermes-agent-template", "runtime-scaffold");
632
632
  const skillsDir = join3(home, ".agents", "skills");
633
+ const pmExternalSkillGlobalDir = join3(home, "code", "skillex", "skill-sets", "global", ".system");
634
+ const pmExternalSkillBmadDir = join3(home, "code", "skillex", "packs", "bmad", "6.10.2");
633
635
  return `# hermes-agent-template \u2014 host configuration
634
636
  # Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
635
637
  #
@@ -645,9 +647,14 @@ home = "~/.hermes"
645
647
  hermes_bin = "${hermesBin}"
646
648
  hermes_repo = "${hermesRepo}"
647
649
  runtime_scaffold_dir = "${scaffoldDir}"
650
+ # Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
648
651
  fleet_env = "~/.hermes/fleet.env"
649
652
  registry_file = "~/.hermes/agents-registry.yaml"
650
653
  canonical_skills_dir = "${skillsDir}"
654
+ pm_external_skill_dirs = [
655
+ "${pmExternalSkillGlobalDir}",
656
+ "${pmExternalSkillBmadDir}",
657
+ ]
651
658
  symlinked_runtime_skills = []
652
659
 
653
660
  [github]
@@ -1196,14 +1203,14 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
1196
1203
  import { spawnSync as spawnSync4 } from "node:child_process";
1197
1204
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
1198
1205
  import { homedir as homedir3 } from "node:os";
1199
- import { basename as basename2, dirname as dirname4, join as join8, resolve } from "node:path";
1206
+ import { basename as basename2, delimiter, dirname as dirname4, join as join8, resolve } from "node:path";
1200
1207
  import YAML from "yaml";
1201
1208
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
1209
+ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1202
1210
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1203
- var KNOWN_SKILL_ROOTS = [
1211
+ var DEFAULT_SOURCE_SKILL_ROOTS = [
1204
1212
  "/home/delorenj/code/skillex/all-skills",
1205
- "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
1206
- "/home/delorenj/code/pjangler/.agents/skills",
1213
+ join8(homedir3(), ".agents", "skills"),
1207
1214
  join8(homedir3(), ".codex", "skills")
1208
1215
  ];
1209
1216
  function projectRegistryPath(env2 = process.env) {
@@ -1274,8 +1281,7 @@ function buildTicketProviderBlock(input) {
1274
1281
  workspace: input.workspace ?? "",
1275
1282
  identifier: input.identifier,
1276
1283
  board_id: boardId,
1277
- board_url: input.boardUrl ?? (boardId ? `https://trello.com/b/${boardId}` : ""),
1278
- state: "planned"
1284
+ state: boardId ? "linked" : "planned"
1279
1285
  };
1280
1286
  }
1281
1287
  const workspace = input.workspace ?? "33god";
@@ -1284,8 +1290,16 @@ function buildTicketProviderBlock(input) {
1284
1290
  workspace,
1285
1291
  identifier: input.identifier,
1286
1292
  board_id: boardId,
1287
- board_url: input.boardUrl ?? (boardId ? `https://plane.delo.sh/${workspace}/projects/${boardId}/issues/` : ""),
1288
- state: "planned"
1293
+ state: boardId ? "linked" : "planned"
1294
+ };
1295
+ }
1296
+ function defaultProjectAutomation() {
1297
+ return {
1298
+ reconcile: {
1299
+ enabled: false,
1300
+ grace_hours: 0,
1301
+ auto_review: true
1302
+ }
1289
1303
  };
1290
1304
  }
1291
1305
  function slugifyProjectName(value) {
@@ -1319,18 +1333,31 @@ function defaultProjectTargetDir(name, cwd = process.cwd()) {
1319
1333
  const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
1320
1334
  return resolve(dirname4(resolve(cwd)), compactName);
1321
1335
  }
1322
- function resolveSourceSkillPath(sourceSkill) {
1336
+ function sourceSkillRoots(env2 = process.env) {
1337
+ const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
1338
+ const seen = /* @__PURE__ */ new Set();
1339
+ const roots = [];
1340
+ for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
1341
+ const normalized = resolve(expandHome(root));
1342
+ if (seen.has(normalized)) continue;
1343
+ seen.add(normalized);
1344
+ roots.push(normalized);
1345
+ }
1346
+ return roots;
1347
+ }
1348
+ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1323
1349
  if (!sourceSkill) return void 0;
1324
1350
  const expanded = expandHome(sourceSkill);
1325
1351
  const direct = resolve(expanded);
1326
1352
  if (existsSync6(direct)) return direct;
1327
1353
  const name = basename2(sourceSkill);
1328
- for (const root of KNOWN_SKILL_ROOTS) {
1354
+ const roots = sourceSkillRoots(env2);
1355
+ for (const root of roots) {
1329
1356
  const candidate = join8(root, name);
1330
1357
  if (existsSync6(candidate)) return candidate;
1331
1358
  }
1332
- const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
1333
- const hint = existsSync6(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
1359
+ const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1360
+ const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
1334
1361
  throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
1335
1362
  }
1336
1363
  function planProjectInit(input) {
@@ -1370,10 +1397,10 @@ function planProjectInit(input) {
1370
1397
  type: input.ticketProvider ?? "plane",
1371
1398
  identifier,
1372
1399
  boardId: input.boardId ?? input.planeProjectId,
1373
- boardUrl: input.boardUrl,
1374
1400
  workspace: input.boardWorkspace ?? input.planeWorkspace
1375
1401
  }),
1376
1402
  agents,
1403
+ automation: existing?.automation ?? defaultProjectAutomation(),
1377
1404
  created_at: existing?.created_at ?? now,
1378
1405
  updated_at: now
1379
1406
  };
@@ -1401,7 +1428,6 @@ function planProjectInit(input) {
1401
1428
  planeProjectId: project.ticket_provider.board_id ?? "",
1402
1429
  ticketWorkspace: project.ticket_provider.workspace ?? "",
1403
1430
  boardId: project.ticket_provider.board_id ?? "",
1404
- boardUrl: project.ticket_provider.board_url ?? "",
1405
1431
  projectIdentifier: identifier,
1406
1432
  primaryLanguage: project.template.commonproject.primary_language,
1407
1433
  agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
@@ -1513,10 +1539,10 @@ function projectManifestFromRegistryProject(project) {
1513
1539
  workspace: project.ticket_provider.workspace ?? "",
1514
1540
  identifier: project.ticket_provider.identifier ?? "",
1515
1541
  board_id: project.ticket_provider.board_id ?? "",
1516
- board_url: project.ticket_provider.board_url ?? "",
1517
1542
  state: project.ticket_provider.state
1518
1543
  },
1519
- agents
1544
+ agents,
1545
+ automation: project.automation ?? defaultProjectAutomation()
1520
1546
  };
1521
1547
  }
1522
1548
  function getProject(registry, slug) {
@@ -1535,7 +1561,6 @@ function buildCommonProjectCopierAction(input) {
1535
1561
  plane_project_id: input.planeProjectId ?? "",
1536
1562
  ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
1537
1563
  board_id: input.boardId ?? input.planeProjectId ?? "",
1538
- board_url: input.boardUrl ?? "",
1539
1564
  project_identifier: input.projectIdentifier,
1540
1565
  primary_language: input.primaryLanguage,
1541
1566
  agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
@@ -2142,8 +2167,12 @@ function discoverRoles(repoRoot) {
2142
2167
  planeWorkspace: yamlGet(text2, "ticket_provider.workspace") || yamlGet(text2, "plane.workspace"),
2143
2168
  ticketProviderName: yamlGet(text2, "ticket_provider.name"),
2144
2169
  ticketProviderBoardId: yamlGet(text2, "ticket_provider.board_id"),
2145
- ticketProviderBoardUrl: yamlGet(text2, "ticket_provider.board_url"),
2146
- ticketProviderIdentifier: yamlGet(text2, "plane.identifier")
2170
+ ticketProviderIdentifier: yamlGet(text2, "plane.identifier"),
2171
+ legacyReconcileEnabled: yamlGet(text2, "reconcile.enabled"),
2172
+ legacyReconcileGraceHours: yamlGet(text2, "reconcile.grace_hours"),
2173
+ legacyReconcileAutoReview: yamlGet(text2, "reconcile.auto_review"),
2174
+ legacyScrumGraceHours: yamlGet(text2, "scrum_master.grace_hours"),
2175
+ legacyScrumAutoReview: yamlGet(text2, "scrum_master.auto_review")
2147
2176
  };
2148
2177
  }).filter((value) => Boolean(value));
2149
2178
  }
@@ -2168,10 +2197,39 @@ function templateVersioningScript(ctx) {
2168
2197
  function templateLinkAgentfilesScript(ctx) {
2169
2198
  return templateScript(ctx, "link-agentfiles.sh");
2170
2199
  }
2200
+ function resolveAgentHooksLayer2(ctx) {
2201
+ const override = process.env.PJ_AGENT_HOOKS_LAYER;
2202
+ if (override === "0" || override === "false") return false;
2203
+ 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"));
2206
+ }
2207
+ function evaluateMiseConditionals(template, agentHooksLayer) {
2208
+ const out = [];
2209
+ let depth = 0;
2210
+ let skipDepth = 0;
2211
+ for (const line of template.split("\n")) {
2212
+ const stmt = line.trim();
2213
+ const ifMatch = /^\{%-?\s*if\s+(\w+)\s*-?%\}$/.exec(stmt);
2214
+ if (ifMatch) {
2215
+ depth += 1;
2216
+ const truthy = ifMatch[1] === "agent_hooks_layer" ? agentHooksLayer : false;
2217
+ if (skipDepth === 0 && !truthy) skipDepth = depth;
2218
+ continue;
2219
+ }
2220
+ if (/^\{%-?\s*endif\s*-?%\}$/.test(stmt)) {
2221
+ if (skipDepth === depth) skipDepth = 0;
2222
+ depth = Math.max(0, depth - 1);
2223
+ continue;
2224
+ }
2225
+ if (skipDepth === 0) out.push(line);
2226
+ }
2227
+ return out.join("\n");
2228
+ }
2171
2229
  function renderGeneratedProjectMiseToml(ctx, template) {
2172
2230
  const project = readProjectJson(ctx);
2173
2231
  const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
2174
- return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2232
+ return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2175
2233
  }
2176
2234
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2177
2235
  const targetPath = join11(ctx.repoRoot, "mise.toml");
@@ -2361,6 +2419,23 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
2361
2419
  function readProjectJson(ctx) {
2362
2420
  return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
2363
2421
  }
2422
+ function boolSetting(value, fallback) {
2423
+ if (typeof value === "boolean") return value;
2424
+ if (typeof value === "string") {
2425
+ const normalized = value.trim().toLowerCase();
2426
+ if (["true", "1", "yes", "on"].includes(normalized)) return true;
2427
+ if (["false", "0", "no", "off"].includes(normalized)) return false;
2428
+ }
2429
+ return fallback;
2430
+ }
2431
+ function numberSetting(value, fallback) {
2432
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2433
+ if (typeof value === "string" && value.trim()) {
2434
+ const parsed = Number(value);
2435
+ if (Number.isFinite(parsed)) return parsed;
2436
+ }
2437
+ return fallback;
2438
+ }
2364
2439
  function canonicalProjectJson(ctx) {
2365
2440
  const roles = discoverRoles(ctx.repoRoot);
2366
2441
  const existing = readProjectJson(ctx) ?? {};
@@ -2371,9 +2446,9 @@ function canonicalProjectJson(ctx) {
2371
2446
  workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
2372
2447
  identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
2373
2448
  board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
2374
- board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || ""),
2375
- state: String((existing.ticket_provider?.state ?? "planned") || "planned")
2449
+ state: String((existing.ticket_provider?.state ?? (firstRole?.ticketProviderBoardId ? "linked" : "planned")) || "planned")
2376
2450
  };
2451
+ if (ticketProvider.board_id && ticketProvider.state === "planned") ticketProvider.state = "linked";
2377
2452
  const existingAgents = existing.agents ?? {};
2378
2453
  const discoveredAgents = Object.fromEntries(
2379
2454
  roles.map((role) => [
@@ -2393,13 +2468,27 @@ function canonicalProjectJson(ctx) {
2393
2468
  provisioning_state: existingAgent.provisioning_state
2394
2469
  };
2395
2470
  }
2471
+ const existingAutomation = existing.automation ?? {};
2472
+ const existingReconcile = existingAutomation.reconcile ?? {};
2473
+ const legacyEnabled = roles.find((role) => role.legacyReconcileEnabled)?.legacyReconcileEnabled;
2474
+ const legacyGrace = roles.find((role) => role.legacyReconcileGraceHours || role.legacyScrumGraceHours);
2475
+ const legacyAutoReview = roles.find((role) => role.legacyReconcileAutoReview || role.legacyScrumAutoReview);
2476
+ const automation = {
2477
+ ...existingAutomation,
2478
+ reconcile: {
2479
+ enabled: boolSetting(existingReconcile.enabled, boolSetting(legacyEnabled, false)),
2480
+ grace_hours: numberSetting(existingReconcile.grace_hours, numberSetting(legacyGrace?.legacyReconcileGraceHours || legacyGrace?.legacyScrumGraceHours, 0)),
2481
+ auto_review: boolSetting(existingReconcile.auto_review, boolSetting(legacyAutoReview?.legacyReconcileAutoReview || legacyAutoReview?.legacyScrumAutoReview, true))
2482
+ }
2483
+ };
2396
2484
  return {
2397
2485
  project_name: String(existing.project_name ?? titleCaseSlug(slug)),
2398
2486
  project_description: String(existing.project_description ?? ""),
2399
2487
  project_slug: slug,
2400
2488
  repo_path: ctx.repoRoot,
2401
2489
  ticket_provider: ticketProvider,
2402
- agents
2490
+ agents,
2491
+ automation
2403
2492
  };
2404
2493
  }
2405
2494
  function projectJsonFinding(ctx) {
@@ -2414,7 +2503,7 @@ function projectJsonFinding(ctx) {
2414
2503
  if (!data) {
2415
2504
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json is not valid JSON", details: [], fixable: true };
2416
2505
  }
2417
- for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents"]) {
2506
+ for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents", "automation"]) {
2418
2507
  if (!(key in data)) details.push(`missing key: ${key}`);
2419
2508
  }
2420
2509
  if (data.repo_path !== ctx.repoRoot) details.push(`repo_path should be ${ctx.repoRoot}`);
@@ -2431,9 +2520,18 @@ function projectJsonFinding(ctx) {
2431
2520
  }
2432
2521
  }
2433
2522
  const ticketProvider = data.ticket_provider ?? {};
2434
- for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
2523
+ for (const key of ["type", "workspace", "identifier", "board_id", "state"]) {
2435
2524
  if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
2436
2525
  }
2526
+ if ("board_url" in ticketProvider) details.push("ticket_provider.board_url should be removed; derive it from provider/workspace/board_id");
2527
+ if (!ticketProvider.board_id && roles.some((role) => role.ticketProviderBoardId)) {
2528
+ details.push("ticket_provider.board_id missing even though legacy role.yaml contains a board binding");
2529
+ }
2530
+ const automation = data.automation ?? {};
2531
+ const reconcile = automation.reconcile ?? {};
2532
+ for (const key of ["enabled", "grace_hours", "auto_review"]) {
2533
+ if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2534
+ }
2437
2535
  if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2438
2536
  return {
2439
2537
  id: "sot.project-json",
@@ -2447,7 +2545,7 @@ function projectJsonFinding(ctx) {
2447
2545
  function renderSoul(role) {
2448
2546
  const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
2449
2547
  const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
2450
- const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`reconcile.enabled\` in role.yaml), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
2548
+ const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`automation.reconcile.enabled\` in repo-root \`.project.json\`), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
2451
2549
  const runtimeOwner = role.runtimeOwner || "delorenj";
2452
2550
  return `# ${role.displayName || role.agentId}
2453
2551
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.8",
3
+ "version": "1.2.12",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",