@thincoder/core 0.9.4 → 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 (221) hide show
  1. package/CHANGELOG.md +63 -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 +14 -14
  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 +23 -13
  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 +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -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 +19 -15
  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 +90 -30
  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 +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  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 +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  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 +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  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 +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  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 +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  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/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
@@ -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,28 +17,26 @@
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`
28
26
  * 记账)。核内零端名分支(契约 5——只认 `onWrite` 函数名)。
29
27
  */
30
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
31
- import { resolve } from "node:path"
28
+ import { readFileSync, writeFileSync } from "node:fs"
32
29
 
33
- import { docRootBase, docRootPaths, readManifest } from "../manifest.mjs"
30
+ // 描述面 = `tool-docs/batch.md`(#15 描述外置统一——工厂形描述经 DESC 单一解析面,工厂调用期取值)
31
+ import { DESC } from "../tools/shared.mjs"
34
32
  import { SEGMENT_BY_ROLE, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
33
+ import { batchDocBases, resolveBatchDocPath } from "./batch-paths.mjs"
35
34
  import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
36
35
 
37
- export { SEGMENT_BY_ROLE }
36
+ export { SEGMENT_BY_ROLE, batchDocBases, resolveBatchDocPath }
38
37
 
39
- /** text 单次上限(§2.20.1——超出引导分段追加,不承诺"不新盖戳")。append 迁移域私有区常量
40
- * (随执行体自 batch-segment.mjs 迁入——骨架档只住骨架/状态行单源,lifecycle 档头枚举口径);
41
- * export = shim 导出全超集(旧 `batch-segment.mjs` 导出面等价保持)。 */
38
+ /** text 单次上限(BATCH-RECORD.md §4.1——超出引导分段追加,不承诺"不新盖戳")。append 迁移域私有区常量
39
+ * (随执行体自 batch-segment.mjs 迁入——骨架档只住骨架/状态行单源,lifecycle 档头枚举口径)。 */
42
40
  export const MAX_TEXT_CHARS = 20000
43
41
  /** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
44
42
  const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
@@ -60,58 +58,15 @@ export function configureBatchSegment(impl) {
60
58
  /** 撤销注入(测试与端装配生命周期用——缺省态 = 零记账)。 */
61
59
  export function resetBatchSegment() { injectedOnWrite = null }
62
60
 
63
- /** 可读文件判据(存在且为文件——目录/缺失同判不可读;与 lifecycle 档同名 helper 同型——
64
- * KD-4 单向依赖下各自持有,三行谓词不构成第二权威源)。 */
65
- function readableFile(abs) {
66
- try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
67
- }
68
-
69
61
  /**
70
- * 批次档路径门禁(评审侧 §2.20.2 口径 = **「若传则须可读」**):空/非字符串/不可读 → throw。
71
- * 非空且可读 → 返回绝对路径(`\` 归一——照 `files`/`batchDoc` spawn 门先例)。
72
- * v2:cwd 不可读 → manifest `docRoot.batches` 复判(M3 模块设计 §2.1#4——N3 可迁移;
73
- * 值形态 = 串 | 多根数组——逐基底按序复判,首个可读者胜)。
62
+ * 批次档路径解析单源(`BATCH-RECORD.md` §4.15 · 台账 #287——实现住 `batch-paths.mjs`,本档 re-export):
63
+ * 候选序 cwd → 项目根 → 逐基底 · 读面取首个**可读**(口径仍「若传则须可读」,错误文案逐字零变)·
64
+ * create 取首个落基底内 · 锚定防嵌套(按路径段判)。四处调用点(create / 读面 / 评审门 / spawn 门)
65
+ * 均经该叶档——本档不再持第二份解析实现。
74
66
  */
75
- export function resolveBatchDocPath(cwd, given) {
76
- const base = cwd ?? process.cwd()
77
- const raw = typeof given === "string" ? given.trim() : ""
78
- if (!raw) {
79
- throw new Error("batchDoc must be a non-empty path to the batch record — pass the batch record currently in flight, or omit the parameter entirely when no batch record is in flight.")
80
- }
81
- const abs = resolve(base, raw.replace(/\\/g, "/"))
82
- if (readableFile(abs)) return abs
83
- // 双基底(M3 模块设计 §2.1#4):cwd 不可读 → manifest docRoot.batches 复判(逐基底按序,
84
- // 首个可读者胜);manifest 缺失 / 非法 / 读错 → 无第二基底(v1 单基底语义零变),
85
- // 全不可读 → throw。
86
- let man = { ok: false }
87
- try { man = readManifest(base) } catch { /* 权限等读错——按无 manifest 处理(v1 语义) */ }
88
- if (man.ok) {
89
- for (const root of docRootPaths(man.manifest?.docRoot?.batches, base)) {
90
- const alt = resolve(root, raw.replace(/\\/g, "/"))
91
- if (readableFile(alt)) return alt
92
- }
93
- }
94
- throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.`)
95
- }
96
67
 
97
68
  /**
98
- * 批次档基底根数组(**声明面单源**——轮 2 裁定:create 相对基底 / depth-0 在飞扫描共用,
99
- * 不写死 `docs/batches`):manifest `docRoot.batches`(串 | 多根数组)→ `docRootPaths`
100
- * 归一去重(相对基底按项目根解析);manifest 缺失/非法/读错 → 回退默认基底 `"docs/batches"`
101
- * (v1 单基底语义零变)。恒返回非空数组(create 的 bases[0] / findInFlightBatch 扫描面消费)。
102
- * @param {string} cwd
103
- * @returns {string[]}
104
- */
105
- export function batchDocBases(cwd) {
106
- const base = cwd ?? process.cwd()
107
- let man = { ok: false }
108
- try { man = readManifest(base) } catch { /* 读错 → 按无 manifest 处理(回退默认基底) */ }
109
- const roots = man.ok ? docRootPaths(man.manifest?.docRoot?.batches, base) : []
110
- return roots.length ? roots : [resolve(docRootBase(base), "docs/batches")]
111
- }
112
-
113
- /**
114
- * 设计评审的实例绑定解析(§2.20.2/§2.20.3——评审实例的唯一路径来源):
69
+ * 设计评审的实例绑定解析(BATCH-RECORD.md §4.2/§4.3——评审实例的唯一路径来源):
115
70
  * - 同步路径:调用方(advisor 工具)把实例绑定显式放进 callbacks(**带 batchDoc 键**,
116
71
  * 未绑定即 null)——以此为准,不再回看池条目(防同步/异步混跑时串档);
117
72
  * - 异步路径:本评审所在**池条目**的 `run.batchDoc`(文档集 = 实例键,各评审各取各条)。
@@ -143,7 +98,7 @@ function allowedSegment(agent, review) {
143
98
  }
144
99
 
145
100
  /**
146
- * 凭证剥除 + 伪造轮次标题丢弃(§2.20.1 F6/AC30/AC34)。
101
+ * 凭证剥除 + 伪造轮次标题丢弃(BATCH-RECORD.md §4.1 F6/AC30/AC34)。
147
102
  * 逐行处理:含凭证形态的行 → 剥除该子串,剥后为空则整行丢弃(其余内容逐字保留)。
148
103
  * `dropStamp`(仅 §3 目标段):调用方自带的 `### 轮次 N(评审子代理)` 行被丢弃——
149
104
  * 工具生成才是唯一来源戳(否则伪造戳会污染 N 计数)。
@@ -226,7 +181,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
226
181
  }
227
182
  abs = resolveBatchDocPath(cwd, givenPath)
228
183
  } else if (depth0) {
229
- abs = findInFlightBatch(cwd, batchDocBases(cwd))
184
+ abs = findInFlightBatch(cwd, batchDocBases(cwd)) // 歧义锚 ⇒ 基底 = 候选并集(#828——唯一在飞批照取 / ≥2 照列)
230
185
  } else {
231
186
  if (!batchDoc) {
232
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.")
@@ -239,7 +194,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
239
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.")
240
195
  }
241
196
  if (status === "unknown") {
242
- 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).")
243
198
  }
244
199
  if (typeof args?.text !== "string") {
245
200
  throw new Error("batch_segment: text must be a string (the markdown to append).")
@@ -248,7 +203,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
248
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 顺延).`)
249
204
  }
250
205
  if (/^## §\d/m.test(args.text)) {
251
- 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).")
252
207
  }
253
208
  const body = sanitizeText(args.text, n === 3).replace(/^\n+/, "").replace(/\s+$/, "")
254
209
  if (!body.trim()) {
@@ -274,18 +229,7 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
274
229
  export function batchTool(batchDoc = null, { review = false } = {}) {
275
230
  return {
276
231
  name: "batch",
277
- description:
278
- "Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). " +
279
- "actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic + source) · " +
280
- "append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · " +
281
- "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) · " +
282
- "close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused). " +
283
- "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. " +
284
- "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. " +
285
- "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). " +
286
- "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). " +
287
- "Credential values are stripped mechanically before writing (never write a token or designId value). " +
288
- "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
289
233
  parameters: {
290
234
  type: "object",
291
235
  properties: {
@@ -336,7 +280,7 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
336
280
  async execute(args, ctx) {
337
281
  const agent = ctx?.agent ?? {}
338
282
  const cwd = agent.cwd ?? process.cwd()
339
- const bases = batchDocBases(cwd)
283
+ const bases = batchDocBases(cwd) // 歧义锚 ⇒ 候选项目基底并集(#828——create / 在飞 / 写门基底集同源)
340
284
  const onWritten = injectedOnWrite
341
285
  /** depth-0 目标定位(D-BR21):path 传 ⇒ 可读性门;缺省 ⇒ 唯一在飞批(depth-0)/
342
286
  * 绑定解析(子代理/评审)。status/close 经此注入 lifecycle;append 自带同款分支。 */
@@ -367,44 +311,3 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
367
311
  },
368
312
  }
369
313
  }
370
-
371
- /**
372
- * 过渡别名(BATCH-RECORD §4.14——撤除判据见该节):name 仍 "batch_segment"、schema 仅
373
- * {segment, text}、描述**逐字**保持原 batch-segment.mjs 基准;execute ⇒ append(同一执行体,
374
- * 行为等价——AC-2/AC-9)。生产挂载面已全切主名 `batch`(BATCH-RECORD §4.3);本工厂仅供
375
- * 过渡 shim 导出面(旧测试 / VSC 侧 import 消费)与撤除判据实跑使用。
376
- * @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键)
377
- * @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
378
- */
379
- export function batchSegmentTool(batchDoc = null, { review = false } = {}) {
380
- const tool = batchTool(batchDoc, { review })
381
- const own = review ? "§3" : null
382
- return {
383
- name: "batch_segment",
384
- description:
385
- "Append your own section of the batch record (一段一作者). " +
386
- "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 " +
387
- "(eng-designer → §2, design review → §3, eng-coder → §5) — a write outside your own section is refused. " +
388
- "Append-only: the text lands at the end of your section; existing lines are never rewritten or deleted. " +
389
- "Credential values are stripped mechanically before writing (never write a token or designId value). " +
390
- "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). " +
391
- "Failures are hard and visible (no silent fallback): if the write is refused or fails, say so in your report — “§× 未写入”.",
392
- parameters: {
393
- type: "object",
394
- properties: {
395
- segment: {
396
- type: "string",
397
- description: `The batch-record section you are writing${own ? ` — yours is ${own}` : ""}. Declares the section number only; your identity decides what is actually writable.`,
398
- },
399
- text: {
400
- type: "string",
401
- 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).",
402
- },
403
- },
404
- required: ["segment", "text"],
405
- },
406
- async execute(args, ctx) {
407
- return tool.execute({ action: "append", segment: args?.segment, text: args?.text }, ctx)
408
- },
409
- }
410
- }
@@ -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
  }