@thincoder/core 0.9.1

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 (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
package/skills.mjs ADDED
@@ -0,0 +1,234 @@
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 { readdirSync, readFileSync, statSync } from "node:fs"
14
+ import { join } from "node:path"
15
+ import { homedir } from "node:os"
16
+
17
+ /** Valid skill name pattern (alphanumeric + hyphens/underscores) */
18
+ const NAME_RE = /^[a-zA-Z0-9_-]+$/
19
+
20
+ /**
21
+ * Try to read a skill from a path, return { name, path, description } or null.
22
+ */
23
+ async function tryReadSkill(dir, name, filePath) {
24
+ try {
25
+ const s = await stat(filePath)
26
+ if (!s.isFile()) return null
27
+ const head = await readFile(filePath, "utf8")
28
+ const body = head.slice(0, 400).split("\n")
29
+ let desc = ""
30
+ let inFrontmatter = false
31
+ for (const line of body) {
32
+ const t = line.trim()
33
+ if (t === "---") { inFrontmatter = !inFrontmatter; continue }
34
+ if (inFrontmatter) continue
35
+ if (t && !t.startsWith("#")) {
36
+ desc = t.slice(0, 120)
37
+ break
38
+ }
39
+ }
40
+ return { name, path: filePath, description: desc || "(no description)" }
41
+ } catch {
42
+ return null
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Scan a single skills directory, return skill list.
48
+ * Supports flat .md files and subdirectories with SKILL.md inside.
49
+ * Each skill: { name, path, description } — name derived from filename or directory.
50
+ * Returns empty array if directory is missing or empty.
51
+ */
52
+ async function loadSkillsFromDir(dir) {
53
+ let entries
54
+ try {
55
+ entries = await readdir(dir, { withFileTypes: true })
56
+ // Deterministic order: readdir order is filesystem-dependent; an unsorted scan would
57
+ // reshuffle the skills listing → system prompt byte change with zero content change
58
+ // (2026-08-16 cache audit — that silently misses the provider prefix cache).
59
+ entries.sort((a, b) => a.name.localeCompare(b.name))
60
+ } catch {
61
+ return []
62
+ }
63
+ const skills = []
64
+ const added = new Set()
65
+
66
+ // Pass 1: subdirectories (higher priority — standard convention)
67
+ for (const entry of entries) {
68
+ if (!entry.isDirectory()) continue
69
+ if (!NAME_RE.test(entry.name)) continue
70
+ const skill = await tryReadSkill(dir, entry.name, join(dir, entry.name, "SKILL.md"))
71
+ if (skill) { skills.push(skill); added.add(entry.name) }
72
+ }
73
+
74
+ // Pass 2: flat .md files (backward compat; skipped if subdirectory with same name exists)
75
+ for (const entry of entries) {
76
+ if (!entry.isFile()) continue
77
+ const m = entry.name.match(/^([a-zA-Z0-9_-]+)\.md$/)
78
+ if (!m) continue
79
+ const name = m[1]
80
+ if (added.has(name)) continue
81
+ const skill = await tryReadSkill(dir, name, join(dir, entry.name))
82
+ if (skill) { skills.push(skill); added.add(name) }
83
+ }
84
+ return skills
85
+ }
86
+
87
+ /**
88
+ * Scan both project-level and user-level skills directories.
89
+ * Project-level skills (cwd/.thincoder/skills/) take priority over user-level (~/.thincoder/skills/).
90
+ * Returns merged skill list with project-level skills first.
91
+ */
92
+ export async function loadSkills(cwd) {
93
+ const projectDir = join(cwd, ".thincoder", "skills")
94
+ const userDir = join(homedir(), ".thincoder", "skills")
95
+
96
+ // Load project-level skills first (higher priority)
97
+ const projectSkills = await loadSkillsFromDir(projectDir)
98
+ const added = new Set(projectSkills.map(s => s.name))
99
+
100
+ // Load user-level skills, skipping duplicates
101
+ const userSkills = await loadSkillsFromDir(userDir)
102
+ for (const skill of userSkills) {
103
+ if (!added.has(skill.name)) {
104
+ projectSkills.push(skill)
105
+ added.add(skill.name)
106
+ }
107
+ }
108
+
109
+ return projectSkills
110
+ }
111
+
112
+ /**
113
+ * Generate skill listing text for system prompt injection.
114
+ * At most 3 (small footprint); overflow marked "... and N more".
115
+ * Prefixed with DISREGARD: when listing refreshes (skills added/removed), old listings are auto-invalidated without needing to delete history (inspired by kimi-code).
116
+ */
117
+ export function formatSkillListing(skills) {
118
+ if (skills.length === 0) return ""
119
+ const listed = skills.slice(0, 3)
120
+ const lines = listed.map((s) => `- **${s.name}**: ${s.description}`)
121
+ if (skills.length > 3) lines.push(` ... and ${skills.length - 3} more`)
122
+ return "DISREGARD any earlier skill listings. Current available skills (use the skill tool to load one):\n" + lines.join("\n")
123
+ }
124
+
125
+ /**
126
+ * Read the full content of a specific skill file.
127
+ * Tries project-level directory first, then user-level directory.
128
+ * For each directory, tries subdirectory format (name/SKILL.md) first, then flat format (name.md).
129
+ * Returns text, or null if not found.
130
+ */
131
+ export async function readSkill(cwd, name) {
132
+ if (!NAME_RE.test(name)) return null
133
+
134
+ const dirs = [
135
+ join(cwd, ".thincoder", "skills"),
136
+ join(homedir(), ".thincoder", "skills")
137
+ ]
138
+
139
+ for (const baseDir of dirs) {
140
+ // Try subdirectory format: name/SKILL.md
141
+ try {
142
+ const p = join(baseDir, name, "SKILL.md")
143
+ return await readFile(p, "utf8")
144
+ } catch { /* not found, try flat */ }
145
+
146
+ // Fallback to flat format: name.md
147
+ try {
148
+ const p = join(baseDir, `${name}.md`)
149
+ return await readFile(p, "utf8")
150
+ } catch { /* not found, try next directory */ }
151
+ }
152
+
153
+ return null
154
+ }
155
+
156
+ // ─── 同步 loader 面(#88——「同步 loader 面按核内结构归一」)────────────────────────────
157
+ // 语义与上方异步面**逐条同源**:同发现规则(扁平 `.md` + 子目录 `SKILL.md`)· 同排序
158
+ // (localeCompare——前缀缓存要求确定性)· 同层级优先(项目层 → 用户层,按名去重)。
159
+ // 端侧接核后若仍需同步面,直接用本面(不再自持副本——单一结构)。
160
+
161
+ /** 同步版读取单个 skill 条目(描述提取规则同异步 tryReadSkill)。 */
162
+ function tryReadSkillSync(name, filePath) {
163
+ try {
164
+ const s = statSync(filePath)
165
+ if (!s.isFile()) return null
166
+ const head = readFileSync(filePath, "utf8")
167
+ const body = head.slice(0, 400).split("\n")
168
+ let desc = ""
169
+ let inFrontmatter = false
170
+ for (const line of body) {
171
+ const t = line.trim()
172
+ if (t === "---") { inFrontmatter = !inFrontmatter; continue }
173
+ if (inFrontmatter) continue
174
+ if (t && !t.startsWith("#")) { desc = t.slice(0, 120); break }
175
+ }
176
+ return { name, path: filePath, description: desc || "(no description)" }
177
+ } catch {
178
+ return null
179
+ }
180
+ }
181
+
182
+ /** 同步版扫描单目录(Pass1 子目录 → Pass2 扁平;排序 / 去重规则同异步 loadSkillsFromDir)。 */
183
+ function loadSkillsFromDirSync(dir) {
184
+ let entries
185
+ try {
186
+ entries = readdirSync(dir, { withFileTypes: true })
187
+ entries.sort((a, b) => a.name.localeCompare(b.name))
188
+ } catch {
189
+ return []
190
+ }
191
+ const skills = []
192
+ const added = new Set()
193
+
194
+ for (const entry of entries) {
195
+ if (!entry.isDirectory()) continue
196
+ if (!NAME_RE.test(entry.name)) continue
197
+ const skill = tryReadSkillSync(entry.name, join(dir, entry.name, "SKILL.md"))
198
+ if (skill) { skills.push(skill); added.add(entry.name) }
199
+ }
200
+
201
+ for (const entry of entries) {
202
+ if (!entry.isFile()) continue
203
+ const m = entry.name.match(/^([a-zA-Z0-9_-]+)\.md$/)
204
+ if (!m) continue
205
+ const name = m[1]
206
+ if (added.has(name)) continue
207
+ const skill = tryReadSkillSync(name, join(dir, entry.name))
208
+ if (skill) { skills.push(skill); added.add(name) }
209
+ }
210
+ return skills
211
+ }
212
+
213
+ /** 同步 loader(项目层优先 → 用户层;返回形态与异步 loadSkills 等价)。 */
214
+ export function loadSkillsSync(cwd) {
215
+ const skills = loadSkillsFromDirSync(join(cwd, ".thincoder", "skills"))
216
+ const added = new Set(skills.map((s) => s.name))
217
+ for (const skill of loadSkillsFromDirSync(join(homedir(), ".thincoder", "skills"))) {
218
+ if (!added.has(skill.name)) { skills.push(skill); added.add(skill.name) }
219
+ }
220
+ return skills
221
+ }
222
+
223
+ /** 同步 readSkill(name 校验 + `name/SKILL.md` → `name.md`,项目层 → 用户层——同异步)。 */
224
+ export function readSkillSync(cwd, name) {
225
+ if (!NAME_RE.test(name)) return null
226
+ for (const baseDir of [join(cwd, ".thincoder", "skills"), join(homedir(), ".thincoder", "skills")]) {
227
+ for (const filePath of [join(baseDir, name, "SKILL.md"), join(baseDir, `${name}.md`)]) {
228
+ try {
229
+ if (statSync(filePath).isFile()) return readFileSync(filePath, "utf8")
230
+ } catch { /* try the next candidate */ }
231
+ }
232
+ }
233
+ return null
234
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * text-budget.mjs — 文本额度纯函数(零依赖——TUI-OOM-ROOTCAUSE 批,AGENT-LOOP.md §23.3.1 / TUI.md §15.3.2)。
3
+ *
4
+ * 单一来源(D2):agent 侧捕获截断(spawn-child.mjs——子代理 `_capturedOutput`)与 TUI 面
5
+ * 载体额度(tui/display-budget.mjs)共用本体的 `capText` / `appendCappedText`——两处各自
6
+ * 复制截断逻辑的漂移面被消除。本模块零 import(可被任意层直接引用)。
7
+ *
8
+ * 计量口径 = **UTF-16 码元**(`String.length`——与 JS 字符串内存近似、确定、O(1) 计长;
9
+ * TUI.md §15.2 表 1)。截断形态 = 头保 + 中段标记 + 尾保(标记含真实省略数 N)。
10
+ *
11
+ * 标记串约定:`marker` 模板中以字面 `N` 为省略数占位(逐字进测试断言——如
12
+ * `… [captured output truncated: N chars omitted] …`)。
13
+ */
14
+
15
+ /** 省略数占位符替换(首个 `N`——模板约定;TUI 面 capLines 等复用)。 */
16
+ export function fillMarker(marker, omitted) {
17
+ if (typeof marker === "function") return String(marker(omitted))
18
+ const tpl = String(marker ?? "")
19
+ const i = tpl.indexOf("N")
20
+ return i === -1 ? `${tpl} (${omitted} omitted)` : `${tpl.slice(0, i)}${omitted}${tpl.slice(i + 1)}`
21
+ }
22
+
23
+ /**
24
+ * 头尾保真截断:`text` ≤ `max` 时零拷贝原样返回;超限 → 头 `keepHead` + 标记 + 尾 `keepTail`。
25
+ * keepHead/keepTail 各自夹紧到文本长度(互不重叠——头 + 尾 ≤ 文本长)。
26
+ * @returns {string} 原串或截断串(含 `marker` 替换后的省略数)
27
+ */
28
+ export function capText(text, { max, keepHead = 0, keepTail = 0, marker = "" } = {}) {
29
+ const t = String(text ?? "")
30
+ if (!(max > 0) || t.length <= max) return t
31
+ const head = Math.max(0, Math.min(keepHead, t.length))
32
+ const tail = Math.max(0, Math.min(keepTail, t.length - head))
33
+ const omitted = t.length - head - tail
34
+ return t.slice(0, head) + fillMarker(marker, omitted) + (tail > 0 ? t.slice(t.length - tail) : "")
35
+ }
36
+
37
+ /**
38
+ * 流式累积(滞后水位——摊还 O(1)):`prev + add`;超 `hard` 时立即裁至
39
+ * 头 `head` + 标记 + 尾 `tail`(下一轮从截断值继续累积——稳态长度 ≤ hard)。
40
+ * @returns {string} 合并(可能已截断)的累积串
41
+ */
42
+ export function appendCappedText(prev, add, { hard, head = 0, tail = 0, marker = "" } = {}) {
43
+ const merged = String(prev ?? "") + String(add ?? "")
44
+ if (!(hard > 0) || merged.length <= hard) return merged
45
+ return capText(merged, { max: hard, keepHead: head, keepTail: tail, marker })
46
+ }
47
+
48
+ /**
49
+ * UTF-16 safe HEAD slice(§2.5 #76 / #164 并入——VSC 侧实现归位;VSC 与 CLI 两处独立实现
50
+ * 的安全规则一致):plain `slice(0, N)` cuts BY UTF-16 CODE UNIT —— a surrogate pair
51
+ * straddling the boundary leaves a LONE high surrogate (U+D800-DBFF) that strict UTF-16
52
+ * decoders reject. When the cut point lands on a high surrogate, step back one code unit
53
+ * (the pair is dropped whole).
54
+ */
55
+ export function safeSliceUTF16(text, max) {
56
+ const t = String(text ?? "")
57
+ if (t.length <= max) return t
58
+ const cp = t.charCodeAt(max - 1)
59
+ if (cp >= 0xd800 && cp <= 0xdbff) return t.slice(0, max - 1)
60
+ return t.slice(0, max)
61
+ }
62
+
63
+ /**
64
+ * UTF-16 safe TAIL slice(safeSliceUTF16 的对称面——§2.5 #164 并入)。两个切点都要安全:
65
+ * - START:孤立低代理(U+DC00-DFFF)= 代理对被切开 ⇒ 前进一码元(整对丢弃);
66
+ * - END(原文末尾):末尾孤立高代理(U+D800-DBFF)也去掉——尾切片不得新增头切片规则
67
+ * 同样要避开的孤立代理(VSC 侧强化形态,随融合并入)。
68
+ */
69
+ export function safeSliceUTF16Tail(text, max) {
70
+ const t = String(text ?? "")
71
+ if (t.length <= max) return t
72
+ let start = t.length - max
73
+ const first = t.charCodeAt(start)
74
+ if (first >= 0xdc00 && first <= 0xdfff) start += 1 // 起点落低代理 → 丢弃代理对整体(向前一码元)
75
+ let tail = t.slice(start)
76
+ const last = tail.charCodeAt(tail.length - 1)
77
+ if (last >= 0xd800 && last <= 0xdbff) tail = tail.slice(0, -1) // 终点孤立高代理 → 去掉(截断边界安全)
78
+ return tail
79
+ }
package/token-ttl.mjs ADDED
@@ -0,0 +1,285 @@
1
+ /**
2
+ * token-ttl.mjs — design-token TTL 语义 + 槽位清理 + 会话槽权威台账 I/O(R16——2026-09-06;
3
+ * DESIGN-TOKEN-SETTLEMENT D1-D3——2026-09-08)。
4
+ *
5
+ * 共享目标(D-R16b):过期判定语义在此定义一次,validateDesignToken
6
+ * (agent-tools/design-token.mjs)、恢复过滤(session.mjs applySession)、开模式清理
7
+ * (agent-tools/eng.mjs + tui/cmd-eng.mjs ON 路径)、spawn 门禁过期拒删槽
8
+ * (subagent-spawn.mjs)共引。
9
+ *
10
+ * token 格式 = `uuid:expiresAt`(无签名流程凭证——HMAC 防伪层已删——见
11
+ * ENGINEERING-MODE.md 2026-09-06 段)。过期判定只对**格式合法**的 token 判
12
+ * 过期:格式/畸形串不在此清理(恢复时读回由门禁格式拒、门禁拒时也不删槽——
13
+ * 防误删有效槽——F-R16b ①③)。
14
+ *
15
+ * DESIGN-TOKEN-SETTLEMENT(2026-09-08):
16
+ * - **D1**:persistEngTokens = settle 当场落盘的可复用函数(engTokenSlotFields 序列化 +
17
+ * session 安全写/轮转——guardForeignSlotFile 与 saveSession 共用同一份守卫)。
18
+ * - **D2/D3**:readEngTokensFromSlot / reconcileEngTokensFromSlot = 门禁 miss 回读权威
19
+ * 槽(spawn 门 D2 与 dispatch 写门 D3 同源 reconcile)。
20
+ * - **D3**:**单值镜像 `_engDesignToken` 退役**——AC3 零写 + 仅一次性迁移读。本模块
21
+ * 不再写镜像(engTokenSlotFields 只产多槽表);restoreEngTokens 是唯一迁移读点(旧
22
+ * slot 残留 engDesignToken → 迁入 Map,此后不写不读镜像)。
23
+ *
24
+ * 依赖方向:本模块只 import 底层槽 I/O(session-slots.mjs 原语 + session-guard.mjs 轮转
25
+ * 守卫)——session.mjs 同时 import 本模块(engTokenSlotFields/restoreEngTokens),形成
26
+ * 静态环,与既有的 session ↔ session-slots 环同构(函数声明实例化期已初始化、只函数体
27
+ * 内运行时使用——环安全)。
28
+ */
29
+
30
+ import { existsSync, readFileSync, statSync } from "node:fs"
31
+ import {
32
+ slotPath, writeSessionFile, activeSlot, loadManifest, saveManifest, slotDigest,
33
+ writeEndMarker,
34
+ } from "./session-slots.mjs"
35
+ // F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
36
+ import { guardForeignSlotFile } from "./session-guard.mjs"
37
+
38
+ const DESIGN_TOKEN_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
39
+
40
+ /** 格式校验 + 数值过期时刻:格式合法 → expiresAt 数值;格式/畸形 → null
41
+ * (与 validateDesignToken 的 fail-closed 判定同源——单一权威)。 */
42
+ export function tokenExpiryMs(token) {
43
+ if (!token || typeof token !== "string") return null
44
+ const parts = token.split(":")
45
+ if (parts.length !== 2) return null
46
+ const [uuid, expiresAt] = parts
47
+ if (!DESIGN_TOKEN_UUID_RE.test(uuid)) return null
48
+ if (!/^\d+$/.test(expiresAt)) return null
49
+ const expiry = parseInt(expiresAt, 10)
50
+ if (isNaN(expiry)) return null
51
+ return expiry
52
+ }
53
+
54
+ /** 过期判定——仅格式合法的 token 可判过期(畸形 → false,由门禁格式拒)。 */
55
+ export function tokenExpired(token, now = Date.now()) {
56
+ const expiry = tokenExpiryMs(token)
57
+ return expiry !== null && now > expiry
58
+ }
59
+
60
+ /** 删一个 designId 槽(R16 D-R16c ③——spawn 门禁过期拒的清理面)。
61
+ * 条件由调用方保证(槽在位且槽值 === token 且已过期):Map 无该 id / 值不符 →
62
+ * 不删(防误删有效槽);designId 缺省(单槽省略 designId 路径)→ 按 token 扫删。
63
+ * DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像已退役——不再同步清镜像。
64
+ * 返回是否删了槽。 */
65
+ export function removeDesignTokenSlot(agent, designId, token) {
66
+ const map = agent?._engDesignTokens
67
+ let slotRemoved = false
68
+ if (map instanceof Map && token) {
69
+ if (designId) {
70
+ if (map.get(designId) === token) {
71
+ map.delete(designId)
72
+ slotRemoved = true
73
+ }
74
+ } else {
75
+ for (const [id, t] of map) {
76
+ if (t === token) {
77
+ map.delete(id)
78
+ slotRemoved = true
79
+ break
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return slotRemoved
85
+ }
86
+
87
+ /** 遍历删过期槽(R16 F-R16b ②——eng enter / cmd-eng ON 真转换路径):Map 只留
88
+ * 有效 token。DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像已退役——无
89
+ * legacy 镜像分支。返回清理个数。 */
90
+ export function purgeExpiredDesignTokens(agent) {
91
+ const map = agent?._engDesignTokens
92
+ let cleared = 0
93
+ if (map instanceof Map) {
94
+ for (const [id, t] of [...map]) {
95
+ if (typeof t === "string" && tokenExpired(t)) {
96
+ map.delete(id)
97
+ cleared++
98
+ }
99
+ }
100
+ }
101
+ return cleared
102
+ }
103
+
104
+ // ── 会话槽序列化/恢复面(session.mjs saveSession/applySession 的 token 字段——
105
+ // 2026-09-06 R16:slot 持久化 = 跨重启/跨模式恢复的有意载体) ──
106
+
107
+ /** saveSession 数据面:token 的序列化形态。多槽 Map → {designId: token}
108
+ * (JSON-safe);空/缺 Map → undefined → JSON.stringify 丢 key——清过的会话写
109
+ * NO field,不从上次 save 复活槽。
110
+ * DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `engDesignToken` 退役——
111
+ * saveSession 不再写镜像字段(AC3 零写);只产多槽表 engDesignTokens。 */
112
+ export function engTokenSlotFields(agent) {
113
+ return {
114
+ engDesignTokens: agent._engDesignTokens instanceof Map && agent._engDesignTokens.size > 0
115
+ ? Object.fromEntries(agent._engDesignTokens)
116
+ : undefined,
117
+ }
118
+ }
119
+
120
+ /** applySession 恢复面(F-R16b ①——恢复 TTL 过滤):EXPIRED token 不读回(丢弃——
121
+ * 下次 save 自然清字段,清盘闭环);格式/畸形串读回(门禁拒——不主动删——恢复不
122
+ * 得销毁它无法判定的槽数据)。
123
+ * DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——
124
+ * 本函数是**唯一镜像迁移读点**(AC3):旧 slot 文件可能残留 engDesignToken 单值
125
+ * (D3 前 saveSession 写的 legacy 字段)。当多槽表缺失/为空(Map 空)且残留镜像为
126
+ * **格式合法且未过期** token 时 → 一次性迁入 Map(标 legacy——以 token 自身 uuid
127
+ * 为 designId——确定性、可复现、不依赖曾回显的原 designId)。此后该字段随下次
128
+ * saveSession/persistEngTokens(不再写镜像)自然从槽文件消失,本函数不再读到
129
+ * 残留——一次性语义。镜像若已过期/畸形 → 不迁移(drop——与 Map 过期项同处理,
130
+ * 清盘闭环)。永不写 agent._engDesignToken。 */
131
+ export function restoreEngTokens(agent, data) {
132
+ const map = new Map()
133
+ if (data.engDesignTokens && typeof data.engDesignTokens === "object" && !Array.isArray(data.engDesignTokens)) {
134
+ for (const [id, t] of Object.entries(data.engDesignTokens)) {
135
+ if (!(typeof t === "string" && tokenExpired(t))) map.set(id, t)
136
+ }
137
+ }
138
+ // legacy 单值镜像一次性迁移读(唯一镜像读点——AC3):Map 空且残留为格式合法未过期 token
139
+ if (map.size === 0) {
140
+ const storedMirror = data.engDesignToken
141
+ if (typeof storedMirror === "string" && tokenExpiryMs(storedMirror) !== null && !tokenExpired(storedMirror)) {
142
+ map.set(storedMirror.split(":")[0], storedMirror)
143
+ }
144
+ }
145
+ if (map.size > 0) agent._engDesignTokens = map
146
+ else delete agent._engDesignTokens
147
+ }
148
+
149
+ // ── 会话槽权威台账 I/O(DESIGN-TOKEN-SETTLEMENT D1/D2/D3——槽文件 = 权威结算台账) ──
150
+
151
+ /** 当前会话槽号(读侧——无认领副作用):粘性 _slot 优先;未钉槽时读 manifest active
152
+ * (loadManifest 只读——不在门禁路径触发认领写)。无槽 → null。 */
153
+ function currentSlotNoReadOnly(agent) {
154
+ const slot = agent?._slot
155
+ if (Number.isInteger(slot)) return slot
156
+ const active = loadManifest(agent?.cwd)?.active
157
+ return Number.isInteger(active) ? active : null
158
+ }
159
+
160
+ /** 读当前会话槽文件的权威台账(engDesignTokens 多槽表——不做 TTL 过滤,判定由调用方)。
161
+ * 门禁侧轻读:JSON.parse 只读不改文件(.tmp 回退/改名等 loadSlotFile 副作用不引入)。
162
+ * 单值镜像 engDesignToken 字段已退役——此处不读(AC3——运行时零镜像读;唯一镜像
163
+ * 迁移读在 restoreEngTokens)。返回 {designId: token} 对象或 null(无槽/无文件/不可读)。 */
164
+ export function readEngTokensFromSlot(agent) {
165
+ try {
166
+ const slot = currentSlotNoReadOnly(agent)
167
+ if (slot == null) return null
168
+ const p = slotPath(agent.cwd, slot)
169
+ if (!existsSync(p)) return null
170
+ const data = JSON.parse(readFileSync(p, "utf8"))
171
+ return (data && typeof data === "object" && !Array.isArray(data.engDesignTokens)
172
+ && data.engDesignTokens && typeof data.engDesignTokens === "object")
173
+ ? data.engDesignTokens
174
+ : null
175
+ } catch { return null }
176
+ }
177
+
178
+ /** 内存 miss 时从权威槽 reconcile(D2 spawn 门 + D3 dispatch 写门同源):把槽文件中
179
+ * **未过期**项并进内存 Map(TTL 过滤保留——expired 不并入,fail-closed;格式/畸形串
180
+ * 与 restoreEngTokens 同语义读回——门禁格式拒);槽无活项 → 内存原样。返回判定用 Map。
181
+ * #154(VSC 侧并入——`reconcileEngDesignTokens` 的**清理方向**):内存中的过期项先清(过期项在
182
+ * 任何门禁都过不了 tokenExpired——清掉不改变授权结果,仅防槽计数/落盘被死 token 撑大)。
183
+ * **与 VSC 版的两处有意差异(如实登记,非偏差)**:① 同 id 冲突以**槽为准**(本函数下段
184
+ * `merged.set`——D2「槽 = 权威」;VSC 版为内存优先);② VSC 的 `droppedExpired` 有当场权威
185
+ * 台账回写一步(`setSlotEngDesignTokens`),本形态回写为**惰性**(下一次 `persistEngTokens` /
186
+ * `saveSession` 携带清理后的 Map)。 */
187
+ export function reconcileEngTokensFromSlot(agent) {
188
+ const slots = agent?._engDesignTokens
189
+ if (slots instanceof Map) {
190
+ for (const [id, t] of [...slots]) {
191
+ if (typeof t === "string" && tokenExpired(t)) slots.delete(id)
192
+ }
193
+ }
194
+ const obj = readEngTokensFromSlot(agent)
195
+ if (!obj) return slots instanceof Map ? slots : new Map()
196
+ const live = new Map()
197
+ for (const [id, t] of Object.entries(obj)) {
198
+ if (typeof t === "string" && !tokenExpired(t)) live.set(id, t)
199
+ }
200
+ if (live.size === 0) return slots instanceof Map ? slots : new Map()
201
+ const merged = slots instanceof Map ? slots : new Map()
202
+ for (const [id, t] of live) merged.set(id, t)
203
+ agent._engDesignTokens = merged
204
+ return merged
205
+ }
206
+
207
+ /** 是否存在任一活槽(DESIGN-TOKEN-SETTLEMENT D3——dispatch 写门判定"任一活槽存在";
208
+ * AC4)。先问内存 Map(当前进程活缓存);miss 回读权威槽文件 reconcile(与 spawn 门
209
+ * D2 同源)。判定 fail-closed:格式合法且未过期才算活槽(畸形/过期不构成活槽——不
210
+ * 授权产品代码写)。 */
211
+ export function anyLiveDesignSlot(agent) {
212
+ const live = (m) => {
213
+ for (const t of m.values()) {
214
+ if (typeof t === "string" && tokenExpiryMs(t) !== null && !tokenExpired(t)) return true
215
+ }
216
+ return false
217
+ }
218
+ const m = agent?._engDesignTokens
219
+ if (m instanceof Map && live(m)) return true
220
+ const rec = reconcileEngTokensFromSlot(agent)
221
+ return rec instanceof Map && live(rec)
222
+ }
223
+
224
+ /**
225
+ * D1(DESIGN-TOKEN-SETTLEMENT,2026-09-08):settle 当场同步落盘——把当前内存多槽
226
+ * Map 写入会话槽文件的权威台账,消除 settle→回合尾 saveSession 间的重启丢 token 窗口。
227
+ * 复用 engTokenSlotFields 序列化 + session 安全写/轮转(guardForeignSlotFile =
228
+ * saveSession 同一份守卫——勿裸写文件)。槽文件缺失(本会话首次落盘)→ 写最小全新
229
+ * 记录(字段形状同 saveSession——loadSlotFile 可读);既有文件 → 读-改-写(只动 token
230
+ * 字段,历史等其他字段原样保留)。写失败(writeSessionFile/parse 抛错)→ 抛出/返回
231
+ * false——调用方(settle)失败即 settle 失败(token 不注册、可重评——评审 #1 语义)。
232
+ */
233
+ export function persistEngTokens(agent) {
234
+ const claimedNow = agent._slot == null
235
+ const slot = agent._slot ??= activeSlot(agent.cwd)
236
+ if (claimedNow) writeEndMarker(agent.cwd, slot)
237
+ const p = slotPath(agent.cwd, slot)
238
+ guardForeignSlotFile(agent, p, slot)
239
+ let data = null
240
+ if (existsSync(p)) {
241
+ data = JSON.parse(readFileSync(p, "utf8"))
242
+ } else {
243
+ // 全新槽(claim 先行、首保存落盘前)——最小记录(VSC newSlotData 同构——字段形状
244
+ // 同 saveSession 数据面,loadSlotFile 校验可读;历史留空——本会话首保存尚未发生,
245
+ // 此前历史本就未落盘,token 台账先行落盘不制造任何额外损失)。
246
+ data = {
247
+ version: 2,
248
+ cwd: agent.cwd,
249
+ title: agent.title ?? "",
250
+ activeProvider: agent.activeProvider ?? agent.provider?.name,
251
+ // MODEL-MERGE-SESSION 恒非空形态:随 saveSession 同款回落链(无渠道仍容忍 null)
252
+ activeModel: agent.activeModel ?? agent.provider?.model ?? null,
253
+ history: [],
254
+ contextHistory: [],
255
+ tasks: agent.tasks ?? [],
256
+ planMode: agent.planMode ?? false,
257
+ autoApprove: agent.autoApprove ?? false,
258
+ engineering: agent.config?.agent?.engineering ?? false,
259
+ goal: agent.goal ?? null,
260
+ advisor: agent.config?.advisor ?? null,
261
+ pendingReminders: agent._pendingReminders ?? [],
262
+ sessionStart: agent._sessionStart ?? null,
263
+ }
264
+ }
265
+ const fields = engTokenSlotFields(agent)
266
+ if (fields.engDesignTokens !== undefined) data.engDesignTokens = fields.engDesignTokens
267
+ else delete data.engDesignTokens
268
+ // 单值镜像字段永不复写(D3 零写)——残留 engDesignToken 随本次写一并清(更快退役,
269
+ // 杜绝 stale 镜像 + 空 Map 组合在下次 restore 二次迁移复活死 token 的边角)。
270
+ delete data.engDesignToken
271
+ data.updatedAt = Date.now()
272
+ writeSessionFile(p, data)
273
+ // 记录我们刚写的 mtime——下次保存/守卫跳过重复解析(saveSession 同款)
274
+ try { agent._slotMtime = statSync(p).mtimeMs } catch {}
275
+ // Update slot metadata in manifest(saveSession 同款尾——非致命:数据已安全,
276
+ // metadata 下次 listSlots 惰性恢复)
277
+ try {
278
+ const m = loadManifest(agent.cwd)
279
+ m.slots[slot] = slotDigest(data)
280
+ saveManifest(agent.cwd, m)
281
+ } catch (e) {
282
+ console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)
283
+ }
284
+ return true
285
+ }
@@ -0,0 +1,15 @@
1
+ Apply a unified diff to one or more files, atomically: if any hunk fails to apply, nothing is written.
2
+
3
+ **Use it for whole-file and multi-file changes:** creating MULTIPLE new files at once (`--- /dev/null` header per file), whole-file replacement, and cross-file refactors — one unified-diff call covers the whole change. A batched call is one permission ask and one turn instead of N separate calls.
4
+
5
+ Parameters:
6
+ - patch (required): Unified diff text. One `--- a/path` / `+++ b/path` header pair per file, then `@@ -old,count +new,count @@` hunks. Use `--- /dev/null` to create a new file. The `+++ b/path` pair may be omitted for existing files — a lone `--- a/path` (or `--- b/path`) header followed directly by hunks applies to that path (new files still need `--- /dev/null` + `+++ b/path`).
7
+
8
+ Notes:
9
+ - Hunk header "@@" without coordinates is accepted. Coordinate-less hunks are located by their anchor lines: context lines plus the removed (-) lines, matched as a contiguous sequence — a unique match applies. The anchor-free forms require context: a hunk with no removed (-) lines (pure additions) needs at least 2 context lines for a unique match; a zero/one-context hunk with at least one removed (-) line is located by its anchor sequence (context + removed lines, in order) and applies on a unique match.
10
+ - Returns the applied-file summary (`Applied patch to N file(s)` plus per-file change lines and syntax-check notes for changed .mjs files)
11
+ - Use this for multi-file changes (e.g. rename an interface + update all callers) — one call, all-or-nothing
12
+ - Hunks are located by their context/removed lines, not line numbers — but the context must match the file EXACTLY. Read the files first and generate the patch from actual content
13
+ - If a hunk's context matches multiple locations it is rejected — add more surrounding context lines
14
+ - Deleting files is not supported — use the delete tool
15
+ - For single-file edits, edit is simpler; for full rewrites, write is simpler
@@ -0,0 +1,38 @@
1
+ Execute a shell command and return stdout+stderr. Use for running commands, builds, tests.
2
+
3
+ **Route to a dedicated tool instead of bash:**
4
+ - `cat file` / `head` / `tail` → `read`
5
+ - `ls` / `dir` → `ls`
6
+ - `find` / glob search → `glob`
7
+ - `grep` / `rg` → `grep`
8
+ - `echo >` / `sed` / `printf >` / `cat << EOF` → `write` / `edit` / `hashline_edit` / `apply_patch` (preferred: write tools handle encoding/EOL correctly)
9
+ - `git diff` / `git status` / `git log` → `git` tool
10
+
11
+ Parameters:
12
+ - command (required): Shell command to execute
13
+ - timeout: Timeout in milliseconds (default 120000, max ~300000)
14
+ - filter: Optional — a regex; only output lines matching it are returned (case-insensitive). Use instead of hand-writing a pipe into `findstr`/`grep`.
15
+ {{inject:bash-terminal-face}}
16
+
17
+ Output format:
18
+ ```
19
+ [stdout]:
20
+ <standard output, or "(empty)">
21
+
22
+ [stderr]:
23
+ <standard error, only present if non-empty>
24
+
25
+ (exit code N)
26
+ ```
27
+
28
+ Notes:
29
+ - There is NO TTY — editors, pagers (vim, less), and interactive prompts WILL hang. Always pass non-interactive flags: `git commit -m`, `git --no-pager`, `-y`/`--yes` where applicable
30
+ - The environment sets GIT_PAGER=cat, PAGER=cat, EDITOR=true, TERM=dumb — but still always use non-interactive flags
31
+ - Output is capped at ~200K chars; if you need more, redirect to a file and read it. Truncated output ends with a `[... truncated: N chars omitted]` marker — the missing tail may contain errors, so read the saved log file's tail before trusting success. Use `filter` to narrow instead of hand-piping.
32
+ - Check `[stderr]` for error messages, warnings, and diagnostic output — it is separated from `[stdout]` so you can quickly identify problems.
33
+ - On Windows the shell is **cmd.exe** (NOT Git Bash, NOT PowerShell): `&&`/`||` chaining works, use cmd built-ins (`del`, `dir`, `type`, `findstr`, `tasklist`) and `/dev/null`→`NUL`, `2>/dev/null`→`>nul 2>&1`. Bash-isms (`rm -rf`, `cp -r`, `head`, `$(...)`, `${VAR}`, single quotes, `;` separators) FAIL — the tool prepends a hint when it detects POSIX-only syntax, and still executes. For complex logic prefer the execute tool (node) over shell gymnastics.
34
+ - Never use bash to read, copy, or transmit secret files (.env, keys, tokens)
35
+ - Do NOT run destructive commands (rm -rf, force-push, drop table) without explicit user confirmation
36
+ - After commands that change files (git checkout, npm install, etc.), repo_outline and code_search may be stale — re-run them to get current results.
37
+ - Prefer read/glob/grep/ls for file operations inside the project — file tools and bash reach the same paths (no directory restriction).
38
+ - NEVER use bash to write or modify files (echo/sed/printf > file, cat << EOF, etc.). Use write/edit/insert_after/apply_patch instead — they handle encoding, escaping, and EOL conventions correctly.
@@ -0,0 +1,13 @@
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).