supipowers 1.5.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +135 -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 +268 -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
@@ -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
  }
@@ -1,5 +1,4 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import type { TSchema } from "@sinclair/typebox";
3
2
  import { Value } from "@sinclair/typebox/value";
4
3
  import type {
5
4
  ConfiguredReviewAgent,
@@ -132,6 +131,7 @@ export const ReviewAgentConfigSchema = Type.Object(
132
131
  Type.Literal("xhigh"),
133
132
  Type.Null(),
134
133
  ])),
134
+ peerCoordination: Type.Optional(Type.Boolean()),
135
135
  },
136
136
  { additionalProperties: false },
137
137
  );
@@ -222,25 +222,6 @@ export const ReviewSessionSchema = Type.Object(
222
222
  { additionalProperties: false },
223
223
  );
224
224
 
225
- export interface ReviewValidationError {
226
- path: string;
227
- message: string;
228
- }
229
-
230
- function normalizeErrorPath(path: string): string {
231
- return path.replace(/^\//, "").replace(/\//g, ".") || "(root)";
232
- }
233
-
234
- export function collectReviewValidationErrors(schema: TSchema, data: unknown): ReviewValidationError[] {
235
- return [...Value.Errors(schema, data)].map((error) => ({
236
- path: normalizeErrorPath(error.path),
237
- message: error.message,
238
- }));
239
- }
240
-
241
- export function formatReviewValidationErrors(errors: ReviewValidationError[]): string[] {
242
- return errors.map((error) => `${error.path}: ${error.message}`);
243
- }
244
225
 
245
226
  export function isReviewScopeFile(value: unknown): value is ReviewScopeFile {
246
227
  return Value.Check(ReviewScopeFileSchema, value);
@@ -1,8 +1,12 @@
1
- import reviewOutputSchema from "./prompts/review-output-schema.md" with { type: "text" };
2
1
  import validationReviewPrompt from "./prompts/validation-review.md" with { type: "text" };
3
2
  import type { GateExecutionContext, ReviewFinding, 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 interface ReviewValidationInput {
8
12
  cwd: string;
@@ -14,6 +18,7 @@ export interface ReviewValidationInput {
14
18
  timeoutMs?: number;
15
19
  validatorName?: string;
16
20
  now?: () => Date;
21
+ reliability?: ReliabilityReporter;
17
22
  }
18
23
 
19
24
  export interface ReviewValidationResult {
@@ -80,12 +85,12 @@ export function buildValidationPrompt(
80
85
  validatorName: string,
81
86
  validatedAt: string,
82
87
  ): string {
83
- return renderReviewTemplate(validationReviewPrompt, {
88
+ return renderTemplate(validationReviewPrompt, {
84
89
  scope,
85
90
  findingsJson: JSON.stringify(findings, null, 2),
86
91
  validatorName,
87
92
  validatedAt,
88
- outputSchema: reviewOutputSchema.trim(),
93
+ outputSchema: REVIEW_OUTPUT_SCHEMA_TEXT,
89
94
  });
90
95
  }
91
96
 
@@ -107,7 +112,7 @@ export async function validateReviewFindings(input: ReviewValidationInput): Prom
107
112
  const result = await runWithOutputValidation(input.createAgentSession, {
108
113
  cwd: input.cwd,
109
114
  prompt: buildValidationPrompt(input.scope, input.findings, validatorName, validatedAt),
110
- schema: reviewOutputSchema.trim(),
115
+ schema: REVIEW_OUTPUT_SCHEMA_TEXT,
111
116
  parse(raw) {
112
117
  const output = parseReviewOutput(raw);
113
118
  return {
@@ -118,6 +123,7 @@ export async function validateReviewFindings(input: ReviewValidationInput): Prom
118
123
  model: input.model,
119
124
  thinkingLevel: input.thinkingLevel ?? null,
120
125
  timeoutMs: input.timeoutMs ?? 120_000,
126
+ reliability: input.reliability,
121
127
  });
122
128
 
123
129
  if (result.status === "blocked") {
@@ -2,15 +2,17 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { FixPrSessionLedger } from "../fix-pr/types.js";
4
4
  import type { PlatformPaths } from "../platform/types.js";
5
+ import type { WorkspaceTarget } from "../types.js";
6
+ import { getProjectTargetStatePath } from "../workspace/state-paths.js";
5
7
 
6
8
  const SESSIONS_DIR = "fix-pr-sessions";
7
9
 
8
- function getBaseDir(paths: PlatformPaths, cwd: string): string {
9
- return paths.project(cwd, SESSIONS_DIR);
10
+ function getBaseDir(paths: PlatformPaths, target: WorkspaceTarget): string {
11
+ return getProjectTargetStatePath(paths, target, SESSIONS_DIR);
10
12
  }
11
13
 
12
- export function getSessionDir(paths: PlatformPaths, cwd: string, sessionId: string): string {
13
- return path.join(getBaseDir(paths, cwd), sessionId);
14
+ export function getSessionDir(paths: PlatformPaths, target: WorkspaceTarget, sessionId: string): string {
15
+ return path.join(getBaseDir(paths, target), sessionId);
14
16
  }
15
17
 
16
18
  export function generateFixPrSessionId(): string {
@@ -21,14 +23,14 @@ export function generateFixPrSessionId(): string {
21
23
  return `fpr-${date}-${time}-${rand}`;
22
24
  }
23
25
 
24
- export function createFixPrSession(paths: PlatformPaths, cwd: string, ledger: FixPrSessionLedger): void {
25
- const sessionDir = getSessionDir(paths, cwd, ledger.id);
26
+ export function createFixPrSession(paths: PlatformPaths, target: WorkspaceTarget, ledger: FixPrSessionLedger): void {
27
+ const sessionDir = getSessionDir(paths, target, ledger.id);
26
28
  fs.mkdirSync(path.join(sessionDir, "snapshots"), { recursive: true });
27
29
  fs.writeFileSync(path.join(sessionDir, "ledger.json"), JSON.stringify(ledger, null, 2));
28
30
  }
29
31
 
30
- export function loadFixPrSession(paths: PlatformPaths, cwd: string, sessionId: string): FixPrSessionLedger | null {
31
- const ledgerPath = path.join(getSessionDir(paths, cwd, sessionId), "ledger.json");
32
+ export function loadFixPrSession(paths: PlatformPaths, target: WorkspaceTarget, sessionId: string): FixPrSessionLedger | null {
33
+ const ledgerPath = path.join(getSessionDir(paths, target, sessionId), "ledger.json");
32
34
  if (!fs.existsSync(ledgerPath)) return null;
33
35
  try {
34
36
  return JSON.parse(fs.readFileSync(ledgerPath, "utf-8")) as FixPrSessionLedger;
@@ -37,14 +39,19 @@ export function loadFixPrSession(paths: PlatformPaths, cwd: string, sessionId: s
37
39
  }
38
40
  }
39
41
 
40
- export function updateFixPrSession(paths: PlatformPaths, cwd: string, ledger: FixPrSessionLedger): void {
41
- const ledgerPath = path.join(getSessionDir(paths, cwd, ledger.id), "ledger.json");
42
+ export function updateFixPrSession(paths: PlatformPaths, target: WorkspaceTarget, ledger: FixPrSessionLedger): void {
43
+ const ledgerPath = path.join(getSessionDir(paths, target, ledger.id), "ledger.json");
42
44
  ledger.updatedAt = new Date().toISOString();
43
45
  fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2));
44
46
  }
45
47
 
46
- export function findActiveFixPrSession(paths: PlatformPaths, cwd: string): FixPrSessionLedger | null {
47
- const baseDir = getBaseDir(paths, cwd);
48
+ export function findActiveFixPrSession(
49
+ paths: PlatformPaths,
50
+ target: WorkspaceTarget,
51
+ repo: string,
52
+ prNumber: number,
53
+ ): FixPrSessionLedger | null {
54
+ const baseDir = getBaseDir(paths, target);
48
55
  if (!fs.existsSync(baseDir)) return null;
49
56
 
50
57
  const dirs = fs.readdirSync(baseDir)
@@ -53,8 +60,8 @@ export function findActiveFixPrSession(paths: PlatformPaths, cwd: string): FixPr
53
60
  .reverse();
54
61
 
55
62
  for (const dir of dirs) {
56
- const ledger = loadFixPrSession(paths, cwd, dir);
57
- if (ledger && ledger.status === "running") return ledger;
63
+ const ledger = loadFixPrSession(paths, target, dir);
64
+ if (ledger && ledger.status === "running" && ledger.repo === repo && ledger.prNumber === prNumber) return ledger;
58
65
  }
59
66
  return null;
60
67
  }