thincoder 0.12.62 → 0.12.64

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 (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  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 +257 -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 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +60 -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 +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  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-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +35 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-plan.mjs +9 -0
  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 +24 -1
  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 +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
package/src/session.mjs DELETED
@@ -1,492 +0,0 @@
1
- /**
2
- * session.mjs — session persistence (slot-based model)
3
- * Each project (keyed by cwd hash) keeps unlimited session slots.
4
- * Every session lives in a numbered slot; the manifest tracks the SHARED active pointer
5
- * (2026-09-05 §10 D-6 修订:active = 旧版端/ACP 的恢复依据 + 无记录端的一次性继承源 +
6
- * 列表回退高亮——本端恢复依据 = end marker `{manifest}.cli`,见 session-slots.mjs
7
- * resumeSlot/readEndMarker,SESSION.md §1/§10)。
8
- *
9
- * File layout: {hash}.json.N (slots), {hash}.json.manifest (slot metadata + active pointer).
10
- * Legacy {hash}.json is migrated to a slot on first access.
11
- *
12
- * 2026-08-31 advisor round1 🔴:slot/清单管理拆至 session-slots.mjs(本文件曾超 500 行
13
- * 硬限);本文件只保留核心读写 + re-export 全部 slot 导出(既有 import 路径不变)。
14
- */
15
-
16
- import { readFileSync, renameSync, existsSync, statSync } from "node:fs"
17
- import { basename } from "node:path"
18
- import { resolveCompactThreshold } from "./config.mjs"
19
- import {
20
- slotPath, writeSessionFile, loadManifest, saveManifest, slotDigest,
21
- activeSlot, getSessionId, isProcessAlive,
22
- writeEndMarker, resumeSlot as slotsResumeSlot,
23
- } from "./session-slots.mjs"
24
- // F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
25
- import { guardForeignSlotFile } from "./session-guard.mjs"
26
- import { scheduleSessionGC } from "./session-gc.mjs"
27
- import { engTokenSlotFields, restoreEngTokens } from "./token-ttl.mjs"
28
- // TUI-OOM-ROOTCAUSE 批(SESSION.md §14):人读线记录存储(磁盘为准 + 内存窗口)。
29
- // slimForDisplay/isLegacyTransient 迁入 store 模块(§14.5 文件表)——本文件 re-export 保持
30
- // 既有 import 面(session-slots.mjs 引 isLegacyTransient)。依赖方向单向:store 零项目内依赖。
31
- import {
32
- bindRecordStore, unbindRecordStore, saveProjectedSlot, isLegacyTransient, slimForDisplay,
33
- RECORD_WINDOW_MESSAGES,
34
- } from "./session-store.mjs"
35
- export { isLegacyTransient, slimForDisplay, bindRecordStore, unbindRecordStore }
36
-
37
- // re-export slot 管理(保持既有 import session.mjs 的调用点不变;resumeSlot 下方本地包装导出)
38
- export {
39
- getSessionId, normalizeCwd, sessionPath, slotPath, manifestPath, activePath,
40
- writeSessionFile, slotDigest, loadManifest, saveManifest, activeSlot, listSlots,
41
- deleteSlot, isProcessAlive, END, endMarkerPath,
42
- readEndMarker, writeEndMarker, claimSlot, allocateFresh,
43
- } from "./session-slots.mjs"
44
- // §12.2.5 契约改使 session-slots.mjs 超 500 行硬限 → renameSlot 拆至 session-rename.mjs(§12.3 授权)
45
- export { renameSlot } from "./session-rename.mjs"
46
-
47
- /** 恢复决策包装(SESSION.md §12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
48
- * scheduleSessionGC 内部 setImmediate 空闲执行 + 每进程每前缀去重,不阻塞启动路径(N4)。 */
49
- export function resumeSlot(cwd) {
50
- scheduleSessionGC(cwd)
51
- return slotsResumeSlot(cwd)
52
- }
53
-
54
- // ========== core read/write ==========
55
- // (legacy transient 判定 + slimForDisplay 自本区迁至 session-store.mjs——§14.5 文件表;
56
- // 上方 re-export 保持既有 import 面。)
57
-
58
- /** 未绑定路径的人读线全量物化(模式 F——既有语义逐字保留)。 */
59
- function legacyHistory(agent) {
60
- return (agent._fullHistory ?? agent.history)
61
- .filter((m) => !m.transient && !isLegacyTransient(m))
62
- .map(slimForDisplay)
63
- }
64
-
65
- /** 恢复描述符(§14.3.6——两调用点统一形态):`{ history, total, base }`。
66
- * 绑定态:history = 尾窗 + ±1 页沿头一条(跨页回合标签判定用——不渲染)、total = 绝对
67
- * 总条数(「N messages」标签口径)、base = history[0] 的绝对序号;未绑定(模式 F):
68
- * 回退全量数组(total = 数组长)。 */
69
- export function sessionDescriptor(agent, data) {
70
- const store = agent?._recordStore
71
- if (store) {
72
- const total = store.total()
73
- const win = store.page(Math.max(0, total - RECORD_WINDOW_MESSAGES), total, { margin: 1 })
74
- return { history: win.messages, total, base: win.base }
75
- }
76
- const full = Array.isArray(data?.history) ? data.history : []
77
- return { history: full, total: full.length, base: 0 }
78
- }
79
-
80
- /** 槽摘要(记录存储绑定路径——history 不物化):与 slotDigest(extractSlotMeta) 同形。 */
81
- function digestFromStore(fields, counters) {
82
- const meta = {
83
- messageCount: counters.total,
84
- turnCount: counters.userReal,
85
- firstMessage: counters.firstMessage,
86
- activeProvider: fields.activeProvider ?? "",
87
- updatedAt: fields.updatedAt ?? Date.now(),
88
- title: fields.title ?? "",
89
- }
90
- if (fields.activeModel) meta.activeModel = fields.activeModel
91
- return { ts: Date.now(), ...meta }
92
- }
93
-
94
- /** Save agent state to the active slot file (atomic write). `display` (the old
95
- * WYSIWYG render snapshot) is DEPRECATED — it drifted out of sync with history
96
- * whenever VS Code wrote the slot, and the TUI resumed from a stale snapshot.
97
- * Restore now always rebuilds from history (lazy, see startup.mjs).
98
- * 2026-08-31 会诊 F1:slot 粘性——首次认领后缓存 agent._slot,永不重跑 ensureActive
99
- * (原实现每次保存重推,manifest active 被并发方翻动时会话静默迁移)。
100
- * 返回轮转的 .bak 路径或 null。 */
101
- export function saveSession(agent) {
102
- // _fullHistory is written at the source via pushReal — no flush needed here.
103
- // history = FULL, never-compacted (human-readable; VS Code panel & CLI resume read this)
104
- // contextHistory = machine context (possibly compacted) so CLI resume keeps the token savings
105
- // Human line: transient machine injections never enter the readable record.
106
- // TUI-OOM-ROOTCAUSE 批(SESSION.md §14.3.5):绑定记录存储后 `history` 字段由
107
- // saveProjectedSlot 流式拼接段原文生成(磁盘为准——不物化全量数组);未绑定
108
- // (模式 F:thincoder chat/测试/未覆盖路径)→ 既有全量物化路径逐字保留(D-R6)。
109
- // Machine line (contextHistory): KEEP transient messages — resume must rebuild the
110
- // machine line byte-identical to what the provider cache saw. Dropping them made every
111
- // process restart diverge at the first injection position (git/OS/time reminders are
112
- // re-injected with FRESH content) → whole-prefix cache miss on the CLI's very first
113
- // request of each session (2026-08-16 cache-hit report; Kimi review).
114
- const contextHistory = agent.history.filter((m) => !isLegacyTransient(m))
115
- // fields = 槽 JSON 数据面(history 由投影插入——键序与既有 data 形态同序,history/contextHistory 居后)
116
- const fields = {
117
- version: 2,
118
- cwd: agent.cwd,
119
- title: agent.title ?? "",
120
- // MODEL-MERGE-SESSION 槽双字段恒非空(裁定 a):有 provider 即携带具体复合值——
121
- // 恢复不看 config(F-4)——无渠道默认可回落 ""/null(全新未配态——读侧容忍)
122
- activeProvider: agent.activeProvider ?? agent.provider?.name ?? "",
123
- activeModel: agent.activeModel ?? agent.provider?.model ?? null,
124
- updatedAt: Date.now(),
125
- tasks: agent.tasks ?? [],
126
- planMode: agent.planMode ?? false,
127
- autoApprove: agent.autoApprove ?? false,
128
- engineering: agent.config?.agent?.engineering ?? false,
129
- ...engTokenSlotFields(agent),
130
- goal: agent.goal ?? null,
131
- advisor: agent.config?.advisor ?? null,
132
- pendingReminders: agent._pendingReminders ?? [],
133
- sessionStart: agent._sessionStart ?? null,
134
- }
135
- // 2026-09-05 §10 D-4:首认领(_slot 为 null——如 /session 切换后首保存、“查看对方活槽 →
136
- // 保存 fork 新槽”的落盘槽跟随)写本端记录——marker = 端内最后认领者;粘性 _slot 期间
137
- // 的日常保存不写(F1:保存永不参与竞争)
138
- const claimedNow = agent._slot == null
139
- const slot = agent._slot ??= activeSlot(agent.cwd)
140
- if (claimedNow) writeEndMarker(agent.cwd, slot)
141
- const p = slotPath(agent.cwd, slot)
142
- // 首保存补绑(§14.3.4 表末行——兜底所有未覆盖路径):未绑定且有槽 → 绑定记录存储
143
- // (baseHistory = 当前 _fullHistory 全量;identity = 当前 _sessionStart)
144
- if (!agent._recordStore) {
145
- bindRecordStore(agent, { slotFile: p, identity: agent._sessionStart ?? null, baseHistory: agent._fullHistory ?? [] })
146
- }
147
- // 2026-08-31 会诊 F2 🔴:写前校验磁盘文件的 sessionStart——与本进程会话不符(另一
148
- // 进程/会话的现场)→ 先轮转 .bak 保留再写(11311 条历史被新进程覆盖的实锤场景)。
149
- // 守卫自 2026-09-08 提取为 guardForeignSlotFile(session-guard.mjs——DESIGN-TOKEN-
150
- // SETTLEMENT D1 二次拆分:先入 session-slots、再因 500 行硬限迁 session-guard)——
151
- // saveSession 与 token-ttl persistEngTokens(settle 当场落盘)共用同一份(检查按
152
- // mtime 缓存 _slotMtime:自写未变跳过全量解析)。
153
- const rotated = guardForeignSlotFile(agent, p, slot)
154
- // 绑定态 → 流式投影(段原文拼接——VSC 兼容面逐字同形);未绑定 → 既有全量物化写
155
- if (agent._recordStore) saveProjectedSlot(agent, p, fields, contextHistory)
156
- else writeSessionFile(p, { ...fields, history: legacyHistory(agent), contextHistory })
157
- // 记录我们刚写的 mtime——下次保存跳过重复解析
158
- try { agent._slotMtime = statSync(p).mtimeMs } catch {}
159
- // Update slot metadata in manifest
160
- try {
161
- const m = loadManifest(agent.cwd)
162
- m.slots[slot] = agent._recordStore
163
- ? digestFromStore(fields, agent._recordStore.counters())
164
- : slotDigest({ ...fields, history: legacyHistory(agent) })
165
- saveManifest(agent.cwd, m)
166
- } catch (e) {
167
- // Manifest update failure is non-fatal — data is safe, metadata will lazy-recover on next listSlots
168
- console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)
169
- }
170
- return rotated
171
- }
172
-
173
- /** Load slot file with shared validation — 2026-08-31 会诊 deepseek 🟡 抽取:
174
- * loadSession(active 槽)/switchToSlot/ACP session/load 共享。无认领副作用。
175
- * version 1/2 + history 数组 + cwd 匹配(2026-09-01 会诊 kimi 🔵:cwd 先行——"别人的
176
- * 文件不动"优先于结构校验,异 cwd + 坏 version 不得改名);结构不符改名 .unreadable、
177
- * 解析失败 .tmp 回退成功后提升为正主(损坏主文件改名 .corrupted 保留)、主文件缺失
178
- * 时恢复孤儿 .tmp(rename 前崩溃现场)。 */
179
- export function loadSlotFile(cwd, slot) {
180
- const p = slotPath(cwd, slot)
181
- const tryLoad = (path) => {
182
- try {
183
- if (!existsSync(path)) return null
184
- const data = JSON.parse(readFileSync(path, "utf8"))
185
- if (data.cwd && data.cwd.toLowerCase() !== cwd.toLowerCase()) return null // 别人的文件,不动
186
- if (data?.version !== 1 && data?.version !== 2) {
187
- if (typeof data?.version === "number" && data.version > 2) return null // 新版 CLI 的文件,不动(F2 轮转兜底)
188
- try { renameSync(path, `${path}.unreadable`) } catch {}
189
- console.error(`[session] unsupported version ${data?.version} at ${path} — preserved as ${basename(path)}.unreadable`)
190
- return null
191
- }
192
- if (!Array.isArray(data.history)) {
193
- try { renameSync(path, `${path}.unreadable`) } catch {}
194
- console.error(`[session] slot file ${path}: history is not an array — preserved as ${basename(path)}.unreadable`)
195
- return null
196
- }
197
- data.history = data.history.filter((m) => !isLegacyTransient(m))
198
- return data
199
- } catch (e) {
200
- return { _error: e }
201
- }
202
- }
203
-
204
- let result = tryLoad(p)
205
- if (result && !result._error) return result
206
- if (result?._error) {
207
- console.error(`[session] failed to load slot ${slot}: ${result._error.message}. Trying .tmp fallback...`)
208
- const tmpResult = tryLoad(`${p}.tmp`)
209
- if (tmpResult && !tmpResult._error) {
210
- console.error(`[session] recovered from .tmp fallback`)
211
- // 2026-09-01 会诊 🟢:.tmp 提升为正主(损坏主文件改名 .corrupted 保留现场)——
212
- // 否则主文件留在原地,每次加载都重复"恢复",且保存侧 F2 会持续轮转它。
213
- try {
214
- renameSync(p, `${p}.corrupted`)
215
- renameSync(`${p}.tmp`, p)
216
- } catch {}
217
- return tmpResult
218
- }
219
- console.error(`[session] .tmp fallback also failed — session lost.`)
220
- try { renameSync(p, `${p}.corrupted`) } catch {}
221
- } else if (!result) {
222
- // 2026-09-01 advisor 🔵:主文件缺失但孤儿 .tmp 存在(原子写 rename 前崩溃)——
223
- // 恢复并提升为正主(SESSION.md §4 的 .tmp 回退语义应覆盖此场景)。
224
- const orphan = tryLoad(`${p}.tmp`)
225
- if (orphan && !orphan._error) {
226
- console.error(`[session] slot ${slot} main file missing — recovered orphan .tmp`)
227
- try { renameSync(`${p}.tmp`, p) } catch {}
228
- return orphan
229
- }
230
- }
231
- return null
232
- }
233
-
234
- /** Load session data for this cwd — 2026-09-05 §10 D-2:平移为 resumeSlot 的数据包装
235
- * (签名不变——测试兼容;读槽校验/.tmp 回退/legacy 兜底语义原样保留在 resumeSlot 的
236
- * data 层)。恢复目标选择按本端记录(end marker)——manifest active 仅作无记录端的
237
- * 一次性继承源(D-6),不再作本端恢复第一依据。 */
238
- export function loadSession(cwd) {
239
- return resumeSlot(cwd).data
240
- }
241
-
242
- /** slimForDisplay 截断的 arguments 以 U+2026(…)结尾——不是合法 JSON 的完整值。
243
- * v1 老文件回退播种机器线时置为 {}(合法空参数),防止半截 \\uXXXX 毒化发送载荷(会诊 F6)。 */
244
- function stripTruncatedToolArgs(m) {
245
- if (m?.role !== "assistant" || !Array.isArray(m.tool_calls)) return m
246
- let changed = false
247
- const tool_calls = m.tool_calls.map((tc) => {
248
- const args = tc?.function?.arguments
249
- if (typeof args === "string" && args.endsWith("…")) {
250
- changed = true
251
- return { ...tc, function: { ...tc.function, arguments: "{}" } }
252
- }
253
- return tc
254
- })
255
- return changed ? { ...m, tool_calls } : m
256
- }
257
-
258
- /** Apply loaded session data onto an agent object; returns true if the runtime composite
259
- * was switched. 2026-08-31 会诊 F1:清空 _slot/_slotMtime 缓存——切换后下次保存重新认领。
260
- *
261
- * MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
262
- * "三支"误读):
263
- * ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel,
264
- * legacy 槽 null/空串回该渠道默认模型 `providers[].model`(单值——2026-09-10 MODEL-SELECTION
265
- * v2;老 models[] 首候选形态已退场))+ 重算
266
- * compactThreshold(auto 时——按恢复后模型)——不看 config(defaultModel 只是新会话起点);
267
- * ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
268
- * 复验 validateProvider 弹重选)。
269
- * 删旧支:"activeModel==null 清 stale override 回渠道默认"——前提 = 渠道默认字段——已随
270
- * 三旧层删除消失——双字段恒非空故不可能触发(评审 #7)。 */
271
- export function applySession(agent, data, opts = {}) {
272
- // 机读线语义(历史注释保留):data.history = FULL 未压缩记录(人读线);data.contextHistory =
273
- // 压缩后机器线——恢复各从其源(保留压缩收益——从完整 history 重建会塞回中间过程——实测
274
- // prompt 膨胀 283%)。v1 老文件(无 contextHistory)回退播种,剥离被 slimForDisplay 截断的
275
- // tool_calls.arguments(… 结尾 → 置 {}——截断可劈断 \\uXXXX 产生 400 毒载荷)。length > 0 才
276
- // 当机读线(contextHistory: [] 是"无机读线"而非空机器线——空机器线静默丢全部上下文)。
277
- agent.config ??= {} // ACP test mocks may omit config; be defensive like the ??= below
278
- const full = Array.isArray(data.history) ? data.history : []
279
- // SESSION.md §11.2(2026-09-08——F2/F3):恢复事件按当前 data 重定——载入历史非空 = 会话
280
- // 恢复 → 下个 depth-0 回合 env-state resumed:yes 一次(pushEnvStateReminder 读即清——每次恢复
281
- // 一次)。/new 与空历史槽切换不武装(显式清 false)——无恢复事件恒 no(F3 伪触发消除——不再靠
282
- // _sessionStart != null 推断;已武装 agent 切到空历史槽也不残留上次恢复事件——评审 round2 🟡)。
283
- // _processRestartPending 不在此清——由启动路径(bin/thincoder.mjs)设、prepareRun 发句清
284
- // (N6——双信号独立:切槽不报进程重启)。
285
- // 跨端异名互指(结构债批 5 N7——双端同语义各自独立实现、命名不统一是刻意——SESSION.md
286
- // §11.2):VSC 端同机制载体异名 = agent._resumedPending(src/agent/setup.mjs hydrateRun——
287
- // restore:true 工厂 + fullHistory 非空时武装——每槽恢复一次)+ 模块级 restartDetectionDone
288
- // 一次性闸(src/agent/setup-reminders.mjs——process restarted 句的进程级信号)。
289
- agent._envResumed = full.length > 0
290
- const ch = data.contextHistory
291
- const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
292
- // 绑定态(opts.slot 在场):人读线 = 记录存储尾窗(下方绑定块填充)——不复制全量 JSON 数组
293
- agent._fullHistory = opts.slot != null ? [] : [...full]
294
- agent.history = [...machine]
295
- agent.title = data.title ?? ""
296
- agent.tasks = data.tasks ?? []
297
- agent.planMode = data.planMode ?? false
298
- agent.autoApprove = data.autoApprove ?? false
299
- agent.goal = data.goal ?? null
300
- agent._pendingReminders = data.pendingReminders ?? []
301
- agent._sessionStart = data.sessionStart ?? null
302
- // R16 token 字段恢复(过期过滤——见 token-ttl.mjs restoreEngTokens)
303
- restoreEngTokens(agent, data)
304
- // engineering is session-level (2026-08-29): the slot value is the CLI session's authority
305
- // — config.json is only the initial default / cross-end mirror. A legacy slot without the
306
- // field keeps whatever config.json seeded (unchanged behavior).
307
- if (data.engineering !== undefined) {
308
- agent.config.agent ??= {}
309
- agent.config.agent.engineering = data.engineering === true
310
- }
311
- if (data.advisor) {
312
- agent.config.advisor = { ...data.advisor }
313
- }
314
- // Reset stall/compaction state on session switch
315
- agent._compressFailures = 0
316
- agent._verifyRetries = 0
317
- agent._verifyPassed = false
318
- agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
319
- agent._slotMtime = null
320
- // ── 记录存储绑定(TUI-OOM-ROOTCAUSE——§14.3.4/§14.3.5)──
321
- // opts.slot 在场(启动恢复 / 非占用的 /session 切换 / ACP 钉槽)→ 绑定(身份核验 + 对账 +
322
- // 窗口)——人读线 = store.tail(200);未传(模式 F:被他人活进程占用的槽 / 测试 / ACP fork)
323
- // → 解绑——人读线维持全量数组(D-R6 零回归)。
324
- if (opts.slot != null) {
325
- const store = bindRecordStore(agent, {
326
- slotFile: slotPath(agent.cwd, opts.slot),
327
- identity: data.sessionStart ?? null,
328
- baseHistory: full,
329
- })
330
- agent._fullHistory = store.tail(RECORD_WINDOW_MESSAGES)
331
- } else {
332
- unbindRecordStore(agent)
333
- }
334
- // ── MODEL-MERGE-SESSION 恢复(F-4——两支)──
335
- const slotProvider = data.activeProvider ? agent.providers?.find((pr) => pr.name === data.activeProvider) : null
336
- if (slotProvider) {
337
- // ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空——legacy 槽 activeModel
338
- // null/空串 = 无 override——回该渠道默认模型(`providers[].model` 单值——M3)。
339
- // F-2d(MODEL-400-FIX):`||` 非 `??`——空串也兜(activeModel="" 的槽会让 `??` 不落链 →
340
- // 空槽 model 恒有值,provider.model 键不缺失)
341
- const prevName = agent.activeProvider
342
- const prevModel = agent.activeModel ?? null
343
- const slotModel = data.activeModel || slotProvider.model
344
- const switched = prevName !== slotProvider.name || prevModel !== slotModel
345
- agent.activeProvider = slotProvider.name
346
- agent.activeModel = slotModel
347
- agent.provider = { ...slotProvider }
348
- if (slotModel) agent.provider.model = slotModel
349
- // 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)
350
- if (switched && agent.config?.agent?.compactThresholdAuto && agent.provider.model) {
351
- agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
352
- }
353
- return switched
354
- }
355
- // ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
356
- // config/defaultModel 运行时;两方都无效由调用侧复验 validateProvider 弹重选)
357
- return false
358
- }
359
-
360
- /**
361
- * Create a new session slot: allocate a free slot number,
362
- * write an empty session, and mark it as the active slot.
363
- * No limit on the number of sessions.
364
- */
365
- export function newSession(cwd) {
366
- const m = loadManifest(cwd)
367
-
368
- // 2026-09-01 advisor 🔵:先清理死主条目(与 ensureActive 分支2 语义一致)——否则
369
- // "死主 + 文件缺失"的空槽(m.slots 有条目、无文件、属主已死)永不复用,槽号持续
370
- // 增长。死主且无文件 = 该会话从未落盘(进程死了没保存),条目可安全删除回收。
371
- // 2026-09-01 会诊 deepseek/glm 🟡:清理必须经 deletions 显式落盘——saveManifest 条目级
372
- // 合并会把磁盘死条目从 fresh 复活回写,仅传 m 等于没删(VS Code newSlot 已修,CLI 对称)。
373
- const mySessionId = getSessionId()
374
- const deadSlots = []
375
- for (const [s, owner] of Object.entries(m.slotSessions ?? {})) {
376
- if (owner && owner !== mySessionId) {
377
- const pid = parseInt(owner.split("-")[0])
378
- if (!pid || !isProcessAlive(pid)) {
379
- delete m.slotSessions[s]
380
- if (!existsSync(slotPath(cwd, Number(s)))) delete m.slots[s]
381
- deadSlots.push(s)
382
- }
383
- }
384
- }
385
-
386
- // Find next available slot number — 2026-08-31 会诊 deepseek 🟡:不能只看 manifest
387
- // 条目(丢失更新可能让条目消失而文件仍在)——复用该号会直接覆写真实会话
388
- // (F2 防护不覆盖 newSession 的空数据直写)。
389
- // 2026-08-31 advisor round2 🟡:同时跳过"已被另一活进程认领但尚未落盘"的号
390
- // (slotSessions 有条目、slots 无条目、文件不存在)——否则双进程会认领同一号。
391
- const liveClaimed = (n) => {
392
- const owner = m.slotSessions?.[n]
393
- return !!(owner && owner !== mySessionId && isProcessAlive(parseInt(owner.split("-")[0])))
394
- }
395
- let slot = 1
396
- while (m.slots[slot] || existsSync(slotPath(cwd, slot)) || liveClaimed(slot)) slot++
397
-
398
- // Write empty session — 2026-09-01 advisor 🔵:补 contextHistory/planMode 字段与
399
- // VS Code newSlot 对齐(SESSION.md §3 v2 格式双端一致;两端读侧均有兜底,功能等价)
400
- const data = { version: 2, cwd, title: "", updatedAt: Date.now(), history: [], contextHistory: [], tasks: [], planMode: false, goal: null, autoApprove: false, advisor: null, pendingReminders: [], sessionStart: null }
401
- writeSessionFile(slotPath(cwd, slot), data)
402
- m.slots[slot] = slotDigest(data)
403
- m.active = slot
404
- // 2026-08-31 advisor round1 🟡:与 ensureActive 认领模型一致——立即记录新 slot 所有权,
405
- // 否则 /new 后到首次保存之间并发方(VS Code/另一 CLI)会把新 active 槽当"空闲可恢复"
406
- // 认领 → 双进程写同一槽(F2 轮转互旋)。
407
- m.slotSessions ??= {}
408
- m.slotSessions[slot] = mySessionId
409
- // 2026-09-01 会诊三家 🟡:显式翻 active 的调用点传 setActive(saveManifest 默认保留
410
- // 磁盘 fresh.active,避免把并发方刚翻的指针回滚)
411
- // 2026-09-01 会诊 deepseek/glm 🟡:deletions 过滤掉本调用刚重新认领的槽(防删掉自己的
412
- // 新属主)——与 ensureActive deadParam / VS Code newSlot 同型。
413
- const deletions = deadSlots.length
414
- ? {
415
- slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId),
416
- slots: deadSlots.filter((s) => !m.slots[s]),
417
- }
418
- : null
419
- saveManifest(cwd, m, deletions, { setActive: true })
420
- // 2026-09-05 §10 D-4:/new 落点写本端记录(显式切换跟随——T-M8)
421
- writeEndMarker(cwd, slot)
422
- return slot
423
- }
424
-
425
- /** 清空会话运行态(/new 用,2026-08-31 会诊 F3):_fullHistory/title/_sessionStart 等
426
- * 全部会话级状态 + 一次性注入标志必须全清——否则新会话首次落盘把旧会话完整人类线 +
427
- * 旧标题写进新槽(实锤 .19/.3 双副本);注入标志不清则 /new 后新会话永不注入
428
- * OS/cwd reminder(2026-09-01 会诊 glm 🟡)。autoApprove 是用户偏好,跨会话保留(有意)。 */
429
- export function resetSessionState(agent) {
430
- agent._fullHistory = []
431
- agent.history = []
432
- agent.title = ""
433
- agent.tasks = []
434
- agent._sessionStart = null
435
- // DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——不再维护
436
- agent._engDesignTokens = new Map() // multi-design slots die with the session (2026-09-01 fix #2)
437
- agent._compressFailures = 0
438
- agent._verifyRetries = 0
439
- agent._verifyPassed = undefined
440
- agent._runStartHistoryLen = 0
441
- agent._lastPromptTokens = null
442
- agent._usageAtLen = null
443
- agent.planMode = false
444
- agent.goal = null
445
- agent._pendingReminders = []
446
- agent._slot = null
447
- agent._slotMtime = null
448
- agent._osReminderInjected = false
449
- // §11.2(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
450
- // _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
451
- agent._envResumed = false
452
- agent._processRestartPending = false
453
- agent._lastEngState = false
454
- // /new 或切换:记录存储解绑(新槽由 cmd-new / 首保存补绑重新挂载——§14.3.4)
455
- unbindRecordStore(agent)
456
- }
457
-
458
- /** Switch the manifest active pointer to a slot. Returns the slot's session data
459
- * (null if the slot doesn't exist / can't be read). 2026-08-31 会诊 deepseek 🔴:
460
- * 原实现经 loadSession → activeSlot 有认领副作用——目标槽被活进程占用时 ensureActive
461
- * 分支 3 会把 active 拨到新空槽并读回 null + 劫持对方指针。现改为 loadSlotFile 直接读
462
- * (无认领副作用);目标槽空闲则一并认领、被另一活进程占用则不认领(slotOccupancy——
463
- * 下次保存经 activeSlot 自然 fork 到新槽);只改 manifest 指针(setActive 意图)。
464
- * 2026-09-01 会诊三家 🟡:saveManifest 条目级合并 + setActive(不把并发方刚翻的指针回滚)。 */
465
- export function switchToSlot(cwd, slot) {
466
- const m = loadManifest(cwd)
467
- if (!m.slots[slot]) return null
468
- const data = loadSlotFile(cwd, slot)
469
- if (!data) return null
470
- m.active = slot
471
- const occ = slotOccupancy(cwd, slot)
472
- if (!occ.occupied) {
473
- m.slotSessions ??= {}
474
- m.slotSessions[slot] = getSessionId()
475
- }
476
- saveManifest(cwd, m, null, { setActive: true })
477
- // 2026-09-05 §10 D-4:/session N 跟随"最后查看的槽"(成功切换才写)
478
- writeEndMarker(cwd, slot)
479
- return data
480
- }
481
-
482
- /** 目标槽是否被另一活进程占用(2026-09-01 会诊/advisor 🟡):排除本进程属主——
483
- * /session 重选当前槽不误报;同进程双会话防护由 ACP sameProcessPinned 承担。 */
484
- export function slotOccupancy(cwd, slot) {
485
- const m = loadManifest(cwd)
486
- const owner = m.slotSessions?.[slot]
487
- if (!owner) return { occupied: false }
488
- if (owner === getSessionId()) return { occupied: false }
489
- const pid = parseInt(owner.split("-")[0])
490
- if (!pid || !isProcessAlive(pid)) return { occupied: false }
491
- return { occupied: true, owner }
492
- }
package/src/skills.mjs DELETED
@@ -1,153 +0,0 @@
1
- /**
2
- * skills.mjs — skill system
3
- * Discovers .md skill files AND subdirectory SKILL.md from .thincoder/skills/ directory,
4
- * injects into system prompt for agent to load on demand.
5
- * Use the skill tool to activate a specific skill; content is written into conversation history wrapped in <skill-loaded>.
6
- *
7
- * Supported formats:
8
- * .thincoder/skills/my-skill.md (flat, name = "my-skill")
9
- * .thincoder/skills/my-skill/SKILL.md (subdirectory, name = "my-skill")
10
- */
11
-
12
- import { readFile, readdir, stat } from "node:fs/promises"
13
- import { join } from "node:path"
14
- import { homedir } from "node:os"
15
-
16
- /** Valid skill name pattern (alphanumeric + hyphens/underscores) */
17
- const NAME_RE = /^[a-zA-Z0-9_-]+$/
18
-
19
- /**
20
- * Try to read a skill from a path, return { name, path, description } or null.
21
- */
22
- async function tryReadSkill(dir, name, filePath) {
23
- try {
24
- const s = await stat(filePath)
25
- if (!s.isFile()) return null
26
- const head = await readFile(filePath, "utf8")
27
- const body = head.slice(0, 400).split("\n")
28
- let desc = ""
29
- let inFrontmatter = false
30
- for (const line of body) {
31
- const t = line.trim()
32
- if (t === "---") { inFrontmatter = !inFrontmatter; continue }
33
- if (inFrontmatter) continue
34
- if (t && !t.startsWith("#")) {
35
- desc = t.slice(0, 120)
36
- break
37
- }
38
- }
39
- return { name, path: filePath, description: desc || "(no description)" }
40
- } catch {
41
- return null
42
- }
43
- }
44
-
45
- /**
46
- * Scan a single skills directory, return skill list.
47
- * Supports flat .md files and subdirectories with SKILL.md inside.
48
- * Each skill: { name, path, description } — name derived from filename or directory.
49
- * Returns empty array if directory is missing or empty.
50
- */
51
- async function loadSkillsFromDir(dir) {
52
- let entries
53
- try {
54
- entries = await readdir(dir, { withFileTypes: true })
55
- // Deterministic order: readdir order is filesystem-dependent; an unsorted scan would
56
- // reshuffle the skills listing → system prompt byte change with zero content change
57
- // (2026-08-16 cache audit — that silently misses the provider prefix cache).
58
- entries.sort((a, b) => a.name.localeCompare(b.name))
59
- } catch {
60
- return []
61
- }
62
- const skills = []
63
- const added = new Set()
64
-
65
- // Pass 1: subdirectories (higher priority — standard convention)
66
- for (const entry of entries) {
67
- if (!entry.isDirectory()) continue
68
- if (!NAME_RE.test(entry.name)) continue
69
- const skill = await tryReadSkill(dir, entry.name, join(dir, entry.name, "SKILL.md"))
70
- if (skill) { skills.push(skill); added.add(entry.name) }
71
- }
72
-
73
- // Pass 2: flat .md files (backward compat; skipped if subdirectory with same name exists)
74
- for (const entry of entries) {
75
- if (!entry.isFile()) continue
76
- const m = entry.name.match(/^([a-zA-Z0-9_-]+)\.md$/)
77
- if (!m) continue
78
- const name = m[1]
79
- if (added.has(name)) continue
80
- const skill = await tryReadSkill(dir, name, join(dir, entry.name))
81
- if (skill) { skills.push(skill); added.add(name) }
82
- }
83
- return skills
84
- }
85
-
86
- /**
87
- * Scan both project-level and user-level skills directories.
88
- * Project-level skills (cwd/.thincoder/skills/) take priority over user-level (~/.thincoder/skills/).
89
- * Returns merged skill list with project-level skills first.
90
- */
91
- export async function loadSkills(cwd) {
92
- const projectDir = join(cwd, ".thincoder", "skills")
93
- const userDir = join(homedir(), ".thincoder", "skills")
94
-
95
- // Load project-level skills first (higher priority)
96
- const projectSkills = await loadSkillsFromDir(projectDir)
97
- const added = new Set(projectSkills.map(s => s.name))
98
-
99
- // Load user-level skills, skipping duplicates
100
- const userSkills = await loadSkillsFromDir(userDir)
101
- for (const skill of userSkills) {
102
- if (!added.has(skill.name)) {
103
- projectSkills.push(skill)
104
- added.add(skill.name)
105
- }
106
- }
107
-
108
- return projectSkills
109
- }
110
-
111
- /**
112
- * Generate skill listing text for system prompt injection.
113
- * At most 3 (small footprint); overflow marked "... and N more".
114
- * Prefixed with DISREGARD: when listing refreshes (skills added/removed), old listings are auto-invalidated without needing to delete history (inspired by kimi-code).
115
- */
116
- export function formatSkillListing(skills) {
117
- if (skills.length === 0) return ""
118
- const listed = skills.slice(0, 3)
119
- const lines = listed.map((s) => `- **${s.name}**: ${s.description}`)
120
- if (skills.length > 3) lines.push(` ... and ${skills.length - 3} more`)
121
- return "DISREGARD any earlier skill listings. Current available skills (use the skill tool to load one):\n" + lines.join("\n")
122
- }
123
-
124
- /**
125
- * Read the full content of a specific skill file.
126
- * Tries project-level directory first, then user-level directory.
127
- * For each directory, tries subdirectory format (name/SKILL.md) first, then flat format (name.md).
128
- * Returns text, or null if not found.
129
- */
130
- export async function readSkill(cwd, name) {
131
- if (!NAME_RE.test(name)) return null
132
-
133
- const dirs = [
134
- join(cwd, ".thincoder", "skills"),
135
- join(homedir(), ".thincoder", "skills")
136
- ]
137
-
138
- for (const baseDir of dirs) {
139
- // Try subdirectory format: name/SKILL.md
140
- try {
141
- const p = join(baseDir, name, "SKILL.md")
142
- return await readFile(p, "utf8")
143
- } catch { /* not found, try flat */ }
144
-
145
- // Fallback to flat format: name.md
146
- try {
147
- const p = join(baseDir, `${name}.md`)
148
- return await readFile(p, "utf8")
149
- } catch { /* not found, try next directory */ }
150
- }
151
-
152
- return null
153
- }