thincoder 0.12.61 → 0.12.63

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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
@@ -1,181 +0,0 @@
1
- /**
2
- * checklist-sync.mjs — checklist 并发写同步机(R10 F4,MULTI-INSTANCE-COLLAB.md §2a.3
3
- * ——2026-09-06,自 checklist.mjs 拆出:门控 + 合并 + 序列化——防 checklist.mjs 超 300
4
- * 行 advisory 线;本模块为纯同步机制,不持 checklist 业务状态)。
5
- *
6
- * 双端 checklist 同路径 {cwd}/.thincoder/checklist.md——多实例并发写原为裸整文件覆盖
7
- * (丢项)。写前 mtime 门控 + ID 键合并(评审修正 #6 基线规则:基线 = 本读-改-写链内
8
- * 最近一次 stat 的 mtime;本进程自写后刷新——parse 规范化写回不误触发 merged/重分配)。
9
- * .bak 不做(决策点② A:cwd 在 git 仓库内——git 兜底现场)。
10
- *
11
- * 冲突语义(flushWrite 内收口):写前 stat ≠ 基线(对端并发写过)→ 重读磁盘 → 双方
12
- * union by ID(本端新增 + 盘上他端新增都保留;同 ID 不同文本 → 盘上保留、本端重分配
13
- * max+1 续分配;同 ID 仅状态不同 → in_progress 胜过 pending——标记不丢);checklist
14
- * 文件合并时按 done 文件归档 ID 排除(含子任务点号 ID——并发 mark-done 的"盘上有旧
15
- * pending 副本"不复活已归档项——done 集合 union 的另一半)。checklist-done.md 自身
16
- * 合并 = 纯 union(无归档排除)。
17
- *
18
- * 调用方(checklist.mjs)通过 flushWrite 的 opts 注入:reread(磁盘无写回重读)、
19
- * doneFile 路径、isDone(目标文件是否为 done 文件)——避免本模块反向依赖 checklist
20
- * 的 parse/树结构。
21
- */
22
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
23
-
24
- const baselines = new Map() // filePath → 最近一次读的 stat mtime(null = 读时文件缺失)
25
-
26
- /** F4 测试缝(伪属主范式——T-F4a..d):在每次写前门控处注入"对端写盘"钩子——模拟本链
27
- * parse 后、write 前对端实例的真实并发写。生产不设置(null → 零行为影响)。 */
28
- let writeGateHook = null
29
- export function _setWriteGateHookForTest(fn) { writeGateHook = fn }
30
-
31
- /** 文件缺失 → null(门控 t0 基线形态)。 */
32
- export function statMtime(p) {
33
- try { return statSync(p).mtimeMs } catch { return null }
34
- }
35
-
36
- /** 调用方(parse 读链)登记基线——读后写链以它为门控比对基准。 */
37
- export function noteReadBaseline(filePath, mtime) {
38
- baselines.set(filePath, mtime)
39
- }
40
-
41
- /** Done 文件归档根 ID 清单(含子任务点号 ID:T1 与 T1.2 都进——归档排除用;编号预留
42
- * nextRootId 的 /^T(\d+)$/ 过滤不受影响)。 */
43
- export function readDoneRoots(doneFile) {
44
- if (!existsSync(doneFile)) return []
45
- const roots = []
46
- for (const line of readFileSync(doneFile, "utf-8").split("\n")) {
47
- const m = line.match(/^- \[.\] ((?:T\d+)(?:\.\d+)*): /)
48
- if (m) roots.push({ id: m[1] })
49
- }
50
- return roots
51
- }
52
-
53
- /** Serialize items to markdown lines (2-space indent per depth) — tree structure preserved. */
54
- function renderItems(nodes, depth, out) {
55
- const indent = " ".repeat(depth)
56
- for (const item of nodes) {
57
- const mark = item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "
58
- const label = item.id ? `${item.id}: ${item.text}` : item.text
59
- out.push(`${indent}- [${mark}] ${label}`)
60
- if (item.children?.length) renderItems(item.children, depth + 1, out)
61
- }
62
- }
63
-
64
- /**
65
- * 门控 + 合并 + 写(F4 收口——parse 规范化写回 / add / mark 双写都经它)。
66
- * @param filePath 目标文件(checklist.md 或 checklist-done.md)
67
- * @param items 本端(读-改-写链产物)的顶层 items
68
- * @param opts.reread 磁盘无写回重读函数(返回盘上顶层 items——合并触发时调用)
69
- * @param opts.doneFile done 文件路径(checklist 合并的归档排除源)
70
- * @param opts.isDone 目标文件是否为 done 文件(true = 纯 union,无归档排除)
71
- * @returns { merged, items }——merged=true 时 items 为落盘真相(本端节点原地重分配——
72
- * 调用方持有的引用/消息读到的即最终 ID);merged=false 时 items 为原输入。
73
- */
74
- export function flushWrite(filePath, items, { reread, doneFile, isDone }) {
75
- if (writeGateHook) writeGateHook(filePath) // 测试缝:模拟对端此刻写盘
76
- const base = baselines.get(filePath) // undefined = 本链无读(防御——无基线无从门控)
77
- const now = statMtime(filePath)
78
- let finalItems = items
79
- let merged = false
80
- if (base !== undefined && now !== base) {
81
- // 磁盘被对端并发改过(本链读后)→ 重读磁盘 + 合并(ID union)
82
- const diskItems = reread()
83
- finalItems = mergeItems(diskItems, items, doneFile, isDone)
84
- merged = true
85
- }
86
- const out = []
87
- renderItems(finalItems, 0, out)
88
- writeFileSync(filePath, out.join("\n") + "\n")
89
- baselines.set(filePath, statMtime(filePath)) // 自写后刷新基线(防自写误判并发)
90
- return { merged, items: finalItems }
91
- }
92
-
93
- // ── F4 合并(ID 键 union)─────────────────────────────────────────────────────
94
-
95
- function flattenTree(nodes, out = []) {
96
- for (const n of nodes) {
97
- out.push(n)
98
- if (n.children?.length) flattenTree(n.children, out)
99
- }
100
- return out
101
- }
102
-
103
- /** 层内递归合并(磁盘序在前;本端独有/重分配项追加尾部)——规则见文件头注释。 */
104
- function mergeLevel(diskNodes, ourNodes, env) {
105
- const diskById = new Map(diskNodes.filter((n) => n.id != null).map((n) => [n.id, n]))
106
- const ourById = new Map(ourNodes.filter((n) => n.id != null).map((n) => [n.id, n]))
107
- const result = []
108
- const pending = [] // 本端独有 + 同 ID 异文本(待重分配)——追加在磁盘序之后
109
- for (const d of diskNodes) {
110
- if (d.id != null && !env.isDone && env.doneIds.has(d.id)) continue // 已归档(对端 mark-done)→ 不复活
111
- const o = d.id != null ? ourById.get(d.id) : null
112
- if (!o) { result.push(d); continue } // 盘上独有 → 整棵保留
113
- const childEnv = { ...env, prefix: `${d.id}.` } // 子层重分配域 = 父 ID 前缀("T1" → "T1.")
114
- if (o.text === d.text && o.status === d.status) {
115
- // 双端同一节点(parse 规范化幂等场景)→ 去重;子树仍逐层合并(防丢任一端子新增)
116
- d.children = mergeLevel(d.children ?? [], o.children ?? [], childEnv)
117
- result.push(d)
118
- continue
119
- }
120
- if (o.text === d.text) {
121
- // 仅状态不同 → in_progress 胜过 pending(标记意图不丢;同文本不可能 pending×pending)
122
- const carry = d.status === "in_progress" ? d : o
123
- carry.children = mergeLevel(d.children ?? [], o.children ?? [], childEnv)
124
- result.push(carry)
125
- continue
126
- }
127
- // 同 ID 不同文本(并发 add 各自从同基线分到同号)→ 盘上保留;本端项排队重分配
128
- result.push(d)
129
- pending.push(o)
130
- }
131
- for (const o of ourNodes) {
132
- if (o.id == null) { pending.push(o); continue } // 无 ID 行(理论极少——parse 已归一)→ 原样追加
133
- if (!env.isDone && env.doneIds.has(o.id)) continue // 本端项已被归档 → 丢弃(不复活)
134
- if (!diskById.has(o.id)) pending.push(o) // 本端独有 → 保留(含子树)
135
- // 共享 ID 已在磁盘循环处理(去重/状态/文本冲突排队)
136
- }
137
- for (const o of pending) {
138
- if (o.id != null && diskById.has(o.id)) reassignNode(o, env) // 文本冲突项 → 重分配 max+1 续分配
139
- result.push(o)
140
- }
141
- return result
142
- }
143
-
144
- /** 重分配:层前缀(根 "T" / 子树 `${parentId}.`)下 max+1;子树 ID 前缀同步换新
145
- * ("T4.1" → "T5.1"——ID 层级与父节点保持一致)。原地改——调用方引用即最终 ID。 */
146
- function reassignNode(node, env) {
147
- let max = 0
148
- for (const id of env.allIds) {
149
- if (!id?.startsWith(env.prefix)) continue
150
- const suffix = id.slice(env.prefix.length)
151
- if (/^\d+$/.test(suffix)) max = Math.max(max, parseInt(suffix))
152
- }
153
- const oldId = node.id
154
- const newId = `${env.prefix}${max + 1}`
155
- node.id = newId
156
- env.allIds.add(newId)
157
- if (oldId) renameSubtreePrefixes(node, oldId, newId)
158
- }
159
-
160
- function renameSubtreePrefixes(node, oldId, newId) {
161
- for (const c of node.children ?? []) {
162
- if (c.id?.startsWith(`${oldId}.`)) {
163
- c.id = newId + c.id.slice(oldId.length)
164
- renameSubtreePrefixes(c, oldId, newId)
165
- }
166
- }
167
- }
168
-
169
- /** 合并入口:isDone = checklist-done 文件(纯 union——无归档排除);否则按 done 文件
170
- * 当前归档 ID 排除(含点号子 ID——对端 mark-done 归档的项不因本端旧 parse 复活)。 */
171
- function mergeItems(diskItems, ourItems, doneFile, isDone) {
172
- const doneRoots = isDone ? [] : readDoneRoots(doneFile)
173
- const doneIds = new Set(doneRoots.map((r) => r.id))
174
- const allIds = new Set() // 重分配 max 计算域:双端全树 + 归档根(根层防撞)
175
- for (const n of flattenTree(diskItems).concat(flattenTree(ourItems))) {
176
- if (n.id != null) allIds.add(n.id)
177
- }
178
- for (const id of doneIds) allIds.add(id)
179
- const env = { isDone, doneIds, allIds, prefix: "T" } // 根层前缀 "T";子层在 mergeLevel 递归时推导
180
- return mergeLevel(diskItems, ourItems, env)
181
- }
@@ -1,13 +0,0 @@
1
- Manage the task checklist in .thincoder/checklist.md. Use at these points: after requirements are confirmed — add one entry per requirement point; when starting work — mark in_progress; when verified complete — mark done. Checklist entries map to requirement/design points — project-level tracking across sessions. For in-session subtask breakdown of a single checklist item, use the `task` tool instead. Completed items are auto-archived to .thincoder/checklist-done.md.
2
-
3
- Items support tree hierarchy via indentation (2 spaces per level) and auto-assigned IDs (T1, T1.1, T1.2.1). Use the `parent` parameter to add a child under an existing task.
4
-
5
- Parameters:
6
- - action: "add" | "mark" | "list"
7
- - id: task ID to mark, e.g. "T3" (with "mark"; preferred — use the ID returned by `add`, or from `list`)
8
- - item: text for new item (with "add")
9
- - index: 1-based index (with "mark"; fallback — use only when you have no ID)
10
- - status: "pending" | "in_progress" | "done" (with "mark")
11
- - parent: parent task ID for hierarchical tasks, e.g. "T1" (with "add")
12
-
13
- Note: marking a parent "done" requires all its child tasks already done — otherwise it is rejected (complete the children before marking the parent done).
@@ -1,299 +0,0 @@
1
- import { readFileSync, existsSync, mkdirSync } from "node:fs"
2
- import { join, dirname } from "node:path"
3
- import { DESC } from "./shared.mjs"
4
- // F4 并发写同步机(门控 + ID union 合并 + 序列化)在 checklist-sync.mjs——本文件只留
5
- // parse/树操作与工具执行;设计见 MULTI-INSTANCE-COLLAB.md §2a.3(D-F4a/b)。
6
- import { statMtime, noteReadBaseline, readDoneRoots, flushWrite } from "./checklist-sync.mjs"
7
-
8
- const CHECKLIST = "checklist.md"
9
- const DONE = "checklist-done.md"
10
-
11
- function checklistPath(cwd) { return join(cwd, ".thincoder", CHECKLIST) }
12
- function donePath(cwd) { return join(cwd, ".thincoder", DONE) }
13
-
14
- /**
15
- * Parse checklist file into tree-structured items.
16
- * Indentation (2 spaces per level) determines parent-child relationships.
17
- * Each item: { id, index, depth, status, text, children[] }
18
- * "index" is the 1-based position in the flat markdown list.
19
- */
20
- function parse(filePath) {
21
- return readItems(filePath, { writeback: true })
22
- }
23
-
24
- /** 读盘 → 建树 → 无 ID 行一次性分配(assignIds);writeback=true 时(公开 parse 语义)
25
- * 分配后立即经 write() 落盘(同走 F4 门控——并发写合并/放弃重分配)。基线在**读前**
26
- * stat(stat→read 序:对端在读与 stat 微窗口写入只造成假合并,绝不漏检静默覆盖)。 */
27
- function readItems(filePath, { writeback }) {
28
- const base = statMtime(filePath) // 基线 = 本链最近 stat(评审修正 #6)
29
- let text = null
30
- try {
31
- if (existsSync(filePath)) text = readFileSync(filePath, "utf-8")
32
- } catch { text = null } // 读失败(对端删/半写)→ 按缺失降级
33
- noteReadBaseline(filePath, text === null ? null : base)
34
- if (text === null) return []
35
- const lines = text.split("\n")
36
- const items = []
37
- let flatIdx = 0
38
- const stack = [{ children: items, depth: -1 }] // virtual root
39
-
40
- for (const line of lines) {
41
- const m = line.match(/^(\s*)- \[(.)\] (.+)$/)
42
- if (!m) continue
43
- flatIdx++
44
- const indent = m[1]
45
- const depth = Math.floor(indent.length / 2) // 2 spaces = 1 level
46
- const raw = m[2]
47
- const status = raw === "x" ? "done" : raw === "~" ? "in_progress" : "pending"
48
- const text2 = m[3].trim()
49
-
50
- // Strip ALL leading "T[\d.]+:" tokens (historical dirty data can accumulate
51
- // "T15: T15: T15:"); keep the first token as the ID and the rest as text.
52
- let id = null
53
- let bareText = text2
54
- let idTok
55
- while ((idTok = bareText.match(/^(T[\d.]+):\s*/))) {
56
- if (id == null) id = idTok[1]
57
- bareText = bareText.slice(idTok[0].length)
58
- }
59
- const node = { id, index: flatIdx, depth, status, text: bareText, children: [] }
60
-
61
- // Find parent by popping stack until we find a node at depth-1
62
- while (stack.length > 1 && stack.at(-1).depth >= depth) stack.pop()
63
- const parent = stack.at(-1)
64
- parent.children.push(node)
65
- stack.push({ children: node.children, depth, id: node.id })
66
- }
67
-
68
- // Assign stable IDs to lines that lacked an explicit one, exactly once.
69
- // IDs are "max existing number + 1" (not position-based) so gaps left by
70
- // archived items never collide, and persisted IDs never drift on re-read.
71
- let assigned = false
72
- function assignIds(nodes, parentId) {
73
- for (const n of nodes) {
74
- if (!n.id) {
75
- n.id = parentId ? nextChildId(parentId, nodes, doneIds) : nextRootId(nodes, doneRoots)
76
- assigned = true
77
- }
78
- if (n.children?.length) assignIds(n.children, n.id)
79
- }
80
- }
81
- // Root IDs archived to the done file also reserve numbers (mirrors the `add`
82
- // path's double-file scan), so auto-assigned IDs never collide with them.
83
- const doneRoots = readDoneRoots(join(dirname(filePath), DONE))
84
- const doneIds = doneRoots.map((r) => r.id) // 含点号子 ID——子层分配同样预留
85
- assignIds(items, null)
86
- if (writeback && assigned) {
87
- // 规范化写回(同走门控——F4):合并发生时返回落盘真相——调用方(add/mark/list)在
88
- // 磁盘真值上继续,避免基于过期 parse 再触发一轮合并
89
- const r = write(filePath, items)
90
- if (r.merged) return r.items
91
- }
92
- return items
93
- }
94
-
95
- function nextRootId(items, doneItems) {
96
- let max = 0
97
- for (const list of [items, doneItems]) {
98
- for (const c of list ?? []) {
99
- const m = c.id?.match(/^T(\d+)$/)
100
- if (m) max = Math.max(max, parseInt(m[1]))
101
- }
102
- }
103
- return `T${max + 1}`
104
- }
105
-
106
- function nextChildId(parentId, children, archivedIds) {
107
- let max = 0
108
- const prefix = `${parentId}.`
109
- for (const c of children) {
110
- if (c.id?.startsWith(prefix)) {
111
- const suffix = c.id.slice(prefix.length)
112
- if (/^\d+$/.test(suffix)) max = Math.max(max, parseInt(suffix))
113
- }
114
- }
115
- // 归档预留(F4 补正):已归档同前缀点号 ID 不复用——重加子项撞归档 ID 会被门控合并误删
116
- for (const id of archivedIds ?? []) {
117
- if (typeof id !== "string" || !id.startsWith(prefix)) continue
118
- const suffix = id.slice(prefix.length)
119
- if (/^\d+$/.test(suffix)) max = Math.max(max, parseInt(suffix))
120
- }
121
- return `${prefix}${max + 1}`
122
- }
123
-
124
- /** Write items back to file, preserving tree structure — F4 收口点(门控 + 合并见
125
- * checklist-sync.mjs flushWrite)。返回 { merged, items }(同 flushWrite 契约)。 */
126
- function write(filePath, items) {
127
- mkdirSync(dirname(filePath), { recursive: true })
128
- return flushWrite(filePath, items, {
129
- reread: () => readItems(filePath, { writeback: false }), // 合并用磁盘重读(无写回)
130
- doneFile: join(dirname(filePath), DONE), // checklist 合并的归档排除源
131
- isDone: filePath.endsWith(DONE), // done 文件自身 = 纯 union
132
- })
133
- }
134
-
135
- /** Find a node by ID in the tree */
136
- function findById(items, id) {
137
- for (const item of items) {
138
- if (item.id === id) return { parent: items, item, idx: items.indexOf(item) }
139
- if (item.children?.length) {
140
- const found = findById(item.children, id)
141
- if (found) return found
142
- }
143
- }
144
- return null
145
- }
146
-
147
- /** Flatten tree for mark action (index-based) */
148
- function flatten(items, out = []) {
149
- for (const item of items) {
150
- out.push(item)
151
- if (item.children?.length) flatten(item.children, out)
152
- }
153
- return out
154
- }
155
-
156
- /** True if every descendant (children, grandchildren, …) is done. */
157
- function allChildrenDone(node) {
158
- for (const c of node.children ?? []) {
159
- if (c.status !== "done" || !allChildrenDone(c)) return false
160
- }
161
- return true
162
- }
163
-
164
- /** Recursively clone a subtree for archiving, forcing every status to done. */
165
- function archiveSubtree(node) {
166
- return {
167
- id: node.id,
168
- index: 0,
169
- depth: 0,
170
- status: "done",
171
- text: node.text,
172
- children: (node.children ?? []).map(archiveSubtree),
173
- }
174
- }
175
-
176
- /** Parse pending items only (for context injection) */
177
- export function pendingItems(cwd) {
178
- const flat = flatten(parse(checklistPath(cwd)))
179
- return flat.filter(i => i.status !== "done")
180
- }
181
-
182
- export const checklistTool = {
183
- name: "checklist",
184
- description: DESC("checklist"),
185
- parameters: {
186
- type: "object",
187
- properties: {
188
- action: {
189
- type: "string",
190
- enum: ["add", "mark", "list"],
191
- description: "add a new item / mark item status / list all items"
192
- },
193
- id: {
194
- type: "string",
195
- description: "Task ID to mark (preferred — use the ID returned by add, e.g. 'T3')"
196
- },
197
- item: {
198
- type: "string",
199
- description: "Item text (required for add)"
200
- },
201
- index: {
202
- type: "number",
203
- description: "1-based item index (fallback for mark, only when id is absent)"
204
- },
205
- status: {
206
- type: "string",
207
- enum: ["pending", "in_progress", "done"],
208
- description: "New status (required for mark)"
209
- },
210
- parent: {
211
- type: "string",
212
- description: "Parent task ID for tree-structured tasks (e.g. 'T1')"
213
- },
214
- },
215
- required: ["action"],
216
- },
217
- readonly: false,
218
- execute(args, ctx) {
219
- switch (args.action) {
220
- case "add": {
221
- if (!args.item || typeof args.item !== "string") return "Error: 'item' is required for add"
222
- const items = parse(checklistPath(ctx.cwd))
223
-
224
- let target = items
225
- let parentId = null
226
- if (args.parent) {
227
- const found = findById(items, args.parent)
228
- if (!found) return `Error: parent '${args.parent}' not found. Use 'list' to see all task IDs.`
229
- target = found.item.children
230
- parentId = found.item.id
231
- }
232
-
233
- // 归档预留(根 + 点号子 ID——add 父/子都扫 done 文件;防撞号 + 防合并误删)
234
- const doneItems = parse(donePath(ctx.cwd))
235
- const id = parentId
236
- ? nextChildId(parentId, target, doneItems.map((i) => i.id))
237
- : nextRootId(items, doneItems)
238
- const node = { id, index: 0, depth: parentId ? 1 : 0, status: "pending", text: args.item, children: [] }
239
- target.push(node)
240
- write(checklistPath(ctx.cwd), items)
241
- // F4:合并可能原地重分配本端新项(并发同号)——以 node.id(落盘真相)报回
242
- return `Added: [ ] ${node.id}: ${args.item}${parentId ? ` (under ${parentId})` : ""}`
243
- }
244
- case "mark": {
245
- if (args.id == null && args.index == null) return "Error: 'id' or 'index' is required for mark"
246
- const status = args.status
247
- if (!status || !["pending", "in_progress", "done"].includes(status)) return "Error: 'status' is required (pending|in_progress|done)"
248
- const cp = checklistPath(ctx.cwd)
249
- const items = parse(cp)
250
- let item
251
- if (args.id != null) {
252
- const found = findById(items, args.id)
253
- if (!found) return `Error: id '${args.id}' not found. Use 'list' to see all task IDs.`
254
- item = found.item
255
- } else {
256
- const flat = flatten(items)
257
- if (args.index < 1 || args.index > flat.length) return `Error: index ${args.index} out of range (1-${flat.length})`
258
- item = flat[args.index - 1]
259
- }
260
- const old = item.status
261
- if (old === status) return `Already ${status}: ${item.text}`
262
- if (status === "done" && item.children?.length && !allChildrenDone(item)) {
263
- return "Error: 父任务仍有未完成的子任务,先处理子任务再标父 done"
264
- }
265
- item.status = status
266
- if (status === "done") {
267
- // Move the whole subtree to the done file (hierarchy preserved).
268
- const dp = donePath(ctx.cwd)
269
- const doneItems = parse(dp)
270
- doneItems.push(archiveSubtree(item))
271
- write(dp, doneItems)
272
- // Remove the subtree from the tree.
273
- const found = findById(items, item.id)
274
- if (found) found.parent.splice(found.idx, 1)
275
- }
276
- write(cp, items)
277
- return `Marked ${item.id} ${old} → ${status}`
278
- }
279
- case "list": {
280
- const items = parse(checklistPath(ctx.cwd))
281
- if (items.length === 0) return "(checklist is empty)"
282
- const marks = { pending: " ", in_progress: "~", done: "x" }
283
- const lines = []
284
- function render(nodes, depth) {
285
- const indent = " ".repeat(depth)
286
- for (const n of nodes) {
287
- const idTag = n.id ? `${n.id}: ` : ""
288
- lines.push(`${indent}- [${marks[n.status]}] ${idTag}${n.text}`)
289
- if (n.children?.length) render(n.children, depth + 1)
290
- }
291
- }
292
- render(items, 0)
293
- return lines.join("\n")
294
- }
295
- default:
296
- return `Error: unknown action '${args.action}'`
297
- }
298
- },
299
- }
@@ -1,13 +0,0 @@
1
- Delete a file. Use when the agent created a temporary or junk file that should be cleaned up, or when the user explicitly asks to delete something. Refuses to delete git-tracked files as a safety measure — tracked files should be edited or removed via bash with explicit user confirmation.
2
-
3
- **Route to delete instead of bash:** `del file` / `rm file` → delete (single files). Use bash `rm -rf` only for directories (delete is single-file).
4
-
5
- Parameters:
6
- - path (required): File path, relative to cwd or absolute
7
- - force: Allow deleting git-tracked files (default false)
8
-
9
- Notes:
10
- - Untracked or non-git files are deleted immediately
11
- - Tracked files require force=true (user must confirm separately)
12
- - Directories must be removed with bash (rm -rf)
13
- - Returns `Deleted <path>` (or `Error: ...` on failure/tracked refusal).