chainlesschain 0.162.155 → 0.162.156

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 (166) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-Cth_zZeA.js → AIOps-DjBXlypQ.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-zfwA3Oxn.js → ActionButton-Cv9er9bp.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-FeKc4aU2.js → Analytics-CLKDjSNs.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-C6WIrDZk.js → AppLayout-CfyZNzmJ.js} +3 -3
  7. package/src/assets/web-panel/assets/{Audit-C2gRq5Qi.js → Audit-CXafD178.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-TeTIFiW-.js → BackgroundAgents-CP5c-GSd.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-CLJk3k6i.js → Backup-BbYLQfyt.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-5ms537NY.js → BaseInput-Bqe0MwZL.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-4Nr5U9PJ.js → Chat-jNA_yUPH.js} +4 -4
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-BOFLF01s.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-B2Bca5ev.js → Checkbox-Dlzz-zD1.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-D6Uysdbv.js → Codegen-BAsDQjGf.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-B3XQ8xc7.js → Col-Ju0HrAuS.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CqwKtoPz.js → Community-DynJd43Z.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BZZINTus.js → Compact-CabbZVww.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Dr3BXkWA.js → Compliance-DW4BmAoh.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-rx_x9Wqc.js → Cowork-Bk1S_7X-.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-BpZr7rHc.js → Cron-By4sh_NM.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-CeV5EYVg.js → Crosschain-DdZdbS1C.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-fe2Qaowz.js → DID-CkL-sQyS.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Fh_EdWVa.js → Dashboard-g_mOpumg.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-N1PIwWYf.js → Dropdown-DtHyE7Ao.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-B6vK93Ed.js → EmailListRenderer-Kpvov5Wr.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-e3iUZhAg.js → FamilyGuardDashboard-C4kZoePR.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-uInc9YcN.js → Federation-BAwWoK2N.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DmQZSX0z.js → FormItemContext-DeI0i5f8.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-CV_Z8OAo.js → GenericCardRenderer-CkcKRtos.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-jXAhNN8d.js → Git-kX6jjI9y.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-Blw1esBB.js → Governance-BXJCBm6x.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CoK8Ia0Z.js → Inference-ujD2JSKo.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-5xrglMTK.js → KnowledgeGraph-3Dnuf1Wh.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BGIG9798.js → Logs-lVAZCtUB.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-DFqhJMuY.js → Marketplace-qnzNPm-A.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-7U8tEhwK.js → McpTools-CqjzefRU.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-BrvboPLE.js → Memory-BFiPAxk6.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-nzcd7d1H.js → MobileBridge-D0O43VwF.js} +1 -1
  39. package/src/assets/web-panel/assets/{MobileProjects-DETVNKa7.js → MobileProjects-B3now48t.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-SZcCYec3.js → Mtc-DybhtaCS.js} +2 -2
  41. package/src/assets/web-panel/assets/{MtcAudit-Cx_d3N17.js → MtcAudit-D7MmY4F0.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-DtcmSUo-.js → Multisig-wZvGJnB8.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-Dgx2QHl-.js → NLProgramming-BNPpgfvn.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-oHTGEHoJ.js → Notes-B6FS3KBj.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-qKD-IlOl.js → NotificationSettings-CK0yla9U.js} +1 -1
  46. package/src/assets/web-panel/assets/OrderTableRenderer-CXfPHIpy.js +1 -0
  47. package/src/assets/web-panel/assets/{Organization-BBNXnltP.js → Organization-geqgwN1T.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-Df2fDbvT.js → Overflow-BWJYZFbC.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-DUHj6zjO.js → P2P-DnU1505F.js} +2 -2
  50. package/src/assets/web-panel/assets/{PdhVaultBrowser-B2nWgdoV.js → PdhVaultBrowser-BGY74KnC.js} +4 -4
  51. package/src/assets/web-panel/assets/{Permissions-CiW0afny.js → Permissions-CgArrWtQ.js} +3 -3
  52. package/src/assets/web-panel/assets/{PersonalDataHub-cc1mHU_D.js → PersonalDataHub-C2vYE61a.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-B93zTapU.js → Pipeline-CBYLI4yW.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-Db1jXWfd.js → Privacy-B-PdW9aN.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-DFoZ1h9U.js → ProjectInit-CuWQegbV.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-BkkUnmYs.js → ProjectSettings-4d32m_cS.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-Xa779XLC.js → Projects-VLA7ttwd.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-DOF01IEN.js → Providers-B_Izb_oa.js} +1 -1
  59. package/src/assets/web-panel/assets/{QrScannerModal-TohRsKd0.js → QrScannerModal-C6B_ndXV.js} +1 -1
  60. package/src/assets/web-panel/assets/{QuickAsk-DjLcr1ZU.js → QuickAsk-C6tavL-y.js} +1 -1
  61. package/src/assets/web-panel/assets/{Recommend--Ism6bMx.js → Recommend-CF-LaZ6x.js} +1 -1
  62. package/src/assets/web-panel/assets/{RemoteSession-B8CL3YHM.js → RemoteSession-DBqTTQ9_.js} +2 -2
  63. package/src/assets/web-panel/assets/{Reputation-CaRxB3Ep.js → Reputation-B01FALUi.js} +1 -1
  64. package/src/assets/web-panel/assets/{Row-BioxZBBH.js → Row-DO0fd48A.js} +1 -1
  65. package/src/assets/web-panel/assets/{RssFeed-B2LipwH3.js → RssFeed-BV5LDzFR.js} +2 -2
  66. package/src/assets/web-panel/assets/{Search-BpjLBV8u.js → Search-CTti4_MP.js} +1 -1
  67. package/src/assets/web-panel/assets/{Security-TibHuJWP.js → Security-DwSrWRCw.js} +4 -4
  68. package/src/assets/web-panel/assets/{Services-DtkuWseW.js → Services-BEf1QyM4.js} +2 -2
  69. package/src/assets/web-panel/assets/{Skeleton-BIcPQae6.js → Skeleton-LZXTpovy.js} +1 -1
  70. package/src/assets/web-panel/assets/Skills-SKgc0Zug.js +1 -0
  71. package/src/assets/web-panel/assets/{Sla-uUSU185m.js → Sla-C_QVOtkN.js} +1 -1
  72. package/src/assets/web-panel/assets/{SpeechSettings-B7Bgc9mk.js → SpeechSettings-v5TW2V-X.js} +1 -1
  73. package/src/assets/web-panel/assets/{SyncSettings-K-XrJ2s9.js → SyncSettings-CQPjHdqU.js} +2 -2
  74. package/src/assets/web-panel/assets/Tasks-DCd-uYcI.js +1 -0
  75. package/src/assets/web-panel/assets/{Templates-CaFnyGDK.js → Templates-62VDIjtj.js} +1 -1
  76. package/src/assets/web-panel/assets/{Tenant-C2X0PtEC.js → Tenant-BnFQ0pNy.js} +1 -1
  77. package/src/assets/web-panel/assets/{Terminal-CKU5yVsT.js → Terminal-D3v0MGSj.js} +2 -2
  78. package/src/assets/web-panel/assets/{TimelineRenderer-8trZeYDC.js → TimelineRenderer-BI1Ka-cu.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tokens-BUEEe9US.js → Tokens-BwX1jXYP.js} +1 -1
  80. package/src/assets/web-panel/assets/{Trigger-COU97I7Y.js → Trigger-CSA8TqNx.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trust-DNWQChaP.js → Trust-Bn3IjCx7.js} +1 -1
  82. package/src/assets/web-panel/assets/{UkeySign-DWX_VtCp.js → UkeySign-pUbOAqNi.js} +1 -1
  83. package/src/assets/web-panel/assets/{VideoEditing-D9eIDhsp.js → VideoEditing-Db2RercF.js} +1 -1
  84. package/src/assets/web-panel/assets/{Wallet-BOnCCgIL.js → Wallet-CpR1BVAl.js} +4 -4
  85. package/src/assets/web-panel/assets/{WebAuthn-DN4dD-W8.js → WebAuthn-Yxtkn7Zp.js} +5 -5
  86. package/src/assets/web-panel/assets/{WorkflowEditor-ClvDb_jX.js → WorkflowEditor-CYLe9a2E.js} +1 -1
  87. package/src/assets/web-panel/assets/{chat-DIxQ_Eeb.js → chat-gsOGdak0.js} +1 -1
  88. package/src/assets/web-panel/assets/{colors-BOX3WKcP.js → colors-D9fN299a.js} +1 -1
  89. package/src/assets/web-panel/assets/{compact-item-Bny82VDi.js → compact-item-vkd7s-ay.js} +1 -1
  90. package/src/assets/web-panel/assets/{createContext-Cnoy3enS.js → createContext-C7YkbMyy.js} +1 -1
  91. package/src/assets/web-panel/assets/devWarning-DEcR2h6k.js +1 -0
  92. package/src/assets/web-panel/assets/{hasIn-BJ7fkx3F.js → hasIn-ig7reRSU.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CZCTan8Q.js → index-9TxdMYju.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-7GLXvG8p.js → index-B7QRpKif.js} +1 -1
  95. package/src/assets/web-panel/assets/index-BEibanjg.js +1 -0
  96. package/src/assets/web-panel/assets/{index-CEPTiBWy.js → index-BHHN_KTS.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DBhkJqVF.js → index-BMw48d19.js} +1 -1
  98. package/src/assets/web-panel/assets/{index--CcGiwAI.js → index-BYe9HRSt.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-qOBKrw-g.js → index-BfpVcXKa.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BcmXbIwv.js → index-BhftELHI.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C--PPqwi.js → index-BkP17Z95.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Kio1ekYg.js → index-BpGlLbRv.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CZ6CHiCQ.js → index-Bt-9ZhIu.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BM0mQhVv.js → index-BvDEukXe.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BJ8-oLHI.js → index-C7AY39sg.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-2N9Y1cLx.js → index-C8HZMygt.js} +1 -1
  107. package/src/assets/web-panel/assets/{index--Xb2lq6E.js → index-CAkz1f1r.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BbZynivw.js → index-CDWTycrg.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BDkN6YOI.js → index-CGzGe5tv.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-D2qNdrNW.js → index-Co2NYyew.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BWPey0Uf.js → index-CspGymmg.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C1nHEOFE.js → index-D2yhtk6a.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CEq5Vh0i.js → index-D6s_kbjt.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-D9QEc7Tq.js → index-DFM1oNe1.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Bk5yST66.js → index-DFfxF7Vt.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Ty4w18mR.js → index-DPguq3X7.js} +3 -3
  117. package/src/assets/web-panel/assets/{index-CQ-xLCVb.js → index-DQ8U7kzC.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-B6WoW8DJ.js → index-DZAxCnjS.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CnjtAKdw.js → index-DhdXbsXX.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-OaNFXV8N.js → index-DuwbKHoS.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CN6emwbq.js → index-DvCJXmrx.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-rn6xd12s.js → index-Dy_TKZ9h.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CW6n-y1L.js → index-FzGr6CVk.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-ceH9DsAi.js → index-PkH39qsp.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DxX8hhmL.js → index-SDrcQXO1.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B7Fah_BX.js → index-W52sHRdr.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DSRIjAWO.js → index-bVbgYBhv.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BP7HFsAK.js → index-gj8kxcAi.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-B1-h2QrC.js → index-gpurplyq.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Bvheiep2.js → index-nX8YE6cj.js} +1 -1
  131. package/src/assets/web-panel/assets/index-z_25tU3p.js +1 -0
  132. package/src/assets/web-panel/assets/{initDefaultProps-BmxMBuUZ.js → initDefaultProps-BrgfVRg9.js} +1 -1
  133. package/src/assets/web-panel/assets/{motion-DnXNwo2k.js → motion-D_3xmwac.js} +1 -1
  134. package/src/assets/web-panel/assets/{move-WnIJwmZN.js → move-DDDJdlxw.js} +1 -1
  135. package/src/assets/web-panel/assets/{mtc-parser-DAto-c2w.js → mtc-parser-BK2uEWwn.js} +1 -1
  136. package/src/assets/web-panel/assets/{omit-BtvW9W0H.js → omit-BPHKbDcf.js} +1 -1
  137. package/src/assets/web-panel/assets/{pickAttrs-DFqI9-_C.js → pickAttrs-LNnTscuz.js} +1 -1
  138. package/src/assets/web-panel/assets/{placementArrow-Dax6wOK3.js → placementArrow-CvmTZll5.js} +1 -1
  139. package/src/assets/web-panel/assets/{responsiveObserve-BUFWWC83.js → responsiveObserve-eBj7qi2J.js} +1 -1
  140. package/src/assets/web-panel/assets/{slide-DyARyuSz.js → slide-D68hHkrM.js} +1 -1
  141. package/src/assets/web-panel/assets/{statusUtils-BZSDugpi.js → statusUtils-daTlaeo0.js} +1 -1
  142. package/src/assets/web-panel/assets/{styleChecker-B7o_K-4R.js → styleChecker-DtyKMtHB.js} +1 -1
  143. package/src/assets/web-panel/assets/{useFlexGapSupport-Dg7Bg8VG.js → useFlexGapSupport-ekFwFs9h.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFs-DQYXo5Rk.js → useFs-jRaSEoF4.js} +1 -1
  145. package/src/assets/web-panel/assets/{usePersonalDataHub-E8NBAnkq.js → usePersonalDataHub-DD5nGzjA.js} +1 -1
  146. package/src/assets/web-panel/assets/{vnode-DD71_w1b.js → vnode-KcgLUiom.js} +1 -1
  147. package/src/assets/web-panel/assets/{zoom-zc9EMd3q.js → zoom-Cyk2PQ-s.js} +1 -1
  148. package/src/assets/web-panel/index.html +1 -1
  149. package/src/command-manifest.json +15 -1
  150. package/src/commands/agenda.js +228 -0
  151. package/src/commands/batch.js +345 -0
  152. package/src/data/changelog.json +7 -2
  153. package/src/index.js +12 -0
  154. package/src/lib/agent-batch.js +235 -0
  155. package/src/lib/agent-notify.js +66 -0
  156. package/src/lib/agent-schedule-store.js +330 -0
  157. package/src/runtime/agent-core.js +104 -0
  158. package/src/runtime/coding-agent-contract-shared.cjs +104 -0
  159. package/src/runtime/coding-agent-policy.cjs +28 -0
  160. package/src/assets/web-panel/assets/ChatBubbleRenderer-BFEnA8LN.js +0 -1
  161. package/src/assets/web-panel/assets/OrderTableRenderer-CiU4Sy1J.js +0 -1
  162. package/src/assets/web-panel/assets/Skills-DoTWIQOX.js +0 -1
  163. package/src/assets/web-panel/assets/Tasks-B3hEXNcI.js +0 -1
  164. package/src/assets/web-panel/assets/devWarning-Dr9dzlQ5.js +0 -1
  165. package/src/assets/web-panel/assets/index-B92M3t6B.js +0 -1
  166. package/src/assets/web-panel/assets/index-d2UcGpK-.js +0 -1
@@ -0,0 +1,345 @@
1
+ /**
2
+ * `cc batch` — dynamic worktree batch (gap-analysis 第四阶段 #4). Split a big
3
+ * change into independent UNITS, run each in its own git worktree in parallel,
4
+ * test each, and aggregate diff/test/merge-conflict results.
5
+ *
6
+ * cc batch --units units.json # run declared units
7
+ * cc batch --units units.json --test "npm test" --concurrency 4 --merge
8
+ * cc batch --decompose "migrate all foo() calls to bar()" --parts 8
9
+ * # ask an agent to split the goal into a units.json, then run it
10
+ * cc batch --decompose "…" --parts 8 --plan-only # emit units, don't run
11
+ *
12
+ * units.json: { "units": [ { "key": "u1", "prompt": "…", "test": "npm test -w a" } ] }
13
+ *
14
+ * Real isolation comes from worktree-isolator (the same primitive `cc team
15
+ * --worktree` uses); the fan-out + aggregation core lives in lib/agent-batch.js
16
+ * (fully injectable). Branches persist for inspection unless merged.
17
+ */
18
+
19
+ import fs from "fs";
20
+ import path from "path";
21
+ import { fileURLToPath } from "url";
22
+ import { spawn } from "child_process";
23
+ import { execFileSync } from "child_process";
24
+ import chalk from "chalk";
25
+ import { logger } from "../lib/logger.js";
26
+ import { runBatch } from "../lib/agent-batch.js";
27
+ import {
28
+ createWorktree,
29
+ removeWorktree,
30
+ previewWorktreeMerge,
31
+ mergeWorktree,
32
+ } from "../harness/worktree-isolator.js";
33
+ import { findProjectRoot } from "../lib/project-detector.js";
34
+
35
+ const BIN = path.resolve(
36
+ path.dirname(fileURLToPath(import.meta.url)),
37
+ "..",
38
+ "..",
39
+ "bin",
40
+ "chainlesschain.js",
41
+ );
42
+
43
+ const DECOMPOSE_SCHEMA = {
44
+ type: "object",
45
+ properties: {
46
+ units: {
47
+ type: "array",
48
+ items: {
49
+ type: "object",
50
+ properties: {
51
+ key: { type: "string" },
52
+ prompt: { type: "string" },
53
+ test: { type: "string" },
54
+ },
55
+ required: ["key", "prompt"],
56
+ },
57
+ },
58
+ },
59
+ required: ["units"],
60
+ };
61
+
62
+ export function registerBatchCommand(program) {
63
+ program
64
+ .command("batch")
65
+ .description(
66
+ "Split a large change into independent units, run each in its own git worktree in parallel, and aggregate results",
67
+ )
68
+ .option(
69
+ "--units <file>",
70
+ "Units JSON file ({ units: [{key, prompt, test?}] })",
71
+ )
72
+ .option(
73
+ "--decompose <goal>",
74
+ "Ask an agent to split this goal into units first (structured output)",
75
+ )
76
+ .option("--parts <n>", "Target number of units for --decompose", "6")
77
+ .option(
78
+ "--plan-only",
79
+ "With --decompose: print the units and exit (no run)",
80
+ )
81
+ .option("--concurrency <n>", "Max worktrees running at once", "4")
82
+ .option("--test <cmd>", "Default test command run in each worktree")
83
+ .option("--merge", "Merge clean, passing branches back to base")
84
+ .option("--model <model>", "Model for agent runs")
85
+ .option("--json", "Machine-readable JSON output")
86
+ .action(async (options) => {
87
+ process.exitCode = await runBatchCommand(options);
88
+ });
89
+ }
90
+
91
+ export async function runBatchCommand(options = {}, _deps = {}) {
92
+ const log = _deps.log || ((m) => console.log(m));
93
+ const errLog = _deps.err || ((m) => logger.error(m));
94
+ const repoDir =
95
+ _deps.repoDir || findProjectRoot(process.cwd()) || process.cwd();
96
+
97
+ // 1. Resolve the unit list — from a file, or by decomposing a goal.
98
+ let units;
99
+ try {
100
+ if (options.units) {
101
+ const doc = JSON.parse(fs.readFileSync(options.units, "utf8"));
102
+ units = Array.isArray(doc) ? doc : doc.units;
103
+ } else if (options.decompose) {
104
+ units = await (_deps.decompose || decomposeGoal)(
105
+ options.decompose,
106
+ Number(options.parts) || 6,
107
+ { model: options.model, repoDir },
108
+ );
109
+ if (options.planOnly) {
110
+ log(JSON.stringify({ units }, null, 2));
111
+ return 0;
112
+ }
113
+ } else {
114
+ errLog("Provide --units <file> or --decompose <goal>.");
115
+ return 4;
116
+ }
117
+ } catch (err) {
118
+ errLog(`Failed to resolve units: ${err.message}`);
119
+ return 4;
120
+ }
121
+
122
+ // 2. Run the batch with real worktree + agent + git deps.
123
+ const deps = _deps.batchDeps || {
124
+ createWorktree: (key, branch) => createWorktree(repoDir, branch).path,
125
+ removeWorktree: (worktreePath, opts) =>
126
+ removeWorktree(repoDir, worktreePath, opts),
127
+ runAgent:
128
+ _deps.runAgent ||
129
+ ((prompt, cwd) => spawnAgent(prompt, cwd, { model: options.model })),
130
+ runTest: _deps.runTest || runTestCommand,
131
+ diffStat: _deps.diffStat || gitDiffStat,
132
+ commit: _deps.commit || gitCommitAll,
133
+ previewMerge: (branch) => previewWorktreeMerge(repoDir, branch),
134
+ mergeBranch: (branch, message) =>
135
+ mergeWorktree(repoDir, branch, { message }),
136
+ branchFor: (key) => `batch/${sanitize(key)}`,
137
+ };
138
+
139
+ const onEvent = options.json ? () => {} : (ev) => log(formatEvent(ev));
140
+
141
+ let outcome;
142
+ try {
143
+ outcome = await runBatch(
144
+ {
145
+ units,
146
+ concurrency: options.concurrency,
147
+ test: options.test || null,
148
+ merge: options.merge === true,
149
+ onEvent,
150
+ },
151
+ deps,
152
+ );
153
+ } catch (err) {
154
+ errLog(`Batch failed: ${err.message}`);
155
+ return 1;
156
+ }
157
+
158
+ if (options.json) {
159
+ log(JSON.stringify(outcome, null, 2));
160
+ } else {
161
+ log("");
162
+ log(chalk.bold(" Batch summary:"));
163
+ const s = outcome.summary;
164
+ log(
165
+ ` ${s.total} units — ${chalk.green(s.done + " done")}, ` +
166
+ `${chalk.red(s.testFailed + " test-failed")}, ` +
167
+ `${chalk.yellow(s.errored + " errored")}, ${s.noChanges} no-change`,
168
+ );
169
+ if (options.merge) {
170
+ log(
171
+ ` merged ${chalk.green(s.merged)}, conflicts ${chalk.yellow(s.conflicted)}`,
172
+ );
173
+ }
174
+ log("");
175
+ }
176
+ // Non-zero when anything failed so CI can gate on it.
177
+ return outcome.summary.testFailed + outcome.summary.errored > 0 ? 1 : 0;
178
+ }
179
+
180
+ // ─── real effectful deps ───────────────────────────────────────────────────
181
+
182
+ function sanitize(key) {
183
+ return String(key).replace(/[^a-zA-Z0-9._-]/g, "-");
184
+ }
185
+
186
+ function spawnAgent(prompt, cwd, opts = {}) {
187
+ return new Promise((resolve, reject) => {
188
+ const args = [
189
+ BIN,
190
+ "agent",
191
+ "-p",
192
+ prompt,
193
+ "--permission-mode",
194
+ "acceptEdits",
195
+ "--output-format",
196
+ "text",
197
+ ];
198
+ if (opts.model) args.push("--model", opts.model);
199
+ const child = spawn(process.execPath, args, {
200
+ cwd,
201
+ env: { ...process.env, CLAUDECODE: "1" },
202
+ windowsHide: true,
203
+ });
204
+ let err = "";
205
+ child.stderr?.on("data", (d) => (err += d.toString("utf8")));
206
+ child.on("error", (e) => reject(new Error(e.message)));
207
+ child.on("close", (code) =>
208
+ code === 0
209
+ ? resolve({ code })
210
+ : reject(new Error(err.trim() || `agent exited ${code}`)),
211
+ );
212
+ });
213
+ }
214
+
215
+ function runTestCommand(command, cwd) {
216
+ return new Promise((resolve, reject) => {
217
+ const child = spawn(command, {
218
+ cwd,
219
+ shell: true,
220
+ env: process.env,
221
+ windowsHide: true,
222
+ });
223
+ let err = "";
224
+ child.stderr?.on("data", (d) => (err += d.toString("utf8")));
225
+ child.on("error", (e) => reject(new Error(e.message)));
226
+ child.on("close", (code) =>
227
+ code === 0
228
+ ? resolve({ code })
229
+ : reject(new Error(err.trim() || `test exited ${code}`)),
230
+ );
231
+ });
232
+ }
233
+
234
+ /** `git add -A` staged numstat totals for a worktree. */
235
+ function gitDiffStat(cwd) {
236
+ try {
237
+ execFileSync("git", ["add", "-A"], { cwd, stdio: "ignore" });
238
+ const out = execFileSync("git", ["diff", "--cached", "--numstat"], {
239
+ cwd,
240
+ encoding: "utf8",
241
+ });
242
+ let insertions = 0;
243
+ let deletions = 0;
244
+ let filesChanged = 0;
245
+ for (const line of out.split("\n")) {
246
+ const trimmed = line.trim();
247
+ if (!trimmed) continue;
248
+ const [add, del] = trimmed.split("\t");
249
+ filesChanged += 1;
250
+ insertions += add === "-" ? 0 : parseInt(add, 10) || 0;
251
+ deletions += del === "-" ? 0 : parseInt(del, 10) || 0;
252
+ }
253
+ return { filesChanged, insertions, deletions };
254
+ } catch {
255
+ return { filesChanged: 0, insertions: 0, deletions: 0 };
256
+ }
257
+ }
258
+
259
+ function gitCommitAll(cwd, message) {
260
+ try {
261
+ execFileSync("git", ["add", "-A"], { cwd, stdio: "ignore" });
262
+ execFileSync("git", ["commit", "-m", message, "--no-verify"], {
263
+ cwd,
264
+ stdio: "ignore",
265
+ });
266
+ return true;
267
+ } catch {
268
+ return false; // nothing to commit / commit failed
269
+ }
270
+ }
271
+
272
+ /** Spawn a headless agent that emits a units JSON via --json-schema. */
273
+ async function decomposeGoal(goal, parts, opts = {}) {
274
+ const prompt =
275
+ `Split this task into ${parts} INDEPENDENT units that can each be done in a ` +
276
+ `separate git worktree WITHOUT depending on each other's changes. For each ` +
277
+ `unit give a short kebab-case "key", a self-contained "prompt" (the full ` +
278
+ `instruction for an agent working only on that unit), and optionally a "test" ` +
279
+ `shell command to verify it.\n\nTask: ${goal}`;
280
+ return new Promise((resolve, reject) => {
281
+ const args = [
282
+ BIN,
283
+ "agent",
284
+ "-p",
285
+ prompt,
286
+ "--json-schema",
287
+ JSON.stringify(DECOMPOSE_SCHEMA),
288
+ "--permission-mode",
289
+ "plan",
290
+ ];
291
+ if (opts.model) args.push("--model", opts.model);
292
+ const child = spawn(process.execPath, args, {
293
+ cwd: opts.repoDir || process.cwd(),
294
+ env: { ...process.env, CLAUDECODE: "1" },
295
+ windowsHide: true,
296
+ });
297
+ let out = "";
298
+ let err = "";
299
+ child.stdout?.on("data", (d) => (out += d.toString("utf8")));
300
+ child.stderr?.on("data", (d) => (err += d.toString("utf8")));
301
+ child.on("error", (e) => reject(new Error(e.message)));
302
+ child.on("close", (code) => {
303
+ if (code !== 0) {
304
+ reject(new Error(err.trim() || `decompose agent exited ${code}`));
305
+ return;
306
+ }
307
+ try {
308
+ const parsed = JSON.parse(out.trim());
309
+ resolve(parsed.units);
310
+ } catch (e) {
311
+ reject(new Error(`could not parse decomposition output: ${e.message}`));
312
+ }
313
+ });
314
+ });
315
+ }
316
+
317
+ function formatEvent(ev) {
318
+ switch (ev.type) {
319
+ case "batch:start":
320
+ return chalk.dim(
321
+ ` batch: ${ev.units} units, concurrency ${ev.concurrency}`,
322
+ );
323
+ case "unit:start":
324
+ return chalk.dim(` ▶ ${ev.key} (${ev.branch})`);
325
+ case "unit:done": {
326
+ const badge =
327
+ ev.status === "done"
328
+ ? chalk.green("✓")
329
+ : ev.status === "test-failed"
330
+ ? chalk.red("✗ test")
331
+ : ev.status === "error"
332
+ ? chalk.red("✗ error")
333
+ : chalk.gray("·");
334
+ return ` ${badge} ${ev.key} (${ev.filesChanged} files)`;
335
+ }
336
+ case "unit:integrated":
337
+ return ev.conflicts
338
+ ? chalk.yellow(` ⚠ ${ev.key}: ${ev.conflicts} conflict(s)`)
339
+ : chalk.green(` ↳ ${ev.key}: ${ev.merged ? "merged" : "clean"}`);
340
+ default:
341
+ return "";
342
+ }
343
+ }
344
+
345
+ export const _internal = { decomposeGoal, gitDiffStat, DECOMPOSE_SCHEMA };
@@ -3,7 +3,7 @@
3
3
  "releases": [
4
4
  {
5
5
  "productVersion": "Unreleased",
6
- "cliVersion": "0.162.155",
6
+ "cliVersion": "0.162.156",
7
7
  "cliVersions": [
8
8
  "0.4.46",
9
9
  "0.37.4",
@@ -12,11 +12,16 @@
12
12
  "0.162.152",
13
13
  "0.162.153",
14
14
  "0.162.154",
15
- "0.162.155"
15
+ "0.162.155",
16
+ "0.162.156"
16
17
  ],
17
18
  "date": null,
18
19
  "title": "",
19
20
  "sections": [
21
+ {
22
+ "heading": "Added — cc CLI 0.162.156:agenda/batch 双命令(第四阶段收口)+ @chainlesschain/agent-sdk 0.1.0 npm 首发(CLI-only npm 发版)",
23
+ "body": "> `chainlesschain` 0.162.155 → **0.162.156** 发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`);**`@chainlesschain/agent-sdk` 0.1.0 同 run 首发 npm**(平台化第三阶段产物,npm-publish.yml / release.yml 均已加发布 step,防 trap #127 漏发)。纯 `packages/cli/src`(+web-panel dist 随包)+ 新独立包,未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。命令数 **170 → 172**(新增顶层 `agenda`/`batch`,清单已重生,13 文档面已扫)。\n\n- **`cc agenda` + notify/schedule 双 agent 工具(第四阶段批19)**:Monitor/Cron/Push 暴露为 agent 可调工具(AGENT_TOOLS 21→23)——`notify` 推送到用户已配置的多通道通知器(Telegram/企业微信/钉钉/飞书);`schedule` 一工具五动作(wakeup/cron/monitor/list/cancel),单轮进程持不住定时器 → 意图持久化到 `~/.chainlesschain/agent-schedule/*.jsonl`(0600),由 `cc agenda list|run|cancel` 消费触发(wakeup/cron 到期 spawn `cc agent -p`,monitor 跑命令匹配 stop_when 即通知)。两工具 LOW 风险、plan 模式禁用;29 单测 + 真进程 smoke。\n- **`cc batch` 动态 worktree 批处理(第四阶段批20,第四阶段全收口)**:把大改动拆成独立单元并行跑——`--units <file>` 或 `--decompose <goal> --parts N`(agent 经 `--json-schema` 产单元 JSON,`--plan-only` 只打印);每单元独立 git worktree(复用 `cc team --worktree` 的 worktree-isolator),`--test` 门控、`--merge` 顺序集成(后合分支与已合冲突时报告不强合、测试失败单元永不合并);15 单测 + 2 真 git/worktree 集成测试。\n- **`@chainlesschain/agent-sdk` 0.1.0 首发**:`cc agent` stream-json 双工协议的 TypeScript 正式契约(Agent Protocol v1)——流式事件全词汇 + 审批回调(fail-closed)+ 检查点 + 会话恢复;`AgentSession` spawn 客户端(Windows `cmd.exe /c` shim + `NoDefaultCurrentDirectoryInExePath` 反劫持 + `taskkill /T` 进程树回收内置)、`attachBackgroundSession` 后台管道接管、`/browser` 浏览器安全入口(web-panel bg-\\* 帧)、`/protocol` 纯类型入口;双构建 ESM+CJS,零运行时依赖,36 测试(单元+真管道集成+真 CLI e2e)。VS Code 扩展(vendored)与 web-panel(vite alias)已消费;JetBrains 对齐语言中立 `docs/PROTOCOL.md`。用户文档 `docs-site/docs/chainlesschain/agent-sdk.md`。\n- 修复(随本版仓内,不在 cli 包面):SDK 真 CLI e2e 抓出的两 IDE 首会话不落盘洞(IDE 重载后 resume 静默空会话)已在两插件修复,随各自下次插件发版出货。"
24
+ },
20
25
  {
21
26
  "heading": "Added — cc CLI 0.162.155:权限模式/auto 分类器 + 后台会话可交互接管全家桶(第一阶段收口,CLI-only npm 发版)",
22
27
  "body": "> `chainlesschain` 0.162.154 → **0.162.155** 已发 npm `latest`(经 `npm-publish.yml`,`--provenance --access public`)。纯 `packages/cli/src`(+web-panel dist 随包)增量,未触 `pdh/lib` → 无 Android cc bundle rollover / 无 USR_VERSION 改动。gap-analysis「第一阶段:安全与可运营性」4 项全部落地(批 1-15,9 commit `e3ab46c73f..1618a3ca83`)。命令数 **165 → 170**(新增顶层 `attach`/`auto-mode`/`daemon`/`logs`/`remote-control`,清单已重生)。发版前本机三层全绿(unit 21808 / integration 除 1 已知 real-spawn flake 隔离 3 连绿 / e2e 628)。\n\n- **权限模式补齐 `manual`/`auto`/`dontAsk`**(headless + 交互 REPL 双面):`manual`=default 别名;`auto`=可配置分类器——settings `autoMode.decisions` 按 riskLevel(对象/数组形)与 tool/commandPattern 细粒度规则(`*` glob,声明序优先)映射 allow/ask/deny,未配置时字节不变映射 trusted;`dontAsk`=需确认动作直接 deny 不弹审批(REPL 两处 confirmer 全覆盖)。REPL `/permissions auto|dontask` 中途切换,Shift+Tab 循环兼容;`cc agent --permission-mode` 对交互式会话生效(顺带修 `resolveAgentPolicy` 白名单静默丢弃 `--vim/--think/--fallback-model/--pdh` 等交互 flag 的预存 bug)。\n- **`cc auto-mode defaults|config [--json]`**:内置分类默认值文档 + user/project/local/managed 分层合并后的有效配置(含细粒度规则展示);修 `mergeSandboxSettings` 数组分支把数组形 decisions 毁成 `\"[object Object]\"` 的潜伏 bug(新 `mergeAutoModeSettings`,decisions=有序规则集 closer 层整体替换)。\n- **`cc permissions recent|denials [--json|--clear]` + 决策逐层解释链**:跨会话持久 denial ring buffer(headless + REPL 双源);被拒 run_shell 附带 `permissionChain`(settings-rules→shell-policy→approval-gate 各层判定+理由),`/permissions denials` 与 `cc permissions recent` 渲染紧凑链——一眼看清「allow 规则为何没生效」。\n- **后台会话可运营全家桶**:顶层 `cc logs <id>` / `cc attach <id>` / `cc daemon status|view|stop|rename|resume`。worker heartbeat + workerPid/agentPid 分离 + stale-heartbeat/PID-reuse 防误杀(可靠标 `lost`);`cc daemon resume <id> <prompt>` 把崩溃/终态会话按 sessionId 续接为新后台运行(原 argv 有意不持久化防 secrets 落盘)。\n- **`cc attach` 可交互接管(session transport)**:worker 每会话托管本地 NDJSON 控制通道(Win named pipe / POSIX socket,随机 token 存 0600 state 文件);attach 后键入即发 follow-up prompt(同 `--session` 自动续接对话史,多轮 turn 循环),`/stop` 截断当前轮,turn 运行中断开任务照跑、idle 断开会话收尾;transport 不可用自动回退日志流,`--no-input` 强制纯日志。\n- **web-panel「后台 Agent」面板**:新 `bg-*` WS 协议(list/view/attach/prompt/stop-turn/detach/stop/rename/resume,attach 期间推送 worker 事件与日志增量);takeover token 永不过 WS 边界。面板含列表/接管卡片/日志流/重命名/续跑。\n- 修复:后台 rename 与 worker read-modify-write 竞态(写后校验+有界重试);`settingsVerdict` 未传入 executeToolInner 的解释链断点;launcher/worker 状态竞写(先写 state 后 spawn + heartbeat 重申 transport)。\n- **`cc remote-control`(第四阶段批17,随本版一同发布)**:统一远控入口(别名 `rc`)——start/status/stop 组合既有 remote-session 栈(WS server + E2EE relay + registry + ledger + push + audit),配对 URI 双模式(relay E2EE / 直连 LAN),跨进程 0600 state 发现;顺带修 `cc serve --remote-session-relay-url` 白名单丢参从未生效的预存 bug。\n- **MCP tool search(第二阶段批16,随本版一同发布)**:大规模 MCP 面下工具定义超上下文阈值(默认窗口 10%,`mcp.toolSearch` settings / `CC_TOOL_SEARCH` env 可配)时自动换成 `[deferred]` stub + 内部 `tool_search` 检索工具;完整 schema 经 tool result 返回(append-only,保 prompt cache 稳定);REPL `/context` 新增 MCP tool schemas 占用与优化建议一节。未超阈值/关闭时行为字节不变。"
package/src/index.js CHANGED
@@ -156,6 +156,12 @@ import { registerServeCommand } from "./commands/serve.js";
156
156
  // Remote Control unified entry (第四阶段 #1)
157
157
  import { registerRemoteControlCommand } from "./commands/remote-control.js";
158
158
 
159
+ // Agenda: consumer for agent-scheduled wakeups/crons/monitors (第四阶段 #3)
160
+ import { registerAgendaCommand } from "./commands/agenda.js";
161
+
162
+ // Batch: dynamic worktree batch — split a change into parallel units (第四阶段 #4)
163
+ import { registerBatchCommand } from "./commands/batch.js";
164
+
159
165
  // Web UI
160
166
  import { registerUiCommand } from "./commands/ui.js";
161
167
  import { registerPackCommand } from "./commands/pack.js";
@@ -586,6 +592,12 @@ export function createProgram(opts = {}) {
586
592
  // Remote Control unified entry
587
593
  registerRemoteControlCommand(program);
588
594
 
595
+ // Agenda: fire agent-scheduled wakeups/crons/monitors
596
+ registerAgendaCommand(program);
597
+
598
+ // Batch: dynamic worktree batch
599
+ registerBatchCommand(program);
600
+
589
601
  // Web UI
590
602
  registerUiCommand(program);
591
603
 
@@ -0,0 +1,235 @@
1
+ /**
2
+ * agent-batch — dynamic worktree batch core (gap-analysis 第四阶段 #4).
3
+ *
4
+ * Takes a large task already decomposed into independent UNITS and runs each in
5
+ * its OWN git worktree, in parallel (bounded concurrency), then aggregates the
6
+ * outcome: per-unit agent status, test result, diff stat, and — on integration
7
+ * — a merge-conflict preview against base. This is the `/batch` shape from
8
+ * Claude Code, built on the same worktree isolation `cc team --worktree` uses.
9
+ *
10
+ * Pure over injected deps (worktree lifecycle, agent/test spawns, git) so the
11
+ * whole fan-out + aggregation is unit-testable without a real repo or LLM.
12
+ *
13
+ * A `unit` is `{ key, prompt, test? }`. Concurrency is a simple worker pool.
14
+ * Integration is SEQUENTIAL (like TeamWorktreeCoordinator.integrate) so a later
15
+ * branch that conflicts with an already-merged one is reported, not clobbered.
16
+ */
17
+
18
+ /** Run `items` through `worker` with at most `limit` in flight; preserves order. */
19
+ async function mapPool(items, limit, worker) {
20
+ const results = new Array(items.length);
21
+ let next = 0;
22
+ const runners = new Array(Math.max(1, Math.min(limit, items.length)))
23
+ .fill(0)
24
+ .map(async () => {
25
+ while (true) {
26
+ const i = next++;
27
+ if (i >= items.length) return;
28
+ results[i] = await worker(items[i], i);
29
+ }
30
+ });
31
+ await Promise.all(runners);
32
+ return results;
33
+ }
34
+
35
+ function normalizeUnits(units) {
36
+ if (!Array.isArray(units) || units.length === 0) {
37
+ throw new Error("batch requires a non-empty `units` array");
38
+ }
39
+ const seen = new Set();
40
+ return units.map((u, i) => {
41
+ const key = String(u.key || `unit-${i + 1}`);
42
+ if (seen.has(key)) throw new Error(`duplicate unit key "${key}"`);
43
+ seen.add(key);
44
+ if (!u.prompt || typeof u.prompt !== "string") {
45
+ throw new Error(`unit "${key}" has no prompt`);
46
+ }
47
+ return { key, prompt: u.prompt, test: u.test || null };
48
+ });
49
+ }
50
+
51
+ /**
52
+ * @param {object} opts
53
+ * @param {Array<{key,prompt,test?}>} opts.units
54
+ * @param {number} [opts.concurrency=4]
55
+ * @param {string} [opts.test] default test command for units without one
56
+ * @param {boolean} [opts.merge] merge clean branches back to base
57
+ * @param {(msg:object)=>void} [opts.onEvent] progress callback
58
+ * @param {object} deps injected effectful ops (see command wiring)
59
+ * @returns {Promise<{units:Array, summary:object}>}
60
+ */
61
+ export async function runBatch(opts, deps) {
62
+ const units = normalizeUnits(opts.units);
63
+ const concurrency = Math.max(1, Number(opts.concurrency) || 4);
64
+ const onEvent = opts.onEvent || (() => {});
65
+ const {
66
+ createWorktree,
67
+ removeWorktree,
68
+ runAgent,
69
+ runTest,
70
+ diffStat,
71
+ commit,
72
+ previewMerge,
73
+ mergeBranch,
74
+ branchFor = (key) => `batch/${key}`,
75
+ } = deps;
76
+
77
+ onEvent({ type: "batch:start", units: units.length, concurrency });
78
+
79
+ const results = await mapPool(units, concurrency, async (unit) => {
80
+ const branch = branchFor(unit.key);
81
+ const record = {
82
+ key: unit.key,
83
+ branch,
84
+ status: "pending",
85
+ filesChanged: 0,
86
+ insertions: 0,
87
+ deletions: 0,
88
+ testCommand: unit.test || opts.test || null,
89
+ testPassed: null,
90
+ committed: false,
91
+ error: null,
92
+ };
93
+ let worktreePath = null;
94
+ try {
95
+ worktreePath = createWorktree(unit.key, branch);
96
+ onEvent({ type: "unit:start", key: unit.key, branch });
97
+ await runAgent(unit.prompt, worktreePath);
98
+
99
+ const stat = diffStat(worktreePath);
100
+ record.filesChanged = stat.filesChanged;
101
+ record.insertions = stat.insertions;
102
+ record.deletions = stat.deletions;
103
+
104
+ const testCmd = unit.test || opts.test || null;
105
+ if (testCmd) {
106
+ try {
107
+ await runTest(testCmd, worktreePath);
108
+ record.testPassed = true;
109
+ } catch (err) {
110
+ record.testPassed = false;
111
+ record.testError = err.message;
112
+ }
113
+ }
114
+
115
+ if (stat.filesChanged > 0) {
116
+ record.committed = commit(worktreePath, `batch: ${unit.key}`);
117
+ }
118
+ // A unit whose tests FAILED is reported failed even though its worktree
119
+ // committed — integration below skips non-passing units so a red unit
120
+ // never merges into base.
121
+ record.status =
122
+ record.testPassed === false
123
+ ? "test-failed"
124
+ : record.filesChanged === 0
125
+ ? "no-changes"
126
+ : "done";
127
+ } catch (err) {
128
+ record.status = "error";
129
+ record.error = err.message;
130
+ }
131
+ onEvent({
132
+ type: "unit:done",
133
+ key: unit.key,
134
+ status: record.status,
135
+ testPassed: record.testPassed,
136
+ filesChanged: record.filesChanged,
137
+ });
138
+ return { record, worktreePath };
139
+ });
140
+
141
+ // Sequential integration: only units that DID change, committed, and did not
142
+ // fail tests are eligible. Preview each; merge clean ones when --merge.
143
+ for (const { record } of results) {
144
+ if (!record.committed || record.status === "test-failed") {
145
+ record.integration = { eligible: false, reason: record.status };
146
+ continue;
147
+ }
148
+ let preview;
149
+ try {
150
+ preview = previewMerge(record.branch);
151
+ } catch (err) {
152
+ record.integration = {
153
+ eligible: true,
154
+ clean: false,
155
+ merged: false,
156
+ conflicts: [],
157
+ error: err.message,
158
+ };
159
+ continue;
160
+ }
161
+ const clean = preview?.success === true;
162
+ const conflicts = preview?.conflicts || [];
163
+ let merged = false;
164
+ if (clean && opts.merge) {
165
+ let mergeResult;
166
+ try {
167
+ mergeResult = mergeBranch(record.branch, `Merge batch ${record.key}`);
168
+ } catch (err) {
169
+ mergeResult = { success: false, message: err.message };
170
+ }
171
+ merged = mergeResult?.success === true;
172
+ if (!merged && (!conflicts || conflicts.length === 0)) {
173
+ record.integration = {
174
+ eligible: true,
175
+ clean,
176
+ merged,
177
+ conflicts: mergeResult?.conflicts || [],
178
+ error: mergeResult?.message || null,
179
+ };
180
+ continue;
181
+ }
182
+ }
183
+ record.integration = { eligible: true, clean, merged, conflicts };
184
+ onEvent({
185
+ type: "unit:integrated",
186
+ key: record.key,
187
+ clean,
188
+ merged,
189
+ conflicts: conflicts.length,
190
+ });
191
+ }
192
+
193
+ // Best-effort teardown of every worktree we created (branches persist for the
194
+ // user to inspect/merge later unless already merged).
195
+ if (typeof removeWorktree === "function") {
196
+ for (const { record, worktreePath } of results) {
197
+ if (!worktreePath) continue;
198
+ try {
199
+ removeWorktree(worktreePath, {
200
+ deleteBranch: record.integration?.merged === true,
201
+ });
202
+ } catch {
203
+ // best-effort — a leftover worktree is recoverable via `git worktree`
204
+ }
205
+ }
206
+ }
207
+
208
+ const records = results.map((r) => r.record);
209
+ const summary = summarize(records);
210
+ onEvent({ type: "batch:done", ...summary });
211
+ return { units: records, summary };
212
+ }
213
+
214
+ function summarize(records) {
215
+ const summary = {
216
+ total: records.length,
217
+ done: 0,
218
+ testFailed: 0,
219
+ noChanges: 0,
220
+ errored: 0,
221
+ merged: 0,
222
+ conflicted: 0,
223
+ };
224
+ for (const r of records) {
225
+ if (r.status === "done") summary.done += 1;
226
+ else if (r.status === "test-failed") summary.testFailed += 1;
227
+ else if (r.status === "no-changes") summary.noChanges += 1;
228
+ else if (r.status === "error") summary.errored += 1;
229
+ if (r.integration?.merged) summary.merged += 1;
230
+ if (r.integration?.conflicts?.length) summary.conflicted += 1;
231
+ }
232
+ return summary;
233
+ }
234
+
235
+ export const _internal = { mapPool, normalizeUnits, summarize };