thincoder 0.12.60 → 0.12.61

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 (152) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +27 -122
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +21 -6
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +26 -9
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/batch-segment.mjs +195 -0
  30. package/src/agent-tools/consult.mjs +23 -10
  31. package/src/agent-tools/design-token.mjs +14 -1
  32. package/src/agent-tools/digest-budget.mjs +76 -0
  33. package/src/agent-tools/eng.mjs +3 -3
  34. package/src/agent-tools/escalate-async.mjs +16 -13
  35. package/src/agent-tools/read-history.mjs +13 -3
  36. package/src/agent-tools/review-streak.mjs +93 -0
  37. package/src/agent-tools/settings.mjs +130 -17
  38. package/src/agent-tools/subagent-actions.mjs +15 -6
  39. package/src/agent-tools/subagent-async.mjs +66 -14
  40. package/src/agent-tools/subagent-panel.mjs +22 -15
  41. package/src/agent-tools/subagent-run.mjs +9 -6
  42. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  43. package/src/agent-tools/subagent-spawn.mjs +63 -16
  44. package/src/agent-tools/subagent.mjs +175 -49
  45. package/src/agent-tools/verify.mjs +13 -34
  46. package/src/agent-tools.mjs +1 -0
  47. package/src/agent.mjs +38 -21
  48. package/src/cli/distill-command.mjs +2 -2
  49. package/src/cli/make-agent.mjs +23 -7
  50. package/src/cli/memory-command.mjs +2 -2
  51. package/src/cli/setup-wizard.mjs +29 -9
  52. package/src/completions.mjs +114 -0
  53. package/src/config-migrate.mjs +70 -0
  54. package/src/config.mjs +132 -63
  55. package/src/conventions.mjs +223 -0
  56. package/src/crash-reports.mjs +7 -2
  57. package/src/expand-home.mjs +16 -0
  58. package/src/generate-title.mjs +1 -1
  59. package/src/hooks.mjs +7 -3
  60. package/src/memory/code-index.mjs +9 -3
  61. package/src/memory/code-sync.mjs +70 -31
  62. package/src/memory/delete.mjs +2 -0
  63. package/src/memory/docs.mjs +10 -6
  64. package/src/memory/file-walk.mjs +109 -0
  65. package/src/memory/schema.mjs +15 -3
  66. package/src/model-ref.mjs +66 -0
  67. package/src/model-specs.mjs +42 -8
  68. package/src/prompt-overlays.mjs +73 -16
  69. package/src/prompts/advisor-design.md +18 -8
  70. package/src/prompts/advisor-round1.md +7 -1
  71. package/src/prompts/advisor-round2.md +13 -2
  72. package/src/prompts/advisor-round3.md +13 -2
  73. package/src/prompts/common.md +115 -0
  74. package/src/prompts/consult-base.md +2 -0
  75. package/src/prompts/discipline-engineering.md +217 -0
  76. package/src/prompts/discipline-normal.md +179 -0
  77. package/src/prompts/persona-coder.md +21 -0
  78. package/src/prompts/persona-eng-coder.md +37 -0
  79. package/src/prompts/persona-eng-designer.md +55 -0
  80. package/src/prompts/persona-engineering.md +54 -0
  81. package/src/prompts/persona-explore.md +15 -0
  82. package/src/prompts/persona-normal.md +27 -0
  83. package/src/prompts/persona-plan.md +26 -0
  84. package/src/provider/anthropic.mjs +4 -4
  85. package/src/provider/core.mjs +13 -32
  86. package/src/provider/errors.mjs +26 -1
  87. package/src/provider/google.mjs +5 -6
  88. package/src/provider/index.mjs +2 -1
  89. package/src/provider/list-models.mjs +93 -0
  90. package/src/provider/rate.mjs +2 -1
  91. package/src/provider/responses.mjs +5 -3
  92. package/src/provider/sse.mjs +3 -4
  93. package/src/proxy.mjs +9 -14
  94. package/src/session-slots.mjs +7 -2
  95. package/src/session.mjs +59 -38
  96. package/src/token-ttl.mjs +2 -1
  97. package/src/tools/{system.mjs → bash.mjs} +6 -243
  98. package/src/tools/file.mjs +30 -10
  99. package/src/tools/git.md +1 -1
  100. package/src/tools/git.mjs +15 -34
  101. package/src/tools/index.mjs +4 -2
  102. package/src/tools/ops.mjs +20 -7
  103. package/src/tools/question.md +1 -0
  104. package/src/tools/question.mjs +26 -0
  105. package/src/tools/read.md +1 -1
  106. package/src/tools/read_image.md +1 -1
  107. package/src/tools/search.mjs +236 -0
  108. package/src/tui/agent-turn.mjs +32 -13
  109. package/src/tui/ansi.mjs +2 -0
  110. package/src/tui/clipboard.mjs +7 -1
  111. package/src/tui/cmd-advisor.mjs +3 -2
  112. package/src/tui/cmd-config.mjs +108 -37
  113. package/src/tui/cmd-eng.mjs +11 -27
  114. package/src/tui/cmd-exit.mjs +6 -8
  115. package/src/tui/cmd-model.mjs +14 -12
  116. package/src/tui/cmd-reindex.mjs +7 -0
  117. package/src/tui/cmd-submodel.mjs +8 -5
  118. package/src/tui/cmd-undo.mjs +4 -3
  119. package/src/tui/index.mjs +38 -37
  120. package/src/tui/key-handler.mjs +61 -17
  121. package/src/tui/key-modes.mjs +86 -8
  122. package/src/tui/layout.mjs +18 -10
  123. package/src/tui/model-catalog.mjs +89 -0
  124. package/src/tui/model-picker.mjs +498 -0
  125. package/src/tui/mouse.mjs +47 -10
  126. package/src/tui/pickers.mjs +28 -410
  127. package/src/tui/render-frame.mjs +32 -16
  128. package/src/tui/render-loop.mjs +2 -0
  129. package/src/tui/render-segments.mjs +5 -19
  130. package/src/tui/render.mjs +37 -5
  131. package/src/tui/slash-commands.mjs +2 -2
  132. package/src/tui/subagent-blocks.mjs +69 -90
  133. package/src/tui/subagent-children.mjs +50 -64
  134. package/src/tui/subagent-freeze.mjs +40 -43
  135. package/src/tui/subagent-panel.mjs +21 -66
  136. package/src/tui/suspension-drive.mjs +28 -82
  137. package/src/tui/tool-args.mjs +4 -2
  138. package/src/tui/tool-display.mjs +4 -3
  139. package/src/tui/tool-events.mjs +23 -16
  140. package/src/tui/tui-lifecycle.mjs +18 -5
  141. package/src/tui/wizard.mjs +52 -18
  142. package/src/tui/wrapped-spawn.mjs +38 -0
  143. package/src/prompts/coder.md +0 -13
  144. package/src/prompts/discipline.md +0 -84
  145. package/src/prompts/eng-coder.md +0 -19
  146. package/src/prompts/engineering-sub.md +0 -14
  147. package/src/prompts/engineering.md +0 -87
  148. package/src/prompts/explore.md +0 -12
  149. package/src/prompts/main.md +0 -34
  150. package/src/prompts/methodology-template.md +0 -38
  151. package/src/prompts/plan.md +0 -9
  152. package/src/prompts/system.md +0 -44
@@ -5,18 +5,18 @@ import { logEvent, errText, headText } from "../log.mjs"
5
5
  import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
6
6
  import { runHooks } from "../hooks.mjs"
7
7
  import { snapshotForUndo } from "../tui/cmd-undo.mjs"
8
- import { isDocFile } from "../advisor/repos.mjs"
8
+ import { isCodePath, loadConventions } from "../conventions.mjs"
9
9
  // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):写工具钩子——peerCollabNote(执行前冲突
10
10
  // 检测——软提示不阻止)+ recordPeerWrites(成功后累积本回合写足迹——回合末 flush)。
11
11
  import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites } from "../peer-domains.mjs"
12
12
  import { writeFileSync, mkdirSync, existsSync } from "node:fs"
13
- import { join, resolve } from "node:path"
13
+ import { join, resolve, relative } from "node:path"
14
14
  import { homedir } from "node:os"
15
15
  // §29 fix A(AGENT-LOOP.md §29——2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
16
16
  // 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
17
17
  // + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
18
18
  // 完成的写在 launchSeq 之前落地 → settle 不再误判 stale(§29 症状根因)。
19
- import { noteMutations } from "../agent-tools/advisor-async.mjs"
19
+ import { noteMutations, inflightDesignReviewConflict } from "../agent-tools/advisor-async.mjs"
20
20
  import { anyLiveDesignSlot } from "../token-ttl.mjs"
21
21
 
22
22
  const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
@@ -182,11 +182,13 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
182
182
  // Engineering mode PARENT gate: the parent agent must not touch code files
183
183
  // before the design review passed. Signaled by a live design slot (design-review
184
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
186
- // (docs/** and root-level docs like METHODOLOGY.md/README.md/AGENTS.md/
187
- // LICENSE) — writing them IS the design/methodology step. Everything under
188
- // src/ (incl. src/prompts/*.md) is product code, not documentation, and
189
- // needs a design token. Mechanically blocks "talk then code".
185
+ // is eng-coder-only and reset per run). Exemptions = the non-code classes of
186
+ // src/conventions.mjs (documentation and temp scratch files) — writing a design
187
+ // document IS the design step. Anything inside a declared code segment (default:
188
+ // src incl. src/prompts/*.md) is product code, not documentation, and needs a
189
+ // design token. The project can declare its own code paths (.thincoder/
190
+ // conventions.json) so a non-src layout is not silently exempted. Mechanically
191
+ // blocks "talk then code".
190
192
  // DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):资格判据 = 权威槽"任一活槽存在"
191
193
  // (anyLiveDesignSlot——查内存 Map,miss 回读槽文件——单值镜像 `_engDesignToken`
192
194
  // 已退役,门禁不再读镜像——AC4)。
@@ -194,14 +196,45 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
194
196
  && !anyLiveDesignSlot(agent)
195
197
  && FILE_MUTATORS.has(toolCall.name)) {
196
198
  const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
199
+ const conv = loadConventions(agent.cwd)
197
200
  // Unknown/missing paths (non-string, e.g. no path argument) are treated
198
- // as code — cannot tell what they touch, so block conservatively.
199
- const touchesCode = paths.some((p) => typeof p !== "string" || /^src[\\/]/.test(p) || !isDocFile(p))
201
+ // as code — cannot tell what they touch, so block conservatively. Known
202
+ // paths go through the single shared classifier: a declared code segment
203
+ // (default: "src") at ANY depth, else anything that is not documentation.
204
+ const touchesCode = paths.some((p) => typeof p !== "string" || isCodePath(p, conv))
200
205
  if (touchesCode) {
206
+ // Undeclared project → point at the declaration file (§4.3 降级可见契约).
207
+ const convNote = conv.declared
208
+ ? ""
209
+ : ` — this path was classified as product code by the default conventions (code paths: ${conv.codePaths.join(", ")}); declare project conventions in .thincoder/conventions.json to adjust.`
201
210
  prepared.push({
202
211
  toolCall, tool, denied: true,
203
212
  reason: "engineering design gate",
204
- hint: "Engineering mode: write the design document in docs/ first, then call advisor with type='design' to review it, and wait for user approval. Implementation is done by eng-coder subagents.",
213
+ hint: `Engineering mode: write the design document first(location per your project's document conventions), then call advisor with type='design' to review it, and wait for user approval. Implementation is done by eng-coder subagents.${convNote}`,
214
+ })
215
+ continue
216
+ }
217
+ }
218
+
219
+ // E(第 11 批·F17/§14.14 E-3d):D5 冻结窗口写前拦截——设计评审在途(点火 → 结算)期间,
220
+ // 父侧对被审文件集(声明文档集 + 批次档)的写入会被拒绝:在途写使本轮结算 stale——pass 轮
221
+ // = token 直接丢失(实证:第 10 批 id=20 整轮作废)。工具面 = FILE_MUTATORS(与变更记账
222
+ // 同集——不记入日志的写面既不判 stale 也不拦);判据与 reviewIsStale 同源
223
+ // (inflightDesignReviewConflict——同 docAbs / 同 normAbs;仅扫 running 未取消的设计条目)。
224
+ // 位置:只读 / autoApprove 短路之前——审批不得绕过冻结;拒绝 = 可见 denied + 逃生门
225
+ // (先 cancel → 改 → 重发)。
226
+ if (FILE_MUTATORS.has(toolCall.name)) {
227
+ let touched = []
228
+ try { touched = tool.touchedPaths ? tool.touchedPaths(args) : [args.path] } catch { touched = [] }
229
+ const absPaths = (touched ?? [])
230
+ .filter((p) => typeof p === "string" && p)
231
+ .map((p) => resolve(agent.cwd, p))
232
+ const conflict = inflightDesignReviewConflict(agent, absPaths)
233
+ if (conflict) {
234
+ prepared.push({
235
+ toolCall, tool, denied: true,
236
+ reason: "d5 freeze window",
237
+ hint: `write refused — design review #${conflict.id} is in flight over ${relative(agent.cwd, conflict.path)} (D5 freeze window). A write now would settle it stale — no token for a pass (the round is lost). Wait for the report, or cancel the review first (subagent action:'cancel' id:'${conflict.id}') and re-launch after the change.`,
205
238
  })
206
239
  continue
207
240
  }
@@ -293,6 +326,8 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
293
326
  ? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
294
327
  : item.reason === "engineering design gate"
295
328
  ? `Error: design review required before any file modification. ${item.hint}`
329
+ : item.reason === "d5 freeze window"
330
+ ? `Error: ${item.hint}`
296
331
  : item.reason === "denied by user"
297
332
  ? "Error: permission denied by user"
298
333
  : item.reason === "blocked by PreToolUse hook"
@@ -352,7 +387,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
352
387
  depth,
353
388
  signal,
354
389
  callbacks,
355
- // §24 D-24b: per-call id — the advisor tool marker-keys its launch so
390
+ // §11.2 D-24b: per-call id — the advisor tool marker-keys its launch so
356
391
  // recordToolResults can split async-ack accounting from sync settles.
357
392
  _toolCallId: item.toolCall.id,
358
393
  onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
@@ -6,7 +6,8 @@ import { readFileSync, readdirSync } from "node:fs"
6
6
  import { homedir } from "node:os"
7
7
  import { writeFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
8
8
  import { join } from "node:path"
9
- import { execSync } from "node:child_process"
9
+ import { execFile } from "node:child_process"
10
+ import { promisify } from "node:util"
10
11
 
11
12
  /** Single source for the AUTO-mode reminder (was duplicated in agent.mjs +
12
13
  * setup.mjs — consult P2, 2026-08-30). */
@@ -138,20 +139,65 @@ export async function offloadToolResult(text, callId, dir = join(configDir, "too
138
139
  }
139
140
  }
140
141
 
141
- /** Collect git branch, recent commits, and working tree status as context text */
142
- export function collectGitContext(cwd) {
142
+ // 失败冷却 v1(用户裁——GIT-ASYNC L21):真失败/超时后 30s 内跳过该 cwd 的收集——
143
+ // 病态 repo 周期拖慢变一次性。VSC setup-reminders.mjs 同构镜像。
144
+ const GIT_FAILURE_COOLDOWN_MS = 30_000
145
+ /** cwd → 最近一次 git 收集失败的 ts。评审 #2:访问时惰性清 >30s 旧条目(防无界累积)。 */
146
+ const gitFailureCooldowns = new Map()
147
+ // maxBuffer 沿用 execSync 默认现值 1 MiB(评审 #1——防大输出仓 ENOBUFS 翻转 → "" 破字节 parity)
148
+ const GIT_MAX_BUFFER = 1024 * 1024
149
+
150
+ const execFileAsync = promisify(execFile)
151
+
152
+ /** 冷却检查 + 惰性清扫:冷却期内的 cwd 返回 true(调用方直接 "" 跳过)。 */
153
+ function gitCooldownActive(cwd) {
154
+ const now = Date.now()
155
+ const lastFailure = gitFailureCooldowns.get(cwd)
156
+ if (lastFailure === undefined) return false
157
+ if (now - lastFailure >= GIT_FAILURE_COOLDOWN_MS) {
158
+ gitFailureCooldowns.delete(cwd) // 惰性清过期条目
159
+ return false
160
+ }
161
+ return true
162
+ }
163
+
164
+ /** Test seams — cooldown Map 状态控制(生产从不调用):
165
+ * `_gitFailureCooldownForTests(cwd, ts)` 读(ts 省略)或写 ts;`_clear…` 删条目。 */
166
+ export function _gitFailureCooldownForTests(cwd, ts) {
167
+ if (ts !== undefined) gitFailureCooldowns.set(cwd, ts)
168
+ return gitFailureCooldowns.get(cwd)
169
+ }
170
+ export function _clearGitFailureCooldownForTests(cwd) {
171
+ gitFailureCooldowns.delete(cwd)
172
+ }
173
+
174
+ /** Git context 纯格式化(branch/log/status = trim 后原串)——独立导出供单测锁字节
175
+ * parity(现拼装逐字节保留——detached/dirty>20 截断/clean 三形态——VSC 镜像)。 */
176
+ export function composeGitContext({ branch, log, status }) {
177
+ const dirty = status ? status.split("\n").length : 0
178
+ return [
179
+ `Git context: on branch \`${branch || "(detached)"}\`${dirty ? `, ${dirty} uncommitted change(s)` : ", working tree clean"}.`,
180
+ log ? `Recent commits:\n${log}` : "",
181
+ status ? `Uncommitted:\n${status.split("\n").slice(0, MAX_GIT_CHANGES_DISPLAY).join("\n")}${dirty > MAX_GIT_CHANGES_DISPLAY ? `\n… (${dirty - MAX_GIT_CHANGES_DISPLAY} more)` : ""}` : "",
182
+ ].filter(Boolean).join("\n")
183
+ }
184
+
185
+ /** Collect git branch, recent commits, and working tree status as context text.
186
+ * GIT-ASYNC L21:3×execSync 串行(最坏 15s 阻塞)→ 3×execFile 并行(Promise.all——
187
+ * 最坏 = 单次 5s 超时——事件循环不冻结)。单 catch → "" 保持 all-or-nothing(任一
188
+ * 失败/超时 → 整段不注入——非 git 快失败同路径);catch 记冷却 ts——30s 内跳过。 */
189
+ export async function collectGitContext(cwd) {
190
+ if (gitCooldownActive(cwd)) return ""
143
191
  try {
144
- const opts = { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: GIT_TIMEOUT_MS }
145
- const branch = execSync("git branch --show-current", opts).trim()
146
- const log = execSync("git --no-pager log --oneline -5", opts).trim()
147
- const status = execSync("git status --short", opts).trim()
148
- const dirty = status ? status.split("\n").length : 0
149
- return [
150
- `Git context: on branch \`${branch || "(detached)"}\`${dirty ? `, ${dirty} uncommitted change(s)` : ", working tree clean"}.`,
151
- log ? `Recent commits:\n${log}` : "",
152
- status ? `Uncommitted:\n${status.split("\n").slice(0, MAX_GIT_CHANGES_DISPLAY).join("\n")}${dirty > MAX_GIT_CHANGES_DISPLAY ? `\n… (${dirty - MAX_GIT_CHANGES_DISPLAY} more)` : ""}` : "",
153
- ].filter(Boolean).join("\n")
192
+ const opts = { cwd, encoding: "utf8", timeout: GIT_TIMEOUT_MS, windowsHide: true, maxBuffer: GIT_MAX_BUFFER }
193
+ const [branch, log, status] = await Promise.all([
194
+ execFileAsync("git", ["branch", "--show-current"], opts).then((r) => r.stdout.trim()),
195
+ execFileAsync("git", ["--no-pager", "log", "--oneline", "-5"], opts).then((r) => r.stdout.trim()),
196
+ execFileAsync("git", ["status", "--short"], opts).then((r) => r.stdout.trim()),
197
+ ])
198
+ return composeGitContext({ branch, log, status })
154
199
  } catch {
200
+ gitFailureCooldowns.set(cwd, Date.now()) // 真失败/超时——冷却 30s
155
201
  return ""
156
202
  }
157
203
  }
@@ -164,6 +210,18 @@ export class ContinueError extends Error {
164
210
  }
165
211
  }
166
212
 
213
+ /** 跨段累计编号帧(TURN-CAP-CONTINUE.md §19.3——第 19 批 TURN-ACROSS-SEGMENTS):
214
+ * 唯一计算点(纯函数)——把链内累计序数换算成面向消费面的编号载荷。
215
+ * - seq = 该轮链内累计序数(1 起——`agent._turnSeq` 每轮 +1,续跑不重置)
216
+ * - turn = 段内轮号(0 起——段内帽判定的循环变量)
217
+ * - maxTurns = 本段预算(段内帽判定值——原样传入)
218
+ * 返回 { turn, maxTurns }:turn = seq(累计已跑轮数);maxTurns = 段前累计 + 本段预算
219
+ * = seq - turn - 1 + maxTurns(差额项 = 本段开始前的链内累计)。
220
+ * 段内帽判定不读本帧(只读段内 turn / maxTurns——N6 零机制改动)。 */
221
+ export function turnFrame(seq, turn, maxTurns) {
222
+ return { turn: seq, maxTurns: seq - turn - 1 + maxTurns }
223
+ }
224
+
167
225
  /** Repair malformed conversation history: remove orphan tool messages and fill missing tool results */
168
226
  export function repairHistory(history) {
169
227
  const out = []
@@ -16,7 +16,11 @@
16
16
  * invalidate verify only (user decision 2026-08-08: reviews trigger on
17
17
  * code mutations, not environment changes).
18
18
  * - advisor calls always advance _advisorRound (convergence budget counts
19
- * attempts, not successes).
19
+ * attempts, not successes). §15 F16 (2026-09-11 第 13 批): the SYNC
20
+ * called-mark is gated by the single incomplete-tail predicate
21
+ * (advisorIncompleteMarker) — a truncated-then-settled CODE review must not
22
+ * count as covered (the guard re-pushes); design reviews keep the mark
23
+ * (no code face — settle-side `failureVerdict` parity).
20
24
  * - Mutations feed _touchedFiles and fire-and-forget memory reindex.
21
25
  */
22
26
  import { pushReal } from "../context.mjs"
@@ -24,7 +28,7 @@ import { specForModel } from "../config.mjs"
24
28
  import { FILE_MUTATORS } from "./helpers.mjs"
25
29
  import { resolve } from "node:path"
26
30
  import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
27
- import { looksLikeReviewOutput } from "../advisor/run.mjs"
31
+ import { looksLikeReviewOutput, advisorIncompleteMarker } from "../advisor/run.mjs"
28
32
 
29
33
  let _reindexFile = null
30
34
 
@@ -96,7 +100,7 @@ export async function recordToolResults(agent, toolByName, results) {
96
100
  }
97
101
  if (toolCall.name === "verify") agent._verifiedThisRun = true
98
102
  if (toolCall.name === "advisor") {
99
- // §24 D-24b (settle accounting split — fix #2): an ASYNC launch returns an
103
+ // §11.2 D-24b (settle accounting split — fix #2): an ASYNC launch returns an
100
104
  // ack and settles later — the settle callback owns its called/round/token
101
105
  // accounting. Only the SYNC path (depth>0 / explicit async:false) accounts
102
106
  // here — per-review instance round++ (marker-keyed by tool call id) + the
@@ -111,10 +115,14 @@ export async function recordToolResults(agent, toolByName, results) {
111
115
  } else if (asyncAck) {
112
116
  agent._advisorAsyncAcks.delete(toolCall.id)
113
117
  } else {
114
- agent._calledAdvisorThisRun = true
115
118
  const reviewId = agent._advisorSyncCalls?.get(toolCall.id)
119
+ const run = reviewId !== undefined ? advisorRuns(agent).get(reviewId) : undefined
120
+ // F16 同步面(第 13 批 §15.2——ADVISOR-CONVERGENCE.md §15):未完成尾 + 非设计面
121
+ // (含类型不可判——run 缺失的 legacy 直调)⇒ 不置「已覆盖」(与 settle 面
122
+ // 逐条 parity;设计评审保持计入——无代码面)。
123
+ const incomplete = advisorIncompleteMarker(String(result))
124
+ if (!(incomplete && run?.reviewType !== "design")) agent._calledAdvisorThisRun = true
116
125
  if (reviewId !== undefined) {
117
- const run = advisorRuns(agent).get(reviewId)
118
126
  if (run) {
119
127
  run.round++
120
128
  agent._advisorRound = run.round
@@ -0,0 +1,39 @@
1
+ /**
2
+ * relay-prefix.mjs — relay 前缀(`role#id/`)文法**单一权威**(第 27 批 §12.3①——R-A2.3)。
3
+ *
4
+ * 零依赖(不 import 任何模块)——TUI(`src/tui/`)与 ACP(`src/acp/`)双向可导入,无环;
5
+ * 生成侧 `src/agent/spawn-child.mjs` 再导出(枢纽)。文法 = 自
6
+ * `src/tui/subagent-blocks.mjs` 逐字迁入(第 27 批 §12.3②——零语义改动)。
7
+ */
8
+
9
+ /** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
10
+ export const RELAY_PREFIX_RE = /^([\w-]+)#(\d+)\//
11
+
12
+ /** 嵌套 relay 前缀通用解析(循环解析任意深度——显示契约 docs/design/TUI.md §6):
13
+ * `eng-coder#2/explore#1/read` → { head(块路由), inner[], label(inner 链——R23:
14
+ * 子块折叠键/外层 currentTool 全路径用), rest }。
15
+ * 单层 = inner[]/label ""——与既有单段匹配语义零改;无前缀 → null。 */
16
+ export function parseRelayPath(text) {
17
+ const segments = []
18
+ let rest = String(text)
19
+ for (;;) {
20
+ const m = rest.match(RELAY_PREFIX_RE)
21
+ if (!m) break
22
+ segments.push(`${m[1]}#${m[2]}`)
23
+ rest = rest.slice(m[0].length)
24
+ }
25
+ if (segments.length === 0) return null
26
+ return {
27
+ head: segments[0],
28
+ inner: segments.slice(1),
29
+ label: segments.slice(1).join("/"),
30
+ rest,
31
+ }
32
+ }
33
+
34
+ /** 构造向单一实现(本批统一的两处构造点——`makeRelay` 与 async 取号分支共用;
35
+ * 另两处字面构造(escalate/advisor async)不在本批声明面——批次档 §2 修正轮登记,如需收敛另批):
36
+ * `relayPrefixOf("coder", 3)` → `"coder#3/"`。 */
37
+ export function relayPrefixOf(label, id) {
38
+ return `${label}#${id}/`
39
+ }
@@ -16,6 +16,8 @@ import { getAsyncPool } from "../agent-tools/async-settle.mjs"
16
16
  // R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末域登记 flush(写工具钩子累积 →
17
17
  // 整写一次本实例 peers 文件——无写入跳过;失败容忍不抛)
18
18
  import { flushPeerDomains } from "../peer-domains.mjs"
19
+ // 第 30 批 D1(Stop 钩子——AGENT-LOOP.md §21):主会话 run 终止事件(触发块见 finalizeAgentTurn)
20
+ import { runHooks } from "../hooks.mjs"
19
21
 
20
22
  /**
21
23
  * 响应后置提醒注入(2026-09-05 实践轮——自 runAgent 响应处理链提取,verbatim):
@@ -113,16 +115,29 @@ export async function injectTurnReminders(agent, ctx) {
113
115
  }
114
116
 
115
117
  /**
116
- * 回合收尾(2026-09-05 实践轮——自 runAgent finally 提取,verbatim + 签名化):
117
- * consult 清理(无孤儿烧 token consult children)→ async 池回合尾分流(Ctrl+C
118
- * 清池不注入 / ContinueError 留池续跑 / 其余 collectSettledAsync)→ auto-turn guard
119
- * 标记继承(对象字段清单——正常结束才继承;中止丢弃;ContinueError 由续跑快照)。
118
+ * 回合收尾(2026-09-05 实践轮——自 runAgent finally 提取,verbatim + 签名化):写足迹
119
+ * flush Stop 钩子(第 30 D1——ctx.depth === 0 的主会话 run 终止事件,
120
+ * fire-and-forget)→ consult 清理(无孤儿烧 token consult children)→ async 池回合尾
121
+ * 分流(Ctrl+C 清池不注入 / ContinueError 留池续跑 / 其余 collectSettledAsync)→ auto-turn
122
+ * guard 标记继承(对象字段清单——正常结束才继承;中止丢弃;ContinueError 由续跑快照)。
120
123
  */
121
124
  export async function finalizeAgentTurn(agent, ctx) {
122
- const { signal, autoTurn, suspDriven, thrownError } = ctx
125
+ const { signal, autoTurn, suspDriven, thrownError, depth } = ctx
123
126
  // R10 L3(MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末登记 flush——本回合写足迹整写
124
127
  // 一次(首行执行:收尾链后续任何异常都不吞登记);无写入 → 跳过(hot 窗口自然衰减)。
125
128
  flushPeerDomains(agent)
129
+ // 第 30 批 D1(Stop 钩子):主会话 run 终止 → fire-and-forget(非阻塞;失败静默——与 PostToolUse 同语义)。
130
+ // 排除用户中止(Ctrl+C / Ctrl+I——中止后 TUI 重建 controller 续跑或已显式停回合)与 AbortError 展开。
131
+ if (depth === 0 && !signal?.aborted && thrownError?.name !== "AbortError") {
132
+ runHooks("Stop", {
133
+ agent,
134
+ error: thrownError && !(thrownError instanceof ContinueError) ? thrownError : undefined,
135
+ extra: {
136
+ turn: agent._currentTurn ?? 0,
137
+ reason: thrownError instanceof ContinueError ? "maxTurns" : thrownError ? "error" : "done",
138
+ },
139
+ }).catch(() => {})
140
+ }
126
141
  // R17(AGENT-LOOP.md §25 D-R17a): consultation sessions are cross-turn
127
142
  // background work now — NO unconditional turn-end cleanup (the old rule aborted
128
143
  // leftover consult children at every turn end because check-loop consumption was
@@ -163,7 +178,7 @@ export async function finalizeAgentTurn(agent, ctx) {
163
178
  // keep _asyncSubagents/_asyncAdvisors/_consultSessions — the resumed run continues them
164
179
  } else {
165
180
  const injectedAdvisor = await collectSettledAsync(agent, { suspDriven })
166
- // §24 D-24b: a normally-ended USER run with no review/child activity closes
181
+ // §11.2 D-24b: a normally-ended USER run with no review/child activity closes
167
182
  // the OPEN code instances — the converged/abandoned thread must not burn the
168
183
  // 5-round cap of a later task (the next code review starts a fresh round 1).
169
184
  // Digest auto-turns are exempt (their disposition precedes the fix round).
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * One unified transient user reminder per turn covers the whole self-awareness
6
6
  * family: env identity (R8 — §10 D-1 END 常量先例:静态常量,不做 cmdline 判别),
7
- * engineering mode (R9), active model (R11), restart awareness (R5 resumed).
7
+ * engineering mode (R9), active model (R11), session slot (F1), restart awareness
8
+ * (R5 resumed). §11.2(2026-09-08):resumed 按会话跟踪——恢复事件由 session.mjs
9
+ * applySession 武装(agent._envResumed——载入历史非空 = 恢复);进程重启句独立走
10
+ * agent._processRestartPending(bin 启动 resume 路径设)——双信号互不绑门(N6)。
8
11
  * Change awareness needs no dedicated injection — every turn carries the
9
12
  * CURRENT state, so a mode/model flip shows up in the next turn's line.
10
13
  * git is NOT a field here (§11.1: CLI 不重复注入 clean|dirty 摘要) — the rich
@@ -16,26 +19,30 @@
16
19
  import { END } from "../session-slots.mjs"
17
20
  import { peerInstances } from "../peer-instances.mjs"
18
21
 
19
- /** env-state line builder — pure, unit-testable. */
20
- export function envStateLine({ mode, model, resumed }) {
21
- return `[System reminder: env: ${END}, mode: ${mode}, model: ${model}, resumed: ${resumed ? "yes" : "no"}.]`
22
+ /** env-state line builder — pure, unit-testable.
23
+ * §11.2(F1):slot 字段入行——位置在 model resumed 前(N3:无绑定 → 显式 null——
24
+ * 不读 manifest active 共享指针——粘性 _slot 才是"本 agent 之槽")。 */
25
+ export function envStateLine({ mode, model, slot, resumed }) {
26
+ return `[System reminder: env: ${END}, mode: ${mode}, model: ${model}, slot: ${slot}, resumed: ${resumed ? "yes" : "no"}.]`
22
27
  }
23
28
 
24
29
  /**
25
30
  * Push the per-turn env-state reminder (setup.mjs calls it for depth-0 runs —
26
- * the line describes the MAIN agent's host/mode/model identity). resumed=yes
27
- * exactly once: the first turn after a restored session (setup.mjs sets
28
- * agent._envResumed alongside the `process restarted` reminder); consumed here
29
- * so every later turn reads resumed=no (T-E5).
31
+ * the line describes the MAIN agent's host/mode/model/slot identity). resumed=yes
32
+ * exactly once per session restore: applySession (session.mjs) arms
33
+ * agent._envResumed when restored history is non-empty; consumed here so every
34
+ * later turn reads resumed=no(§11.2 F2——每次恢复一次;/new 与空历史槽切换不武装——
35
+ * F3 伪触发消除)。slot = 粘性 agent._slot(§11.2 N2/N3——无绑定显式 null)。
30
36
  * Degrades safely: activeModel null → provider.model → "unknown" (T-E12);
31
37
  * a missing config/history never throws (T-E13).
32
38
  */
33
39
  export function pushEnvStateReminder(agent) {
34
40
  const mode = agent.config?.agent?.engineering ? "eng" : "normal"
35
41
  const model = agent.activeModel ?? agent.provider?.model ?? "unknown"
42
+ const slot = agent._slot ?? null
36
43
  const resumed = agent._envResumed === true
37
44
  agent._envResumed = false
38
- agent.history.push({ role: "user", content: envStateLine({ mode, model, resumed }), transient: true })
45
+ agent.history.push({ role: "user", content: envStateLine({ mode, model, slot, resumed }), transient: true })
39
46
  }
40
47
 
41
48
  /**