@delorenj/pjangler 1.1.4 → 1.2.1

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 (140) hide show
  1. package/.mise/scripts/link-agentfiles.sh +9 -0
  2. package/.mise/scripts/versioning.sh +236 -0
  3. package/dist/index.js +498 -277
  4. package/dist/mcp-server.js +1706 -163
  5. package/package.json +4 -2
  6. package/templates/commonproject/.mise/scripts/create-plane-project.sh +26 -0
  7. package/templates/commonproject/copier.yml +2 -0
  8. package/templates/commonproject/template/.agents/hooks/README.md +151 -0
  9. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +63 -0
  10. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-journal-write.sh +327 -0
  11. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-recall.sh +180 -0
  12. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-retain.sh +74 -0
  13. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-session-end.sh +57 -0
  14. package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-bank.sh +57 -0
  15. package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-journal.sh +48 -0
  16. package/templates/commonproject/template/.agents/hooks/hooks.master.json +97 -0
  17. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +30 -0
  18. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +51 -0
  19. package/templates/commonproject/template/.agents/hooks/reminder-for-skill-check.sh +20 -0
  20. package/templates/commonproject/template/.agents/hooks/sync.py +618 -0
  21. package/templates/commonproject/template/.agents/local.example.json +15 -0
  22. package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +62 -0
  23. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +110 -0
  24. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +45 -0
  25. package/templates/commonproject/template/mise.toml.jinja +42 -0
  26. package/templates/hermes-agent/.codegraph/daemon.pid +6 -0
  27. package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +10 -0
  28. package/templates/hermes-agent/README.md +16 -14
  29. package/templates/hermes-agent/copier.yml +7 -25
  30. package/templates/hermes-agent/docs/architecture.md +16 -18
  31. package/templates/hermes-agent/docs/fleet-control-plane/README.md +35 -0
  32. package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +253 -0
  33. package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +280 -0
  34. package/templates/hermes-agent/docs/fleet-control-plane/implementation-readiness-report-2026-06-27.md +118 -0
  35. package/templates/hermes-agent/docs/fleet-control-plane/prd.md +141 -0
  36. package/templates/hermes-agent/docs/operations.md +17 -19
  37. package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +17 -13
  38. package/templates/hermes-agent/docs/{scrum-master → sentinel}/README.md +47 -46
  39. package/templates/hermes-agent/docs/{scrum-master → sentinel}/architecture.md +39 -38
  40. package/templates/hermes-agent/docs/{scrum-master → sentinel}/development.md +63 -99
  41. package/templates/hermes-agent/docs/{scrum-master → sentinel}/providers.md +2 -2
  42. package/templates/hermes-agent/docs/sentinel.md +18 -0
  43. package/templates/hermes-agent/install-local.sh +22 -20
  44. package/templates/hermes-agent/runtime-scaffold/README.md +10 -9
  45. package/templates/hermes-agent/scripts/backfill-fleet-sot.sh +12 -21
  46. package/templates/hermes-agent/scripts/fleet-sync.sh +369 -0
  47. package/templates/hermes-agent/scripts/migrate-unify.sh +252 -0
  48. package/templates/hermes-agent/scripts/unify-pm.sh +148 -0
  49. package/templates/hermes-agent/template/.runtime-scaffold/README.md +10 -9
  50. package/templates/hermes-agent/template/.scripts/01-config.sh +2 -3
  51. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -2
  52. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +47 -19
  53. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +71 -27
  54. package/templates/hermes-agent/template/.scripts/30-telegram.sh +3 -3
  55. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +3 -3
  56. package/templates/hermes-agent/template/.scripts/70-systemd.sh +48 -43
  57. package/templates/hermes-agent/template/.scripts/80-registry.sh +2 -2
  58. package/templates/hermes-agent/template/.scripts/99-summary.sh +3 -4
  59. package/templates/hermes-agent/template/.scripts/_lib.sh +4 -19
  60. package/templates/hermes-agent/template/.scripts/checkpoint.sh +36 -0
  61. package/templates/hermes-agent/template/.scripts/config.example.toml +11 -5
  62. package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.sh → heartbeat.sh} +84 -31
  63. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +1 -1
  64. package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -3
  65. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/emit-event.py +5 -5
  66. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-autonomous-review.sh +6 -6
  67. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-close-gate.sh +2 -2
  68. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/autonomous-delegated-review.md +7 -7
  69. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/bloodbank-events.md +3 -3
  70. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/continuous-ticket-orchestration.md +6 -6
  71. package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.prompt.md.jinja → sentinel.prompt.md.jinja} +5 -4
  72. package/templates/hermes-agent/template/SOUL.md.jinja +3 -6
  73. package/templates/hermes-agent/template/hermes.jinja +6 -15
  74. package/templates/hermes-agent/template/role.yaml.jinja +9 -8
  75. package/templates/commonproject/.claude/commands/bmad/bmb/agents/agent-builder.md +0 -14
  76. package/templates/commonproject/.claude/commands/bmad/bmb/agents/module-builder.md +0 -14
  77. package/templates/commonproject/.claude/commands/bmad/bmb/agents/workflow-builder.md +0 -14
  78. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/agent.md +0 -5
  79. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/module.md +0 -5
  80. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/workflow.md +0 -5
  81. package/templates/commonproject/.claude/commands/bmad/bmm/agents/analyst.md +0 -14
  82. package/templates/commonproject/.claude/commands/bmad/bmm/agents/architect.md +0 -14
  83. package/templates/commonproject/.claude/commands/bmad/bmm/agents/dev.md +0 -14
  84. package/templates/commonproject/.claude/commands/bmad/bmm/agents/pm.md +0 -14
  85. package/templates/commonproject/.claude/commands/bmad/bmm/agents/quick-flow-solo-dev.md +0 -14
  86. package/templates/commonproject/.claude/commands/bmad/bmm/agents/sm.md +0 -14
  87. package/templates/commonproject/.claude/commands/bmad/bmm/agents/tea.md +0 -14
  88. package/templates/commonproject/.claude/commands/bmad/bmm/agents/tech-writer.md +0 -14
  89. package/templates/commonproject/.claude/commands/bmad/bmm/agents/ux-designer.md +0 -14
  90. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/check-implementation-readiness.md +0 -5
  91. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/code-review.md +0 -13
  92. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/correct-course.md +0 -13
  93. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-architecture.md +0 -5
  94. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-epics-and-stories.md +0 -5
  95. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-dataflow.md +0 -13
  96. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-diagram.md +0 -13
  97. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-flowchart.md +0 -13
  98. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-wireframe.md +0 -13
  99. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-product-brief.md +0 -5
  100. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-story.md +0 -13
  101. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-ux-design.md +0 -5
  102. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/dev-story.md +0 -13
  103. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/document-project.md +0 -13
  104. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/generate-project-context.md +0 -5
  105. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/prd.md +0 -5
  106. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-dev.md +0 -5
  107. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-spec.md +0 -5
  108. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/research.md +0 -5
  109. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/retrospective.md +0 -13
  110. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-planning.md +0 -13
  111. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-status.md +0 -13
  112. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-atdd.md +0 -13
  113. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-automate.md +0 -13
  114. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-ci.md +0 -13
  115. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-framework.md +0 -13
  116. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-nfr.md +0 -13
  117. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-design.md +0 -13
  118. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-review.md +0 -13
  119. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-trace.md +0 -13
  120. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-init.md +0 -13
  121. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-status.md +0 -13
  122. package/templates/commonproject/.claude/commands/bmad/cis/agents/brainstorming-coach.md +0 -14
  123. package/templates/commonproject/.claude/commands/bmad/cis/agents/creative-problem-solver.md +0 -14
  124. package/templates/commonproject/.claude/commands/bmad/cis/agents/design-thinking-coach.md +0 -14
  125. package/templates/commonproject/.claude/commands/bmad/cis/agents/innovation-strategist.md +0 -14
  126. package/templates/commonproject/.claude/commands/bmad/cis/agents/presentation-master.md +0 -14
  127. package/templates/commonproject/.claude/commands/bmad/cis/agents/storyteller.md +0 -14
  128. package/templates/commonproject/.claude/commands/bmad/cis/workflows/design-thinking.md +0 -13
  129. package/templates/commonproject/.claude/commands/bmad/cis/workflows/innovation-strategy.md +0 -13
  130. package/templates/commonproject/.claude/commands/bmad/cis/workflows/problem-solving.md +0 -13
  131. package/templates/commonproject/.claude/commands/bmad/cis/workflows/storytelling.md +0 -13
  132. package/templates/commonproject/.claude/commands/bmad/core/agents/bmad-master.md +0 -14
  133. package/templates/commonproject/.claude/commands/bmad/core/tasks/index-docs.md +0 -9
  134. package/templates/commonproject/.claude/commands/bmad/core/tasks/shard-doc.md +0 -9
  135. package/templates/commonproject/.claude/commands/bmad/core/workflows/brainstorming.md +0 -5
  136. package/templates/commonproject/.claude/commands/bmad/core/workflows/party-mode.md +0 -5
  137. package/templates/commonproject/.claude/commands/bmad/custom/workflows/ticket-lifecycle.md +0 -5
  138. package/templates/hermes-agent/docs/scrum-master-sentinel.md +0 -17
  139. package/templates/hermes-agent/template/.scripts/75-scrum-master.sh +0 -117
  140. package/templates/hermes-agent/template/.scripts/90-chain-scrum-master.sh +0 -45
package/dist/index.js CHANGED
@@ -1,10 +1,4 @@
1
1
  #!/usr/bin/env node
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
2
 
9
3
  // src/index.ts
10
4
  import { Command as Command3 } from "commander";
@@ -13,12 +7,7 @@ import { Command as Command3 } from "commander";
13
7
  var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
14
8
  var SOUL_TONES = ["direct", "playful", "formal", "terse"];
15
9
  var ROLE_CHOICES = [
16
- { value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship" },
17
- {
18
- value: "scrum-master",
19
- label: "Scrum Master (Ticket Sentinel)",
20
- hint: "continuous ticket sentinel + autonomous delegated review"
21
- },
10
+ { value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
22
11
  { value: "dev", label: "Developer (dev)", hint: "implements tickets" },
23
12
  { value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
24
13
  { value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
@@ -38,10 +27,12 @@ function deriveProfileName(repo, role) {
38
27
 
39
28
  // src/commands/hermes/EnsureTemplateConfig.ts
40
29
  import { homedir, platform } from "node:os";
41
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
42
- import { join, dirname } from "node:path";
30
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
31
+ import { join as join2, dirname as dirname2 } from "node:path";
43
32
 
44
33
  // src/commands/Command.ts
34
+ import { existsSync, writeFileSync, mkdirSync } from "fs";
35
+ import { join, dirname } from "path";
45
36
  var Command = class {
46
37
  context;
47
38
  constructor(context) {
@@ -54,30 +45,24 @@ var Command = class {
54
45
  return this.context.dryRun ? `[DRY RUN] ${message}` : message;
55
46
  }
56
47
  fileExists(filePath) {
57
- const { existsSync: existsSync6 } = __require("fs");
58
- const { join: join8 } = __require("path");
59
- const fullPath = join8(this.context.targetDir, filePath);
60
- return existsSync6(fullPath);
48
+ const fullPath = join(this.context.targetDir, filePath);
49
+ return existsSync(fullPath);
61
50
  }
62
51
  writeFile(filePath, content) {
63
52
  if (this.context.dryRun) {
64
53
  return;
65
54
  }
66
- const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync4 } = __require("fs");
67
- const { join: join8, dirname: dirname5 } = __require("path");
68
- const fullPath = join8(this.context.targetDir, filePath);
69
- const dir = dirname5(fullPath);
70
- mkdirSync4(dir, { recursive: true });
71
- writeFileSync3(fullPath, content);
55
+ const fullPath = join(this.context.targetDir, filePath);
56
+ const dir = dirname(fullPath);
57
+ mkdirSync(dir, { recursive: true });
58
+ writeFileSync(fullPath, content);
72
59
  }
73
60
  createDirectory(dirPath) {
74
61
  if (this.context.dryRun) {
75
62
  return;
76
63
  }
77
- const { mkdirSync: mkdirSync4 } = __require("fs");
78
- const { join: join8 } = __require("path");
79
- const fullPath = join8(this.context.targetDir, dirPath);
80
- mkdirSync4(fullPath, { recursive: true });
64
+ const fullPath = join(this.context.targetDir, dirPath);
65
+ mkdirSync(fullPath, { recursive: true });
81
66
  }
82
67
  };
83
68
 
@@ -86,26 +71,26 @@ function resolveTemplateConfigPath() {
86
71
  const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
87
72
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
88
73
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
89
- const base = xdg && xdg.length ? xdg : join(homedir(), ".config");
90
- return join(base, "hermes-agent-template", "config.toml");
74
+ const base = xdg && xdg.length ? xdg : join2(homedir(), ".config");
75
+ return join2(base, "hermes-agent-template", "config.toml");
91
76
  }
92
77
  function detectHermesBin(home) {
93
78
  const candidates = [
94
- join(home, "code", "hermes-agent", "venv", "bin", "hermes"),
95
- join(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
96
- join(home, ".local", "bin", "hermes")
79
+ join2(home, "code", "hermes-agent", "venv", "bin", "hermes"),
80
+ join2(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
81
+ join2(home, ".local", "bin", "hermes")
97
82
  ];
98
83
  for (const c of candidates) {
99
- if (existsSync(c)) return c;
84
+ if (existsSync2(c)) return c;
100
85
  }
101
86
  return candidates[0];
102
87
  }
103
88
  function renderHostConfig() {
104
89
  const home = homedir();
105
90
  const hermesBin = detectHermesBin(home);
106
- const hermesRepo = join(home, "code", "hermes-agent");
107
- const scaffoldDir = join(home, "code", "hermes-agent-template", "runtime-scaffold");
108
- const skillsDir = join(home, ".agents", "skills");
91
+ const hermesRepo = join2(home, "code", "hermes-agent");
92
+ const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
93
+ const skillsDir = join2(home, ".agents", "skills");
109
94
  return `# hermes-agent-template \u2014 host configuration
110
95
  # Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
111
96
  #
@@ -149,7 +134,7 @@ var EnsureTemplateConfig = class extends Command {
149
134
  const ctx = this.context;
150
135
  const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
151
136
  const path = resolveTemplateConfigPath();
152
- const exists = existsSync(path);
137
+ const exists = existsSync2(path);
153
138
  if (exists && !force) {
154
139
  console.log(`\u2713 Config present: ${path}`);
155
140
  return { success: true, message: "" };
@@ -159,8 +144,8 @@ var EnsureTemplateConfig = class extends Command {
159
144
  return { success: true, message: "" };
160
145
  }
161
146
  try {
162
- mkdirSync(dirname(path), { recursive: true });
163
- writeFileSync(path, renderHostConfig());
147
+ mkdirSync2(dirname2(path), { recursive: true });
148
+ writeFileSync2(path, renderHostConfig());
164
149
  } catch (err) {
165
150
  const msg = err instanceof Error ? err.message : String(err);
166
151
  return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
@@ -577,12 +562,12 @@ var NodeRecipe = class extends Recipe {
577
562
  };
578
563
 
579
564
  // src/commands/hermes/PromptForAgentConfig.ts
580
- import { basename, join as join2 } from "node:path";
565
+ import { basename, join as join3 } from "node:path";
581
566
  import { readFileSync } from "node:fs";
582
567
  import * as p from "@clack/prompts";
583
568
  function detectTicketProvider(targetDir) {
584
569
  try {
585
- const t = JSON.parse(readFileSync(join2(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
570
+ const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
586
571
  return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
587
572
  } catch {
588
573
  return void 0;
@@ -601,7 +586,6 @@ var PromptForAgentConfig = class extends Command {
601
586
  ctx.modelProvider ??= "";
602
587
  ctx.modelName ??= "";
603
588
  ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
604
- ctx.withScrumMaster ??= false;
605
589
  ctx.skipTelegram ??= true;
606
590
  ctx.skipEmail ??= true;
607
591
  ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
@@ -647,14 +631,6 @@ var PromptForAgentConfig = class extends Command {
647
631
  if (p.isCancel(answer)) return this.cancelled();
648
632
  ctx.ticketProvider = answer;
649
633
  }
650
- if (ctx.role === "pm" && ctx.withScrumMaster === void 0) {
651
- const answer = await p.confirm({
652
- message: "Also provision the paired Scrum Master (Ticket Sentinel) for this repo?",
653
- initialValue: true
654
- });
655
- if (p.isCancel(answer)) return this.cancelled();
656
- ctx.withScrumMaster = answer === true;
657
- }
658
634
  if (!ctx.agentPurpose) {
659
635
  const answer = await p.text({
660
636
  message: "One-line purpose",
@@ -727,21 +703,21 @@ var PromptForAgentConfig = class extends Command {
727
703
  // src/commands/hermes/RunCopierTemplate.ts
728
704
  import { spawnSync } from "node:child_process";
729
705
  import { homedir as homedir2 } from "node:os";
730
- import { join as join3, dirname as dirname2 } from "node:path";
731
- import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
706
+ import { join as join4, dirname as dirname3 } from "node:path";
707
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
732
708
  import { fileURLToPath } from "node:url";
733
709
  import * as p2 from "@clack/prompts";
734
710
  function resolveVendoredTemplate(name) {
735
711
  let dir;
736
712
  try {
737
- dir = dirname2(fileURLToPath(import.meta.url));
713
+ dir = dirname3(fileURLToPath(import.meta.url));
738
714
  } catch {
739
715
  return void 0;
740
716
  }
741
717
  for (let i = 0; i < 8; i++) {
742
- const candidate = join3(dir, "templates", name);
743
- if (existsSync2(join3(candidate, "copier.yml"))) return candidate;
744
- const parent = dirname2(dir);
718
+ const candidate = join4(dir, "templates", name);
719
+ if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
720
+ const parent = dirname3(dir);
745
721
  if (parent === dir) break;
746
722
  dir = parent;
747
723
  }
@@ -753,14 +729,13 @@ var RunCopierTemplate = class extends Command {
753
729
  const { targetRepo, role, agentPurpose, soulTone, modelProvider, modelName } = ctx;
754
730
  const ticketProvider = ctx.ticketProvider ?? "plane";
755
731
  const profileName = ctx.profileName ?? (targetRepo && role ? deriveProfileName(targetRepo, role) : void 0);
756
- const withScrumMaster = role === "pm" && ctx.withScrumMaster === true;
757
732
  if (!targetRepo || !role) {
758
733
  return {
759
734
  success: false,
760
735
  message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
761
736
  };
762
737
  }
763
- const roleDir = join3(ctx.targetDir, "agents", "hermes", role);
738
+ const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
764
739
  ctx.roleDir = roleDir;
765
740
  ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
766
741
  const which = spawnSync("which", ["copier"], { encoding: "utf8" });
@@ -770,7 +745,7 @@ var RunCopierTemplate = class extends Command {
770
745
  message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
771
746
  };
772
747
  }
773
- if (existsSync2(join3(roleDir, "role.yaml")) && !ctx.force) {
748
+ if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
774
749
  if (ctx.yes) {
775
750
  ctx.force = true;
776
751
  } else {
@@ -797,9 +772,9 @@ var RunCopierTemplate = class extends Command {
797
772
  SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
798
773
  SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
799
774
  };
800
- const LOCAL_TEMPLATE = join3(homedir2(), "code", "hermes-agent-template");
775
+ const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
801
776
  const vendored = resolveVendoredTemplate("hermes-agent");
802
- const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync2(join3(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
777
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join4(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
803
778
  const args = [
804
779
  "copy",
805
780
  templateSrc,
@@ -820,8 +795,6 @@ var RunCopierTemplate = class extends Command {
820
795
  `soul_tone=${soulTone ?? "direct"}`,
821
796
  "--data",
822
797
  `ticket_provider=${ticketProvider}`,
823
- "--data",
824
- `with_scrum_master=${withScrumMaster}`,
825
798
  "--trust",
826
799
  "--vcs-ref=HEAD"
827
800
  ];
@@ -832,7 +805,7 @@ var RunCopierTemplate = class extends Command {
832
805
  message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
833
806
  };
834
807
  }
835
- mkdirSync2(join3(ctx.targetDir, "agents", "hermes"), { recursive: true });
808
+ mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
836
809
  const spinner4 = p2.spinner();
837
810
  spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
838
811
  const result = spawnSync("copier", args, {
@@ -857,8 +830,8 @@ var RunCopierTemplate = class extends Command {
857
830
 
858
831
  // src/commands/hermes/WireTelegram.ts
859
832
  import { spawnSync as spawnSync2 } from "node:child_process";
860
- import { join as join4 } from "node:path";
861
- import { existsSync as existsSync3, unlinkSync } from "node:fs";
833
+ import { join as join5 } from "node:path";
834
+ import { existsSync as existsSync4, unlinkSync } from "node:fs";
862
835
  import * as p3 from "@clack/prompts";
863
836
  var WireTelegram = class extends Command {
864
837
  async invoke() {
@@ -946,15 +919,15 @@ var WireTelegram = class extends Command {
946
919
  if (p3.isCancel(allowedAnswer)) {
947
920
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
948
921
  }
949
- const script = join4(roleDir, ".scripts", "30-telegram.sh");
950
- if (!existsSync3(script)) {
922
+ const script = join5(roleDir, ".scripts", "30-telegram.sh");
923
+ if (!existsSync4(script)) {
951
924
  return {
952
925
  success: false,
953
926
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
954
927
  };
955
928
  }
956
- const marker = join4(roleDir, ".scripts", ".done-30-telegram");
957
- if (existsSync3(marker)) unlinkSync(marker);
929
+ const marker = join5(roleDir, ".scripts", ".done-30-telegram");
930
+ if (existsSync4(marker)) unlinkSync(marker);
958
931
  const spinner4 = p3.spinner();
959
932
  spinner4.start("Verifying token + wiring profile");
960
933
  const result = spawnSync2("bash", [script], {
@@ -981,8 +954,8 @@ function cap(s) {
981
954
 
982
955
  // src/commands/hermes/WireEmail.ts
983
956
  import { spawnSync as spawnSync3 } from "node:child_process";
984
- import { join as join5 } from "node:path";
985
- import { existsSync as existsSync4, unlinkSync as unlinkSync2 } from "node:fs";
957
+ import { join as join6 } from "node:path";
958
+ import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
986
959
  import * as p4 from "@clack/prompts";
987
960
  var WireEmail = class extends Command {
988
961
  async invoke() {
@@ -997,8 +970,8 @@ var WireEmail = class extends Command {
997
970
  if (!targetRepo || !role || !roleDir) {
998
971
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
999
972
  }
1000
- const script = join5(roleDir, ".scripts", "50-email.sh");
1001
- if (!existsSync4(script)) {
973
+ const script = join6(roleDir, ".scripts", "50-email.sh");
974
+ if (!existsSync5(script)) {
1002
975
  return { success: false, message: `\u2717 ${script} not found` };
1003
976
  }
1004
977
  let token = process.env.CF_EMAIL_ROUTING_TOKEN;
@@ -1060,8 +1033,8 @@ var WireEmail = class extends Command {
1060
1033
  }
1061
1034
  }
1062
1035
  }
1063
- const marker = join5(roleDir, ".scripts", ".done-50-email");
1064
- if (existsSync4(marker)) unlinkSync2(marker);
1036
+ const marker = join6(roleDir, ".scripts", ".done-50-email");
1037
+ if (existsSync5(marker)) unlinkSync2(marker);
1065
1038
  const spinner4 = p4.spinner();
1066
1039
  spinner4.start("Creating Cloudflare Email Routing rule");
1067
1040
  const result = spawnSync3("bash", [script], {
@@ -1090,7 +1063,7 @@ var PrintHermesSummary = class extends Command {
1090
1063
  const email = `${targetRepo}-${role}@delo.sh`;
1091
1064
  const gw = `hermes-${agentId}-gateway.service`;
1092
1065
  const csm = `hermes-${agentId}-consumer.service`;
1093
- const ckpt = `hermes-${agentId}-checkpoint.timer`;
1066
+ const hb = `hermes-${agentId}-heartbeat.timer`;
1094
1067
  const lines = [];
1095
1068
  lines.push(`agent_id ${agentId}`);
1096
1069
  lines.push(`role dir ${ctx.roleDir}`);
@@ -1100,7 +1073,7 @@ var PrintHermesSummary = class extends Command {
1100
1073
  lines.push("");
1101
1074
  lines.push("Start daemons:");
1102
1075
  lines.push(` systemctl --user start ${csm}`);
1103
- lines.push(` systemctl --user start ${ckpt}`);
1076
+ lines.push(` systemctl --user start ${hb}`);
1104
1077
  if (!skipTelegram) {
1105
1078
  lines.push(` systemctl --user start ${gw}`);
1106
1079
  } else {
@@ -1147,6 +1120,193 @@ var HermesAgentRecipe = class extends Recipe {
1147
1120
  }
1148
1121
  };
1149
1122
 
1123
+ // src/commands/AgentHooksCommands.ts
1124
+ import { homedir as homedir3 } from "node:os";
1125
+ import { join as join7, dirname as dirname4 } from "node:path";
1126
+ import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
1127
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1128
+ function resolveTemplateRoot() {
1129
+ const candidates = [];
1130
+ if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
1131
+ candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
1132
+ }
1133
+ try {
1134
+ let dir = dirname4(fileURLToPath2(import.meta.url));
1135
+ for (let i = 0; i < 8; i++) {
1136
+ candidates.push(join7(dir, "templates", "commonproject", "template"));
1137
+ const parent = dirname4(dir);
1138
+ if (parent === dir) break;
1139
+ dir = parent;
1140
+ }
1141
+ } catch {
1142
+ }
1143
+ candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1144
+ for (const c of candidates) {
1145
+ if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
1146
+ }
1147
+ throw new Error(
1148
+ "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
1149
+ );
1150
+ }
1151
+ var CopyAgentHooksTree = class extends Command {
1152
+ async invoke() {
1153
+ let templateRoot;
1154
+ try {
1155
+ templateRoot = resolveTemplateRoot();
1156
+ } catch (e) {
1157
+ return { success: false, message: `\u26A0\uFE0F ${e.message}` };
1158
+ }
1159
+ const items = [
1160
+ { rel: ".agents/hooks", dir: true },
1161
+ { rel: ".agents/local.example.json", dir: false },
1162
+ { rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
1163
+ { rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
1164
+ { rel: ".mise/scripts/hindsight-setup.sh", dir: false }
1165
+ ];
1166
+ const created = [];
1167
+ const skipped = [];
1168
+ for (const { rel, dir } of items) {
1169
+ const src = join7(templateRoot, rel);
1170
+ const dest = join7(this.context.targetDir, rel);
1171
+ if (!existsSync6(src)) continue;
1172
+ if (existsSync6(dest) && !this.context.force) {
1173
+ skipped.push(rel);
1174
+ continue;
1175
+ }
1176
+ if (!this.context.dryRun) {
1177
+ mkdirSync4(dirname4(dest), { recursive: true });
1178
+ cpSync(src, dest, { recursive: dir, force: true });
1179
+ }
1180
+ created.push(rel);
1181
+ }
1182
+ const verb = this.context.dryRun ? "Would copy" : "Copied";
1183
+ const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
1184
+ return {
1185
+ success: created.length > 0,
1186
+ message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
1187
+ };
1188
+ }
1189
+ };
1190
+ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1191
+ static MARKER = "# pjangler:agent-hooks";
1192
+ static CR = "{{config_root}}";
1193
+ // mise's own runtime var — emitted literally
1194
+ async invoke() {
1195
+ const misePath = join7(this.context.targetDir, "mise.toml");
1196
+ if (!existsSync6(misePath)) {
1197
+ return {
1198
+ success: false,
1199
+ message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
1200
+ };
1201
+ }
1202
+ let content = readFileSync2(misePath, "utf8");
1203
+ if (content.includes(_WireMiseAgentHooks.MARKER)) {
1204
+ return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
1205
+ }
1206
+ const cr = _WireMiseAgentHooks.CR;
1207
+ const enterAdds = [
1208
+ ` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
1209
+ ` "${cr}/.agents/hooks/sync.py --install --quiet",`
1210
+ ].join("\n");
1211
+ const leaveBlock = [
1212
+ "leave = [",
1213
+ ` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
1214
+ ` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
1215
+ "]"
1216
+ ].join("\n");
1217
+ let wiredHooks = false;
1218
+ const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
1219
+ if (enterRe.test(content)) {
1220
+ content = content.replace(enterRe, (_m, head, close) => {
1221
+ const sep = /[,[]\s*$/.test(head) ? "" : ",";
1222
+ return `${head}${sep}
1223
+ ${enterAdds}${close}`;
1224
+ });
1225
+ const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
1226
+ if (leaveRe.test(content)) {
1227
+ content = content.replace(leaveRe, (_m, head, close) => {
1228
+ const sep = /[,[]\s*$/.test(head) ? "" : ",";
1229
+ return `${head}${sep}
1230
+ "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
1231
+ "${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
1232
+ });
1233
+ } else {
1234
+ content = content.replace(enterRe, (m) => `${m}
1235
+ ${leaveBlock}`);
1236
+ }
1237
+ wiredHooks = true;
1238
+ }
1239
+ const appended = [
1240
+ "",
1241
+ _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
1242
+ "[[watch_files]]",
1243
+ 'patterns = [".agents/hooks/hooks.master.json"]',
1244
+ 'task = "hooks-sync"',
1245
+ "",
1246
+ "[tasks.hooks-sync]",
1247
+ 'description = "Fan out hooks.master.json to each agent CLI (claude/codex/kimi/hermes)"',
1248
+ `run = "${cr}/.agents/hooks/sync.py --install"`,
1249
+ "",
1250
+ "[tasks.hooks-check]",
1251
+ 'description = "Drift gate: verify generated hook configs match hooks.master.json"',
1252
+ `run = "${cr}/.agents/hooks/sync.py --check"`,
1253
+ "",
1254
+ "[tasks.hooks-uninstall]",
1255
+ 'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
1256
+ `run = "${cr}/.agents/hooks/sync.py --uninstall"`,
1257
+ "",
1258
+ "[tasks.link-project-skills-to-clis]",
1259
+ 'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
1260
+ `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
1261
+ "",
1262
+ "[tasks.unlink-project-skills-from-clis]",
1263
+ 'description = "Remove project skill symlinks from shared per-CLI dirs"',
1264
+ `run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
1265
+ "",
1266
+ "[tasks.skills-relink]",
1267
+ 'description = "Re-fan the project skill set to all CLIs"',
1268
+ `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
1269
+ "",
1270
+ "[tasks.hindsight-setup]",
1271
+ `description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
1272
+ `run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
1273
+ "",
1274
+ _WireMiseAgentHooks.MARKER + ":end",
1275
+ ""
1276
+ ].join("\n");
1277
+ content = content.replace(/\n*$/, "\n") + appended;
1278
+ if (!this.context.dryRun) writeFileSync3(misePath, content);
1279
+ if (wiredHooks) {
1280
+ return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
1281
+ }
1282
+ return {
1283
+ success: true,
1284
+ message: this.formatMessage(
1285
+ `\u2705 Added agent-hooks tasks to mise.toml.
1286
+ \u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
1287
+ enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
1288
+ leave += "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh", "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
1289
+ )
1290
+ };
1291
+ }
1292
+ };
1293
+
1294
+ // src/recipes/AgentHooksRecipe.ts
1295
+ var AgentHooksRecipe = class extends Recipe {
1296
+ constructor(context) {
1297
+ super(context);
1298
+ this.addIngredient(CopyAgentHooksTree).addIngredient(WireMiseAgentHooks);
1299
+ }
1300
+ printNextSteps() {
1301
+ console.log("\u{1FA9D} Agent-hooks layer installed!");
1302
+ console.log(" Next steps:");
1303
+ console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
1304
+ console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
1305
+ console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
1306
+ console.log(` 4. If you run a global agent system: echo '{"skills":{"defer_to_global":true}}' > .agents/local.json`);
1307
+ }
1308
+ };
1309
+
1150
1310
  // src/utils/registry.ts
1151
1311
  var RECIPE_REGISTRY = {
1152
1312
  mise: {
@@ -1180,9 +1340,27 @@ var RECIPE_REGISTRY = {
1180
1340
  "WireEmail",
1181
1341
  "PrintHermesSummary"
1182
1342
  ]
1343
+ },
1344
+ "agent-hooks": {
1345
+ name: "agent-hooks",
1346
+ description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
1347
+ class: AgentHooksRecipe,
1348
+ commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
1183
1349
  }
1184
1350
  };
1185
1351
  var COMMAND_REGISTRY = {
1352
+ CopyAgentHooksTree: {
1353
+ name: "CopyAgentHooksTree",
1354
+ description: "Copy the generic agent-hooks tree (hooks SSOT + sync engine + scripts) from the CommonProject template",
1355
+ group: "agent-hooks",
1356
+ class: CopyAgentHooksTree
1357
+ },
1358
+ WireMiseAgentHooks: {
1359
+ name: "WireMiseAgentHooks",
1360
+ description: "Merge agent-hooks enter/leave + tasks into an existing mise.toml (idempotent)",
1361
+ group: "agent-hooks",
1362
+ class: WireMiseAgentHooks
1363
+ },
1186
1364
  AddDockerfile: {
1187
1365
  name: "AddDockerfile",
1188
1366
  description: "Create Dockerfile for containerization",
@@ -1261,10 +1439,10 @@ function createRecipe(name, context) {
1261
1439
  }
1262
1440
 
1263
1441
  // src/parity/index.ts
1264
- import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2, chmodSync, copyFileSync } from "node:fs";
1265
- import { dirname as dirname3, join as join6, relative, resolve } from "node:path";
1266
- import { fileURLToPath as fileURLToPath2 } from "node:url";
1267
- import { homedir as homedir3 } from "node:os";
1442
+ import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
1443
+ import { dirname as dirname5, join as join8, relative, resolve } from "node:path";
1444
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1445
+ import { homedir as homedir4 } from "node:os";
1268
1446
  import { spawnSync as spawnSync4 } from "node:child_process";
1269
1447
  var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
1270
1448
  # agent files to the main AGENTS.md file.
@@ -1312,12 +1490,12 @@ description = "Force every versioned file up to the highest version"
1312
1490
  run = "{{config_root}}/.mise/scripts/versioning.sh sync"
1313
1491
  # <<< mise-versioning <<<`;
1314
1492
  function resolvePjanglerRoot() {
1315
- let dir = dirname3(fileURLToPath2(import.meta.url));
1316
- while (dir !== dirname3(dir)) {
1317
- if (existsSync5(join6(dir, "package.json")) && existsSync5(join6(dir, "templates", "commonproject", "copier.yml"))) {
1493
+ let dir = dirname5(fileURLToPath3(import.meta.url));
1494
+ while (dir !== dirname5(dir)) {
1495
+ if (existsSync7(join8(dir, "package.json")) && existsSync7(join8(dir, "templates", "commonproject", "copier.yml"))) {
1318
1496
  return dir;
1319
1497
  }
1320
- dir = dirname3(dir);
1498
+ dir = dirname5(dir);
1321
1499
  }
1322
1500
  throw new Error("Unable to resolve pjangler root");
1323
1501
  }
@@ -1325,17 +1503,17 @@ function normalizeNewlines(value) {
1325
1503
  return value.replace(/\r\n/g, "\n");
1326
1504
  }
1327
1505
  function readText(path) {
1328
- return normalizeNewlines(readFileSync2(path, "utf8"));
1506
+ return normalizeNewlines(readFileSync3(path, "utf8"));
1329
1507
  }
1330
1508
  function safeReadText(path) {
1331
- return existsSync5(path) ? readText(path) : null;
1509
+ return existsSync7(path) ? readText(path) : null;
1332
1510
  }
1333
1511
  function ensureParent(path) {
1334
- mkdirSync3(dirname3(path), { recursive: true });
1512
+ mkdirSync5(dirname5(path), { recursive: true });
1335
1513
  }
1336
1514
  function writeText(path, content) {
1337
1515
  ensureParent(path);
1338
- writeFileSync2(path, content);
1516
+ writeFileSync4(path, content);
1339
1517
  }
1340
1518
  function tryParseJson(text3) {
1341
1519
  if (!text3) return null;
@@ -1352,7 +1530,7 @@ function titleCaseSlug(slug) {
1352
1530
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1353
1531
  }
1354
1532
  function readSymlinkTarget(path) {
1355
- if (!existsSync5(path)) return null;
1533
+ if (!existsSync7(path)) return null;
1356
1534
  try {
1357
1535
  return readlinkSync(path);
1358
1536
  } catch {
@@ -1360,7 +1538,7 @@ function readSymlinkTarget(path) {
1360
1538
  }
1361
1539
  }
1362
1540
  function ensureSymlink(path, target, dryRun) {
1363
- if (existsSync5(path)) {
1541
+ if (existsSync7(path)) {
1364
1542
  const stat = lstatSync(path);
1365
1543
  if (stat.isSymbolicLink()) {
1366
1544
  const current = readSymlinkTarget(path);
@@ -1376,6 +1554,29 @@ function ensureSymlink(path, target, dryRun) {
1376
1554
  if (!dryRun) symlinkSync(target, path);
1377
1555
  return { changed: true };
1378
1556
  }
1557
+ function bootstrapAgentsFile(repoRoot, dryRun) {
1558
+ const agentsPath = join8(repoRoot, "AGENTS.md");
1559
+ if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
1560
+ for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1561
+ const source = join8(repoRoot, file);
1562
+ if (!existsSync7(source)) continue;
1563
+ const stat = lstatSync(source);
1564
+ if (stat.isSymbolicLink()) continue;
1565
+ if (stat.isFile()) {
1566
+ if (!dryRun) renameSync(source, agentsPath);
1567
+ return { changedFiles: [agentsPath], details: [`Moved ${file} to AGENTS.md before wiring agent-file symlinks`] };
1568
+ }
1569
+ return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
1570
+ }
1571
+ const readmePath = join8(repoRoot, "README.md");
1572
+ if (existsSync7(readmePath)) {
1573
+ const stat = lstatSync(readmePath);
1574
+ if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
1575
+ if (!dryRun) copyFileSync(readmePath, agentsPath);
1576
+ return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
1577
+ }
1578
+ return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
1579
+ }
1379
1580
  function yamlGet(text3, keyPath) {
1380
1581
  const parts = keyPath.split(".");
1381
1582
  const lines = text3.split("\n");
@@ -1407,12 +1608,12 @@ function yamlGet(text3, keyPath) {
1407
1608
  return "";
1408
1609
  }
1409
1610
  function discoverRoles(repoRoot) {
1410
- const rolesDir = join6(repoRoot, "agents", "hermes");
1411
- if (!existsSync5(rolesDir)) return [];
1611
+ const rolesDir = join8(repoRoot, "agents", "hermes");
1612
+ if (!existsSync7(rolesDir)) return [];
1412
1613
  return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
1413
- const roleDir = join6(rolesDir, entry.name);
1414
- const roleYamlPath = join6(roleDir, "role.yaml");
1415
- if (!existsSync5(roleYamlPath)) return null;
1614
+ const roleDir = join8(rolesDir, entry.name);
1615
+ const roleYamlPath = join8(roleDir, "role.yaml");
1616
+ if (!existsSync7(roleYamlPath)) return null;
1416
1617
  const text3 = readText(roleYamlPath);
1417
1618
  const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
1418
1619
  return {
@@ -1436,7 +1637,7 @@ function discoverRoles(repoRoot) {
1436
1637
  }).filter((value) => Boolean(value));
1437
1638
  }
1438
1639
  function registryPath(homeDir) {
1439
- return join6(homeDir, ".hermes", "agents-registry.yaml");
1640
+ return join8(homeDir, ".hermes", "agents-registry.yaml");
1440
1641
  }
1441
1642
  function systemctlUser(args) {
1442
1643
  const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -1446,12 +1647,19 @@ function systemctlUser(args) {
1446
1647
  stderr: result.stderr.trim()
1447
1648
  };
1448
1649
  }
1650
+ function templateScript(ctx, name) {
1651
+ const source = join8(ctx.pjanglerRoot, ".mise", "scripts", name);
1652
+ return existsSync7(source) ? readText(source) : void 0;
1653
+ }
1449
1654
  function templateVersioningScript(ctx) {
1450
- return readText(join6(ctx.pjanglerRoot, ".mise", "scripts", "versioning.sh"));
1655
+ return templateScript(ctx, "versioning.sh");
1656
+ }
1657
+ function templateLinkAgentfilesScript(ctx) {
1658
+ return templateScript(ctx, "link-agentfiles.sh");
1451
1659
  }
1452
1660
  function templateVersionFilesConf(ctx, repoRoot) {
1453
- const packageJson = join6(repoRoot, "package.json");
1454
- return existsSync5(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";
1661
+ const packageJson = join8(repoRoot, "package.json");
1662
+ return existsSync7(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
1455
1663
  }
1456
1664
  function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
1457
1665
  if (startMarker.test(text3)) {
@@ -1470,29 +1678,64 @@ ${text3.slice(match.index)}`;
1470
1678
  ${block}
1471
1679
  `;
1472
1680
  }
1473
- function upsertLinkAgentfilesBlock(text3) {
1681
+ var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
1682
+ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/hermes"];
1683
+ function requiredMisePathEntries(ctx) {
1684
+ const required = [...BASE_MISE_PATH_ENTRIES];
1685
+ for (const candidate of CONDITIONAL_HERMES_PATHS) {
1686
+ if (existsSync7(join8(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1687
+ }
1688
+ return required;
1689
+ }
1690
+ function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
1691
+ const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
1692
+ const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1693
+ if (!envMatch || typeof envMatch.index !== "number") {
1694
+ return `[env]
1695
+ ${render(required)}
1696
+
1697
+ ${text3.replace(/^\s+/, "")}`;
1698
+ }
1699
+ const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
1700
+ const section = envMatch[2];
1701
+ const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
1702
+ const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
1703
+ if (!pathLine) {
1704
+ return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
1705
+ }
1706
+ const current = [...pathLine[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
1707
+ const merged = [...current];
1708
+ for (const value of required) {
1709
+ if (!merged.includes(value)) merged.push(value);
1710
+ }
1711
+ const nextLine = render(merged);
1712
+ if (pathLine[0] === nextLine) return text3;
1713
+ return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
1714
+ }
1715
+ function upsertLinkAgentfilesBlock(text3, ctx) {
1716
+ const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
1474
1717
  const existing = /# This block will handle the linking of[\s\S]*?\[tasks\.link-agentfiles\][\s\S]*?run = "\{\{config_root\}\}\/\.mise\/scripts\/link-agentfiles\.sh"/;
1475
- if (existing.test(text3)) {
1476
- return text3.replace(existing, LINK_AGENTFILES_BLOCK);
1718
+ if (existing.test(withPath)) {
1719
+ return withPath.replace(existing, LINK_AGENTFILES_BLOCK);
1477
1720
  }
1478
- const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
1721
+ const versioningIndex = withPath.indexOf("# >>> mise-versioning >>>");
1479
1722
  if (versioningIndex >= 0) {
1480
- return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${LINK_AGENTFILES_BLOCK}
1723
+ return `${withPath.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${LINK_AGENTFILES_BLOCK}
1481
1724
 
1482
- ${text3.slice(versioningIndex)}`;
1725
+ ${withPath.slice(versioningIndex)}`;
1483
1726
  }
1484
- return `${text3.replace(/\s*$/, "")}
1727
+ return `${withPath.replace(/\s*$/, "")}
1485
1728
 
1486
1729
  ${LINK_AGENTFILES_BLOCK}
1487
1730
  `;
1488
1731
  }
1489
1732
  function readProjectJson(ctx) {
1490
- return tryParseJson(safeReadText(join6(ctx.repoRoot, ".project.json")));
1733
+ return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
1491
1734
  }
1492
1735
  function canonicalProjectJson(ctx) {
1493
1736
  const roles = discoverRoles(ctx.repoRoot);
1494
1737
  const existing = readProjectJson(ctx) ?? {};
1495
- const slug = String(existing.project_slug ?? slugifyRepoName(dirname3(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
1738
+ const slug = String(existing.project_slug ?? slugifyRepoName(dirname5(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
1496
1739
  const firstRole = roles[0];
1497
1740
  const ticketProvider = {
1498
1741
  type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
@@ -1519,12 +1762,12 @@ function canonicalProjectJson(ctx) {
1519
1762
  };
1520
1763
  }
1521
1764
  function projectJsonFinding(ctx) {
1522
- const projectPath = join6(ctx.repoRoot, ".project.json");
1523
- const planeJsonPath = join6(ctx.repoRoot, ".plane.json");
1765
+ const projectPath = join8(ctx.repoRoot, ".project.json");
1766
+ const planeJsonPath = join8(ctx.repoRoot, ".plane.json");
1524
1767
  const details = [];
1525
1768
  const data = readProjectJson(ctx);
1526
1769
  const roles = discoverRoles(ctx.repoRoot);
1527
- if (!existsSync5(projectPath)) {
1770
+ if (!existsSync7(projectPath)) {
1528
1771
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
1529
1772
  }
1530
1773
  if (!data) {
@@ -1550,7 +1793,7 @@ function projectJsonFinding(ctx) {
1550
1793
  for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
1551
1794
  if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
1552
1795
  }
1553
- if (existsSync5(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
1796
+ if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
1554
1797
  return {
1555
1798
  id: "sot.project-json",
1556
1799
  title: "Canonical .project.json",
@@ -1562,8 +1805,8 @@ function projectJsonFinding(ctx) {
1562
1805
  }
1563
1806
  function renderSoul(role) {
1564
1807
  const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
1565
- const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : role.role === "scrum-master" ? "Operational, skeptical, and schedule-aware. Prefer explicit next actions, evidence, and status transitions." : "Direct and brief.";
1566
- 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.` : role.role === "scrum-master" ? `You own the continuous-ticket sentinel. You watch the ticket board, enforce workflow policy, and keep work moving without inventing requirements.` : `You operate as the ${role.role} agent for this repo.`;
1808
+ 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.";
1809
+ 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.`;
1567
1810
  const runtimeOwner = role.runtimeOwner || "delorenj";
1568
1811
  return `# ${role.displayName || role.agentId}
1569
1812
 
@@ -1583,7 +1826,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
1583
1826
 
1584
1827
  ## Scope
1585
1828
 
1586
- You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME resolves through the named profile \`${role.profileName || role.agentId}\`, which is symlinked to the runtime submodule at \`./runtime/\` (repo \`${runtimeOwner}/${role.runtimeRepo}\`). Your \`config.yaml\` inherits shared non-secret defaults from the fleet default profile; secrets, SOUL, memories, skills, sessions, gateway state, and runtime files remain local to this profile.
1829
+ You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the runtime submodule at \`./runtime/\` (repo \`${runtimeOwner}/${role.runtimeRepo}\`), which \`~/.hermes/profiles/${role.profileName || role.agentId}\` symlinks to (so \`--profile\` invocations resolve here too); Hermes loads its \`config.yaml\` directly. Secrets, SOUL, memories, skills, sessions, gateway state, and runtime files all live local to that runtime.
1587
1830
 
1588
1831
  ## Tone
1589
1832
 
@@ -1600,8 +1843,7 @@ Your memory is the submodule at \`./runtime/memories/\`. Use durable memory deli
1600
1843
  }
1601
1844
  function renderHermesWrapper(role) {
1602
1845
  return `#!/usr/bin/env bash
1603
- # Launcher for ${role.agentId}. Resolves HERMES_HOME through the fleet profile
1604
- # when available, then falls back to the local runtime submodule.
1846
+ # Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime submodule.
1605
1847
 
1606
1848
  set -euo pipefail
1607
1849
 
@@ -1620,12 +1862,7 @@ CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
1620
1862
 
1621
1863
  FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
1622
1864
  PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
1623
- PROFILE_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
1624
- if [[ -d "$PROFILE_HOME" ]]; then
1625
- HERMES_HOME="$PROFILE_HOME"
1626
- else
1627
- HERMES_HOME="$RUNTIME_HOME"
1628
- fi
1865
+ HERMES_HOME="$RUNTIME_HOME"
1629
1866
 
1630
1867
  if [[ ! -d "$RUNTIME_HOME" ]]; then
1631
1868
  echo "hermes: runtime submodule not initialized at $RUNTIME_HOME" >&2
@@ -1637,17 +1874,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
1637
1874
  `.replace(/\u0010/g, "$");
1638
1875
  }
1639
1876
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
1640
- if (!existsSync5(sourceDir)) return;
1641
- mkdirSync3(targetDir, { recursive: true });
1877
+ if (!existsSync7(sourceDir)) return;
1878
+ mkdirSync5(targetDir, { recursive: true });
1642
1879
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
1643
- const sourcePath = join6(sourceDir, entry.name);
1880
+ const sourcePath = join8(sourceDir, entry.name);
1644
1881
  if (skip?.(sourcePath)) continue;
1645
- const targetPath = join6(targetDir, entry.name);
1882
+ const targetPath = join8(targetDir, entry.name);
1646
1883
  if (entry.isDirectory()) {
1647
1884
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
1648
1885
  continue;
1649
1886
  }
1650
- if (existsSync5(targetPath)) continue;
1887
+ if (existsSync7(targetPath)) continue;
1651
1888
  changedFiles.push(targetPath);
1652
1889
  if (!dryRun) {
1653
1890
  ensureParent(targetPath);
@@ -1656,7 +1893,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
1656
1893
  }
1657
1894
  }
1658
1895
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
1659
- const gitmodulesPath = join6(repoRoot, ".gitmodules");
1896
+ const gitmodulesPath = join8(repoRoot, ".gitmodules");
1660
1897
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
1661
1898
  const owner = role.runtimeOwner || "delorenj";
1662
1899
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -1678,7 +1915,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
1678
1915
  repo: ${role.repo}
1679
1916
  role: ${role.role}
1680
1917
  display_name: ${JSON.stringify(role.displayName || role.agentId)}
1681
- project_path: ${ctxEscape(role.roleDir ? dirname3(dirname3(dirname3(role.roleDir))) : "")}
1918
+ project_path: ${ctxEscape(role.roleDir ? dirname5(dirname5(dirname5(role.roleDir))) : "")}
1682
1919
  role_dir: ${ctxEscape(role.roleDir)}
1683
1920
  profile_name: ${role.profileName || role.agentId}
1684
1921
  telegram:
@@ -1691,7 +1928,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
1691
1928
  systemd:
1692
1929
  gateway_unit: hermes-${role.agentId}-gateway.service
1693
1930
  consumer_unit: hermes-${role.agentId}-consumer.service
1694
- checkpoint_timer: hermes-${role.agentId}-checkpoint.timer
1931
+ heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
1695
1932
  `;
1696
1933
  const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
1697
1934
  ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
@@ -1756,13 +1993,17 @@ var RULES = [
1756
1993
  id: "mise.config-root",
1757
1994
  title: "mise config_root + AGENTS link hooks",
1758
1995
  audit: (ctx) => {
1759
- const misePath = join6(ctx.repoRoot, "mise.toml");
1760
- if (!existsSync5(misePath)) {
1996
+ const misePath = join8(ctx.repoRoot, "mise.toml");
1997
+ if (!existsSync7(misePath)) {
1761
1998
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
1762
1999
  }
1763
2000
  const text3 = readText(misePath);
1764
2001
  const details = [];
1765
- if (!text3.includes('_.path = [".mise/scripts", "agents/hermes/pm"]')) details.push("[env]._.path should include .mise/scripts and agents/hermes/pm");
2002
+ const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2003
+ if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2004
+ const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2005
+ const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2006
+ if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
1766
2007
  if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
1767
2008
  if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
1768
2009
  if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
@@ -1777,25 +2018,37 @@ var RULES = [
1777
2018
  };
1778
2019
  },
1779
2020
  migrate: (ctx, finding) => {
1780
- const path = join6(ctx.repoRoot, "mise.toml");
2021
+ const path = join8(ctx.repoRoot, "mise.toml");
1781
2022
  const changedFiles = [];
1782
- if (!existsSync5(path)) {
2023
+ if (!existsSync7(path)) {
1783
2024
  return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; initialize mise first", changedFiles, details: [] };
1784
2025
  }
1785
2026
  let text3 = readText(path);
1786
- const next = upsertLinkAgentfilesBlock(text3);
2027
+ const next = upsertLinkAgentfilesBlock(text3, ctx);
1787
2028
  if (next !== text3) {
1788
2029
  changedFiles.push(path);
1789
2030
  if (!ctx.dryRun) writeText(path, next);
1790
2031
  text3 = next;
1791
2032
  }
2033
+ const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2034
+ const expectedScript = templateLinkAgentfilesScript(ctx);
2035
+ if (expectedScript === void 0) {
2036
+ 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: [] };
2037
+ }
2038
+ if (safeReadText(linkAgentfilesPath) !== expectedScript) {
2039
+ changedFiles.push(linkAgentfilesPath);
2040
+ if (!ctx.dryRun) {
2041
+ writeText(linkAgentfilesPath, expectedScript);
2042
+ chmodSync(linkAgentfilesPath, 493);
2043
+ }
2044
+ }
1792
2045
  return {
1793
2046
  id: finding.id,
1794
2047
  title: finding.title,
1795
2048
  status: changedFiles.length ? "applied" : "noop",
1796
2049
  summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
1797
2050
  changedFiles,
1798
- details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block"] : []
2051
+ details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block and script"] : []
1799
2052
  };
1800
2053
  }
1801
2054
  },
@@ -1804,13 +2057,13 @@ var RULES = [
1804
2057
  title: "managed mise versioning block",
1805
2058
  audit: (ctx) => {
1806
2059
  const details = [];
1807
- const misePath = join6(ctx.repoRoot, "mise.toml");
1808
- const versioningPath = join6(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
1809
- const manifestPath = join6(ctx.repoRoot, ".mise", "version-files.conf");
2060
+ const misePath = join8(ctx.repoRoot, "mise.toml");
2061
+ const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2062
+ const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
1810
2063
  const text3 = safeReadText(misePath);
1811
2064
  if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
1812
- if (!existsSync5(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
1813
- if (!existsSync5(manifestPath)) details.push(".mise/version-files.conf missing");
2065
+ if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2066
+ if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
1814
2067
  return {
1815
2068
  id: "mise.versioning",
1816
2069
  title: "managed mise versioning block",
@@ -1822,8 +2075,8 @@ var RULES = [
1822
2075
  },
1823
2076
  migrate: (ctx, finding) => {
1824
2077
  const changedFiles = [];
1825
- const misePath = join6(ctx.repoRoot, "mise.toml");
1826
- if (!existsSync5(misePath)) {
2078
+ const misePath = join8(ctx.repoRoot, "mise.toml");
2079
+ if (!existsSync7(misePath)) {
1827
2080
  return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; cannot inject versioning block", changedFiles, details: [] };
1828
2081
  }
1829
2082
  const currentMise = readText(misePath);
@@ -1832,8 +2085,11 @@ var RULES = [
1832
2085
  changedFiles.push(misePath);
1833
2086
  if (!ctx.dryRun) writeText(misePath, nextMise);
1834
2087
  }
1835
- const versioningPath = join6(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2088
+ const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
1836
2089
  const expectedScript = templateVersioningScript(ctx);
2090
+ if (expectedScript === void 0) {
2091
+ 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: [] };
2092
+ }
1837
2093
  if (safeReadText(versioningPath) !== expectedScript) {
1838
2094
  changedFiles.push(versioningPath);
1839
2095
  if (!ctx.dryRun) {
@@ -1841,7 +2097,7 @@ var RULES = [
1841
2097
  chmodSync(versioningPath, 493);
1842
2098
  }
1843
2099
  }
1844
- const manifestPath = join6(ctx.repoRoot, ".mise", "version-files.conf");
2100
+ const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
1845
2101
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
1846
2102
  if (safeReadText(manifestPath) !== expectedManifest) {
1847
2103
  changedFiles.push(manifestPath);
@@ -1861,13 +2117,24 @@ var RULES = [
1861
2117
  id: "sot.agent-symlinks",
1862
2118
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
1863
2119
  audit: (ctx) => {
1864
- const agentsPath = join6(ctx.repoRoot, "AGENTS.md");
1865
- if (!existsSync5(agentsPath)) {
1866
- return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
2120
+ const agentsPath = join8(ctx.repoRoot, "AGENTS.md");
2121
+ if (!existsSync7(agentsPath)) {
2122
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join8(ctx.repoRoot, file)));
2123
+ if (fallbackSources.length === 0) {
2124
+ return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
2125
+ }
2126
+ return {
2127
+ id: "sot.agent-symlinks",
2128
+ title: "AGENTS/CLAUDE/GEMINI symlink contract",
2129
+ status: "fail",
2130
+ summary: "AGENTS.md missing but can be derived from existing project documentation",
2131
+ details: [`AGENTS.md can be created from ${fallbackSources[0]}`],
2132
+ fixable: true
2133
+ };
1867
2134
  }
1868
2135
  const details = [];
1869
2136
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1870
- const full = join6(ctx.repoRoot, file);
2137
+ const full = join8(ctx.repoRoot, file);
1871
2138
  const target = readSymlinkTarget(full);
1872
2139
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
1873
2140
  }
@@ -1883,22 +2150,26 @@ var RULES = [
1883
2150
  migrate: (ctx, finding) => {
1884
2151
  const changedFiles = [];
1885
2152
  const details = [];
1886
- if (!existsSync5(join6(ctx.repoRoot, "AGENTS.md"))) {
1887
- return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot wire symlinks", changedFiles, details: [] };
2153
+ const blockedDetails = [];
2154
+ const bootstrap = bootstrapAgentsFile(ctx.repoRoot, ctx.dryRun);
2155
+ changedFiles.push(...bootstrap.changedFiles);
2156
+ details.push(...bootstrap.details);
2157
+ if (bootstrap.blocked) {
2158
+ return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
1888
2159
  }
1889
2160
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1890
- const full = join6(ctx.repoRoot, file);
2161
+ const full = join8(ctx.repoRoot, file);
1891
2162
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
1892
- if (result.blocked) details.push(result.blocked);
2163
+ if (result.blocked) blockedDetails.push(result.blocked);
1893
2164
  if (result.changed) changedFiles.push(full);
1894
2165
  }
1895
2166
  return {
1896
2167
  id: finding.id,
1897
2168
  title: finding.title,
1898
- status: details.length ? "blocked" : changedFiles.length ? "applied" : "noop",
1899
- summary: details.length ? "One or more files could not be replaced safely" : changedFiles.length ? "Symlink contract repaired" : "No changes required",
2169
+ status: blockedDetails.length ? "blocked" : changedFiles.length ? "applied" : "noop",
2170
+ summary: blockedDetails.length ? "One or more files could not be replaced safely" : changedFiles.length ? "Symlink contract repaired" : "No changes required",
1900
2171
  changedFiles,
1901
- details
2172
+ details: [...details, ...blockedDetails]
1902
2173
  };
1903
2174
  }
1904
2175
  },
@@ -1909,7 +2180,7 @@ var RULES = [
1909
2180
  migrate: (ctx, finding) => {
1910
2181
  const changedFiles = [];
1911
2182
  const details = [];
1912
- const path = join6(ctx.repoRoot, ".project.json");
2183
+ const path = join8(ctx.repoRoot, ".project.json");
1913
2184
  const existing = readProjectJson(ctx) ?? {};
1914
2185
  const canonical = canonicalProjectJson(ctx);
1915
2186
  const merged = { ...existing, ...canonical };
@@ -1919,10 +2190,10 @@ var RULES = [
1919
2190
  changedFiles.push(path);
1920
2191
  if (!ctx.dryRun) writeText(path, expected);
1921
2192
  }
1922
- const planeJson = join6(ctx.repoRoot, ".plane.json");
1923
- if (existsSync5(planeJson)) {
2193
+ const planeJson = join8(ctx.repoRoot, ".plane.json");
2194
+ if (existsSync7(planeJson)) {
1924
2195
  const backup = `${planeJson}.migrated-backup`;
1925
- if (existsSync5(backup)) {
2196
+ if (existsSync7(backup)) {
1926
2197
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
1927
2198
  } else {
1928
2199
  changedFiles.push(backup);
@@ -1944,8 +2215,8 @@ var RULES = [
1944
2215
  title: ".env.op + gitignore secrets contract",
1945
2216
  audit: (ctx) => {
1946
2217
  const details = [];
1947
- const envOp = safeReadText(join6(ctx.repoRoot, ".env.op"));
1948
- const gitignore = safeReadText(join6(ctx.repoRoot, ".gitignore"));
2218
+ const envOp = safeReadText(join8(ctx.repoRoot, ".env.op"));
2219
+ const gitignore = safeReadText(join8(ctx.repoRoot, ".gitignore"));
1949
2220
  if (!envOp) {
1950
2221
  details.push(".env.op missing");
1951
2222
  } else {
@@ -1970,12 +2241,12 @@ var RULES = [
1970
2241
  migrate: (ctx, finding) => {
1971
2242
  const changedFiles = [];
1972
2243
  const details = [];
1973
- const envOpPath = join6(ctx.repoRoot, ".env.op");
1974
- if (!existsSync5(envOpPath)) {
2244
+ const envOpPath = join8(ctx.repoRoot, ".env.op");
2245
+ if (!existsSync7(envOpPath)) {
1975
2246
  changedFiles.push(envOpPath);
1976
- if (!ctx.dryRun) writeText(envOpPath, readText(join6(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2247
+ if (!ctx.dryRun) writeText(envOpPath, readText(join8(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
1977
2248
  }
1978
- const gitignorePath = join6(ctx.repoRoot, ".gitignore");
2249
+ const gitignorePath = join8(ctx.repoRoot, ".gitignore");
1979
2250
  const gitignore = safeReadText(gitignorePath) ?? "";
1980
2251
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
1981
2252
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -2002,7 +2273,7 @@ var RULES = [
2002
2273
  title: ".copier-answers.yml provenance + drift report",
2003
2274
  audit: (ctx) => {
2004
2275
  const details = [];
2005
- const path = join6(ctx.repoRoot, ".copier-answers.yml");
2276
+ const path = join8(ctx.repoRoot, ".copier-answers.yml");
2006
2277
  const text3 = safeReadText(path);
2007
2278
  const project = readProjectJson(ctx);
2008
2279
  if (!text3) {
@@ -2033,12 +2304,12 @@ var RULES = [
2033
2304
  const changedFiles = [];
2034
2305
  const project = canonicalProjectJson(ctx);
2035
2306
  const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2036
- _src_path: ${join6(ctx.pjanglerRoot, "templates", "commonproject")}
2307
+ _src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
2037
2308
  project_description: ${String(project.project_description)}
2038
2309
  project_name: ${String(project.project_name)}
2039
2310
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2040
2311
  `;
2041
- const path = join6(ctx.repoRoot, ".copier-answers.yml");
2312
+ const path = join8(ctx.repoRoot, ".copier-answers.yml");
2042
2313
  if (safeReadText(path) !== text3) {
2043
2314
  changedFiles.push(path);
2044
2315
  if (!ctx.dryRun) writeText(path, text3);
@@ -2057,15 +2328,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2057
2328
  id: "bmad.scaffold",
2058
2329
  title: "BMAD modules/docs scaffold",
2059
2330
  audit: (ctx) => {
2060
- const sourceRoot = join6(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2061
- const targetRoot = join6(ctx.repoRoot, "_bmad");
2331
+ const sourceRoot = join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2332
+ const targetRoot = join8(ctx.repoRoot, "_bmad");
2062
2333
  const sentinels = [
2063
- join6("core", "config.yaml"),
2064
- join6("custom", "config.yaml"),
2065
- join6("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2066
- join6("bmm", "workflows", "workflow-status", "workflow.yaml")
2334
+ join8("core", "config.yaml"),
2335
+ join8("custom", "config.yaml"),
2336
+ join8("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2337
+ join8("bmm", "workflows", "workflow-status", "workflow.yaml")
2067
2338
  ];
2068
- const missing = sentinels.filter((file) => existsSync5(join6(sourceRoot, file)) && !existsSync5(join6(targetRoot, file)));
2339
+ const missing = sentinels.filter((file) => existsSync7(join8(sourceRoot, file)) && !existsSync7(join8(targetRoot, file)));
2069
2340
  return {
2070
2341
  id: "bmad.scaffold",
2071
2342
  title: "BMAD modules/docs scaffold",
@@ -2077,7 +2348,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2077
2348
  },
2078
2349
  migrate: (ctx, finding) => {
2079
2350
  const changedFiles = [];
2080
- copyMissingRecursive(join6(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join6(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2351
+ copyMissingRecursive(join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join8(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2081
2352
  return {
2082
2353
  id: finding.id,
2083
2354
  title: finding.title,
@@ -2098,12 +2369,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2098
2369
  return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
2099
2370
  }
2100
2371
  const details = [];
2101
- for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
2102
- if (!existsSync5(join6(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join6(role.roleDir, rel))}`);
2372
+ 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"]) {
2373
+ if (!existsSync7(join8(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join8(role.roleDir, rel))}`);
2103
2374
  }
2104
- const gitmodules = safeReadText(join6(ctx.repoRoot, ".gitmodules")) ?? "";
2375
+ const gitmodules = safeReadText(join8(ctx.repoRoot, ".gitmodules")) ?? "";
2105
2376
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
2106
- if (!profileMetaInheritsDefault(join6(role.roleDir, "runtime", "profile.yaml"))) {
2377
+ if (!profileMetaInheritsDefault(join8(role.roleDir, "runtime", "profile.yaml"))) {
2107
2378
  details.push("runtime/profile.yaml missing inherited default config metadata");
2108
2379
  }
2109
2380
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -2124,79 +2395,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2124
2395
  if (!role) {
2125
2396
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
2126
2397
  }
2127
- const templateRoleDir = join6(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2128
- writeIfDifferent(join6(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2129
- writeIfDifferent(join6(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2130
- writeIfDifferent(join6(role.roleDir, ".gitignore"), readText(join6(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2131
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2132
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2133
- copyMissingRecursive(join6(templateRoleDir, ".scripts"), join6(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("continuous-ticket-sentinel.prompt.md.jinja"));
2134
- upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2135
- const profileMetaUpdated = upsertInheritedProfileMeta(join6(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2136
- if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2137
- const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2138
- if (registryUpdated) details.push(`updated ${registryUpdated}`);
2139
- return {
2140
- id: finding.id,
2141
- title: finding.title,
2142
- status: changedFiles.length ? "applied" : "noop",
2143
- summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
2144
- changedFiles,
2145
- details
2146
- };
2147
- }
2148
- },
2149
- {
2150
- id: "hermes.scrum-master-scaffold",
2151
- title: "Hermes scrum-master scaffold parity",
2152
- audit: (ctx) => {
2153
- const roles = discoverRoles(ctx.repoRoot);
2154
- const role = roles.find((item) => item.role === "scrum-master");
2155
- if (!role) {
2156
- return { id: "hermes.scrum-master-scaffold", title: "Hermes scrum-master scaffold parity", status: "skip", summary: "No scrum-master role present", details: [], fixable: false };
2157
- }
2158
- const details = [];
2159
- for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/75-scrum-master.sh", ".scripts/scrum-master/continuous-ticket-sentinel.sh", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
2160
- if (!existsSync5(join6(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join6(role.roleDir, rel))}`);
2161
- }
2162
- const gitmodules = safeReadText(join6(ctx.repoRoot, ".gitmodules")) ?? "";
2163
- if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing scrum-master runtime submodule entry");
2164
- if (!profileMetaInheritsDefault(join6(role.roleDir, "runtime", "profile.yaml"))) {
2165
- details.push("runtime/profile.yaml missing inherited default config metadata");
2166
- }
2167
- const registry = safeReadText(registryPath(ctx.homeDir));
2168
- if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
2169
- return {
2170
- id: "hermes.scrum-master-scaffold",
2171
- title: "Hermes scrum-master scaffold parity",
2172
- status: details.length === 0 ? "pass" : "fail",
2173
- summary: details.length === 0 ? "scrum-master scaffold parity verified" : `${details.length} scrum-master scaffold issue(s) detected`,
2174
- details,
2175
- fixable: true
2176
- };
2177
- },
2178
- migrate: (ctx, finding) => {
2179
- const role = discoverRoles(ctx.repoRoot).find((item) => item.role === "scrum-master");
2180
- const changedFiles = [];
2181
- const details = [];
2182
- if (!role) {
2183
- return { id: finding.id, title: finding.title, status: "blocked", summary: "No scrum-master role present", changedFiles, details: [] };
2184
- }
2185
- const templateRoleDir = join6(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2186
- writeIfDifferent(join6(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2187
- writeIfDifferent(join6(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2188
- writeIfDifferent(join6(role.roleDir, ".gitignore"), readText(join6(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2189
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2190
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2191
- copyMissingRecursive(join6(templateRoleDir, ".scripts"), join6(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("continuous-ticket-sentinel.prompt.md.jinja"));
2192
- const promptSource = join6(templateRoleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md.jinja");
2193
- const promptTarget = join6(role.roleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md");
2194
- if (!existsSync5(promptTarget)) {
2195
- const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo);
2398
+ const templateRoleDir = join8(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2399
+ writeIfDifferent(join8(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2400
+ writeIfDifferent(join8(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2401
+ writeIfDifferent(join8(role.roleDir, ".gitignore"), readText(join8(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2402
+ copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2403
+ copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2404
+ copyMissingRecursive(join8(templateRoleDir, ".scripts"), join8(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2405
+ const promptSource = join8(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2406
+ const promptTarget = join8(role.roleDir, ".scripts", "sentinel.prompt.md");
2407
+ if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
2408
+ 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);
2196
2409
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
2197
2410
  }
2198
2411
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2199
- const profileMetaUpdated = upsertInheritedProfileMeta(join6(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2412
+ const profileMetaUpdated = upsertInheritedProfileMeta(join8(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2200
2413
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2201
2414
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2202
2415
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -2204,7 +2417,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2204
2417
  id: finding.id,
2205
2418
  title: finding.title,
2206
2419
  status: changedFiles.length ? "applied" : "noop",
2207
- summary: changedFiles.length ? "scrum-master scaffold normalized" : "No changes required",
2420
+ summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
2208
2421
  changedFiles,
2209
2422
  details
2210
2423
  };
@@ -2224,14 +2437,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2224
2437
  }
2225
2438
  const details = [];
2226
2439
  for (const role of roles) {
2227
- for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-checkpoint.timer`]) {
2440
+ for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
2228
2441
  const state = checkUnit(unit);
2229
2442
  if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
2230
2443
  }
2231
- if (role.role === "scrum-master") {
2232
- const state = checkUnit(`hermes-${role.agentId}-continuous-ticket-sentinel.timer`);
2233
- if (!state.enabled || !state.active) details.push(`hermes-${role.agentId}-continuous-ticket-sentinel.timer should be enabled+active`);
2234
- }
2235
2444
  }
2236
2445
  return {
2237
2446
  id: "systemd.sentinel",
@@ -2254,10 +2463,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2254
2463
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
2255
2464
  }
2256
2465
  for (const role of roles) {
2257
- const sysDir = join6(ctx.homeDir, ".config", "systemd", "user");
2258
- const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-checkpoint.timer`];
2259
- if (role.role === "scrum-master") units.push(`hermes-${role.agentId}-continuous-ticket-sentinel.timer`);
2260
- const allUnitsPresent = units.every((unit) => existsSync5(join6(sysDir, unit)));
2466
+ const sysDir = join8(ctx.homeDir, ".config", "systemd", "user");
2467
+ const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
2468
+ const allUnitsPresent = units.every((unit) => existsSync7(join8(sysDir, unit)));
2261
2469
  if (allUnitsPresent) {
2262
2470
  if (ctx.dryRun) {
2263
2471
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -2269,8 +2477,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2269
2477
  }
2270
2478
  continue;
2271
2479
  }
2272
- for (const script of [join6(role.roleDir, ".scripts", "70-systemd.sh"), role.role === "scrum-master" ? join6(role.roleDir, ".scripts", "75-scrum-master.sh") : ""]) {
2273
- if (!script || !existsSync5(script)) continue;
2480
+ for (const script of [join8(role.roleDir, ".scripts", "70-systemd.sh")]) {
2481
+ if (!script || !existsSync7(script)) continue;
2274
2482
  if (ctx.dryRun) {
2275
2483
  details.push(`would run: bash ${script}`);
2276
2484
  } else {
@@ -2306,7 +2514,7 @@ function runAudit(repoArg) {
2306
2514
  repoRoot: resolve(repoArg ?? process.cwd()),
2307
2515
  dryRun: true,
2308
2516
  pjanglerRoot,
2309
- homeDir: homedir3()
2517
+ homeDir: homedir4()
2310
2518
  };
2311
2519
  const rules = RULES.map((rule) => rule.audit(ctx));
2312
2520
  return {
@@ -2322,13 +2530,26 @@ function runMigration(selector, repoArg, dryRun, all) {
2322
2530
  repoRoot: resolve(repoArg ?? process.cwd()),
2323
2531
  dryRun,
2324
2532
  pjanglerRoot,
2325
- homeDir: homedir3()
2533
+ homeDir: homedir4()
2326
2534
  };
2327
2535
  const selected = all ? RULES : RULES.filter((rule) => rule.id === selector);
2328
2536
  if (!selected.length) {
2329
2537
  throw new Error(`Unknown parity rule: ${selector}`);
2330
2538
  }
2331
- const results = selected.map((rule) => rule.migrate(ctx, rule.audit(ctx)));
2539
+ const results = selected.map((rule) => {
2540
+ try {
2541
+ return rule.migrate(ctx, rule.audit(ctx));
2542
+ } catch (err) {
2543
+ return {
2544
+ id: rule.id,
2545
+ title: rule.title,
2546
+ status: "blocked",
2547
+ summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
2548
+ changedFiles: [],
2549
+ details: []
2550
+ };
2551
+ }
2552
+ });
2332
2553
  const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
2333
2554
  return {
2334
2555
  repo: ctx.repoRoot,
@@ -2364,18 +2585,18 @@ function formatMigrationReport(report) {
2364
2585
  }
2365
2586
 
2366
2587
  // src/utils/version.ts
2367
- import { readFileSync as readFileSync3 } from "node:fs";
2368
- import { dirname as dirname4, join as join7 } from "node:path";
2369
- import { fileURLToPath as fileURLToPath3 } from "node:url";
2588
+ import { readFileSync as readFileSync4 } from "node:fs";
2589
+ import { dirname as dirname6, join as join9 } from "node:path";
2590
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
2370
2591
  var PJANGLER_VERSION = (() => {
2371
2592
  try {
2372
- let dir = dirname4(fileURLToPath3(import.meta.url));
2593
+ let dir = dirname6(fileURLToPath4(import.meta.url));
2373
2594
  for (let i = 0; i < 4; i++) {
2374
2595
  try {
2375
- const raw = readFileSync3(join7(dir, "package.json"), "utf8");
2596
+ const raw = readFileSync4(join9(dir, "package.json"), "utf8");
2376
2597
  return JSON.parse(raw).version ?? "0.0.0";
2377
2598
  } catch {
2378
- const parent = dirname4(dir);
2599
+ const parent = dirname6(dir);
2379
2600
  if (parent === dir) break;
2380
2601
  dir = parent;
2381
2602
  }