@thincoder/core 0.10.8 → 0.10.10

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 (85) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/advisor/messages.mjs +4 -5
  3. package/advisor/project-context.mjs +69 -44
  4. package/agent/family-tools.mjs +11 -9
  5. package/agent/helpers.mjs +12 -6
  6. package/agent/post-turn.mjs +34 -0
  7. package/agent/run-stages.mjs +43 -9
  8. package/agent/run-start.mjs +1 -0
  9. package/agent/setup.mjs +10 -2
  10. package/agent/turn-loop.mjs +23 -13
  11. package/agent-tools/audit-block.mjs +4 -2
  12. package/agent-tools/batch-gate.mjs +197 -0
  13. package/agent-tools/batch-lifecycle.mjs +216 -36
  14. package/agent-tools/batch-skeleton.mjs +87 -0
  15. package/agent-tools/batch.mjs +11 -3
  16. package/agent-tools/checkpoint.mjs +11 -6
  17. package/agent-tools/context.mjs +28 -5
  18. package/agent-tools/design-slots.mjs +7 -33
  19. package/agent-tools/doc-check.mjs +50 -7
  20. package/agent-tools/manifest.mjs +44 -12
  21. package/agent-tools/spawn-gates.mjs +100 -41
  22. package/agent-tools/ssh-session.mjs +108 -61
  23. package/agent-tools/ssh-transport.mjs +17 -4
  24. package/agent-tools/ssh-vault.mjs +291 -0
  25. package/agent-tools/subagent-spawn.mjs +52 -30
  26. package/agent-tools/subagent.mjs +14 -8
  27. package/agent-tools/timer.mjs +2 -2
  28. package/agent-tools.mjs +1 -1
  29. package/config-defaults.mjs +92 -0
  30. package/config-normalize.mjs +109 -0
  31. package/config.mjs +88 -197
  32. package/declaration.mjs +5 -12
  33. package/doc-check/anchors.mjs +68 -34
  34. package/doc-check/index.mjs +143 -32
  35. package/doc-check/version.mjs +84 -0
  36. package/doc-check/width.mjs +10 -54
  37. package/i18n.mjs +7 -0
  38. package/ledger-cmd.mjs +314 -61
  39. package/ledger-db.mjs +22 -10
  40. package/ledger-migrate.mjs +15 -9
  41. package/ledger-tools.mjs +115 -69
  42. package/ledger.mjs +13 -10
  43. package/log.mjs +2 -2
  44. package/manifest-schema.mjs +7 -21
  45. package/manifest.mjs +4 -4
  46. package/memory/code-search.mjs +5 -14
  47. package/memory/docs.mjs +5 -14
  48. package/memory/read-gate.mjs +26 -0
  49. package/package.json +1 -1
  50. package/permission.mjs +7 -1
  51. package/prompts/advisor-design.md +95 -44
  52. package/prompts/advisor-round1.md +75 -27
  53. package/prompts/advisor-round2.md +68 -31
  54. package/prompts/advisor-round3.md +77 -33
  55. package/prompts/common.md +283 -113
  56. package/prompts/consult-base.md +16 -9
  57. package/prompts/discipline-engineering.md +213 -83
  58. package/prompts/discipline-normal.md +137 -85
  59. package/prompts/persona-coder.md +8 -6
  60. package/prompts/persona-eng-coder.md +42 -21
  61. package/prompts/persona-eng-designer.md +101 -43
  62. package/prompts/persona-engineering.md +376 -127
  63. package/prompts/persona-explore.md +3 -3
  64. package/prompts/persona-normal.md +29 -21
  65. package/prompts/persona-plan.md +7 -7
  66. package/prompts/subagent-base.md +4 -4
  67. package/provider/anthropic.mjs +4 -2
  68. package/provider/core.mjs +4 -2
  69. package/provider/google.mjs +5 -1
  70. package/provider/responses-request.mjs +3 -2
  71. package/session-lifecycle.mjs +6 -6
  72. package/team.mjs +2 -1
  73. package/token-window.mjs +1 -1
  74. package/tool-docs/batch.md +1 -1
  75. package/tool-docs/doc_check.md +47 -12
  76. package/tool-docs/fetch.md +3 -2
  77. package/tool-docs/ledger.md +7 -5
  78. package/tool-docs/manifest-read.md +8 -0
  79. package/tool-docs/manifest.md +9 -8
  80. package/tool-docs/ssh.md +23 -6
  81. package/tool-docs/subagent.md +7 -12
  82. package/tool-docs/websearch.md +3 -1
  83. package/tools/ssh.mjs +24 -11
  84. package/tools/web-deepseek.mjs +172 -0
  85. package/tools/web.mjs +29 -4
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.10.9] — 2026-10-19
9
+
10
+ ### Removed
11
+
12
+ - **`design-slots` 的 `reveal` 开关与掩蔽(token-visibility-simplify 批 2026-10-17)**:清点面改**直读**——逐槽行恒携 `designToken`(过期照列、`status` 自标态;记录畸形 ⇒ `—`);删净 `reveal` 开关 / 掩蔽 / 一次一槽限制(用户裁定「没必要把 token 藏起来,agent 要就告诉他」);签发链(mint / settle / TTL / persist)零改。
13
+
8
14
  ## [0.10.8] — 2026-10-16
9
15
 
10
16
  > 0.10.7 → 0.10.8(月内 +1——发布时定号)
@@ -155,15 +155,14 @@ function buildAdvisorUserMessageInner(agent, prior, reviewType, designToken = nu
155
155
  }
156
156
  }
157
157
 
158
- // Engineering mode: the project's DECLARED standards document
159
- // (advisor.standardsDoc) — undeclared degrades visibly (PO-2).
158
+ // Engineering mode: the project standards section — the declaration face is gone with the
159
+ // manifest `advisor` key, so this is always an explicit degradation (PO-2).
160
160
  if (agent.config?.agent?.engineering) {
161
161
  injectProjectStandards(agent, parts, guideRoot)
162
162
  }
163
163
 
164
- // Document map: declared path wins, the built-in fallback probe (docs/README.md
165
- // → docs/design/README.md) stays; neither → explicit degradation sentence
166
- // (never the old silent skip).
164
+ // Document map: derived from the manifest `docRoot` (declared roots' README, then their common
165
+ // ancestor); neither → explicit degradation sentence (never a silent skip).
167
166
  injectDocumentMap(agent, parts, guideRoot ?? agent.cwd)
168
167
 
169
168
  parts.push("## Instructions")
@@ -12,13 +12,14 @@
12
12
  * absent — in a project with a different layout the reviewer still scored the
13
13
  * dimension it could no longer check (silent failure, the worst class: invisible
14
14
  * and unfixable by the user). Missing pieces now say so, and the reviewer is told
15
- * to state the limitation in its findings. Projects can point at their own files
16
- * through the project manifest (advisor.docMap / advisor.standardsDoc).
15
+ * to state the limitation in its findings. The document map is derived from the
16
+ * manifest's `docRoot` (the one place that says where the docs live); the standards
17
+ * face has no derivable convention name and is therefore an explicit degradation.
17
18
  */
18
19
  import { readFileSync, existsSync } from "node:fs"
19
20
  import { resolve, join, relative, dirname, sep } from "node:path"
20
21
  import { providerSpec } from "../config.mjs"
21
- import { loadProjectDeclaration } from "../conventions.mjs"
22
+ import { DEFAULT_MANIFEST, docRootPaths, readManifest } from "../manifest.mjs"
22
23
 
23
24
  /** Project guide (AGENTS.md) injection budget — decision 2026-08-08:
24
25
  * NO fixed truncation; long-context models (1M+) get up to 5% of their context
@@ -32,8 +33,8 @@ const PROJECT_GUIDE_FRACTION = 0.05 // 5% of the reviewer model's context window
32
33
  export const NO_GUIDE_NOTICE = "(No AGENTS.md found — neither at the working directory root nor in any review-scope subdirectory. Judge the user's requirements from the conversation background, and say so explicitly if the requirements are unclear.)"
33
34
 
34
35
  /** Explicit degradation sentences (PORTABILITY design §4.4 — verbatim). */
35
- export const NO_DOC_MAP_NOTICE = "(No document map found under the project root, and none is declared — the Document ownership criterion is degraded: check placement against the Project Guide where present, and state the limitation in your findings.)"
36
- export const NO_STANDARDS_NOTICE = "(No project standards document was declared — judge methodology compliance from the Project Guide (when present) and the review criteria above; state the limitation in your findings.)"
36
+ export const NO_DOC_MAP_NOTICE = "(No document map found — no README.md under the declared document roots and none at their common ancestor — the Document ownership criterion is degraded: check placement against the Project Guide where present, and state the limitation in your findings.)"
37
+ export const NO_STANDARDS_NOTICE = "(No project standards document — nothing in the project declares one — judge methodology compliance from the Project Guide (when present) and the review criteria above; state the limitation in your findings.)"
37
38
  export const NO_GIT_NOTICE = "(No git repository detected — change-set context is unavailable; read the review-scope files directly.)"
38
39
 
39
40
  /**
@@ -126,25 +127,66 @@ export function injectProjectGuide(agent, parts, scopeFiles = []) {
126
127
  return root // guide injected — requirement-fit criteria apply (truthy root)
127
128
  }
128
129
 
130
+ /** `docRoot` probe order: the face a reviewer needs first (design) → down to the batch records. */
131
+ const DOC_ROOT_ORDER = ["design", "requirements", "specs", "modules", "batches"]
132
+
133
+ /** Declared document roots as ABSOLUTE paths, in declaration order (`null` / missing face ⇒ no root). */
134
+ function declaredDocRoots(base) {
135
+ const read = readManifest(base)
136
+ const docRoot = (read.ok && read.manifest ? read.manifest : DEFAULT_MANIFEST).docRoot ?? {}
137
+ const out = []
138
+ for (const key of DOC_ROOT_ORDER) {
139
+ for (const p of docRootPaths(docRoot[key], base)) if (!out.includes(p)) out.push(p)
140
+ }
141
+ return out
142
+ }
143
+
144
+ /** Common ancestor of every root (segment-wise — `docs/design` and `docs/designX` are not siblings).
145
+ * A lone drive root is no map home ⇒ null. */
146
+ function commonAncestor(paths) {
147
+ if (paths.length === 0) return null
148
+ const split = (p) => p.split(/[\\/]+/)
149
+ let acc = split(paths[0])
150
+ for (const p of paths.slice(1)) {
151
+ const segs = split(p)
152
+ let i = 0
153
+ while (i < acc.length && i < segs.length && acc[i] === segs[i]) i++
154
+ acc = acc.slice(0, i)
155
+ }
156
+ if (acc.length === 0) return null
157
+ const joined = acc.join(sep)
158
+ return /^[A-Za-z]:$/.test(joined) ? null : joined
159
+ }
160
+
129
161
  /**
130
- * Inject the project document map (PO-1). Resolution: the DECLARED path
131
- * (`advisor.docMap`, project-root-relative) wins; the built-in probe
132
- * (`docs/README.md` → `docs/design/README.md`) stays as a fallback for projects
133
- * laid out that way. Neither usable → an explicit degradation sentence — never a
134
- * silent skip: the reviewer must know the Document-ownership criterion is unchecked.
162
+ * Discover the document map from the manifest's `docRoot` — a pure fs probe, no index generation
163
+ * (the map is a hand-maintained register; a generated one would be a second index, and a second
164
+ * index is exactly the duplicate pointer the retired declaration face used to create).
165
+ * Ladder: ① `README.md` under each declared doc root (declaration order) ② `README.md` at the common
166
+ * ancestor of every declared root. Neither ⇒ null (caller injects `NO_DOC_MAP_NOTICE` — no silent skip).
167
+ * @param {string} base project root (or any dir inside it — `docRoot` resolves against the project root)
168
+ * @returns {string|null} absolute path of the map file, or null when degraded
169
+ */
170
+ export function findDocumentMap(base) {
171
+ const roots = declaredDocRoots(base)
172
+ for (const r of roots) {
173
+ const p = join(r, "README.md")
174
+ if (existsSync(p)) return p
175
+ }
176
+ const ancestor = commonAncestor(roots)
177
+ if (!ancestor) return null
178
+ const p = join(ancestor, "README.md")
179
+ return existsSync(p) ? p : null
180
+ }
181
+
182
+ /**
183
+ * Inject the project document map (PO-1), derived from `docRoot`. Degrades visibly: the reviewer
184
+ * must know the Document-ownership criterion is unchecked — never a silent skip.
135
185
  * @returns {string|null} the injected map path, or null when degraded
136
186
  */
137
187
  export function injectDocumentMap(agent, parts, root) {
138
188
  const base = root ?? agent.cwd
139
- const conv = loadProjectDeclaration(agent.cwd)
140
- let mapPath = null
141
- if (conv.advisor.docMap) {
142
- const declared = resolve(base, conv.advisor.docMap)
143
- if (existsSync(declared)) mapPath = declared
144
- } else {
145
- mapPath = [resolve(base, "docs", "README.md"), resolve(base, "docs", "design", "README.md")]
146
- .find((p) => existsSync(p)) ?? null
147
- }
189
+ const mapPath = findDocumentMap(base)
148
190
  parts.push("## Document Map")
149
191
  let text = null
150
192
  if (mapPath) {
@@ -166,32 +208,15 @@ export function injectDocumentMap(agent, parts, root) {
166
208
  }
167
209
 
168
210
  /**
169
- * Inject the project standards document (PO-2). DECLARATION ONLY
170
- * (`advisor.standardsDoc`, project-root-relative): the old hardcoded
171
- * `METHODOLOGY.md` probe injected a file most projects do not have — and its
172
- * absence was silent. Undeclared (or unreadable) → explicit degradation sentence.
173
- * @returns {string|null} the injected standards path, or null when degraded
211
+ * Inject the project standards section (PO-2). The declaration face is gone with the manifest
212
+ * `advisor` key, and no standards file name is derivable from what the project does declare (the old
213
+ * `METHODOLOGY.md` probe was deleted exactly because most projects have none). The section stays so the
214
+ * reviewer is TOLD the dimension is unchecked — visible degradation, not a silent skip.
215
+ * @returns {null} always degraded
174
216
  */
175
- export function injectProjectStandards(agent, parts, root) {
176
- const base = root ?? agent.cwd
177
- const conv = loadProjectDeclaration(agent.cwd)
178
- const declared = conv.advisor.standardsDoc ? resolve(base, conv.advisor.standardsDoc) : null
217
+ export function injectProjectStandards(_agent, parts, _root) {
179
218
  parts.push("## Project Standards")
180
- let text = null
181
- if (declared && existsSync(declared)) {
182
- try {
183
- text = readFileSync(declared, "utf8")
184
- } catch (e) {
185
- console.warn(`[advisor] project standards unreadable at ${declared}: ${e.message}`)
186
- }
187
- }
188
- if (text === null) {
189
- parts.push(NO_STANDARDS_NOTICE) // degraded — visible by contract
190
- parts.push("")
191
- return null
192
- }
193
- parts.push("The project declares these standards for this review. Judge the design/changes against them:")
194
- parts.push(text)
219
+ parts.push(NO_STANDARDS_NOTICE) // degraded — visible by contract
195
220
  parts.push("")
196
- return declared
221
+ return null
197
222
  }
@@ -26,7 +26,7 @@ export async function assembleFamilyTools({
26
26
  decorate = null, // object 端差面:{ subagent?, consultStart?, consultStop?, settings? }
27
27
  } = {}) {
28
28
  // CORE-UNIFICATION TOOLS #83:consult 家族随统一登记册自 `../agent-tools.mjs` 取用(单一来源)
29
- const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, manifestTool, consultStartTool, consultStopTool, parentChannelTool, docCheckTool } = await import("../agent-tools.mjs")
29
+ const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool, contextTool, batchTool, manifestTool, manifestReadTool, consultStartTool, consultStopTool, parentChannelTool, docCheckTool } = await import("../agent-tools.mjs")
30
30
  // 台账统一入口(2026-10-05 统一入口批——台账 #923):全量变体 = depth-0 段 ∥ 只读变体 = 全部
31
31
  // depth>0 段。动态 import——ledger 链静态达 node:sqlite(W8 契约②);两变体同名单对象、schema 窄化。
32
32
  const { ledgerTool, ledgerReadTool } = await import("../ledger.mjs")
@@ -125,8 +125,8 @@ export async function assembleFamilyTools({
125
125
  ...subagentTool,
126
126
  name: "subagent",
127
127
  description: designer
128
- ? "Spawn a read-only `explore` sub-agent to SURVEY the current state for the design: it reads code / docs / existing designs and reports evidence with file:line. BLOCKING ONLY (no async) and action:'spawn' ONLY — the survey channel is read-only; escalate/status/cancel/panel/consume-design/observe/send are not available. Survey budget: ≤6 explore spawns per batch — the main agent's survey result is reference only; do your own."
129
- : "Spawn a read-only `explore` sub-agent to AUDIT your delivery against the design: it compares the delivered code with the design for divergence — partially implemented acceptance criteria, silent simplifications, doc drift, changes outside the approved file list. BLOCKING ONLY (no async — the audit report decides your next protocol step). action:'spawn' ONLY — the audit channel is a read-only spawn; escalate/status/cancel/panel/consume-design/observe/send are not available. The audit task book is appended MECHANICALLY — your own spawn task (docs involved / acceptance criteria / file list) plus the files you actually touched; never hand the audit a self-written file list (a self-report could omit exactly the out-of-scope file it must catch).",
128
+ ? "Spawn a read-only `explore` sub-agent to SURVEY the current state for the design: it reads code / docs / existing designs and reports evidence with file:line. BLOCKING ONLY (no async) and action:'spawn' ONLY — the survey channel is read-only; escalate/status/cancel/panel/consume-design/observe/send are not available. Survey budget: ≤6 explore spawns per batch — the main agent's survey result is reference only; do your own. The spawn carries the five task-book parameters (four core: goal / knownFacts / acceptance / reportFormat + `scope`); a task-only spawn is refused."
129
+ : "Spawn a read-only `explore` sub-agent to AUDIT your delivery against the design: it compares the delivered code with the design for divergence — partially implemented acceptance criteria, silent simplifications, doc drift, changes outside the approved file list. BLOCKING ONLY (no async — the audit report decides your next protocol step). action:'spawn' ONLY — the audit channel is a read-only spawn; escalate/status/cancel/panel/consume-design/observe/send are not available. The audit task book is appended MECHANICALLY — your own spawn task (docs involved / acceptance criteria / file list) plus the files you actually touched; never hand the audit a self-written file list (a self-report could omit exactly the out-of-scope file it must catch). The spawn carries the five task-book parameters (four core: goal / knownFacts / acceptance / reportFormat + `scope`); a task-only spawn is refused.",
130
130
  parameters: { ...subagentTool.parameters, properties: props },
131
131
  }
132
132
  })()
@@ -149,7 +149,7 @@ export async function assembleFamilyTools({
149
149
  // 团队台账全量变体(五 action——同律:子代理段只挂读变体;登录态门在工具内)
150
150
  teamLedgerTool,
151
151
  // manifest 工具(`MANIFEST.md` §2.10 · 2026-10-08 · 台账 #1098):主 agent 读写出口——
152
- // **两模式同挂**(需求未设模式门);子代理各段零挂(未装配 = 权限面第一道)。
152
+ // **两模式同挂**(需求未设模式门);全量变体仅 depth-0——子代理各段挂只读变体(见下 `:187-193`)。
153
153
  manifestTool,
154
154
  // 文档纪律机检(`DOC-DISCIPLINE.md` §3.20 条目 S):只读报告态——depth-0 主 agent 段;
155
155
  // 写文档 / 设计档的角色段(eng-coder / eng-designer)另挂(见下两分支)。
@@ -182,13 +182,15 @@ export async function assembleFamilyTools({
182
182
  // consult 段不入(其角色语义 = 父发起的一次性会诊,父在其 settle 前不期望中途对话)。
183
183
  // 台账只读变体(query / count)——全部 depth>0 角色段(含 consult ∕ 兜底;写三 schema 级不可达);
184
184
  // 团队台账只读变体(team-ledger 批)同律。
185
- : engChildRole === "eng-coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, advisorTool, verifyTool, batchTool(batchDoc), docCheckTool,
185
+ // manifest 只读变体(MANIFEST.md §2.10.1 · 2026-10-22 · 台账 #1738):同名 `manifest`、action 枚举只剩 `read`;
186
+ // 与上两条只读先例同律挂在全部 depth>0 角色段(全量变体仅 depth-0——两变体互斥装配,同名不塌缩)。
187
+ : engChildRole === "eng-coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool, advisorTool, verifyTool, batchTool(batchDoc), docCheckTool,
186
188
  ...(engChildSubagent ? [engChildSubagent] : [])]
187
- : engChildRole === "eng-designer" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, batchTool(batchDoc), docCheckTool,
189
+ : engChildRole === "eng-designer" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool, batchTool(batchDoc), docCheckTool,
188
190
  ...(engChildSubagent ? [engChildSubagent] : [])]
189
- : role === "coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, verifyTool, advisorTool, ...(batchDoc ? [batchTool(batchDoc)] : [])]
190
- : role === "consult" ? [recentChangesTool, ledgerReadTool, teamLedgerReadTool]
191
- : [parentChannelTool, ledgerReadTool, teamLedgerReadTool]
191
+ : role === "coder" ? [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool, verifyTool, advisorTool, ...(batchDoc ? [batchTool(batchDoc)] : [])]
192
+ : role === "consult" ? [recentChangesTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool]
193
+ : [parentChannelTool, ledgerReadTool, teamLedgerReadTool, manifestReadTool]
192
194
 
193
195
  // 固定段(装配序 = agent.tools → 固定段 → 家族段 → extraTools):timer 全模式全深度;
194
196
  // plan 随模式位——ENG-PLAN-EXCLUSION(FR31 ① · KD8 卸载而非「注册 + 报错」· KD11 全深度两端);
package/agent/helpers.mjs CHANGED
@@ -275,6 +275,12 @@ export function turnFrame(seq, turn, maxTurns) {
275
275
  return { turn: seq, maxTurns: seq - turn - 1 + maxTurns }
276
276
  }
277
277
 
278
+ /** #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.2 ① 判据式):上轮结账至今写序是否推进;非整数(未跟踪面 / NaN)⇒ true 记「有落笔」不判(不用 `?? 0` 兜底——未跟踪面当零落笔正是本收正要消除的误报方向)。 */
279
+ export function wroteSince(agent, mark) {
280
+ const seq = agent._mutationSeq
281
+ return Number.isInteger(seq) ? seq !== mark : true
282
+ }
283
+
278
284
  /** Repair malformed conversation history: remove orphan tool messages and fill missing tool results */
279
285
  export function repairHistory(history) {
280
286
  const out = []
@@ -451,14 +457,14 @@ export const AUTO_TURN_DIGEST_DOMAIN =
451
457
  /** Engineering-mode auto-turn digest domain variant (mode = `agent.config.agent.engineering`;
452
458
  * clause 2 drops the task-tool pointer — F10: the task tool is disabled in engineering mode,
453
459
  * the batch record + ledger are the tracking authority). Single line, verbatim from the
454
- * design doc (TOOLS.md §6.15.3); mechanism = AGENT-LOOP-ASYNC-POOL.md §6.8. Content authority = parent side. */
460
+ * design doc (TOOLS.md §6.15.3); mechanism = AGENT-LOOP-ASYNC-POOL.md §6.8. Content basis = the mechanism doc; landing = eng-coder. */
455
461
  export const AUTO_TURN_DIGEST_DOMAIN_ENG =
456
462
  "[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) (engineering mode: the task tool is disabled — no task-list update is expected this turn; the batch record + ledger are the tracking authority and are updated in real user turns); 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
457
463
 
458
464
  /** Up-stream wake-turn domain (AGENT-LOOP-UPSTREAM.md §6.27.12.8): a running subagent sent
459
465
  * an in-flight message and is waiting for the reply — the digest domain's "No one is waiting
460
466
  * for this reply" is the opposite of the truth, so the wake turn gets its own text.
461
- * Verbatim from the design doc (single line — no newlines). Content authority = parent side. */
467
+ * Verbatim from the design doc (single line — no newlines). Content basis = the mechanism doc; landing = eng-coder. */
462
468
  export const UPSTREAM_TURN_DOMAIN =
463
469
  "[System reminder: auto-turn — a running subagent sent you an in-flight message (shown below). No user message is waiting. Decide it now and reply with subagent action:'send' (id + message) — the child consumes the reply at its next turn boundary and keeps working on the unaffected parts; if the message needs no answer, say so in one line and move on. If finished subagent reports are also present above, summarize them as usual in the same turn. Do not start new work: FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the reply is sent.]"
464
470
 
@@ -466,16 +472,16 @@ export const UPSTREAM_TURN_DOMAIN =
466
472
  * expired while the session was idle and the end's latch opened this turn automatically —
467
473
  * neither the digest domain ("no one is waiting", organize-only) nor the up-stream domain
468
474
  * (a subagent waiting for a reply) fits: the timer's own semantics is "act on what the
469
- * timer was for". Verbatim from the parent-side final text (single line — no newlines).
470
- * Content authority = parent side. */
475
+ * timer was for". Verbatim from the mechanism doc (single line — no newlines).
476
+ * Content basis = the mechanism doc; landing = eng-coder. */
471
477
  export const TIMER_TURN_DOMAIN =
472
478
  "[System reminder: auto-turn — a timer you set earlier has expired (reminder above). No user message is waiting. Act on what the timer was for now: pull the data or run the check rather than continuing to reason in the abstract. Stay within the work that was in flight when the timer was set; do not open unrelated new work. Wrap up as soon as the action is done or blocked.]"
473
479
 
474
480
  /** Digest-accounting requirement line (AGENT-LOOP-ASYNC-POOL.md §6.31.3 · batch digest-accounting ·
475
481
  * 2026-10-05):消化轮账目要求——两档同发(手动 / AUTO);manual 域文本只覆盖手动档 ⇒ 要求行
476
482
  * 独立常量承载,避免 AUTO 轮无要求。`<ids>` = 未销账 id 清单占位(动态拼 = digest-account.mjs
477
- * `digestAccountRequirement`);注入面 = transient(机器线独有)。Verbatim from the parent-side
478
- * final text (single line — no newlines); content authority = parent side. */
483
+ * `digestAccountRequirement`);注入面 = transient(机器线独有)。Verbatim from the mechanism doc
484
+ * (single line — no newlines). Content basis = the mechanism doc; landing = eng-coder. */
479
485
  export const DIGEST_ACCOUNT_DOMAIN =
480
486
  "[System reminder: digest accounting — for every report listed below as not yet accounted, emit exactly one line: \"[digest-ack #<id>] digested — <key points>\" (handled) or \"[digest-ack #<id>] deferred — <reason>\" (not handled). Not yet accounted: <ids>. Do not omit any.]"
481
487
 
@@ -10,6 +10,31 @@ export const STALL_WINDOW_SIZE = 5
10
10
  export const STALL_THRESHOLD = 3
11
11
  export const GOAL_BUDGET_WARN_RATIO = 0.75
12
12
 
13
+ // 设计轮轮数注入阈值(AGENT-LOOP.md §6.6「设计轮轮数注入」· 台账 #1661):
14
+ // 5 = 在 persona 判定点(过 7 轮未落盘 ⇒ 降级)之前 3 轮给出计数;8 = 与 persona 同一判定点;
15
+ // 15 = persona 的首版预算(注入串里的 15 一律由它插值——改常量即改注入文本,不另留字面)。
16
+ export const DESIGN_TURN_NOTICE_AT = 5
17
+ export const DESIGN_TURN_SKELETON_AT = 8
18
+ export const DESIGN_FIRST_VERSION_TURNS = 15
19
+
20
+ /**
21
+ * 设计轮轮数提醒(纯函数——注入串 | `null`;判据源 = 链内累计轮号 + 本设计轮是否落过盘)。
22
+ * 逐字注入形 = `AGENT-LOOP.md` §6.6「注入形逐字」;角色表当前一行(`eng-designer`),
23
+ * `eng-coder` 零注入(其零落笔面另由零落笔看门狗兜)。
24
+ * @param {{role?:string|null, turn?:number, wrote?:boolean}} o `turn` = 链内累计轮号(1 起)
25
+ * @returns {string|null}
26
+ */
27
+ export function designTurnReminder({ role, turn, wrote } = {}) {
28
+ if (role !== "eng-designer") return null
29
+ const used = Number(turn ?? 0)
30
+ if (!Number.isFinite(used) || used < DESIGN_TURN_NOTICE_AT) return null
31
+ const budget = DESIGN_FIRST_VERSION_TURNS
32
+ if (used >= DESIGN_TURN_SKELETON_AT && !wrote) {
33
+ return `[System reminder: design round turn ${used}/${budget} — nothing has been written to disk yet. The 7-turn mark has passed with nothing landed: degrade the delivery NOW — write the skeleton (design doc + batch record §2) plus an unresolved list, then refine in place.]`
34
+ }
35
+ return `[System reminder: design round turn ${used}/${budget} — ${wrote ? "something has been written to disk" : "nothing has been written to disk yet"}. First version budget is ${budget} turns; past 7 turns with nothing landed the rule is a skeleton + unresolved list.]`
36
+ }
37
+
13
38
  /**
14
39
  * Inject all post-turn events into agent history.
15
40
  * Must be called after every tool-execution turn within runAgent's loop.
@@ -66,5 +91,14 @@ export function injectPostTurn(agent, results, recentCallSigs, callbacks, turn,
66
91
  })
67
92
  }
68
93
 
94
+ // 设计轮轮数注入(AGENT-LOOP.md §6.6 · 台账 #1661):轮号取 `_currentTurn`(链内累计、续跑不重置),
95
+ // 写入痕迹取 `_touchedFiles`(在 resume 保留集内 ⇒ 续跑段沿用前段痕迹);段内 `turn` 形参不参与判据。
96
+ const designReminder = designTurnReminder({
97
+ role: agent._role,
98
+ turn: agent._currentTurn ?? 0,
99
+ wrote: (agent._touchedFiles ?? []).length > 0,
100
+ })
101
+ if (designReminder) agent.history.push({ role: "user", content: designReminder })
102
+
69
103
  callbacks.onTurnEnd?.(agent, turn)
70
104
  }
@@ -8,6 +8,9 @@
8
8
  */
9
9
 
10
10
  import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
11
+ // §6.17/D-CC32 溢出守卫(兜底,非主修):预算面单源 requestBudget ∥ 再估面 contextUsage(与压缩判定同一函数)
12
+ import { requestBudget } from "../config.mjs"
13
+ import { contextUsage } from "../token-window.mjs"
11
14
  import { ensureAutoReminder, injectEngineeringReminder, ContinueError, snapshotGuard } from "./helpers.mjs"
12
15
  import { pushManifestStateReminder } from "./setup-reminders.mjs"
13
16
  import { cleanupConsultSessions } from "../agent-tools/consult.mjs"
@@ -84,6 +87,16 @@ export async function runCompactionCheck(agent, ctx) {
84
87
  // 其余全同(同 splitHistory / 同尾部预算 / 同摘要链 / 同回注 / 同基线失效)。
85
88
  const pending = agent._pendingCompact
86
89
  agent._pendingCompact = null
90
+ // 成功记账(§6.17:成功记账段抽局部——阈值面与守卫面共用同一段,零复制)
91
+ const bookkeepCompaction = () => {
92
+ agent._compressFailures = 0
93
+ agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
94
+ recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
95
+ // Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
96
+ // the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
97
+ callbacks.onCompress?.(agent._lastCompressInfo ?? {})
98
+ ensureAutoReminder(agent)
99
+ }
87
100
  try {
88
101
  let compacted = await compressIfNeeded(
89
102
  agent, threshold, callbacks,
@@ -96,15 +109,7 @@ export async function runCompactionCheck(agent, ctx) {
96
109
  agent.history.push({ role: "user", content: compactNoopNote(), transient: true })
97
110
  compacted = await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)
98
111
  }
99
- if (compacted) {
100
- agent._compressFailures = 0
101
- agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
102
- recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
103
- // Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
104
- // the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
105
- callbacks.onCompress?.(agent._lastCompressInfo ?? {})
106
- ensureAutoReminder(agent)
107
- }
112
+ if (compacted) bookkeepCompaction()
108
113
  } catch (compressError) {
109
114
  // AbortError must not be swallowed: user cancellation must propagate
110
115
  if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
@@ -122,6 +127,35 @@ export async function runCompactionCheck(agent, ctx) {
122
127
  if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
123
128
  }
124
129
  }
130
+
131
+ // 溢出守卫(CONTEXT-COMPACTION.md §6.17 / D-CC32——兜底,非主修;本缝 = catch 闭合后、返回前)。
132
+ // 判据 = maxTokensSource !== "absent" ∧ prompt 估算 > limit(limit = window − reserve,恒为数)。
133
+ // **判据不用 maxTokens**:未发时为 undefined ⇒ NaN ⇒ 守卫静默失效(§6.17);absent 档客户端未预留
134
+ // ⇒ 守卫不覆盖(服务端 400 兜——把规格 maxOutput 当实发判会让 doubao 三行永久发不出去)。
135
+ // 真 ⇒ 以 force 强制压缩一次(先压再判);再估仍超 ⇒ 抛三实数、回合请求零发。不截断、不静默削输出。
136
+ const budget = requestBudget(agent.provider)
137
+ if (budget.maxTokensSource !== "absent") {
138
+ let usage = contextUsage(agent, compactionOverhead)
139
+ if (usage.total > budget.limit) {
140
+ const compacted = await compressIfNeeded(
141
+ agent, threshold, callbacks, { ...compactionOverhead, force: true }, signal,
142
+ )
143
+ if (compacted) bookkeepCompaction()
144
+ usage = contextUsage(agent, compactionOverhead)
145
+ if (usage.total > budget.limit) {
146
+ const remedy = budget.limit < 0
147
+ ? `max_tokens ${budget.maxTokens} alone exceeds the window — lower provider.maxTokens or switch to a larger-window model`
148
+ : "reduce the conversation (/compact or prune) before retrying"
149
+ const overflow = new Error(
150
+ `context overflow: prompt ~${usage.total} tokens + max_tokens ${budget.maxTokens} > window ${budget.window}` +
151
+ ` (limit ${budget.limit}) — forced compaction ran but the prompt still exceeds the window; ${remedy}`,
152
+ )
153
+ overflow.kind = "context_overflow"
154
+ overflow.retryable = false
155
+ throw overflow
156
+ }
157
+ }
158
+ }
125
159
  }
126
160
 
127
161
  /**
@@ -105,6 +105,7 @@ export async function beginRun(agent, input, callbacks, {
105
105
  // #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值常量 / 零自动动作):段起点复位(首段 /
106
106
  // 续跑新段——与 `_continueSegments` 同点);段内累加,采集点在回合环(与 `_turnSeq` 同源面)。
107
107
  agent._zeroWriteTurns = 0
108
+ agent._segmentTurns = 0 // #934 B 面读数分母(§6.32.4):同点复位——同段同源(分子 ≤ 分母)
108
109
  if (!resume) {
109
110
  // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):链内累计编号
110
111
  // 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
package/agent/setup.mjs CHANGED
@@ -22,8 +22,8 @@ import { assembleFamilyTools } from "./family-tools.mjs"
22
22
  import { normalizeOrigin } from "../memory/origin.mjs"
23
23
  import { searchOrigins } from "../memory/code-search.mjs"
24
24
  import { INDEX_ORIGIN_ROW_WARN } from "../memory/schema.mjs"
25
+ import { resolveCompactThreshold } from "../config.mjs"
25
26
 
26
- const DEFAULT_COMPACT_THRESHOLD = 100_000
27
27
  const DOC_SEARCH_LIMIT = 5
28
28
  const DOC_CHUNK_PREVIEW_LEN = 300
29
29
  /** UTF-16 安全截断(2026-09-02 deepseek 400 根因):slice(0, N) 按码元切会把 emoji 代理对切成孤立
@@ -51,7 +51,9 @@ export async function prepareRun(agent, input, callbacks, {
51
51
  injections = null, toolDecorate = undefined,
52
52
  } = {}) {
53
53
  const maxTurns = overrideTurns ?? agent.config?.agent?.maxTurns ?? DEFAULT_MAX_TURNS
54
- const threshold = agent.config?.agent?.compactThreshold ?? DEFAULT_COMPACT_THRESHOLD
54
+ // §6.17 每 run 现算:数值在场 = 显式原样;不在场 = auto 现算(按在用 provider——切模型后下一 run 跟手)。
55
+ // 派生值不落 agent.compactThreshold(显式键永不被改写)。
56
+ const threshold = resolveCompactThreshold(agent.config?.agent?.compactThreshold, agent.provider).value
55
57
  // 注入开关(PROMPT-SYSTEM.md §6.24 · 台账 #1420):判据 `!== false`(缺省 = 现行为零变)——
56
58
  // 逐 run 现读 `agent.config` ⇒ `settings` 工具 ∕ `/config` 写入后本会话下一次组装即生效。
57
59
  const injectDocs = agent.config?.agent?.injectDocs !== false
@@ -62,6 +64,12 @@ export async function prepareRun(agent, input, callbacks, {
62
64
 
63
65
  agent._lastPromptTokens = null
64
66
  agent._usageAtLen = null
67
+ // Cumulative provider cache accounting (batch 2026-10-18-context-cache-visibility):
68
+ // the service reports prompt_cache_hit/miss per response; the terminal faces each kept
69
+ // their own tally, so the CORE had none and `context` could not report cache efficiency.
70
+ agent._cacheHitTokens = 0
71
+ agent._cacheMissTokens = 0
72
+ agent._usageTurns = 0
65
73
  agent.history = repairHistory(agent.history)
66
74
 
67
75
  if (!resume) {
@@ -18,6 +18,7 @@ import {
18
18
  turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 TURN-CAP-CONTINUE.md §4)
19
19
  ContinueError,
20
20
  FILE_MUTATORS, toolTouchPaths,
21
+ wroteSince, // #934(§6.32.2 ① 判据式):写序是否推进——轮顶 ∥ 撞帽收尾两处判据点共用
21
22
  } from "./helpers.mjs"
22
23
 
23
24
  export async function runTurnLoop(agent, {
@@ -57,9 +58,10 @@ export async function runTurnLoop(agent, {
57
58
  // §6.31.5 落痕写点门(#939):载体吸收经 `carrierField` 单点(§6.31.4 同式);动态 import = 零新增静态边(沿上方两条同款)。
58
59
  const { carrierField } = await import("../agent-tools/async-settle.mjs")
59
60
 
60
- // #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作):计数基线——回合环逐轮
61
- // 比对 `_touchedFiles` 增量(轮顶对上一轮结账;撞帽收尾轮在环外单结)。
62
- agent._turnFilesMark = agent._touchedFiles.length
61
+ // #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作)∥ #934(零落笔看门狗):计数基线——
62
+ // 判据物 = 写序(每次写事件 +1,不去重);`_touchedFiles` 是去重路径集,同档重写长度不变 ⇒ 旧判据在
63
+ // 此读成零落笔(本收正要消除的误报方向)。环内逐轮结账、撞帽收尾轮在环外单结(同一谓词)。
64
+ agent._mutSeqMark = agent._mutationSeq
63
65
 
64
66
  for (let turn = 0; turn < maxTurns; turn++) {
65
67
  // #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环头检查点——中止恒以
@@ -73,21 +75,23 @@ export async function runTurnLoop(agent, {
73
75
  const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
74
76
  agent._currentTurn = frame.turn
75
77
  agent._maxTurns = frame.maxTurns
76
- // #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今 `_touchedFiles`
77
- // 零增 ⇒ 上一轮零落盘,计数 +1;mark 前移(收尾轮在环外结账——见下方 ContinueError 前)。
78
- // #934(零落笔看门狗——§6.32.2 ① 单点采集):streak 与越阈判定同点——上轮零写 ⇒ +1 并判越阈;
79
- // 有写 ⇒ streak 归零 + 清闩(写后重臂——每连续零写段恰一条)。#417 计数语义逐字保持。
78
+ // #934 B 面读数分母(§6.32.4):本段轮数逐轮 +1——段起点复位见 run-start.mjs(与零落笔计数同点)。
79
+ agent._segmentTurns = (agent._segmentTurns ?? 0) + 1
80
+ // #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今写序未推进
81
+ // ⇒ 上一轮零落笔,计数 +1;写序标每轮前移(撞帽收尾轮在环外结账——见下方 ContinueError 前)。
82
+ // #934(零落笔看门狗——§6.32.2 ① 单点采集):streak 与越阈判定同点——上轮零落笔 ⇒ +1 并判越阈;
83
+ // 有落笔 ⇒ streak 归零 + 清闩(写后重臂——每连续零落笔段恰一条)。#417 计数与 streak 同源同谓词。
80
84
  if (turn > 0) {
81
- if (agent._touchedFiles.length === agent._turnFilesMark) {
85
+ if (!wroteSince(agent, agent._mutSeqMark)) {
82
86
  agent._zeroWriteTurns += 1
83
87
  agent._zeroWriteStreak += 1
84
- maybeZeroWriteAlert(agent) // #934:连续零写越阈 ⇒ 自动上行提醒(只推——§6.32)
88
+ maybeZeroWriteAlert(agent) // #934:连续零落笔越阈 ⇒ 自动上行提醒(只推——§6.32)
85
89
  } else {
86
90
  agent._zeroWriteStreak = 0
87
91
  agent._zeroWriteAlerted = false // 写后重臂
88
92
  }
89
93
  }
90
- agent._turnFilesMark = agent._touchedFiles.length
94
+ agent._mutSeqMark = agent._mutationSeq
91
95
  // D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
92
96
  // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
93
97
  // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
@@ -180,6 +184,12 @@ export async function runTurnLoop(agent, {
180
184
 
181
185
  if (response.usage) {
182
186
  callbacks.onUsage?.(response.usage)
187
+ // Core-side cumulative cache tally (batch 2026-10-18-context-cache-visibility):
188
+ // every agent (parent or child) keeps its own hit/miss so `context` can report
189
+ // cache efficiency instead of the terminal faces being the only tally holders.
190
+ agent._cacheHitTokens = (agent._cacheHitTokens ?? 0) + (response.usage.prompt_cache_hit_tokens ?? 0)
191
+ agent._cacheMissTokens = (agent._cacheMissTokens ?? 0) + (response.usage.prompt_cache_miss_tokens ?? 0)
192
+ agent._usageTurns = (agent._usageTurns ?? 0) + 1
183
193
  if (response.usage.prompt_tokens != null) {
184
194
  agent._lastPromptTokens = response.usage.prompt_tokens
185
195
  agent._usageAtLen = agent.history.length
@@ -265,9 +275,9 @@ export async function runTurnLoop(agent, {
265
275
  injectPostTurn(agent, results, recentCallSigs, callbacks, turn, { suspDriven })
266
276
  }
267
277
 
268
- // #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)若零落盘同样计入——轮顶计数只
269
- // 覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
270
- if (maxTurns > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
278
+ // #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)单结——与环内同一谓词(不另作 alert ∥ streak
279
+ // 判定,链尾无下一轮可推报);轮顶计数只覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
280
+ if (maxTurns > 0 && !wroteSince(agent, agent._mutSeqMark)) agent._zeroWriteTurns += 1
271
281
  // #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环尾检查点——中止恒以
272
282
  // AbortError 收束(不落 ContinueError——用户 Stop 优先于继续提示;消费契约 = 外层按 reason 判定)。
273
283
  if (signal?.aborted) throw abortError(signal, "agent", "turn-tail")
@@ -23,9 +23,11 @@
23
23
  */
24
24
  export function summarizeEngTaskBook(taskInput) {
25
25
  if (!taskInput) return "(unavailable)"
26
+ // 五项拼装形下,`designPoints` 一段同时承载「设计档落点」与「受影响文件清单」——
27
+ // 前两段同取该块是有意的(语义都在其中);代价 = 摘要里该块出现两次,不是抽取失败。
26
28
  const SECTIONS = [
27
- { name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/] },
28
- { name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/] },
29
+ { name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/, /^Design points/i] },
30
+ { name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/, /^Design points/i] },
29
31
  { name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
30
32
  ]
31
33
  const lines = taskInput.split("\n")