chainlesschain 0.162.61 → 0.162.65

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 (162) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-BiB8WfIz.js → AIOps-DjJf_QIn.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-NLBhC6jG.js → ActionButton-BT45g-KL.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-k62j-xiL.js → Analytics-CRaTHble.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-spr0Sm6J.js → AppLayout-72r5TM1u.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-C3NHJos3.js → Audit-BNlvJ3Yc.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-C2V9tGqF.js → Backup-Kuj0-vBg.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-DeKm11mH.js → BaseInput-_pKOPRf4.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-CHZ2CU7x.js → Chat-CMNhGWK5.js} +5 -5
  10. package/src/assets/web-panel/assets/ChatBubbleRenderer-DxJmwLv8.js +1 -0
  11. package/src/assets/web-panel/assets/{Checkbox-q6E9VeLr.js → Checkbox-B5R2TdAI.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen--4w4QpUI.js → Codegen-69RAQ0Gi.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-DLOkwTsj.js → Col-DlbssQEY.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-B1LxJGfE.js → Community-DU3SAZIS.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-C_769oQZ.js → Compact-BqdNnAZv.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-zsI0s7vB.js → Compliance-D9a9-ihS.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-A1WA6whF.js → Cowork-DWBtOBbU.js} +3 -3
  18. package/src/assets/web-panel/assets/{Cron-C3JDTyyd.js → Cron-ClSuf90k.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-D8O6uB86.js → Crosschain-BFjRKvpa.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-BpOebm5d.js → DID-BwBGRlMm.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-Defso6kA.js → Dashboard-CHrXGmQ3.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-Cv9BrwT_.js → Dropdown-C24B5sk2.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-BWKHbh4C.js → EmailListRenderer-DaXTSK5p.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard--m_Ru7Ci.js → FamilyGuardDashboard-65d89G5t.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-Bs6ZcAP0.js → Federation-CkWdqmVs.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-CcAs3Acx.js → FormItemContext-BV4W2nrT.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-DI1oL4pK.js → GenericCardRenderer-D60KJ0_b.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-C27t3-fW.js → Git-BIKuoGvW.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-Dr_syXc_.js → Governance-CKnJpq5X.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-CWM8dIbA.js → Inference-C7G3YGeg.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph--cFDUZv3.js → KnowledgeGraph-D7fCUd4B.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-Cnn2_Onf.js → Logs-C0unjcbC.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-4T9ok3Gz.js → Marketplace-BzLlnyI8.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-BQvZwqcN.js → McpTools-DSKFRB1-.js} +4 -4
  35. package/src/assets/web-panel/assets/{Memory-BE9rPkM_.js → Memory-C_QrLAnt.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-DJ3j5lXC.js → MobileBridge-DBeaFERD.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileProjects-qasLvYdb.js → MobileProjects-C2L_RttC.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-D3CSPTD9.js → Mtc-B3Tdh6-l.js} +4 -4
  39. package/src/assets/web-panel/assets/{MtcAudit-DaYVGCN5.js → MtcAudit-B3O_EUvt.js} +4 -4
  40. package/src/assets/web-panel/assets/{Multisig-Dz1c5r5w.js → Multisig--60rVmDj.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BIKV_K-a.js → NLProgramming-D60vxATf.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-f6t-rmOa.js → Notes-D2gj2uFI.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-DHLQh8Fy.js → NotificationSettings-D0DWHNlF.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-CDMZ3o6i.js → OrderTableRenderer-CNi1B7fH.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-DIsL758p.js → Organization-BRcdFgAd.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-BMM7apnZ.js → Overflow-C3_Oap7v.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-CTGMmTvi.js → P2P-DEbZ93QW.js} +2 -2
  48. package/src/assets/web-panel/assets/PdhVaultBrowser-DN_pmo2N.js +7 -0
  49. package/src/assets/web-panel/assets/{Permissions-Bed5JxMx.js → Permissions-CPj3C9o2.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-CIiZhSM5.js → PersonalDataHub-BZGupZzh.js} +4 -4
  51. package/src/assets/web-panel/assets/{Pipeline-CtirPodz.js → Pipeline-SMLW1BG7.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-DuXhXhE7.js → Privacy-o24SJ2no.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-DZrnguBl.js → ProjectInit-DxjAXD8f.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-HIltqsJ1.js → ProjectSettings-DipynlqL.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-BWFkePg4.js → Projects-CZ9egQ8r.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-DEP0Jdvl.js → Providers-x3p-wcab.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-T2THoHNx.js → QuickAsk-CZ7beKFC.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-CvbxaSwm.js → Recommend-VJCd2i9_.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-6Afy6tfp.js → Reputation-pl12NmBF.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-DY8OPWaO.js → Row-NkSeo4Tb.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-wDGWb9pZ.js → RssFeed-B17vp67R.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-D_zHAwZY.js → Search-Dij0_m6W.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-Czq7AlGG.js → Security-n9CSBX-9.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-Ac1g0ZcG.js → Services-bZOzqHdK.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-DXQ3eeSW.js → Skeleton-B23D5vJ-.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-CWRioX4u.js → Skills-IXh-0mk0.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-BlHthzfs.js → Sla-QEofxmdK.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-Ct240JmL.js → SpeechSettings-u68R59ft.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-BgDIt8Q-.js → SyncSettings-B3tc986U.js} +2 -2
  70. package/src/assets/web-panel/assets/Tasks-Cp2QxGrr.js +1 -0
  71. package/src/assets/web-panel/assets/{Templates-Dp9QhyIw.js → Templates-CMWiWxiH.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-CHTYMxzY.js → Tenant-M8aPJ3C7.js} +1 -1
  73. package/src/assets/web-panel/assets/Terminal-CK3zKjIE.js +3 -0
  74. package/src/assets/web-panel/assets/{TimelineRenderer-Bh8jA18j.js → TimelineRenderer-DF6aIS-d.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-DWkTd5dv.js → Tokens-BcfMMw_e.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-CRgVg6sd.js → Trigger-jIbNmxvm.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-BgWOXd0W.js → Trust-ChLil6CZ.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-BlTUB9Y-.js → UkeySign-B1WzYAon.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-DI64XgNb.js → VideoEditing-Dwm0LyCc.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-CJ3TNGiG.js → Wallet-DVyxsX-O.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-B2-rWWoV.js → WebAuthn-WYPNy2Q7.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-VI9otbaH.js → WorkflowEditor-Br3dCsmv.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-CgYfiaVh.js → chat-fAKHY2HK.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-B9EhRTky.js → colors-BXqS-Bwi.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-Cb7bjraa.js → compact-item-BgCQhtW3.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-DlXPeXuj.js → createContext-weZBwqHy.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BpXdFCJ4.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-BbgRfrdf.js → hasIn-Dx68UNFL.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-2ts5iOIB.js → index-5e-OAZOb.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-Caiu2avX.js → index-B0rgvjX8.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-OVrh8wTN.js → index-B8-2rQdr.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-DPaffcT8.js → index-B8zNZ_oH.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-Ira0HLPr.js → index-B9NeNwHP.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CMyzmvtJ.js → index-BC-4la9j.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DDzNdZcX.js → index-BSQEQCft.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-De59Xat-.js → index-BawcE_zG.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dx4sm6dm.js → index-Bazltj8w.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-5CrFMQjt.js → index-BehvfmYd.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-ojRAd7Nq.js → index-BjsidvP5.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DbLJShJB.js → index-Bo4UTTla.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-1iUK_kAw.js → index-BqcH_mKR.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BrPKR2RZ.js +1 -0
  103. package/src/assets/web-panel/assets/{index-C95qWAh4.js → index-Bwv_UrNF.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DXxa7PR8.js → index-C0ZjD3Ac.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DUU9DY4J.js → index-CEjLe8FJ.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Bk7r1a9x.js → index-CJXZDwkf.js} +1 -1
  107. package/src/assets/web-panel/assets/index-CMEfvACO.js +1 -0
  108. package/src/assets/web-panel/assets/{index-Cf9zwbk-.js → index-CX9cxRnU.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bt-lPYpq.js → index-CfqzwaAV.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BfSS-U5o.js → index-ChdeuOni.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D6t-Shqr.js → index-ClfP1Yax.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BU8hEUyq.js → index-Co5cQnlv.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DkQIyK-V.js → index-CxfVwfub.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-fG-1gXy0.js → index-Cxsfc5Ou.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C8DB27uJ.js → index-DCYJDUab.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C73WgOc2.js → index-DG2KCc8h.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-D8kB0k3E.js → index-DNF3aCJF.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BoEFFKn3.js → index-DXuz90bX.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Dh6qWb1v.js → index-Dbv5btEU.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-p03wNqiP.js → index-DgUC575c.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BD2W-qsS.js → index-G_wPnPoA.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CGx8aO_Y.js → index-HIN85jl7.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-c7-Jd6WB.js → index-LxcdLeFj.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D-Zz9PvD.js → index-loaP_41H.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-AR-QpAkP.js → index-ohVNy7ua.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cmr31VCO.js → index-qUBHSW_3.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-CHR47Q5B.js → index-u2U9t07r.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-JT674ACa.js → initDefaultProps-M7xH4eUK.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-CokflrA9.js → motion-BrJP4mFE.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-CfMhRpyC.js → move-BufxEuU9.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-ClYc5II5.js → omit-s6dtQtFP.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-CTwEb_8h.js → pickAttrs-BcYdIZqz.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-Cb3StU_t.js → placementArrow-Ds-3Hw3n.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-uIxkx5M1.js → responsiveObserve-BRtrRTxl.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-B9HZBQ7i.js → slide-RJzqMQM4.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-C72bwYl0.js → statusUtils-BuBhJXvr.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-BFTtaQb8.js → styleChecker-so8acGHq.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-DKl5j41_.js → useFlexGapSupport-BpkV467K.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-BUHS6bo3.js → useFs-lES1RctZ.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-D8KrYSq4.js → usePersonalDataHub-DdCNi4bA.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-JYP-aZDj.js → vnode-nAeEg_3h.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-BFusdxdH.js → zoom-BWjRAfRy.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/review.js +463 -0
  146. package/src/index.js +2 -0
  147. package/src/lib/cost-budget.js +109 -0
  148. package/src/lib/ide-context.js +50 -0
  149. package/src/lib/personal-data-hub-wiring.js +16 -0
  150. package/src/repl/agent-repl.js +36 -0
  151. package/src/repl/tasks-status.js +82 -0
  152. package/src/runtime/agent-core.js +38 -3
  153. package/src/runtime/headless-runner.js +51 -3
  154. package/src/runtime/headless-stream.js +62 -4
  155. package/src/runtime/mcp-config.js +6 -0
  156. package/src/assets/web-panel/assets/ChatBubbleRenderer-DEXSa7tC.js +0 -1
  157. package/src/assets/web-panel/assets/PdhVaultBrowser-DWwmm0k1.js +0 -7
  158. package/src/assets/web-panel/assets/Tasks-3PTmatJP.js +0 -1
  159. package/src/assets/web-panel/assets/Terminal-X-NGwLpv.js +0 -3
  160. package/src/assets/web-panel/assets/devWarning-D-Hp8s_8.js +0 -1
  161. package/src/assets/web-panel/assets/index-BvnHuxVM.js +0 -1
  162. package/src/assets/web-panel/assets/index-Dkm5IGwX.js +0 -1
@@ -0,0 +1,463 @@
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 git(["rev-parse", "--is-inside-work-tree"], { cwd }).trim() === "true";
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Build the `git diff` argv for the requested scope. Pure.
74
+ *
75
+ * @param {object} opts { staged, base, range, paths }
76
+ * @param {boolean} [stat] append --stat for the summary variant
77
+ * @returns {{ args:string[], scope:string, label:string }}
78
+ */
79
+ export function resolveDiffArgs(opts = {}, stat = false) {
80
+ const { staged, base, range, paths } = opts;
81
+ let args;
82
+ let scope;
83
+ let label;
84
+ if (range) {
85
+ args = ["diff", range];
86
+ scope = "range";
87
+ label = `range ${range}`;
88
+ } else if (base) {
89
+ // three-dot: changes on HEAD since it diverged from <base> (PR-style).
90
+ args = ["diff", `${base}...HEAD`];
91
+ scope = "base";
92
+ label = `${base}...HEAD`;
93
+ } else if (staged) {
94
+ args = ["diff", "--cached"];
95
+ scope = "staged";
96
+ label = "staged changes";
97
+ } else {
98
+ args = ["diff", "HEAD"];
99
+ scope = "working";
100
+ label = "working tree vs HEAD";
101
+ }
102
+ if (stat) args.push("--stat");
103
+ const cleanPaths = Array.isArray(paths) ? paths.filter(Boolean) : [];
104
+ if (cleanPaths.length) args.push("--", ...cleanPaths);
105
+ return { args, scope, label };
106
+ }
107
+
108
+ /** Normalize/validate an effort tier; defaults to "medium". Pure. */
109
+ export function normalizeEffort(value) {
110
+ if (!value) return "medium";
111
+ const v = String(value).toLowerCase().trim();
112
+ if (!VALID_EFFORTS.includes(v)) {
113
+ throw new Error(
114
+ `Invalid effort "${value}". Expected one of: ${VALID_EFFORTS.join(", ")}.`,
115
+ );
116
+ }
117
+ return v;
118
+ }
119
+
120
+ /** Resolve the review lens from flags. Pure. */
121
+ export function resolveReviewMode({ security, simplify } = {}) {
122
+ if (security && simplify) {
123
+ throw new Error("--security and --simplify are mutually exclusive.");
124
+ }
125
+ if (security) return "security";
126
+ if (simplify) return "simplify";
127
+ return "default";
128
+ }
129
+
130
+ const LENS = Object.freeze({
131
+ default:
132
+ "Review the changes for, in priority order: (1) CORRECTNESS bugs — logic " +
133
+ "errors, unhandled edge cases, null/undefined hazards, off-by-one, race " +
134
+ "conditions, missing/incorrect error handling, resource leaks, wrong API " +
135
+ "usage, broken invariants; and (2) CLEANUP — duplicated logic that could " +
136
+ "reuse an existing helper, code that could be simplified, and obvious " +
137
+ "inefficiencies. Correctness findings come first.",
138
+ security:
139
+ "This is a SECURITY review. Look only for security-relevant defects: " +
140
+ "injection (SQL / shell / path), broken authentication or authorization, " +
141
+ "secrets committed in code, weak or misused cryptography, SSRF, unsafe " +
142
+ "deserialization, path traversal, XSS, insecure randomness, missing input " +
143
+ "validation, TOCTOU races, and insecure defaults. Ignore style and " +
144
+ "non-security cleanups.",
145
+ simplify:
146
+ "This is a CLEANUP-ONLY review — do NOT hunt for bugs. Look for: duplicated " +
147
+ "logic that could reuse existing code, over-complicated code that can be " +
148
+ "simplified, inefficient patterns, and wrong-altitude abstractions. Only " +
149
+ "propose changes that PRESERVE behavior.",
150
+ });
151
+
152
+ const EFFORT_GUIDE = Object.freeze({
153
+ low:
154
+ "Report ONLY the few highest-confidence, highest-impact findings. Skip " +
155
+ "anything speculative or minor.",
156
+ medium:
157
+ "Report high-confidence findings across the whole diff. Skip speculative " +
158
+ "nitpicks.",
159
+ high:
160
+ "Be thorough across the whole diff. You may include lower-confidence " +
161
+ "findings, but clearly mark each as such.",
162
+ });
163
+
164
+ /**
165
+ * Build the review prompt embedding the diff. Pure.
166
+ *
167
+ * @param {object} o { diff, summary, effort, mode, fix, label, untrackedBlocks, truncated }
168
+ * @returns {string}
169
+ */
170
+ export function buildReviewPrompt(o = {}) {
171
+ const {
172
+ diff = "",
173
+ summary = "",
174
+ effort = "medium",
175
+ mode = "default",
176
+ fix = false,
177
+ label = "working tree vs HEAD",
178
+ untrackedBlocks = "",
179
+ truncated = false,
180
+ } = o;
181
+
182
+ const tail = fix
183
+ ? "First identify the issues as above. Then APPLY the fixes directly with " +
184
+ "your edit tools — make the smallest change that resolves each issue and " +
185
+ "match the surrounding code's style. Every file edit is automatically " +
186
+ "checkpointed and reversible, so edit confidently. Stay within the " +
187
+ "reviewed change and its immediate dependencies — do not refactor " +
188
+ "unrelated code. Do NOT run destructive shell commands. When done, output " +
189
+ "a Markdown summary: what you changed (with `path:line`) and any issue you " +
190
+ "deliberately did NOT fix, each with a one-line reason."
191
+ : "Output a Markdown report. For each finding give: a severity " +
192
+ "(Critical / High / Medium / Low), the `path:line`, a one-line title, " +
193
+ "why it matters, and a concrete suggested fix (a short code snippet when " +
194
+ "it helps). Group findings by severity, most severe first. If nothing is " +
195
+ "worth raising, say so plainly. Do NOT modify any files — this is a " +
196
+ "review only.";
197
+
198
+ return [
199
+ `You are an expert code reviewer. ${LENS[mode]}`,
200
+ EFFORT_GUIDE[effort],
201
+ "",
202
+ `The change under review is the ${label}, shown below as a unified diff. ` +
203
+ "Before judging a hunk, use your read/search tools to open the " +
204
+ "surrounding code so your findings reflect real context, not just the " +
205
+ "diff window.",
206
+ "",
207
+ summary ? "Diffstat:\n```\n" + summary.trim() + "\n```\n" : "",
208
+ "Unified diff:",
209
+ "```diff",
210
+ truncated
211
+ ? diff + "\n\n[... diff truncated — review what is shown; note the cutoff]"
212
+ : diff,
213
+ "```",
214
+ untrackedBlocks ? "\n" + untrackedBlocks : "",
215
+ "",
216
+ tail,
217
+ ]
218
+ .filter((s) => s !== "")
219
+ .join("\n");
220
+ }
221
+
222
+ /**
223
+ * Collect untracked files (working scope only) and render them as labeled
224
+ * "new file" blocks so brand-new code is reviewed too (git diff HEAD omits it).
225
+ */
226
+ function collectUntracked(cwd, git) {
227
+ let list = [];
228
+ try {
229
+ list = git(["ls-files", "--others", "--exclude-standard"], { cwd })
230
+ .split("\n")
231
+ .map((s) => s.trim())
232
+ .filter(Boolean);
233
+ } catch {
234
+ return { blocks: "", files: [], skipped: [] };
235
+ }
236
+ if (!list.length) return { blocks: "", files: [], skipped: [] };
237
+
238
+ const parts = [];
239
+ const included = [];
240
+ const skipped = [];
241
+ let total = 0;
242
+ for (const rel of list) {
243
+ if (total >= MAX_UNTRACKED_TOTAL_CHARS) {
244
+ skipped.push(rel);
245
+ continue;
246
+ }
247
+ let content;
248
+ try {
249
+ content = fs.readFileSync(path.resolve(cwd, rel), "utf-8");
250
+ } catch {
251
+ skipped.push(rel);
252
+ continue;
253
+ }
254
+ // Skip files that look binary (NUL byte in the first chunk).
255
+ if (content.includes("\u0000")) {
256
+ skipped.push(rel);
257
+ continue;
258
+ }
259
+ let body = content;
260
+ if (body.length > MAX_UNTRACKED_FILE_CHARS) {
261
+ body = body.slice(0, MAX_UNTRACKED_FILE_CHARS) + "\n[... file truncated]";
262
+ }
263
+ total += body.length;
264
+ included.push(rel);
265
+ parts.push(`New file \`${rel}\`:\n\`\`\`\n${body}\n\`\`\``);
266
+ }
267
+ return {
268
+ blocks: parts.length ? "Untracked new files:\n\n" + parts.join("\n\n") : "",
269
+ files: included,
270
+ skipped,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Core review run — collects the diff and dispatches one headless agent turn.
276
+ * Deps are injected for tests (git / runAgentHeadless / config helpers).
277
+ *
278
+ * @returns {Promise<{exitCode:number, isError:boolean, scope:string, empty?:boolean}>}
279
+ */
280
+ export async function runReview(options = {}, deps = {}) {
281
+ const cwd = options.cwd || process.cwd();
282
+ const git = deps.git || gitCli;
283
+ const repoCheck = deps.isGitRepo || isGitRepo;
284
+
285
+ if (!repoCheck(cwd, git)) {
286
+ throw new Error(
287
+ "cc review needs a git work tree (the diff is the review input).",
288
+ );
289
+ }
290
+
291
+ const effort = normalizeEffort(options.effort);
292
+ const mode = resolveReviewMode(options);
293
+ const fix = options.fix === true;
294
+
295
+ const scopeOpts = {
296
+ staged: options.staged === true,
297
+ base: options.base || null,
298
+ range: options.range || null,
299
+ paths: options.paths || [],
300
+ };
301
+
302
+ const { args: diffArgs, scope, label } = resolveDiffArgs(scopeOpts, false);
303
+ const { args: statArgs } = resolveDiffArgs(scopeOpts, true);
304
+
305
+ let diff = "";
306
+ let summary = "";
307
+ try {
308
+ diff = git(diffArgs, { cwd });
309
+ summary = git(statArgs, { cwd });
310
+ } catch (err) {
311
+ throw new Error(`git diff failed: ${err.message}`);
312
+ }
313
+
314
+ // Untracked new files only matter for the default working scope; staged /
315
+ // base / range are already fully described by git.
316
+ let untracked = { blocks: "", files: [], skipped: [] };
317
+ if (scope === "working" && options.untracked !== false) {
318
+ untracked = collectUntracked(cwd, git);
319
+ }
320
+
321
+ const hasDiff = Boolean(diff.trim());
322
+ const hasUntracked = Boolean(untracked.blocks);
323
+ if (!hasDiff && !hasUntracked) {
324
+ return { exitCode: 0, isError: false, scope, empty: true };
325
+ }
326
+
327
+ const truncated = diff.length > MAX_DIFF_CHARS;
328
+ if (truncated) diff = diff.slice(0, MAX_DIFF_CHARS);
329
+
330
+ const prompt = buildReviewPrompt({
331
+ diff: hasDiff ? diff : "(no tracked changes)",
332
+ summary,
333
+ effort,
334
+ mode,
335
+ fix,
336
+ label,
337
+ untrackedBlocks: untracked.blocks,
338
+ truncated,
339
+ });
340
+
341
+ // LLM defaults: honor .chainlesschain/config.json `llm` like cc agent/ask.
342
+ // Explicit flags win. Best-effort — never block the review.
343
+ try {
344
+ const loadConfig = deps.loadConfig || (await import("../lib/config-manager.js")).loadConfig;
345
+ const { applyConfigLlmDefaults } =
346
+ deps.applyConfigLlmDefaults
347
+ ? { applyConfigLlmDefaults: deps.applyConfigLlmDefaults }
348
+ : await import("../lib/llm-config-defaults.js");
349
+ applyConfigLlmDefaults(options, loadConfig().llm || {}, {
350
+ explicitModel: options.model,
351
+ });
352
+ } catch {
353
+ // fall back to runner defaults
354
+ }
355
+
356
+ // review-only → plan mode (clamped to read-only tools, cannot mutate).
357
+ // --fix → acceptEdits + auto-checkpoint (reversible edits).
358
+ const permissionMode = fix ? "acceptEdits" : "plan";
359
+ const autoCheckpoint = fix && options.checkpoint !== false;
360
+ const defaultMaxTurns = fix ? 40 : 20;
361
+ const maxTurns = Number.isFinite(options.maxTurns)
362
+ ? options.maxTurns
363
+ : defaultMaxTurns;
364
+
365
+ const run = deps.runAgentHeadless ||
366
+ (await import("../runtime/headless-runner.js")).runAgentHeadless;
367
+
368
+ const outcome = await run({
369
+ prompt,
370
+ model: options.model,
371
+ provider: options.provider,
372
+ baseUrl: options.baseUrl,
373
+ apiKey: options.apiKey,
374
+ outputFormat: options.outputFormat || "text",
375
+ permissionMode,
376
+ autoCheckpoint,
377
+ checkpointSession: options.checkpointSession || `review-${scope}`,
378
+ maxTurns,
379
+ cwd,
380
+ // The diff carries `@@` hunk markers and may contain `@tokens`; never run
381
+ // the @file-reference expander over it.
382
+ expandFileRefs: false,
383
+ });
384
+
385
+ return { ...outcome, scope, empty: false };
386
+ }
387
+
388
+ export function registerReviewCommand(program) {
389
+ program
390
+ .command("review [effort]")
391
+ .description(
392
+ "Diff-first code review of your changes (Claude-Code /code-review parity)",
393
+ )
394
+ .option("--staged", "Review staged changes only (git diff --cached)")
395
+ .option("--base <ref>", "Review this branch vs a base ref (<ref>...HEAD)")
396
+ .option("--range <range>", "Review an explicit revision range (e.g. A..B)")
397
+ .option(
398
+ "--paths <paths...>",
399
+ "Limit the review to these paths (repeatable)",
400
+ )
401
+ .option("-e, --effort <level>", "low | medium | high (default: medium)")
402
+ .option("--fix", "Apply the fixes to the working tree (auto-checkpointed)")
403
+ .option("--security", "Security-focused review (/security-review parity)")
404
+ .option("--simplify", "Cleanup-only review, no bug hunt (/simplify parity)")
405
+ .option("--no-untracked", "Skip untracked new files (working scope)")
406
+ .option("--no-checkpoint", "With --fix: do not auto-checkpoint before edits")
407
+ .option("--model <model>", "Override the review model")
408
+ .option("--provider <provider>", "Override the LLM provider")
409
+ .option("--base-url <url>", "Override the API base URL")
410
+ .option("--api-key <key>", "Override the API key")
411
+ .option("--max-turns <n>", "Cap agent loop iterations")
412
+ .option("--json", "Emit the agent result envelope as JSON")
413
+ .action(async (effortArg, options) => {
414
+ try {
415
+ const merged = {
416
+ ...options,
417
+ effort: options.effort || effortArg,
418
+ // commander stores --no-untracked as untracked:false, --no-checkpoint
419
+ // as checkpoint:false; pass them through unchanged.
420
+ maxTurns: options.maxTurns
421
+ ? parseInt(options.maxTurns, 10)
422
+ : undefined,
423
+ outputFormat: options.json ? "json" : "text",
424
+ cwd: process.cwd(),
425
+ };
426
+
427
+ // Pre-flight messaging on stderr so stdout stays a clean payload.
428
+ const mode = resolveReviewMode(merged);
429
+ const effort = normalizeEffort(merged.effort);
430
+ const { label } = resolveDiffArgs({
431
+ staged: merged.staged,
432
+ base: merged.base,
433
+ range: merged.range,
434
+ paths: merged.paths,
435
+ });
436
+ const modeLabel =
437
+ mode === "security"
438
+ ? "security"
439
+ : mode === "simplify"
440
+ ? "cleanup-only"
441
+ : "bugs + cleanup";
442
+ logger.info(
443
+ chalk.gray(
444
+ `Reviewing ${label} · ${effort} effort · ${modeLabel}` +
445
+ (merged.fix ? " · applying fixes" : " · read-only"),
446
+ ),
447
+ );
448
+
449
+ const result = await runReview(merged, {});
450
+
451
+ if (result.empty) {
452
+ logger.log(chalk.gray("No changes to review."));
453
+ return;
454
+ }
455
+ if (result.isError) {
456
+ process.exitCode = result.exitCode || 1;
457
+ }
458
+ } catch (err) {
459
+ logger.error(chalk.red(`review failed: ${err.message}`));
460
+ process.exitCode = 1;
461
+ }
462
+ });
463
+ }
package/src/index.js CHANGED
@@ -64,6 +64,7 @@ import { registerGoalCommand } from "./commands/goal.js";
64
64
  import { registerCommandCommand } from "./commands/command.js";
65
65
  import { registerCompactCommand } from "./commands/compact.js";
66
66
  import { registerLoopCommand } from "./commands/loop.js";
67
+ import { registerReviewCommand } from "./commands/review.js";
67
68
  import { registerPermissionsCommand } from "./commands/permissions.js";
68
69
  import { registerOutputStyleCommand } from "./commands/output-style.js";
69
70
  import { registerStatuslineCommand } from "./commands/statusline.js";
@@ -477,6 +478,7 @@ export function createProgram(opts = {}) {
477
478
  registerCommandCommand(program);
478
479
  registerCompactCommand(program);
479
480
  registerLoopCommand(program);
481
+ registerReviewCommand(program);
480
482
  registerPermissionsCommand(program);
481
483
  registerOutputStyleCommand(program);
482
484
  registerStatuslineCommand(program);
@@ -0,0 +1,109 @@
1
+ /**
2
+ * cost-budget — a hard USD spend cap for unattended agent runs
3
+ * (Claude-Code `--max-budget-usd` parity).
4
+ *
5
+ * Where IterationBudget caps the number of agent-loop turns, CostBudget caps the
6
+ * estimated dollar cost: it accumulates the per-call cost (via llm-pricing) as
7
+ * token-usage events arrive and reports when the cap is reached, so the runner
8
+ * can stop BEFORE making another paid LLM call. Because a call's cost is only
9
+ * known after it returns, a run may overshoot by at most one call — it never
10
+ * starts a new turn once over budget.
11
+ *
12
+ * Local/free providers (ollama, …) and unpriced models cost $0 here, so a cap
13
+ * can never trigger for them; `shouldWarnInactive()` lets the caller surface a
14
+ * one-time "cap inactive" notice instead of silently doing nothing.
15
+ *
16
+ * Pure + dependency-light (only llm-pricing) so it is unit-testable without a
17
+ * real agent loop.
18
+ */
19
+
20
+ import { estimateCost } from "./llm-pricing.js";
21
+
22
+ const round = (n, dp = 6) => {
23
+ const f = Math.pow(10, dp);
24
+ return Math.round((Number(n) + Number.EPSILON) * f) / f;
25
+ };
26
+
27
+ /** Parse a `--max-budget-usd` value into a positive number, or null when unset. */
28
+ export function parseBudgetUsd(value) {
29
+ if (value == null || value === "") return null;
30
+ const n = Number(value);
31
+ if (!Number.isFinite(n) || n <= 0) {
32
+ throw new Error(
33
+ `Invalid --max-budget-usd "${value}". Expected a positive number of US dollars.`,
34
+ );
35
+ }
36
+ return n;
37
+ }
38
+
39
+ export class CostBudget {
40
+ /**
41
+ * @param {object} opts
42
+ * @param {number|null} [opts.limitUsd] cap in USD; null/≤0 → disabled
43
+ * @param {object} [opts.table] merged price table (mergePricing output)
44
+ */
45
+ constructor({ limitUsd = null, table = undefined } = {}) {
46
+ const lim = Number(limitUsd);
47
+ this.limitUsd = Number.isFinite(lim) && lim > 0 ? lim : null;
48
+ this.table = table;
49
+ this.spentUsd = 0;
50
+ this.priced = false; // priced ≥1 non-free usage record
51
+ this.sawUnpriced = false; // saw tokens we couldn't price
52
+ this.sawFree = false; // saw a free/local provider
53
+ this._warned = false;
54
+ }
55
+
56
+ enabled() {
57
+ return this.limitUsd != null;
58
+ }
59
+
60
+ /**
61
+ * Fold one token-usage record into the running spend.
62
+ * @returns {object} the estimateCost() result for this record
63
+ */
64
+ add({ provider, model, usage } = {}) {
65
+ const est = estimateCost({
66
+ provider,
67
+ model,
68
+ inputTokens: usage?.input_tokens || 0,
69
+ outputTokens: usage?.output_tokens || 0,
70
+ table: this.table,
71
+ });
72
+ const tokens = (usage?.input_tokens || 0) + (usage?.output_tokens || 0);
73
+ if (est.free) {
74
+ this.sawFree = true;
75
+ } else if (est.matched) {
76
+ this.spentUsd = round(this.spentUsd + est.totalCost);
77
+ this.priced = true;
78
+ } else if (tokens > 0) {
79
+ this.sawUnpriced = true;
80
+ }
81
+ return est;
82
+ }
83
+
84
+ /** True once the running spend has reached/passed the cap. */
85
+ exceeded() {
86
+ return this.limitUsd != null && this.spentUsd >= this.limitUsd;
87
+ }
88
+
89
+ /** USD left under the cap (Infinity when disabled). */
90
+ remaining() {
91
+ return this.limitUsd == null
92
+ ? Infinity
93
+ : Math.max(0, round(this.limitUsd - this.spentUsd));
94
+ }
95
+
96
+ /**
97
+ * True the FIRST time we can tell the cap can't bite — a cap was set but every
98
+ * usage so far has been free/local or unpriced, so spend stays $0. Lets the
99
+ * caller print a one-time "cap inactive" warning instead of a silent no-op.
100
+ */
101
+ shouldWarnInactive() {
102
+ if (!this.enabled() || this._warned || this.priced) return false;
103
+ if (this.sawUnpriced || this.sawFree) {
104
+ this._warned = true;
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ }
@@ -278,6 +278,13 @@ export function hasIdeOpenDiff(mcp) {
278
278
  * Run one blocking openDiff review in the connected IDE. Returns
279
279
  * { outcome:"accepted", finalText|null } — the IDE wrote the file itself
280
280
  * { outcome:"rejected" } — nothing was written
281
+ * { outcome:"changes-requested", comments, reviewedText }
282
+ * — the user annotated the diff with
283
+ * revision notes instead of
284
+ * accepting/rejecting; nothing was
285
+ * written and the caller should
286
+ * feed `comments` back to the agent
287
+ * so it revises and re-proposes.
281
288
  * null — IDE unavailable / transport
282
289
  * error / malformed reply → the
283
290
  * caller falls back to its normal
@@ -309,10 +316,53 @@ export async function requestIdeDiffApproval(mcp, req = {}) {
309
316
  finalText: typeof data.finalText === "string" ? data.finalText : null,
310
317
  };
311
318
  }
319
+ if (data?.outcome === "changes-requested") {
320
+ return {
321
+ outcome: "changes-requested",
322
+ comments: Array.isArray(data.comments) ? data.comments : [],
323
+ reviewedText:
324
+ typeof data.reviewedText === "string" ? data.reviewedText : null,
325
+ };
326
+ }
312
327
  if (data?.outcome === "rejected") return { outcome: "rejected" };
313
328
  return null; // anything else is not a verdict — fail safe to fallback
314
329
  }
315
330
 
331
+ /**
332
+ * Render line-anchored review comments (from an openDiff "changes-requested"
333
+ * verdict) into a compact feedback block the agent can act on. Each comment is
334
+ * `{ line?, endLine?, lineText?, note }` with 0-based editor lines. Returns
335
+ * null when there is no actionable note. Pure — safe to unit-test.
336
+ */
337
+ export function formatReviewComments(comments, { path: filePath } = {}) {
338
+ if (!Array.isArray(comments) || comments.length === 0) return null;
339
+ const lines = comments
340
+ .map((c) => {
341
+ if (!c || typeof c.note !== "string" || c.note.trim().length === 0) {
342
+ return null;
343
+ }
344
+ const start = Number.isInteger(c.line) ? c.line + 1 : null; // 0→1-based
345
+ const end = Number.isInteger(c.endLine) ? c.endLine + 1 : start;
346
+ const where =
347
+ start != null
348
+ ? end != null && end !== start
349
+ ? `lines ${start}-${end}`
350
+ : `line ${start}`
351
+ : "(general)";
352
+ const anchor =
353
+ typeof c.lineText === "string" && c.lineText.trim().length > 0
354
+ ? ` ⟪${c.lineText.trim().slice(0, 120)}⟫`
355
+ : "";
356
+ return ` • ${where}: ${c.note.trim()}${anchor}`;
357
+ })
358
+ .filter(Boolean);
359
+ if (lines.length === 0) return null;
360
+ const header = filePath
361
+ ? `Review comments on ${filePath}:`
362
+ : "Review comments:";
363
+ return `${header}\n${lines.join("\n")}`;
364
+ }
365
+
316
366
  // ─── Explicit @selection / @diagnostics at-mentions (Claude-Code parity) ────
317
367
  //
318
368
  // The ambient `<ide-context>` block above shares the selection on every turn.