supipowers 1.5.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/README.md +14 -8
  2. package/bin/install.mjs +20 -5
  3. package/bin/install.ts +95 -0
  4. package/package.json +8 -4
  5. package/skills/context-mode/SKILL.md +17 -10
  6. package/skills/harness/SKILL.md +94 -0
  7. package/skills/ui-design/SKILL.md +63 -0
  8. package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
  9. package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
  10. package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
  11. package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
  12. package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
  13. package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
  14. package/skills/ultraplan-discover/SKILL.md +96 -0
  15. package/skills/ultraplan-intake/SKILL.md +89 -0
  16. package/skills/ultraplan-research/SKILL.md +129 -0
  17. package/skills/ultraplan-review/SKILL.md +86 -0
  18. package/skills/ultraplan-review-scope/SKILL.md +111 -0
  19. package/skills/ultraplan-review-structure/SKILL.md +120 -0
  20. package/skills/ultraplan-review-tdd/SKILL.md +142 -0
  21. package/skills/ultraplan-scout/SKILL.md +110 -0
  22. package/skills/ultraplan-synthesize/SKILL.md +124 -0
  23. package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
  24. package/src/ai/schema-text.ts +129 -0
  25. package/src/ai/structured-output.ts +274 -0
  26. package/src/ai/template.ts +27 -0
  27. package/src/bootstrap.ts +63 -28
  28. package/src/commands/agents.ts +131 -42
  29. package/src/commands/ai-review.ts +251 -30
  30. package/src/commands/clear.ts +434 -0
  31. package/src/commands/commit.ts +1 -0
  32. package/src/commands/config.ts +242 -44
  33. package/src/commands/context.ts +55 -28
  34. package/src/commands/doctor.ts +234 -6
  35. package/src/commands/fix-pr.ts +306 -131
  36. package/src/commands/generate.ts +111 -21
  37. package/src/commands/memory.ts +192 -0
  38. package/src/commands/model-picker.ts +28 -21
  39. package/src/commands/model.ts +18 -8
  40. package/src/commands/optimize-context.ts +408 -29
  41. package/src/commands/plan.ts +2 -0
  42. package/src/commands/qa.ts +312 -137
  43. package/src/commands/release.ts +259 -76
  44. package/src/commands/review.ts +293 -59
  45. package/src/commands/status.ts +200 -13
  46. package/src/commands/supi.ts +3 -35
  47. package/src/commands/ui-design.ts +394 -0
  48. package/src/commands/ultraplan.ts +1518 -0
  49. package/src/commands/update.ts +86 -0
  50. package/src/config/defaults.ts +62 -0
  51. package/src/config/loader.ts +448 -60
  52. package/src/config/schema.ts +108 -2
  53. package/src/context/optimizer.ts +25 -33
  54. package/src/context/rule-renderer.ts +223 -0
  55. package/src/context/savings.ts +258 -0
  56. package/src/context/startup-check.ts +380 -0
  57. package/src/context/startup-optimizer.ts +355 -0
  58. package/src/context/tokenignore.ts +146 -0
  59. package/src/context-mode/cache-handle.ts +49 -0
  60. package/src/context-mode/cache-preview.ts +71 -0
  61. package/src/context-mode/cache-store.ts +738 -0
  62. package/src/context-mode/compressor.ts +131 -26
  63. package/src/context-mode/dedup.ts +108 -0
  64. package/src/context-mode/detector.ts +35 -4
  65. package/src/context-mode/event-extractor.ts +14 -12
  66. package/src/context-mode/event-store.ts +91 -36
  67. package/src/context-mode/hooks.ts +798 -56
  68. package/src/context-mode/knowledge/store.ts +255 -11
  69. package/src/context-mode/memory-store.ts +325 -0
  70. package/src/context-mode/metrics-recorder.ts +158 -0
  71. package/src/context-mode/metrics-store.ts +765 -0
  72. package/src/context-mode/model.ts +24 -0
  73. package/src/context-mode/processor-keys.ts +29 -0
  74. package/src/context-mode/processors/build.ts +66 -0
  75. package/src/context-mode/processors/docker.ts +57 -0
  76. package/src/context-mode/processors/git.ts +111 -0
  77. package/src/context-mode/processors/json.ts +112 -0
  78. package/src/context-mode/processors/k8s.ts +67 -0
  79. package/src/context-mode/processors/lint.ts +67 -0
  80. package/src/context-mode/processors/log.ts +86 -0
  81. package/src/context-mode/processors/registry.ts +116 -0
  82. package/src/context-mode/processors/test-runner.ts +102 -0
  83. package/src/context-mode/processors/types.ts +20 -0
  84. package/src/context-mode/repomap.ts +400 -0
  85. package/src/context-mode/routing.ts +97 -24
  86. package/src/context-mode/sandbox/runners.ts +5 -1
  87. package/src/context-mode/snapshot-builder.ts +106 -11
  88. package/src/context-mode/source-hash.ts +173 -0
  89. package/src/context-mode/tool-name.ts +11 -0
  90. package/src/context-mode/tools.ts +654 -22
  91. package/src/context-mode/web/fetcher.ts +31 -12
  92. package/src/debug/logger.ts +2 -1
  93. package/src/deps/registry.ts +1 -1
  94. package/src/discipline/failure-summarizer.ts +170 -0
  95. package/src/discipline/failure-taxonomy.ts +131 -0
  96. package/src/discipline/workflow-invariants.ts +125 -0
  97. package/src/discovery/index.ts +31 -0
  98. package/src/discovery/lsp.ts +87 -0
  99. package/src/discovery/rank.ts +144 -0
  100. package/src/discovery/sources.ts +89 -0
  101. package/src/discovery/workflow.ts +87 -0
  102. package/src/docs/contracts.ts +39 -0
  103. package/src/docs/drift.ts +117 -87
  104. package/src/fix-pr/assessment.ts +200 -0
  105. package/src/fix-pr/contracts.ts +47 -0
  106. package/src/fix-pr/fetch-comments.ts +80 -0
  107. package/src/fix-pr/prompt-builder.ts +58 -40
  108. package/src/fix-pr/scripts/exec.ts +34 -0
  109. package/src/fix-pr/scripts/trigger-review.ts +106 -0
  110. package/src/fix-pr/scripts/wait-and-check.ts +108 -0
  111. package/src/fix-pr/types.ts +4 -0
  112. package/src/git/branch-finish.ts +5 -0
  113. package/src/git/commit-contract.ts +83 -0
  114. package/src/git/commit.ts +121 -184
  115. package/src/git/status.ts +62 -8
  116. package/src/harness/anti_slop/architecture-parser.ts +210 -0
  117. package/src/harness/anti_slop/backend-factory.ts +30 -0
  118. package/src/harness/anti_slop/backend.ts +140 -0
  119. package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
  120. package/src/harness/anti_slop/fallow-adapter.ts +305 -0
  121. package/src/harness/anti_slop/installer.ts +227 -0
  122. package/src/harness/anti_slop/queue.ts +216 -0
  123. package/src/harness/anti_slop/recommend.ts +84 -0
  124. package/src/harness/anti_slop/score.ts +180 -0
  125. package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
  126. package/src/harness/artifacts/agents-md.ts +88 -0
  127. package/src/harness/artifacts/checks-wiring.ts +57 -0
  128. package/src/harness/artifacts/docs-tree.ts +79 -0
  129. package/src/harness/artifacts/lint-configs.ts +136 -0
  130. package/src/harness/artifacts/review-agents.ts +67 -0
  131. package/src/harness/bare-entry.ts +108 -0
  132. package/src/harness/command.ts +1010 -0
  133. package/src/harness/default-agents/design.md +23 -0
  134. package/src/harness/default-agents/discover.md +18 -0
  135. package/src/harness/default-agents/implement.md +24 -0
  136. package/src/harness/default-agents/plan.md +19 -0
  137. package/src/harness/default-agents/research.md +21 -0
  138. package/src/harness/default-agents/validate.md +22 -0
  139. package/src/harness/gc/reporter.ts +28 -0
  140. package/src/harness/gc/runner.ts +136 -0
  141. package/src/harness/hooks/layer-context-inject.ts +155 -0
  142. package/src/harness/hooks/post-session-sweep.ts +130 -0
  143. package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
  144. package/src/harness/hooks/register.ts +118 -0
  145. package/src/harness/model.ts +117 -0
  146. package/src/harness/pipeline.ts +348 -0
  147. package/src/harness/project-paths.ts +235 -0
  148. package/src/harness/stage-runner.ts +107 -0
  149. package/src/harness/stages/design.ts +386 -0
  150. package/src/harness/stages/discover.ts +454 -0
  151. package/src/harness/stages/implement.ts +162 -0
  152. package/src/harness/stages/plan.ts +335 -0
  153. package/src/harness/stages/research.ts +263 -0
  154. package/src/harness/stages/validate.ts +684 -0
  155. package/src/harness/storage.ts +467 -0
  156. package/src/harness/tools.ts +426 -0
  157. package/src/lsp/bridge.ts +56 -95
  158. package/src/lsp/capabilities.ts +108 -0
  159. package/src/lsp/contracts.ts +35 -0
  160. package/src/lsp/detector.ts +8 -12
  161. package/src/markdown-frontmatter.ts +68 -0
  162. package/src/mempalace/bridge.ts +129 -0
  163. package/src/mempalace/config.ts +75 -0
  164. package/src/mempalace/format.ts +163 -0
  165. package/src/mempalace/hooks.ts +370 -0
  166. package/src/mempalace/installer-helper.ts +194 -0
  167. package/src/mempalace/python/mempalace_bridge.py +440 -0
  168. package/src/mempalace/runtime.ts +565 -0
  169. package/src/mempalace/schema.ts +264 -0
  170. package/src/mempalace/session-summary.ts +198 -0
  171. package/src/mempalace/tool.ts +186 -0
  172. package/src/mempalace/uv.ts +256 -0
  173. package/src/migrate/runner.ts +354 -0
  174. package/src/planning/approval-flow.ts +206 -9
  175. package/src/planning/plan-writer-prompt.ts +4 -3
  176. package/src/planning/planning-ask-tool.ts +39 -0
  177. package/src/planning/render-markdown.ts +74 -0
  178. package/src/planning/spec.ts +42 -0
  179. package/src/planning/system-prompt.ts +11 -8
  180. package/src/planning/validate.ts +84 -0
  181. package/src/platform/omp.ts +15 -2
  182. package/src/platform/system-prompt.ts +37 -0
  183. package/src/platform/test-utils.ts +3 -0
  184. package/src/platform/types.ts +6 -1
  185. package/src/qa/config.ts +12 -6
  186. package/src/qa/detect-app-type.ts +13 -6
  187. package/src/qa/matrix.ts +12 -6
  188. package/src/qa/prompt-builder.ts +28 -30
  189. package/src/qa/scripts/dev-server-utils.ts +72 -0
  190. package/src/qa/scripts/run-e2e-tests.ts +226 -0
  191. package/src/qa/scripts/start-dev-server.ts +138 -0
  192. package/src/qa/scripts/stop-dev-server.ts +77 -0
  193. package/src/qa/session.ts +13 -7
  194. package/src/quality/ai-setup.ts +27 -25
  195. package/src/quality/contracts.ts +34 -0
  196. package/src/quality/gates/ai-review.ts +20 -58
  197. package/src/quality/gates/command.ts +249 -46
  198. package/src/quality/review-gates.ts +18 -2
  199. package/src/quality/runner.ts +63 -22
  200. package/src/quality/schemas.ts +37 -2
  201. package/src/quality/setup.ts +96 -16
  202. package/src/release/changelog.ts +1 -1
  203. package/src/release/channels/custom.ts +13 -3
  204. package/src/release/channels/types.ts +5 -0
  205. package/src/release/contracts.ts +90 -0
  206. package/src/release/executor.ts +122 -45
  207. package/src/release/prompt.ts +18 -2
  208. package/src/release/targets.ts +86 -0
  209. package/src/release/version.ts +96 -71
  210. package/src/review/agent-loader.ts +221 -109
  211. package/src/review/fixer.ts +10 -6
  212. package/src/review/multi-agent-runner.ts +114 -13
  213. package/src/review/output.ts +12 -139
  214. package/src/review/runner.ts +12 -6
  215. package/src/review/scope.ts +144 -24
  216. package/src/review/types.ts +1 -20
  217. package/src/review/validator.ts +12 -6
  218. package/src/storage/fix-pr-sessions.ts +21 -14
  219. package/src/storage/plans.ts +14 -5
  220. package/src/storage/qa-sessions.ts +25 -19
  221. package/src/storage/reliability-metrics.ts +180 -0
  222. package/src/storage/reports.ts +8 -7
  223. package/src/storage/review-sessions.ts +55 -20
  224. package/src/tool-catalog/active-tool-controller.ts +164 -0
  225. package/src/tool-catalog/active-tool-planner.ts +212 -0
  226. package/src/tool-catalog/tool-groups.ts +102 -0
  227. package/src/types.ts +1399 -5
  228. package/src/ui-design/backend-adapter.ts +78 -0
  229. package/src/ui-design/backends/local-html.ts +82 -0
  230. package/src/ui-design/backends/pencil-mcp.ts +111 -0
  231. package/src/ui-design/components-scanner.ts +124 -0
  232. package/src/ui-design/config.ts +55 -0
  233. package/src/ui-design/pen-scanner.ts +95 -0
  234. package/src/ui-design/pen-selector.ts +72 -0
  235. package/src/ui-design/prompt-builder.ts +73 -0
  236. package/src/ui-design/scanner.ts +136 -0
  237. package/src/ui-design/session.ts +974 -0
  238. package/src/ui-design/system-prompt.ts +312 -0
  239. package/src/ui-design/tokens-scanner.ts +181 -0
  240. package/src/ui-design/types.ts +96 -0
  241. package/src/ultraplan/agent-catalog.ts +522 -0
  242. package/src/ultraplan/authoring/agent-catalog.ts +310 -0
  243. package/src/ultraplan/authoring/authoring-tools.ts +552 -0
  244. package/src/ultraplan/authoring/command-handlers.ts +339 -0
  245. package/src/ultraplan/authoring/markdown.ts +510 -0
  246. package/src/ultraplan/authoring/model.ts +162 -0
  247. package/src/ultraplan/authoring/pipeline.ts +319 -0
  248. package/src/ultraplan/authoring/stage-runner.ts +141 -0
  249. package/src/ultraplan/authoring/stages/approve.ts +249 -0
  250. package/src/ultraplan/authoring/stages/discover.ts +289 -0
  251. package/src/ultraplan/authoring/stages/intake.ts +203 -0
  252. package/src/ultraplan/authoring/stages/research.ts +399 -0
  253. package/src/ultraplan/authoring/stages/review.ts +333 -0
  254. package/src/ultraplan/authoring/stages/scout.ts +188 -0
  255. package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
  256. package/src/ultraplan/authoring/storage.ts +594 -0
  257. package/src/ultraplan/authoring/synth-gate.ts +165 -0
  258. package/src/ultraplan/authoring-draft.ts +653 -0
  259. package/src/ultraplan/authoring-persist.ts +180 -0
  260. package/src/ultraplan/authoring-tool.ts +608 -0
  261. package/src/ultraplan/authoring-wizard.ts +587 -0
  262. package/src/ultraplan/batch/merge.ts +98 -0
  263. package/src/ultraplan/batch/planner.ts +150 -0
  264. package/src/ultraplan/batch/presenter.ts +97 -0
  265. package/src/ultraplan/batch/storage.ts +420 -0
  266. package/src/ultraplan/batch/supervisor.ts +317 -0
  267. package/src/ultraplan/batch/worker.ts +26 -0
  268. package/src/ultraplan/batch/worktree.ts +110 -0
  269. package/src/ultraplan/contracts.ts +1593 -0
  270. package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
  271. package/src/ultraplan/default-agents/authoring/intake.md +12 -0
  272. package/src/ultraplan/default-agents/authoring/planner.md +12 -0
  273. package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
  274. package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
  275. package/src/ultraplan/default-agents/authoring/scout.md +12 -0
  276. package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
  277. package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
  278. package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
  279. package/src/ultraplan/default-agents/backend-executor.md +10 -0
  280. package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
  281. package/src/ultraplan/default-agents/backend-tester.md +10 -0
  282. package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
  283. package/src/ultraplan/default-agents/frontend-executor.md +10 -0
  284. package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
  285. package/src/ultraplan/default-agents/frontend-tester.md +10 -0
  286. package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
  287. package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
  288. package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
  289. package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
  290. package/src/ultraplan/execution/contract.ts +71 -0
  291. package/src/ultraplan/execution/policy.ts +217 -0
  292. package/src/ultraplan/execution/runtime-tools.ts +107 -0
  293. package/src/ultraplan/execution/session-runner.ts +281 -0
  294. package/src/ultraplan/next-router.ts +85 -0
  295. package/src/ultraplan/presenter.ts +359 -0
  296. package/src/ultraplan/project-paths.ts +342 -0
  297. package/src/ultraplan/runtime/active-execution.ts +72 -0
  298. package/src/ultraplan/runtime/apply-mutation.ts +416 -0
  299. package/src/ultraplan/runtime/blockers.ts +243 -0
  300. package/src/ultraplan/runtime/hook-bridge.ts +486 -0
  301. package/src/ultraplan/runtime/launch-context.ts +207 -0
  302. package/src/ultraplan/runtime/migration.ts +524 -0
  303. package/src/ultraplan/runtime/normalize.ts +281 -0
  304. package/src/ultraplan/runtime/proof.ts +260 -0
  305. package/src/ultraplan/runtime/reducer.ts +416 -0
  306. package/src/ultraplan/runtime/repair.ts +251 -0
  307. package/src/ultraplan/runtime/tracker-storage.ts +368 -0
  308. package/src/ultraplan/session-selection.ts +291 -0
  309. package/src/ultraplan/storage.ts +374 -0
  310. package/src/utils/editor.ts +38 -0
  311. package/src/utils/executable.ts +80 -0
  312. package/src/utils/paths.ts +1 -20
  313. package/src/utils/shell.ts +31 -0
  314. package/src/visual/companion.ts +2 -1
  315. package/src/visual/scripts/frame-template.html +60 -0
  316. package/src/visual/scripts/index.js +59 -13
  317. package/src/visual/scripts/package.json +3 -0
  318. package/src/visual/start-server.ts +2 -1
  319. package/src/workspace/git-scope.ts +64 -0
  320. package/src/workspace/locks.ts +23 -0
  321. package/src/workspace/package-manager.ts +117 -0
  322. package/src/workspace/path-mapping.ts +75 -0
  323. package/src/workspace/project-slug.ts +92 -0
  324. package/src/workspace/repo-root.ts +137 -0
  325. package/src/workspace/selector.ts +115 -0
  326. package/src/workspace/state-paths.ts +118 -0
  327. package/src/workspace/targets.ts +313 -0
  328. package/src/fix-pr/scripts/diff-comments.sh +0 -33
  329. package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
  330. package/src/fix-pr/scripts/trigger-review.sh +0 -36
  331. package/src/fix-pr/scripts/wait-and-check.sh +0 -37
  332. package/src/qa/scripts/detect-app-type.sh +0 -68
  333. package/src/qa/scripts/discover-routes.sh +0 -143
  334. package/src/qa/scripts/run-e2e-tests.sh +0 -131
  335. package/src/qa/scripts/start-dev-server.sh +0 -46
  336. package/src/qa/scripts/stop-dev-server.sh +0 -36
  337. package/src/review/prompts/fix-output-schema.md +0 -18
  338. package/src/review/prompts/review-output-schema.md +0 -38
  339. package/src/review/template.ts +0 -15
  340. /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
@@ -0,0 +1,108 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { fetchPrComments, parsePrCommentsJsonl } from "../fetch-comments.js";
5
+ import type { PrComment } from "../types.js";
6
+ import { createCliPlatformExec } from "./exec.js";
7
+
8
+ export interface WaitAndCheckSummary {
9
+ hasNewComments: boolean;
10
+ count: number;
11
+ iteration: number;
12
+ error?: string;
13
+ }
14
+
15
+ function fingerprint(comment: PrComment): string {
16
+ return `${comment.id}\t${comment.updatedAt ?? ""}`;
17
+ }
18
+
19
+ function diffComments(previous: readonly PrComment[], current: readonly PrComment[]): PrComment[] {
20
+ if (previous.length === 0) {
21
+ return [...current];
22
+ }
23
+
24
+ const previousFingerprints = new Set(previous.map(fingerprint));
25
+ return current.filter((comment) => !previousFingerprints.has(fingerprint(comment)));
26
+ }
27
+
28
+ async function sleep(ms: number): Promise<void> {
29
+ await new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+
32
+ export async function waitAndCheck(
33
+ sessionDir: string,
34
+ delaySeconds: number,
35
+ iteration: number,
36
+ repo: string,
37
+ prNumber: number,
38
+ ): Promise<{ exitCode: number; output: string }> {
39
+ const snapshotsDir = path.join(sessionDir, "snapshots");
40
+ const previousSnapshotPath = path.join(snapshotsDir, `comments-${iteration - 1}.jsonl`);
41
+ const newSnapshotPath = path.join(snapshotsDir, `comments-${iteration}.jsonl`);
42
+
43
+ await sleep(delaySeconds * 1_000);
44
+
45
+ const fetchError = await fetchPrComments(
46
+ createCliPlatformExec() as any,
47
+ repo,
48
+ prNumber,
49
+ newSnapshotPath,
50
+ process.cwd(),
51
+ );
52
+
53
+ if (fetchError) {
54
+ const summary: WaitAndCheckSummary = {
55
+ hasNewComments: false,
56
+ count: 0,
57
+ iteration,
58
+ error: fetchError,
59
+ };
60
+ return { exitCode: 1, output: JSON.stringify(summary) };
61
+ }
62
+
63
+ const previousComments = fs.existsSync(previousSnapshotPath)
64
+ ? parsePrCommentsJsonl(fs.readFileSync(previousSnapshotPath, "utf8"))
65
+ : [];
66
+ const currentComments = fs.existsSync(newSnapshotPath)
67
+ ? parsePrCommentsJsonl(fs.readFileSync(newSnapshotPath, "utf8"))
68
+ : [];
69
+ const changedComments = diffComments(previousComments, currentComments);
70
+ const summary: WaitAndCheckSummary = {
71
+ hasNewComments: changedComments.length > 0,
72
+ count: changedComments.length,
73
+ iteration,
74
+ };
75
+
76
+ const lines = changedComments.map((comment) => JSON.stringify(comment));
77
+ lines.push(JSON.stringify(summary));
78
+ return {
79
+ exitCode: 0,
80
+ output: lines.join("\n"),
81
+ };
82
+ }
83
+
84
+ async function main(): Promise<void> {
85
+ const [sessionDir, delayArg, iterationArg, repo, prNumberArg] = process.argv.slice(2);
86
+ const delaySeconds = Number.parseInt(delayArg ?? "", 10);
87
+ const iteration = Number.parseInt(iterationArg ?? "", 10);
88
+ const prNumber = Number.parseInt(prNumberArg ?? "", 10);
89
+
90
+ if (!sessionDir || !Number.isInteger(delaySeconds) || !Number.isInteger(iteration) || !repo || !Number.isInteger(prNumber)) {
91
+ console.log(JSON.stringify({
92
+ hasNewComments: false,
93
+ count: 0,
94
+ iteration: Number.isInteger(iteration) ? iteration : 0,
95
+ error: "Usage: wait-and-check.ts <session_dir> <delay_seconds> <iteration> <owner/repo> <pr_number>",
96
+ }));
97
+ process.exit(1);
98
+ }
99
+
100
+ const result = await waitAndCheck(sessionDir, delaySeconds, iteration, repo, prNumber);
101
+ console.log(result.output);
102
+ process.exit(result.exitCode);
103
+ }
104
+
105
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
106
+ if (isMain) {
107
+ void main();
108
+ }
@@ -1,3 +1,5 @@
1
+ import type { FixPrAssessmentBatch } from "./contracts.js";
2
+
1
3
  /** Supported automated PR reviewers */
2
4
  export type ReviewerType = "coderabbit" | "copilot" | "gemini" | "none";
3
5
 
@@ -58,4 +60,6 @@ export interface FixPrSessionLedger {
58
60
  iteration: number;
59
61
  config: FixPrConfig;
60
62
  commentsProcessed: number[];
63
+ /** Validated per-comment assessment artifact. Absent in older sessions. */
64
+ assessment?: FixPrAssessmentBatch;
61
65
  }
@@ -71,6 +71,11 @@ export function buildBranchFinishPrompt(
71
71
  "",
72
72
  "```bash",
73
73
  `git push -u origin ${branchName}`,
74
+ "```",
75
+ "",
76
+ "Then open the PR. **Preferred:** call the `github` tool with `op: \"pr_create\"` — supply `title` and `body` (or `fill: true` to auto-fill from commits); it returns the new PR URL. **Fallback** if the tool is unavailable in your runtime:",
77
+ "",
78
+ "```bash",
74
79
  `gh pr create --title "<title>" --body "<summary>"`,
75
80
  "```",
76
81
  "",
@@ -0,0 +1,83 @@
1
+ // src/git/commit-contract.ts
2
+ //
3
+ // Schema-backed contract for AI-generated commit plans. The AI must return a
4
+ // CommitPlan matching this schema; parseStructuredOutput enforces the structure
5
+ // and runWithOutputValidation retries with schema feedback on drift. Coverage
6
+ // (every staged file appears in exactly one commit) is a runtime rule that
7
+ // can't live in the schema — see validateCommitPlanCoverage.
8
+
9
+ import { type Static, Type } from "@sinclair/typebox";
10
+ import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
11
+ import type { ValidationError } from "../types.js";
12
+
13
+ export const CommitGroupSchema = Type.Object(
14
+ {
15
+ type: Type.Union(VALID_COMMIT_TYPES.map((value) => Type.Literal(value))),
16
+ scope: Type.Union([Type.String(), Type.Null()]),
17
+ summary: Type.String({ minLength: 1 }),
18
+ details: Type.Array(Type.String()),
19
+ files: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
20
+ },
21
+ { additionalProperties: false },
22
+ );
23
+
24
+ export const CommitPlanSchema = Type.Object(
25
+ {
26
+ commits: Type.Array(CommitGroupSchema, { minItems: 1 }),
27
+ },
28
+ { additionalProperties: false },
29
+ );
30
+
31
+ export type CommitGroup = Static<typeof CommitGroupSchema>;
32
+ export type CommitPlan = Static<typeof CommitPlanSchema>;
33
+
34
+ /**
35
+ * Verify every staged file appears in exactly one commit and that no commit
36
+ * references a file outside the staged set. Returns an empty array on success;
37
+ * a non-empty array indicates the plan is unusable and the caller must block.
38
+ */
39
+ export function validateCommitPlanCoverage(
40
+ plan: CommitPlan,
41
+ stagedFiles: string[],
42
+ ): ValidationError[] {
43
+ const errors: ValidationError[] = [];
44
+ const stagedSet = new Set(stagedFiles);
45
+ const occurrences = new Map<string, number[]>();
46
+
47
+ plan.commits.forEach((commit, commitIdx) => {
48
+ commit.files.forEach((file, fileIdx) => {
49
+ if (!stagedSet.has(file)) {
50
+ errors.push({
51
+ path: `commits[${commitIdx}].files[${fileIdx}]`,
52
+ message: `File is not in the staged set: ${file}`,
53
+ });
54
+ }
55
+ const existing = occurrences.get(file);
56
+ if (existing) {
57
+ existing.push(commitIdx);
58
+ } else {
59
+ occurrences.set(file, [commitIdx]);
60
+ }
61
+ });
62
+ });
63
+
64
+ for (const [file, commitIdxs] of occurrences) {
65
+ if (commitIdxs.length > 1) {
66
+ errors.push({
67
+ path: "commits",
68
+ message: `File appears in multiple commits (indices ${commitIdxs.join(", ")}): ${file}`,
69
+ });
70
+ }
71
+ }
72
+
73
+ for (const file of stagedFiles) {
74
+ if (!occurrences.has(file)) {
75
+ errors.push({
76
+ path: "commits",
77
+ message: `Staged file not covered by any commit: ${file}`,
78
+ });
79
+ }
80
+ }
81
+
82
+ return errors;
83
+ }
package/src/git/commit.ts CHANGED
@@ -4,9 +4,11 @@
4
4
  // a conventional-commit plan (optionally split by file), presents
5
5
  // the plan for user approval, then executes file-level staging + commit.
6
6
 
7
- import type { Platform } from "../platform/types.js";
7
+ import type { Platform, PlatformPaths } from "../platform/types.js";
8
+ import { appendReliabilityRecord } from "../storage/reliability-metrics.js";
8
9
  import { createWorkflowProgress } from "../platform/progress.js";
9
10
  import { VALID_COMMIT_TYPES } from "../release/commit-types.js";
11
+ import { resolveRepoRoot } from "../workspace/repo-root.js";
10
12
  import { validateCommitMessage } from "./commit-msg.js";
11
13
  import { getWorkingTreeStatus } from "./status.js";
12
14
  import { discoverCommitConventions } from "./conventions.js";
@@ -18,17 +20,13 @@ import { loadModelConfig } from "../config/model-config.js";
18
20
 
19
21
  // ── Public types ───────────────────────────────────────────
20
22
 
21
- export interface CommitGroup {
22
- type: string;
23
- scope: string | null;
24
- summary: string;
25
- details: string[];
26
- files: string[];
27
- }
23
+ import { CommitPlanSchema, validateCommitPlanCoverage, type CommitGroup, type CommitPlan } from "./commit-contract.js";
24
+ import { parseStructuredOutput, runWithOutputValidation, formatValidationErrors } from "../ai/structured-output.js";
25
+ import { renderSchemaText } from "../ai/schema-text.js";
28
26
 
29
- export interface CommitPlan {
30
- commits: CommitGroup[];
31
- }
27
+ export type { CommitGroup, CommitPlan };
28
+
29
+ const COMMIT_PLAN_SCHEMA_TEXT = renderSchemaText(CommitPlanSchema);
32
30
 
33
31
  export interface CommitResult {
34
32
  committed: number;
@@ -135,6 +133,49 @@ function createProgress(ctx: any) {
135
133
  };
136
134
  }
137
135
 
136
+ // ── Staging context ───────────────────────────────────────────
137
+
138
+ interface CommitStagingContext {
139
+ stagedFiles: string[];
140
+ }
141
+
142
+ async function ensureStagedChanges(
143
+ platform: Platform,
144
+ ctx: any,
145
+ cwd: string,
146
+ status: Awaited<ReturnType<typeof getWorkingTreeStatus>>,
147
+ progress: ReturnType<typeof createProgress>,
148
+ ): Promise<CommitStagingContext | null> {
149
+ const exec = platform.exec.bind(platform);
150
+
151
+ if (status.stagedFiles.length === 0) {
152
+ progress.activate(1, `${status.files.length} file(s)`);
153
+ const addResult = await exec("git", ["add", "-A"], { cwd });
154
+ if (addResult.code !== 0) {
155
+ notifyError(ctx, "git add failed", addResult.stderr || "Non-zero exit");
156
+ return null;
157
+ }
158
+ progress.complete(1, `${status.files.length} file(s)`);
159
+ } else {
160
+ progress.activate(1, `${status.stagedFiles.length} staged`);
161
+ progress.complete(1, `${status.stagedFiles.length} staged`);
162
+ }
163
+
164
+ const stagedFilesResult = await exec("git", ["diff", "--cached", "--name-only"], { cwd });
165
+ if (stagedFilesResult.code !== 0) {
166
+ notifyError(ctx, "git diff failed", stagedFilesResult.stderr || "Could not read staged files");
167
+ return null;
168
+ }
169
+
170
+ const stagedFiles = normalizeLineEndings(stagedFilesResult.stdout).trim().split("\n").filter(Boolean);
171
+ if (stagedFiles.length === 0) {
172
+ notifyInfo(ctx, "Nothing to commit", "No changes after staging");
173
+ return null;
174
+ }
175
+
176
+ return { stagedFiles };
177
+ }
178
+
138
179
  // ── Main entry point ───────────────────────────────────────
139
180
 
140
181
  /**
@@ -148,7 +189,7 @@ export async function analyzeAndCommit(
148
189
  options: CommitOptions = {},
149
190
  ): Promise<CommitResult | null> {
150
191
  const exec = platform.exec.bind(platform);
151
- const cwd: string = ctx.cwd;
192
+ const cwd = await resolveRepoRoot(platform, ctx.cwd);
152
193
  const progress = createProgress(ctx);
153
194
 
154
195
  try {
@@ -163,32 +204,24 @@ export async function analyzeAndCommit(
163
204
  }
164
205
  progress.complete(0, `${status.files.length} file(s)`);
165
206
 
166
- // 2. Stage everything (match OMP behavior: include untracked)
167
- progress.activate(1, `${status.files.length} file(s)`);
168
- const addResult = await exec("git", ["add", "-A"], { cwd });
169
- if (addResult.code !== 0) {
170
- progress.dispose();
171
- notifyError(ctx, "git add failed", addResult.stderr || "Non-zero exit");
207
+ const stagingContext = await ensureStagedChanges(
208
+ platform,
209
+ ctx,
210
+ cwd,
211
+ status,
212
+ progress,
213
+ );
214
+ if (!stagingContext) {
172
215
  return null;
173
216
  }
174
- progress.complete(1, `${status.files.length} file(s)`);
175
217
 
176
218
  // 3. Gather diff information
177
- progress.activate(2);
178
- const [diffResult, statResult, filesResult] = await Promise.all([
219
+ const fileList = stagingContext.stagedFiles;
220
+ progress.activate(2, `${fileList.length} file(s)`);
221
+ const [diffResult, statResult] = await Promise.all([
179
222
  exec("git", ["diff", "--cached"], { cwd }),
180
223
  exec("git", ["diff", "--cached", "--stat"], { cwd }),
181
- exec("git", ["diff", "--cached", "--name-only"], { cwd }),
182
224
  ]);
183
-
184
- const fileList = normalizeLineEndings(filesResult.stdout).trim().split("\n").filter(Boolean);
185
- if (fileList.length === 0) {
186
- progress.complete(2, "empty");
187
- progress.dispose();
188
- notifyInfo(ctx, "Nothing to commit", "No changes after staging");
189
- await exec("git", ["reset", "HEAD"], { cwd });
190
- return null;
191
- }
192
225
  progress.complete(2, `${fileList.length} file(s)`);
193
226
 
194
227
  // 4. Discover conventions
@@ -207,6 +240,7 @@ export async function analyzeAndCommit(
207
240
 
208
241
  let plan: CommitPlan | null = null;
209
242
  let agentReason: string | undefined;
243
+ let agentAttempts = 0;
210
244
 
211
245
  if (platform.capabilities.agentSessions) {
212
246
  progress.activate(4, `${fileList.length} file(s)`);
@@ -223,20 +257,21 @@ export async function analyzeAndCommit(
223
257
  "harness role";
224
258
  let detail = sourceLabel;
225
259
  if (candidate.thinkingLevel) {
226
- detail += ` \u00b7 ${candidate.thinkingLevel} thinking`;
260
+ detail += ` · ${candidate.thinkingLevel} thinking`;
227
261
  }
228
262
  ctx.ui?.setStatus?.("supi-model", `Model: ${candidate.model} (${detail})`);
229
263
  }
230
264
 
231
- const agentResult = await tryAgentPlan(platform, cwd, prompt, candidate.model);
265
+ const agentResult = await tryAgentPlan(platform, cwd, prompt, fileList, candidate.model);
232
266
  if (agentResult.plan) {
233
- plan = validatePlanFiles(agentResult.plan, fileList);
267
+ plan = agentResult.plan;
234
268
  progress.complete(4, `${plan.commits.length} commit(s)`);
235
269
  break;
236
270
  }
237
271
 
238
272
  // Store last failure reason; try next candidate
239
273
  agentReason = agentResult.reason;
274
+ agentAttempts = agentResult.attempts;
240
275
  }
241
276
 
242
277
  if (!plan) {
@@ -254,7 +289,7 @@ export async function analyzeAndCommit(
254
289
  const reason = !platform.capabilities.agentSessions
255
290
  ? "no agent sessions"
256
291
  : agentReason;
257
- return manualFallback(platform, ctx, cwd, fileList, reason);
292
+ return manualFallback(platform, ctx, cwd, fileList, platform.paths, agentAttempts, reason);
258
293
  }
259
294
 
260
295
  // 6. Present plan for approval
@@ -262,11 +297,11 @@ export async function analyzeAndCommit(
262
297
  const planDisplay = formatPlanForDisplay(plan);
263
298
  notifyInfo(ctx, "Commit plan ready", "\n" + planDisplay);
264
299
  const commitLabel = plan.commits.length === 1
265
- ? `commit \u2014 ${formatCommitHeader(plan.commits[0])}`
266
- : `commit \u2014 apply ${plan.commits.length} commits`;
300
+ ? `commit ${formatCommitHeader(plan.commits[0])}`
301
+ : `commit apply ${plan.commits.length} commits`;
267
302
  const action = await ctx.ui.select("Proceed?", [
268
303
  commitLabel,
269
- "abort \u2014 cancel",
304
+ "abort cancel",
270
305
  ]);
271
306
 
272
307
  if (!action || action.startsWith("abort")) {
@@ -292,62 +327,53 @@ interface AgentPlanResult {
292
327
  plan: CommitPlan | null;
293
328
  /** Human-readable reason when plan is null */
294
329
  reason?: string;
330
+ /** Number of attempts made by runWithOutputValidation (0 if never reached). */
331
+ attempts: number;
295
332
  }
296
333
 
297
334
  async function tryAgentPlan(
298
335
  platform: Platform,
299
336
  cwd: string,
300
337
  prompt: string,
338
+ stagedFiles: string[],
301
339
  model?: string,
302
340
  ): Promise<AgentPlanResult> {
303
- let session: Awaited<ReturnType<Platform["createAgentSession"]>> | null = null;
304
341
  try {
305
- session = await platform.createAgentSession({ cwd, hasUI: false, ...(model ? { model } : {}) });
306
-
307
- const agentDone = new Promise<void>((resolve) => {
308
- session!.subscribe((event: any) => {
309
- if (event.type === "agent_end") resolve();
310
- });
311
- });
312
-
313
- await session.prompt(prompt);
314
- await agentDone;
315
-
316
- // Extract JSON from the last assistant message
317
- const messages = session.state.messages;
318
- const lastAssistant = [...messages]
319
- .reverse()
320
- .find((m: any) => m.role === "assistant");
321
-
322
- if (!lastAssistant) return { plan: null, reason: "no assistant response" };
323
-
324
- const text =
325
- typeof lastAssistant.content === "string"
326
- ? lastAssistant.content
327
- : Array.isArray(lastAssistant.content)
328
- ? lastAssistant.content
329
- .filter((b: any) => b.type === "text")
330
- .map((b: any) => b.text)
331
- .join("\n")
332
- : "";
342
+ const result = await runWithOutputValidation<CommitPlan>(
343
+ platform.createAgentSession.bind(platform),
344
+ {
345
+ cwd,
346
+ prompt,
347
+ schema: COMMIT_PLAN_SCHEMA_TEXT,
348
+ parse: (raw) => parseStructuredOutput<CommitPlan>(raw, CommitPlanSchema),
349
+ model,
350
+ maxAttempts: 3,
351
+ reliability: {
352
+ paths: platform.paths,
353
+ cwd,
354
+ command: "commit",
355
+ operation: "commit-plan",
356
+ },
357
+ },
358
+ );
333
359
 
334
- if (!text) return { plan: null, reason: "empty agent response" };
360
+ if (result.status === "blocked") {
361
+ return { plan: null, reason: result.error, attempts: result.attempts };
362
+ }
335
363
 
336
- const plan = parseCommitPlan(text);
337
- if (!plan) return { plan: null, reason: diagnoseParseFailure(text) };
364
+ const coverageErrors = validateCommitPlanCoverage(result.output, stagedFiles);
365
+ if (coverageErrors.length > 0) {
366
+ return {
367
+ plan: null,
368
+ reason: `Commit plan coverage check failed: ${formatValidationErrors(coverageErrors).join("; ")}`,
369
+ attempts: result.attempts,
370
+ };
371
+ }
338
372
 
339
- return { plan };
373
+ return { plan: result.output, attempts: result.attempts };
340
374
  } catch (err) {
341
375
  const message = err instanceof Error ? err.message : String(err);
342
- return { plan: null, reason: message };
343
- } finally {
344
- if (session) {
345
- try {
346
- await session.dispose();
347
- } catch {
348
- // Swallow disposal errors
349
- }
350
- }
376
+ return { plan: null, reason: message, attempts: 0 };
351
377
  }
352
378
  }
353
379
 
@@ -358,10 +384,24 @@ async function manualFallback(
358
384
  ctx: any,
359
385
  cwd: string,
360
386
  fileList: string[],
387
+ paths: PlatformPaths,
388
+ attempts: number,
361
389
  reason?: string,
362
390
  ): Promise<CommitResult | null> {
363
391
  const exec = platform.exec.bind(platform);
364
392
 
393
+ try {
394
+ appendReliabilityRecord(paths, cwd, {
395
+ ts: new Date().toISOString(),
396
+ command: "commit",
397
+ operation: "commit-plan",
398
+ outcome: "fallback",
399
+ attempts,
400
+ reason,
401
+ cwd,
402
+ });
403
+ } catch {}
404
+
365
405
  notifyInfo(
366
406
  ctx,
367
407
  "AI commit unavailable",
@@ -389,24 +429,6 @@ async function manualFallback(
389
429
  return { committed: 1, messages: [message] };
390
430
  }
391
431
 
392
- // ── Plan validation ────────────────────────────────────────
393
-
394
- /**
395
- * Filter an AI-generated commit plan against the actual staged file list.
396
- * Removes hallucinated paths that aren't staged, and drops empty groups.
397
- * Falls back to the original plan if filtering would leave nothing.
398
- */
399
- export function validatePlanFiles(plan: CommitPlan, stagedFiles: string[]): CommitPlan {
400
- const stagedSet = new Set(stagedFiles);
401
- const validCommits = plan.commits
402
- .map((group) => ({
403
- ...group,
404
- files: group.files.filter((f) => stagedSet.has(f)),
405
- }))
406
- .filter((group) => group.files.length > 0);
407
-
408
- return validCommits.length > 0 ? { commits: validCommits } : plan;
409
- }
410
432
 
411
433
 
412
434
  // ── Commit execution ───────────────────────────────────────
@@ -542,6 +564,7 @@ export function buildAnalysisPrompt(input: PromptInput): string {
542
564
  parts.push(`**Developer context:** ${userContext}`, "");
543
565
  }
544
566
 
567
+
545
568
  // Diff content — truncate for large diffs
546
569
  const diffBytes = Buffer.byteLength(normalizedDiff, "utf8");
547
570
 
@@ -589,93 +612,7 @@ export function buildAnalysisPrompt(input: PromptInput): string {
589
612
  return parts.join("\n");
590
613
  }
591
614
 
592
- // ── Plan parsing ───────────────────────────────────────────
593
-
594
- /**
595
- * Produce a human-readable reason why parseCommitPlan returned null.
596
- * Used for diagnostics — never shown raw to the user.
597
- */
598
- function diagnoseParseFailure(text: string): string {
599
- const fenceRe = /```json\s*\n([\s\S]*?)```/;
600
- const match = fenceRe.exec(text);
601
- if (!match) return "no JSON code block in response";
602
-
603
- let parsed: any;
604
- try {
605
- parsed = JSON.parse(match[1]);
606
- } catch {
607
- return "JSON parse error in response";
608
- }
609
-
610
- if (!parsed.commits || !Array.isArray(parsed.commits)) return "missing commits array";
611
- if (parsed.commits.length === 0) return "empty commits array";
612
615
 
613
- for (const c of parsed.commits) {
614
- if (!c.type) return "commit missing type";
615
- if (!c.summary) return "commit missing summary";
616
- if (!Array.isArray(c.files) || c.files.length === 0) return "commit missing files";
617
- if (!(VALID_COMMIT_TYPES as readonly string[]).includes(c.type)) {
618
- return `invalid commit type: ${c.type}`;
619
- }
620
- }
621
-
622
- // Check for duplicate files across groups
623
- const seen = new Set<string>();
624
- for (const group of parsed.commits) {
625
- for (const file of group.files) {
626
- if (seen.has(file)) return `duplicate file across commits: ${file}`;
627
- seen.add(file);
628
- }
629
- }
630
-
631
- return "unknown parse failure";
632
- }
633
-
634
-
635
- /** Exported for testing */
636
- export function parseCommitPlan(text: string): CommitPlan | null {
637
- // Look for ```json ... ``` fenced block
638
- const fenceRe = /```json\s*\n([\s\S]*?)```/;
639
- const match = fenceRe.exec(text);
640
- if (!match) return null;
641
-
642
- try {
643
- const parsed = JSON.parse(match[1]);
644
- if (!parsed.commits || !Array.isArray(parsed.commits)) return null;
645
-
646
- const commits: CommitGroup[] = [];
647
- for (const c of parsed.commits) {
648
- if (!c.type || !c.summary || !Array.isArray(c.files) || c.files.length === 0) {
649
- return null;
650
- }
651
- if (!(VALID_COMMIT_TYPES as readonly string[]).includes(c.type)) {
652
- return null;
653
- }
654
- commits.push({
655
- type: c.type,
656
- scope: c.scope ?? null,
657
- summary: String(c.summary),
658
- details: Array.isArray(c.details) ? c.details.map(String) : [],
659
- files: c.files.map(String),
660
- });
661
- }
662
-
663
- if (commits.length === 0) return null;
664
-
665
- // Validate: no duplicate files across groups
666
- const seen = new Set<string>();
667
- for (const group of commits) {
668
- for (const file of group.files) {
669
- if (seen.has(file)) return null;
670
- seen.add(file);
671
- }
672
- }
673
-
674
- return { commits };
675
- } catch {
676
- return null;
677
- }
678
- }
679
616
 
680
617
  // ── Formatting ─────────────────────────────────────────────
681
618