chainlesschain 0.162.31 → 0.162.32

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 (165) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-BqWP6FKu.js → AIOps-Cg_uWAVl.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-CXwMgOvX.js → ActionButton-DSFtQ1c2.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DAebZ4IY.js → Analytics-BMxpkw8y.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-CYsqYoME.js → AppLayout-tgVxlmsx.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-BbTtX1Nf.js → Audit-DwzGllcp.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-DgqY2Eb-.js → Backup-BG28Y2MV.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-Cq2ZuSoO.js → BaseInput-TXthbazl.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-D2kqpUyO.js → Chat-D096SxaD.js} +5 -5
  10. package/src/assets/web-panel/assets/ChatBubbleRenderer-PIx0Eu9I.js +1 -0
  11. package/src/assets/web-panel/assets/{Checkbox-_9swHpyc.js → Checkbox-Czttw1JS.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-Cr9YbCPl.js → Codegen-DZtMgv4q.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col--wdpCMxx.js → Col-D3DnfExY.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DuFcVnLu.js → Community-Bj5AdwqY.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-1yzYeT04.js → Compact-BQ8Zszub.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-Dq3aU9Df.js → Compliance-DXacb34n.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-CrWcnIg8.js → Cowork-BgMUBTkw.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron-Bh6fKZ0h.js → Cron-fqBWOqlN.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-8ofPaWVW.js → Crosschain-E4oa1MWy.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-D3EiYm3w.js → DID-pwgfYZaV.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-BFjEdFne.js → Dashboard-n8mdLFIR.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-pYVPcP6O.js → Dropdown--6DYqxk7.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-zBPodwJ1.js → EmailListRenderer-CkjQluz3.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CyQTW6PW.js → FamilyGuardDashboard-u-QTQ-OC.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-Ctaq3zYq.js → Federation-D219M5Qc.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-CWYJCLq1.js → FormItemContext-BBU_aopC.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-B1g6t9R9.js → GenericCardRenderer-pTMCIHcM.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-DH-v8iwd.js → Git-ClcCARWt.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-jZxXvOs5.js → Governance-CvUi3I93.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-D07LRghn.js → Inference-DT-a4pVg.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-DnGtRZhx.js → KnowledgeGraph-DHMs2LY8.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-D2pM9C4W.js → Logs-D2s4eV1N.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-UyIO7C7r.js → Marketplace-YC5-fx-6.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-Bf1gvZPf.js → McpTools-7JHTEC4T.js} +3 -3
  35. package/src/assets/web-panel/assets/{Memory-C1bWj4RN.js → Memory-BudotVLD.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-C_Ot1H_a.js → MobileBridge-CAiRyLVU.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileProjects-zr-PpsT_.js → MobileProjects-CrJJOCFw.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-CnzFUz5J.js → Mtc-d0iY0CeK.js} +5 -5
  39. package/src/assets/web-panel/assets/{MtcAudit-CAAh99wz.js → MtcAudit-aI2cG1UP.js} +4 -4
  40. package/src/assets/web-panel/assets/{Multisig-D6IAg6HE.js → Multisig-4bF70khG.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BFMarxb0.js → NLProgramming-CwLib1S7.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-BRp9ro3t.js → Notes-Wt7AuFRU.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-C0Au3Cxb.js → NotificationSettings-D081vV_7.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-ISp6btRY.js → OrderTableRenderer-DCPei1L9.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-DYoxLBRX.js → Organization-BNEsUNdP.js} +2 -2
  46. package/src/assets/web-panel/assets/{Overflow-rO8JJWGJ.js → Overflow-B_1iUXDD.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-DJleeXIK.js → P2P-Dbc-kNwJ.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-DM5qghFp.js → PdhVaultBrowser-D8Xh289k.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-D5v4Beya.js → Permissions-C77mM6-n.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-c2ZTX0Pv.js → PersonalDataHub-Dj0J3r_K.js} +3 -3
  51. package/src/assets/web-panel/assets/{Pipeline-Crrkyhpz.js → Pipeline-B6F0WQ2C.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-DZVyrJKa.js → Privacy-eDKOkyyq.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-DKg7J0gz.js → ProjectInit-DAWwhr5_.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-3ndmTvVH.js → ProjectSettings-DwdK8k6I.js} +2 -2
  55. package/src/assets/web-panel/assets/Projects-Cb3p5QAP.js +1 -0
  56. package/src/assets/web-panel/assets/{Providers-BeqBVMhB.js → Providers--DcYxQfN.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-DKAAxzuA.js → QuickAsk-DU268niT.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-Byu7IGei.js → Recommend-ChnflhV1.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-BKhWAmCu.js → Reputation-DSsY3bQG.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-BFtn11O6.js → Row-Zb-EjmgQ.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-D5a0PT0k.js → RssFeed-CGLiixZB.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-DAkuaZNe.js → Search-Dhr_po-U.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-C79Ml2Ms.js → Security-GMYNhGsR.js} +2 -2
  64. package/src/assets/web-panel/assets/{Services-BBk_jH6-.js → Services-DiOpnVY0.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-Cy0VvL0M.js → Skeleton-DG3ez6ME.js} +1 -1
  66. package/src/assets/web-panel/assets/Skills-DZGptytP.js +1 -0
  67. package/src/assets/web-panel/assets/{Sla-CbX1f8xN.js → Sla-CtGpE3xA.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-BIkoUjws.js → SpeechSettings-DQFw6Cf9.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-DG6Swk7G.js → SyncSettings-C8X78RpX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-C9R8sgyi.js → Tasks-DtVkhWCV.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-AaJPeCIz.js → Templates-SF9_ZWsV.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-jVFRofww.js → Tenant-BbIQSVZz.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-DHBMzfK6.js → Terminal-DKr5zDwu.js} +2 -2
  74. package/src/assets/web-panel/assets/{TimelineRenderer-9RFfOHSI.js → TimelineRenderer-BtLaNaWr.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-ZTfwuABF.js → Tokens-CfYbk2NG.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-Xo7uZNQs.js → Trigger-BLX_XDP0.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-C0cTPYvn.js → Trust-BWxUv9PR.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-DmMKio71.js → UkeySign-DRwTyQD4.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-DP7B-oGT.js → VideoEditing-BsC4VOSo.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-B1kZDARo.js → Wallet-CSsO1NJU.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-Bo5kBx27.js → WebAuthn-z1MxiFzS.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-DGI9SNHH.js → WorkflowEditor-B1vV7uuJ.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-y97W1CIG.js → chat-C0NJRaL2.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-DtTNo0sH.js → colors-CHRiteWF.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-D0q0exuS.js → compact-item-2XmBBKPD.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-D7pLFs2I.js → createContext-DkedHC38.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-DmNpkOdC.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-CXjG5B2j.js → hasIn-Bpn9Xrlw.js} +1 -1
  89. package/src/assets/web-panel/assets/index-7nAysteg.js +1 -0
  90. package/src/assets/web-panel/assets/{index-B3y_4OdG.js → index-B5NGWgHp.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BJUf19Wd.js → index-BItcSqan.js} +3 -3
  92. package/src/assets/web-panel/assets/index-BKWSQilQ.js +1 -0
  93. package/src/assets/web-panel/assets/{index-POaFzYGS.js → index-BN068mCR.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CSdhC7Qo.js → index-BOsIgPge.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-4mWZhCzz.js → index-BYUd69vM.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-_3wPBMKt.js → index-BYmwEaIk.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-B4dPdrvC.js → index-BZ1gOoiG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B_hjkMtX.js → index-BfY9U3X5.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Dr45Nm9V.js → index-BveL_4n3.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BgmvrPJH.js → index-CCg6ZY4t.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-gFLQe31v.js → index-CJOoo72F.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DY6KLlgG.js → index-CToQxpWz.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CkGFqlYX.js → index-CWgWrrWs.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BO644Q4S.js → index-CdR7RfRP.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-kvV0f4tV.js → index-Cljnfuxu.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BU944DeT.js → index-CxvA72CP.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CKrbutAQ.js → index-CyJpmSHZ.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-qoB3whR9.js → index-D7U411hK.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Cbqu804A.js → index-D9mNfpxi.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DjCawXk1.js → index-DAFLFMXQ.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BdhEYW2a.js → index-DAeHmElB.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-EaIfumgW.js → index-DDy_RDjs.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BzCPx1cq.js → index-DE5Qm9UI.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BgyrM0UN.js → index-DM9JrnYi.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-8jxbZupG.js → index-DMbF-Euw.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-1dwtkcJv.js → index-DUBsq_1G.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-TrBGgrwG.js → index-De49R7TX.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-aarO4HT9.js → index-De5vOO9V.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BnLrbXDA.js → index-Dk7P-q3n.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-YWOEx3rP.js → index-DryKGM_t.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BPXhU-jp.js → index-DtU4qZRF.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CFsPe2N7.js → index-NuBsCRaR.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-6np5ESBM.js → index-Sk3-3tKa.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-bVJvqDAz.js → index-alGjpoM1.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Ct6xtKkc.js → index-cfSUlOfY.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D_4WcI1V.js → index-i4W_EAuh.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BqVjUN8b.js → index-uHGxyZtQ.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-BnXISaAa.js → initDefaultProps-DlDE-QgI.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-ChY7C0zJ.js → motion-CodUbIRF.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-ByFZMFM5.js → move-DaLwsHeR.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-BYeliY1H.js → omit-DdVg-3rL.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-B9dcAKnu.js → pickAttrs-KLR1EVCo.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-D3F_txz7.js → placementArrow-ChV7HvNw.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-ClkwY7wS.js → responsiveObserve-BB_A8dBt.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-BNgy2Eea.js → slide-Bc1tQnIK.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-Bv3heMCD.js → statusUtils-CgrveSb0.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-DVdlHbQm.js → styleChecker-vXAYhhjz.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-alrRY5BK.js → useFlexGapSupport-BCIMPfq9.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-CcVh0-Vu.js → useFs-DMZGdr6G.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-CkkHPhyq.js → usePersonalDataHub-118tWI_Z.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-DWi0X9WN.js → vnode-Z7O2Y7JP.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-DCbqxxLH.js → zoom-BXym6zmD.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/agent.js +27 -0
  145. package/src/commands/checkpoint.js +253 -53
  146. package/src/commands/compact.js +150 -0
  147. package/src/commands/goal.js +417 -0
  148. package/src/commands/hub.js +7 -0
  149. package/src/harness/prompt-compressor.js +71 -1
  150. package/src/index.js +4 -0
  151. package/src/lib/agent-core.js +1 -0
  152. package/src/lib/checkpoint-store.js +523 -0
  153. package/src/lib/goal-context.js +87 -0
  154. package/src/lib/goal-store.js +308 -0
  155. package/src/repl/agent-repl.js +43 -7
  156. package/src/runtime/agent-core.js +245 -1
  157. package/src/runtime/headless-runner.js +25 -0
  158. package/src/runtime/headless-stream.js +13 -0
  159. package/src/runtime/policies/agent-policy.js +1 -0
  160. package/src/assets/web-panel/assets/ChatBubbleRenderer-C-svYkrC.js +0 -1
  161. package/src/assets/web-panel/assets/Projects-ll5wnj2L.js +0 -1
  162. package/src/assets/web-panel/assets/Skills-OQNky3uI.js +0 -1
  163. package/src/assets/web-panel/assets/devWarning-BDK34w0I.js +0 -1
  164. package/src/assets/web-panel/assets/index-B6SaRuCI.js +0 -1
  165. package/src/assets/web-panel/assets/index-B9ekWb3I.js +0 -1
@@ -0,0 +1,308 @@
1
+ /**
2
+ * goal-store — cross-session persistent goals / OKRs for `cc goal`.
3
+ *
4
+ * Unlike a session (short-lived context) or a checkpoint (file state), a goal
5
+ * is a long-lived objective the agent should advance toward across many
6
+ * sessions. This is the standalone store + ops; wiring the goal into the agent
7
+ * loop (so each turn is measured against it) lives in goal-context.js.
8
+ *
9
+ * On-disk layout (under <home>/goals, overridable via opts.root for tests):
10
+ * <root>/<id>.json one goal per file
11
+ *
12
+ * Distinct from:
13
+ * - cc session (short-term conversation context)
14
+ * - cc memory (durable facts, not objectives)
15
+ * - cc planmode (a single run's plan, not a cross-session objective)
16
+ * - cc workflow (execution orchestration, not intent)
17
+ */
18
+
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import { getHomeDir } from "./paths.js";
22
+
23
+ /** Valid goal lifecycle states. `active` goals are the ones injected. */
24
+ export const GOAL_STATUS = Object.freeze({
25
+ ACTIVE: "active",
26
+ PAUSED: "paused",
27
+ DONE: "done",
28
+ ABANDONED: "abandoned",
29
+ });
30
+
31
+ const STATUS_VALUES = new Set(Object.values(GOAL_STATUS));
32
+
33
+ function defaultRoot() {
34
+ return path.join(getHomeDir(), "goals");
35
+ }
36
+
37
+ function ensureDir(dir) {
38
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+
41
+ function newId(prefix) {
42
+ // Date.now/random are fine here (plain CLI lib, not a resumable workflow).
43
+ const rand = Math.random().toString(36).slice(2, 8);
44
+ return `${prefix}-${Date.now()}-${rand}`;
45
+ }
46
+
47
+ function nowIso() {
48
+ return new Date().toISOString();
49
+ }
50
+
51
+ function goalFile(root, id) {
52
+ return path.join(root, `${id}.json`);
53
+ }
54
+
55
+ /** Clamp a value to a 0–100 integer percentage, or null when not a number. */
56
+ function clampPct(v) {
57
+ if (v == null || v === "") return null;
58
+ const n = Math.round(Number(v));
59
+ if (!Number.isFinite(n)) return null;
60
+ return Math.max(0, Math.min(100, n));
61
+ }
62
+
63
+ /**
64
+ * Derive a 0–100 progress from completed key results. Returns null when there
65
+ * are no key results (caller may keep a manually-set progress instead).
66
+ */
67
+ function derivedProgress(keyResults) {
68
+ if (!Array.isArray(keyResults) || keyResults.length === 0) return null;
69
+ const done = keyResults.filter((k) => k.done).length;
70
+ return Math.round((done / keyResults.length) * 100);
71
+ }
72
+
73
+ /**
74
+ * Create a goal.
75
+ * @param {object} input { objective, title, keyResults?: string[]|object[] }
76
+ * @param {object} [opts] { root }
77
+ * @returns {object} the persisted goal
78
+ */
79
+ export function createGoal(input = {}, opts = {}) {
80
+ const root = opts.root || defaultRoot();
81
+ const objective = String(input.objective || "").trim();
82
+ if (!objective) {
83
+ throw new Error("createGoal requires an objective");
84
+ }
85
+ const id = input.id || newId("goal");
86
+ const keyResults = (input.keyResults || []).map((kr) => normalizeKr(kr));
87
+ const goal = {
88
+ id,
89
+ title: String(input.title || objective).trim(),
90
+ objective,
91
+ keyResults,
92
+ status: GOAL_STATUS.ACTIVE,
93
+ progress: derivedProgress(keyResults) ?? 0,
94
+ linkedSessions: [],
95
+ notes: [],
96
+ drift: { lastProgressAt: null, flags: [] },
97
+ createdAt: nowIso(),
98
+ updatedAt: nowIso(),
99
+ };
100
+ ensureDir(root);
101
+ fs.writeFileSync(goalFile(root, id), JSON.stringify(goal, null, 2), "utf-8");
102
+ return goal;
103
+ }
104
+
105
+ function normalizeKr(kr) {
106
+ if (typeof kr === "string") {
107
+ return {
108
+ id: newId("kr"),
109
+ text: kr.trim(),
110
+ target: null,
111
+ current: 0,
112
+ done: false,
113
+ };
114
+ }
115
+ return {
116
+ id: kr.id || newId("kr"),
117
+ text: String(kr.text || "").trim(),
118
+ target: kr.target == null ? null : Number(kr.target),
119
+ current: Number(kr.current) || 0,
120
+ done: !!kr.done,
121
+ };
122
+ }
123
+
124
+ /** Load a goal by id, or null. */
125
+ export function getGoal(id, opts = {}) {
126
+ const root = opts.root || defaultRoot();
127
+ const file = goalFile(root, id);
128
+ if (!fs.existsSync(file)) return null;
129
+ try {
130
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ function saveGoal(goal, opts = {}) {
137
+ const root = opts.root || defaultRoot();
138
+ ensureDir(root);
139
+ goal.updatedAt = nowIso();
140
+ fs.writeFileSync(
141
+ goalFile(root, goal.id),
142
+ JSON.stringify(goal, null, 2),
143
+ "utf-8",
144
+ );
145
+ return goal;
146
+ }
147
+
148
+ /** List goals, newest first. Optionally filter by status. */
149
+ export function listGoals(opts = {}) {
150
+ const root = opts.root || defaultRoot();
151
+ if (!fs.existsSync(root)) return [];
152
+ const out = [];
153
+ for (const name of fs.readdirSync(root)) {
154
+ if (!name.endsWith(".json")) continue;
155
+ const g = getGoal(name.slice(0, -5), { root });
156
+ if (!g) continue;
157
+ if (opts.status && g.status !== opts.status) continue;
158
+ out.push(g);
159
+ }
160
+ return out.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
161
+ }
162
+
163
+ /** Mutate a goal via `fn(goal)`; throws if not found. */
164
+ function mutate(id, fn, opts = {}) {
165
+ const goal = getGoal(id, opts);
166
+ if (!goal) throw new Error(`no such goal: ${id}`);
167
+ fn(goal);
168
+ return saveGoal(goal, opts);
169
+ }
170
+
171
+ /** Add a key result to a goal. */
172
+ export function addKeyResult(id, text, krOpts = {}, opts = {}) {
173
+ return mutate(
174
+ id,
175
+ (g) => {
176
+ g.keyResults.push(normalizeKr({ text, target: krOpts.target }));
177
+ const dp = derivedProgress(g.keyResults);
178
+ if (dp != null) g.progress = dp;
179
+ },
180
+ opts,
181
+ );
182
+ }
183
+
184
+ /** Update a key result (current value and/or done flag). */
185
+ export function setKeyResult(id, krId, patch = {}, opts = {}) {
186
+ return mutate(
187
+ id,
188
+ (g) => {
189
+ const kr = g.keyResults.find((k) => k.id === krId);
190
+ if (!kr) throw new Error(`no such key result: ${krId}`);
191
+ if (patch.current != null) kr.current = Number(patch.current);
192
+ if (patch.done != null) kr.done = !!patch.done;
193
+ if (
194
+ kr.target != null &&
195
+ patch.current != null &&
196
+ Number(patch.current) >= kr.target
197
+ ) {
198
+ kr.done = true;
199
+ }
200
+ const dp = derivedProgress(g.keyResults);
201
+ if (dp != null) g.progress = dp;
202
+ g.drift.lastProgressAt = nowIso();
203
+ },
204
+ opts,
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Record progress: set an explicit percentage and/or append a note.
210
+ * @param {object} input { pct?, note?, by? }
211
+ */
212
+ export function recordProgress(id, input = {}, opts = {}) {
213
+ return mutate(
214
+ id,
215
+ (g) => {
216
+ const pct = clampPct(input.pct);
217
+ if (pct != null) g.progress = pct;
218
+ if (input.note) {
219
+ g.notes.push({
220
+ at: nowIso(),
221
+ text: String(input.note),
222
+ by: input.by === "agent" ? "agent" : "user",
223
+ });
224
+ }
225
+ g.drift.lastProgressAt = nowIso();
226
+ },
227
+ opts,
228
+ );
229
+ }
230
+
231
+ /** Attach a session id to a goal (idempotent). */
232
+ export function linkSession(id, sessionId, opts = {}) {
233
+ if (!sessionId) throw new Error("linkSession requires a sessionId");
234
+ return mutate(
235
+ id,
236
+ (g) => {
237
+ if (!g.linkedSessions.includes(sessionId)) {
238
+ g.linkedSessions.push(sessionId);
239
+ }
240
+ },
241
+ opts,
242
+ );
243
+ }
244
+
245
+ /** Detach a session id from a goal. */
246
+ export function unlinkSession(id, sessionId, opts = {}) {
247
+ return mutate(
248
+ id,
249
+ (g) => {
250
+ g.linkedSessions = g.linkedSessions.filter((s) => s !== sessionId);
251
+ },
252
+ opts,
253
+ );
254
+ }
255
+
256
+ /** Set a goal's status (active/paused/done/abandoned). */
257
+ export function setStatus(id, status, opts = {}) {
258
+ if (!STATUS_VALUES.has(status)) {
259
+ throw new Error(
260
+ `invalid status "${status}" — expected one of: ${[...STATUS_VALUES].join(", ")}`,
261
+ );
262
+ }
263
+ return mutate(
264
+ id,
265
+ (g) => {
266
+ g.status = status;
267
+ if (status === GOAL_STATUS.DONE && g.progress < 100) g.progress = 100;
268
+ },
269
+ opts,
270
+ );
271
+ }
272
+
273
+ /** Delete a goal. Returns true if it existed. */
274
+ export function deleteGoal(id, opts = {}) {
275
+ const root = opts.root || defaultRoot();
276
+ const file = goalFile(root, id);
277
+ if (!fs.existsSync(file)) return false;
278
+ fs.rmSync(file);
279
+ return true;
280
+ }
281
+
282
+ /**
283
+ * Resolve the goal that should be bound to the current run, in priority order:
284
+ * 1. explicit id (--goal <id>)
285
+ * 2. an active goal linked to the current session
286
+ * 3. when exactly one active goal exists, that one
287
+ * 4. null
288
+ *
289
+ * Only `active` goals are ever auto-resolved (steps 2–3); an explicit id is
290
+ * honored regardless of status so a user can re-inspect a paused goal.
291
+ *
292
+ * @param {object} [sel] { explicitId, sessionId }
293
+ * @param {object} [opts] { root }
294
+ */
295
+ export function resolveActiveGoal(sel = {}, opts = {}) {
296
+ if (sel.explicitId) {
297
+ return getGoal(sel.explicitId, opts);
298
+ }
299
+ const active = listGoals({ ...opts, status: GOAL_STATUS.ACTIVE });
300
+ if (active.length === 0) return null;
301
+ if (sel.sessionId) {
302
+ const linked = active.find((g) => g.linkedSessions.includes(sel.sessionId));
303
+ if (linked) return linked;
304
+ }
305
+ if (active.length === 1) return active[0];
306
+ // Ambiguous (multiple active, none linked) — caller must pick explicitly.
307
+ return null;
308
+ }
@@ -92,8 +92,18 @@ async function executeTool(name, args) {
92
92
  */
93
93
  async function agentLoop(messages, options) {
94
94
  const usageEvents = [];
95
- for await (const event of coreAgentLoop(messages, options)) {
96
- if (event.type === "tool-executing") {
95
+ // The REPL runs its own auto-compaction (after each turn, with metrics +
96
+ // persisted compact events), so opt out of the agent loop's in-loop
97
+ // compaction to avoid compacting the same history twice.
98
+ for await (const event of coreAgentLoop(messages, {
99
+ autoCompact: false,
100
+ ...options,
101
+ })) {
102
+ if (event.type === "checkpoint") {
103
+ process.stdout.write(
104
+ chalk.gray(` ⎌ checkpoint ${event.id} (before ${event.tool})\n`),
105
+ );
106
+ } else if (event.type === "tool-executing") {
97
107
  process.stdout.write(
98
108
  chalk.gray(
99
109
  ` [${event.tool}] ${formatToolArgs(event.tool, event.args)}\n`,
@@ -158,6 +168,9 @@ export async function startAgentRepl(options = {}) {
158
168
  const additionalDirectories = Array.isArray(options.additionalDirectories)
159
169
  ? options.additionalDirectories
160
170
  : [];
171
+ // Snapshot the work tree before each mutating tool (git engine) so the user
172
+ // can `cc checkpoint restore` to just before any tool call.
173
+ const autoCheckpoint = options.autoCheckpoint === true;
161
174
 
162
175
  // --fallback-model: retry a turn's LLM call once on a backup model when the
163
176
  // primary errors out (overload / network). Built once; passed into every
@@ -718,8 +731,10 @@ export async function startAgentRepl(options = {}) {
718
731
 
719
732
  if (trimmed === "/compact") {
720
733
  if (_compressor && messages.length > 3) {
721
- const { messages: compacted, stats } =
722
- await _compressor.compress(messages);
734
+ const { messages: compacted, stats } = await _compressor.compress(
735
+ messages,
736
+ { preserveToolPairs: true },
737
+ );
723
738
  messages.length = 0;
724
739
  messages.push(...compacted);
725
740
  recordCompressionMetric(stats, {
@@ -1521,6 +1536,23 @@ export async function startAgentRepl(options = {}) {
1521
1536
  try {
1522
1537
  process.stdout.write("\n");
1523
1538
  const iterationBudget = new IterationBudget({ owner: sessionId });
1539
+ // Bind a cross-session goal (cc goal) into this run, if one resolves.
1540
+ // Composes WITH defaultPrepareCall — never replaces it. Best-effort.
1541
+ let prepareCall = defaultPrepareCall;
1542
+ try {
1543
+ const { resolveActiveGoal } = await import("../lib/goal-store.js");
1544
+ const boundGoal = resolveActiveGoal({ sessionId });
1545
+ if (boundGoal) {
1546
+ const { goalPrepareCall, composePrepareCall } =
1547
+ await import("../lib/goal-context.js");
1548
+ prepareCall = composePrepareCall([
1549
+ defaultPrepareCall,
1550
+ goalPrepareCall(boundGoal),
1551
+ ]);
1552
+ }
1553
+ } catch (_e) {
1554
+ /* goal binding is best-effort — fall back to defaultPrepareCall */
1555
+ }
1524
1556
  const { content: response, usageEvents } = await agentLoop(messages, {
1525
1557
  provider,
1526
1558
  model: activeModel,
@@ -1531,7 +1563,9 @@ export async function startAgentRepl(options = {}) {
1531
1563
  sessionId,
1532
1564
  cwd: process.cwd(),
1533
1565
  additionalDirectories,
1534
- prepareCall: defaultPrepareCall,
1566
+ autoCheckpoint,
1567
+ checkpointSession: sessionId,
1568
+ prepareCall,
1535
1569
  approvalGate: _approvalGate,
1536
1570
  mcpClient: _bundleMcpClient || undefined,
1537
1571
  chatFn: _fallbackChatFn,
@@ -1613,8 +1647,10 @@ export async function startAgentRepl(options = {}) {
1613
1647
  _compressor.shouldAutoCompact(messages)
1614
1648
  ) {
1615
1649
  try {
1616
- const { messages: compacted, stats } =
1617
- await _compressor.compress(messages);
1650
+ const { messages: compacted, stats } = await _compressor.compress(
1651
+ messages,
1652
+ { preserveToolPairs: true },
1653
+ );
1618
1654
  messages.length = 0;
1619
1655
  messages.push(...compacted);
1620
1656
  recordCompressionMetric(stats, {
@@ -1663,7 +1663,21 @@ export async function chatWithTools(rawMessages, options) {
1663
1663
  throwIfAborted(signal);
1664
1664
 
1665
1665
  if (provider === "ollama") {
1666
- const response = await fetch(`${baseUrl}/api/chat`, {
1666
+ const apiUrl = `${baseUrl}/api/chat`;
1667
+ // Real-time token deltas (Claude-Code `--include-partial-messages`): when
1668
+ // the caller supplies an onToken hook, stream the response and forward each
1669
+ // content chunk as it arrives. Tool calls + usage are accumulated and the
1670
+ // same {message, usage} shape is returned, so the agent loop is unchanged.
1671
+ // Without onToken we keep the cheaper single-shot non-streaming request.
1672
+ if (typeof options.onToken === "function") {
1673
+ return await _chatOllamaStreaming(
1674
+ apiUrl,
1675
+ { model, messages, tools },
1676
+ options.onToken,
1677
+ signal,
1678
+ );
1679
+ }
1680
+ const response = await fetch(apiUrl, {
1667
1681
  method: "POST",
1668
1682
  headers: { "Content-Type": "application/json" },
1669
1683
  signal,
@@ -1817,6 +1831,109 @@ export async function chatWithTools(rawMessages, options) {
1817
1831
  return out;
1818
1832
  }
1819
1833
 
1834
+ // ─── Ollama streaming (token deltas for --include-partial-messages) ─────────
1835
+ //
1836
+ // Ollama `/api/chat` with `stream:true` returns NDJSON: one JSON object per
1837
+ // line, each carrying an incremental `message.content` chunk, optional
1838
+ // `message.tool_calls` (emitted whole, not byte-streamed), and a final line
1839
+ // with `done:true` + `prompt_eval_count`/`eval_count` token totals. We reduce
1840
+ // the stream line-by-line so onToken fires live, then finalize into the same
1841
+ // {message, usage} shape the non-streaming branch returns.
1842
+
1843
+ function _ollamaInitState() {
1844
+ return {
1845
+ role: "assistant",
1846
+ content: "",
1847
+ toolCalls: null,
1848
+ promptEval: 0,
1849
+ evalCount: 0,
1850
+ };
1851
+ }
1852
+
1853
+ function _ollamaReduceLine(state, line, onToken) {
1854
+ const s = (line || "").trim();
1855
+ if (!s) return state;
1856
+ let obj;
1857
+ try {
1858
+ obj = JSON.parse(s);
1859
+ } catch {
1860
+ return state; // tolerate partial/garbage lines mid-stream
1861
+ }
1862
+ const msg = obj.message;
1863
+ if (msg) {
1864
+ if (msg.role) state.role = msg.role;
1865
+ if (typeof msg.content === "string" && msg.content) {
1866
+ state.content += msg.content;
1867
+ if (typeof onToken === "function") {
1868
+ try {
1869
+ onToken(msg.content);
1870
+ } catch {
1871
+ // A failing UI hook must never break the agent run.
1872
+ }
1873
+ }
1874
+ }
1875
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length) {
1876
+ state.toolCalls = (state.toolCalls || []).concat(msg.tool_calls);
1877
+ }
1878
+ }
1879
+ if (obj.prompt_eval_count) state.promptEval = obj.prompt_eval_count;
1880
+ if (obj.eval_count) state.evalCount = obj.eval_count;
1881
+ return state;
1882
+ }
1883
+
1884
+ function _ollamaFinalize(state) {
1885
+ const message = { role: state.role, content: state.content };
1886
+ if (state.toolCalls && state.toolCalls.length) {
1887
+ message.tool_calls = state.toolCalls;
1888
+ }
1889
+ const data = { message };
1890
+ if (state.promptEval || state.evalCount) {
1891
+ data.usage = {
1892
+ input_tokens: state.promptEval,
1893
+ output_tokens: state.evalCount,
1894
+ };
1895
+ }
1896
+ return data;
1897
+ }
1898
+
1899
+ /**
1900
+ * Pure reducer over an iterable of Ollama NDJSON lines. Exported for tests so
1901
+ * the parse/accumulate logic can be exercised without a live HTTP stream.
1902
+ */
1903
+ export function _accumulateOllamaStream(lines, onToken) {
1904
+ const state = _ollamaInitState();
1905
+ for (const line of lines) _ollamaReduceLine(state, line, onToken);
1906
+ return _ollamaFinalize(state);
1907
+ }
1908
+
1909
+ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
1910
+ const response = await fetch(apiUrl, {
1911
+ method: "POST",
1912
+ headers: { "Content-Type": "application/json" },
1913
+ signal,
1914
+ body: JSON.stringify({ ...body, stream: true }),
1915
+ });
1916
+ if (!response.ok) {
1917
+ throw new Error(`Ollama error: ${response.status}`);
1918
+ }
1919
+ const state = _ollamaInitState();
1920
+ const reader = response.body.getReader();
1921
+ const decoder = new TextDecoder();
1922
+ let buf = "";
1923
+ for (;;) {
1924
+ const { done, value } = await reader.read();
1925
+ if (done) break;
1926
+ buf += decoder.decode(value, { stream: true });
1927
+ let idx;
1928
+ while ((idx = buf.indexOf("\n")) >= 0) {
1929
+ _ollamaReduceLine(state, buf.slice(0, idx), onToken);
1930
+ buf = buf.slice(idx + 1);
1931
+ }
1932
+ }
1933
+ if (buf.trim()) _ollamaReduceLine(state, buf, onToken);
1934
+ return _ollamaFinalize(state);
1935
+ }
1936
+
1820
1937
  function _normalizeAnthropicResponse(data) {
1821
1938
  const content = data.content || [];
1822
1939
  const textBlocks = content.filter((b) => b.type === "text");
@@ -1843,11 +1960,58 @@ function _normalizeAnthropicResponse(data) {
1843
1960
 
1844
1961
  // ─── Agent loop (async generator) ─────────────────────────────────────────
1845
1962
 
1963
+ // Tools that never mutate the workspace — auto-checkpoint skips these.
1964
+ const _CHECKPOINT_READ_ONLY = new Set([
1965
+ "read_file",
1966
+ "search_files",
1967
+ "list_dir",
1968
+ "list_skills",
1969
+ "search_sessions",
1970
+ ]);
1971
+
1972
+ let _checkpointStoreP = null;
1973
+ function _loadCheckpointStore() {
1974
+ if (!_checkpointStoreP) {
1975
+ _checkpointStoreP = import("../lib/checkpoint-store.js");
1976
+ }
1977
+ return _checkpointStoreP;
1978
+ }
1979
+
1980
+ /**
1981
+ * Best-effort auto-checkpoint of the working tree BEFORE a mutating tool runs,
1982
+ * so a later `cc checkpoint restore` can roll back to just before that tool.
1983
+ * Enabled via toolContext.autoCheckpoint; uses the git engine only (no-op
1984
+ * outside a git work tree). Never throws — checkpointing must not block a tool.
1985
+ *
1986
+ * @returns {Promise<string|null>} the checkpoint id, or null when skipped
1987
+ */
1988
+ async function _autoCheckpointBeforeTool(toolContext, toolName, toolArgs) {
1989
+ if (!toolContext?.autoCheckpoint) return null;
1990
+ if (_CHECKPOINT_READ_ONLY.has(toolName)) return null;
1991
+ const cwd = toolContext.cwd || process.cwd();
1992
+ try {
1993
+ const store = await _loadCheckpointStore();
1994
+ if (!store.isCheckpointAvailable(cwd)) return null;
1995
+ const res = store.createCheckpoint(cwd, {
1996
+ session: toolContext.checkpointSession || "agent",
1997
+ label: `before ${toolName}: ${formatToolArgs(toolName, toolArgs)}`.slice(
1998
+ 0,
1999
+ 120,
2000
+ ),
2001
+ skipIfUnchanged: true,
2002
+ });
2003
+ return res?.id || null;
2004
+ } catch {
2005
+ return null; // checkpoint failure must never block the tool
2006
+ }
2007
+ }
2008
+
1846
2009
  /**
1847
2010
  * Async generator that drives the agentic tool-use loop.
1848
2011
  *
1849
2012
  * Yields events:
1850
2013
  * { type: "slot-filling", slot, question } — when asking user for missing info
2014
+ * { type: "checkpoint", id, tool } — auto-checkpoint before a mutating tool
1851
2015
  * { type: "tool-executing", tool, args }
1852
2016
  * { type: "tool-result", tool, result, error }
1853
2017
  * { type: "response-complete", content }
@@ -1855,6 +2019,38 @@ function _normalizeAnthropicResponse(data) {
1855
2019
  * @param {Array} messages - mutable messages array (will be appended to)
1856
2020
  * @param {object} options - provider, model, baseUrl, apiKey, contextEngine, hookDb, skillLoader, cwd, slotFiller, interaction
1857
2021
  */
2022
+ /**
2023
+ * Lazily build (and cache on `options`) the PromptCompressor used for in-loop
2024
+ * auto-compaction. Returns null when the feature is off or the module can't be
2025
+ * loaded — callers treat that as "don't compact". Cached (including null) so we
2026
+ * import once per run, not once per iteration.
2027
+ */
2028
+ async function _getAutoCompactor(options) {
2029
+ if (Object.prototype.hasOwnProperty.call(options, "_autoCompactor")) {
2030
+ return options._autoCompactor;
2031
+ }
2032
+ let compressor = null;
2033
+ try {
2034
+ const { feature } = await import("../lib/feature-flags.js");
2035
+ if (feature("PROMPT_COMPRESSOR")) {
2036
+ const { PromptCompressor } =
2037
+ await import("../harness/prompt-compressor.js");
2038
+ compressor = new PromptCompressor({
2039
+ model: options.model,
2040
+ provider: options.provider,
2041
+ });
2042
+ }
2043
+ } catch {
2044
+ compressor = null;
2045
+ }
2046
+ try {
2047
+ options._autoCompactor = compressor;
2048
+ } catch {
2049
+ // options may be frozen — fine, we just re-import next iteration
2050
+ }
2051
+ return compressor;
2052
+ }
2053
+
1858
2054
  export async function* agentLoop(messages, options) {
1859
2055
  // Shared iteration budget — replaces hardcoded MAX_ITERATIONS.
1860
2056
  // When options.iterationBudget is provided (e.g. from parent agent),
@@ -1879,6 +2075,9 @@ export async function* agentLoop(messages, options) {
1879
2075
  approvalGate: options.approvalGate || null,
1880
2076
  shellConfirm: options.shellConfirm || null,
1881
2077
  additionalDirectories: options.additionalDirectories || null,
2078
+ autoCheckpoint: options.autoCheckpoint || false,
2079
+ checkpointSession:
2080
+ options.checkpointSession || options.sessionId || "agent",
1882
2081
  };
1883
2082
 
1884
2083
  throwIfAborted(signal);
@@ -1979,6 +2178,42 @@ export async function* agentLoop(messages, options) {
1979
2178
  };
1980
2179
  }
1981
2180
 
2181
+ // Headless auto-compaction (Claude-Code `--print` parity). Keeps long
2182
+ // `-p` / `--resume` runs under the model's context window instead of
2183
+ // growing until the provider rejects the request. Opt-out with
2184
+ // `autoCompact: false` (the interactive REPL does this — it compacts on its
2185
+ // own schedule). Default-on, gated by the PROMPT_COMPRESSOR flag + a size
2186
+ // threshold inside the compressor, so it only fires for genuinely large
2187
+ // contexts. Safe to compact here: the previous iteration always finishes
2188
+ // its full tool_call→tool_result cycle before we loop, so `messages` has no
2189
+ // dangling call; `preserveToolPairs` then guarantees compaction never
2190
+ // orphans a tool result. Best-effort — a failure never aborts the run.
2191
+ if (options.autoCompact !== false && messages.length > 4) {
2192
+ try {
2193
+ const compactor = await _getAutoCompactor(options);
2194
+ if (compactor && compactor.shouldAutoCompact(messages)) {
2195
+ const { messages: compacted, stats } = await compactor.compress(
2196
+ messages,
2197
+ { preserveToolPairs: true },
2198
+ );
2199
+ if (stats.saved > 0 && compacted.length < messages.length) {
2200
+ messages.splice(0, messages.length, ...compacted);
2201
+ if (typeof options.onCompaction === "function") {
2202
+ try {
2203
+ options.onCompaction(stats, compacted);
2204
+ } catch {
2205
+ // persistence is best-effort
2206
+ }
2207
+ }
2208
+ yield { type: "compaction", stats, runId };
2209
+ }
2210
+ }
2211
+ } catch (_e) {
2212
+ if (isAbortError(_e) || signal?.aborted) throw _e;
2213
+ // Compaction is best-effort — proceed with the uncompacted messages.
2214
+ }
2215
+ }
2216
+
1982
2217
  // Turn-scoped context injection (open-agents prepareCall parity).
1983
2218
  // prepareCall runs fresh each iteration and returns an ephemeral
1984
2219
  // system-message supplement that is NOT persisted to messages history.
@@ -2050,6 +2285,15 @@ export async function* agentLoop(messages, options) {
2050
2285
  toolArgs = {};
2051
2286
  }
2052
2287
 
2288
+ // Auto-checkpoint the work tree before a mutating tool (opt-in), so the
2289
+ // user can `cc checkpoint restore` back to just before this call.
2290
+ const cpId = await _autoCheckpointBeforeTool(
2291
+ toolContext,
2292
+ toolName,
2293
+ toolArgs,
2294
+ );
2295
+ if (cpId) yield { type: "checkpoint", id: cpId, tool: toolName };
2296
+
2053
2297
  yield { type: "tool-executing", tool: toolName, args: toolArgs };
2054
2298
 
2055
2299
  let toolResult;