@thincoder/core 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * eng tool: enter/exit engineering mode.
3
+ * In engineering mode the agent follows design-before-code methodology.
4
+ * Toggled here at session level (in-memory flag; the session slot is the sole authority —
5
+ * saveSession round-trips it at turn end; /eng writes the slot directly). The old ctx
6
+ * state-persist hook was removed 2026-09-08 (ENG-SESSION-PROVIDER-CLEANUP D1.2): no
7
+ * provider existed and its legacy payload keys were never read by applySession.
8
+ * R16 (2026-09-06): design tokens are session-level flow credentials — mode toggles
9
+ * do NOT clear them (ON→OFF keeps, OFF→ON does not require a fresh review); only TTL
10
+ * expiry cleans tokens, at three points: restore filtering (session.mjs applySession) /
11
+ * eng enter expired cleanup (below + cmd-eng ON) / spawn-gate expired rejection
12
+ * (subagent-spawn.mjs). See docs/design/ENG-TOKEN-BINDING-TUNING.md §5/§5.1 (F-R16).
13
+ * #41(2026-09-18):enter 分支改「先判后翻」——判据 = 入口决策树单源
14
+ * `resolveEngineeringManifest`(docs/core/design/MANIFEST.md §2.8 F2 / KD-M1-20/21);
15
+ * 拒翻 ⇒ 返回原因串 + **零副作用**(模式保持 OFF)。OFF 方向与幂等 enter 零改。
16
+ */
17
+ import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
18
+ import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
19
+ import { resolveEngineeringManifest } from "../manifest.mjs"
20
+
21
+ // ─── 持久化镜像 / 面板提示注入缝(#91——「写盘镜像 / 面板提示按端注入」,形态参 §2.13.5 注入缝)──
22
+ /**
23
+ * 工程模式切换通知注入位(**缺省不覆盖** = no-op——CLI 语义(只进会话),零行为变;端装配层
24
+ * 可覆盖为本端镜像写盘 / 面板提示)。核内零端名分支(契约 5——本档只认 `onToggle` 函数名)。
25
+ * 契约:`onToggle(enabled, ctx) → string | null | undefined | Promise<同>`——非空字符串 =
26
+ * 追加到结果文案尾(如镜像写入碰撞提示);其余值 = 无提示。调用时机 = 状态翻转后(观察新态)。
27
+ */
28
+ let injectedMirror = null
29
+ export function configureEngMirror(impl) {
30
+ injectedMirror = impl && typeof impl.onToggle === "function" ? impl : null
31
+ }
32
+ /** 撤销注入(测试与端装配生命周期用——缺省态 = 无镜像 / 无面板提示)。 */
33
+ export function resetEngMirror() { injectedMirror = null }
34
+
35
+ /** 镜像通知串(缺省空串;仅非空字符串追加)。 */
36
+ async function mirrorNotice(enabled, ctx) {
37
+ const out = await injectedMirror?.onToggle(enabled, ctx)
38
+ return typeof out === "string" && out.length > 0 ? ` ${out}` : ""
39
+ }
40
+
41
+ export const engTool = {
42
+ name: "eng",
43
+ description:
44
+ "Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents. " +
45
+ "Returns the mode state — 'Engineering mode activated/exited' (an already-active state is acknowledged).",
46
+ parameters: {
47
+ type: "object",
48
+ properties: {
49
+ action: { type: "string", enum: ["enter", "exit"], description: "Enter or exit engineering mode" },
50
+ },
51
+ required: ["action"],
52
+ },
53
+ readonly: true,
54
+ async execute(args, ctx) {
55
+ ctx.agent.config.agent ??= {}
56
+ if (args.action === "exit") {
57
+ ctx.agent.config.agent.engineering = false
58
+ // R16 (F-R16a): OFF 不清 token——有效 token 跨模式存活(设计评审过的产物不因
59
+ // 开关重复烧)。过期清理跑在另外三处(恢复过滤 / 开模式 / spawn 门禁拒)。
60
+ ctx.agent._advisorRound = 0 // reset convergence budget
61
+ ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
62
+ ctx.agent._touchedFiles = [] // clear mutation tracking
63
+ ctx.agent._lastEngState = false
64
+ ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
65
+ ctx.agent._pendingReminders.push(ENG_OFF_REMINDER)
66
+ return "Engineering mode exited. Standard discipline now applies. You may edit files directly." + await mirrorNotice(false, ctx)
67
+ }
68
+ if (args.action === "enter") {
69
+ // Idempotent enter (v2 2026-08-25): already in engineering mode → pure no-op
70
+ // (cleanup only runs on a real off→on transition — T-R16c precondition: first
71
+ // exit/OFF, then enter; an already-on enter must not touch tokens at all).
72
+ if (ctx.agent.config.agent.engineering) {
73
+ return "Engineering mode already active. Existing design tokens stay valid."
74
+ }
75
+ // #41 先判后翻(MANIFEST.md §2.8 F2 / KD-M1-21):判据 = 入口决策树**单源**
76
+ // (resolveEngineeringManifest——KD-M1-20),判据通过才写态。拒翻 ⇒ 返回原因串 +
77
+ // **零副作用**(不清 token / 不重置 _advisorRuns / 不入列 ENG_ON_REMINDER /
78
+ // 不动 _lastEngState / 不触镜像缝);准 ⇒ 缺档格就地建档(writer:'main')+
79
+ // agent.manifest ← 结果(翻转面唯一赋值点——相位行当回合起活,§2.8 F2 放行行)。
80
+ const r = resolveEngineeringManifest(ctx.agent.cwd ?? ctx.cwd ?? process.cwd(), { writer: "main" })
81
+ if (!r.ok) {
82
+ return `Error: cannot enter engineering mode — ${r.message} (mode unchanged)`
83
+ }
84
+ ctx.agent.manifest = r.manifest
85
+ ctx.agent.config.agent.engineering = true
86
+ // R16 (F-R16b ②): off→on 不重评——只清过期 token(用户裁定"打开工程模式时
87
+ // 应该清理"),有效 token 原样保留——遍历 Map 删过期,返回文案含清理个数。
88
+ const cleared = purgeExpiredDesignTokens(ctx.agent)
89
+ ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
90
+ ctx.agent._lastEngState = true
91
+ ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
92
+ ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
93
+ let msg = "Engineering mode activated. Design-before-code enforced: write a design document first (location per your project's document conventions), run advisor with type='design', get user approval, then implement via eng-coder subagents."
94
+ if (cleared > 0) {
95
+ msg += ` Cleared ${cleared} expired design token${cleared === 1 ? "" : "s"} — 清 ${cleared} 个过期 token,有效 token 保留(TTL 内不重评)。`
96
+ }
97
+ msg += await mirrorNotice(true, ctx)
98
+ return msg
99
+ }
100
+ return "Invalid action: expected 'enter' or 'exit'"
101
+ },
102
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * escalate-async.mjs — async 飞刀 runner (AGENT-LOOP.md §25 D-R17b — R17, 2026-09-06).
3
+ *
4
+ * The subagent action:"escalate" is DEFAULT-async at depth 0 (decision ③: the
5
+ * depth-0-only escalate flips to async like every other background family;
6
+ * `async: false` keeps the legacy synchronous flight). An async escalate runs in
7
+ * the SHARED "other" pool domain (decision Q2 🅰 — the entry lives in
8
+ * `_asyncSubagents` with role "escalate"/_pool "other", so it shares the 4-slot
9
+ * other domain with explore/plan/coder spawns — capacity, queueing, refill,
10
+ * status and cancel all come from the generic machinery):
11
+ * - launch → ack {id, role:"escalate", status:"running"|"queued"[, position]};
12
+ * the turn ends naturally, the session suspends (poolLive counts the entry).
13
+ * - settle THREE-WAY classification (review #4): done → merge-all mutations back
14
+ * into the parent's bookkeeping + overlap warning appended to the report
15
+ * (report-level, not a gate — round2 #4); error (child failure / turn cap) →
16
+ * partial mutations merged ONLY when the parent did not touch overlapping
17
+ * files since launch (overlap → no merge + differences listed in the report);
18
+ * cancelled → never reaches the digest stream (D-M6 — no merge, stopped
19
+ * reminder).
20
+ * - the classified report lands in `_pendingAsyncResults`(pending 单容器 +role——
21
+ * ASYNC-RESULT-CONTAINER.md D2——挂起期 settle 移交)when the settle happens in a
22
+ * suspension, or stays pooled for the turn-end collection otherwise;
23
+ * injectAsyncResult's escalate
24
+ * role branch delivers the digest ("报告已 merge——可继续处置" — the action
25
+ * domain still follows the consuming turn's tier — no family exception).
26
+ */
27
+ import { relative, isAbsolute } from "node:path"
28
+ import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
29
+ import { runWithContinue, TURN_CAP_MARK, wrapChildCallbacks } from "../agent/spawn-child.mjs"
30
+ // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口常量(单源——store 零依赖)。
31
+ import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
32
+ import { logEvent } from "../log.mjs"
33
+ import { deathLine } from "../abort-provenance.mjs"
34
+ import {
35
+ mergeChildMutations, runningPoolCount, poolDomainOf, poolLimitsFor, ASYNC_POOL_LIMITS, enqueueAsk,
36
+ } from "./subagent-async.mjs"
37
+ import { refreshQueuedTokens, nextSubagentId, consumeSubagentToken, assertPoolKeyFree } from "./subagent-scheduler.mjs"
38
+ import { mutationSeqOf } from "./advisor-async.mjs"
39
+ // ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
40
+ import { bindChildController, buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
41
+
42
+ /** Parent-side mutations (absolute paths) committed AFTER the escalate launch —
43
+ * the overlap scan feeds the settle classification (review #4/round2 #4: the
44
+ * parent may have edited files while the flight ran). */
45
+ function parentMutationsSince(parent, launchSeq) {
46
+ const out = []
47
+ for (const m of parent?._mutLog ?? []) {
48
+ if (m.seq <= (launchSeq ?? -1)) continue
49
+ for (const p of m.paths ?? []) if (!out.includes(p)) out.push(p)
50
+ }
51
+ return out
52
+ }
53
+
54
+ /** Files BOTH sides touched since launch (case-insensitive key on win32 —
55
+ * filesOverlap precedent). */
56
+ function overlapPaths(parent, launchSeq, childTouched) {
57
+ const since = parentMutationsSince(parent, launchSeq)
58
+ if (since.length === 0 || !childTouched?.length) return []
59
+ const key = (p) => (process.platform === "win32" ? String(p).toLowerCase() : String(p))
60
+ const sinceKeys = new Set(since.map(key))
61
+ const hits = childTouched.filter((p) => sinceKeys.has(key(p)))
62
+ // relative display form (touchedFilesNote precedent)
63
+ const cwd = parent?.cwd ?? process.cwd()
64
+ return hits.map((p) => {
65
+ const r = relative(cwd, p)
66
+ return r && !r.startsWith("..") && !isAbsolute(r) ? r : p
67
+ })
68
+ }
69
+
70
+ /** Escalate-side touched files, relative display (or a placeholder). */
71
+ function childTouchedDisplay(child, cwd) {
72
+ const touched = child?._touchedFiles ?? []
73
+ if (touched.length === 0) return "(none recorded)"
74
+ const shown = touched.map((f) => {
75
+ const r = relative(cwd ?? process.cwd(), f)
76
+ return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
77
+ })
78
+ return shown.join(", ")
79
+ }
80
+
81
+ /** Relative touched-file list (mirror of the sync path's note). */
82
+ function touchedFilesNote(child, cwd) {
83
+ const touched = child?._touchedFiles ?? []
84
+ if (touched.length === 0) return ""
85
+ const shown = touched.map((f) => {
86
+ const r = relative(cwd ?? process.cwd(), f)
87
+ return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
88
+ })
89
+ return `\nTouched files: ${shown.join(", ")}`
90
+ }
91
+
92
+ /**
93
+ * Async escalate settle — three-way classification (§25 D-R17b review #4):
94
+ * - done: merge ALL mutations (the escalation's changes are the parent's —
95
+ * verify/advisor guards must see them) + overlap warning into the report
96
+ * (report-level hint — not a gate — round2 #4);
97
+ * - error (child failure / turn cap): partial mutations merge ONLY without a
98
+ * parent-side overlap — overlap → NO merge + differences listed (the report
99
+ * carries both sides so the model can decide);
100
+ * - cancelled: nothing merges, never reaches pending (D-M6 — stopped reminder).
101
+ * Runs BEFORE the pending transfer / digest injection. Mutates entry.report /
102
+ * entry.error into the FINAL digest body (the family wording rides
103
+ * injectAsyncResult's escalate branch).
104
+ */
105
+ export function classifyEscalateSettle(parent, entry) {
106
+ if (entry.cancelled) return { cancelled: true }
107
+ // §23.3.1(TUI-OOM-ROOTCAUSE)——childAgent 语义面:settle 分类(touched 摘要/partial
108
+ // merge)与 status/observe 摘要的读取源;**消化注入完成后由 `releaseSettledEntry`
109
+ // (async-settle.mjs)置 null**(池内未消化窗口不释放——表 2 候选 2 否决)。
110
+ const child = entry.childAgent
111
+ const touched = child?._touchedFiles ?? []
112
+ const overlap = overlapPaths(parent, entry.launchSeq, touched)
113
+ const hasError = entry.error != null
114
+ const raw = hasError ? entry.error : entry.report
115
+ if (hasError) {
116
+ // error branch (child failure / turn cap — review #4): partial mutations
117
+ // merge ONLY when the parent did not touch overlapping files since launch.
118
+ // childAgent may be null when the flight died before child creation (advisor
119
+ // 复评 🟡2——防御:无 child 即无 partial mutations——不 merge 不崩)。
120
+ const merged = child != null && overlap.length === 0 && mergeChildMutations(parent, child)
121
+ let decision = ""
122
+ if (merged) {
123
+ decision = `\nPartial changes merged into the parent's bookkeeping (no parent-side overlap since launch).`
124
+ } else if (overlap.length > 0) {
125
+ decision = `\nPartial changes NOT merged — the parent changed overlapping files while this escalate ran: ${overlap.join(", ")}. Escalate-side changes: ${childTouchedDisplay(child, parent?.cwd)}. Review the conflict and decide what to keep (report-level — not a gate).`
126
+ }
127
+ // (nothing to merge + no overlap → the plain error report stands alone)
128
+ entry.error = `${raw}${decision}`
129
+ entry.report = null
130
+ return { cancelled: false, merged, overlap }
131
+ }
132
+ // done — merge-all + overlap warning into the report (report-level — not a gate)
133
+ const merged = mergeChildMutations(parent, child)
134
+ const overlapNote = overlap.length > 0
135
+ ? `\n⚠ Overlapping writes: the parent changed ${overlap.join(", ")} while this escalate ran — mutations merged all the same; review those files before building on the report (report-level warning — not a gate).`
136
+ : ""
137
+ entry.report = `${raw}${overlapNote}`
138
+ return { cancelled: false, merged, overlap }
139
+ }
140
+
141
+ /**
142
+ * Launch the async escalate (preflights already passed in executeEscalateAction):
143
+ * pool admission into the shared OTHER domain → entry in `_asyncSubagents`
144
+ * (generic queue/refill/status/cancel machinery) → ack. The flight starts on
145
+ * entry.start() (immediately, or later via maybeRefillAsync when a slot frees).
146
+ */
147
+ export function launchEscalateAsync(parent, ctx, launch) {
148
+ const { task, provider, tag, effortNote } = launch
149
+ parent._asyncSubagents ??= new Map()
150
+ parent._asyncQueue ??= []
151
+ // Async id allocation (AGENT-LOOP.md §15 D-A1 precedent): reserve the relay
152
+ // counter at launch — the returned id stays stable while the entry sits queued.
153
+ // The [model] token (TUI block creation) is DEFERRED to actual start so queued
154
+ // flights don't paint an empty panel block (subagent-parity).
155
+ // SUBAGENT-ID-COUNTER-AGENT(2026-09-09):取号统一走 nextSubagentId(池活续号
156
+ // 兜底——counter 载体= agent 本体 _subAgentCounter——跨 run/跨压缩存活)。
157
+ const id = nextSubagentId(parent)
158
+ // ED-5(AGENT-LOOP-SUBAGENT.md §6.21)一次性取号令牌消费(同步配对——本函数内取号 →
159
+ // 消费无 await 间隙);断言通过即置 undefined(防同一令牌跨站点复用)。
160
+ consumeSubagentToken(parent, id, "escalate launch", "escalate")
161
+ const relayPrefix = `escalate#${id}/`
162
+ const entry = {
163
+ id, role: "escalate", relayPrefix,
164
+ _pool: poolDomainOf("escalate"), // other — shares the domain with explore/plan/coder (§11.1 D-24a)
165
+ status: "queued",
166
+ position: undefined,
167
+ report: null, error: null, done: false, cancelled: false,
168
+ promise: null, _settle: null, _settleSeq: 0,
169
+ model: provider.model ?? null,
170
+ startedAt: null,
171
+ turn: 0, maxTurns: 0,
172
+ controller: null,
173
+ _files: undefined, _dependsOn: undefined,
174
+ childAgent: null,
175
+ tag, effortNote, launchSeq: mutationSeqOf(parent),
176
+ }
177
+ const limits = poolLimitsFor(parent)
178
+ entry.status = runningPoolCount(parent, entry._pool) >= (limits[entry._pool] ?? ASYNC_POOL_LIMITS[entry._pool])
179
+ ? "queued" : "running"
180
+ entry.promise = new Promise((res) => { entry._settle = res })
181
+ const ctrl = new AbortController()
182
+ entry.controller = ctrl
183
+ // D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md——D5 同款:_sessionSignal 兜底)·
184
+ // #98 链结单点(interrupt 豁免面——Ctrl+I 不逐链中止池内子代理)。
185
+ bindChildController(ctrl, buildChildSignal(parent, ctx))
186
+ // Turn mirror (⟦ev⟧turn from the child runAgent → entry.turn/maxTurns — status parity).
187
+ const flight = async () => {
188
+ entry.status = "running"
189
+ entry.position = undefined
190
+ entry.startedAt = Date.now()
191
+ ctx.callbacks?.onToken?.(relayPrefix + "⟦ev⟧async\x1e")
192
+ ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (provider.model ?? ""))
193
+ const child = createAgent({
194
+ provider,
195
+ tools: parent.tools,
196
+ config: parent.config,
197
+ cwd: parent.cwd,
198
+ memory: parent.memory,
199
+ // G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
200
+ role: "coder",
201
+ })
202
+ entry.childAgent = child // settle 分类/status touched 摘要绑定(start 时刻)
203
+ child._historyWindow = RECORD_WINDOW_MESSAGES // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口(四处创建点同置)
204
+ child._logId = relayPrefix.slice(0, -1)
205
+ // SUBAGENT-UPSTREAM-CHANNEL(§6.27.4 W3——escalate **async**:工具返回注走异步形)。
206
+ child._upstream = { parent, label: relayPrefix.slice(0, -1), sync: false }
207
+ logEvent("child:spawn", { role: "escalate", id: child._logId, kind: "async", status: "running", ms: 0 })
208
+ const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
209
+ const relayOnToken = childCallbacks.onToken
210
+ if (relayOnToken) {
211
+ childCallbacks.onToken = (t) => {
212
+ const ev = String(t).match(/^⟦ev⟧turn\x1e(\d+)\x1e(\d+)\x1e/)
213
+ if (ev) {
214
+ entry.turn = Number(ev[1]) || 0
215
+ entry.maxTurns = Number(ev[2]) || 0
216
+ }
217
+ return relayOnToken(t)
218
+ }
219
+ }
220
+ const runner = ctx.runAgent ?? runAgent
221
+ const runOpts = {
222
+ depth: 1,
223
+ maxTurns: parent.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
224
+ signal: entry.controller.signal,
225
+ // §2.5 #78 并入:escalate 子代理输出流式(VSC subagent-escalate-async 同款豁免)。
226
+ streamOutput: true,
227
+ }
228
+ // 权限按 async 子代理同款装配:AUTO 直放行;手动档经父 _permQueue(并行子代理
229
+ // 审批不叠弹窗)——背景飞行撞门时无 handler → denied 不悬挂(D-S7 同规则)。
230
+ const childPermission = parent.autoApprove
231
+ ? async () => true
232
+ : async (name, toolArgs) => {
233
+ if (!ctx.onPermissionRequest) return false
234
+ // 残环批(2026-09-16):询问名携归属键(`escalate#<id>/<tool>`——端侧键形解析的
235
+ // 输入契约;原裸 `escalate/${tool}` 无 id ⇒ 并行条目归属歧义)
236
+ const ask = () => ctx.onPermissionRequest(`${entry.relayPrefix.slice(0, -1)}/${name}`, toolArgs)
237
+ return enqueueAsk(parent, "_permQueue", ask)
238
+ }
239
+ const report = await runWithContinue(
240
+ (childAgent, input, cbs, opts) => runner(childAgent, input, cbs, opts),
241
+ child, task,
242
+ { ...childCallbacks, onPermissionRequest: childPermission },
243
+ runOpts,
244
+ {
245
+ // 后台飞行不弹 continue 面板(D-A3 §15 例外同款):AUTO && engineering 自动
246
+ // resume——escalate 只在 normal 模式可用(engineering 拒)——恒自动拒 → partial。
247
+ askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
248
+ onDeclined: (e, output) => `escalate (${tag})${entry.effortNote} ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`,
249
+ },
250
+ )
251
+ // done: compose the post-op body (the settle classification appends the
252
+ // overlap warning / merge notes afterwards)
253
+ return `escalate (${tag})${entry.effortNote} post-op report:\n${report || (child._capturedOutput ?? "").slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
254
+ }
255
+ entry.start = () => {
256
+ flight()
257
+ .then((report) => {
258
+ // Turn-cap partial (runWithContinue auto-declined) classifies as the ERROR
259
+ // branch (design: 撞 turn cap = error — partial-merge decision applies):
260
+ // move it to entry.error so the classification + error digest wording fire.
261
+ if (String(report).includes(TURN_CAP_MARK)) entry.error = report
262
+ else entry.report = report
263
+ })
264
+ .catch((e) => {
265
+ // 运行失败/中止:错误文本落 entry.error(子代理同款——cancel 分支忽略它;
266
+ // Ctrl+I 中止的残条目由收尾消化带错误文本——不落空 "(no report)" digest)。
267
+ const child = entry.childAgent
268
+ // §20.3 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
269
+ entry.error = `escalate (${tag}) error: ${deathLine(e, entry.controller?.signal)}\nPartial output: ${(child?._capturedOutput ?? "").slice(0, 2000)}`
270
+ })
271
+ .finally(() => {
272
+ // settle 公共收尾单点(ASYNC-RESULT-CONTAINER.md D3——settleAsyncEntry):日志三连
273
+ // /cancelled 分支(出池+墓碑+⟦ev⟧stopped+提醒)/挂起分流(pending 单容器+出池——
274
+ // 统一守卫 !parentAborted——D4)/公共尾部(settleSeq/_settle/唤醒 waiter + 腾槽补位
275
+ // ——helper 尾部恒补)统一走共享 helper。族特有 hook = 三分类 merge 决策
276
+ // (classifyEscalateSettle——done/error 分类;cancelled 不经此——helper cancelled
277
+ // 分支先行)。
278
+ settleAsyncEntry(parent, entry, {
279
+ pool: parent._asyncSubagents,
280
+ ctx,
281
+ onAccounting: () => {
282
+ // Three-way settle classification (done/error/cancelled — review #4):
283
+ // done → merge-all + 重叠警告;error → 无父侧重叠才 partial merge;
284
+ // cancelled 不经此(helper cancelled 分支先行)。
285
+ void classifyEscalateSettle(parent, entry)
286
+ },
287
+ })
288
+ })
289
+ }
290
+ // ED-5(§6.21)入池键守卫:同 id 二次入池 = 覆写(静默丢报告 + status/cancel 错址)⇒ 抛错。
291
+ assertPoolKeyFree(parent._asyncSubagents, id, "escalate")
292
+ parent._asyncSubagents.set(String(id), entry)
293
+ logEvent("child:spawn", { role: "escalate", id: `escalate#${id}`, kind: "async", status: entry.status, ms: 0 })
294
+ if (entry.status === "queued") {
295
+ parent._asyncQueue.push(entry)
296
+ entry.position = parent._asyncQueue.length
297
+ refreshQueuedTokens(parent, ctx.callbacks?.onToken)
298
+ return JSON.stringify({ id: String(id), role: "escalate", status: "queued", position: entry.position })
299
+ }
300
+ entry.start()
301
+ return JSON.stringify({ id: String(id), role: "escalate", status: "running" })
302
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * goal tool: lifecycle management for long-running autonomous goals (completion contract).
3
+ * Three states: active / complete / blocked. Completion must pass a verify evidence threshold;
4
+ * blocked is only accepted after the same condition persists 3 consecutive times.
5
+ * The system injects status + budget progress + audit discipline every turn.
6
+ */
7
+ export const goalTool = {
8
+ name: "goal",
9
+ description:
10
+ "Manage a long-running autonomous goal. " +
11
+ "action='set': create or replace the goal — must have a verifiable completion criterion (a machine-checkable proof, not vague effort). " +
12
+ "action='complete': mark achieved — only after the criterion's check has actually passed. " +
13
+ "action='blocked': report an impasse (requires 'reason') — only after 3 genuine attempts. " +
14
+ "action='cancel': abandon the goal. " +
15
+ "Returns a status line — the goal set/updated/completed/blocked/cancelled confirmation, or Error: ... with the reason.",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ action: { type: "string", enum: ["set", "complete", "blocked", "cancel"], description: "Goal lifecycle action" },
20
+ objective: { type: "string", description: "What you are trying to accomplish (for 'set')" },
21
+ criteria: { type: "string", description: "How completion is PROVEN: the exact check to run, e.g. 'npm test passes', 'grep finds no TODO marker' (required for 'set')" },
22
+ reason: { type: "string", description: "The blocking condition (required for 'blocked')" },
23
+ },
24
+ required: ["action"],
25
+ },
26
+ readonly: true,
27
+ async execute(args, ctx) {
28
+ const agent = ctx.agent
29
+ if (args.action === "cancel") {
30
+ agent.goal = null
31
+ return "Goal cancelled. If the goal was blocked or impossible, explain why in your next message — the user can clarify, adjust scope, or confirm cancellation."
32
+ }
33
+ if (args.action === "set") {
34
+ if (!args.objective) return "Error: 'objective' required for 'set' action."
35
+ if (!args.criteria) {
36
+ return "Error: 'criteria' required for 'set' — a goal without a machine-checkable proof of completion is a wish, not a goal. Name the exact check (tests, command output, search result) that proves it's done."
37
+ }
38
+ agent.goal = {
39
+ objective: String(args.objective).slice(0, 500),
40
+ criteria: String(args.criteria).slice(0, 500),
41
+ setAt: Date.now(),
42
+ status: "active",
43
+ turnsUsed: 0,
44
+ _blockTally: null, // { reason, count } — consecutive count of the same blocking condition (for blocked audit)
45
+ }
46
+ return `Goal set: ${agent.goal.objective}\nDone when: ${agent.goal.criteria}\nThe system will inject goal status every turn. Completion and blocked claims are audited — see the reminders.`
47
+ }
48
+ if (!agent.goal || agent.goal.status !== "active") {
49
+ return `Error: no active goal to '${args.action}' (current: ${agent.goal?.status ?? "none"}). Set one first.`
50
+ }
51
+ if (args.action === "complete") {
52
+ // Evidence chain threshold: files were mutated this run without verify — refuse completion (aligns with completion guard)
53
+ if (agent._mutatedThisRun && !agent._verifiedThisRun) {
54
+ return "Error: files were modified but verify has not run. Run the check your criteria names AND the verify tool before marking the goal complete — false completion is the worst outcome of autonomous work."
55
+ }
56
+
57
+ // Independent judge: verify the goal was actually achieved
58
+ // Only applies when the agent is at depth 0 (not a subagent) and has history to review
59
+ if (ctx.depth === 0 && agent.history.length > 2) {
60
+ try {
61
+ // Extract recent activity: last 4 assistant messages (summarizing what was done)
62
+ const recent = agent.history.filter(m => m.role === "assistant").slice(-4)
63
+ const activity = recent.map(m => (m.content ?? "").slice(0, 500)).join("\n---\n")
64
+ const { chat } = await import("../provider/index.mjs")
65
+ const judgeRes = await chat(agent.provider, {
66
+ messages: [{
67
+ role: "user",
68
+ content: `You are an independent goal judge. Evaluate whether this goal has been achieved based on the agent's activity.
69
+
70
+ Goal: ${agent.goal.objective}
71
+ Success criteria: ${agent.goal.criteria}
72
+
73
+ Recent agent activity:
74
+ ${activity || "(no activity recorded)"}
75
+
76
+ Has this goal been achieved? Answer ONLY "YES" or "NO" followed by a one-sentence reason.`,
77
+ }],
78
+ tools: [],
79
+ signal: AbortSignal.timeout(10_000),
80
+ // §18.6 D-TR4/D-TR6(2026-09-04 fix round1):goal 独立评审调用经 chat()
81
+ // 唯一采集点——补轨迹元数据 + traces 开关透传(agent.config.traces.enabled
82
+ // ——关=不落盘必须全覆盖——与 agent.mjs/context.mjs 同模式)
83
+ logCtx: {
84
+ stage: "goal", kind: "goal",
85
+ role: agent._role ?? null, depth: ctx.depth,
86
+ session: agent._sessionStart ?? null, cwd: agent.cwd,
87
+ traces: agent.config?.traces?.enabled !== false,
88
+ },
89
+ })
90
+ const verdict = (judgeRes.content ?? "").trim()
91
+ if (verdict.toUpperCase().startsWith("NO")) {
92
+ return `Goal NOT complete (judge says NO): ${verdict.slice(2).trim()}\n\nContinue working or report blocked if this is a true impasse.`
93
+ }
94
+ if (!verdict.toUpperCase().startsWith("YES")) {
95
+ return `Goal completion unverified — judge response ambiguous: "${verdict.slice(0, 200)}". Re-check your criteria and try again with clear evidence.`
96
+ }
97
+ } catch {
98
+ // Judge unavailable — allow completion but note it
99
+ }
100
+ }
101
+
102
+ agent.goal.status = "complete"
103
+ return `Goal verified complete ✓: ${agent.goal.objective}\nIn your next message, summarize the evidence (what check ran, what it showed) — the user should be able to audit this claim.`
104
+ }
105
+ if (args.action === "blocked") {
106
+ if (!args.reason) return "Error: 'reason' required for 'blocked' action."
107
+ // Blocked audit: same condition must appear 3 consecutive times (only counts as real blocking if different approaches still hit the same wall)
108
+ const tally = agent.goal._blockTally
109
+ const count = tally?.reason === args.reason ? tally.count + 1 : 1
110
+ agent.goal._blockTally = { reason: args.reason, count }
111
+ if (count < 3) {
112
+ return `Blocked not accepted yet (${count}/3 for this condition). Try a genuinely different approach first; report blocked only if the same condition stops you ${3 - count} more time(s).`
113
+ }
114
+ agent.goal.status = "blocked"
115
+ return `Goal marked blocked after 3 attempts: ${args.reason}\nExplain the blocker to the user in your next message — what you tried, and what you need (clarification, permission, a decision).`
116
+ }
117
+ return `Error: unknown action '${args.action}'.`
118
+ },
119
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * panel-blocks.mjs — §19.6 D-P1 面板块列表(读时现算,纯函数)。
3
+ *
4
+ * 来源 = `thincoder-cli/src/tui/subagent-freeze.mjs` 的 `computePanelBlocks`——**逐字随迁**
5
+ * (CORE-UNIFICATION §2.5 #159:`panel` 动作以 CLI 为准;#99:VSC 无 panel = **有意端差**
6
+ * ⇒ 其载荷面在 VSC 不存在,未挂载(`state` 缺省)即返 `null` = 无面板,消费面降级——
7
+ * 这正是「④ 段以注入剔除」在核内的形态:核只读注入进来的 state,不判端名)。
8
+ *
9
+ * 现算面板块列表(state.subTasks 活值纯推导——无时点快照语义)。
10
+ * status 三态映射:done+awaitingDigest → "awaitingDigest";done → "done";
11
+ * queued(waiting/等位未启动)→ "queued";其余 → "running"(§20 D-SD3b queued 态入镜口径保留)。
12
+ */
13
+ export function computePanelBlocks(state) {
14
+ const subs = state?.subTasks
15
+ if (!subs) return null // 未挂载(headless/mock 无 TUI state)——无面板
16
+ const blocks = []
17
+ for (const sub of Object.values(subs)) {
18
+ // §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(视图与面板
19
+ // 所见一致——action:'panel' 视图 + freeze 门控读此态)。
20
+ const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
21
+ blocks.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
22
+ }
23
+ return blocks
24
+ }