@thincoder/core 0.10.3 → 0.10.5

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 (172) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +1 -1
  3. package/advisor/citations.mjs +29 -3
  4. package/advisor/compaction.mjs +31 -7
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +2 -1
  8. package/advisor/run.mjs +9 -5
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/assemble.mjs +1 -1
  12. package/agent/chat-call.mjs +1 -1
  13. package/agent/child-marks.mjs +2 -2
  14. package/agent/completion.mjs +3 -1
  15. package/agent/digest-account.mjs +189 -0
  16. package/agent/dispatch-gates.mjs +8 -0
  17. package/agent/dispatch.mjs +39 -18
  18. package/agent/family-tools.mjs +27 -16
  19. package/agent/helpers.mjs +12 -3
  20. package/agent/light-round.mjs +66 -0
  21. package/agent/post-turn.mjs +8 -4
  22. package/agent/record-results.mjs +4 -0
  23. package/agent/run-stages.mjs +32 -7
  24. package/agent/run-start.mjs +50 -3
  25. package/agent/setup-reminders.mjs +1 -1
  26. package/agent/setup.mjs +18 -3
  27. package/agent/subagent-event.mjs +1 -1
  28. package/agent/suspension.mjs +37 -12
  29. package/agent/timers.mjs +56 -5
  30. package/agent/turn-loop.mjs +30 -5
  31. package/agent/write-gate.mjs +20 -32
  32. package/agent-tools/advisor-async.mjs +15 -7
  33. package/agent-tools/advisor-settle.mjs +2 -2
  34. package/agent-tools/advisor.mjs +34 -33
  35. package/agent-tools/async-discard.mjs +34 -0
  36. package/agent-tools/async-settle.mjs +32 -7
  37. package/agent-tools/audit-block.mjs +1 -1
  38. package/agent-tools/bash-async.mjs +1 -1
  39. package/agent-tools/batch-lifecycle.mjs +59 -17
  40. package/agent-tools/batch-paths.mjs +24 -4
  41. package/agent-tools/batch-skeleton.mjs +59 -43
  42. package/agent-tools/batch.mjs +39 -18
  43. package/agent-tools/browser-async.mjs +198 -0
  44. package/agent-tools/checkpoint.mjs +1 -1
  45. package/agent-tools/design-slots.mjs +232 -0
  46. package/agent-tools/design-token.mjs +20 -0
  47. package/agent-tools/digest-budget.mjs +1 -1
  48. package/agent-tools/escalate-async.mjs +4 -2
  49. package/agent-tools/manifest.mjs +193 -0
  50. package/agent-tools/parent-channel.mjs +7 -4
  51. package/agent-tools/review-facts.mjs +143 -16
  52. package/agent-tools/settings.mjs +1 -1
  53. package/agent-tools/skill.mjs +1 -1
  54. package/agent-tools/subagent-actions-query.mjs +13 -6
  55. package/agent-tools/subagent-async.mjs +31 -15
  56. package/agent-tools/subagent-panel.mjs +2 -1
  57. package/agent-tools/subagent-scheduler.mjs +8 -5
  58. package/agent-tools/subagent-spawn.mjs +30 -56
  59. package/agent-tools/subagent.mjs +21 -8
  60. package/agent-tools/timer.mjs +186 -12
  61. package/agent-tools/zero-write-watch.mjs +77 -0
  62. package/agent-tools.mjs +2 -1
  63. package/agent.mjs +2 -2
  64. package/attachments.mjs +1 -1
  65. package/config-io.mjs +18 -13
  66. package/config-migrate.mjs +13 -29
  67. package/config-presets.mjs +33 -29
  68. package/config.mjs +68 -25
  69. package/context-degrade.mjs +1 -1
  70. package/context-echo.mjs +1 -1
  71. package/context-push.mjs +2 -2
  72. package/context.mjs +1 -1
  73. package/conventions.mjs +28 -8
  74. package/declaration.mjs +18 -2
  75. package/escape.mjs +1 -1
  76. package/file-links.mjs +1 -1
  77. package/file-refs.mjs +1 -1
  78. package/history-window.mjs +1 -1
  79. package/i18n.mjs +7 -0
  80. package/ledger-cmd.mjs +62 -16
  81. package/ledger-db.mjs +6 -0
  82. package/ledger-surface.mjs +49 -5
  83. package/ledger-tools.mjs +158 -87
  84. package/ledger-variant-notice.mjs +47 -0
  85. package/ledger.mjs +11 -18
  86. package/log.mjs +1 -1
  87. package/manifest-discovery.mjs +1 -1
  88. package/manifest-schema.mjs +7 -5
  89. package/manifest.mjs +5 -3
  90. package/mcp/transport-stdio.mjs +1 -1
  91. package/mcp.mjs +1 -1
  92. package/memory/code-index.mjs +1 -1
  93. package/memory/code-search.mjs +1 -1
  94. package/memory/code-sync.mjs +1 -1
  95. package/memory/docs.mjs +6 -2
  96. package/memory/file-list.mjs +1 -1
  97. package/memory/file-walk.mjs +17 -5
  98. package/memory/maintain.mjs +410 -0
  99. package/memory/memory-tool.mjs +21 -6
  100. package/memory/schema.mjs +19 -7
  101. package/memory/sweep.mjs +8 -5
  102. package/memory/sync-tail.mjs +16 -10
  103. package/model-ref.mjs +16 -13
  104. package/model-specs.mjs +21 -9
  105. package/package.json +1 -1
  106. package/peer-instances.mjs +1 -1
  107. package/permission.mjs +33 -0
  108. package/process-probe.mjs +2 -2
  109. package/prompts/advisor-design.md +4 -2
  110. package/prompts/advisor-round1.md +1 -1
  111. package/prompts/advisor-round2.md +1 -1
  112. package/prompts/advisor-round3.md +1 -1
  113. package/prompts/common.md +38 -4
  114. package/prompts/consult-base.md +1 -1
  115. package/prompts/discipline-engineering.md +32 -15
  116. package/prompts/discipline-normal.md +46 -2
  117. package/prompts/persona-eng-designer.md +6 -6
  118. package/prompts/persona-engineering.md +18 -9
  119. package/prompts/persona-normal.md +2 -0
  120. package/prompts/subagent-base.md +3 -0
  121. package/provider/anthropic.mjs +14 -2
  122. package/provider/core.mjs +6 -0
  123. package/provider/errors.mjs +28 -3
  124. package/provider/google.mjs +22 -10
  125. package/provider/list-models.mjs +3 -2
  126. package/provider/responses-request.mjs +3 -1
  127. package/provider/responses.mjs +38 -4
  128. package/provider/sse.mjs +3 -0
  129. package/provider-flows.mjs +14 -15
  130. package/proxy-chunked.mjs +102 -0
  131. package/proxy-target.mjs +54 -0
  132. package/proxy-transport.mjs +298 -0
  133. package/proxy.mjs +17 -248
  134. package/session-gc.mjs +2 -2
  135. package/session-lifecycle.mjs +18 -14
  136. package/session-slot-claims.mjs +2 -2
  137. package/session-slots-manifest.mjs +1 -1
  138. package/session-slots.mjs +3 -3
  139. package/shell-identity.mjs +107 -0
  140. package/team-ledger.mjs +282 -0
  141. package/team.mjs +223 -0
  142. package/tool-docs/advisor.md +1 -1
  143. package/tool-docs/bash.md +2 -1
  144. package/tool-docs/batch.md +1 -1
  145. package/tool-docs/browser.md +48 -0
  146. package/tool-docs/delete.md +7 -5
  147. package/tool-docs/ledger.md +6 -0
  148. package/tool-docs/manifest.md +16 -0
  149. package/tool-docs/memory.md +1 -0
  150. package/tool-docs/notify_parent.md +5 -0
  151. package/tool-docs/process.md +4 -4
  152. package/tool-docs/question.md +1 -0
  153. package/tool-docs/subagent.md +14 -3
  154. package/tool-docs/team_ledger.md +9 -0
  155. package/tool-docs/timer.md +6 -0
  156. package/tool-docs/wait_for.md +1 -0
  157. package/tools/bash.mjs +9 -4
  158. package/tools/browser.mjs +161 -0
  159. package/tools/git.mjs +1 -1
  160. package/tools/index.mjs +4 -4
  161. package/tools/ops.mjs +26 -10
  162. package/tools/patch.mjs +8 -2
  163. package/tools/repomap.mjs +1 -1
  164. package/tools/write-path.mjs +8 -6
  165. package/traces/trace-cleanup.mjs +1 -1
  166. package/traces/trace-store.mjs +1 -1
  167. package/vision-reader.mjs +10 -16
  168. package/tool-docs/ledger_add.md +0 -1
  169. package/tool-docs/ledger_close.md +0 -1
  170. package/tool-docs/ledger_count.md +0 -1
  171. package/tool-docs/ledger_query.md +0 -1
  172. package/tool-docs/ledger_update.md +0 -1
@@ -0,0 +1,232 @@
1
+ /**
2
+ * design-slots.mjs — design-token 槽位清点 + 消费核(2026-10-07 批 ledger-tool · 台账 #927 并入)。
3
+ * 设计单源 = `docs/core/design/DESIGN-TOKEN-SETTLEMENT.md` §10(F-SL1 清点面 / F-SL2 批量消费)。
4
+ *
5
+ * 面一 `executeDesignSlotsAction`(`subagent` action `design-slots`——只读):
6
+ * 数据源 = 槽文件权威表(`readEngTokensFromSlot`——无 TTL 过滤原样)∪ 内存 Map(并集去重,
7
+ * 冲突以槽为准——D-S3 同源);**零写**(不 reconcile 回写、不落盘)。列表面 = 逐槽一行
8
+ * (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥ 批;龄序 = 最老在前)+ 计数尾行;
9
+ * 「批」= `_advisorRuns` 按 designId 命中 run 的 `docSetKey` 中 `batches/` 段档取 basename
10
+ * (KD-SL2——零新存储;不可得 ⇒ `—`)。
11
+ * 面二 `executeConsumeDesignAction`(`subagent` action `consume-design` 消费核——自
12
+ * `subagent-spawn.mjs` 迁入):选择器恰一(`designId` ∥ `designIds` ∥ `expired` ∥
13
+ * `olderThanDays`——KD-SL3 fail-closed 不猜);目标集 = 并集视图按选择器解析 → 逐枚移出
14
+ * (`removeDesignTokenSlot` 同款 token 值守护)→ **单次** `persistEngTokens` 落盘(D-S6 单点);
15
+ * 落盘失败 ⇒ 内存快照整体回滚 + 抛(KD-SL4——不留半消费态)。单枚形态(designId / 无选择器
16
+ * 单槽):语义 / 拒文 / 回执**现行逐字不变**。
17
+ *
18
+ * 签发链行为零改(mint / settle / TTL / persist 存储形——#868 面另论);工程模式父侧(depth-0)限定
19
+ * (门 = 本档两执行器;depth 面由装配与受限变体门承载)。
20
+ */
21
+ import { persistEngTokens, readEngTokensFromSlot, reconcileEngTokensFromSlot, removeDesignTokenSlot, tokenExpired, tokenExpiryMs } from "../token-ttl.mjs"
22
+ import { effectiveTokenTtlMs } from "./design-token.mjs"
23
+ import { advisorRuns } from "./advisor-async.mjs"
24
+
25
+ const DAY_MS = 86400000
26
+
27
+ /** 槽并集视图(F-SL1 数据源 / F-SL2 目标集解析):内存 Map 先行、槽文件权威表覆盖(同 id 冲突以槽
28
+ * 为准——D-S3 同源);纯读——零回写、零落盘。 */
29
+ function unionSlots(agent) {
30
+ const merged = new Map()
31
+ const mem = agent?._engDesignTokens
32
+ if (mem instanceof Map) for (const [id, t] of mem) merged.set(id, t)
33
+ const file = readEngTokensFromSlot(agent)
34
+ if (file) for (const [id, t] of Object.entries(file)) merged.set(id, t)
35
+ return merged
36
+ }
37
+
38
+ /** 「批」列派生(KD-SL2——零新存储):会话内评审实例注册表 `_advisorRuns` 按 designId 命中的**最新**
39
+ * run ⇒ `docSetKey`(被审文档绝对路径集 JSON)中 `batches/` 段档取 basename;不可得(跨重启 /
40
+ * 模式重进 / 注册表回收)⇒ `—`(如实降级)。 */
41
+ function batchOf(agent, designId) {
42
+ const runs = advisorRuns(agent)
43
+ if (!(runs instanceof Map)) return "—"
44
+ for (const run of [...runs.values()].reverse()) {
45
+ if (run?.designId !== designId || typeof run.docSetKey !== "string") continue
46
+ const name = batchBasename(run.docSetKey)
47
+ if (name) return name
48
+ }
49
+ return "—"
50
+ }
51
+
52
+ /** docSetKey(JSON 串)→ `batches/` 段档 basename(首命中);不可解析 / 无命中 ⇒ null。 */
53
+ function batchBasename(docSetKey) {
54
+ let list = null
55
+ try { list = JSON.parse(docSetKey) } catch { return null }
56
+ if (!Array.isArray(list)) return null
57
+ for (const p of list) {
58
+ if (typeof p !== "string") continue
59
+ const parts = p.split(/[\\/]/)
60
+ const i = parts.lastIndexOf("batches")
61
+ if (i >= 0 && i < parts.length - 1 && parts[i + 1]) return parts[i + 1]
62
+ }
63
+ return null
64
+ }
65
+
66
+ /** 槽事实(清点列与选择器共用):状态 = `tokenExpired` 单源;`ageDays` = now − (expiresAt −
67
+ * `effectiveTokenTtlMs`) 派生(TTL 配置曾改时为近似——如实注);`expiresAt` 精确;畸形 / 格式外
68
+ * token ⇒ 两项 `—`(不可判龄)。 */
69
+ function slotFacts(agent, designId, token, now, ttlMs) {
70
+ const expiry = typeof token === "string" ? tokenExpiryMs(token) : null
71
+ return {
72
+ designId, token,
73
+ expired: typeof token === "string" && tokenExpired(token, now),
74
+ ageDays: expiry === null ? null : (now - (expiry - ttlMs)) / DAY_MS,
75
+ expiresAt: expiry,
76
+ batch: batchOf(agent, designId),
77
+ }
78
+ }
79
+
80
+ /** 龄序比较(最老在前——剩余有效期升序;不可判龄(畸形串)排尾——不冒充最老)。 */
81
+ const byOldest = (a, b) => (b.ageDays ?? -Infinity) - (a.ageDays ?? -Infinity)
82
+
83
+ /**
84
+ * `subagent` action `design-slots`(F-SL1——只读清点面):工程模式父侧限定(与 consume-design
85
+ * 同门)。输出 = 行文本(逐槽行 + 计数尾行);字段 / 序 / 降级标记 = 契约,行内措辞 = 实现面。
86
+ * 分类 = 只读(planMode 放行 / 免审批 / digest 放行——谓词在 `agent/dispatch-gates.mjs`)。
87
+ */
88
+ export function executeDesignSlotsAction(args, ctx) {
89
+ const parent = ctx?.agent
90
+ if (!parent?.config?.agent?.engineering) {
91
+ throw new Error("Engineering mode is not active — design-slots applies only to engineering-mode design tokens (spawn 同门).")
92
+ }
93
+ const now = Date.now()
94
+ const ttlMs = effectiveTokenTtlMs(parent)
95
+ const entries = [...unionSlots(parent)].map(([id, t]) => slotFacts(parent, id, t, now, ttlMs))
96
+ entries.sort(byOldest)
97
+ const lines = entries.map((e) =>
98
+ `- designId=${e.designId} status=${e.expired ? "expired" : "live"} ageDays=${e.ageDays === null ? "—" : e.ageDays.toFixed(1)} expiresAt=${e.expiresAt === null ? "—" : new Date(e.expiresAt).toISOString()} batch=${e.batch}`)
99
+ const live = entries.filter((e) => !e.expired).length
100
+ lines.push(`design-slots: ${entries.length} slot(s) — ${live} live / ${entries.length - live} expired`)
101
+ return lines.join("\n")
102
+ }
103
+
104
+ /**
105
+ * `subagent` action `consume-design` 消费核(F-SL2;自 `subagent-spawn.mjs` 迁入——2026-10-07):
106
+ * 选择器恰一——`designId`(现行 · 单枚)∥ `designIds`(显式非空清单)∥ `expired:true`(全部过期项)
107
+ * ∥ `olderThanDays:N`(ageDays > N);多选择器同给 / 选择器缺失且多槽 ⇒ 拒(fail-closed——不猜)。
108
+ * 单枚形态走 `consumeSingle`(拒文 / 回执逐字不变);批量走 `consumeBatch`(逐枚行 + `consumed N` 计数行)。
109
+ */
110
+ export function executeConsumeDesignAction(args, ctx) {
111
+ const parent = ctx?.agent
112
+ if (!parent?.config?.agent?.engineering) {
113
+ throw new Error("Engineering mode is not active — consume-design applies only to engineering-mode design tokens (spawn 同门).")
114
+ }
115
+ const slots = parent?._engDesignTokens
116
+ const hasSlots = slots instanceof Map && slots.size > 0
117
+ const designId = args?.designId ? String(args.designId) : undefined
118
+ const selectors = []
119
+ if (designId !== undefined) selectors.push("designId")
120
+ if (args?.designIds !== undefined && args?.designIds !== null) selectors.push("designIds")
121
+ if (args?.expired !== undefined && args?.expired !== null) selectors.push("expired")
122
+ if (args?.olderThanDays !== undefined && args?.olderThanDays !== null) selectors.push("olderThanDays")
123
+ if (selectors.length > 1) {
124
+ throw new Error(`consume-design: pass exactly ONE selector — designId / designIds / expired / olderThanDays (got: ${selectors.join(" + ")}); a batch consume never guesses its target set.`)
125
+ }
126
+ if (selectors.length === 0) {
127
+ // 无选择器 = 单枚兼容形态(现行语义零变):缺 designId 且多槽 ⇒ 拒(不误消费任一槽)
128
+ if (hasSlots && slots.size > 1) {
129
+ throw new Error(`consume-design: Multiple approved designs in this session (${slots.size}) — pass the designId parameter (echoed with each token) to choose which design to close out.`)
130
+ }
131
+ return consumeSingle(parent, undefined)
132
+ }
133
+ if (selectors[0] === "designId") return consumeSingle(parent, designId)
134
+ return consumeBatch(parent, selectors[0], args)
135
+ }
136
+
137
+ /** 单枚消费(现行形态逐字保留——读内存槽;未知 / 已消费 ⇒ 幂等 no-op;落盘失败 ⇒ 槽回滚 + 抛)。 */
138
+ function consumeSingle(parent, designId) {
139
+ const slots = parent?._engDesignTokens
140
+ const hasSlots = slots instanceof Map && slots.size > 0
141
+ // 读槽值:给定 designId → 精确槽(未知/已消费 → undefined);缺省 → 唯一槽
142
+ let token = null
143
+ if (designId && hasSlots) token = slots.get(designId) ?? null
144
+ else if (!designId && hasSlots) token = [...slots.values()][0]
145
+ // 未知 designId / 已消费 / 无任何槽 → 幂等 no-op 提示(不报错——评审 #3 定死)
146
+ if (!token) {
147
+ return `consume-design: no live slot${designId ? ` for designId ${designId}` : ""} (already consumed or never issued) — idempotent no-op, nothing changed.`
148
+ }
149
+ const slotId = designId ?? [...slots.keys()][0]
150
+ removeDesignTokenSlot(parent, designId, token)
151
+ try {
152
+ persistEngTokens(parent)
153
+ } catch (e) {
154
+ if (slotId) parent._engDesignTokens.set(slotId, token)
155
+ throw new Error(`consume-design: the slot was removed in memory but could NOT be durably deleted from the slot file (${e.message}) — the slot is restored in memory; retry consume-design.`)
156
+ }
157
+ return `design slot consumed — designId ${designId ?? "(single-design session)"} is closed out; a further eng-coder spawn for this design is mechanically rejected, and any new work (including deviation fixes) requires a fresh advisor(type='design') review and token.`
158
+ }
159
+
160
+ /** 批量消费(F-SL2 / KD-SL3 / KD-SL4):目标集 = 并集视图按选择器解析;逐枚移出(token 值守护)→
161
+ * 单次 persist;落盘失败 ⇒ 内存快照整体回滚 + 抛。回执 = 逐枚行 + 计数行(`consumed N`)。
162
+ * 落盘前先经 D2 权威表 reconcile(槽 = 权威;未选中的**活**槽因此并入内存、随 persist 保住——
163
+ * 过期项则随本次落盘一并出清,即「落盘携带(如实)」注)。 */
164
+ function consumeBatch(parent, selector, args) {
165
+ const now = Date.now()
166
+ const ttlMs = effectiveTokenTtlMs(parent)
167
+ const snapshot = parent._engDesignTokens instanceof Map ? new Map(parent._engDesignTokens) : null
168
+ reconcileEngTokensFromSlot(parent)
169
+ const union = unionSlots(parent)
170
+ const targets = []
171
+ const seen = new Set()
172
+ if (selector === "designIds") {
173
+ if (!Array.isArray(args.designIds) || args.designIds.length === 0) {
174
+ throw new Error("consume-design: designIds must be a non-empty array of design ids — pass an explicit list, or use expired / olderThanDays.")
175
+ }
176
+ for (const raw of args.designIds) {
177
+ const id = typeof raw === "string" ? raw.trim() : ""
178
+ if (!id) throw new Error(`consume-design: designIds entries must be non-empty strings (got ${JSON.stringify(raw)}).`)
179
+ if (seen.has(id)) continue // 重复条目去重(同一 id 只计一次——回执计数不虚增)
180
+ seen.add(id)
181
+ const token = union.get(id) ?? null
182
+ targets.push({ designId: id, token, consumed: token !== null }) // 未知 / 已消费 ⇒ 逐枚 no-op(幂等)
183
+ }
184
+ } else if (selector === "expired") {
185
+ if (args.expired !== true) {
186
+ throw new Error("consume-design: expired must be true — it selects every expired slot; pass designIds / olderThanDays for other shapes.")
187
+ }
188
+ for (const [id, t] of union) {
189
+ if (typeof t === "string" && tokenExpired(t, now)) {
190
+ const expiry = tokenExpiryMs(t)
191
+ targets.push({ designId: id, token: t, consumed: true, ageDays: expiry === null ? null : (now - (expiry - ttlMs)) / DAY_MS })
192
+ }
193
+ }
194
+ targets.sort(byOldest)
195
+ } else {
196
+ const n = args.olderThanDays
197
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 0) {
198
+ throw new Error("consume-design: olderThanDays must be a non-negative number — pass the day threshold (e.g. olderThanDays: 30).")
199
+ }
200
+ for (const [id, t] of union) {
201
+ const expiry = typeof t === "string" ? tokenExpiryMs(t) : null
202
+ if (expiry === null) continue // 畸形串不可判龄——不被龄选择器选中(清点面照列)
203
+ const ageDays = (now - (expiry - ttlMs)) / DAY_MS
204
+ if (ageDays > n) targets.push({ designId: id, token: t, consumed: true, ageDays })
205
+ }
206
+ targets.sort(byOldest)
207
+ }
208
+ const removals = targets.filter((t) => t.consumed)
209
+ for (const t of removals) removeDesignTokenSlot(parent, t.designId, t.token)
210
+ if (removals.length > 0) {
211
+ try {
212
+ persistEngTokens(parent)
213
+ } catch (e) {
214
+ // KD-SL4:内存快照整体回滚 + 抛(不留半消费态——可重试)
215
+ if (snapshot) {
216
+ const map = parent._engDesignTokens instanceof Map ? parent._engDesignTokens : (parent._engDesignTokens = new Map())
217
+ map.clear()
218
+ for (const [id, v] of snapshot) map.set(id, v)
219
+ } else {
220
+ delete parent._engDesignTokens
221
+ }
222
+ throw new Error(`consume-design: ${removals.length} slot(s) were removed in memory but could NOT be durably deleted from the slot file (${e.message}) — the slots are restored in memory; retry consume-design.`)
223
+ }
224
+ }
225
+ const lines = targets.map((t) => (t.consumed
226
+ ? `- designId ${t.designId} — consumed`
227
+ : `- designId ${t.designId} — no live slot (already consumed or never issued) — idempotent no-op, nothing changed.`))
228
+ lines.push(removals.length > 0
229
+ ? `consumed ${removals.length} design slot(s) — a further eng-coder spawn for these designs is mechanically rejected; any new work (including deviation fixes) requires a fresh advisor(type='design') review and token.`
230
+ : "consumed 0 design slot(s) — nothing changed (idempotent no-op).")
231
+ return lines.join("\n")
232
+ }
@@ -95,6 +95,19 @@ export function stripDesignTokenEcho(text, designToken) {
95
95
  const NO_VALID_ECHO_MARK = "未回显有效 token——未签发 (no valid token echo — no design token was issued)"
96
96
  const TRUNCATED_ECHO_MARK = "注:回显被截断(uuid 之后缺失或不符)——已按批准结算;全串 token 已签发 (note: truncated echo — the part after the uuid was missing or inexact; settled as approved; the full token was issued)"
97
97
 
98
+ /** §5.2 verdict gate (#940): the echo alone is NOT the verdict — the receipt must
99
+ * carry a `VERDICT: pass` line and no `VERDICT: changes-required` line (fail-closed). */
100
+ const VERDICT_PASS_LINE = /^[ \t]*[*_`]*[ \t]*VERDICT:[ \t]*pass[ \t]*[*_`]*[ \t]*$/m
101
+ const VERDICT_CHANGES_LINE = /^[ \t]*[*_`]*[ \t]*VERDICT:[ \t]*changes-required[ \t]*[*_`]*[ \t]*$/m
102
+ export function verdictGateFailure(rawResult) {
103
+ if (typeof rawResult !== "string") return "no-pass-line"
104
+ if (VERDICT_CHANGES_LINE.test(rawResult)) return "changes-required"
105
+ return VERDICT_PASS_LINE.test(rawResult) ? null : "no-pass-line"
106
+ }
107
+
108
+ /** §5.2 rejection mark (verbatim — machine-greppable; reason = no-pass-line | changes-required). */
109
+ const VERDICT_GATE_MARK = (reason) => `裁定非通过——token 未签发 (review verdict is not pass — no design token was issued; reason: ${reason}; the echoed token was stripped)`
110
+
98
111
  /**
99
112
  * Shared design-review settlement (sync wrapper + async settle): the token echo
100
113
  * IS the verdict — the advisor echoes it only on approval. On echo (full string
@@ -134,6 +147,13 @@ export function settleDesignReview(agent, run, designToken, rawResult, opts = {}
134
147
  const stripped = stripDesignTokenEcho(rawResult, designToken).trim()
135
148
  return { passed: false, output: [stripped, NO_VALID_ECHO_MARK].filter(Boolean).join("\n\n") }
136
149
  }
150
+ // §5.2 裁定闸(#940):签发 = 回显 ∧ 裁定行——缺 `VERDICT: pass` 行或含 `VERDICT: changes-required` 行
151
+ // ⇒ 拒签(与 M1 径同形:剥净回显 + 恒定标记;槽零写 ∥ 实例保持——修后重评)。
152
+ const gateFailure = verdictGateFailure(rawResult)
153
+ if (gateFailure !== null) {
154
+ const stripped = stripDesignTokenEcho(rawResult, designToken).trim()
155
+ return { passed: false, output: [stripped, VERDICT_GATE_MARK(gateFailure)].filter(Boolean).join("\n\n") }
156
+ }
137
157
  // Echoed the token → review passed. Issue it to the parent for eng-coder.
138
158
  agent._engDesignTokens ??= new Map()
139
159
  agent._engDesignTokens.set(run.designId, designToken)
@@ -55,7 +55,7 @@ export function digestBudgetOver(agent, size) {
55
55
  return over
56
56
  }
57
57
 
58
- /** Test seam: 落盘目录重定向(单测沙箱——生产从不调用)。 */
58
+ /** Test seam: 落盘目录重定向(单元测试沙箱——生产从不调用)。 */
59
59
  let _digestOffloadDirOverride = null
60
60
  export function _setDigestOffloadDirForTest(dir) { _digestOffloadDirOverride = dir }
61
61
 
@@ -107,8 +107,10 @@ function touchedFilesNote(child, cwd) {
107
107
  export function classifyEscalateSettle(parent, entry) {
108
108
  if (entry.cancelled) return { cancelled: true }
109
109
  // AGENT-LOOP.md §6.15(TUI-OOM-ROOTCAUSE)——childAgent 语义面:settle 分类(touched 摘要/partial
110
- // merge)与 status/observe 摘要的读取源;**消化注入完成后由 `releaseSettledEntry`
111
- // (async-settle.mjs)置 null**(池内未消化窗口不释放——表 2 候选 2 否决)。
110
+ // merge)与 status/observe 摘要的读取源;**消化注入完成后释放**(池内未消化窗口不释放——表 2 候选 2 否决);
111
+ // fallback 面(无 `_daSession`)由 `releaseSettledEntry`(async-settle.mjs)置 null(逐字沿用);会话态
112
+ // (`_daSession`)注入后改调 `releaseChildHold`(仅释放 `childAgent`——`report` 保留至销账 / 升级;
113
+ // §6.31.4 ∥ KD-DA7)。
112
114
  const child = entry.childAgent
113
115
  const touched = child?._touchedFiles ?? []
114
116
  const overlap = overlapPaths(parent, entry.launchSeq, touched)
@@ -0,0 +1,193 @@
1
+ /**
2
+ * agent-tools/manifest.mjs — `manifest` 工具:M1 项目状态档(`PROJECT-MANIFEST.json`)的
3
+ * agent 侧读写出口(2026-10-08 · 台账 #1098;权威设计 = `docs/core/design/MANIFEST.md` §2.10)。
4
+ *
5
+ * 三动作(KD-M1-38):
6
+ * - `read` ⇒ `projectView(target)` 五态报明(稳定锚 `state=ok|missing|invalid|ambiguous|no-project`);
7
+ * `ok` 态再 `readManifest(view.root)` 取细节面(`projectView` 面不含 `missingKeys` /
8
+ * `unknownKeys`)——**不抛**(缺档报明,非拒)。
9
+ * - `init` ⇒ 缺档建默认档(`initManifest`——内容 = `DEFAULT_MANIFEST`);**已带档 ⇒ 拒**
10
+ * (不覆盖——`initManifest` 本体无覆盖保护,工具面补此闸)。
11
+ * - `write` ⇒ **点改**:读现档 → 设节点 → 落盘(`writeManifest` 内先 `validateManifest`——
12
+ * 编辑结果非法 ⇒ 拒 + 盘零变);**缺档 ⇒ 拒** + 引导 `init`(写不建档)。
13
+ *
14
+ * 写面白名单(判据单源 = `MANIFEST_SCHEMA`——钉死谓词):`keys` 中**非** `nestedKeys` 父键者
15
+ * (`version` / `phase` / `promptsLanding` / `codePaths` 整键)+ `nestedKeys` 子键
16
+ * (`docRoot.*` / `checkConfig.*` / `index.*`(含 `publicRepos`)/ `advisor.*`);**四族整键写
17
+ * (传整个对象)⇒ 拒**;白名单外 ⇒ 拒(拼写保护——未知键写盘 = 静默死键,KD-M1-36 问题本体)。
18
+ * `value` 解析 = 同款口径 `settings.mjs:216-228`(先 `JSON.parse`,失败取字面串;`"null"` ⇒ `null`
19
+ * ——`docRoot.*` 的「本面无」,KD-M1-37)。
20
+ *
21
+ * 目标面(KD-M1-39——判据复用零新解析器):`read` = `projectView`;`init` / `write` =
22
+ * `projectRootView`(歧义 ⇒ 拒 + 候选全列——不猜);`resolveProjectRoot` 对歧义塌缩 `null` 再回落 cwd
23
+ * ⇒ 写 / 建两动作**不得**直用。
24
+ *
25
+ * 写路径复用(零新写路径):落盘恒经 `writeManifest` / `initManifest`——`writer` 恒显式 `'main'`
26
+ * (缺省 `'subagent'` = 写门 fail-closed 缺省,KD-M1-3 / AC-M1-5 零改)。
27
+ *
28
+ * 报告面随动(KD-M1-40):本工具**不写** `agent.manifest`(第二值源否——工具目标可为非锚项目)——
29
+ * 写盘 = 盘面变化,情境行经既有 ③b mtime 门控于下回合采纳(`agent/setup-reminders.mjs`)。
30
+ */
31
+ import { existsSync } from "node:fs"
32
+ import { join, resolve } from "node:path"
33
+ import { MANIFEST_REL, MANIFEST_SCHEMA, initManifest, projectRootView, projectView, readManifest, writeManifest } from "../manifest.mjs"
34
+ import { DESC } from "../tools/shared.mjs"
35
+
36
+ /** 拒面统一前缀(设计 §2.10:`manifest <action>: refused — …`)——盘零变类拒绝一律经此抛错。 */
37
+ function refuse(action, reason) {
38
+ return new Error(`manifest ${action}: refused — ${reason}`)
39
+ }
40
+
41
+ /** `target` 解析(先例 `eng.mjs:81` 缺省会话锚 ∥ `git.mjs:32-35` 相对形归一):缺省 = 会话锚
42
+ * (`ctx.agent.cwd ?? ctx.cwd ?? process.cwd()`);显式相对形按会话锚归一(绝对形原样)。 */
43
+ function resolveTarget(args, ctx) {
44
+ const base = ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
45
+ return args?.target ? resolve(base, String(args.target)) : resolve(base)
46
+ }
47
+
48
+ /** 写面白名单谓词(**钉死**——设计 §2.10):点分路径恰两形——
49
+ * ① 单段:`keys` ∩ ∉ `nestedKeys`(= `version` / `phase` / `promptsLanding` / `codePaths` 整键);
50
+ * ② 双段:`nestedKeys[父]` 含子键(四族子键);其余(四族整键写 / 未知键 / 更深路径)⇒ 拒。
51
+ * `Array.isArray` 守卫:原型链名(`__proto__` / `constructor` / `toString` 类)取到非数组 ⇒ 落拒面
52
+ * (不得 TypeError 直出——白名单外一律 `refused` 统一前缀)。 */
53
+ function isWritableKey(key) {
54
+ const segs = String(key).split(".")
55
+ if (segs.length === 1) return MANIFEST_SCHEMA.keys.includes(segs[0]) && !Object.hasOwn(MANIFEST_SCHEMA.nestedKeys, segs[0])
56
+ if (segs.length === 2) {
57
+ const subs = MANIFEST_SCHEMA.nestedKeys[segs[0]]
58
+ return Array.isArray(subs) && subs.includes(segs[1])
59
+ }
60
+ return false
61
+ }
62
+
63
+ /** `value` 解析(同款口径 = `settings.mjs:216-228`):先 `JSON.parse`(`"null"` ⇒ `null`;数值 /
64
+ * 布尔 / 数组皆 JSON 形),失败取字面串(裸串如 `docs/design`)。 */
65
+ function parseValue(raw) {
66
+ const s = String(raw)
67
+ try { return JSON.parse(s) } catch { return s }
68
+ }
69
+
70
+ /** 点分路径设值(自动建中间对象——settings `setKeyPath` 同款;白名单谓词已保父段为已知族)。 */
71
+ function setKeyPath(obj, path, value) {
72
+ const segs = String(path).split(".")
73
+ let cur = obj
74
+ for (let i = 0; i < segs.length - 1; i++) {
75
+ if (cur[segs[i]] === null || typeof cur[segs[i]] !== "object") cur[segs[i]] = {}
76
+ cur = cur[segs[i]]
77
+ }
78
+ cur[segs[segs.length - 1]] = value
79
+ }
80
+
81
+ /** 歧义拒文案(候选全列——不猜;init / write 共用):多候选 = 显式指定 target,机制不代选。 */
82
+ function ambiguousReason(target, candidates) {
83
+ return `ambiguous target ${target} — ${candidates.length} candidate projects (the mechanism never picks one); pass target explicitly:\n` +
84
+ candidates.map((c) => `- ${c}`).join("\n")
85
+ }
86
+
87
+ /** read 动作(五态报明——**不抛**):`state=` 稳定锚 + 各态附行(ok 细节面 / missing 引导 /
88
+ * ambiguous 候选全列 / invalid 错误行)。 */
89
+ function readAction(target) {
90
+ const view = projectView(target)
91
+ const lines = [`state=${view.state}`]
92
+ if (view.path) lines.push(`path=${view.path}`)
93
+ if (view.state === "ok") {
94
+ const m = readManifest(view.root) // 细节面:missingKeys / unknownKeys / errors(非空才出)
95
+ lines.push(`manifest=${JSON.stringify(m.manifest)}`)
96
+ if (m.missingKeys?.length) lines.push(`missingKeys=${m.missingKeys.join(", ")}`)
97
+ if (m.unknownKeys?.length) lines.push(`unknownKeys=${m.unknownKeys.join(", ")}`)
98
+ if (m.errors?.length) lines.push(`errors=${m.errors.join(";")}`)
99
+ } else if (view.state === "missing") {
100
+ lines.push(`hint: run manifest action=init to create the default manifest here`)
101
+ } else if (view.state === "no-project") {
102
+ lines.push(`hint: no project on the ancestor chain and none below — manifest action=init can create one at the target`)
103
+ } else if (view.state === "ambiguous") {
104
+ lines.push(`candidates (${(view.candidates ?? []).length}):`)
105
+ for (const c of view.candidates ?? []) lines.push(`- ${c}`)
106
+ lines.push(`hint: pass target explicitly — the mechanism never picks one`)
107
+ } else if (view.state === "invalid") {
108
+ if (view.errors?.length) lines.push(`errors=${view.errors.join(";")}`)
109
+ }
110
+ return lines.join("\n")
111
+ }
112
+
113
+ export const manifestTool = {
114
+ name: "manifest",
115
+ description: DESC("manifest"),
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ action: { type: "string", enum: ["read", "init", "write"], description: "read — current manifest + validation state (five-state anchor, never throws); init — create the default manifest when missing (refused when one already exists); write — point-edit one node (key + value) of an existing manifest" },
120
+ target: { type: "string", description: "Explicit target directory — defaults to the session anchor; relative paths resolve against it. Ambiguous targets are refused with the full candidate list (never picks one)." },
121
+ key: { type: "string", description: "(write) Dot path of the node to set — version / phase / promptsLanding / codePaths, or a sub-key of docRoot / checkConfig / index / advisor (e.g. docRoot.design / index.publicRepos / advisor.docMap); whole-object writes of those four families are refused", },
122
+ value: { type: "string", description: "(write) New value — parsed as JSON first (null / true / 5 / [\"a\"]), otherwise kept as a literal string; \"null\" is the explicit no-such-face for docRoot.<key>" },
123
+ },
124
+ required: ["action"],
125
+ },
126
+ readonly: false, // 动作级分类(dispatch-gates `isSubagentReadonlyAction`):read = 只读类;init / write 保持侧效门
127
+ async execute(args, ctx) {
128
+ const action = args?.action
129
+ if (!["read", "init", "write"].includes(action)) {
130
+ throw new Error(`manifest: action must be one of read/init/write — got ${JSON.stringify(action)}`)
131
+ }
132
+ const target = resolveTarget(args, ctx)
133
+
134
+ if (action === "read") return readAction(target)
135
+
136
+ // init / write:目标面 = `projectRootView`(歧义直读形——不猜;KD-M1-39)
137
+ const rootView = projectRootView(target)
138
+ if (rootView.state === "ambiguous") {
139
+ throw refuse(action, ambiguousReason(target, rootView.candidates))
140
+ }
141
+
142
+ if (action === "init") {
143
+ const root = rootView.state === "ok" ? rootView.root : target // 梯⑤(无项目)⇒ 落点 = target 自身
144
+ const file = join(root, MANIFEST_REL)
145
+ if (existsSync(file)) {
146
+ throw refuse("init", `${file} already carries a manifest — init never overwrites (use action=write to edit it)`)
147
+ }
148
+ try {
149
+ initManifest(root, { writer: "main" }) // 落盘恒经写门(writer 闸 + 校验前闸——零新写路径)
150
+ } catch (e) {
151
+ throw refuse("init", `could not write the default manifest at ${file} — ${e?.message ?? String(e)}`)
152
+ }
153
+ return `manifest init: created default manifest (DEFAULT_MANIFEST)\npath=${file}`
154
+ }
155
+
156
+ // write:缺档 ⇒ 拒 + 引导 init(写不建档——多写权面唯一的建档动作 = init)
157
+ if (rootView.state === "none") {
158
+ throw refuse("write", `no project at ${target} — write requires an existing ${MANIFEST_REL}; run manifest action=init first (write never creates one)`)
159
+ }
160
+ const root = rootView.root
161
+ const file = join(root, MANIFEST_REL)
162
+ const key = args?.key
163
+ if (!key) throw new Error("manifest write: key is required (dot path, e.g. docRoot.design)")
164
+ if (args?.value === undefined) throw new Error("manifest write: value is required (JSON-parsed first, otherwise a literal string)")
165
+ if (!isWritableKey(key)) {
166
+ throw refuse("write", `key "${key}" is not writable — writable nodes: version / phase / promptsLanding / codePaths + sub-keys of docRoot / checkConfig / index / advisor (whole-object writes of the four families are refused)`)
167
+ }
168
+ let current
169
+ try {
170
+ current = readManifest(root)
171
+ } catch (e) {
172
+ throw refuse("write", `could not read ${file} — ${e?.message ?? String(e)}`)
173
+ }
174
+ if (!current.ok) {
175
+ if (current.reason === "missing") {
176
+ throw refuse("write", `${file} does not exist — run manifest action=init first (write never creates one)`)
177
+ }
178
+ if (!current.manifest) {
179
+ throw refuse("write", `${file} is not editable (fail-closed): ${(current.errors ?? []).join(";")}`)
180
+ }
181
+ // 现档可解析但校验非法:照常点改——编辑结果仍经落盘前 `validateManifest`(非法 ⇒ 拒 + 盘零变)
182
+ }
183
+ const value = parseValue(args.value)
184
+ const manifest = current.manifest
185
+ setKeyPath(manifest, String(key), value)
186
+ try {
187
+ writeManifest(root, manifest, { writer: "main" }) // 落盘前 validateManifest(编辑结果非法 ⇒ 拒 + 盘零变)
188
+ } catch (e) {
189
+ throw refuse("write", `the edited manifest fails the pre-write validation (disk unchanged) — ${e?.message ?? String(e)}`)
190
+ }
191
+ return `manifest write: set ${key} = ${JSON.stringify(value)}\npath=${file}`
192
+ },
193
+ }
@@ -20,7 +20,7 @@
20
20
  * 生命周期与子代理生命周期**解耦**(settle / cancel 不迁移、不清队列——F3)。
21
21
  * - 父侧唤醒 = ask 入队尾调 `wakeAsyncWaiters(parent)`(**同步**,零 await;`note` 不唤醒)——
22
22
  * 零新字段 / 零新容器 / 零新注册:等待栓数组由挂起驱动注册,非挂起期数组空 ⇒ no-op。
23
- * - 父侧消费 = `drainChildUpstream(agent)`(`thincoder-core/agent.mjs` 循环头单点,紧邻
23
+ * - 父侧消费 = `drainChildUpstream(agent)`(`thincoder-core/agent/turn-loop.mjs` 循环头单点,紧邻
24
24
  * `consumeInjected?.(agent)`——同址反向):全部 pending **合并一条** user 消息(`pushReal`
25
25
  * 非 transient——事务性事件落盘,D-UC5),每条附结束注脚(已 settle / 已 cancel 两形态;
26
26
  * 其余态不附——不臆断)。
@@ -151,10 +151,13 @@ function endNote(agent, entry) {
151
151
  return ""
152
152
  }
153
153
 
154
+ /** kind → 投递标识(单点渲染——机读面 / 日志留原 kind;用户 2026-10-06 裁定逐字)。 */
155
+ const KIND_MARK = { ask: "[上抛·待裁]", note: "[上抛·知会]" }
156
+
154
157
  /**
155
- * 父侧消费点(回合边界单点——`agent.mjs` 循环头;空队列 no-op = 零历史变更、零开销):
158
+ * 父侧消费点(回合边界单点——`agent/turn-loop.mjs` 循环头;空队列 no-op = 零历史变更、零开销):
156
159
  * 全部 pending 消息**合并为一条** user 消息注入(`pushReal`——不带 `transient`,事件落盘),
157
- * 按入队序逐条列示(来源 `role#id` + `kind` + message(`escapeXml`)+ 结束注脚)。
160
+ * 按入队序逐条列示(来源 `role#id` + **kind 标识**(`ask` ⇒ `[上抛·待裁]` ∥ `note` ⇒ `[上抛·知会]`)+ message(`escapeXml`)+ 结束注脚)。
158
161
  * @param {object} agent 父(消费方)agent 形态。
159
162
  * @returns {number} 本次消费条目数(0 = 空队列 no-op)。
160
163
  */
@@ -168,7 +171,7 @@ export function drainChildUpstream(agent) {
168
171
  + "Answer with subagent action:'send' (id + message) if the decision is yours; an unanswered ask means that child skips the part and reports it as not done.]"
169
172
  : `[System reminder: in-flight message from your subagent ${entries[0].from} — it keeps working on the unaffected parts. `
170
173
  + "Answer with subagent action:'send' (id + message) if the decision is yours; an unanswered ask means the child skips that part and reports it as not done.]"
171
- const rows = entries.map((e) => `${many ? "- " : ""}${e.kind} · ${e.from}: ${escapeXml(e.message)}${endNote(agent, e)}`)
174
+ const rows = entries.map((e) => `${many ? "- " : ""}${KIND_MARK[e.kind] ?? e.kind} · ${e.from}: ${escapeXml(e.message)}${endNote(agent, e)}`)
172
175
  pushReal(agent, { role: "user", content: [header, ...rows].join("\n") })
173
176
  return entries.length
174
177
  }