thincoder 0.12.62 → 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.
- package/CHANGELOG.md +9 -0
- package/README.md +13 -12
- package/bin/thincoder.mjs +53 -27
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +9 -9
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +26 -10
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +10 -5
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +46 -22
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/wizard.mjs +3 -3
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -355
|
@@ -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
|
-
}
|
package/src/tools/checklist.md
DELETED
|
@@ -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).
|
package/src/tools/checklist.mjs
DELETED
|
@@ -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
|
-
}
|
package/src/tools/delete.md
DELETED
|
@@ -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).
|