chainlesschain 0.162.61 → 0.162.66

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 (164) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-BiB8WfIz.js → AIOps-BeJlvHR1.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-NLBhC6jG.js → ActionButton-B93fwcal.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-k62j-xiL.js → Analytics-B-Lc0FRK.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-spr0Sm6J.js → AppLayout-nv2C8TdH.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-C3NHJos3.js → Audit-B4pwb1Oe.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-C2V9tGqF.js → Backup-B4HFWkJA.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-DeKm11mH.js → BaseInput-BQk1ONWO.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-CHZ2CU7x.js → Chat-B2IobFfI.js} +5 -5
  10. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DEXSa7tC.js → ChatBubbleRenderer-jJL-hGlG.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-q6E9VeLr.js → Checkbox-BHCU5kit.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen--4w4QpUI.js → Codegen-Bqgq9-0q.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-DLOkwTsj.js → Col-BqDf398Z.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-B1LxJGfE.js → Community-Cd58ltip.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-C_769oQZ.js → Compact-CWa3CY1X.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-zsI0s7vB.js → Compliance-BNJWsGi0.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-A1WA6whF.js → Cowork-ChOCC2KD.js} +3 -3
  18. package/src/assets/web-panel/assets/{Cron-C3JDTyyd.js → Cron-BpWtgfDE.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-D8O6uB86.js → Crosschain-Csy7U94a.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-BpOebm5d.js → DID-CIh7lr9T.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-Defso6kA.js → Dashboard-DYJUc9Jy.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-Cv9BrwT_.js → Dropdown-GnUptPAU.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-BWKHbh4C.js → EmailListRenderer-sE9mvxjT.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard--m_Ru7Ci.js → FamilyGuardDashboard-Dg7-GHSu.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-Bs6ZcAP0.js → Federation-BiH_O7jy.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-CcAs3Acx.js → FormItemContext-C1AZ_qE-.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-DI1oL4pK.js → GenericCardRenderer-WpsC5meD.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-C27t3-fW.js → Git-L4XGb5Qj.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-Dr_syXc_.js → Governance-CJ9Gn_A9.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-CWM8dIbA.js → Inference-Dlc9Ey87.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph--cFDUZv3.js → KnowledgeGraph-CfndHiBW.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-Cnn2_Onf.js → Logs-CSOLZERs.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-4T9ok3Gz.js → Marketplace-Cjiz9wPY.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-BQvZwqcN.js → McpTools-C8UNhnTj.js} +4 -4
  35. package/src/assets/web-panel/assets/{Memory-BE9rPkM_.js → Memory-BhOoGXRL.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-DJ3j5lXC.js → MobileBridge-rKKcGEvg.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileProjects-qasLvYdb.js → MobileProjects-KvGFVl79.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-D3CSPTD9.js → Mtc-jyA3mXYt.js} +5 -5
  39. package/src/assets/web-panel/assets/{MtcAudit-DaYVGCN5.js → MtcAudit-BYi6sulR.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-Dz1c5r5w.js → Multisig-CWZGD6_3.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BIKV_K-a.js → NLProgramming-bOIPDOh5.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-f6t-rmOa.js → Notes-_I6Hs_bJ.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-DHLQh8Fy.js → NotificationSettings-Bj9Bcy2A.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-CDMZ3o6i.js → OrderTableRenderer-C5zZeOhJ.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-DIsL758p.js → Organization-B-SfOynT.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-BMM7apnZ.js → Overflow-CG7JBYts.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-CTGMmTvi.js → P2P-vpebJYvr.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-DWwmm0k1.js → PdhVaultBrowser-CPeKixo2.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-Bed5JxMx.js → Permissions-CpflO2Ac.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-CIiZhSM5.js → PersonalDataHub-D6CjgWDH.js} +4 -4
  51. package/src/assets/web-panel/assets/{Pipeline-CtirPodz.js → Pipeline-CgC59gHt.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-DuXhXhE7.js → Privacy-CoZn6LrI.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-DZrnguBl.js → ProjectInit-BmNYdFPv.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-HIltqsJ1.js → ProjectSettings-C13HHOUG.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-BWFkePg4.js → Projects-BSHYaYKU.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-DEP0Jdvl.js → Providers-Dh6ys5NR.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-T2THoHNx.js → QuickAsk-Cljz9ZIS.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-CvbxaSwm.js → Recommend-CEAVAYGZ.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-6Afy6tfp.js → Reputation-C7AxH6cu.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-DY8OPWaO.js → Row-D5Jgzbof.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-wDGWb9pZ.js → RssFeed-V6vBnNBE.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-D_zHAwZY.js → Search-dfDC6aHa.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-Czq7AlGG.js → Security-BOSRXul6.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-Ac1g0ZcG.js → Services-dsNT3Tra.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-DXQ3eeSW.js → Skeleton-_rXFZqCe.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-CWRioX4u.js → Skills-WWvbl-N6.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-BlHthzfs.js → Sla-BZT_pwjV.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-Ct240JmL.js → SpeechSettings-BSVqkQ0F.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-BgDIt8Q-.js → SyncSettings-TLHuQW_s.js} +2 -2
  70. package/src/assets/web-panel/assets/Tasks-8-jiv3Dt.js +1 -0
  71. package/src/assets/web-panel/assets/{Templates-Dp9QhyIw.js → Templates-D7qs_H3G.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-CHTYMxzY.js → Tenant-B4hRLxlc.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-X-NGwLpv.js → Terminal-lrlEETgH.js} +2 -2
  74. package/src/assets/web-panel/assets/{TimelineRenderer-Bh8jA18j.js → TimelineRenderer-Do8UQaNj.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-DWkTd5dv.js → Tokens-wyuwl9gS.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-CRgVg6sd.js → Trigger-Ck4j8Emr.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-BgWOXd0W.js → Trust-Z07lGZvX.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-BlTUB9Y-.js → UkeySign-5rI48ojV.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-DI64XgNb.js → VideoEditing-Dm8PV-Ss.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-CJ3TNGiG.js → Wallet-BwWYuV0j.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-B2-rWWoV.js → WebAuthn-CavU3f2i.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-VI9otbaH.js → WorkflowEditor-jj1aB37x.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-CgYfiaVh.js → chat-BpRqPqbA.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-B9EhRTky.js → colors-BBJU99fO.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-Cb7bjraa.js → compact-item-t2Elz5Kg.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-DlXPeXuj.js → createContext-B_D6Nida.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-CmJstpP_.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-BbgRfrdf.js → hasIn-D5k1KNpe.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-ojRAd7Nq.js → index-1lcpLp-e.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-BoEFFKn3.js → index-2KvtrQkP.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BU8hEUyq.js → index-33FQbw3H.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-Bt-lPYpq.js → index-3TymUGUQ.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-D8kB0k3E.js → index-6iwQSswx.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-c7-Jd6WB.js → index-B2cxsdFe.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DPaffcT8.js → index-BCpJT0on.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-p03wNqiP.js → index-BH_HjIO6.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-1iUK_kAw.js → index-BVEb-kUY.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-AR-QpAkP.js → index-BaS1rfcr.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BfSS-U5o.js → index-BfKFGtsC.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DDzNdZcX.js → index-Bo72gKe8.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CGx8aO_Y.js → index-C-271Q6Z.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-2ts5iOIB.js → index-C3alXfss.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-C95qWAh4.js → index-CB1AFQiL.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-De59Xat-.js → index-CHyyhgQ3.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Cmr31VCO.js → index-CP2Nz1mx.js} +3 -3
  106. package/src/assets/web-panel/assets/{index-fG-1gXy0.js → index-CT_-tscA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-D6t-Shqr.js → index-CWwjhqgi.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C73WgOc2.js → index-CYdBeNTv.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CMyzmvtJ.js → index-CgCeon6Z.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DUU9DY4J.js → index-Ctt8xM0M.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BD2W-qsS.js → index-CwAKqpJd.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Ira0HLPr.js → index-CwEJnamf.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Dh6qWb1v.js → index-D8LnFSKZ.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-D-Zz9PvD.js → index-DGtbeySv.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Bk7r1a9x.js → index-DHHTczBZ.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DbLJShJB.js → index-DIgtJB5J.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Cf9zwbk-.js → index-DLfAmDGs.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Caiu2avX.js → index-DRh9m8hf.js} +1 -1
  119. package/src/assets/web-panel/assets/index-Dric_1LC.js +1 -0
  120. package/src/assets/web-panel/assets/{index-CHR47Q5B.js → index-DzIq7BlR.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-OVrh8wTN.js → index-E4x-hzLB.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-5CrFMQjt.js → index-GSUbdu-w.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Dx4sm6dm.js → index-GSpv5udU.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DkQIyK-V.js → index-NMj4bTp1.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C8DB27uJ.js → index-OmJk1MzD.js} +1 -1
  126. package/src/assets/web-panel/assets/index-eFol7ymc.js +1 -0
  127. package/src/assets/web-panel/assets/{index-DXxa7PR8.js → index-eVBDpynR.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-JT674ACa.js → initDefaultProps-CuryY55W.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-CokflrA9.js → motion-IVsWxV-s.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-CfMhRpyC.js → move-DNKsQLrH.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-ClYc5II5.js → omit-gXu4NluL.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-CTwEb_8h.js → pickAttrs-CVb4Ykex.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-Cb3StU_t.js → placementArrow-BarpAJK0.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-uIxkx5M1.js → responsiveObserve-eaDhQlY1.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-B9HZBQ7i.js → slide-C8oU8Wlo.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-C72bwYl0.js → statusUtils-DrBdj2xb.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-BFTtaQb8.js → styleChecker-_Rycq1-Q.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-DKl5j41_.js → useFlexGapSupport-BagU5XLY.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-BUHS6bo3.js → useFs-DZYMOaAQ.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-D8KrYSq4.js → usePersonalDataHub-CDMZ5QvU.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-JYP-aZDj.js → vnode-Bg7MuEf5.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-BFusdxdH.js → zoom-oJIwomWy.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/agent.js +39 -0
  145. package/src/commands/insights.js +137 -0
  146. package/src/commands/review.js +807 -0
  147. package/src/index.js +4 -0
  148. package/src/lib/cost-budget.js +109 -0
  149. package/src/lib/ide-context.js +50 -0
  150. package/src/lib/personal-data-hub-wiring.js +36 -0
  151. package/src/lib/session-insights.js +145 -0
  152. package/src/lib/skill-loader.js +18 -3
  153. package/src/repl/agent-repl.js +34 -0
  154. package/src/repl/tasks-status.js +82 -0
  155. package/src/runtime/agent-core.js +38 -3
  156. package/src/runtime/headless-runner.js +51 -3
  157. package/src/runtime/headless-stream.js +62 -4
  158. package/src/runtime/mcp-config.js +6 -0
  159. package/src/skills-bundled/run/SKILL.md +49 -0
  160. package/src/skills-bundled/verify/SKILL.md +49 -0
  161. package/src/assets/web-panel/assets/Tasks-3PTmatJP.js +0 -1
  162. package/src/assets/web-panel/assets/devWarning-D-Hp8s_8.js +0 -1
  163. package/src/assets/web-panel/assets/index-BvnHuxVM.js +0 -1
  164. package/src/assets/web-panel/assets/index-Dkm5IGwX.js +0 -1
@@ -0,0 +1,807 @@
1
+ /**
2
+ * cc review — diff-first code review (Claude-Code `/code-review` parity).
3
+ *
4
+ * cc review review the working tree vs HEAD
5
+ * cc review --staged review staged changes only (git diff --cached)
6
+ * cc review --base main review this branch vs main (main...HEAD)
7
+ * cc review --range A..B review an explicit revision range
8
+ * cc review high broader, more thorough pass (effort tier)
9
+ * cc review --fix apply fixes to the working tree (reversible)
10
+ * cc review --security security-focused review (/security-review)
11
+ * cc review --simplify cleanup-only review (/simplify), no bug hunt
12
+ *
13
+ * "Diff-first" means the changed lines are collected with git and handed to the
14
+ * agent up front, so the review is anchored on what actually changed instead of
15
+ * the whole repo. The agent still has read/search tools to open surrounding code
16
+ * for context.
17
+ *
18
+ * Two modes:
19
+ * - review-only (default): runs in PLAN permission mode → the agent is clamped
20
+ * to read-only tools and CANNOT modify files. It emits a Markdown findings
21
+ * report on stdout.
22
+ * - --fix: runs in acceptEdits mode with auto-checkpointing ON, so the agent
23
+ * applies the fixes directly; every file edit is captured as a git-plumbing
24
+ * shadow commit first, so the whole pass is reversible with `cc checkpoint
25
+ * restore <id>`.
26
+ *
27
+ * Requires a git work tree (the diff is the input). LLM defaults follow
28
+ * .chainlesschain/config.json `llm` exactly like `cc agent` / `cc ask`.
29
+ */
30
+
31
+ import { spawnSync } from "node:child_process";
32
+ import fs from "node:fs";
33
+ import path from "node:path";
34
+ import chalk from "chalk";
35
+ import { logger } from "../lib/logger.js";
36
+
37
+ /** Diffs larger than this are truncated before going to the model. */
38
+ const MAX_DIFF_CHARS = 200_000;
39
+ /** Per-untracked-file and total caps when inlining brand-new files. */
40
+ const MAX_UNTRACKED_FILE_CHARS = 32_000;
41
+ const MAX_UNTRACKED_TOTAL_CHARS = 128_000;
42
+
43
+ const VALID_EFFORTS = Object.freeze(["low", "medium", "high"]);
44
+
45
+ /**
46
+ * Run git with an argv array (no shell → no quoting hazards). UTF-8 in/out.
47
+ * Returns trimmed stdout; throws with git's stderr on failure.
48
+ */
49
+ function gitCli(args, { cwd } = {}) {
50
+ const res = spawnSync("git", args, {
51
+ cwd,
52
+ encoding: "utf-8",
53
+ windowsHide: true,
54
+ maxBuffer: 256 * 1024 * 1024,
55
+ });
56
+ if (res.error) throw res.error;
57
+ if (res.status !== 0) {
58
+ const msg = (res.stderr || res.stdout || "").toString().trim();
59
+ throw new Error(msg || `git ${args.join(" ")} failed (exit ${res.status})`);
60
+ }
61
+ return (res.stdout || "").toString();
62
+ }
63
+
64
+ function isGitRepo(cwd, git = gitCli) {
65
+ try {
66
+ return (
67
+ git(["rev-parse", "--is-inside-work-tree"], { cwd }).trim() === "true"
68
+ );
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Build the `git diff` argv for the requested scope. Pure.
76
+ *
77
+ * @param {object} opts { staged, base, range, paths }
78
+ * @param {boolean} [stat] append --stat for the summary variant
79
+ * @returns {{ args:string[], scope:string, label:string }}
80
+ */
81
+ export function resolveDiffArgs(opts = {}, stat = false) {
82
+ const { staged, base, range, paths } = opts;
83
+ let args;
84
+ let scope;
85
+ let label;
86
+ if (range) {
87
+ args = ["diff", range];
88
+ scope = "range";
89
+ label = `range ${range}`;
90
+ } else if (base) {
91
+ // three-dot: changes on HEAD since it diverged from <base> (PR-style).
92
+ args = ["diff", `${base}...HEAD`];
93
+ scope = "base";
94
+ label = `${base}...HEAD`;
95
+ } else if (staged) {
96
+ args = ["diff", "--cached"];
97
+ scope = "staged";
98
+ label = "staged changes";
99
+ } else {
100
+ args = ["diff", "HEAD"];
101
+ scope = "working";
102
+ label = "working tree vs HEAD";
103
+ }
104
+ if (stat) args.push("--stat");
105
+ const cleanPaths = Array.isArray(paths) ? paths.filter(Boolean) : [];
106
+ if (cleanPaths.length) args.push("--", ...cleanPaths);
107
+ return { args, scope, label };
108
+ }
109
+
110
+ /** Normalize/validate an effort tier; defaults to "medium". Pure. */
111
+ export function normalizeEffort(value) {
112
+ if (!value) return "medium";
113
+ const v = String(value).toLowerCase().trim();
114
+ if (!VALID_EFFORTS.includes(v)) {
115
+ throw new Error(
116
+ `Invalid effort "${value}". Expected one of: ${VALID_EFFORTS.join(", ")}.`,
117
+ );
118
+ }
119
+ return v;
120
+ }
121
+
122
+ /** Resolve the review lens from flags. Pure. */
123
+ export function resolveReviewMode({ security, simplify } = {}) {
124
+ if (security && simplify) {
125
+ throw new Error("--security and --simplify are mutually exclusive.");
126
+ }
127
+ if (security) return "security";
128
+ if (simplify) return "simplify";
129
+ return "default";
130
+ }
131
+
132
+ const LENS = Object.freeze({
133
+ default:
134
+ "Review the changes for, in priority order: (1) CORRECTNESS bugs — logic " +
135
+ "errors, unhandled edge cases, null/undefined hazards, off-by-one, race " +
136
+ "conditions, missing/incorrect error handling, resource leaks, wrong API " +
137
+ "usage, broken invariants; and (2) CLEANUP — duplicated logic that could " +
138
+ "reuse an existing helper, code that could be simplified, and obvious " +
139
+ "inefficiencies. Correctness findings come first.",
140
+ security:
141
+ "This is a SECURITY review. Look only for security-relevant defects: " +
142
+ "injection (SQL / shell / path), broken authentication or authorization, " +
143
+ "secrets committed in code, weak or misused cryptography, SSRF, unsafe " +
144
+ "deserialization, path traversal, XSS, insecure randomness, missing input " +
145
+ "validation, TOCTOU races, and insecure defaults. Ignore style and " +
146
+ "non-security cleanups.",
147
+ simplify:
148
+ "This is a CLEANUP-ONLY review — do NOT hunt for bugs. Look for: duplicated " +
149
+ "logic that could reuse existing code, over-complicated code that can be " +
150
+ "simplified, inefficient patterns, and wrong-altitude abstractions. Only " +
151
+ "propose changes that PRESERVE behavior.",
152
+ });
153
+
154
+ const EFFORT_GUIDE = Object.freeze({
155
+ low:
156
+ "Report ONLY the few highest-confidence, highest-impact findings. Skip " +
157
+ "anything speculative or minor.",
158
+ medium:
159
+ "Report high-confidence findings across the whole diff. Skip speculative " +
160
+ "nitpicks.",
161
+ high:
162
+ "Be thorough across the whole diff. You may include lower-confidence " +
163
+ "findings, but clearly mark each as such.",
164
+ });
165
+
166
+ /**
167
+ * Build the review prompt embedding the diff. Pure.
168
+ *
169
+ * @param {object} o { diff, summary, effort, mode, fix, label, untrackedBlocks, truncated }
170
+ * @returns {string}
171
+ */
172
+ export function buildReviewPrompt(o = {}) {
173
+ const {
174
+ diff = "",
175
+ summary = "",
176
+ effort = "medium",
177
+ mode = "default",
178
+ fix = false,
179
+ comment = false,
180
+ label = "working tree vs HEAD",
181
+ untrackedBlocks = "",
182
+ truncated = false,
183
+ } = o;
184
+
185
+ // comment mode → machine-readable findings so each maps to a PR inline comment.
186
+ const commentTail =
187
+ "Output ONLY a JSON array of findings and nothing else — no prose, no " +
188
+ "markdown fence. Each element: " +
189
+ '{"path": "<repo-relative file path exactly as in the diff>", ' +
190
+ '"line": <integer line number in the NEW version of the file, which MUST ' +
191
+ "appear in the diff>, " +
192
+ '"severity": "Critical"|"High"|"Medium"|"Low", ' +
193
+ '"title": "<one-line summary>", ' +
194
+ '"body": "<why it matters + a concrete suggested fix>"}. ' +
195
+ "Only report lines that are present in the diff (added/context lines on the " +
196
+ "new side). Do NOT modify any files. If there is nothing worth raising, " +
197
+ "output exactly [].";
198
+
199
+ const tail = comment
200
+ ? commentTail
201
+ : fix
202
+ ? "First identify the issues as above. Then APPLY the fixes directly with " +
203
+ "your edit tools — make the smallest change that resolves each issue and " +
204
+ "match the surrounding code's style. Every file edit is automatically " +
205
+ "checkpointed and reversible, so edit confidently. Stay within the " +
206
+ "reviewed change and its immediate dependencies — do not refactor " +
207
+ "unrelated code. Do NOT run destructive shell commands. When done, output " +
208
+ "a Markdown summary: what you changed (with `path:line`) and any issue you " +
209
+ "deliberately did NOT fix, each with a one-line reason."
210
+ : "Output a Markdown report. For each finding give: a severity " +
211
+ "(Critical / High / Medium / Low), the `path:line`, a one-line title, " +
212
+ "why it matters, and a concrete suggested fix (a short code snippet when " +
213
+ "it helps). Group findings by severity, most severe first. If nothing is " +
214
+ "worth raising, say so plainly. Do NOT modify any files — this is a " +
215
+ "review only.";
216
+
217
+ return [
218
+ `You are an expert code reviewer. ${LENS[mode]}`,
219
+ EFFORT_GUIDE[effort],
220
+ "",
221
+ `The change under review is the ${label}, shown below as a unified diff. ` +
222
+ "Before judging a hunk, use your read/search tools to open the " +
223
+ "surrounding code so your findings reflect real context, not just the " +
224
+ "diff window.",
225
+ "",
226
+ summary ? "Diffstat:\n```\n" + summary.trim() + "\n```\n" : "",
227
+ "Unified diff:",
228
+ "```diff",
229
+ truncated
230
+ ? diff +
231
+ "\n\n[... diff truncated — review what is shown; note the cutoff]"
232
+ : diff,
233
+ "```",
234
+ untrackedBlocks ? "\n" + untrackedBlocks : "",
235
+ "",
236
+ tail,
237
+ ]
238
+ .filter((s) => s !== "")
239
+ .join("\n");
240
+ }
241
+
242
+ /**
243
+ * Collect untracked files (working scope only) and render them as labeled
244
+ * "new file" blocks so brand-new code is reviewed too (git diff HEAD omits it).
245
+ */
246
+ function collectUntracked(cwd, git) {
247
+ let list = [];
248
+ try {
249
+ list = git(["ls-files", "--others", "--exclude-standard"], { cwd })
250
+ .split("\n")
251
+ .map((s) => s.trim())
252
+ .filter(Boolean);
253
+ } catch {
254
+ return { blocks: "", files: [], skipped: [] };
255
+ }
256
+ if (!list.length) return { blocks: "", files: [], skipped: [] };
257
+
258
+ const parts = [];
259
+ const included = [];
260
+ const skipped = [];
261
+ let total = 0;
262
+ for (const rel of list) {
263
+ if (total >= MAX_UNTRACKED_TOTAL_CHARS) {
264
+ skipped.push(rel);
265
+ continue;
266
+ }
267
+ let content;
268
+ try {
269
+ content = fs.readFileSync(path.resolve(cwd, rel), "utf-8");
270
+ } catch {
271
+ skipped.push(rel);
272
+ continue;
273
+ }
274
+ // Skip files that look binary (NUL byte in the first chunk).
275
+ if (content.includes("\u0000")) {
276
+ skipped.push(rel);
277
+ continue;
278
+ }
279
+ let body = content;
280
+ if (body.length > MAX_UNTRACKED_FILE_CHARS) {
281
+ body = body.slice(0, MAX_UNTRACKED_FILE_CHARS) + "\n[... file truncated]";
282
+ }
283
+ total += body.length;
284
+ included.push(rel);
285
+ parts.push(`New file \`${rel}\`:\n\`\`\`\n${body}\n\`\`\``);
286
+ }
287
+ return {
288
+ blocks: parts.length ? "Untracked new files:\n\n" + parts.join("\n\n") : "",
289
+ files: included,
290
+ skipped,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Collect the diff (+ untracked new files) for a review scope. Shared by
296
+ * runReview and runReviewComment. Returns the (possibly truncated) diff.
297
+ */
298
+ function collectReviewDiff(scopeOpts, { cwd, git, includeUntracked }) {
299
+ const { args: diffArgs, scope, label } = resolveDiffArgs(scopeOpts, false);
300
+ const { args: statArgs } = resolveDiffArgs(scopeOpts, true);
301
+ let diff = "";
302
+ let summary = "";
303
+ try {
304
+ diff = git(diffArgs, { cwd });
305
+ summary = git(statArgs, { cwd });
306
+ } catch (err) {
307
+ throw new Error(`git diff failed: ${err.message}`);
308
+ }
309
+ // Untracked new files only matter for the default working scope; staged /
310
+ // base / range are already fully described by git.
311
+ let untracked = { blocks: "", files: [], skipped: [] };
312
+ if (scope === "working" && includeUntracked) {
313
+ untracked = collectUntracked(cwd, git);
314
+ }
315
+ const hasDiff = Boolean(diff.trim());
316
+ const hasUntracked = Boolean(untracked.blocks);
317
+ const truncated = diff.length > MAX_DIFF_CHARS;
318
+ if (truncated) diff = diff.slice(0, MAX_DIFF_CHARS);
319
+ return {
320
+ diff,
321
+ summary,
322
+ scope,
323
+ label,
324
+ untracked,
325
+ hasDiff,
326
+ hasUntracked,
327
+ truncated,
328
+ };
329
+ }
330
+
331
+ /** Run `gh` with an argv array (no shell). UTF-8 in/out; throws on failure. */
332
+ function ghCli(args, { cwd, input } = {}) {
333
+ const res = spawnSync("gh", args, {
334
+ cwd,
335
+ input,
336
+ encoding: "utf-8",
337
+ windowsHide: true,
338
+ maxBuffer: 64 * 1024 * 1024,
339
+ });
340
+ if (res.error) throw res.error;
341
+ if (res.status !== 0) {
342
+ const msg = (res.stderr || res.stdout || "").toString().trim();
343
+ throw new Error(msg || `gh ${args.join(" ")} failed (exit ${res.status})`);
344
+ }
345
+ return (res.stdout || "").toString();
346
+ }
347
+
348
+ /** Resolve the PR for the current branch via gh (read-only). Throws if none. */
349
+ function resolvePr(cwd, gh) {
350
+ let out;
351
+ try {
352
+ out = gh(
353
+ ["pr", "view", "--json", "number,baseRefName,headRefName,headRefOid,url"],
354
+ { cwd },
355
+ );
356
+ } catch (err) {
357
+ throw new Error(
358
+ `no open PR for the current branch (gh: ${err.message}). ` +
359
+ "Push the branch and open a PR first, or use `cc review` without --comment.",
360
+ );
361
+ }
362
+ let pr;
363
+ try {
364
+ pr = JSON.parse(out);
365
+ } catch {
366
+ throw new Error("could not parse `gh pr view` output.");
367
+ }
368
+ let repo = null;
369
+ try {
370
+ repo = JSON.parse(gh(["repo", "view", "--json", "nameWithOwner"], { cwd }))
371
+ .nameWithOwner;
372
+ } catch {
373
+ repo = null;
374
+ }
375
+ return { ...pr, repo };
376
+ }
377
+
378
+ /**
379
+ * Parse the agent's findings JSON (lenient). Strips a code fence, extracts the
380
+ * first JSON array, and keeps only well-formed findings. Pure.
381
+ *
382
+ * @returns {{path:string,line:number,severity:string,title:string,body:string}[]}
383
+ */
384
+ export function parseFindings(text) {
385
+ if (!text) return [];
386
+ let s = String(text).trim();
387
+ const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
388
+ if (fence) s = fence[1].trim();
389
+ const start = s.indexOf("[");
390
+ const end = s.lastIndexOf("]");
391
+ if (start === -1 || end === -1 || end < start) return [];
392
+ let arr;
393
+ try {
394
+ arr = JSON.parse(s.slice(start, end + 1));
395
+ } catch {
396
+ return [];
397
+ }
398
+ if (!Array.isArray(arr)) return [];
399
+ return arr
400
+ .filter(
401
+ (f) =>
402
+ f &&
403
+ typeof f === "object" &&
404
+ f.path &&
405
+ Number.isFinite(Number(f.line)) &&
406
+ Number(f.line) > 0,
407
+ )
408
+ .map((f) => ({
409
+ path: String(f.path),
410
+ line: Math.floor(Number(f.line)),
411
+ severity: f.severity ? String(f.severity) : "Note",
412
+ title: f.title ? String(f.title) : "",
413
+ body: f.body ? String(f.body) : f.title ? String(f.title) : "finding",
414
+ }));
415
+ }
416
+
417
+ /** Format one finding into a PR comment body. Pure. */
418
+ export function buildCommentBody(f) {
419
+ const sev = f.severity ? `**[${f.severity}]** ` : "";
420
+ const title = f.title ? `${f.title}\n\n` : "";
421
+ return `${sev}${title}${f.body}`.trim();
422
+ }
423
+
424
+ /** Build the GitHub "create review" API payload from findings. Pure. */
425
+ export function buildReviewPayload(findings, { commitId } = {}) {
426
+ const comments = (findings || []).map((f) => ({
427
+ path: f.path,
428
+ line: f.line,
429
+ side: "RIGHT",
430
+ body: buildCommentBody(f),
431
+ }));
432
+ const payload = {
433
+ event: "COMMENT",
434
+ body: `cc review — ${comments.length} finding(s).`,
435
+ comments,
436
+ };
437
+ if (commitId) payload.commit_id = commitId;
438
+ return payload;
439
+ }
440
+
441
+ /**
442
+ * Post the findings to the PR as a single review with inline comments
443
+ * (outward-facing — callers gate this behind --dry-run / confirmation).
444
+ */
445
+ export function postReviewComments(pr, findings, { gh = ghCli, cwd, commitId } = {}) {
446
+ if (!pr || !pr.repo || !pr.number) {
447
+ throw new Error("cannot post: PR repo/number not resolved.");
448
+ }
449
+ const payload = buildReviewPayload(findings, { commitId });
450
+ const out = gh(
451
+ [
452
+ "api",
453
+ "--method",
454
+ "POST",
455
+ `repos/${pr.repo}/pulls/${pr.number}/reviews`,
456
+ "--input",
457
+ "-",
458
+ ],
459
+ { cwd, input: JSON.stringify(payload) },
460
+ );
461
+ try {
462
+ return JSON.parse(out);
463
+ } catch {
464
+ return { raw: out };
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Comment-mode review: resolve the PR, collect its diff, get machine-readable
470
+ * findings from one read-only agent turn. Side-effect-free (PR resolution is a
471
+ * read); the caller posts after confirmation. Deps injected for tests.
472
+ *
473
+ * @returns {Promise<{empty:boolean, pr:object, findings:object[], scope?:string,
474
+ * label?:string, isError?:boolean}>}
475
+ */
476
+ export async function runReviewComment(options = {}, deps = {}) {
477
+ const cwd = options.cwd || process.cwd();
478
+ const git = deps.git || gitCli;
479
+ const gh = deps.gh || ghCli;
480
+ const repoCheck = deps.isGitRepo || isGitRepo;
481
+ if (!repoCheck(cwd, git)) {
482
+ throw new Error("cc review needs a git work tree.");
483
+ }
484
+
485
+ const pr = (deps.resolvePr || resolvePr)(cwd, gh);
486
+
487
+ // Default the scope to the PR's base branch (review the PR's changes) unless
488
+ // the user gave an explicit scope.
489
+ const explicitScope =
490
+ options.staged || options.base || options.range || (options.paths || []).length;
491
+ const scopeOpts = {
492
+ staged: options.staged === true,
493
+ base: options.base || (explicitScope ? null : pr.baseRefName),
494
+ range: options.range || null,
495
+ paths: options.paths || [],
496
+ };
497
+
498
+ const collected = collectReviewDiff(scopeOpts, {
499
+ cwd,
500
+ git,
501
+ includeUntracked: false,
502
+ });
503
+ if (!collected.hasDiff && !collected.hasUntracked) {
504
+ return { empty: true, pr, findings: [] };
505
+ }
506
+
507
+ // LLM config defaults (parity with runReview).
508
+ try {
509
+ const loadConfig =
510
+ deps.loadConfig || (await import("../lib/config-manager.js")).loadConfig;
511
+ const { applyConfigLlmDefaults } = deps.applyConfigLlmDefaults
512
+ ? { applyConfigLlmDefaults: deps.applyConfigLlmDefaults }
513
+ : await import("../lib/llm-config-defaults.js");
514
+ applyConfigLlmDefaults(options, loadConfig().llm || {}, {
515
+ explicitModel: options.model,
516
+ });
517
+ } catch {
518
+ /* fall back to runner defaults */
519
+ }
520
+
521
+ const prompt = buildReviewPrompt({
522
+ diff: collected.hasDiff ? collected.diff : "(no tracked changes)",
523
+ summary: collected.summary,
524
+ effort: normalizeEffort(options.effort),
525
+ mode: resolveReviewMode(options),
526
+ comment: true,
527
+ label: collected.label,
528
+ truncated: collected.truncated,
529
+ });
530
+
531
+ const run =
532
+ deps.runAgentHeadless ||
533
+ (await import("../runtime/headless-runner.js")).runAgentHeadless;
534
+ // Suppress the runner's own stdout/stderr — we only want the structured result.
535
+ const outcome = await run(
536
+ {
537
+ prompt,
538
+ model: options.model,
539
+ provider: options.provider,
540
+ baseUrl: options.baseUrl,
541
+ apiKey: options.apiKey,
542
+ outputFormat: "text",
543
+ permissionMode: "plan",
544
+ maxTurns: Number.isFinite(options.maxTurns) ? options.maxTurns : 20,
545
+ cwd,
546
+ expandFileRefs: false,
547
+ },
548
+ { writeOut: () => {}, writeErr: () => {} },
549
+ );
550
+
551
+ return {
552
+ empty: false,
553
+ pr,
554
+ scope: collected.scope,
555
+ label: collected.label,
556
+ findings: parseFindings(outcome.result || ""),
557
+ isError: outcome.isError === true,
558
+ };
559
+ }
560
+
561
+ /**
562
+ * Core review run — collects the diff and dispatches one headless agent turn.
563
+ * Deps are injected for tests (git / runAgentHeadless / config helpers).
564
+ *
565
+ * @returns {Promise<{exitCode:number, isError:boolean, scope:string, empty?:boolean}>}
566
+ */
567
+ export async function runReview(options = {}, deps = {}) {
568
+ const cwd = options.cwd || process.cwd();
569
+ const git = deps.git || gitCli;
570
+ const repoCheck = deps.isGitRepo || isGitRepo;
571
+
572
+ if (!repoCheck(cwd, git)) {
573
+ throw new Error(
574
+ "cc review needs a git work tree (the diff is the review input).",
575
+ );
576
+ }
577
+
578
+ const effort = normalizeEffort(options.effort);
579
+ const mode = resolveReviewMode(options);
580
+ const fix = options.fix === true;
581
+
582
+ const scopeOpts = {
583
+ staged: options.staged === true,
584
+ base: options.base || null,
585
+ range: options.range || null,
586
+ paths: options.paths || [],
587
+ };
588
+
589
+ const { diff, summary, scope, label, untracked, hasDiff, hasUntracked, truncated } =
590
+ collectReviewDiff(scopeOpts, {
591
+ cwd,
592
+ git,
593
+ includeUntracked: options.untracked !== false,
594
+ });
595
+ if (!hasDiff && !hasUntracked) {
596
+ return { exitCode: 0, isError: false, scope, empty: true };
597
+ }
598
+
599
+ const prompt = buildReviewPrompt({
600
+ diff: hasDiff ? diff : "(no tracked changes)",
601
+ summary,
602
+ effort,
603
+ mode,
604
+ fix,
605
+ label,
606
+ untrackedBlocks: untracked.blocks,
607
+ truncated,
608
+ });
609
+
610
+ // LLM defaults: honor .chainlesschain/config.json `llm` like cc agent/ask.
611
+ // Explicit flags win. Best-effort — never block the review.
612
+ try {
613
+ const loadConfig =
614
+ deps.loadConfig || (await import("../lib/config-manager.js")).loadConfig;
615
+ const { applyConfigLlmDefaults } = deps.applyConfigLlmDefaults
616
+ ? { applyConfigLlmDefaults: deps.applyConfigLlmDefaults }
617
+ : await import("../lib/llm-config-defaults.js");
618
+ applyConfigLlmDefaults(options, loadConfig().llm || {}, {
619
+ explicitModel: options.model,
620
+ });
621
+ } catch {
622
+ // fall back to runner defaults
623
+ }
624
+
625
+ // review-only → plan mode (clamped to read-only tools, cannot mutate).
626
+ // --fix → acceptEdits + auto-checkpoint (reversible edits).
627
+ const permissionMode = fix ? "acceptEdits" : "plan";
628
+ const autoCheckpoint = fix && options.checkpoint !== false;
629
+ const defaultMaxTurns = fix ? 40 : 20;
630
+ const maxTurns = Number.isFinite(options.maxTurns)
631
+ ? options.maxTurns
632
+ : defaultMaxTurns;
633
+
634
+ const run =
635
+ deps.runAgentHeadless ||
636
+ (await import("../runtime/headless-runner.js")).runAgentHeadless;
637
+
638
+ const outcome = await run({
639
+ prompt,
640
+ model: options.model,
641
+ provider: options.provider,
642
+ baseUrl: options.baseUrl,
643
+ apiKey: options.apiKey,
644
+ outputFormat: options.outputFormat || "text",
645
+ permissionMode,
646
+ autoCheckpoint,
647
+ checkpointSession: options.checkpointSession || `review-${scope}`,
648
+ maxTurns,
649
+ cwd,
650
+ // The diff carries `@@` hunk markers and may contain `@tokens`; never run
651
+ // the @file-reference expander over it.
652
+ expandFileRefs: false,
653
+ });
654
+
655
+ return { ...outcome, scope, empty: false };
656
+ }
657
+
658
+ export function registerReviewCommand(program) {
659
+ program
660
+ .command("review [effort]")
661
+ .description(
662
+ "Diff-first code review of your changes (Claude-Code /code-review parity)",
663
+ )
664
+ .option("--staged", "Review staged changes only (git diff --cached)")
665
+ .option("--base <ref>", "Review this branch vs a base ref (<ref>...HEAD)")
666
+ .option("--range <range>", "Review an explicit revision range (e.g. A..B)")
667
+ .option(
668
+ "--paths <paths...>",
669
+ "Limit the review to these paths (repeatable)",
670
+ )
671
+ .option("-e, --effort <level>", "low | medium | high (default: medium)")
672
+ .option("--fix", "Apply the fixes to the working tree (auto-checkpointed)")
673
+ .option("--security", "Security-focused review (/security-review parity)")
674
+ .option("--simplify", "Cleanup-only review, no bug hunt (/simplify parity)")
675
+ .option("--no-untracked", "Skip untracked new files (working scope)")
676
+ .option(
677
+ "--no-checkpoint",
678
+ "With --fix: do not auto-checkpoint before edits",
679
+ )
680
+ .option("--model <model>", "Override the review model")
681
+ .option("--provider <provider>", "Override the LLM provider")
682
+ .option("--base-url <url>", "Override the API base URL")
683
+ .option("--api-key <key>", "Override the API key")
684
+ .option("--max-turns <n>", "Cap agent loop iterations")
685
+ .option(
686
+ "--comment",
687
+ "Post findings as inline comments on the current branch's PR (via gh); defaults the scope to the PR base",
688
+ )
689
+ .option(
690
+ "--dry-run",
691
+ "With --comment: show what would be posted without posting",
692
+ )
693
+ .option("--json", "Emit the agent result envelope as JSON")
694
+ .action(async (effortArg, options) => {
695
+ try {
696
+ const merged = {
697
+ ...options,
698
+ effort: options.effort || effortArg,
699
+ // commander stores --no-untracked as untracked:false, --no-checkpoint
700
+ // as checkpoint:false; pass them through unchanged.
701
+ maxTurns: options.maxTurns
702
+ ? parseInt(options.maxTurns, 10)
703
+ : undefined,
704
+ outputFormat: options.json ? "json" : "text",
705
+ cwd: process.cwd(),
706
+ };
707
+
708
+ // Pre-flight messaging on stderr so stdout stays a clean payload.
709
+ const mode = resolveReviewMode(merged);
710
+ const effort = normalizeEffort(merged.effort);
711
+ const { label } = resolveDiffArgs({
712
+ staged: merged.staged,
713
+ base: merged.base,
714
+ range: merged.range,
715
+ paths: merged.paths,
716
+ });
717
+ const modeLabel =
718
+ mode === "security"
719
+ ? "security"
720
+ : mode === "simplify"
721
+ ? "cleanup-only"
722
+ : "bugs + cleanup";
723
+
724
+ // ── --comment: review the PR's diff and post inline comments ────────
725
+ if (merged.comment) {
726
+ logger.info(
727
+ chalk.gray(
728
+ `Reviewing PR diff · ${effort} effort · ${modeLabel} · ` +
729
+ (merged.dryRun ? "dry-run" : "will post comments"),
730
+ ),
731
+ );
732
+ const res = await runReviewComment(merged, {});
733
+ if (res.empty) {
734
+ logger.log(chalk.gray("No changes to review on this PR."));
735
+ return;
736
+ }
737
+ logger.log(
738
+ chalk.bold(
739
+ `${res.findings.length} finding(s) for PR #${res.pr.number}`,
740
+ ) + chalk.gray(` ${res.pr.url || ""}`),
741
+ );
742
+ for (const f of res.findings) {
743
+ logger.log(
744
+ ` ${chalk.yellow(`[${f.severity}]`)} ${chalk.cyan(`${f.path}:${f.line}`)} ${f.title}`,
745
+ );
746
+ }
747
+ if (res.findings.length === 0) {
748
+ logger.log(chalk.gray("Nothing to post."));
749
+ return;
750
+ }
751
+ if (merged.dryRun) {
752
+ logger.log(
753
+ chalk.gray("(--dry-run: not posting; re-run without --dry-run)"),
754
+ );
755
+ return;
756
+ }
757
+ // Outward-facing: confirm before posting when interactive.
758
+ if (process.stdin.isTTY) {
759
+ const { confirm } = await import("@inquirer/prompts");
760
+ const ok = await confirm({
761
+ message: `Post ${res.findings.length} comment(s) to PR #${res.pr.number}?`,
762
+ default: false,
763
+ }).catch(() => false);
764
+ if (!ok) {
765
+ logger.log(chalk.gray("Aborted — nothing posted."));
766
+ return;
767
+ }
768
+ }
769
+ try {
770
+ postReviewComments(res.pr, res.findings, {
771
+ cwd: merged.cwd,
772
+ commitId: res.pr.headRefOid,
773
+ });
774
+ logger.log(
775
+ chalk.green(
776
+ `✓ posted ${res.findings.length} comment(s) to PR #${res.pr.number}`,
777
+ ),
778
+ );
779
+ } catch (err) {
780
+ logger.error(chalk.red(`failed to post review: ${err.message}`));
781
+ process.exitCode = 1;
782
+ }
783
+ return;
784
+ }
785
+
786
+ logger.info(
787
+ chalk.gray(
788
+ `Reviewing ${label} · ${effort} effort · ${modeLabel}` +
789
+ (merged.fix ? " · applying fixes" : " · read-only"),
790
+ ),
791
+ );
792
+
793
+ const result = await runReview(merged, {});
794
+
795
+ if (result.empty) {
796
+ logger.log(chalk.gray("No changes to review."));
797
+ return;
798
+ }
799
+ if (result.isError) {
800
+ process.exitCode = result.exitCode || 1;
801
+ }
802
+ } catch (err) {
803
+ logger.error(chalk.red(`review failed: ${err.message}`));
804
+ process.exitCode = 1;
805
+ }
806
+ });
807
+ }