@thincoder/core 0.9.5 → 0.10.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 (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
@@ -7,20 +7,33 @@
7
7
  * 契约(§4.15 逐字):**候选序** = cwd → 项目根 → 逐基底(基底 = `batchDocBases` 声明面单源);
8
8
  * **判据分野** = 读面取首个**可读**文件 / create 取首个**落基底根内**者;**锚定防嵌套** = 串以任一基底的
9
9
  * 「项目根相对前缀」打头且前缀后为**路径段边界** ⇒ 只解析项目根形(不二次拼接;不落基底内 ⇒ fail-closed);
10
- * **零变面** = 绝对路径照旧、三个错误文案逐字、「参数在 + 路径可读」判据句。
10
+ * **歧义锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属显式拒;读面候选腿追加;
11
+ * 在飞 ∥ 写门基底集 = 候选并集(`batchDocBases`);**零变面** = 绝对路径照旧、三个错误文案逐字、
12
+ * 「参数在 + 路径可读」判据句、`ok` / `none` 两态。
11
13
  *
12
14
  * 依赖(KD-4 单向):本档为**叶档**——只 import `node:*` 与 `../manifest.mjs`;主档 re-export 保既有面。
13
15
  */
14
16
  import { existsSync, statSync } from "node:fs"
15
17
  import { isAbsolute, relative, resolve, sep } from "node:path"
16
18
 
17
- import { docRootBase, docRootPaths, readManifest, resolveProjectRoot } from "../manifest.mjs"
19
+ import { docRootBase, docRootPaths, owningProject, projectRootView, readManifest, resolveProjectRoot } from "../manifest.mjs"
18
20
 
19
21
  /** 可读文件判据(存在且为文件——目录 / 缺失同判不可读)。 */
20
22
  function readableFile(abs) {
21
23
  try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
22
24
  }
23
25
 
26
+ /** 歧义锚候选(判定单点 = `projectRootView`——`ambiguous` 态 ⇒ 候选全列**按名排序**;余者 null)。 */
27
+ function ambiguousCandidates(cwd) {
28
+ const view = projectRootView(cwd ?? process.cwd())
29
+ return view.state === "ambiguous" ? view.candidates : null
30
+ }
31
+
32
+ /** 路径同一性比较(win32 大小写不敏感——同越基底判据口径)。 */
33
+ function samePath(a, b) {
34
+ return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b
35
+ }
36
+
24
37
  /** 「项目根」取根单源(§4.15 条 1——归属 ∨ 发现,无 manifest ⇒ `resolve(cwd ?? ".")` 兜底且**单值**):
25
38
  * 同式 = 台账库键 / 写门指针基准的同一子表达式(`design/LEDGER.md` §6.1 口径 2)。 */
26
39
  export function batchProjectRoot(cwd) {
@@ -37,9 +50,16 @@ function declaredBases(cwd) {
37
50
 
38
51
  /** 批次档基底根数组(**声明面单源**——create / 在飞扫描 / 锚定判据共用):manifest `docRoot.batches`
39
52
  * (串 | 多根数组)→ 声明基底;缺失 / 非法 ⇒ 回退默认基底 `"docs/batches"`(恒返回非空数组)。
53
+ * **歧义锚**(条 5)⇒ 候选项目基底**并集**(候选按名排序、并集保序)——在飞扫描 / 写门基底集同取此值。
40
54
  * @param {string} cwd
41
55
  * @returns {string[]} */
42
56
  export function batchDocBases(cwd) {
57
+ const amb = ambiguousCandidates(cwd)
58
+ if (amb) {
59
+ const union = []
60
+ for (const c of amb) for (const b of batchDocBases(c)) if (!union.includes(b)) union.push(b)
61
+ return union
62
+ }
43
63
  const roots = declaredBases(cwd)
44
64
  return roots.length ? roots : [resolve(docRootBase(cwd), "docs/batches")]
45
65
  }
@@ -76,6 +96,21 @@ function insideBases(abs, bases) {
76
96
  const outsideBasesError = (raw) => new Error(`batch: create path resolves outside the batch-record base roots — a batch record must live under the declared docRoot.batches base (fail-closed). Path: ${raw}`)
77
97
  /** create 锚定但落基底外(§4.15 新文案逐字——不二次拼接)。 */
78
98
  const anchoredEscapeError = (raw) => new Error(`batch: create path is anchored at a batch base root but does not resolve under the declared docRoot.batches root(s) — refusing to nest it (fail-closed). Path: ${raw}`)
99
+ /** 歧义锚 create 拒面(**文案逐字** = `BATCH-RECORD.md` §4.15 条 5——机检锚 `ambiguous session anchor`)。 */
100
+ const ambiguousCreateError = (raw, candidates) => new Error(`batch: ambiguous session anchor — create target "${raw}" belongs to no candidate project; pass an explicit project path (e.g. "${candidates[0]}/docs/batches/<file>.md"). Candidates: ${candidates.join(", ")}`)
101
+
102
+ /** 歧义锚读面候选腿(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
103
+ * (候选按名排序、并集保序——「首个可读」在此即为确定判读)。 */
104
+ function ambiguousReadLegs(cwd, p) {
105
+ const amb = ambiguousCandidates(cwd)
106
+ if (!amb) return []
107
+ const out = []
108
+ for (const c of amb) {
109
+ out.push(resolve(c, p))
110
+ for (const b of declaredBases(c)) out.push(resolve(b, p))
111
+ }
112
+ return out
113
+ }
79
114
 
80
115
  /**
81
116
  * **读面**解析(append / status / close 与评审门 / spawn 门共用):首个**可读**候选胜;无 ⇒ null
@@ -93,7 +128,7 @@ export function resolveBatchReadPath(cwd, given) {
93
128
  const abs = resolve(batchProjectRoot(base), p)
94
129
  return readableFile(abs) ? abs : null
95
130
  }
96
- const candidates = [resolve(base, p), resolve(batchProjectRoot(base), p), ...declaredBases(base).map((b) => resolve(b, p))]
131
+ const candidates = [resolve(base, p), resolve(batchProjectRoot(base), p), ...declaredBases(base).map((b) => resolve(b, p)), ...ambiguousReadLegs(base, p)]
97
132
  for (const abs of candidates) if (readableFile(abs)) return abs
98
133
  return null
99
134
  }
@@ -116,6 +151,8 @@ export function resolveBatchDocPath(cwd, given) {
116
151
  /**
117
152
  * **create 面**解析(§4.15 条 2/3):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
118
153
  * 根内**者(新档不存在——可读性无判别力);锚定串只解析项目根形,不落基底内 ⇒ fail-closed 新文案。
154
+ * **歧义锚**(条 5):目标所属项目(`owningProject`)∈ 锚候选集 ⇒ 基底 = 该项目声明面(落其基底内;
155
+ * 越出 ⇒ 既有 fail-closed);无所属 ⇒ **显式拒**(列候选 + 显式路径指引——不静默落锚)。
119
156
  * @param {string} cwd 会话锚
120
157
  * @param {string} given create `path` 原值
121
158
  * @param {string[]} bases 已解析的基底根数组(`batchDocBases` 单源;调用方传入以免重复读档)
@@ -125,18 +162,25 @@ export function resolveBatchCreatePath(cwd, given, bases) {
125
162
  const base = cwd ?? process.cwd()
126
163
  const raw = String(given ?? "").trim()
127
164
  const p = raw.replace(/\\/g, "/")
165
+ let roots = bases
166
+ const amb = ambiguousCandidates(base)
167
+ if (amb) {
168
+ const owner = owningProject(isAbsolute(raw) ? resolve(raw) : resolve(base, p))
169
+ if (!owner || !amb.some((c) => samePath(c, owner))) throw ambiguousCreateError(raw, amb)
170
+ roots = batchDocBases(owner)
171
+ }
128
172
  if (isAbsolute(raw)) {
129
173
  const abs = resolve(raw)
130
- if (!insideBases(abs, bases)) throw outsideBasesError(raw)
174
+ if (!insideBases(abs, roots)) throw outsideBasesError(raw)
131
175
  return abs
132
176
  }
133
- if (anchoredPrefix(p, basePrefixes(base, bases))) {
177
+ if (anchoredPrefix(p, basePrefixes(base, roots))) {
134
178
  const abs = resolve(batchProjectRoot(base), p)
135
- if (!insideBases(abs, bases)) throw anchoredEscapeError(raw)
179
+ if (!insideBases(abs, roots)) throw anchoredEscapeError(raw)
136
180
  return abs
137
181
  }
138
- for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p), ...bases.map((b) => resolve(b, p))]) {
139
- if (insideBases(abs, bases)) return abs
182
+ for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p), ...roots.map((b) => resolve(b, p))]) {
183
+ if (insideBases(abs, roots)) return abs
140
184
  }
141
185
  throw outsideBasesError(raw)
142
186
  }
@@ -117,7 +117,7 @@ export function readBatchStatusLine(src) {
117
117
  nextRe.lastIndex = s1.index + s1[0].length
118
118
  const next = nextRe.exec(src)
119
119
  const body = src.slice(s1.index + s1[0].length, next ? next.index : src.length)
120
- const m = body.split("\n").map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
120
+ const m = body.split(/\r?\n/).map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
121
121
  if (!m) return "unknown"
122
122
  const value = m[1].trim()
123
123
  if (value.includes(STATUS_WORDS[1].closed)) return "closed"
@@ -125,6 +125,24 @@ export function readBatchStatusLine(src) {
125
125
  return "unknown"
126
126
  }
127
127
 
128
+ /**
129
+ * §N 段内「`**状态行**:` 行存在性」判据(#422③——close 对无状态行档自建机读位的**采集面**:
130
+ * 与 readBatchStatusLine 的「值可解析」正交——无行 ⇒ 可自建;有行而值不可解析 ⇒ 仍 fail-closed)。
131
+ * 段界定消费 sectionHeaderRe(同单源——`## §20` 不误命中 `§2`)。
132
+ * @param {string} src — 档全文
133
+ * @param {number|string} seg — 段号
134
+ * @returns {boolean} 段内是否存在形态匹配行(段头缺失同判 false)
135
+ */
136
+ export function sectionHasStatusLine(src, seg) {
137
+ const hdr = sectionHeaderRe(seg).exec(src)
138
+ if (!hdr) return false
139
+ const nextRe = /^## §\d/gm
140
+ nextRe.lastIndex = hdr.index + hdr[0].length
141
+ const next = nextRe.exec(src)
142
+ const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
143
+ return body.split(/\r?\n/).some((line) => STATUS_LINE_RE.test(line))
144
+ }
145
+
128
146
  /**
129
147
  * create 骨架模板(BATCH-RECORD.md §4.10 行为规格的**代码单源**——红线:模板只覆盖骨架与
130
148
  * 占位,不模板化内容):`## §1–§6` 段头(含职责署名)+ 档头 boilerplate(六段一段一作者句 +
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * 单工具 `batch`,action = create(建档,§4.11)/ append(段写入——原 batch-segment.mjs 语义
6
6
  * 逐字迁移,§4.1 全表)/ status(段属主状态行流转,§4.12)/ close(收口冻结,§4.13)。
7
- * 权威规格 = `docs/core/design/BATCH-RECORD.md` §4(契约/挂载/用例 BR-1–26/骨架 §4.10/
8
- * 参数面 §4.11–§4.13/别名 §4.14);骨架与判定字面单源 = `batch-skeleton.mjs`;lifecycle 面 =
7
+ * 权威规格 = `docs/core/design/BATCH-RECORD.md` §4(契约/挂载/用例 §4.8/骨架 §4.10/
8
+ * 参数面 §4.11–§4.13);骨架与判定字面单源 = `batch-skeleton.mjs`;lifecycle 面 =
9
9
  * `batch-lifecycle.mjs`。**依赖单向(KD-4):skeleton ← lifecycle ← 主档**。
10
10
  *
11
11
  * 身份判据(D-BR17/D-BR18/D-BR21):主 agent 调用 ctx 带 `depth === 0`(dispatch 装配);
@@ -17,11 +17,9 @@
17
17
  * ——轮 2 #3 裁定②)。append 迁移面错误串**逐字保持 "batch_segment:" 前缀**(§4.1 全表锚断言);
18
18
  * "batch:" 前缀 = 本批新增错误面(create/close/status/findInFlight/path 门/dispatch)。
19
19
  *
20
- * 导出面:`batchTool`(主名)· `batchSegmentTool`(**过渡别名**——name 仍 "batch_segment",
21
- * schema 仅 {segment,text}、描述逐字,execute ⇒ append 同一执行体;撤除判据 = BATCH-RECORD
22
- * §4.14)· `resolveBatchDocPath` · `batchDocBases` · `batchDocForReview` ·
20
+ * 导出面:`batchTool`(主名)· `resolveBatchDocPath` · `batchDocBases` · `batchDocForReview` ·
23
21
  * `configureBatchSegment` / `resetBatchSegment`(#84 记账缝——契约名不变,AC-11 零改)·
24
- * `MAX_TEXT_CHARS` · `SEGMENT_BY_ROLE`(re-export——shim 导出全超集需要)。
22
+ * `MAX_TEXT_CHARS` · `SEGMENT_BY_ROLE`。
25
23
  *
26
24
  * #84 记账面(CORE-UNIFICATION §2.13.4):写入成功后的记账**按端注入**——核内缺省 no-op = CLI
27
25
  * 语义零行为变;端装配经 `configureBatchSegment({ onWrite })` 覆盖(VSC 侧现形 = `_touchedFiles`
@@ -29,6 +27,8 @@
29
27
  */
30
28
  import { readFileSync, writeFileSync } from "node:fs"
31
29
 
30
+ // 描述面 = `tool-docs/batch.md`(#15 描述外置统一——工厂形描述经 DESC 单一解析面,工厂调用期取值)
31
+ import { DESC } from "../tools/shared.mjs"
32
32
  import { SEGMENT_BY_ROLE, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
33
33
  import { batchDocBases, resolveBatchDocPath } from "./batch-paths.mjs"
34
34
  import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
@@ -36,8 +36,7 @@ import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchReco
36
36
  export { SEGMENT_BY_ROLE, batchDocBases, resolveBatchDocPath }
37
37
 
38
38
  /** text 单次上限(BATCH-RECORD.md §4.1——超出引导分段追加,不承诺"不新盖戳")。append 迁移域私有区常量
39
- * (随执行体自 batch-segment.mjs 迁入——骨架档只住骨架/状态行单源,lifecycle 档头枚举口径);
40
- * export = shim 导出全超集(旧 `batch-segment.mjs` 导出面等价保持)。 */
39
+ * (随执行体自 batch-segment.mjs 迁入——骨架档只住骨架/状态行单源,lifecycle 档头枚举口径)。 */
41
40
  export const MAX_TEXT_CHARS = 20000
42
41
  /** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
43
42
  const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
@@ -182,7 +181,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
182
181
  }
183
182
  abs = resolveBatchDocPath(cwd, givenPath)
184
183
  } else if (depth0) {
185
- abs = findInFlightBatch(cwd, batchDocBases(cwd))
184
+ abs = findInFlightBatch(cwd, batchDocBases(cwd)) // 歧义锚 ⇒ 基底 = 候选并集(#828——唯一在飞批照取 / ≥2 照列)
186
185
  } else {
187
186
  if (!batchDoc) {
188
187
  throw new Error("batch_segment: no batch record is bound to this caller — there is no path parameter by design (the target arrives via the spawn binding / the review instance key). Report the section as not written.")
@@ -195,7 +194,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
195
194
  throw new Error("batch_segment: 已收口档不回改 — the bound batch record's §1 status line contains 「已收口」, so the record is frozen: its body is never written to again (整档冻结;改 = 新批新档). Nothing was written. Report the section as not written.")
196
195
  }
197
196
  if (status === "unknown") {
198
- throw new Error("batch_segment: 状态行不可解析或缺失 — the bound batch record has no §1 `**状态行**:` line whose value contains 已收口 or 进行中 (fail-closed: the write is refused as if frozen). Ask the record's creator to set the §1 status line, then call again. Nothing was written.")
197
+ throw new Error("batch_segment: 状态行不可解析或缺失 — the bound batch record has no §1 `**状态行**:` line whose value contains 已收口 or 进行中 (fail-closed: the write is refused as if frozen). Ask the record's creator to set the §1 status line, then call again. Nothing was written. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close).")
199
198
  }
200
199
  if (typeof args?.text !== "string") {
201
200
  throw new Error("batch_segment: text must be a string (the markdown to append).")
@@ -204,7 +203,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
204
203
  throw new Error(`batch_segment: text is ${args.text.length} characters — the limit is ${MAX_TEXT_CHARS} per call. Split it into multiple calls: each call becomes its own section and the round number N continues (分段追加——每次调用各成节、N 顺延).`)
205
204
  }
206
205
  if (/^## §\d/m.test(args.text)) {
207
- throw new Error("batch_segment: the text contains a section header line matching `^## §N` — that would break section location and the append-only guarantee. Rewrite it (escape the heading, or drop the leading `## §N`), then call again (骨架保护). Nothing was written.")
206
+ throw new Error("batch_segment: the text contains a section header line matching `^## §N` — that would break section location and the append-only guarantee. Rewrite it (escape the heading, or drop the leading `## §N`), then call again (骨架保护). Nothing was written. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close).")
208
207
  }
209
208
  const body = sanitizeText(args.text, n === 3).replace(/^\n+/, "").replace(/\s+$/, "")
210
209
  if (!body.trim()) {
@@ -230,18 +229,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
230
229
  export function batchTool(batchDoc = null, { review = false } = {}) {
231
230
  return {
232
231
  name: "batch",
233
- description:
234
- "Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). " +
235
- "actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic + source) · " +
236
- "append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · " +
237
- "status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry, decorated only by surrounding symbols / a trailing ISO date / whitespace; prose goes to the separate `note` field, which lands as a parenthetical) · " +
238
- "close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused). " +
239
- "Identity fixes what you may write (段白名单): main agent (depth 0) — append §1/§4/§6, status §1 only, create/close; eng-designer — §2; eng-coder — §5; design review (review binding) — §3. A write outside your own section is refused. " +
240
- "Skeleton placeholders (dead literals — `#<编号>` / `<板块>` and the older `<BATCH-ID>` / `<讨论来源>`) in the record header or in your target section block append/status: fill them first (the main agent fills the header right after create); `<§N 模板占位:…>` template lines are legal and never blocked. " +
241
- "Target record: sub-agents and design reviews have NO path parameter in practice — the record arrives via the spawn binding / the review instance key (passing path is refused); depth-0 MAY pass path (若传则须可读), omitting it picks the unique in-flight record (0 or ≥2 in flight ⇒ refuse — pass path). " +
242
- "A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
243
- "Credential values are stripped mechanically before writing (never write a token or designId value). " +
244
- "Failures are hard and visible (no silent fallback): if a write is refused or fails, say so in your report — “§× 未写入”.",
232
+ description: DESC("batch"), // 模型可见文本 = tool-docs/batch.md
245
233
  parameters: {
246
234
  type: "object",
247
235
  properties: {
@@ -292,7 +280,7 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
292
280
  async execute(args, ctx) {
293
281
  const agent = ctx?.agent ?? {}
294
282
  const cwd = agent.cwd ?? process.cwd()
295
- const bases = batchDocBases(cwd)
283
+ const bases = batchDocBases(cwd) // 歧义锚 ⇒ 候选项目基底并集(#828——create / 在飞 / 写门基底集同源)
296
284
  const onWritten = injectedOnWrite
297
285
  /** depth-0 目标定位(D-BR21):path 传 ⇒ 可读性门;缺省 ⇒ 唯一在飞批(depth-0)/
298
286
  * 绑定解析(子代理/评审)。status/close 经此注入 lifecycle;append 自带同款分支。 */
@@ -323,44 +311,3 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
323
311
  },
324
312
  }
325
313
  }
326
-
327
- /**
328
- * 过渡别名(BATCH-RECORD §4.14——撤除判据见该节):name 仍 "batch_segment"、schema 仅
329
- * {segment, text}、描述**逐字**保持原 batch-segment.mjs 基准;execute ⇒ append(同一执行体,
330
- * 行为等价——AC-2/AC-9)。生产挂载面已全切主名 `batch`(BATCH-RECORD §4.3);本工厂仅供
331
- * 过渡 shim 导出面(旧测试 / VSC 侧 import 消费)与撤除判据实跑使用。
332
- * @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键)
333
- * @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
334
- */
335
- export function batchSegmentTool(batchDoc = null, { review = false } = {}) {
336
- const tool = batchTool(batchDoc, { review })
337
- const own = review ? "§3" : null
338
- return {
339
- name: "batch_segment",
340
- description:
341
- "Append your own section of the batch record (一段一作者). " +
342
- "There is NO path parameter: the target record is bound to you (at spawn for eng-designer/eng-coder, per review instance for a design review) and your identity fixes the section you may write " +
343
- "(eng-designer → §2, design review → §3, eng-coder → §5) — a write outside your own section is refused. " +
344
- "Append-only: the text lands at the end of your section; existing lines are never rewritten or deleted. " +
345
- "Credential values are stripped mechanically before writing (never write a token or designId value). " +
346
- "A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
347
- "Failures are hard and visible (no silent fallback): if the write is refused or fails, say so in your report — “§× 未写入”.",
348
- parameters: {
349
- type: "object",
350
- properties: {
351
- segment: {
352
- type: "string",
353
- description: `The batch-record section you are writing${own ? ` — yours is ${own}` : ""}. Declares the section number only; your identity decides what is actually writable.`,
354
- },
355
- text: {
356
- type: "string",
357
- description: "The markdown to append (verbatim — findings table + VERDICT + counts for a design review §3). Limit 20000 characters per call; longer content is refused — split it into multiple calls (each call becomes its own section, N continues).",
358
- },
359
- },
360
- required: ["segment", "text"],
361
- },
362
- async execute(args, ctx) {
363
- return tool.execute({ action: "append", segment: args?.segment, text: args?.text }, ctx)
364
- },
365
- }
366
- }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * checkpoint.mjs — 撞帽续期检查点叶(TURN-CAP-CONTINUE.md §1 #7 / §3.1)。
3
+ *
4
+ * 异步族(后台子代理 / 异步飞刀 / 会诊)撞帽 ⇒ 不发用户卡:登记「挂起 + 父侧 ask」,
5
+ * 等父代理决定——`send` 兑现(新段预算、history 保留、任务文本不重注入)或
6
+ * `cancel` / 会话收尾 / Stop 降级(既有 partial 路径 + `TURN_CAP_MARK`)。
7
+ * 挂起不 settle、不重派:池条目仍在飞(status / observe 可见)、无新 child / 无新池条目、
8
+ * 兑现后同一 id 继续(D-TC12)。
9
+ *
10
+ * 三档逃逸(§3.1):
11
+ * ① 兑现 = 父 `send` 两处(池条目 `settleTurnCheckpoint` · 会诊会话 `settleConsultCheckpoint`);
12
+ * ② 中止信号(Stop / cancel / 会话中止)⇒ resolve(false) ⇒ 既有 onDeclined 降级;
13
+ * ③ 不可答(无 `_upstream.parent` ∨ `sync === true`)⇒ 不登记 ⇒ 既有路径(零新挂起)。
14
+ *
15
+ * 零依赖叶(只引父侧通道 + 池 accessor):端壳不经本档静态闭包。
16
+ */
17
+ import { pushChildUpstream } from "./parent-channel.mjs"
18
+ import { getAsyncPool } from "./async-settle.mjs"
19
+
20
+ /**
21
+ * 段数 / 累计轮次二元文本(§4 留痕口径——两值同读一次):
22
+ * `_continueSegments`(链内段数,首段 1)· `_turnSeq`(链内累计轮次,跨段不重置)。
23
+ * 两值均挂子 agent 本体。
24
+ * #417 追加第三元「本段零落盘轮数」(`_zeroWriteTurns`——核回合环采集,与 `_turnSeq` 同源面):
25
+ * **只报数**(零阈值 / 零自动动作——父侧据此判断「在推进」还是「在原地读」)。缺该字段的
26
+ * 执行面(端侧自有回合环、未跟踪)不渲染该元——缺省不冒充 `0`(「未跟踪」不得读作「零消耗」)。
27
+ */
28
+ function traceText(child) {
29
+ const base = `segments ${child?._continueSegments ?? 1} · accumulated ${child?._turnSeq ?? 0} turns`
30
+ return Number.isInteger(child?._zeroWriteTurns) ? `${base} · zero-write rounds this segment: ${child._zeroWriteTurns}` : base
31
+ }
32
+
33
+ /** 留痕片段(§3.1 池条目留痕行):挂池条目摘要**文本**面(status / observe)——桥字段零新增。 */
34
+ export function turnCapTrace(entry) {
35
+ return traceText(entry?.childAgent)
36
+ }
37
+
38
+ /** 去向一句话(ask 载荷末项):答复通道随族——池条目 = `send` / `cancel`;
39
+ * 会诊会话 = `send` / `consult_stop`(会话级停;`cancel` 只认子代理池)。 */
40
+ function replyHint(parent, carrier, key) {
41
+ const consult = getAsyncPool(parent, "consult")?.get(key) === carrier
42
+ const stop = consult ? `consult_stop (id ${key})` : `subagent action:'cancel' (id ${key})`
43
+ return `reply with subagent action:'send' (id ${key}) to grant a new segment, or ${stop} to stop it (partial)`
44
+ }
45
+
46
+ /**
47
+ * 登记检查点并挂起(异步族 askContinue 的唯一实现——逃逸 ①②③):
48
+ * 登记 `carrier._turnCapRec` + 父侧 ask(载荷 = 段数 / 累计轮次 + 去向一句话)——
49
+ * ask 入队即唤醒父侧挂起驱动(`pushChildUpstream`)。
50
+ * @param {object} child 撞帽的子 agent(`_upstream` 携父与来源标签)。
51
+ * @param {object} carrier 兑现载体:池条目(子代 / 飞刀)或会诊 session。
52
+ * @param {{turn?: number, signal?: AbortSignal}} payload 撞帽轮次 + 中止信号。
53
+ * @returns {Promise<boolean>} true = 父 `send` 兑现 ⇒ 重入新段;false = 中止 / 不可答 / 兑现 miss。
54
+ */
55
+ export function registerTurnCapCheckpoint(child, carrier, { turn = 0, signal } = {}) {
56
+ const parent = child?._upstream?.parent
57
+ // ③ 不可答降级:无父(headless 内嵌)或同步族(后台检查点只服务异步族——同步族保留用户卡)
58
+ if (!parent || child?._upstream?.sync === true || !carrier) return Promise.resolve(false)
59
+ if (signal?.aborted) return Promise.resolve(false) // ② 已中止 ⇒ 立即降级(不进挂起)
60
+ const key = String(carrier.id ?? "")
61
+ const rec = { child, carrier, turn }
62
+ // 先臂后报请(顺序即防御):`rec.resolve` 与中止监听在入队**前**就位——否则
63
+ // `pushChildUpstream` 同步唤醒等待栓的窗口里「登记已写、resolve 未挂」,兑现假成功而挂起不收敛。
64
+ let abortedNow = signal?.aborted === true
65
+ const promise = new Promise((resolve) => {
66
+ // 注销单点(幂等):兑现 / 中止两路共用——登记清除后重复兑现 miss(回落既有路径)。
67
+ const finish = (ok) => {
68
+ if (carrier._turnCapRec === rec) delete carrier._turnCapRec
69
+ signal?.removeEventListener?.("abort", onAbort)
70
+ resolve(ok)
71
+ }
72
+ const onAbort = () => finish(false)
73
+ rec.resolve = finish
74
+ signal?.addEventListener?.("abort", onAbort, { once: true })
75
+ // 注册后复检(`permission-gate.mjs:80-81` 同款序):监听只认注册后的状态转移,
76
+ // 窗口内已中止 ⇒ 监听不落地,此处兜住。
77
+ if (signal?.aborted) { abortedNow = true; onAbort() }
78
+ })
79
+ if (abortedNow) return promise // ② 窗口内已中止 ⇒ 不登记、不报请(直接走既有 partial 降级)
80
+ carrier._turnCapRec = rec
81
+ pushChildUpstream({
82
+ parent,
83
+ from: child._upstream.label,
84
+ kind: "ask",
85
+ message: `[turn cap] ${traceText(child)} — this child hit its per-segment turn cap and is suspended `
86
+ + `(history kept, no re-dispatch). ${replyHint(parent, carrier, key)}`,
87
+ })
88
+ return promise
89
+ }
90
+
91
+ /** ① 池条目兑现(send 面第一条):读登记 → 注销 → resolve(true)。
92
+ * miss(无挂起 / 非检查点 send)⇒ false——调用方回落既有注入路径;兑现后幂等 miss。 */
93
+ export function settleTurnCheckpoint(carrier) {
94
+ const rec = carrier?._turnCapRec
95
+ if (!rec) return false
96
+ rec.resolve?.(true)
97
+ return true
98
+ }
99
+
100
+ /** ① 会诊会话兑现(send 面第二条——id 不属子代理池时试会诊会话):父方向注入该会话在飞
101
+ * 子代理的注入队列(下一回合边界消费;`consumeInjected` 同族)⇒ 兑现登记。
102
+ * miss ⇒ false——调用方回落 `unknown async subagent id`。 */
103
+ export function settleConsultCheckpoint(agent, key, message) {
104
+ const rec = getAsyncPool(agent, "consult")?.get(String(key))?._turnCapRec
105
+ if (!rec) return false
106
+ rec.child._injected ??= []
107
+ rec.child._injected.push(String(message).trim())
108
+ rec.resolve?.(true)
109
+ return true
110
+ }
111
+
112
+ /** ① 兑现后的 send 早返体(既有 send 结果同形 + 检查点语汇):兑现即不落「未命中 / 静默丢弃」
113
+ * ——父侧可见它确实续了新段。 */
114
+ export function resumedSendResult(key, label) {
115
+ return JSON.stringify({
116
+ id: String(key),
117
+ status: "delivered",
118
+ resumed: true,
119
+ note: `turn-cap checkpoint delivered — ${label} ${key} resumes with a fresh segment budget (history kept, no task-text re-injection); the message is consumed as an ordinary user instruction at its next turn boundary.`,
120
+ })
121
+ }
@@ -29,15 +29,17 @@ export function childOwnerLabel(role, id, model) {
29
29
  * 构建 child 权限通道。返回 null(无 `ctx.callbacks.onPermissionRequired`——headless /
30
30
  * 无关角色调用侧未挂;AUTO 判定走父门询问时 live 读——同判据单源,ED-2 2026-09-16)或
31
31
  * `async (toolName, args, diffInfo) => boolean`。
32
- * @param {{ ctx: object, id: number, role: string, model?: string|null, signal?: AbortSignal|null }} deps
32
+ * `forceAsk`(默认 false)= 本询 AUTO 豁免**透传**(续期恒须人答——TURN-CAP-CONTINUE.md §1 #7;
33
+ * 置位点唯一 = VSC `panel-callbacks.mjs` 的 `continue` 名分支)——其余调用逐字零变化。
34
+ * @param {{ ctx: object, id: number, role: string, model?: string|null, signal?: AbortSignal|null, forceAsk?: boolean }} deps
33
35
  */
34
- export function makeChildPermission({ ctx, id, role, model = null, signal = null }) {
36
+ export function makeChildPermission({ ctx, id, role, model = null, signal = null, forceAsk = false }) {
35
37
  if (!ctx?.callbacks?.onPermissionRequired) return null
36
38
  const owner = { label: childOwnerLabel(role, id, model), role, id }
37
39
  return async (toolName, args, diffInfo) => {
38
40
  ctx.callbacks.onSubagentApproval?.({ id, role, model, tool: toolName })
39
41
  try {
40
- return await ctx.callbacks.onPermissionRequired(toolName, args, diffInfo, { owner, signal })
42
+ return await ctx.callbacks.onPermissionRequired(toolName, args, diffInfo, { owner, signal, forceAsk })
41
43
  } finally {
42
44
  ctx.callbacks.onSubagentApproval?.({ id, role, model, tool: null })
43
45
  }