supipowers 1.5.2 → 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 +149 -45
  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 +19 -9
  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 +298 -127
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +115 -14
  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 +11 -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 +1401 -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
@@ -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 {
@@ -88,8 +184,12 @@ async function runConfiguredAgent(
88
184
  };
89
185
  },
90
186
  model: agent.model ?? input.model,
91
- thinkingLevel: input.thinkingLevel ?? null,
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 {
@@ -1,80 +1,15 @@
1
- import type { TSchema } from "@sinclair/typebox";
2
- import { Value } from "@sinclair/typebox/value";
3
- import invalidOutputRetryPrompt from "./prompts/invalid-output-retry.md" with { type: "text" };
4
- import { runStructuredAgentSession } from "../quality/ai-session.js";
5
- import { stripMarkdownCodeFence } from "../text.js";
6
- import type { GateExecutionContext, ReviewOutput } from "../types.js";
7
- import {
8
- ReviewOutputSchema,
9
- collectReviewValidationErrors,
10
- formatReviewValidationErrors,
11
- } from "./types.js";
12
- import { renderReviewTemplate } from "./template.js";
13
-
14
- export interface StructuredParseResult<T> {
15
- output: T | null;
16
- error: string | null;
17
- }
18
-
19
- export interface OutputValidationRunOptions<T> {
20
- cwd: string;
21
- prompt: string;
22
- schema: string;
23
- parse: (raw: string) => StructuredParseResult<T>;
24
- model?: string;
25
- thinkingLevel?: string | null;
26
- timeoutMs?: number;
27
- maxAttempts?: number;
28
- }
29
-
30
- export type OutputValidationRunResult<T> =
31
- | {
32
- status: "ok";
33
- output: T;
34
- rawOutput: string;
35
- attempts: number;
36
- }
37
- | {
38
- status: "blocked";
39
- error: string;
40
- rawOutputs: string[];
41
- attempts: number;
42
- };
43
-
44
- function truncateForPrompt(text: string, maxLength = 1200): string {
45
- const normalized = text.trim();
46
- if (normalized.length <= maxLength) {
47
- return normalized;
48
- }
49
-
50
- return `${normalized.slice(0, maxLength - 1)}…`;
51
- }
52
-
53
- export function parseStructuredOutput<T>(raw: string, schema: TSchema): StructuredParseResult<T> {
54
- let parsed: unknown;
55
-
56
- try {
57
- parsed = JSON.parse(stripMarkdownCodeFence(raw));
58
- } catch (error) {
59
- return {
60
- output: null,
61
- error: error instanceof Error ? `Invalid JSON: ${error.message}` : "Invalid JSON.",
62
- };
63
- }
64
-
65
- if (!Value.Check(schema, parsed)) {
66
- const errors = formatReviewValidationErrors(collectReviewValidationErrors(schema, parsed));
67
- return {
68
- output: null,
69
- error: errors.length > 0 ? errors.join("; ") : "Output does not match the required schema.",
70
- };
71
- }
72
-
73
- return {
74
- output: parsed as T,
75
- error: null,
76
- };
77
- }
1
+ // src/review/output.ts
2
+ //
3
+ // Review-specific thin wrappers over the shared structured-output foundation.
4
+ // Everything generic (parseStructuredOutput<T>, runWithOutputValidation<T>,
5
+ // StructuredOutputResult<T>, validation-error helpers) lives in
6
+ // src/ai/structured-output.ts. This module exists only so review callers can
7
+ // parse a raw model response into ReviewOutput without rewriting the schema
8
+ // reference at every site.
9
+
10
+ import { parseStructuredOutput } from "../ai/structured-output.js";
11
+ import type { ReviewOutput } from "../types.js";
12
+ import { ReviewOutputSchema } from "./types.js";
78
13
 
79
14
  export function parseReviewOutput(raw: string): ReviewOutput | null {
80
15
  return parseStructuredOutput<ReviewOutput>(raw, ReviewOutputSchema).output;
@@ -83,65 +18,3 @@ export function parseReviewOutput(raw: string): ReviewOutput | null {
83
18
  export function explainReviewOutputFailure(raw: string): string | null {
84
19
  return parseStructuredOutput<ReviewOutput>(raw, ReviewOutputSchema).error;
85
20
  }
86
-
87
- export async function runWithOutputValidation<T>(
88
- createAgentSession: GateExecutionContext["createAgentSession"],
89
- options: OutputValidationRunOptions<T>,
90
- ): Promise<OutputValidationRunResult<T>> {
91
- const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
92
- const rawOutputs: string[] = [];
93
- let prompt = options.prompt;
94
-
95
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
96
- const result = await runStructuredAgentSession(createAgentSession, {
97
- cwd: options.cwd,
98
- prompt,
99
- model: options.model,
100
- thinkingLevel: options.thinkingLevel ?? null,
101
- timeoutMs: options.timeoutMs,
102
- });
103
-
104
- if (result.status !== "ok") {
105
- return {
106
- status: "blocked",
107
- error: result.error,
108
- rawOutputs,
109
- attempts: attempt,
110
- };
111
- }
112
-
113
- rawOutputs.push(result.finalText);
114
- const parsed = options.parse(result.finalText);
115
- if (parsed.output) {
116
- return {
117
- status: "ok",
118
- output: parsed.output,
119
- rawOutput: result.finalText,
120
- attempts: attempt,
121
- };
122
- }
123
-
124
- if (attempt === maxAttempts) {
125
- return {
126
- status: "blocked",
127
- error: parsed.error ?? "Agent output was invalid.",
128
- rawOutputs,
129
- attempts: attempt,
130
- };
131
- }
132
-
133
- prompt = renderReviewTemplate(invalidOutputRetryPrompt, {
134
- prompt: options.prompt,
135
- error: parsed.error ?? "Agent output was invalid.",
136
- previousOutput: truncateForPrompt(result.finalText),
137
- schema: options.schema,
138
- });
139
- }
140
-
141
- return {
142
- status: "blocked",
143
- error: "Output validation exhausted without producing a valid result.",
144
- rawOutputs,
145
- attempts: maxAttempts,
146
- };
147
- }
@@ -1,8 +1,12 @@
1
- import reviewOutputSchema from "./prompts/review-output-schema.md" with { type: "text" };
2
1
  import singleReviewPrompt from "./prompts/single-review.md" with { type: "text" };
3
2
  import type { GateExecutionContext, ReviewLevel, ReviewOutput, ReviewScope } from "../types.js";
4
- import { explainReviewOutputFailure, parseReviewOutput, runWithOutputValidation } from "./output.js";
5
- import { renderReviewTemplate } from "./template.js";
3
+ import { runWithOutputValidation, type ReliabilityReporter } from "../ai/structured-output.js";
4
+ import { renderSchemaText } from "../ai/schema-text.js";
5
+ import { explainReviewOutputFailure, parseReviewOutput } from "./output.js";
6
+ import { renderTemplate } from "../ai/template.js";
7
+ import { ReviewOutputSchema } from "./types.js";
8
+
9
+ const REVIEW_OUTPUT_SCHEMA_TEXT = renderSchemaText(ReviewOutputSchema);
6
10
 
7
11
  export type SingleReviewLevel = Extract<ReviewLevel, "quick" | "deep">;
8
12
 
@@ -14,6 +18,7 @@ export interface SingleReviewRunnerInput {
14
18
  model?: string;
15
19
  thinkingLevel?: string | null;
16
20
  timeoutMs?: number;
21
+ reliability?: ReliabilityReporter;
17
22
  }
18
23
 
19
24
  export interface SingleReviewRunResult {
@@ -70,12 +75,12 @@ export function normalizeReviewOutput(output: ReviewOutput): ReviewOutput {
70
75
  }
71
76
 
72
77
  export function buildSingleReviewPrompt(scope: ReviewScope, level: SingleReviewLevel): string {
73
- return renderReviewTemplate(singleReviewPrompt, {
78
+ return renderTemplate(singleReviewPrompt, {
74
79
  level,
75
80
  scope,
76
81
  isQuick: level === "quick",
77
82
  isDeep: level === "deep",
78
- outputSchema: reviewOutputSchema.trim(),
83
+ outputSchema: REVIEW_OUTPUT_SCHEMA_TEXT,
79
84
  });
80
85
  }
81
86
 
@@ -83,7 +88,7 @@ export async function runSingleReview(input: SingleReviewRunnerInput): Promise<S
83
88
  const result = await runWithOutputValidation(input.createAgentSession, {
84
89
  cwd: input.cwd,
85
90
  prompt: buildSingleReviewPrompt(input.scope, input.level),
86
- schema: reviewOutputSchema.trim(),
91
+ schema: REVIEW_OUTPUT_SCHEMA_TEXT,
87
92
  parse(raw) {
88
93
  const output = parseReviewOutput(raw);
89
94
  return {
@@ -94,6 +99,7 @@ export async function runSingleReview(input: SingleReviewRunnerInput): Promise<S
94
99
  model: input.model,
95
100
  thinkingLevel: input.thinkingLevel ?? null,
96
101
  timeoutMs: input.timeoutMs ?? 120_000,
102
+ reliability: input.reliability,
97
103
  });
98
104
 
99
105
  if (result.status === "blocked") {
@@ -1,5 +1,7 @@
1
1
  import type { Platform, PlatformContext } from "../platform/types.js";
2
- import type { ReviewScope, ReviewScopeFile, ReviewScopeStats } from "../types.js";
2
+ import type { ReviewScope, ReviewScopeFile, ReviewScopeStats, WorkspaceTarget } from "../types.js";
3
+ import { filterGitLogOnelineToWorkspaceTarget } from "../workspace/git-scope.js";
4
+ import { findWorkspaceTargetForPath } from "../workspace/path-mapping.js";
3
5
 
4
6
  interface ExcludedReviewScopeFile {
5
7
  path: string;
@@ -14,6 +16,11 @@ export interface ParsedReviewDiff {
14
16
  stats: ReviewScopeStats;
15
17
  }
16
18
 
19
+ export interface ReviewWorkspaceSelection {
20
+ target: WorkspaceTarget;
21
+ targets: WorkspaceTarget[];
22
+ }
23
+
17
24
  export const EXCLUDED_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
18
25
  { pattern: /\.lock$/, reason: "lock file" },
19
26
  { pattern: /-lock\.(json|yaml|yml)$/, reason: "lock file" },
@@ -200,6 +207,51 @@ function ensureReviewableScope(scope: ReviewScope, message: string): ReviewScope
200
207
  return scope;
201
208
  }
202
209
 
210
+ function buildTargetLabel(selection: ReviewWorkspaceSelection): string {
211
+ return `${selection.target.name} (${selection.target.relativeDir})`;
212
+ }
213
+
214
+ function appendTargetContext(description: string, selection?: ReviewWorkspaceSelection | null): string {
215
+ return selection ? `${description} for ${buildTargetLabel(selection)}` : description;
216
+ }
217
+
218
+ function buildEmptyScopeMessage(baseMessage: string, selection?: ReviewWorkspaceSelection | null): string {
219
+ return selection
220
+ ? `${baseMessage} for ${buildTargetLabel(selection)}.`
221
+ : `${baseMessage}.`;
222
+ }
223
+
224
+ function ensureTargetScopeHasDiff(
225
+ diff: string,
226
+ emptyScopeMessage: string,
227
+ selection?: ReviewWorkspaceSelection | null,
228
+ ): void {
229
+ if (selection && !diff.trim()) {
230
+ throw new Error(emptyScopeMessage);
231
+ }
232
+ }
233
+
234
+ function filterDiffToWorkspaceTarget(diffOutput: string, selection?: ReviewWorkspaceSelection | null): string {
235
+ if (!selection || !diffOutput.trim()) {
236
+ return diffOutput;
237
+ }
238
+
239
+ return diffOutput
240
+ .split(/^diff --git /m)
241
+ .filter(Boolean)
242
+ .map((chunk) => `diff --git ${chunk}`)
243
+ .filter((chunk) => {
244
+ const headerMatch = chunk.match(/^diff --git a\/(.+?) b\/(.+)/m);
245
+ const repoRelativePath = headerMatch?.[2]?.trim();
246
+ if (!repoRelativePath) {
247
+ return false;
248
+ }
249
+
250
+ return findWorkspaceTargetForPath(selection.targets, repoRelativePath)?.id === selection.target.id;
251
+ })
252
+ .join("\n");
253
+ }
254
+
203
255
  export async function listReviewBaseBranches(platform: Pick<Platform, "exec">, cwd: string): Promise<string[]> {
204
256
  const output = await execGit(platform, cwd, ["branch", "--all", "--format=%(refname:short)"]);
205
257
  return [...new Set(
@@ -220,9 +272,18 @@ export async function listRecentReviewCommits(
220
272
  platform: Pick<Platform, "exec">,
221
273
  cwd: string,
222
274
  count = 20,
275
+ selection?: ReviewWorkspaceSelection | null,
223
276
  ): Promise<string[]> {
224
- const output = await execGit(platform, cwd, ["log", "--oneline", `-${count}`]);
225
- return output
277
+ if (!selection) {
278
+ const output = await execGit(platform, cwd, ["log", "--oneline", `-${count}`]);
279
+ return output
280
+ .split("\n")
281
+ .map((line) => line.trim())
282
+ .filter((line) => line.length > 0);
283
+ }
284
+
285
+ const output = await execGit(platform, cwd, ["log", `-${count}`, "--format=%H%x1f%s%x1e", "--name-only"]);
286
+ return filterGitLogOnelineToWorkspaceTarget(output, selection.targets, selection.target)
226
287
  .split("\n")
227
288
  .map((line) => line.trim())
228
289
  .filter((line) => line.length > 0);
@@ -233,63 +294,120 @@ export async function loadPullRequestScope(
233
294
  cwd: string,
234
295
  baseBranch: string,
235
296
  currentBranch?: string,
297
+ selection?: ReviewWorkspaceSelection | null,
236
298
  ): Promise<ReviewScope> {
237
299
  const branch = currentBranch ?? await getCurrentReviewBranch(platform, cwd);
238
- const diff = await execGit(platform, cwd, ["diff", "--no-ext-diff", "--binary", `${baseBranch}...${branch}`]);
300
+ const rawDiff = await execGit(platform, cwd, ["diff", "--no-ext-diff", "--binary", `${baseBranch}...${branch}`]);
301
+ const diff = filterDiffToWorkspaceTarget(rawDiff, selection);
302
+ ensureTargetScopeHasDiff(
303
+ diff,
304
+ buildEmptyScopeMessage("No reviewable files remain after filtering PR-style changes", selection),
305
+ selection,
306
+ );
239
307
  const scope = createScope(
240
308
  "pull-request",
241
- `Reviewing changes between ${baseBranch} and ${branch}`,
309
+ appendTargetContext(`Reviewing changes between ${baseBranch} and ${branch}`, selection),
242
310
  diff,
243
311
  { baseBranch },
244
312
  );
245
- return ensureReviewableScope(scope, "No reviewable files remain after filtering PR-style changes.");
313
+ return ensureReviewableScope(
314
+ scope,
315
+ buildEmptyScopeMessage("No reviewable files remain after filtering PR-style changes", selection),
316
+ );
246
317
  }
247
318
 
248
- export async function loadUncommittedScope(platform: Pick<Platform, "exec">, cwd: string): Promise<ReviewScope> {
319
+ export async function loadUncommittedScope(
320
+ platform: Pick<Platform, "exec">,
321
+ cwd: string,
322
+ selection?: ReviewWorkspaceSelection | null,
323
+ ): Promise<ReviewScope> {
249
324
  const [unstaged, staged, untracked] = await Promise.all([
250
325
  execGit(platform, cwd, ["diff", "--no-ext-diff", "--binary"]),
251
326
  execGit(platform, cwd, ["diff", "--cached", "--no-ext-diff", "--binary"]),
252
327
  buildUntrackedDiff(platform, cwd),
253
328
  ]);
254
- const diff = [unstaged, staged, untracked].filter((chunk) => chunk.trim().length > 0).join("\n");
255
- const scope = createScope("uncommitted", "Reviewing uncommitted changes", diff);
256
- return ensureReviewableScope(scope, "No reviewable files remain after filtering uncommitted changes.");
329
+ const rawDiff = [unstaged, staged, untracked].filter((chunk) => chunk.trim().length > 0).join("\n");
330
+ const diff = filterDiffToWorkspaceTarget(rawDiff, selection);
331
+ ensureTargetScopeHasDiff(
332
+ diff,
333
+ buildEmptyScopeMessage("No reviewable files remain after filtering uncommitted changes", selection),
334
+ selection,
335
+ );
336
+ const scope = createScope(
337
+ "uncommitted",
338
+ appendTargetContext("Reviewing uncommitted changes", selection),
339
+ diff,
340
+ );
341
+ return ensureReviewableScope(
342
+ scope,
343
+ buildEmptyScopeMessage("No reviewable files remain after filtering uncommitted changes", selection),
344
+ );
257
345
  }
258
346
 
259
347
  export async function loadCommitScope(
260
348
  platform: Pick<Platform, "exec">,
261
349
  cwd: string,
262
350
  commit: string,
351
+ selection?: ReviewWorkspaceSelection | null,
263
352
  ): Promise<ReviewScope> {
264
- const diff = await execGit(platform, cwd, ["show", "--format=", "--no-ext-diff", "--binary", commit]);
265
- const scope = createScope("commit", `Reviewing commit ${commit}`, diff, { commit });
266
- return ensureReviewableScope(scope, "No reviewable files remain after filtering commit changes.");
353
+ const rawDiff = await execGit(platform, cwd, ["show", "--format=", "--no-ext-diff", "--binary", commit]);
354
+ const diff = filterDiffToWorkspaceTarget(rawDiff, selection);
355
+ ensureTargetScopeHasDiff(
356
+ diff,
357
+ buildEmptyScopeMessage("No reviewable files remain after filtering commit changes", selection),
358
+ selection,
359
+ );
360
+ const scope = createScope(
361
+ "commit",
362
+ appendTargetContext(`Reviewing commit ${commit}`, selection),
363
+ diff,
364
+ { commit },
365
+ );
366
+ return ensureReviewableScope(
367
+ scope,
368
+ buildEmptyScopeMessage("No reviewable files remain after filtering commit changes", selection),
369
+ );
267
370
  }
268
371
 
269
372
  export async function loadCustomReviewScope(
270
373
  platform: Pick<Platform, "exec">,
271
374
  cwd: string,
272
375
  instructions: string,
376
+ selection?: ReviewWorkspaceSelection | null,
273
377
  ): Promise<ReviewScope> {
274
- let diff = "";
378
+ let rawDiff = "";
275
379
 
276
380
  try {
277
- diff = await execGit(platform, cwd, ["diff", "--no-ext-diff", "--binary", "HEAD"]);
381
+ rawDiff = await execGit(platform, cwd, ["diff", "--no-ext-diff", "--binary", "HEAD"]);
278
382
  } catch {
279
- diff = "";
383
+ rawDiff = "";
280
384
  }
281
385
 
282
- return createScope(
386
+ const diff = filterDiffToWorkspaceTarget(rawDiff, selection);
387
+ ensureTargetScopeHasDiff(
388
+ diff,
389
+ buildEmptyScopeMessage("No reviewable files remain after filtering custom review changes", selection),
390
+ selection,
391
+ );
392
+ const scope = createScope(
283
393
  "custom",
284
- `Custom review: ${instructions.slice(0, 60)}`,
394
+ appendTargetContext(`Custom review: ${instructions.slice(0, 60)}`, selection),
285
395
  diff,
286
396
  { customInstructions: instructions },
287
397
  );
398
+
399
+ return selection
400
+ ? ensureReviewableScope(
401
+ scope,
402
+ buildEmptyScopeMessage("No reviewable files remain after filtering custom review changes", selection),
403
+ )
404
+ : scope;
288
405
  }
289
406
 
290
407
  export async function selectReviewScope(
291
408
  platform: Pick<Platform, "exec">,
292
409
  ctx: Pick<PlatformContext, "cwd" | "ui">,
410
+ selection?: ReviewWorkspaceSelection | null,
293
411
  ): Promise<ReviewScope | null> {
294
412
  const choice = await ctx.ui.select(
295
413
  "What should /supi:review inspect?",
@@ -317,17 +435,19 @@ export async function selectReviewScope(
317
435
  if (!selected) {
318
436
  return null;
319
437
  }
320
- return loadPullRequestScope(platform, ctx.cwd, selected);
438
+ return loadPullRequestScope(platform, ctx.cwd, selected, undefined, selection);
321
439
  }
322
440
 
323
441
  if (choice.startsWith("Uncommitted changes")) {
324
- return loadUncommittedScope(platform, ctx.cwd);
442
+ return loadUncommittedScope(platform, ctx.cwd, selection);
325
443
  }
326
444
 
327
445
  if (choice.startsWith("Specific commit")) {
328
- const commits = await listRecentReviewCommits(platform, ctx.cwd);
446
+ const commits = await listRecentReviewCommits(platform, ctx.cwd, 20, selection);
329
447
  if (commits.length === 0) {
330
- throw new Error("No commits found.");
448
+ throw new Error(selection
449
+ ? `No commits found for ${buildTargetLabel(selection)}.`
450
+ : "No commits found.");
331
451
  }
332
452
  const selected = await ctx.ui.select("Commit to review", commits, {
333
453
  helpText: "Select a recent commit · Esc to cancel",
@@ -339,7 +459,7 @@ export async function selectReviewScope(
339
459
  if (!commit) {
340
460
  throw new Error("Could not determine the selected commit hash.");
341
461
  }
342
- return loadCommitScope(platform, ctx.cwd, commit);
462
+ return loadCommitScope(platform, ctx.cwd, commit, selection);
343
463
  }
344
464
 
345
465
  const instructions = await ctx.ui.input("Custom review focus", {
@@ -349,5 +469,5 @@ export async function selectReviewScope(
349
469
  if (!instructions?.trim()) {
350
470
  return null;
351
471
  }
352
- return loadCustomReviewScope(platform, ctx.cwd, instructions.trim());
472
+ return loadCustomReviewScope(platform, ctx.cwd, instructions.trim(), selection);
353
473
  }