chainlesschain 0.162.30 → 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.
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-CsNttUU7.js → AIOps-Cg_uWAVl.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lgohjckQ.js → ActionButton-DSFtQ1c2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-ccV3LAca.js → Analytics-BMxpkw8y.js} +3 -3
- package/src/assets/web-panel/assets/AppLayout-tgVxlmsx.js +9 -0
- package/src/assets/web-panel/assets/{Audit-B1gFM5U9.js → Audit-DwzGllcp.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BeWE3ERo.js → Backup-BG28Y2MV.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CDkPsNG2.js → BaseInput-TXthbazl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-ztb9ia6e.js → Chat-D096SxaD.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Dlw_6n3M.js → ChatBubbleRenderer-PIx0Eu9I.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BcfRBlIY.js → Checkbox-Czttw1JS.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DOs99xkr.js → Codegen-DZtMgv4q.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D1X6tYlj.js → Col-D3DnfExY.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DTksIWtz.js → Community-Bj5AdwqY.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DIJtAYBO.js → Compact-BQ8Zszub.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BBf7LF_k.js → Compliance-DXacb34n.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-UBPXQ40s.js → Cowork-BgMUBTkw.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CkRm1jPB.js → Cron-fqBWOqlN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-qALlTl7e.js → Crosschain-E4oa1MWy.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CqyqVS6E.js → DID-pwgfYZaV.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-n8mdLFIR.js +3 -0
- package/src/assets/web-panel/assets/{Dropdown-Cb5UzbSZ.js → Dropdown--6DYqxk7.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CarBq8Fk.js → EmailListRenderer-CkjQluz3.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CSiGXaZz.js → FamilyGuardDashboard-u-QTQ-OC.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DUxhVoBN.js → Federation-D219M5Qc.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BoMQpkhx.js → FormItemContext-BBU_aopC.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DTVqC_CX.js → GenericCardRenderer-pTMCIHcM.js} +1 -1
- package/src/assets/web-panel/assets/{Git-C_XuPtK5.js → Git-ClcCARWt.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BZyqlqz-.js → Governance-CvUi3I93.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DdZVUimI.js → Inference-DT-a4pVg.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-IzZ-jnCn.js → KnowledgeGraph-DHMs2LY8.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-koTK6eNc.js → Logs-D2s4eV1N.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-6zpJ1L8n.js → Marketplace-YC5-fx-6.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Ywc4IVks.js → McpTools-7JHTEC4T.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-C_zB9dUa.js → Memory-BudotVLD.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Nc05r24L.js → MobileBridge-CAiRyLVU.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-BJGxL526.js → MobileProjects-CrJJOCFw.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Im7SIcz1.js → Mtc-d0iY0CeK.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BFFzvzMD.js → MtcAudit-aI2cG1UP.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-CcNEbycq.js → Multisig-4bF70khG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CDH6OTXN.js → NLProgramming-CwLib1S7.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Dqg3QXcU.js → Notes-Wt7AuFRU.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CDVmK1eU.js → NotificationSettings-D081vV_7.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-DCPei1L9.js +1 -0
- package/src/assets/web-panel/assets/{Organization-DJb9bRQS.js → Organization-BNEsUNdP.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CK7Q5dje.js → Overflow-B_1iUXDD.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CJIyYfwc.js → P2P-Dbc-kNwJ.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-uqRULcuw.js → PdhVaultBrowser-D8Xh289k.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-Crvwt6bq.js → Permissions-C77mM6-n.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DcN5OWzg.js → PersonalDataHub-Dj0J3r_K.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DfWJvvJW.js → Pipeline-B6F0WQ2C.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DepD0S3v.js → Privacy-eDKOkyyq.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B7OKhH27.js → ProjectInit-DAWwhr5_.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BJ4ueRFv.js → ProjectSettings-DwdK8k6I.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Cb3p5QAP.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Dl0FT1S3.js → Providers--DcYxQfN.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-V2hYLhfp.js → QuickAsk-DU268niT.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-8Kaiodgv.js → Recommend-ChnflhV1.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CsxB3JGg.js → Reputation-DSsY3bQG.js} +1 -1
- package/src/assets/web-panel/assets/{Row-6-x7tEYq.js → Row-Zb-EjmgQ.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Buv6f5tw.js → RssFeed-CGLiixZB.js} +3 -3
- package/src/assets/web-panel/assets/{Search-ABrDz84n.js → Search-Dhr_po-U.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DqOJmz18.js → Security-GMYNhGsR.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cq4Tda3q.js → Services-DiOpnVY0.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-n74QlyYq.js → Skeleton-DG3ez6ME.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CC0iozL5.js → Skills-DZGptytP.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-hwRgJ99Z.js → Sla-CtGpE3xA.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B6Bs6_-8.js → SpeechSettings-DQFw6Cf9.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CTp2dZ0z.js → SyncSettings-C8X78RpX.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-D70Lis6S.js → Tasks-DtVkhWCV.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Cags0ssw.js → Templates-SF9_ZWsV.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BxCMzzGt.js → Tenant-BbIQSVZz.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-v05SDqHd.js → Terminal-DKr5zDwu.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BLUDHbBL.js → TimelineRenderer-BtLaNaWr.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-D-xKLJYv.js → Tokens-CfYbk2NG.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B47tVIbH.js → Trigger-BLX_XDP0.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DmRU9kfs.js → Trust-BWxUv9PR.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DzgSGs-c.js → UkeySign-DRwTyQD4.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C6qu58up.js → VideoEditing-BsC4VOSo.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dh8ZWx8f.js → Wallet-CSsO1NJU.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DFHOVuAY.js → WebAuthn-z1MxiFzS.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B_fyQ3Y_.js → WorkflowEditor-B1vV7uuJ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BR-WxnCQ.js → chat-C0NJRaL2.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C-6RysQe.js → colors-CHRiteWF.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-B_9_SCKN.js → compact-item-2XmBBKPD.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D6rklIbE.js → createContext-DkedHC38.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DmNpkOdC.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BrotgSvd.js → hasIn-Bpn9Xrlw.js} +1 -1
- package/src/assets/web-panel/assets/index-7nAysteg.js +1 -0
- package/src/assets/web-panel/assets/{index-MCmNzIC7.js → index-B5NGWgHp.js} +1 -1
- package/src/assets/web-panel/assets/{index-GzuCTHVZ.js → index-BItcSqan.js} +3 -3
- package/src/assets/web-panel/assets/index-BKWSQilQ.js +1 -0
- package/src/assets/web-panel/assets/{index-DTCUOKu9.js → index-BN068mCR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bv9BrnD2.js → index-BOsIgPge.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfqUsPl2.js → index-BYUd69vM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn21XmDt.js → index-BYmwEaIk.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWmJukRW.js → index-BZ1gOoiG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bwkg_EJk.js → index-BfY9U3X5.js} +1 -1
- package/src/assets/web-panel/assets/{index-MBOwmoOi.js → index-BveL_4n3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJ70GAW2.js → index-CCg6ZY4t.js} +1 -1
- package/src/assets/web-panel/assets/{index-B85rQNYG.js → index-CJOoo72F.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn5ghmbB.js → index-CToQxpWz.js} +1 -1
- package/src/assets/web-panel/assets/{index-rWiOF7Iu.js → index-CWgWrrWs.js} +1 -1
- package/src/assets/web-panel/assets/{index-PzM_GlKb.js → index-CdR7RfRP.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZehgEQYa.js → index-Cljnfuxu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsDNNDBN.js → index-CxvA72CP.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6KqyxG1.js → index-CyJpmSHZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-E_5VXq8H.js → index-D7U411hK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgp_QFo.js → index-D9mNfpxi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTpElYJs.js → index-DAFLFMXQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMnomft7.js → index-DAeHmElB.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2yXH6vy.js → index-DDy_RDjs.js} +1 -1
- package/src/assets/web-panel/assets/{index-kkjq_hwC.js → index-DE5Qm9UI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTh0fWI4.js → index-DM9JrnYi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DigjvHuo.js → index-DMbF-Euw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DkpDFJRn.js → index-DUBsq_1G.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIiCIC2j.js → index-De49R7TX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsWVDOd2.js → index-De5vOO9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAfRNHna.js → index-Dk7P-q3n.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdDmzoPE.js → index-DryKGM_t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTQkYbir.js → index-DtU4qZRF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CK8YwdNd.js → index-NuBsCRaR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaLhL3Tj.js → index-Sk3-3tKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTpxOc5s.js → index-alGjpoM1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrGp-4E2.js → index-cfSUlOfY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbRl_gIW.js → index-i4W_EAuh.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCWzUY8K.js → index-uHGxyZtQ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C2v_L5na.js → initDefaultProps-DlDE-QgI.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DNDqGbfr.js → motion-CodUbIRF.js} +1 -1
- package/src/assets/web-panel/assets/{move-xvpQ_6hJ.js → move-DaLwsHeR.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Cb0FsfrO.js → omit-DdVg-3rL.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BxhYpnum.js → pickAttrs-KLR1EVCo.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B3soaW4h.js → placementArrow-ChV7HvNw.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B-eRSLvd.js → responsiveObserve-BB_A8dBt.js} +1 -1
- package/src/assets/web-panel/assets/{slide--cM2ZOx-.js → slide-Bc1tQnIK.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DjBhfi8Q.js → statusUtils-CgrveSb0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C30mMh8o.js → styleChecker-vXAYhhjz.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-f7y2Qlzs.js → useFlexGapSupport-BCIMPfq9.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-iTCXoLoZ.js → useFs-DMZGdr6G.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BH0RXmVF.js → usePersonalDataHub-118tWI_Z.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DQtmeDXM.js → vnode-Z7O2Y7JP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-vw50zkLZ.js → zoom-BXym6zmD.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +333 -1
- package/src/commands/ask.js +35 -1
- package/src/commands/checkpoint.js +439 -0
- package/src/commands/compact.js +150 -0
- package/src/commands/cost.js +114 -0
- package/src/commands/goal.js +417 -0
- package/src/commands/hub.js +7 -0
- package/src/commands/session.js +22 -2
- package/src/harness/prompt-compressor.js +71 -1
- package/src/index.js +8 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/checkpoint-store.js +523 -0
- package/src/lib/file-checkpoint.js +300 -0
- package/src/lib/goal-context.js +87 -0
- package/src/lib/goal-store.js +308 -0
- package/src/lib/llm-pricing.js +227 -0
- package/src/lib/personal-data-hub-wiring.js +30 -0
- package/src/lib/recent-session.js +72 -0
- package/src/lib/session-picker.js +68 -0
- package/src/repl/agent-repl.js +101 -9
- package/src/repl/chat-repl.js +16 -1
- package/src/runtime/agent-core.js +313 -32
- package/src/runtime/fallback-model.js +109 -0
- package/src/runtime/file-ref-expander.js +258 -0
- package/src/runtime/headless-runner.js +601 -0
- package/src/runtime/headless-stream.js +315 -0
- package/src/runtime/policies/agent-policy.js +7 -0
- package/src/runtime/quiet-stdout.js +35 -0
- package/src/runtime/system-prompt.js +60 -0
- package/src/assets/web-panel/assets/AppLayout-B0hl5cPk.js +0 -9
- package/src/assets/web-panel/assets/Dashboard-XlMpT7K_.js +0 -3
- package/src/assets/web-panel/assets/OrderTableRenderer-Bg0bkfjR.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dl_hPdhU.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BiN5HELJ.js +0 -1
- package/src/assets/web-panel/assets/index-BhxiT2LJ.js +0 -1
- package/src/assets/web-panel/assets/index-DBNSZ2oz.js +0 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-checkpoint — manual file-state snapshot / rewind for `cc checkpoint`.
|
|
3
|
+
*
|
|
4
|
+
* Claude-Code "rewind" parity at the CLI level: snapshot a set of files (or
|
|
5
|
+
* directories) before a risky agentic run, then restore them if it goes wrong.
|
|
6
|
+
* This is the standalone store + ops; auto-snapshotting inside the agent
|
|
7
|
+
* tool-loop is a separate follow-up (it lives in the churn-prone agent-core).
|
|
8
|
+
*
|
|
9
|
+
* On-disk layout (under <home>/checkpoints, overridable via opts.root for tests):
|
|
10
|
+
* <root>/<id>.json manifest { id, label, createdAt, cwd, files:[...] }
|
|
11
|
+
* <root>/<id>/<sha256> raw bytes of each distinct file (content-addressed,
|
|
12
|
+
* so duplicate contents are stored once)
|
|
13
|
+
*
|
|
14
|
+
* Distinct from `cc workflow checkpoint` (which snapshots workflow EXECUTION
|
|
15
|
+
* state in the DB, not files).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { getHomeDir } from "./paths.js";
|
|
22
|
+
|
|
23
|
+
/** Directories never walked into when a checkpoint path is a directory. */
|
|
24
|
+
export const SKIP_DIRS = new Set([
|
|
25
|
+
"node_modules",
|
|
26
|
+
".git",
|
|
27
|
+
"dist",
|
|
28
|
+
"build",
|
|
29
|
+
".chainlesschain",
|
|
30
|
+
".next",
|
|
31
|
+
".cache",
|
|
32
|
+
"coverage",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/** Safety cap so `checkpoint create <huge-dir>` can't snapshot the universe. */
|
|
36
|
+
export const DEFAULT_MAX_FILES = 2000;
|
|
37
|
+
|
|
38
|
+
function defaultRoot() {
|
|
39
|
+
return path.join(getHomeDir(), "checkpoints");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureDir(dir) {
|
|
43
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sha256(buf) {
|
|
47
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function newId() {
|
|
51
|
+
// Date.now/random are fine here (plain CLI lib, not a resumable workflow).
|
|
52
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
53
|
+
return `cp-${Date.now()}-${rand}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Recursively collect regular files under an absolute path, honoring SKIP_DIRS
|
|
58
|
+
* and a running maxFiles budget. Symlinks are not followed.
|
|
59
|
+
*/
|
|
60
|
+
function collectFiles(abs, { maxFiles, acc }) {
|
|
61
|
+
let stat;
|
|
62
|
+
try {
|
|
63
|
+
stat = fs.lstatSync(abs);
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (stat.isSymbolicLink()) return;
|
|
68
|
+
if (stat.isFile()) {
|
|
69
|
+
if (acc.length >= maxFiles) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`checkpoint exceeds ${maxFiles} files — narrow the paths or raise maxFiles`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
acc.push(abs);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (stat.isDirectory()) {
|
|
78
|
+
if (SKIP_DIRS.has(path.basename(abs))) return;
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = fs.readdirSync(abs);
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const name of entries) {
|
|
86
|
+
collectFiles(path.join(abs, name), { maxFiles, acc });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a checkpoint snapshotting the given paths (files and/or dirs).
|
|
93
|
+
*
|
|
94
|
+
* @param {string[]} paths
|
|
95
|
+
* @param {object} [opts] { cwd, label, root, maxFiles }
|
|
96
|
+
* @returns {{ id, label, createdAt, cwd, fileCount, files:Array }}
|
|
97
|
+
*/
|
|
98
|
+
export function createCheckpoint(paths, opts = {}) {
|
|
99
|
+
const cwd = opts.cwd || process.cwd();
|
|
100
|
+
const root = opts.root || defaultRoot();
|
|
101
|
+
const maxFiles = Number.isFinite(opts.maxFiles)
|
|
102
|
+
? opts.maxFiles
|
|
103
|
+
: DEFAULT_MAX_FILES;
|
|
104
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
105
|
+
if (list.filter(Boolean).length === 0) {
|
|
106
|
+
throw new Error("createCheckpoint requires at least one path");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const absFiles = [];
|
|
110
|
+
for (const p of list) {
|
|
111
|
+
if (!p) continue;
|
|
112
|
+
const abs = path.resolve(cwd, p);
|
|
113
|
+
if (!fs.existsSync(abs)) {
|
|
114
|
+
throw new Error(`no such path: ${p}`);
|
|
115
|
+
}
|
|
116
|
+
collectFiles(abs, { maxFiles, acc: absFiles });
|
|
117
|
+
}
|
|
118
|
+
// De-dupe (overlapping paths) while preserving order.
|
|
119
|
+
const uniqueAbs = [...new Set(absFiles)];
|
|
120
|
+
|
|
121
|
+
const id = opts.id || newId();
|
|
122
|
+
const blobDir = path.join(root, id);
|
|
123
|
+
ensureDir(blobDir);
|
|
124
|
+
|
|
125
|
+
const files = [];
|
|
126
|
+
for (const abs of uniqueAbs) {
|
|
127
|
+
const buf = fs.readFileSync(abs);
|
|
128
|
+
const hash = sha256(buf);
|
|
129
|
+
const blobPath = path.join(blobDir, hash);
|
|
130
|
+
if (!fs.existsSync(blobPath)) fs.writeFileSync(blobPath, buf);
|
|
131
|
+
files.push({
|
|
132
|
+
rel: path.relative(cwd, abs) || path.basename(abs),
|
|
133
|
+
abs,
|
|
134
|
+
bytes: buf.length,
|
|
135
|
+
sha256: hash,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const manifest = {
|
|
140
|
+
id,
|
|
141
|
+
label: opts.label || "",
|
|
142
|
+
createdAt: new Date().toISOString(),
|
|
143
|
+
cwd,
|
|
144
|
+
fileCount: files.length,
|
|
145
|
+
files,
|
|
146
|
+
};
|
|
147
|
+
ensureDir(root);
|
|
148
|
+
fs.writeFileSync(
|
|
149
|
+
path.join(root, `${id}.json`),
|
|
150
|
+
JSON.stringify(manifest, null, 2),
|
|
151
|
+
"utf-8",
|
|
152
|
+
);
|
|
153
|
+
return manifest;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Load a checkpoint manifest by id, or null. */
|
|
157
|
+
export function getCheckpoint(id, opts = {}) {
|
|
158
|
+
const root = opts.root || defaultRoot();
|
|
159
|
+
const file = path.join(root, `${id}.json`);
|
|
160
|
+
if (!fs.existsSync(file)) return null;
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** List all checkpoint manifests, newest first. */
|
|
169
|
+
export function listCheckpoints(opts = {}) {
|
|
170
|
+
const root = opts.root || defaultRoot();
|
|
171
|
+
if (!fs.existsSync(root)) return [];
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const name of fs.readdirSync(root)) {
|
|
174
|
+
if (!name.endsWith(".json")) continue;
|
|
175
|
+
const m = getCheckpoint(name.slice(0, -5), { root });
|
|
176
|
+
if (m) {
|
|
177
|
+
out.push({
|
|
178
|
+
id: m.id,
|
|
179
|
+
label: m.label,
|
|
180
|
+
createdAt: m.createdAt,
|
|
181
|
+
cwd: m.cwd,
|
|
182
|
+
fileCount: m.fileCount,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compare the current on-disk state against a checkpoint.
|
|
191
|
+
* @returns {{ id, modified:[], unchanged:[], deleted:[] }}
|
|
192
|
+
* modified = content differs; deleted = file is gone now.
|
|
193
|
+
*/
|
|
194
|
+
export function diffCheckpoint(id, opts = {}) {
|
|
195
|
+
const root = opts.root || defaultRoot();
|
|
196
|
+
const m = getCheckpoint(id, { root });
|
|
197
|
+
if (!m) throw new Error(`no such checkpoint: ${id}`);
|
|
198
|
+
const modified = [];
|
|
199
|
+
const unchanged = [];
|
|
200
|
+
const deleted = [];
|
|
201
|
+
for (const f of m.files) {
|
|
202
|
+
if (!fs.existsSync(f.abs)) {
|
|
203
|
+
deleted.push(f.rel);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const cur = sha256(fs.readFileSync(f.abs));
|
|
207
|
+
if (cur === f.sha256) unchanged.push(f.rel);
|
|
208
|
+
else modified.push(f.rel);
|
|
209
|
+
}
|
|
210
|
+
return { id, modified, unchanged, deleted };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Restore files from a checkpoint to their original paths. By default a safety
|
|
215
|
+
* checkpoint of the CURRENT contents is taken first, so a restore is itself
|
|
216
|
+
* reversible. `dryRun` reports what would change without writing.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} id
|
|
219
|
+
* @param {object} [opts] { root, dryRun, skipSafety, cwd }
|
|
220
|
+
* @returns {{ id, restored:[], unchanged:[], missingBlob:[], safetyId:string|null }}
|
|
221
|
+
*/
|
|
222
|
+
export function restoreCheckpoint(id, opts = {}) {
|
|
223
|
+
const root = opts.root || defaultRoot();
|
|
224
|
+
const m = getCheckpoint(id, { root });
|
|
225
|
+
if (!m) throw new Error(`no such checkpoint: ${id}`);
|
|
226
|
+
|
|
227
|
+
const restored = [];
|
|
228
|
+
const unchanged = [];
|
|
229
|
+
const missingBlob = [];
|
|
230
|
+
const toWrite = [];
|
|
231
|
+
|
|
232
|
+
for (const f of m.files) {
|
|
233
|
+
const blobPath = path.join(root, id, f.sha256);
|
|
234
|
+
if (!fs.existsSync(blobPath)) {
|
|
235
|
+
missingBlob.push(f.rel);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const blob = fs.readFileSync(blobPath);
|
|
239
|
+
const cur = fs.existsSync(f.abs) ? fs.readFileSync(f.abs) : null;
|
|
240
|
+
if (cur && sha256(cur) === f.sha256) {
|
|
241
|
+
unchanged.push(f.rel);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
toWrite.push({ abs: f.abs, rel: f.rel, blob });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (opts.dryRun) {
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
restored: toWrite.map((w) => w.rel),
|
|
251
|
+
unchanged,
|
|
252
|
+
missingBlob,
|
|
253
|
+
safetyId: null,
|
|
254
|
+
dryRun: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Snapshot the current state of the files we're about to overwrite, so the
|
|
259
|
+
// restore can itself be rewound. Only the files that actually change and
|
|
260
|
+
// currently exist need protecting.
|
|
261
|
+
let safetyId = null;
|
|
262
|
+
if (!opts.skipSafety) {
|
|
263
|
+
const existing = toWrite
|
|
264
|
+
.filter((w) => fs.existsSync(w.abs))
|
|
265
|
+
.map((w) => w.abs);
|
|
266
|
+
if (existing.length > 0) {
|
|
267
|
+
const safety = createCheckpoint(existing, {
|
|
268
|
+
root,
|
|
269
|
+
cwd: m.cwd,
|
|
270
|
+
label: `auto-before-restore-${id}`,
|
|
271
|
+
});
|
|
272
|
+
safetyId = safety.id;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const w of toWrite) {
|
|
277
|
+
ensureDir(path.dirname(w.abs));
|
|
278
|
+
fs.writeFileSync(w.abs, w.blob);
|
|
279
|
+
restored.push(w.rel);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { id, restored, unchanged, missingBlob, safetyId };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Delete a checkpoint (manifest + blobs). Returns true if it existed. */
|
|
286
|
+
export function deleteCheckpoint(id, opts = {}) {
|
|
287
|
+
const root = opts.root || defaultRoot();
|
|
288
|
+
const file = path.join(root, `${id}.json`);
|
|
289
|
+
const blobDir = path.join(root, id);
|
|
290
|
+
let existed = false;
|
|
291
|
+
if (fs.existsSync(file)) {
|
|
292
|
+
fs.rmSync(file);
|
|
293
|
+
existed = true;
|
|
294
|
+
}
|
|
295
|
+
if (fs.existsSync(blobDir)) {
|
|
296
|
+
fs.rmSync(blobDir, { recursive: true, force: true });
|
|
297
|
+
existed = true;
|
|
298
|
+
}
|
|
299
|
+
return existed;
|
|
300
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-context — turn-scoped injection of the active goal into the agent loop.
|
|
3
|
+
*
|
|
4
|
+
* When a goal is bound to a run, a compact reminder of its objective + open key
|
|
5
|
+
* results is supplemented into each LLM call (via agent-core's `prepareCall`
|
|
6
|
+
* seam), so every turn is measured against the goal without polluting the
|
|
7
|
+
* persistent message history.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: this composes WITH the existing `defaultPrepareCall` (turn-context)
|
|
10
|
+
* rather than replacing it — both suffixes are concatenated. Keep the produced
|
|
11
|
+
* text terse: it is re-sent on every iteration and is billed each time.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Hard cap on the open key results we list, to bound per-turn token cost. */
|
|
15
|
+
const MAX_KEY_RESULTS = 8;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the compact system-prompt supplement for a goal, or null when the goal
|
|
19
|
+
* is missing / not active.
|
|
20
|
+
* @param {object|null} goal
|
|
21
|
+
* @returns {string|null}
|
|
22
|
+
*/
|
|
23
|
+
export function buildGoalContext(goal) {
|
|
24
|
+
if (!goal || goal.status !== "active") return null;
|
|
25
|
+
const objective = String(goal.objective || "").trim();
|
|
26
|
+
if (!objective) return null;
|
|
27
|
+
|
|
28
|
+
const pct = Number.isFinite(goal.progress) ? goal.progress : 0;
|
|
29
|
+
const lines = [`Active goal (${pct}% complete): ${objective}`];
|
|
30
|
+
|
|
31
|
+
const open = (goal.keyResults || []).filter((k) => k && !k.done);
|
|
32
|
+
for (const kr of open.slice(0, MAX_KEY_RESULTS)) {
|
|
33
|
+
const target =
|
|
34
|
+
kr.target != null ? ` [${kr.current ?? 0}/${kr.target}]` : "";
|
|
35
|
+
lines.push(`- key result: ${kr.text}${target}`);
|
|
36
|
+
}
|
|
37
|
+
if (open.length > MAX_KEY_RESULTS) {
|
|
38
|
+
lines.push(`- (+${open.length - MAX_KEY_RESULTS} more key results)`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
lines.push(
|
|
42
|
+
"Each turn, prefer actions that advance these key results; if you must diverge, briefly say why.",
|
|
43
|
+
);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A `prepareCall`-shaped function bound to a goal. Defensive: never throws
|
|
49
|
+
* (agent-core swallows prepareCall errors, but we avoid relying on that).
|
|
50
|
+
* @param {object|null} goal
|
|
51
|
+
* @returns {(ctx:object) => ({systemSuffix:string}|null)}
|
|
52
|
+
*/
|
|
53
|
+
export function goalPrepareCall(goal) {
|
|
54
|
+
return () => {
|
|
55
|
+
try {
|
|
56
|
+
const suffix = buildGoalContext(goal);
|
|
57
|
+
return suffix ? { systemSuffix: suffix } : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compose several `prepareCall` functions into one. Each is invoked per turn
|
|
66
|
+
* and their non-empty `systemSuffix` strings are concatenated. A failing
|
|
67
|
+
* member is skipped, never fatal.
|
|
68
|
+
* @param {Array<Function|null|undefined>} fns
|
|
69
|
+
* @returns {(ctx:object) => Promise<{systemSuffix:string}|null>}
|
|
70
|
+
*/
|
|
71
|
+
export function composePrepareCall(fns) {
|
|
72
|
+
const list = (fns || []).filter((f) => typeof f === "function");
|
|
73
|
+
return async (ctx) => {
|
|
74
|
+
const parts = [];
|
|
75
|
+
for (const fn of list) {
|
|
76
|
+
try {
|
|
77
|
+
const r = await fn(ctx);
|
|
78
|
+
if (r && typeof r.systemSuffix === "string" && r.systemSuffix.trim()) {
|
|
79
|
+
parts.push(r.systemSuffix);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
/* a failing member must not break the turn */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return parts.length ? { systemSuffix: parts.join("\n\n") } : null;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -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
|
+
}
|