supipowers 1.5.3 → 2.0.0

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 (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +131 -42
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +18 -8
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +129 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +264 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +221 -109
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +114 -13
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +1 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1399 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -11,12 +11,18 @@ import type {
11
11
  ReviewAgentDefinition,
12
12
  ReviewAgentsConfig,
13
13
  } from "../types.js";
14
- import { normalizeLineEndings } from "../text.js";
14
+ import { parseMarkdownFrontmatter } from "../markdown-frontmatter.js";
15
+ import { resolvePackageManager } from "../workspace/package-manager.js";
16
+ import { resolveRepoRootFromFs } from "../workspace/repo-root.js";
17
+ import {
18
+ getRootStateDir,
19
+ getWorkspaceStateDir,
20
+ } from "../workspace/state-paths.js";
21
+ import { discoverWorkspaceTargets } from "../workspace/targets.js";
22
+ import { collectValidationErrors, formatValidationErrors } from "../ai/structured-output.js";
15
23
  import {
16
24
  ReviewAgentFrontmatterSchema,
17
25
  ReviewAgentsConfigSchema,
18
- collectReviewValidationErrors,
19
- formatReviewValidationErrors,
20
26
  } from "./types.js";
21
27
 
22
28
  const REVIEW_AGENTS_DIR = "review-agents";
@@ -41,6 +47,16 @@ export interface LoadedReviewAgents {
41
47
  agents: ConfiguredReviewAgent[];
42
48
  }
43
49
 
50
+ export interface ReviewAgentLoadOptions {
51
+ repoRoot?: string;
52
+ workspaceRelativeDir?: string | null;
53
+ }
54
+
55
+ export interface ResolvedReviewAgentContext {
56
+ repoRoot: string;
57
+ workspaceRelativeDir: string | null;
58
+ }
59
+
44
60
  const CONFIG_HEADER = [
45
61
  "# Review Agents Configuration",
46
62
  "#",
@@ -50,6 +66,7 @@ const CONFIG_HEADER = [
50
66
  "# data: string - markdown file name in the agents directory",
51
67
  "# model: string - model id (e.g. \"anthropic/claude-sonnet-4-20250514\") or null to inherit",
52
68
  "# thinkingLevel: string - off | minimal | low | medium | high | xhigh | null to inherit",
69
+ "# peerCoordination: boolean - opt this agent into IRC peer coordination (multi-agent reviews only); omit or set false to disable",
53
70
  "#",
54
71
  ].join("\n");
55
72
 
@@ -65,6 +82,7 @@ function serializeConfigYaml(agents: ReviewAgentConfig[]): string {
65
82
  ` data: ${a.data}`,
66
83
  ` model: ${a.model ?? "null"}`,
67
84
  ` thinkingLevel: ${a.thinkingLevel ?? "null"}`,
85
+ ...(a.peerCoordination === true ? [` peerCoordination: true`] : []),
68
86
  ]),
69
87
  "",
70
88
  ].join("\n");
@@ -113,6 +131,13 @@ function migrateConfigIfNeeded(configPath: string): void {
113
131
  } else if (current && trimmed.startsWith("thinkingLevel:")) {
114
132
  const val = trimmed.slice("thinkingLevel:".length).trim();
115
133
  current.thinkingLevel = val === "null" ? null : (val as any);
134
+ } else if (current && trimmed.startsWith("peerCoordination:")) {
135
+ const val = trimmed.slice("peerCoordination:".length).trim();
136
+ if (val === "true") {
137
+ current.peerCoordination = true;
138
+ } else if (val === "false") {
139
+ current.peerCoordination = false;
140
+ }
116
141
  }
117
142
  }
118
143
  if (current?.name) agents.push(current as ReviewAgentConfig);
@@ -128,7 +153,7 @@ function migrateConfigIfNeeded(configPath: string): void {
128
153
  }
129
154
 
130
155
  function validateReviewAgentsConfig(data: unknown): ReviewAgentsConfig {
131
- const errors = formatReviewValidationErrors(collectReviewValidationErrors(ReviewAgentsConfigSchema, data));
156
+ const errors = formatValidationErrors(collectValidationErrors(ReviewAgentsConfigSchema, data));
132
157
  if (errors.length > 0) {
133
158
  throw new Error(`Invalid review-agents config: ${errors.join("; ")}`);
134
159
  }
@@ -142,57 +167,27 @@ async function importYamlFile(filePath: string): Promise<unknown> {
142
167
  return imported.default;
143
168
  }
144
169
 
145
- function parseFrontmatter(frontmatter: string, filePath: string): ReviewAgentDefinition {
146
- const metadata: Record<string, unknown> = {};
147
-
148
- for (const line of frontmatter.split("\n")) {
149
- const trimmed = line.trim();
150
- if (!trimmed) {
151
- continue;
152
- }
153
-
154
- const separator = trimmed.indexOf(":");
155
- if (separator === -1) {
156
- continue;
157
- }
158
-
159
- const key = trimmed.slice(0, separator).trim();
160
- const value = trimmed.slice(separator + 1).trim();
161
- metadata[key] = value;
162
- }
163
-
164
- const errors = formatReviewValidationErrors(
165
- collectReviewValidationErrors(ReviewAgentFrontmatterSchema, metadata),
170
+ export function parseReviewAgentMarkdown(content: string, filePath: string): ReviewAgentDefinition {
171
+ const { frontmatter, body } = parseMarkdownFrontmatter(content, filePath);
172
+ const errors = formatValidationErrors(
173
+ collectValidationErrors(ReviewAgentFrontmatterSchema, frontmatter),
166
174
  );
167
175
  if (errors.length > 0) {
168
176
  throw new Error(`Invalid agent frontmatter in ${filePath}: ${errors.join("; ")}`);
169
177
  }
170
178
 
171
- return {
172
- name: String(metadata.name),
173
- description: String(metadata.description),
174
- focus: typeof metadata.focus === "string" ? metadata.focus : null,
175
- prompt: "",
176
- filePath,
179
+ const parsed = frontmatter as {
180
+ name: string;
181
+ description: string;
182
+ focus?: string;
177
183
  };
178
- }
179
-
180
- export function parseReviewAgentMarkdown(content: string, filePath: string): ReviewAgentDefinition {
181
- const normalized = normalizeLineEndings(content);
182
- const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
183
- if (!match) {
184
- throw new Error(`Review agent file ${filePath} is missing YAML frontmatter.`);
185
- }
186
-
187
- const definition = parseFrontmatter(match[1], filePath);
188
- const prompt = match[2]?.trim();
189
- if (!prompt) {
190
- throw new Error(`Review agent file ${filePath} has an empty prompt body.`);
191
- }
192
184
 
193
185
  return {
194
- ...definition,
195
- prompt,
186
+ name: parsed.name,
187
+ description: parsed.description,
188
+ focus: parsed.focus ?? null,
189
+ prompt: body,
190
+ filePath,
196
191
  };
197
192
  }
198
193
 
@@ -204,37 +199,71 @@ export function getReviewAgentsConfigPath(paths: PlatformPaths, cwd: string): st
204
199
  return path.join(getReviewAgentsDir(paths, cwd), CONFIG_FILE);
205
200
  }
206
201
 
207
- export function ensureDefaultReviewAgents(paths: PlatformPaths, cwd: string): void {
208
- const agentsDir = getReviewAgentsDir(paths, cwd);
209
- fs.mkdirSync(agentsDir, { recursive: true });
202
+ export function getRootReviewAgentsDir(paths: PlatformPaths, repoRoot: string): string {
203
+ return path.join(getRootStateDir(paths, repoRoot), REVIEW_AGENTS_DIR);
204
+ }
210
205
 
211
- // Default agent markdown files are installed globally only.
212
- writeIfMissing(getReviewAgentsConfigPath(paths, cwd), buildDefaultConfigText());
213
- migrateConfigIfNeeded(getReviewAgentsConfigPath(paths, cwd));
206
+ export function getRootReviewAgentsConfigPath(paths: PlatformPaths, repoRoot: string): string {
207
+ return path.join(getRootReviewAgentsDir(paths, repoRoot), CONFIG_FILE);
214
208
  }
215
209
 
216
- export async function loadReviewAgentsConfig(paths: PlatformPaths, cwd: string): Promise<ReviewAgentsConfig> {
217
- ensureGlobalDefaultReviewAgents(paths);
218
- ensureDefaultReviewAgents(paths, cwd);
219
- return validateReviewAgentsConfig(await importYamlFile(getReviewAgentsConfigPath(paths, cwd)));
210
+ export function getWorkspaceReviewAgentsDir(
211
+ paths: PlatformPaths,
212
+ repoRoot: string,
213
+ workspaceRelativeDir: string,
214
+ ): string {
215
+ return path.join(getWorkspaceStateDir(paths, repoRoot, workspaceRelativeDir), REVIEW_AGENTS_DIR);
220
216
  }
221
217
 
222
- export async function loadReviewAgents(paths: PlatformPaths, cwd: string): Promise<LoadedReviewAgents> {
223
- const agentsDir = getReviewAgentsDir(paths, cwd);
224
- const configPath = getReviewAgentsConfigPath(paths, cwd);
225
- const globalAgentsDir = getGlobalReviewAgentsDir(paths);
226
- const config = await loadReviewAgentsConfig(paths, cwd);
218
+ export function getWorkspaceReviewAgentsConfigPath(
219
+ paths: PlatformPaths,
220
+ repoRoot: string,
221
+ workspaceRelativeDir: string,
222
+ ): string {
223
+ return path.join(getWorkspaceReviewAgentsDir(paths, repoRoot, workspaceRelativeDir), CONFIG_FILE);
224
+ }
225
+
226
+
227
+ export function resolveReviewAgentContext(
228
+ cwd: string,
229
+ options?: ReviewAgentLoadOptions,
230
+ ): ResolvedReviewAgentContext {
231
+ const repoRoot = options?.repoRoot ?? resolveRepoRootFromFs(cwd);
232
+ const explicitWorkspaceRelativeDir = options?.workspaceRelativeDir;
233
+ if (explicitWorkspaceRelativeDir !== undefined) {
234
+ return {
235
+ repoRoot,
236
+ workspaceRelativeDir: explicitWorkspaceRelativeDir && explicitWorkspaceRelativeDir !== "."
237
+ ? explicitWorkspaceRelativeDir
238
+ : null,
239
+ };
240
+ }
241
+
242
+ const packageManager = resolvePackageManager(repoRoot);
243
+ const workspaceTarget = discoverWorkspaceTargets(repoRoot, packageManager.id)
244
+ .filter((target) => cwd === target.packageDir || cwd.startsWith(`${target.packageDir}${path.sep}`))
245
+ .sort((left, right) => right.packageDir.length - left.packageDir.length)[0];
227
246
 
228
- const agents = config.agents
247
+ return {
248
+ repoRoot,
249
+ workspaceRelativeDir: workspaceTarget?.kind === "workspace" ? workspaceTarget.relativeDir : null,
250
+ };
251
+ }
252
+
253
+ function loadAgentsFromConfig(
254
+ config: ReviewAgentsConfig,
255
+ lookupDirs: string[],
256
+ missingScopeLabel: string,
257
+ scope?: ConfiguredReviewAgent["scope"],
258
+ ): ConfiguredReviewAgent[] {
259
+ return config.agents
229
260
  .filter((agent) => agent.enabled)
230
261
  .map((agent) => {
231
- const projectFilePath = path.join(agentsDir, agent.data);
232
- const globalFilePath = path.join(globalAgentsDir, agent.data);
233
- const filePath = fs.existsSync(projectFilePath) ? projectFilePath : globalFilePath;
234
- if (!fs.existsSync(filePath)) {
235
- throw new Error(
236
- `Configured review agent file does not exist in project or global scope: ${agent.data}`
237
- );
262
+ const filePath = lookupDirs
263
+ .map((dir) => path.join(dir, agent.data))
264
+ .find((candidatePath) => fs.existsSync(candidatePath));
265
+ if (!filePath) {
266
+ throw new Error(`Configured review agent file does not exist in ${missingScopeLabel}: ${agent.data}`);
238
267
  }
239
268
 
240
269
  const definition = parseReviewAgentMarkdown(fs.readFileSync(filePath, "utf-8"), filePath);
@@ -250,8 +279,55 @@ export async function loadReviewAgents(paths: PlatformPaths, cwd: string): Promi
250
279
  data: agent.data,
251
280
  model: agent.model,
252
281
  thinkingLevel: agent.thinkingLevel ?? null,
282
+ ...(agent.peerCoordination === true ? { peerCoordination: true } : {}),
283
+ ...(scope ? { scope } : {}),
253
284
  } satisfies ConfiguredReviewAgent;
254
285
  });
286
+ }
287
+
288
+ function mergeAgentLayers(
289
+ lowerAgents: ConfiguredReviewAgent[],
290
+ higherAgents: ConfiguredReviewAgent[],
291
+ higherConfig: ReviewAgentsConfig,
292
+ ): ConfiguredReviewAgent[] {
293
+ const higherConfigNames = new Set(higherConfig.agents.map((agent) => agent.name));
294
+ return [
295
+ ...lowerAgents.filter((agent) => !higherConfigNames.has(agent.name)),
296
+ ...higherAgents,
297
+ ];
298
+ }
299
+
300
+ export function ensureDefaultReviewAgents(paths: PlatformPaths, cwd: string): void {
301
+ const agentsDir = getReviewAgentsDir(paths, cwd);
302
+ fs.mkdirSync(agentsDir, { recursive: true });
303
+
304
+ // Default agent markdown files are installed globally only.
305
+ writeIfMissing(getReviewAgentsConfigPath(paths, cwd), buildDefaultConfigText());
306
+ migrateConfigIfNeeded(getReviewAgentsConfigPath(paths, cwd));
307
+ }
308
+
309
+ export async function loadReviewAgentsConfig(
310
+ paths: PlatformPaths,
311
+ cwd: string,
312
+ options?: ReviewAgentLoadOptions,
313
+ ): Promise<ReviewAgentsConfig> {
314
+ const context = resolveReviewAgentContext(cwd, options);
315
+ ensureGlobalDefaultReviewAgents(paths);
316
+ ensureDefaultReviewAgents(paths, context.repoRoot);
317
+ return validateReviewAgentsConfig(await importYamlFile(getRootReviewAgentsConfigPath(paths, context.repoRoot)));
318
+ }
319
+
320
+ export async function loadReviewAgents(
321
+ paths: PlatformPaths,
322
+ cwd: string,
323
+ options?: ReviewAgentLoadOptions,
324
+ ): Promise<LoadedReviewAgents> {
325
+ const context = resolveReviewAgentContext(cwd, options);
326
+ const agentsDir = getRootReviewAgentsDir(paths, context.repoRoot);
327
+ const configPath = getRootReviewAgentsConfigPath(paths, context.repoRoot);
328
+ const globalAgentsDir = getGlobalReviewAgentsDir(paths);
329
+ const config = await loadReviewAgentsConfig(paths, cwd, context);
330
+ const agents = loadAgentsFromConfig(config, [agentsDir, globalAgentsDir], "root or global scope");
255
331
 
256
332
  return {
257
333
  agentsDir,
@@ -292,31 +368,64 @@ export async function loadGlobalReviewAgents(paths: PlatformPaths): Promise<Load
292
368
  const agentsDir = getGlobalReviewAgentsDir(paths);
293
369
  const configPath = getGlobalReviewAgentsConfigPath(paths);
294
370
  const config = await loadGlobalReviewAgentsConfig(paths);
371
+ const agents = loadAgentsFromConfig(config, [agentsDir], "global scope", "global");
295
372
 
296
- const agents = config.agents
297
- .filter((agent) => agent.enabled)
298
- .map((agent) => {
299
- const filePath = path.join(agentsDir, agent.data);
300
- if (!fs.existsSync(filePath)) {
301
- throw new Error(`Configured review agent file does not exist: ${filePath}`);
302
- }
373
+ return {
374
+ agentsDir,
375
+ configPath,
376
+ config,
377
+ agents,
378
+ };
379
+ }
303
380
 
304
- const definition = parseReviewAgentMarkdown(fs.readFileSync(filePath, "utf-8"), filePath);
305
- if (definition.name !== agent.name) {
306
- throw new Error(
307
- `Configured agent name "${agent.name}" does not match frontmatter name "${definition.name}" in ${filePath}.`,
308
- );
309
- }
381
+ async function loadWorkspaceReviewAgentsConfig(
382
+ paths: PlatformPaths,
383
+ cwd: string,
384
+ options: ReviewAgentLoadOptions,
385
+ ): Promise<ReviewAgentsConfig> {
386
+ const context = resolveReviewAgentContext(cwd, options);
387
+ if (!context.workspaceRelativeDir) {
388
+ return { agents: [] };
389
+ }
310
390
 
311
- return {
312
- ...definition,
313
- enabled: agent.enabled,
314
- data: agent.data,
315
- model: agent.model,
316
- thinkingLevel: agent.thinkingLevel ?? null,
317
- scope: "global" as const,
318
- } satisfies ConfiguredReviewAgent;
319
- });
391
+ const configPath = getWorkspaceReviewAgentsConfigPath(
392
+ paths,
393
+ context.repoRoot,
394
+ context.workspaceRelativeDir,
395
+ );
396
+ if (!fs.existsSync(configPath)) {
397
+ return { agents: [] };
398
+ }
399
+
400
+ migrateConfigIfNeeded(configPath);
401
+ return validateReviewAgentsConfig(await importYamlFile(configPath));
402
+ }
403
+
404
+ async function loadWorkspaceReviewAgents(
405
+ paths: PlatformPaths,
406
+ cwd: string,
407
+ options: ReviewAgentLoadOptions,
408
+ ): Promise<LoadedReviewAgents | null> {
409
+ const context = resolveReviewAgentContext(cwd, options);
410
+ if (!context.workspaceRelativeDir) {
411
+ return null;
412
+ }
413
+
414
+ const agentsDir = getWorkspaceReviewAgentsDir(paths, context.repoRoot, context.workspaceRelativeDir);
415
+ const configPath = getWorkspaceReviewAgentsConfigPath(
416
+ paths,
417
+ context.repoRoot,
418
+ context.workspaceRelativeDir,
419
+ );
420
+ const rootAgentsDir = getRootReviewAgentsDir(paths, context.repoRoot);
421
+ const globalAgentsDir = getGlobalReviewAgentsDir(paths);
422
+ const config = await loadWorkspaceReviewAgentsConfig(paths, cwd, context);
423
+ const agents = loadAgentsFromConfig(
424
+ config,
425
+ [agentsDir, rootAgentsDir, globalAgentsDir],
426
+ "workspace, root, or global scope",
427
+ "workspace",
428
+ );
320
429
 
321
430
  return {
322
431
  agentsDir,
@@ -326,31 +435,34 @@ export async function loadGlobalReviewAgents(paths: PlatformPaths): Promise<Load
326
435
  };
327
436
  }
328
437
 
329
- // ── Merged Loading (Global + Project) ──────────────────────
438
+ // ── Merged Loading (Global + Root + Workspace) ───────────────
330
439
 
331
440
  export async function loadMergedReviewAgents(
332
441
  paths: PlatformPaths,
333
442
  cwd: string,
443
+ options?: ReviewAgentLoadOptions,
334
444
  ): Promise<LoadedReviewAgents> {
445
+ const context = resolveReviewAgentContext(cwd, options);
335
446
  const globalResult = await loadGlobalReviewAgents(paths);
336
- const projectResult = await loadReviewAgents(paths, cwd);
337
-
338
- // Project config is authoritative: any agent named in the project config
339
- // (enabled or disabled) shadows the global version with the same name.
340
- const projectConfigNames = new Set(projectResult.config.agents.map((a) => a.name));
341
- const uniqueGlobalAgents = globalResult.agents.filter((a) => !projectConfigNames.has(a.name));
342
-
343
- // Tag project agents with scope
344
- const projectAgents = projectResult.agents.map((agent) => ({
345
- ...agent,
346
- scope: "project" as const,
347
- }));
447
+ const rootResult = await loadReviewAgents(paths, cwd, context);
448
+ const rootAgents = rootResult.agents.map((agent) => ({ ...agent, scope: "root" as const }));
449
+ const mergedRootAgents = mergeAgentLayers(globalResult.agents, rootAgents, rootResult.config);
450
+
451
+ const workspaceResult = await loadWorkspaceReviewAgents(paths, cwd, context);
452
+ if (!workspaceResult) {
453
+ return {
454
+ agentsDir: rootResult.agentsDir,
455
+ configPath: rootResult.configPath,
456
+ config: rootResult.config,
457
+ agents: mergedRootAgents,
458
+ };
459
+ }
348
460
 
349
461
  return {
350
- agentsDir: projectResult.agentsDir,
351
- configPath: projectResult.configPath,
352
- config: projectResult.config,
353
- agents: [...uniqueGlobalAgents, ...projectAgents],
462
+ agentsDir: workspaceResult.agentsDir,
463
+ configPath: workspaceResult.configPath,
464
+ config: workspaceResult.config,
465
+ agents: mergeAgentLayers(mergedRootAgents, workspaceResult.agents, workspaceResult.config),
354
466
  };
355
467
  }
356
468
 
@@ -1,5 +1,4 @@
1
1
  import fixFindingsPrompt from "./prompts/fix-findings.md" with { type: "text" };
2
- import fixOutputSchemaPrompt from "./prompts/fix-output-schema.md" with { type: "text" };
3
2
  import type {
4
3
  GateExecutionContext,
5
4
  ReviewFinding,
@@ -8,9 +7,12 @@ import type {
8
7
  ReviewOutput,
9
8
  ReviewScope,
10
9
  } from "../types.js";
11
- import { parseStructuredOutput, runWithOutputValidation } from "./output.js";
10
+ import { parseStructuredOutput, runWithOutputValidation, type ReliabilityReporter } from "../ai/structured-output.js";
11
+ import { renderSchemaText } from "../ai/schema-text.js";
12
12
  import { ReviewFixOutputSchema } from "./types.js";
13
- import { renderReviewTemplate } from "./template.js";
13
+ import { renderTemplate } from "../ai/template.js";
14
+
15
+ const REVIEW_FIX_OUTPUT_SCHEMA_TEXT = renderSchemaText(ReviewFixOutputSchema);
14
16
 
15
17
  export interface ReviewFixInput {
16
18
  cwd: string;
@@ -20,6 +22,7 @@ export interface ReviewFixInput {
20
22
  model?: string;
21
23
  thinkingLevel?: string | null;
22
24
  timeoutMs?: number;
25
+ reliability?: ReliabilityReporter;
23
26
  }
24
27
 
25
28
  export interface ReviewFixRunResult {
@@ -127,10 +130,10 @@ function normalizeFixOutput(
127
130
  }
128
131
 
129
132
  export function buildFixPrompt(scope: ReviewScope, findings: ReviewFinding[]): string {
130
- return renderReviewTemplate(fixFindingsPrompt, {
133
+ return renderTemplate(fixFindingsPrompt, {
131
134
  scope,
132
135
  findingsJson: JSON.stringify(findings, null, 2),
133
- fixOutputSchema: fixOutputSchemaPrompt.trim(),
136
+ fixOutputSchema: REVIEW_FIX_OUTPUT_SCHEMA_TEXT,
134
137
  });
135
138
  }
136
139
 
@@ -158,13 +161,14 @@ export async function runAutoFix(input: ReviewFixInput): Promise<ReviewFixRunRes
158
161
  const result = await runWithOutputValidation(input.createAgentSession, {
159
162
  cwd: input.cwd,
160
163
  prompt: buildFixPrompt(input.scope, fixableFindings),
161
- schema: fixOutputSchemaPrompt.trim(),
164
+ schema: REVIEW_FIX_OUTPUT_SCHEMA_TEXT,
162
165
  parse(raw) {
163
166
  return parseStructuredOutput<ReviewFixOutput>(raw, ReviewFixOutputSchema);
164
167
  },
165
168
  model: input.model,
166
169
  thinkingLevel: input.thinkingLevel ?? null,
167
170
  timeoutMs: input.timeoutMs ?? 180_000,
171
+ reliability: input.reliability,
168
172
  });
169
173
 
170
174
  if (result.status === "blocked") {
@@ -1,9 +1,13 @@
1
1
  import agentReviewWrapperPrompt from "./prompts/agent-review-wrapper.md" with { type: "text" };
2
2
  import outputInstructionsPrompt from "./prompts/output-instructions.md" with { type: "text" };
3
- import reviewOutputSchema from "./prompts/review-output-schema.md" with { type: "text" };
4
3
  import type { ConfiguredReviewAgent, GateExecutionContext, ReviewOutput, ReviewScope } from "../types.js";
5
- import { explainReviewOutputFailure, parseReviewOutput, runWithOutputValidation } from "./output.js";
6
- import { renderReviewTemplate } from "./template.js";
4
+ import { runWithOutputValidation, type ReliabilityReporter } from "../ai/structured-output.js";
5
+ import { renderSchemaText } from "../ai/schema-text.js";
6
+ import { explainReviewOutputFailure, parseReviewOutput } from "./output.js";
7
+ import { renderTemplate } from "../ai/template.js";
8
+ import { ReviewOutputSchema } from "./types.js";
9
+
10
+ const REVIEW_OUTPUT_SCHEMA_TEXT = renderSchemaText(ReviewOutputSchema);
7
11
 
8
12
  export interface MultiAgentReviewInput {
9
13
  cwd: string;
@@ -13,8 +17,11 @@ export interface MultiAgentReviewInput {
13
17
  model?: string;
14
18
  thinkingLevel?: string | null;
15
19
  timeoutMs?: number;
20
+ /** Tool ids active in the host runtime. Used to gate `peerCoordination` on `irc`. */
21
+ activeTools?: string[];
16
22
  onAgentStart?: (agent: ConfiguredReviewAgent) => void;
17
23
  onAgentComplete?: (result: MultiAgentAgentResult) => void;
24
+ reliability?: ReliabilityReporter;
18
25
  }
19
26
 
20
27
  export interface MultiAgentAgentResult {
@@ -30,8 +37,8 @@ export interface MultiAgentReviewResult {
30
37
  }
31
38
 
32
39
  function renderOutputInstructions(): string {
33
- return renderReviewTemplate(outputInstructionsPrompt, {
34
- outputSchema: reviewOutputSchema.trim(),
40
+ return renderTemplate(outputInstructionsPrompt, {
41
+ outputSchema: REVIEW_OUTPUT_SCHEMA_TEXT,
35
42
  });
36
43
  }
37
44
 
@@ -41,18 +48,95 @@ export function buildConfiguredAgentPrompt(agent: ConfiguredReviewAgent, scope:
41
48
  }
42
49
 
43
50
  const outputInstructions = renderOutputInstructions();
44
- const agentPrompt = renderReviewTemplate(
45
- agent.prompt.replaceAll("{output_instructions}", "{{outputInstructions}}"),
46
- { outputInstructions },
47
- );
51
+ const agentPrompt = renderTemplate(agent.prompt.replaceAll("{output_instructions}", "{{outputInstructions}}"),
52
+ { outputInstructions },);
48
53
 
49
- return renderReviewTemplate(agentReviewWrapperPrompt, {
54
+ return renderTemplate(agentReviewWrapperPrompt, {
50
55
  agent,
51
56
  agentPrompt,
52
57
  scope,
53
58
  });
54
59
  }
55
60
 
61
+ interface PeerCoordinationIdentity {
62
+ agent: ConfiguredReviewAgent;
63
+ id: string;
64
+ displayName: string;
65
+ }
66
+
67
+ const PEER_COORDINATION_AGENT_ID_PREFIX = "supi-review";
68
+
69
+ function normalizePeerCoordinationIdSegment(name: string, fallbackIndex: number): string {
70
+ const normalized = name
71
+ .trim()
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9]+/g, "-")
74
+ .replace(/^-+|-+$/g, "");
75
+ return normalized || `agent-${fallbackIndex + 1}`;
76
+ }
77
+
78
+ function normalizePeerCoordinationDisplayName(name: string, fallbackIndex: number): string {
79
+ const normalized = name.trim().replace(/\s+/g, " ");
80
+ return normalized || `review-agent-${fallbackIndex + 1}`;
81
+ }
82
+
83
+ export function buildPeerCoordinationIdentities(
84
+ agents: ConfiguredReviewAgent[],
85
+ ): PeerCoordinationIdentity[] {
86
+ const countsByBaseId = new Map<string, number>();
87
+ return agents.map((agent, index) => {
88
+ const segment = normalizePeerCoordinationIdSegment(agent.name, index);
89
+ const baseId = `${PEER_COORDINATION_AGENT_ID_PREFIX}-${segment}`;
90
+ const previousCount = countsByBaseId.get(baseId) ?? 0;
91
+ countsByBaseId.set(baseId, previousCount + 1);
92
+ return {
93
+ agent,
94
+ id: previousCount === 0 ? baseId : `${baseId}-${previousCount + 1}`,
95
+ displayName: normalizePeerCoordinationDisplayName(agent.name, index),
96
+ };
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Build the IRC peer-coordination prompt block for one agent.
102
+ *
103
+ * Returns `null` when peer coordination is disabled, the host runtime does not
104
+ * expose the `irc` tool, or the agent has no peers (single-agent run, or only
105
+ * itself opted in).
106
+ */
107
+ export function buildPeerCoordinationPromptBlock(
108
+ agent: ConfiguredReviewAgent,
109
+ peers: ConfiguredReviewAgent[],
110
+ activeTools: string[],
111
+ identities: PeerCoordinationIdentity[] = buildPeerCoordinationIdentities(peers),
112
+ ): string | null {
113
+ if (agent.peerCoordination !== true) return null;
114
+ if (!activeTools.includes("irc")) return null;
115
+
116
+ const self = identities.find((identity) => identity.agent === agent);
117
+ if (!self) return null;
118
+
119
+ const otherPeers = identities.filter(
120
+ (identity) => identity.agent.peerCoordination === true && identity.agent !== agent,
121
+ );
122
+ if (otherPeers.length === 0) return null;
123
+
124
+ return [
125
+ "## IRC peer coordination",
126
+ "",
127
+ `You are running as \`${self.id}\` in a multi-agent review. Other reviewers also opted into peer coordination:`,
128
+ ...otherPeers.map((peer) => `- \`${peer.id}\` — ${peer.displayName}`),
129
+ "",
130
+ "Use the OMP `irc` tool when continuing alone is wasteful or wrong:",
131
+ "- Send a DM with `irc({ op: \"send\", to: \"<peer-id>\", message: \"...\" })` when you spot work a peer is already filing, when you need their finding for context, or when you would otherwise duplicate analysis.",
132
+ "- Reply in plain prose. Do **NOT** send JSON status payloads. Do **NOT** quote the message you are replying to.",
133
+ "- One DM is one round-trip. Do **NOT** follow up with \"did you get my message?\".",
134
+ "- Use exactly the peer ids listed above. Do **NOT** invent ids from agent names, and do **NOT** broadcast unless you genuinely need every peer.",
135
+ "- If a peer has already filed an equivalent finding, do **NOT** file a duplicate; defer to theirs.",
136
+ "",
137
+ ].join("\n");
138
+ }
139
+
56
140
  function aggregateAgentOutputs(results: MultiAgentAgentResult[]): ReviewOutput {
57
141
  const findings = results.flatMap((result) =>
58
142
  result.output.findings.map((finding) => ({
@@ -73,13 +157,25 @@ function aggregateAgentOutputs(results: MultiAgentAgentResult[]): ReviewOutput {
73
157
  async function runConfiguredAgent(
74
158
  input: Omit<MultiAgentReviewInput, "agents">,
75
159
  agent: ConfiguredReviewAgent,
160
+ peers: ConfiguredReviewAgent[],
161
+ peerIdentities: PeerCoordinationIdentity[],
76
162
  ): Promise<MultiAgentAgentResult> {
77
163
  input.onAgentStart?.(agent);
78
164
 
165
+ const identity = peerIdentities.find((peer) => peer.agent === agent);
166
+ const basePrompt = buildConfiguredAgentPrompt(agent, input.scope);
167
+ const peerBlock = buildPeerCoordinationPromptBlock(
168
+ agent,
169
+ peers,
170
+ input.activeTools ?? [],
171
+ peerIdentities,
172
+ );
173
+ const prompt = peerBlock ? `${peerBlock}\n${basePrompt}` : basePrompt;
174
+
79
175
  const result = await runWithOutputValidation(input.createAgentSession, {
80
176
  cwd: input.cwd,
81
- prompt: buildConfiguredAgentPrompt(agent, input.scope),
82
- schema: reviewOutputSchema.trim(),
177
+ prompt,
178
+ schema: REVIEW_OUTPUT_SCHEMA_TEXT,
83
179
  parse(raw) {
84
180
  const output = parseReviewOutput(raw);
85
181
  return {
@@ -89,7 +185,11 @@ async function runConfiguredAgent(
89
185
  },
90
186
  model: agent.model ?? input.model,
91
187
  thinkingLevel: agent.thinkingLevel ?? input.thinkingLevel ?? null,
188
+ ...(peerBlock && identity
189
+ ? { agentId: identity.id, agentDisplayName: identity.displayName }
190
+ : {}),
92
191
  timeoutMs: input.timeoutMs ?? 120_000,
192
+ reliability: input.reliability,
93
193
  });
94
194
 
95
195
  if (result.status === "blocked") {
@@ -124,8 +224,9 @@ async function runConfiguredAgent(
124
224
  }
125
225
 
126
226
  export async function runMultiAgentReview(input: MultiAgentReviewInput): Promise<MultiAgentReviewResult> {
227
+ const peerIdentities = buildPeerCoordinationIdentities(input.agents);
127
228
  const results = await Promise.all(
128
- input.agents.map((agent) => runConfiguredAgent(input, agent)),
229
+ input.agents.map((agent) => runConfiguredAgent(input, agent, input.agents, peerIdentities)),
129
230
  );
130
231
 
131
232
  return {