thincoder 0.12.59 → 0.12.60

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 (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -7,6 +7,7 @@
7
7
  import { hasCodeMutations } from "../advisor/repos.mjs"
8
8
  import { pushReal } from "../context.mjs"
9
9
  import { MAX_ADVISOR_ROUNDS } from "../advisor/run.mjs"
10
+ import { advisorReviewPending, effectiveAdvisorRound } from "../agent-tools/advisor-async.mjs"
10
11
 
11
12
  const MAX_VERIFY_PUSHBACKS = 2
12
13
  const MAX_VERIFY_RETRIES = 3
@@ -77,18 +78,20 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
77
78
  pushReal(agent, { role: "assistant", content: response.content })
78
79
  agent.history.push({
79
80
  role: "user",
80
- content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: call the verify tool to run syntax checks and tests. If verify reports failures, fix them and run verify again. If verification is genuinely impossible here, say so explicitly in your reply.]",
81
+ content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: run the project's verification yourself (per its AGENTS.md test method), then call verify declaring the outcome via verification.status. verify mechanically gates on your declaration. If verification is genuinely impossible here, say so explicitly in your reply.]",
81
82
  })
82
83
  callbacks.onTurnEnd?.(agent, turn)
83
84
  return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
84
85
  }
85
- // Verified but still failing → pushback to fix (up to MAX_VERIFY_RETRIES)
86
+ // Verified but not passed → pushback to fix/complete (up to MAX_VERIFY_RETRIES).
87
+ // _verifyPassed === false means the declaration was failed, skipped without a
88
+ // reason, or not declared — not necessarily that a test failed.
86
89
  if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
87
90
  agent._verifyRetries++
88
91
  pushReal(agent, { role: "assistant", content: response.content })
89
92
  agent.history.push({
90
93
  role: "user",
91
- content: `[System reminder: verify reported test failures (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}). Review the failures, fix the issues, then run verify again. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
94
+ content: `[System reminder: (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}) verify was not passed — either your verification declared failed, was skipped without a reason, or was not declared. Fix or complete your verification, then call verify again declaring the outcome. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
92
95
  })
93
96
  callbacks.onTurnEnd?.(agent, turn)
94
97
  return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
@@ -103,7 +106,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
103
106
  pushReal(agent, { role: "assistant", content: response.content })
104
107
  agent.history.push({
105
108
  role: "user",
106
- content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete the user needs to know the work is unfinished.]`,
109
+ content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted. You have not passed verification. Either state explicitly that your verification could not be completed, or run verify again once it is. If your verification could not be completed, say so explicitly in your reply to the user — state what you tried and what you believe is blocking you, and do not present the work as complete; the user needs to know it is unfinished.]`,
107
110
  })
108
111
  callbacks.onTurnEnd?.(agent, turn)
109
112
  return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
@@ -117,18 +120,21 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
117
120
  const cfg = agent.config?.advisor
118
121
  const advisorReview = cfg?.guard === true
119
122
  if (depth === 0 && advisorReview && !agent.config?.agent?.engineering) {
120
- // Cap sync: beyond MAX_ADVISOR_ROUNDS the advisor tool refuses to review
121
- // (run.mjs convergence cap) pushing back further would loop forever
122
- // (fix pushback cap-refused call fix ). The cap message from the
123
- // last accepted review stands; the user decides manually.
124
- if (agent._mutatedThisRun && !agent._calledAdvisorThisRun && hasCodeMutations(agent)
123
+ // §24 D-24b (T-24b4 — guard timing): an async review that is still in flight
124
+ // (or queued in the advisor pool) means the review was launched the guard
125
+ // does NOT push back while it is pending (未决不算未评审); once it settles
126
+ // non-stale it marks _calledAdvisorThisRun, and a STALE settle leaves the
127
+ // mark unset so the guard pushes back here again (fix #2 no silent skip).
128
+ const pending = advisorReviewPending(agent)
129
+ const rounds = effectiveAdvisorRound(agent)
130
+ if (!pending && agent._mutatedThisRun && !agent._calledAdvisorThisRun && hasCodeMutations(agent)
125
131
  && advisorPushbacks < MAX_ADVISOR_PUSHBACKS
126
- && (agent._advisorRound || 0) < MAX_ADVISOR_ROUNDS) {
132
+ && rounds < MAX_ADVISOR_ROUNDS) {
127
133
  advisorPushbacks++
128
134
  pushReal(agent, { role: "assistant", content: response.content })
129
135
  agent.history.push({
130
136
  role: "user",
131
- content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${agent._advisorRound + 1}). Call the \`advisor\` tool now. This is required, not optional — do not skip it even if you believe the changes are trivial — the review will be quick either way. After the review, produce a response table for every issue found (see discipline rules for format).]`,
137
+ content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${rounds + 1}). Call the \`advisor\` tool now. This is required, not optional — do not skip it even if you believe the changes are trivial — the review will be quick either way. After the review, produce a response table for every issue found (see discipline rules for format).]`,
132
138
  })
133
139
  callbacks.onTurnEnd?.(agent, turn)
134
140
  return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
@@ -6,9 +6,18 @@ import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
6
6
  import { runHooks } from "../hooks.mjs"
7
7
  import { snapshotForUndo } from "../tui/cmd-undo.mjs"
8
8
  import { isDocFile } from "../advisor/repos.mjs"
9
+ // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):写工具钩子——peerCollabNote(执行前冲突
10
+ // 检测——软提示不阻止)+ recordPeerWrites(成功后累积本回合写足迹——回合末 flush)。
11
+ import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites } from "../peer-domains.mjs"
9
12
  import { writeFileSync, mkdirSync, existsSync } from "node:fs"
10
- import { join } from "node:path"
13
+ import { join, resolve } from "node:path"
11
14
  import { homedir } from "node:os"
15
+ // §29 fix A(AGENT-LOOP.md §29——2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
16
+ // 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
17
+ // + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
18
+ // 完成的写在 launchSeq 之前落地 → settle 不再误判 stale(§29 症状根因)。
19
+ import { noteMutations } from "../agent-tools/advisor-async.mjs"
20
+ import { anyLiveDesignSlot } from "../token-ttl.mjs"
12
21
 
13
22
  const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
14
23
 
@@ -40,9 +49,9 @@ function logToolError(toolName, args, error) {
40
49
 
41
50
  /**
42
51
  * §19 action-level classification (AGENT-LOOP.md §19 D-M1): the merged subagent
43
- * tool expresses spawn (side effect) and check/status (read-only queries) through
52
+ * tool expresses spawn (side effect) and status (read-only query) through
44
53
  * its `action` parameter — the tool-level readonly flag can no longer express both.
45
- * dispatch Phase-1/Phase-2 classifies per action: check/status behave as readonly
54
+ * dispatch Phase-1/Phase-2 classifies per action: status behaves as readonly
46
55
  * (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
47
56
  * gates, escalate runs non-readonly AND serially (the retired escalate tool had no
48
57
  * parallel flag — zero behavior change under the merged surface).
@@ -50,13 +59,13 @@ function logToolError(toolName, args, error) {
50
59
  * stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
51
60
  * readonly (planMode pass / no permission ask — never joins a batch approval
52
61
  * group / no handler → not denied — digest 内 cancel 放行).
53
- * §19.6 panel (round1 #5): view 面归只读类(同 check/status——planMode 放行、免
62
+ * §19.6 panel (round1 #5): view 面归只读类(同 status——planMode 放行、免
54
63
  * 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
55
64
  * 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
56
65
  */
57
66
  function isSubagentReadonlyAction(toolName, args) {
58
67
  // §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
59
- // subagent check/status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
68
+ // subagent status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
60
69
  // readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
61
70
  // 动作级判定——不能按工具名(同一 memory 工具的 put/delete/clear 保持侧效门)。
62
71
  if (toolName === "memory") {
@@ -71,7 +80,10 @@ function isSubagentReadonlyAction(toolName, args) {
71
80
  }
72
81
  if (toolName !== "subagent" || !args || typeof args !== "object") return false
73
82
  const action = args.action
74
- if (action === "check" || action === "status") return true
83
+ // §19.8: check 动作已删除——只读面仅剩 status(planMode 放行/免权限审批/可批并行)
84
+ if (action === "status") return true
85
+ // SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行)
86
+ if (action === "observe") return true
75
87
  // §19.6 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
76
88
  if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
77
89
  return false
@@ -79,14 +91,44 @@ function isSubagentReadonlyAction(toolName, args) {
79
91
  function isSubagentControlAction(toolName, args) {
80
92
  if (toolName !== "subagent") return false
81
93
  if (args?.action === "cancel") return true
94
+ // SUBAGENT-OBSERVE-SEND:send = 控制类豁免(同 cancel——父回合内显式调用即授权——
95
+ // 写子输入队列属父对子轻量引导,非产品代码写——免审批、planMode 放行、digest 内放行)
96
+ if (args?.action === "send") return true
82
97
  // §19.6 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
83
98
  if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
84
99
  return false
85
100
  }
101
+ /**
102
+ * §2.6 token 链终消费制(2026-09-07——评审 #7d dispatch 分类):consume-design =
103
+ * 非只读控制动作——planMode 拒绝(不入 readonly/control 豁免——与其他非只读动作同门)、
104
+ * 免权限审批、不入批审批分组(无文件写——控制类直行——只停既有状态不起新副作用)。
105
+ * 与 cancel 的不同:cancel 是控制类豁免(planMode 放行),consume-design 按设计
106
+ * planMode 拒绝——故不并入 isSubagentControlAction,单独谓词只接权限豁免位。
107
+ */
108
+ function isSubagentConsumeDesignAction(toolName, args) {
109
+ return toolName === "subagent" && args?.action === "consume-design"
110
+ }
86
111
  function isSubagentEscalateAction(toolName, args) {
87
112
  return toolName === "subagent" && args?.action === "escalate"
88
113
  }
89
114
 
115
+ /**
116
+ * §29 fix A — 唯一记账点:FILE_MUTATORS 工具执行成功即刻记 mutation seq(abs 路径)。
117
+ * 取代 record-results 批后段 + agent.mjs 中断分支的 noteMutations(不双计——中断+同批
118
+ * launch 场景 seq 单计,AGENT-LOOP.md §29 T-A1i)。调用时机 = 写执行成功(非 Error 前缀
119
+ * 结果——recordPeerWrites 同款门);routed(M2 ACP 客户端执行)成功同样记账。
120
+ */
121
+ function noteExecutedMutation(agent, tool, args) {
122
+ let paths
123
+ try {
124
+ paths = tool.touchedPaths ? tool.touchedPaths(args ?? {}) : [args?.path]
125
+ } catch { return }
126
+ const abs = (paths ?? [])
127
+ .filter((p) => typeof p === "string" && p)
128
+ .map((p) => resolve(agent.cwd, p))
129
+ if (abs.length > 0) noteMutations(agent, abs)
130
+ }
131
+
90
132
  /**
91
133
  * Two-phase execution:
92
134
  * Phase 1 (serial): parse args one by one + planMode check + permission confirmation (side-effecting tools)
@@ -138,14 +180,18 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
138
180
  }
139
181
 
140
182
  // Engineering mode PARENT gate: the parent agent must not touch code files
141
- // before the design review passed. Signaled by _engDesignToken set on
142
- // design-review approval, survives across turns (_engDesignReviewed is
143
- // eng-coder-only and reset per run). Exemptions cover ONLY design artifacts
183
+ // before the design review passed. Signaled by a live design slot (design-review
184
+ // approval — persists in the session slot, survives across turns; _engDesignReviewed
185
+ // is eng-coder-only and reset per run). Exemptions cover ONLY design artifacts
144
186
  // (docs/** and root-level docs like METHODOLOGY.md/README.md/AGENTS.md/
145
187
  // LICENSE) — writing them IS the design/methodology step. Everything under
146
188
  // src/ (incl. src/prompts/*.md) is product code, not documentation, and
147
189
  // needs a design token. Mechanically blocks "talk then code".
148
- if (agent.config?.agent?.engineering && depth === 0 && !agent._engDesignToken
190
+ // DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):资格判据 = 权威槽"任一活槽存在"
191
+ // (anyLiveDesignSlot——查内存 Map,miss 回读槽文件——单值镜像 `_engDesignToken`
192
+ // 已退役,门禁不再读镜像——AC4)。
193
+ if (agent.config?.agent?.engineering && depth === 0
194
+ && !anyLiveDesignSlot(agent)
149
195
  && FILE_MUTATORS.has(toolCall.name)) {
150
196
  const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
151
197
  // Unknown/missing paths (non-string, e.g. no path argument) are treated
@@ -171,7 +217,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
171
217
  // — the exemption never widens what reaches this stage (round4 #3, T-E14).
172
218
  // PreToolUse hooks still run below. Non-eng-coder children keep the manual
173
219
  // parent ask (human in the loop).
174
- if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
220
+ if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || isSubagentConsumeDesignAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
175
221
  if (!(await runHooks("PreToolUse", { agent, toolName: toolCall.name, toolArgs: args }))) {
176
222
  prepared.push({ toolCall, tool, denied: true, reason: "blocked by PreToolUse hook" })
177
223
  continue
@@ -267,6 +313,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
267
313
  const toolName = item.toolCall.name
268
314
  logEvent("tool:call", { tool: toolName, child: agent?._logId })
269
315
  try {
316
+ // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):结构化写工具执行前查 conflicts
317
+ // (命中他实例 hot 域 → 结果附软提示——决策⑥ A 不阻止;一次目录 stat——N3 度量);
318
+ // 足迹累积(D-L3a——"检测+记录一次完成")延后到执行成功(实际写过的文件)。
319
+ const isPeerWriteTool = PEER_WRITE_TOOLS.has(toolName)
320
+ const peerNote = isPeerWriteTool ? peerCollabNote(agent.cwd, item.tool, item.args) : null
270
321
  // Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
271
322
  if (!item.tool?.readonly && item.args) {
272
323
  snapshotForUndo(agent, item.toolCall.name, item.args, agent.cwd)
@@ -276,9 +327,14 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
276
327
  if (callbacks.toolRouter) {
277
328
  const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
278
329
  if (routed?.handled) {
279
- callbacks.onToolResult?.(item.toolCall.name, routed.result, item.toolCall.id)
280
- logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routed.result, 200), child: agent?._logId })
281
- return { ...item, result: routed.result, ok: true }
330
+ const routedOk = !String(routed.result).startsWith("Error:")
331
+ const routedResult = peerNote && routedOk ? `${routed.result}\n${peerNote}` : routed.result
332
+ if (isPeerWriteTool && routedOk) recordPeerWrites(agent, item.tool, item.args)
333
+ // §29 fix A:routed 写成功(客户端执行)同样执行期即刻记账(唯一记账点)
334
+ if (routedOk && FILE_MUTATORS.has(toolName)) noteExecutedMutation(agent, item.tool, item.args)
335
+ callbacks.onToolResult?.(item.toolCall.name, routedResult, item.toolCall.id)
336
+ logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routedResult, 200), child: agent?._logId })
337
+ return { ...item, result: routedResult, ok: true }
282
338
  }
283
339
  }
284
340
  const origConsoleLog = console.log
@@ -296,18 +352,41 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
296
352
  depth,
297
353
  signal,
298
354
  callbacks,
355
+ // §24 D-24b: per-call id — the advisor tool marker-keys its launch so
356
+ // recordToolResults can split async-ack accounting from sync settles.
357
+ _toolCallId: item.toolCall.id,
299
358
  onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
300
359
  onQuestion: callbacks.onQuestion,
301
360
  onPermissionRequest: callbacks.onPermissionRequest,
302
361
  }
303
362
  try {
304
- rawResult = await item.tool.execute(item.args, toolCtx)
363
+ // SUBAGENT-OBSERVE-SEND D1(评审 #1):in-flight 当前工具记账——工具执行期间在
364
+ // agent 上留 _inflightTools Set(子代理 observe 从 dispatch 状态读——卡在长工具
365
+ // 调用时 history 无新回合、恰需此信号);finally 清除。批并行工具同入 Set(observe
366
+ // 如实返回多个在跑工具)。主会话同样记账——无害(无人读)。
367
+ const inflight = agent._inflightTools ?? (agent._inflightTools = new Set())
368
+ inflight.add(toolName)
369
+ try {
370
+ rawResult = await item.tool.execute(item.args, toolCtx)
371
+ } finally {
372
+ inflight.delete(toolName)
373
+ }
305
374
  } finally {
306
375
  console.log = origConsoleLog
307
376
  console.error = origConsoleErr
308
377
  }
309
378
  if (rawResult === undefined) throw new Error(`Tool "${item.toolCall.name}" returned undefined — all tools must return a string value`)
310
379
  const raw = String(rawResult)
380
+ // 写成功(非 "Error:" 字符串结果)→ 足迹计入本回合集合(flush 在 finalizeAgentTurn)
381
+ if (isPeerWriteTool && !raw.startsWith("Error:")) {
382
+ recordPeerWrites(agent, item.tool, item.args)
383
+ }
384
+ // §29 fix A:FILE_MUTATORS 执行成功即刻记账(唯一记账点——取代 record-results 批后
385
+ // 段 + agent.mjs 中断分支——不双计)——同批 launch 前的写在 launchSeq 之前落地 →
386
+ // async advisor settle 不误判 stale(同批 launch 后写仍保守 stale——T-A2/T-24b9)。
387
+ if (FILE_MUTATORS.has(toolName) && !raw.startsWith("Error:")) {
388
+ noteExecutedMutation(agent, item.tool, item.args)
389
+ }
311
390
  // Multimodal tools keep the raw result (base64 images ride the multimodal
312
391
  // channel); everything else offloads oversized text to disk. Flag-driven, not
313
392
  // name-driven (consult P3, 2026-08-30).
@@ -316,11 +395,15 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
316
395
  const resultWithConsole = capturedConsole.length > 0
317
396
  ? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
318
397
  : result
319
- callbacks.onToolResult?.(item.toolCall.name, resultWithConsole, item.toolCall.id, toolCtx._subagentKey)
398
+ // R10 L3:冲突软提示附在工具结果末尾(模型可见——不阻止写)
399
+ const resultForModel = peerNote && !raw.startsWith("Error:")
400
+ ? `${resultWithConsole}\n${peerNote}`
401
+ : resultWithConsole
402
+ callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
320
403
  // PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
321
404
  runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
322
- logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultWithConsole, 200), child: agent?._logId })
323
- return { ...item, result: resultWithConsole, ok: true }
405
+ logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultForModel, 200), child: agent?._logId })
406
+ return { ...item, result: resultForModel, ok: true }
324
407
  } catch (error) {
325
408
  // Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
326
409
  logToolError(item.toolCall.name, item.args, error)
@@ -357,7 +440,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
357
440
  for (const item of prepared) {
358
441
  // escalate action keeps the retired escalate tool's serial placement (no
359
442
  // parallel flag): it flushes the batch and runs alone in call order (§19 —
360
- // spawn stays parallel; check/status classify as readonly and batch freely).
443
+ // spawn stays parallel; status classifies as readonly and batch freely).
361
444
  if (item.tool && !item.tool.readonly
362
445
  && (!item.tool.parallel || isSubagentEscalateAction(item.tool.name, item.args))) {
363
446
  await flush()
@@ -288,3 +288,39 @@ export async function loadProjectInstructions(cwd) {
288
288
  merged
289
289
  )
290
290
  }
291
+
292
+ // Engineering mode reminders + auto-turn digest domain + mode injector
293
+ // (2026-09-05 module-split:自 agent.mjs 迁入——agent.mjs 530 > 500 硬限——agent.mjs
294
+ // re-export 保 import 面:eng.mjs / cmd-eng.mjs / 测试从 agent.mjs import)
295
+
296
+ /** Engineering mode reminder — shared with eng.mjs tool. */
297
+ export const ENG_ON_REMINDER =
298
+ "[System reminder: engineering mode is ON — design-before-code enforced. " +
299
+ "Workflow: Requirements doc → Design doc → advisor(type='design') → " +
300
+ "user approval → eng-coder implementation. Code changes go through eng-coder " +
301
+ "subagents only. Advisor calls are NOT per-turn-mandatory — call only at " +
302
+ "flow nodes or when the user asks.]"
303
+
304
+ /** Engineering mode OFF reminder — shared with the eng tool and the injector. */
305
+ export const ENG_OFF_REMINDER =
306
+ "[System reminder: engineering mode is now OFF — standard discipline applies. " +
307
+ "Changes go through the normal workflow: you may edit files directly, advisor/verify " +
308
+ "guards apply per config.]"
309
+
310
+ /** Manual-tier auto-turn digest domain (AGENT-LOOP.md §17 D-S6): organize-only.
311
+ * Injected per manual auto-turn run — writes/execute/spawns/questions are also
312
+ * mechanically denied (no permission handler + spawn gate); this steers first. */
313
+ export const AUTO_TURN_DIGEST_DOMAIN =
314
+ "[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) update the task list with the task tool (allowed) to mark finished work done; 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.]"
315
+
316
+ /** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
317
+ * OFF is announced too — the model must know the gates lifted; silence after /eng-off
318
+ * left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */
319
+ export function injectEngineeringReminder(agent) {
320
+ const eng = agent.config?.agent?.engineering ?? false
321
+ if (eng !== agent._lastEngState) {
322
+ agent.history.push({ role: "user", content: eng ? ENG_ON_REMINDER : ENG_OFF_REMINDER, transient: true })
323
+ }
324
+ agent._lastEngState = eng
325
+ }
326
+
@@ -22,7 +22,9 @@
22
22
  import { pushReal } from "../context.mjs"
23
23
  import { specForModel } from "../config.mjs"
24
24
  import { FILE_MUTATORS } from "./helpers.mjs"
25
- import { join } from "node:path"
25
+ import { resolve } from "node:path"
26
+ import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
27
+ import { looksLikeReviewOutput } from "../advisor/run.mjs"
26
28
 
27
29
  let _reindexFile = null
28
30
 
@@ -72,6 +74,9 @@ export async function recordToolResults(agent, toolByName, results) {
72
74
  // Direct file edit — code was changed. The prior advisor review and
73
75
  // verify are stale: a review that ran before the edit no longer
74
76
  // covers the current file state.
77
+ // §29 fix A(AGENT-LOOP.md §29——2026-09-07):mutation-seq 记账已移到 dispatch
78
+ // runOne 执行成功即刻(唯一记账点——取代本批后段 + agent.mjs 中断分支——不双计)——
79
+ // 此处仅剩 guard 标志失效(顺序语义:批内同消息的 sync advisor 提交仍在其后置位)。
75
80
  agent._mutatedThisRun = true
76
81
  agent._calledAdvisorThisRun = false
77
82
  agent._verifiedThisRun = false
@@ -91,20 +96,51 @@ export async function recordToolResults(agent, toolByName, results) {
91
96
  }
92
97
  if (toolCall.name === "verify") agent._verifiedThisRun = true
93
98
  if (toolCall.name === "advisor") {
94
- agent._calledAdvisorThisRun = true
95
- // All advisor calls (code and design) share the 5-round convergence
96
- // budget each advances _advisorRound toward MAX_ADVISOR_ROUNDS.
97
- // Always advance the round the convergence protocol cares about
98
- // how many reviews have run (round 1→2→3→4→5), not how many succeeded.
99
- // A failed/interrupted review is still a review attempt and should use
100
- // the next round's prompt on retry.
101
- agent._advisorRound++
99
+ // §24 D-24b (settle accounting split — fix #2): an ASYNC launch returns an
100
+ // ack and settles later the settle callback owns its called/round/token
101
+ // accounting. Only the SYNC path (depth>0 / explicit async:false) accounts
102
+ // here per-review instance round++ (marker-keyed by tool call id) + the
103
+ // legacy mirror._advisorRound_ counter stays for display/back-compat.
104
+ // REFUSED launches (pool full / per-review cap) count as neither a call
105
+ // nor a completion: no called-mark (the guard must keep pushing until a
106
+ // review really runs), no round advance.
107
+ const refused = agent._advisorRefusals?.has(toolCall.id)
108
+ const asyncAck = agent._advisorAsyncAcks?.has(toolCall.id)
109
+ if (refused) {
110
+ agent._advisorRefusals.delete(toolCall.id)
111
+ } else if (asyncAck) {
112
+ agent._advisorAsyncAcks.delete(toolCall.id)
113
+ } else {
114
+ agent._calledAdvisorThisRun = true
115
+ const reviewId = agent._advisorSyncCalls?.get(toolCall.id)
116
+ if (reviewId !== undefined) {
117
+ const run = advisorRuns(agent).get(reviewId)
118
+ if (run) {
119
+ run.round++
120
+ agent._advisorRound = run.round
121
+ // Prior of round 2+ = the last REVIEW-LOOKING output (run.mjs parity).
122
+ // F2e (§29.1): the sync settle's engine-approved suffix (stored on the
123
+ // run by settleDesignReview) is stripped with exact truncation — the
124
+ // prior never carries the raw token / designId.
125
+ if (looksLikeReviewOutput(result)) {
126
+ run.priorOutput = stripApprovedSuffix(result, run.approvedSuffix)
127
+ }
128
+ } else {
129
+ agent._advisorRound++
130
+ }
131
+ agent._advisorSyncCalls.delete(toolCall.id)
132
+ } else {
133
+ // Direct/legacy callers without a resolution marker — plain mirror
134
+ // increment (the per-review registry never saw this call).
135
+ agent._advisorRound++
136
+ }
137
+ }
102
138
  }
103
139
  if (FILE_MUTATORS.has(toolCall.name)) {
104
140
  const args = JSON.parse(toolCall.arguments)
105
141
  const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
106
142
  for (const p of paths) {
107
- const abs = join(agent.cwd, p)
143
+ const abs = resolve(agent.cwd, p)
108
144
  if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
109
145
  if (agent.memory) {
110
146
  // Fire-and-forget: don't block the agent loop on indexing.