@delorenj/pjangler 1.1.5 → 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 (139) hide show
  1. package/.mise/scripts/link-agentfiles.sh +9 -0
  2. package/dist/index.js +481 -277
  3. package/dist/mcp-server.js +1706 -163
  4. package/package.json +3 -2
  5. package/templates/commonproject/.mise/scripts/create-plane-project.sh +26 -0
  6. package/templates/commonproject/copier.yml +2 -0
  7. package/templates/commonproject/template/.agents/hooks/README.md +151 -0
  8. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +63 -0
  9. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-journal-write.sh +327 -0
  10. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-recall.sh +180 -0
  11. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-retain.sh +74 -0
  12. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-session-end.sh +57 -0
  13. package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-bank.sh +57 -0
  14. package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-journal.sh +48 -0
  15. package/templates/commonproject/template/.agents/hooks/hooks.master.json +97 -0
  16. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +30 -0
  17. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +51 -0
  18. package/templates/commonproject/template/.agents/hooks/reminder-for-skill-check.sh +20 -0
  19. package/templates/commonproject/template/.agents/hooks/sync.py +618 -0
  20. package/templates/commonproject/template/.agents/local.example.json +15 -0
  21. package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +62 -0
  22. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +110 -0
  23. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +45 -0
  24. package/templates/commonproject/template/mise.toml.jinja +42 -0
  25. package/templates/hermes-agent/.codegraph/daemon.pid +6 -0
  26. package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +10 -0
  27. package/templates/hermes-agent/README.md +16 -14
  28. package/templates/hermes-agent/copier.yml +7 -25
  29. package/templates/hermes-agent/docs/architecture.md +16 -18
  30. package/templates/hermes-agent/docs/fleet-control-plane/README.md +35 -0
  31. package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +253 -0
  32. package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +280 -0
  33. package/templates/hermes-agent/docs/fleet-control-plane/implementation-readiness-report-2026-06-27.md +118 -0
  34. package/templates/hermes-agent/docs/fleet-control-plane/prd.md +141 -0
  35. package/templates/hermes-agent/docs/operations.md +17 -19
  36. package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +17 -13
  37. package/templates/hermes-agent/docs/{scrum-master → sentinel}/README.md +47 -46
  38. package/templates/hermes-agent/docs/{scrum-master → sentinel}/architecture.md +39 -38
  39. package/templates/hermes-agent/docs/{scrum-master → sentinel}/development.md +63 -99
  40. package/templates/hermes-agent/docs/{scrum-master → sentinel}/providers.md +2 -2
  41. package/templates/hermes-agent/docs/sentinel.md +18 -0
  42. package/templates/hermes-agent/install-local.sh +22 -20
  43. package/templates/hermes-agent/runtime-scaffold/README.md +10 -9
  44. package/templates/hermes-agent/scripts/backfill-fleet-sot.sh +12 -21
  45. package/templates/hermes-agent/scripts/fleet-sync.sh +369 -0
  46. package/templates/hermes-agent/scripts/migrate-unify.sh +252 -0
  47. package/templates/hermes-agent/scripts/unify-pm.sh +148 -0
  48. package/templates/hermes-agent/template/.runtime-scaffold/README.md +10 -9
  49. package/templates/hermes-agent/template/.scripts/01-config.sh +2 -3
  50. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -2
  51. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +47 -19
  52. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +71 -27
  53. package/templates/hermes-agent/template/.scripts/30-telegram.sh +3 -3
  54. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +3 -3
  55. package/templates/hermes-agent/template/.scripts/70-systemd.sh +48 -43
  56. package/templates/hermes-agent/template/.scripts/80-registry.sh +2 -2
  57. package/templates/hermes-agent/template/.scripts/99-summary.sh +3 -4
  58. package/templates/hermes-agent/template/.scripts/_lib.sh +4 -19
  59. package/templates/hermes-agent/template/.scripts/checkpoint.sh +36 -0
  60. package/templates/hermes-agent/template/.scripts/config.example.toml +11 -5
  61. package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.sh → heartbeat.sh} +84 -31
  62. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +1 -1
  63. package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -3
  64. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/emit-event.py +5 -5
  65. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-autonomous-review.sh +6 -6
  66. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-close-gate.sh +2 -2
  67. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/autonomous-delegated-review.md +7 -7
  68. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/bloodbank-events.md +3 -3
  69. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/continuous-ticket-orchestration.md +6 -6
  70. package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.prompt.md.jinja → sentinel.prompt.md.jinja} +5 -4
  71. package/templates/hermes-agent/template/SOUL.md.jinja +3 -6
  72. package/templates/hermes-agent/template/hermes.jinja +6 -15
  73. package/templates/hermes-agent/template/role.yaml.jinja +9 -8
  74. package/templates/commonproject/.claude/commands/bmad/bmb/agents/agent-builder.md +0 -14
  75. package/templates/commonproject/.claude/commands/bmad/bmb/agents/module-builder.md +0 -14
  76. package/templates/commonproject/.claude/commands/bmad/bmb/agents/workflow-builder.md +0 -14
  77. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/agent.md +0 -5
  78. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/module.md +0 -5
  79. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/workflow.md +0 -5
  80. package/templates/commonproject/.claude/commands/bmad/bmm/agents/analyst.md +0 -14
  81. package/templates/commonproject/.claude/commands/bmad/bmm/agents/architect.md +0 -14
  82. package/templates/commonproject/.claude/commands/bmad/bmm/agents/dev.md +0 -14
  83. package/templates/commonproject/.claude/commands/bmad/bmm/agents/pm.md +0 -14
  84. package/templates/commonproject/.claude/commands/bmad/bmm/agents/quick-flow-solo-dev.md +0 -14
  85. package/templates/commonproject/.claude/commands/bmad/bmm/agents/sm.md +0 -14
  86. package/templates/commonproject/.claude/commands/bmad/bmm/agents/tea.md +0 -14
  87. package/templates/commonproject/.claude/commands/bmad/bmm/agents/tech-writer.md +0 -14
  88. package/templates/commonproject/.claude/commands/bmad/bmm/agents/ux-designer.md +0 -14
  89. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/check-implementation-readiness.md +0 -5
  90. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/code-review.md +0 -13
  91. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/correct-course.md +0 -13
  92. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-architecture.md +0 -5
  93. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-epics-and-stories.md +0 -5
  94. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-dataflow.md +0 -13
  95. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-diagram.md +0 -13
  96. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-flowchart.md +0 -13
  97. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-wireframe.md +0 -13
  98. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-product-brief.md +0 -5
  99. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-story.md +0 -13
  100. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-ux-design.md +0 -5
  101. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/dev-story.md +0 -13
  102. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/document-project.md +0 -13
  103. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/generate-project-context.md +0 -5
  104. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/prd.md +0 -5
  105. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-dev.md +0 -5
  106. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-spec.md +0 -5
  107. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/research.md +0 -5
  108. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/retrospective.md +0 -13
  109. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-planning.md +0 -13
  110. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-status.md +0 -13
  111. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-atdd.md +0 -13
  112. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-automate.md +0 -13
  113. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-ci.md +0 -13
  114. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-framework.md +0 -13
  115. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-nfr.md +0 -13
  116. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-design.md +0 -13
  117. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-review.md +0 -13
  118. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-trace.md +0 -13
  119. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-init.md +0 -13
  120. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-status.md +0 -13
  121. package/templates/commonproject/.claude/commands/bmad/cis/agents/brainstorming-coach.md +0 -14
  122. package/templates/commonproject/.claude/commands/bmad/cis/agents/creative-problem-solver.md +0 -14
  123. package/templates/commonproject/.claude/commands/bmad/cis/agents/design-thinking-coach.md +0 -14
  124. package/templates/commonproject/.claude/commands/bmad/cis/agents/innovation-strategist.md +0 -14
  125. package/templates/commonproject/.claude/commands/bmad/cis/agents/presentation-master.md +0 -14
  126. package/templates/commonproject/.claude/commands/bmad/cis/agents/storyteller.md +0 -14
  127. package/templates/commonproject/.claude/commands/bmad/cis/workflows/design-thinking.md +0 -13
  128. package/templates/commonproject/.claude/commands/bmad/cis/workflows/innovation-strategy.md +0 -13
  129. package/templates/commonproject/.claude/commands/bmad/cis/workflows/problem-solving.md +0 -13
  130. package/templates/commonproject/.claude/commands/bmad/cis/workflows/storytelling.md +0 -13
  131. package/templates/commonproject/.claude/commands/bmad/core/agents/bmad-master.md +0 -14
  132. package/templates/commonproject/.claude/commands/bmad/core/tasks/index-docs.md +0 -9
  133. package/templates/commonproject/.claude/commands/bmad/core/tasks/shard-doc.md +0 -9
  134. package/templates/commonproject/.claude/commands/bmad/core/workflows/brainstorming.md +0 -5
  135. package/templates/commonproject/.claude/commands/bmad/core/workflows/party-mode.md +0 -5
  136. package/templates/commonproject/.claude/commands/bmad/custom/workflows/ticket-lifecycle.md +0 -5
  137. package/templates/hermes-agent/docs/scrum-master-sentinel.md +0 -17
  138. package/templates/hermes-agent/template/.scripts/75-scrum-master.sh +0 -117
  139. 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,13 +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
- const source = join6(ctx.pjanglerRoot, ".mise", "scripts", "versioning.sh");
1451
- return existsSync5(source) ? readText(source) : void 0;
1655
+ return templateScript(ctx, "versioning.sh");
1656
+ }
1657
+ function templateLinkAgentfilesScript(ctx) {
1658
+ return templateScript(ctx, "link-agentfiles.sh");
1452
1659
  }
1453
1660
  function templateVersionFilesConf(ctx, repoRoot) {
1454
- const packageJson = join6(repoRoot, "package.json");
1455
- 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";
1456
1663
  }
1457
1664
  function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
1458
1665
  if (startMarker.test(text3)) {
@@ -1471,29 +1678,64 @@ ${text3.slice(match.index)}`;
1471
1678
  ${block}
1472
1679
  `;
1473
1680
  }
1474
- 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));
1475
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"/;
1476
- if (existing.test(text3)) {
1477
- return text3.replace(existing, LINK_AGENTFILES_BLOCK);
1718
+ if (existing.test(withPath)) {
1719
+ return withPath.replace(existing, LINK_AGENTFILES_BLOCK);
1478
1720
  }
1479
- const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
1721
+ const versioningIndex = withPath.indexOf("# >>> mise-versioning >>>");
1480
1722
  if (versioningIndex >= 0) {
1481
- 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}
1482
1724
 
1483
- ${text3.slice(versioningIndex)}`;
1725
+ ${withPath.slice(versioningIndex)}`;
1484
1726
  }
1485
- return `${text3.replace(/\s*$/, "")}
1727
+ return `${withPath.replace(/\s*$/, "")}
1486
1728
 
1487
1729
  ${LINK_AGENTFILES_BLOCK}
1488
1730
  `;
1489
1731
  }
1490
1732
  function readProjectJson(ctx) {
1491
- return tryParseJson(safeReadText(join6(ctx.repoRoot, ".project.json")));
1733
+ return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
1492
1734
  }
1493
1735
  function canonicalProjectJson(ctx) {
1494
1736
  const roles = discoverRoles(ctx.repoRoot);
1495
1737
  const existing = readProjectJson(ctx) ?? {};
1496
- 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"));
1497
1739
  const firstRole = roles[0];
1498
1740
  const ticketProvider = {
1499
1741
  type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
@@ -1520,12 +1762,12 @@ function canonicalProjectJson(ctx) {
1520
1762
  };
1521
1763
  }
1522
1764
  function projectJsonFinding(ctx) {
1523
- const projectPath = join6(ctx.repoRoot, ".project.json");
1524
- const planeJsonPath = join6(ctx.repoRoot, ".plane.json");
1765
+ const projectPath = join8(ctx.repoRoot, ".project.json");
1766
+ const planeJsonPath = join8(ctx.repoRoot, ".plane.json");
1525
1767
  const details = [];
1526
1768
  const data = readProjectJson(ctx);
1527
1769
  const roles = discoverRoles(ctx.repoRoot);
1528
- if (!existsSync5(projectPath)) {
1770
+ if (!existsSync7(projectPath)) {
1529
1771
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
1530
1772
  }
1531
1773
  if (!data) {
@@ -1551,7 +1793,7 @@ function projectJsonFinding(ctx) {
1551
1793
  for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
1552
1794
  if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
1553
1795
  }
1554
- 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");
1555
1797
  return {
1556
1798
  id: "sot.project-json",
1557
1799
  title: "Canonical .project.json",
@@ -1563,8 +1805,8 @@ function projectJsonFinding(ctx) {
1563
1805
  }
1564
1806
  function renderSoul(role) {
1565
1807
  const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
1566
- 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.";
1567
- 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.`;
1568
1810
  const runtimeOwner = role.runtimeOwner || "delorenj";
1569
1811
  return `# ${role.displayName || role.agentId}
1570
1812
 
@@ -1584,7 +1826,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
1584
1826
 
1585
1827
  ## Scope
1586
1828
 
1587
- 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.
1588
1830
 
1589
1831
  ## Tone
1590
1832
 
@@ -1601,8 +1843,7 @@ Your memory is the submodule at \`./runtime/memories/\`. Use durable memory deli
1601
1843
  }
1602
1844
  function renderHermesWrapper(role) {
1603
1845
  return `#!/usr/bin/env bash
1604
- # Launcher for ${role.agentId}. Resolves HERMES_HOME through the fleet profile
1605
- # when available, then falls back to the local runtime submodule.
1846
+ # Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime submodule.
1606
1847
 
1607
1848
  set -euo pipefail
1608
1849
 
@@ -1621,12 +1862,7 @@ CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
1621
1862
 
1622
1863
  FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
1623
1864
  PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
1624
- PROFILE_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
1625
- if [[ -d "$PROFILE_HOME" ]]; then
1626
- HERMES_HOME="$PROFILE_HOME"
1627
- else
1628
- HERMES_HOME="$RUNTIME_HOME"
1629
- fi
1865
+ HERMES_HOME="$RUNTIME_HOME"
1630
1866
 
1631
1867
  if [[ ! -d "$RUNTIME_HOME" ]]; then
1632
1868
  echo "hermes: runtime submodule not initialized at $RUNTIME_HOME" >&2
@@ -1638,17 +1874,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
1638
1874
  `.replace(/\u0010/g, "$");
1639
1875
  }
1640
1876
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
1641
- if (!existsSync5(sourceDir)) return;
1642
- mkdirSync3(targetDir, { recursive: true });
1877
+ if (!existsSync7(sourceDir)) return;
1878
+ mkdirSync5(targetDir, { recursive: true });
1643
1879
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
1644
- const sourcePath = join6(sourceDir, entry.name);
1880
+ const sourcePath = join8(sourceDir, entry.name);
1645
1881
  if (skip?.(sourcePath)) continue;
1646
- const targetPath = join6(targetDir, entry.name);
1882
+ const targetPath = join8(targetDir, entry.name);
1647
1883
  if (entry.isDirectory()) {
1648
1884
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
1649
1885
  continue;
1650
1886
  }
1651
- if (existsSync5(targetPath)) continue;
1887
+ if (existsSync7(targetPath)) continue;
1652
1888
  changedFiles.push(targetPath);
1653
1889
  if (!dryRun) {
1654
1890
  ensureParent(targetPath);
@@ -1657,7 +1893,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
1657
1893
  }
1658
1894
  }
1659
1895
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
1660
- const gitmodulesPath = join6(repoRoot, ".gitmodules");
1896
+ const gitmodulesPath = join8(repoRoot, ".gitmodules");
1661
1897
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
1662
1898
  const owner = role.runtimeOwner || "delorenj";
1663
1899
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -1679,7 +1915,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
1679
1915
  repo: ${role.repo}
1680
1916
  role: ${role.role}
1681
1917
  display_name: ${JSON.stringify(role.displayName || role.agentId)}
1682
- project_path: ${ctxEscape(role.roleDir ? dirname3(dirname3(dirname3(role.roleDir))) : "")}
1918
+ project_path: ${ctxEscape(role.roleDir ? dirname5(dirname5(dirname5(role.roleDir))) : "")}
1683
1919
  role_dir: ${ctxEscape(role.roleDir)}
1684
1920
  profile_name: ${role.profileName || role.agentId}
1685
1921
  telegram:
@@ -1692,7 +1928,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
1692
1928
  systemd:
1693
1929
  gateway_unit: hermes-${role.agentId}-gateway.service
1694
1930
  consumer_unit: hermes-${role.agentId}-consumer.service
1695
- checkpoint_timer: hermes-${role.agentId}-checkpoint.timer
1931
+ heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
1696
1932
  `;
1697
1933
  const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
1698
1934
  ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
@@ -1757,13 +1993,17 @@ var RULES = [
1757
1993
  id: "mise.config-root",
1758
1994
  title: "mise config_root + AGENTS link hooks",
1759
1995
  audit: (ctx) => {
1760
- const misePath = join6(ctx.repoRoot, "mise.toml");
1761
- if (!existsSync5(misePath)) {
1996
+ const misePath = join8(ctx.repoRoot, "mise.toml");
1997
+ if (!existsSync7(misePath)) {
1762
1998
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
1763
1999
  }
1764
2000
  const text3 = readText(misePath);
1765
2001
  const details = [];
1766
- 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(", ")}`);
1767
2007
  if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
1768
2008
  if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
1769
2009
  if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
@@ -1778,25 +2018,37 @@ var RULES = [
1778
2018
  };
1779
2019
  },
1780
2020
  migrate: (ctx, finding) => {
1781
- const path = join6(ctx.repoRoot, "mise.toml");
2021
+ const path = join8(ctx.repoRoot, "mise.toml");
1782
2022
  const changedFiles = [];
1783
- if (!existsSync5(path)) {
2023
+ if (!existsSync7(path)) {
1784
2024
  return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; initialize mise first", changedFiles, details: [] };
1785
2025
  }
1786
2026
  let text3 = readText(path);
1787
- const next = upsertLinkAgentfilesBlock(text3);
2027
+ const next = upsertLinkAgentfilesBlock(text3, ctx);
1788
2028
  if (next !== text3) {
1789
2029
  changedFiles.push(path);
1790
2030
  if (!ctx.dryRun) writeText(path, next);
1791
2031
  text3 = next;
1792
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
+ }
1793
2045
  return {
1794
2046
  id: finding.id,
1795
2047
  title: finding.title,
1796
2048
  status: changedFiles.length ? "applied" : "noop",
1797
2049
  summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
1798
2050
  changedFiles,
1799
- 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"] : []
1800
2052
  };
1801
2053
  }
1802
2054
  },
@@ -1805,13 +2057,13 @@ var RULES = [
1805
2057
  title: "managed mise versioning block",
1806
2058
  audit: (ctx) => {
1807
2059
  const details = [];
1808
- const misePath = join6(ctx.repoRoot, "mise.toml");
1809
- const versioningPath = join6(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
1810
- 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");
1811
2063
  const text3 = safeReadText(misePath);
1812
2064
  if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
1813
- if (!existsSync5(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
1814
- 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");
1815
2067
  return {
1816
2068
  id: "mise.versioning",
1817
2069
  title: "managed mise versioning block",
@@ -1823,8 +2075,8 @@ var RULES = [
1823
2075
  },
1824
2076
  migrate: (ctx, finding) => {
1825
2077
  const changedFiles = [];
1826
- const misePath = join6(ctx.repoRoot, "mise.toml");
1827
- if (!existsSync5(misePath)) {
2078
+ const misePath = join8(ctx.repoRoot, "mise.toml");
2079
+ if (!existsSync7(misePath)) {
1828
2080
  return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; cannot inject versioning block", changedFiles, details: [] };
1829
2081
  }
1830
2082
  const currentMise = readText(misePath);
@@ -1833,7 +2085,7 @@ var RULES = [
1833
2085
  changedFiles.push(misePath);
1834
2086
  if (!ctx.dryRun) writeText(misePath, nextMise);
1835
2087
  }
1836
- const versioningPath = join6(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2088
+ const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
1837
2089
  const expectedScript = templateVersioningScript(ctx);
1838
2090
  if (expectedScript === void 0) {
1839
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: [] };
@@ -1845,7 +2097,7 @@ var RULES = [
1845
2097
  chmodSync(versioningPath, 493);
1846
2098
  }
1847
2099
  }
1848
- const manifestPath = join6(ctx.repoRoot, ".mise", "version-files.conf");
2100
+ const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
1849
2101
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
1850
2102
  if (safeReadText(manifestPath) !== expectedManifest) {
1851
2103
  changedFiles.push(manifestPath);
@@ -1865,13 +2117,24 @@ var RULES = [
1865
2117
  id: "sot.agent-symlinks",
1866
2118
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
1867
2119
  audit: (ctx) => {
1868
- const agentsPath = join6(ctx.repoRoot, "AGENTS.md");
1869
- if (!existsSync5(agentsPath)) {
1870
- 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
+ };
1871
2134
  }
1872
2135
  const details = [];
1873
2136
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1874
- const full = join6(ctx.repoRoot, file);
2137
+ const full = join8(ctx.repoRoot, file);
1875
2138
  const target = readSymlinkTarget(full);
1876
2139
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
1877
2140
  }
@@ -1887,22 +2150,26 @@ var RULES = [
1887
2150
  migrate: (ctx, finding) => {
1888
2151
  const changedFiles = [];
1889
2152
  const details = [];
1890
- if (!existsSync5(join6(ctx.repoRoot, "AGENTS.md"))) {
1891
- 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] };
1892
2159
  }
1893
2160
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1894
- const full = join6(ctx.repoRoot, file);
2161
+ const full = join8(ctx.repoRoot, file);
1895
2162
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
1896
- if (result.blocked) details.push(result.blocked);
2163
+ if (result.blocked) blockedDetails.push(result.blocked);
1897
2164
  if (result.changed) changedFiles.push(full);
1898
2165
  }
1899
2166
  return {
1900
2167
  id: finding.id,
1901
2168
  title: finding.title,
1902
- status: details.length ? "blocked" : changedFiles.length ? "applied" : "noop",
1903
- 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",
1904
2171
  changedFiles,
1905
- details
2172
+ details: [...details, ...blockedDetails]
1906
2173
  };
1907
2174
  }
1908
2175
  },
@@ -1913,7 +2180,7 @@ var RULES = [
1913
2180
  migrate: (ctx, finding) => {
1914
2181
  const changedFiles = [];
1915
2182
  const details = [];
1916
- const path = join6(ctx.repoRoot, ".project.json");
2183
+ const path = join8(ctx.repoRoot, ".project.json");
1917
2184
  const existing = readProjectJson(ctx) ?? {};
1918
2185
  const canonical = canonicalProjectJson(ctx);
1919
2186
  const merged = { ...existing, ...canonical };
@@ -1923,10 +2190,10 @@ var RULES = [
1923
2190
  changedFiles.push(path);
1924
2191
  if (!ctx.dryRun) writeText(path, expected);
1925
2192
  }
1926
- const planeJson = join6(ctx.repoRoot, ".plane.json");
1927
- if (existsSync5(planeJson)) {
2193
+ const planeJson = join8(ctx.repoRoot, ".plane.json");
2194
+ if (existsSync7(planeJson)) {
1928
2195
  const backup = `${planeJson}.migrated-backup`;
1929
- if (existsSync5(backup)) {
2196
+ if (existsSync7(backup)) {
1930
2197
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
1931
2198
  } else {
1932
2199
  changedFiles.push(backup);
@@ -1948,8 +2215,8 @@ var RULES = [
1948
2215
  title: ".env.op + gitignore secrets contract",
1949
2216
  audit: (ctx) => {
1950
2217
  const details = [];
1951
- const envOp = safeReadText(join6(ctx.repoRoot, ".env.op"));
1952
- const gitignore = safeReadText(join6(ctx.repoRoot, ".gitignore"));
2218
+ const envOp = safeReadText(join8(ctx.repoRoot, ".env.op"));
2219
+ const gitignore = safeReadText(join8(ctx.repoRoot, ".gitignore"));
1953
2220
  if (!envOp) {
1954
2221
  details.push(".env.op missing");
1955
2222
  } else {
@@ -1974,12 +2241,12 @@ var RULES = [
1974
2241
  migrate: (ctx, finding) => {
1975
2242
  const changedFiles = [];
1976
2243
  const details = [];
1977
- const envOpPath = join6(ctx.repoRoot, ".env.op");
1978
- if (!existsSync5(envOpPath)) {
2244
+ const envOpPath = join8(ctx.repoRoot, ".env.op");
2245
+ if (!existsSync7(envOpPath)) {
1979
2246
  changedFiles.push(envOpPath);
1980
- 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")));
1981
2248
  }
1982
- const gitignorePath = join6(ctx.repoRoot, ".gitignore");
2249
+ const gitignorePath = join8(ctx.repoRoot, ".gitignore");
1983
2250
  const gitignore = safeReadText(gitignorePath) ?? "";
1984
2251
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
1985
2252
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -2006,7 +2273,7 @@ var RULES = [
2006
2273
  title: ".copier-answers.yml provenance + drift report",
2007
2274
  audit: (ctx) => {
2008
2275
  const details = [];
2009
- const path = join6(ctx.repoRoot, ".copier-answers.yml");
2276
+ const path = join8(ctx.repoRoot, ".copier-answers.yml");
2010
2277
  const text3 = safeReadText(path);
2011
2278
  const project = readProjectJson(ctx);
2012
2279
  if (!text3) {
@@ -2037,12 +2304,12 @@ var RULES = [
2037
2304
  const changedFiles = [];
2038
2305
  const project = canonicalProjectJson(ctx);
2039
2306
  const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2040
- _src_path: ${join6(ctx.pjanglerRoot, "templates", "commonproject")}
2307
+ _src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
2041
2308
  project_description: ${String(project.project_description)}
2042
2309
  project_name: ${String(project.project_name)}
2043
2310
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2044
2311
  `;
2045
- const path = join6(ctx.repoRoot, ".copier-answers.yml");
2312
+ const path = join8(ctx.repoRoot, ".copier-answers.yml");
2046
2313
  if (safeReadText(path) !== text3) {
2047
2314
  changedFiles.push(path);
2048
2315
  if (!ctx.dryRun) writeText(path, text3);
@@ -2061,15 +2328,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2061
2328
  id: "bmad.scaffold",
2062
2329
  title: "BMAD modules/docs scaffold",
2063
2330
  audit: (ctx) => {
2064
- const sourceRoot = join6(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2065
- const targetRoot = join6(ctx.repoRoot, "_bmad");
2331
+ const sourceRoot = join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2332
+ const targetRoot = join8(ctx.repoRoot, "_bmad");
2066
2333
  const sentinels = [
2067
- join6("core", "config.yaml"),
2068
- join6("custom", "config.yaml"),
2069
- join6("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2070
- 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")
2071
2338
  ];
2072
- 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)));
2073
2340
  return {
2074
2341
  id: "bmad.scaffold",
2075
2342
  title: "BMAD modules/docs scaffold",
@@ -2081,7 +2348,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2081
2348
  },
2082
2349
  migrate: (ctx, finding) => {
2083
2350
  const changedFiles = [];
2084
- 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);
2085
2352
  return {
2086
2353
  id: finding.id,
2087
2354
  title: finding.title,
@@ -2102,12 +2369,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2102
2369
  return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
2103
2370
  }
2104
2371
  const details = [];
2105
- 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"]) {
2106
- 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))}`);
2107
2374
  }
2108
- const gitmodules = safeReadText(join6(ctx.repoRoot, ".gitmodules")) ?? "";
2375
+ const gitmodules = safeReadText(join8(ctx.repoRoot, ".gitmodules")) ?? "";
2109
2376
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
2110
- if (!profileMetaInheritsDefault(join6(role.roleDir, "runtime", "profile.yaml"))) {
2377
+ if (!profileMetaInheritsDefault(join8(role.roleDir, "runtime", "profile.yaml"))) {
2111
2378
  details.push("runtime/profile.yaml missing inherited default config metadata");
2112
2379
  }
2113
2380
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -2128,79 +2395,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2128
2395
  if (!role) {
2129
2396
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
2130
2397
  }
2131
- const templateRoleDir = join6(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2132
- writeIfDifferent(join6(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2133
- writeIfDifferent(join6(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2134
- writeIfDifferent(join6(role.roleDir, ".gitignore"), readText(join6(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2135
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2136
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2137
- copyMissingRecursive(join6(templateRoleDir, ".scripts"), join6(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("continuous-ticket-sentinel.prompt.md.jinja"));
2138
- upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2139
- const profileMetaUpdated = upsertInheritedProfileMeta(join6(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2140
- if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2141
- const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2142
- if (registryUpdated) details.push(`updated ${registryUpdated}`);
2143
- return {
2144
- id: finding.id,
2145
- title: finding.title,
2146
- status: changedFiles.length ? "applied" : "noop",
2147
- summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
2148
- changedFiles,
2149
- details
2150
- };
2151
- }
2152
- },
2153
- {
2154
- id: "hermes.scrum-master-scaffold",
2155
- title: "Hermes scrum-master scaffold parity",
2156
- audit: (ctx) => {
2157
- const roles = discoverRoles(ctx.repoRoot);
2158
- const role = roles.find((item) => item.role === "scrum-master");
2159
- if (!role) {
2160
- return { id: "hermes.scrum-master-scaffold", title: "Hermes scrum-master scaffold parity", status: "skip", summary: "No scrum-master role present", details: [], fixable: false };
2161
- }
2162
- const details = [];
2163
- 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"]) {
2164
- if (!existsSync5(join6(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join6(role.roleDir, rel))}`);
2165
- }
2166
- const gitmodules = safeReadText(join6(ctx.repoRoot, ".gitmodules")) ?? "";
2167
- if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing scrum-master runtime submodule entry");
2168
- if (!profileMetaInheritsDefault(join6(role.roleDir, "runtime", "profile.yaml"))) {
2169
- details.push("runtime/profile.yaml missing inherited default config metadata");
2170
- }
2171
- const registry = safeReadText(registryPath(ctx.homeDir));
2172
- if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
2173
- return {
2174
- id: "hermes.scrum-master-scaffold",
2175
- title: "Hermes scrum-master scaffold parity",
2176
- status: details.length === 0 ? "pass" : "fail",
2177
- summary: details.length === 0 ? "scrum-master scaffold parity verified" : `${details.length} scrum-master scaffold issue(s) detected`,
2178
- details,
2179
- fixable: true
2180
- };
2181
- },
2182
- migrate: (ctx, finding) => {
2183
- const role = discoverRoles(ctx.repoRoot).find((item) => item.role === "scrum-master");
2184
- const changedFiles = [];
2185
- const details = [];
2186
- if (!role) {
2187
- return { id: finding.id, title: finding.title, status: "blocked", summary: "No scrum-master role present", changedFiles, details: [] };
2188
- }
2189
- const templateRoleDir = join6(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2190
- writeIfDifferent(join6(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2191
- writeIfDifferent(join6(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2192
- writeIfDifferent(join6(role.roleDir, ".gitignore"), readText(join6(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2193
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2194
- copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2195
- copyMissingRecursive(join6(templateRoleDir, ".scripts"), join6(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("continuous-ticket-sentinel.prompt.md.jinja"));
2196
- const promptSource = join6(templateRoleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md.jinja");
2197
- const promptTarget = join6(role.roleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md");
2198
- if (!existsSync5(promptTarget)) {
2199
- 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);
2200
2409
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
2201
2410
  }
2202
2411
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2203
- 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);
2204
2413
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2205
2414
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2206
2415
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -2208,7 +2417,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2208
2417
  id: finding.id,
2209
2418
  title: finding.title,
2210
2419
  status: changedFiles.length ? "applied" : "noop",
2211
- summary: changedFiles.length ? "scrum-master scaffold normalized" : "No changes required",
2420
+ summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
2212
2421
  changedFiles,
2213
2422
  details
2214
2423
  };
@@ -2228,14 +2437,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2228
2437
  }
2229
2438
  const details = [];
2230
2439
  for (const role of roles) {
2231
- 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`]) {
2232
2441
  const state = checkUnit(unit);
2233
2442
  if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
2234
2443
  }
2235
- if (role.role === "scrum-master") {
2236
- const state = checkUnit(`hermes-${role.agentId}-continuous-ticket-sentinel.timer`);
2237
- if (!state.enabled || !state.active) details.push(`hermes-${role.agentId}-continuous-ticket-sentinel.timer should be enabled+active`);
2238
- }
2239
2444
  }
2240
2445
  return {
2241
2446
  id: "systemd.sentinel",
@@ -2258,10 +2463,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2258
2463
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
2259
2464
  }
2260
2465
  for (const role of roles) {
2261
- const sysDir = join6(ctx.homeDir, ".config", "systemd", "user");
2262
- const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-checkpoint.timer`];
2263
- if (role.role === "scrum-master") units.push(`hermes-${role.agentId}-continuous-ticket-sentinel.timer`);
2264
- 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)));
2265
2469
  if (allUnitsPresent) {
2266
2470
  if (ctx.dryRun) {
2267
2471
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -2273,8 +2477,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2273
2477
  }
2274
2478
  continue;
2275
2479
  }
2276
- for (const script of [join6(role.roleDir, ".scripts", "70-systemd.sh"), role.role === "scrum-master" ? join6(role.roleDir, ".scripts", "75-scrum-master.sh") : ""]) {
2277
- if (!script || !existsSync5(script)) continue;
2480
+ for (const script of [join8(role.roleDir, ".scripts", "70-systemd.sh")]) {
2481
+ if (!script || !existsSync7(script)) continue;
2278
2482
  if (ctx.dryRun) {
2279
2483
  details.push(`would run: bash ${script}`);
2280
2484
  } else {
@@ -2310,7 +2514,7 @@ function runAudit(repoArg) {
2310
2514
  repoRoot: resolve(repoArg ?? process.cwd()),
2311
2515
  dryRun: true,
2312
2516
  pjanglerRoot,
2313
- homeDir: homedir3()
2517
+ homeDir: homedir4()
2314
2518
  };
2315
2519
  const rules = RULES.map((rule) => rule.audit(ctx));
2316
2520
  return {
@@ -2326,7 +2530,7 @@ function runMigration(selector, repoArg, dryRun, all) {
2326
2530
  repoRoot: resolve(repoArg ?? process.cwd()),
2327
2531
  dryRun,
2328
2532
  pjanglerRoot,
2329
- homeDir: homedir3()
2533
+ homeDir: homedir4()
2330
2534
  };
2331
2535
  const selected = all ? RULES : RULES.filter((rule) => rule.id === selector);
2332
2536
  if (!selected.length) {
@@ -2381,18 +2585,18 @@ function formatMigrationReport(report) {
2381
2585
  }
2382
2586
 
2383
2587
  // src/utils/version.ts
2384
- import { readFileSync as readFileSync3 } from "node:fs";
2385
- import { dirname as dirname4, join as join7 } from "node:path";
2386
- 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";
2387
2591
  var PJANGLER_VERSION = (() => {
2388
2592
  try {
2389
- let dir = dirname4(fileURLToPath3(import.meta.url));
2593
+ let dir = dirname6(fileURLToPath4(import.meta.url));
2390
2594
  for (let i = 0; i < 4; i++) {
2391
2595
  try {
2392
- const raw = readFileSync3(join7(dir, "package.json"), "utf8");
2596
+ const raw = readFileSync4(join9(dir, "package.json"), "utf8");
2393
2597
  return JSON.parse(raw).version ?? "0.0.0";
2394
2598
  } catch {
2395
- const parent = dirname4(dir);
2599
+ const parent = dirname6(dir);
2396
2600
  if (parent === dir) break;
2397
2601
  dir = parent;
2398
2602
  }