@thincoder/core 0.9.2 → 0.9.3

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 (52) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -0
  3. package/agent/completion.mjs +3 -1
  4. package/agent/family-tools.mjs +12 -9
  5. package/agent/helpers.mjs +11 -2
  6. package/agent/run-stages.mjs +27 -5
  7. package/agent/setup.mjs +6 -0
  8. package/agent/write-gate.mjs +5 -5
  9. package/agent-tools/advisor-async.mjs +4 -4
  10. package/agent-tools/advisor.mjs +1 -1
  11. package/agent-tools/async-discard.mjs +1 -1
  12. package/agent-tools/audit-block.mjs +106 -0
  13. package/agent-tools/batch-lifecycle.mjs +72 -16
  14. package/agent-tools/batch-skeleton.mjs +69 -7
  15. package/agent-tools/batch.mjs +19 -6
  16. package/agent-tools/context.mjs +174 -0
  17. package/agent-tools/goal.mjs +7 -0
  18. package/agent-tools/parent-channel.mjs +2 -2
  19. package/agent-tools/plan.mjs +6 -6
  20. package/agent-tools/read-history.mjs +122 -24
  21. package/agent-tools/settings.mjs +4 -2
  22. package/agent-tools/subagent-async.mjs +3 -3
  23. package/agent-tools/subagent-spawn.mjs +29 -101
  24. package/agent-tools/task.mjs +11 -0
  25. package/agent-tools.mjs +4 -1
  26. package/agent.mjs +10 -4
  27. package/config.mjs +1 -1
  28. package/context.mjs +66 -121
  29. package/fts-text.mjs +41 -0
  30. package/memory/core.mjs +4 -18
  31. package/memory/schema.mjs +4 -11
  32. package/package.json +5 -1
  33. package/prompts/common.md +2 -2
  34. package/prompts/discipline-engineering.md +17 -2
  35. package/prompts/persona-engineering.md +1 -1
  36. package/session-gc.mjs +11 -0
  37. package/session-index-build.mjs +298 -0
  38. package/session-index-cmd.mjs +61 -0
  39. package/session-index-pass.mjs +95 -0
  40. package/session-index-query.mjs +102 -0
  41. package/session-index.mjs +285 -0
  42. package/session-slots-manifest.mjs +19 -0
  43. package/token-window.mjs +188 -0
  44. package/tools/bash.mjs +4 -15
  45. package/tools/execute.mjs +5 -13
  46. package/tools/git-checkpoint.mjs +1 -1
  47. package/tools/git-ext.mjs +23 -20
  48. package/tools/git-run.mjs +141 -0
  49. package/tools/git.mjs +42 -36
  50. package/tools/index.mjs +3 -1
  51. package/tools/process-tree.mjs +20 -0
  52. package/tools/shared.mjs +8 -4
@@ -1,9 +1,10 @@
1
1
  /**
2
- * agent-tools/read-history.mjs — read_history tool (SESSION.md §6.9 + §6.13 R19 cross-session).
2
+ * agent-tools/read-history.mjs — read_history tool (SESSION.md §6.9 + §6.13 R19 cross-session + §6.19).
3
3
  *
4
4
  * Query message history — THIS session by default, any session on disk with `path`
5
5
  * (SESSION.md §6.13 R19): an explicit session file path deep-queries that file's
6
- * history line; "cwd:<dir>" discovers the sessions stored for that directory.
6
+ * history line; "cwd:<dir>" discovers the sessions stored for that directory; "all"
7
+ * searches every indexed session across projects (§6.19 D-SE47).
7
8
  *
8
9
  * Default (no path) — THIS session's full human-readable record (record store when bound
9
10
  * — disk-backed, SESSION.md §6.14; agent._fullHistory memory fallback otherwise:
@@ -19,22 +20,25 @@
19
20
  * Returns a JSON array in chronological order. Every message without ts comes
20
21
  * back as ts:null and can never match a time window (legacy sessions). Content
21
22
  * is truncated to ~500 chars with an explicit marker — full text lives in the
22
- * session file. assistant tool_calls are summarized to a name list (arguments
23
- * never expanded).
23
+ * session file. assistant tool_calls come back as [{name, arguments}] with the
24
+ * stored argument string capped at 300 chars (matches the stored precision).
24
25
  *
25
- * Cross-session (SESSION.md §6.13 D-R19a): path = a session file path (absolute, or
26
- * relative to the project cwd) → read that file's history line and apply the SAME
27
- * filter surface; path = "cwd:<dir>" list every slot stored for that directory
28
- * (slot number + full file path + title/message count/updatedAt no dead-slot
29
- * filtering, v1 decision). Single-file retrieval is guarded by a line-scan cap
30
- * (READ_HISTORY_SCAN_MAX an oversized file is refused before it is read whole)
31
- * plus a message-count cap (READ_HISTORY_MAX_MESSAGES = 50,000 L24 双保险第二道).
26
+ * Cross-session (SESSION.md §6.13 D-R19a + §6.19 D-SE47 索引优先 + 主存回落):
27
+ * path = a session file path (absolute, or relative to the project cwd) → when the derived
28
+ * session index covers that session the query runs as SQL (no line-scan / message-count
29
+ * guards oversized sessions answer); otherwise the pre-existing JSON path with BOTH
30
+ * guards, verbatim (not a sessions-tree file / index unavailable / no such row). path =
31
+ * "cwd:<dir>" list every slot stored for that directory (slot number + full file path +
32
+ * title/message count/updatedAt no dead-slot filtering, v1 decision). path = "all" →
33
+ * cross-session search over every INDEXED session (keyword = FTS words / phrases; rows carry
34
+ * session.file as the follow-up anchor).
35
+ * The index face enters load-time via dynamic import (W8 contract②: node:sqlite must never
36
+ * join the assembly-time static closure) — a host without node:sqlite falls back to the file
37
+ * path silently, and an index write failure never endangers the session files (zero authority).
32
38
  *
33
39
  * readonly: true — planMode pass / no permission ask. Registered depth-0 only:
34
40
  * subagents get their own throwaway history, so querying "the session" from a
35
41
  * child would be semantically confusing (SESSION.md §6.9 refinement 1 + §6.13 T-R19.4).
36
- * §6.13 R19 extension mirrored per SESSION.md §6.13 — double-end isomorphic, no
37
- * cross-end byte test (thincoder-vscode/src/agent-tools/read-history.mjs).
38
42
  */
39
43
 
40
44
  import { openSync, readSync, closeSync, readFileSync, existsSync, statSync } from "node:fs"
@@ -44,7 +48,10 @@ import { listSlots, slotPath } from "../session-slots.mjs"
44
48
  const DEFAULT_LIMIT = 50
45
49
  const MAX_LIMIT = 200
46
50
  const CONTENT_CAP = 500
51
+ const ARG_CAP = 300
47
52
  const VALID_ROLES = new Set(["user", "assistant", "tool"])
53
+ /** 跨会话检索字面量(§6.19 D-SE47——全部**已索引**会话)。 */
54
+ const ALL_PATH = "all"
48
55
 
49
56
  /** 单槽检索行扫护栏(SESSION.md §6.13 D-R19a——评审 #3 定稿:超限不再读全文,返回定稿错误文案)。 */
50
57
  export const READ_HISTORY_SCAN_MAX = 200_000
@@ -89,6 +96,19 @@ function toolCallName(tc) {
89
96
  return tc?.function?.name ?? tc?.name ?? ""
90
97
  }
91
98
 
99
+ /** Assistant tool_calls output shape: [{name, arguments}] — `arguments` = the STORED string
100
+ * (SESSION.md §6.19 D-SE47: 上限 300 字符 = 存储面同值——不虚构超出存储的精度). */
101
+ function toolCallEntries(tcs) {
102
+ return tcs.map((tc) => {
103
+ const name = toolCallName(tc)
104
+ const args = typeof tc?.function?.arguments === "string" ? tc.function.arguments
105
+ : typeof tc?.arguments === "string" ? tc.arguments : null
106
+ const out = { name }
107
+ if (args !== null) out.arguments = args.length <= ARG_CAP ? args : args.slice(0, ARG_CAP) + "…"
108
+ return out
109
+ }).filter((t) => t.name)
110
+ }
111
+
92
112
  /** Parse a ts window boundary (epoch ms number; numeric strings tolerated). Returns the number or an error string. */
93
113
  function parseTs(value, label) {
94
114
  if (value === undefined || value === null) return null
@@ -107,7 +127,8 @@ function toEntry(m) {
107
127
  if (m.tool_call_id !== undefined) entry.tool_call_id = m.tool_call_id
108
128
  entry.content = truncateContent(messageText(m))
109
129
  if (Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
110
- entry.tool_calls = m.tool_calls.map(toolCallName).filter(Boolean)
130
+ const calls = toolCallEntries(m.tool_calls)
131
+ if (calls.length > 0) entry.tool_calls = calls
111
132
  }
112
133
  return entry
113
134
  }
@@ -168,7 +189,7 @@ function exceedsScanMax(file) {
168
189
  }
169
190
  }
170
191
 
171
- /** Cross-session deep query: one session file, same filter surface (§6.13 D-R19a). */
192
+ /** 跨会话深查(回落面):单个槽文件,同 filter 面(§6.13 D-R19a——两道护栏逐字保留)。 */
172
193
  function querySessionFile(pathArg, { role, kwRe, tool, since, until, direction, limit }, baseCwd) {
173
194
  const file = isAbsolute(pathArg) ? pathArg : resolve(baseCwd ?? process.cwd(), pathArg)
174
195
  if (!existsSync(file)) return `Error: session file not found: ${file}`
@@ -219,6 +240,74 @@ function discoverCwd(raw, baseCwd) {
219
240
  return `Session slots for cwd: ${dir} (newest first):\n${lines.join("\n")}`
220
241
  }
221
242
 
243
+ // ── 索引面(§6.19 D-SE47:索引优先 + 主存回落;动态装载——W8 契约②)─────────────────
244
+
245
+ /** 索引面模块(动态 import:node:sqlite 不进装配期静态闭包);宿主不具备(无 node:sqlite / 装配失败)⇒ null。 */
246
+ async function loadIndexFace() {
247
+ try {
248
+ const [idx, query, build] = await Promise.all([
249
+ import("../session-index.mjs"),
250
+ import("../session-index-query.mjs"),
251
+ import("../session-index-build.mjs"),
252
+ ])
253
+ return { idx, query, build }
254
+ } catch { return null }
255
+ }
256
+
257
+ /** 索引行 → 输出条目(与 JSON 面 `toEntry` 同形——两条路径结果逐条相等,T-11 等价面)。 */
258
+ function indexEntry(r) {
259
+ const entry = { ts: typeof r.ts === "number" ? r.ts : null, role: r.role ?? null }
260
+ if (r.name !== undefined && r.name !== null) entry.name = r.name
261
+ if (r.tool_call_id !== undefined && r.tool_call_id !== null) entry.tool_call_id = r.tool_call_id
262
+ entry.content = truncateContent(r.content ?? "")
263
+ if (Array.isArray(r.tool_calls) && r.tool_calls.length > 0) entry.tool_calls = r.tool_calls
264
+ return entry
265
+ }
266
+
267
+ /** 单会话:索引命中 ⇒ SQL 检索结果;未命中(非 sessions 树 / 库不可用 / 库内无此会话)⇒ `null` = 回落。
268
+ * 懒保证射程 = 有段档(`src=sidecar`——`session-index-build` 内判;`src=json` 档免 ensure)。 */
269
+ async function queryIndexedSession(file, filters) {
270
+ const face = await loadIndexFace()
271
+ if (!face) return null
272
+ let db = null
273
+ try {
274
+ db = face.idx.openSessionIndex()
275
+ if (!db) return null
276
+ try { face.build.ensureSessionIndexed(db, file) } catch { /* 懒保证失败 ⇒ 用现有行判命中(回落路径兜住) */ }
277
+ const rows = face.query.querySessionRows(db, file, filters)
278
+ return rows === null ? null : JSON.stringify(rows.map(indexEntry))
279
+ } catch { return null } finally { if (db) db.close() }
280
+ }
281
+
282
+ /** 当前会话槽文件(`all` 面懒保证目标——触发点①:`SESSION.md` §6.19「查询前对目标会话(`path=<文件>`)/
283
+ * 当前会话(`all`)`ensure`」;射程仍 = 有段档 ⇒ `src=json` 档零动作;无绑定 / 无槽 ⇒ null)。 */
284
+ function currentSessionFile(ctx) {
285
+ const cwd = ctx.agent?.cwd
286
+ const slot = ctx.agent?._slot
287
+ if (!cwd || !Number.isInteger(slot) || slot < 1) return null
288
+ try { return slotPath(cwd, slot) } catch { return null }
289
+ }
290
+
291
+ /** 跨会话检索(`path:"all"`——全部已索引会话):行携 `session.file` 回查锚 + `idx`。 */
292
+ async function queryAllSessions(filters, current = null) {
293
+ const face = await loadIndexFace()
294
+ if (!face) return "Error: session index unavailable — this host has no node:sqlite; query one session with path=<file> instead"
295
+ let db = null
296
+ try {
297
+ db = face.idx.openSessionIndex()
298
+ if (!db) return "Error: session index unavailable — the derived index could not be opened; query one session with path=<file> instead"
299
+ if (current !== null) { try { face.build.ensureSessionIndexed(db, current) } catch { /* 懒保证失败 ⇒ 用现有行(回落面零影响) */ } }
300
+ const rows = face.query.queryAllRows(db, filters)
301
+ return JSON.stringify(rows.map((r) => ({
302
+ session: { slot: r.slot, cwd: r.cwd ?? null, file: r.file, title: r.title ?? null },
303
+ idx: r.idx,
304
+ ...indexEntry(r),
305
+ })))
306
+ } catch (e) {
307
+ return `Error: session index query failed — ${e.message}`
308
+ } finally { if (db) db.close() }
309
+ }
310
+
222
311
  export const readHistoryTool = {
223
312
  name: "read_history",
224
313
  description:
@@ -228,27 +317,32 @@ export const readHistoryTool = {
228
317
  "Filters combine with AND: role / keyword (case-insensitive substring of message text) / " +
229
318
  "tool (tool result messages by name AND the assistant messages that declared the call — pair with tool_call_id / ts for timing) / " +
230
319
  "since-until (epoch ms time window; only messages with ts can match) / limit (default 50, clamped to 200) / direction (which end of the matches to take). " +
231
- "Returns a JSON array in chronological order: [{ts, role, name?, tool_call_id?, content (≈500 chars, truncated marker), tool_calls (names only)}]. " +
320
+ "Returns a JSON array in chronological order: [{ts, role, name?, tool_call_id?, content (≈500 chars, truncated marker), tool_calls ([{name, arguments}] — arguments capped at 300 chars)}]. " +
232
321
  "Messages without ts return ts:null. Content is truncated — the full text is in the session file. " +
233
322
  "Cross-session (path, optional): a session file path deep-queries THAT session's history with the same filters " +
234
- "(relative paths resolve against the project cwd); \"cwd:<dir>\" lists every session slot stored for that directory " +
235
- "one line per slot: slot number + full session file path + title + message count + updatedAt; copy a listed file path into path= to deep-query it. " +
236
- "A session file over 50,000 messages or 200,000 lines is refused (\"session too large\") instead of being read whole.\n" +
323
+ "(relative paths resolve against the project cwd) answered from the derived session index when it covers that session, " +
324
+ "otherwise from the session file itself (a file over 50,000 messages or 200,000 lines is refused as \"session too large\"). " +
325
+ "\"cwd:<dir>\" lists every session slot stored for that directory one line per slot: slot number + full session file path + " +
326
+ "title + message count + updatedAt; copy a listed file path into path= to deep-query it. " +
327
+ "\"all\" searches EVERY INDEXED session at once (cross-project; keyword = FTS words / phrases; each row carries session.file for the follow-up deep query) — " +
328
+ "`all` covers the derived index only: sessions never indexed yet are invisible there (query them with path=<file>, or index everything first with the CLI `thincoder session index --rebuild`).\n" +
237
329
  SEARCH_FAMILY_GUIDE,
238
330
  parameters: {
239
331
  type: "object",
240
332
  properties: {
241
333
  role: { type: "string", enum: ["user", "assistant", "tool"], description: "Only messages with this role." },
242
- keyword: { type: "string", description: "Case-insensitive substring of the message text (multimodal messages match on their text parts)." },
334
+ keyword: { type: "string", description: "Case-insensitive substring of the message text (multimodal messages match on their text parts); with path=\"all\" it is matched as FTS words / phrases over the indexed text." },
243
335
  tool: { type: "string", description: "Only messages for this tool: role=tool messages with name=tool, plus assistant messages that declared a call to it." },
244
336
  since: { type: "integer", description: "Earliest ts to match, epoch ms, INCLUSIVE. Messages without ts never match a time window." },
245
337
  until: { type: "integer", description: "Latest ts to match, epoch ms, INCLUSIVE. since > until yields an empty result." },
246
338
  limit: { type: "integer", description: "Maximum messages to return (default 50; larger values are clamped to 200)." },
247
339
  direction: { type: "string", enum: ["oldest", "newest"], description: "Take the limit window from the oldest or newest end of the matched set (default newest)." },
248
- path: { type: "string", description: "Optional — query another session instead of this one: a session file path (as listed by a \"cwd:<dir>\" call) deep-queries that session; \"cwd:<dir>\" lists that directory's session slots (slot number + full file path + title + message count + updatedAt)." },
340
+ path: { type: "string", description: "Optional — query another session instead of this one: a session file path (as listed by a \"cwd:<dir>\" call) deep-queries that session; \"cwd:<dir>\" lists that directory's session slots (slot number + full file path + title + message count + updatedAt); \"all\" searches every indexed session across projects (FTS words / phrases; rows carry session.file)." },
249
341
  },
250
342
  },
251
343
  readonly: true,
344
+ // 返回类型 = `string`(本会话缺省 / `cwd:` 发现面——与修前逐字同形)∥ `Promise<string>`(索引面:
345
+ // `path=<文件>` 索引优先 / `path:"all"`——需动态装载核索引面)。装配面一律 `await`(dispatch.js 同款)。
252
346
  execute(args, ctx) {
253
347
  const a = args ?? {}
254
348
  if (a.path !== undefined && (typeof a.path !== "string" || a.path.trim().length === 0)) {
@@ -284,9 +378,13 @@ export const readHistoryTool = {
284
378
 
285
379
  const pathArg = typeof a.path === "string" && a.path.trim().length > 0 ? a.path.trim() : null
286
380
  if (pathArg !== null) {
287
- return pathArg.startsWith("cwd:")
288
- ? discoverCwd(pathArg.slice("cwd:".length), baseCwd)
289
- : querySessionFile(pathArg, { role, kwRe, tool, since, until, direction, limit }, baseCwd)
381
+ if (pathArg.startsWith("cwd:")) return discoverCwd(pathArg.slice("cwd:".length), baseCwd)
382
+ const filters = { keyword, role, tool, since, until, limit, direction }
383
+ if (pathArg === ALL_PATH) return queryAllSessions(filters, currentSessionFile(ctx))
384
+ const file = isAbsolute(pathArg) ? pathArg : resolve(baseCwd, pathArg)
385
+ // 索引优先(命中 ⇒ SQL 检索);未命中 ⇒ 既有 JSON 路径 + 两道护栏(逐字保留——回落面)
386
+ return queryIndexedSession(file, filters)
387
+ .then((indexed) => (indexed !== null ? indexed : querySessionFile(pathArg, { role, kwRe, tool, since, until, direction, limit }, baseCwd)))
290
388
  }
291
389
 
292
390
  // 本会话(无 path):绑定记录存储 → 方向流式迭代(磁盘为准——全量可见、内存窗口外
@@ -18,7 +18,8 @@ const SENSITIVE_SEGMENT = /(^|[._-])(api[_-]?key|key|token|secret|password|autho
18
18
  const SENSITIVE_FAMILY = /(^|[._-])(headers|env)($|[._-])/i
19
19
  const MASKED = "••••(masked)"
20
20
 
21
- function isSensitiveKey(path) {
21
+ /** #58(hygiene-sweep 批):谓词/标记导出——CLI `/mcp` 表单现值脱敏同源(`cmd-mcp-form.mjs`)。 */
22
+ export function isSensitiveKey(path) {
22
23
  return SENSITIVE_SEGMENT.test(path) || SENSITIVE_FAMILY.test(path)
23
24
  }
24
25
 
@@ -184,7 +185,8 @@ function flatten(obj, prefix = "", out = []) {
184
185
 
185
186
  /** 行格式化:`key = value (type)`——敏感键值遮罩 */
186
187
  function formatLine({ path, value }) {
187
- const shown = isSensitiveKey(path) ? MASKED : value
188
+ // #58(hygiene-sweep 批):对象值渲染收正(原 `[object Object]`——JSON 化;敏感键仍恒遮)
189
+ const shown = isSensitiveKey(path) ? MASKED : value !== null && typeof value === "object" ? JSON.stringify(value) : value
188
190
  return `${path} = ${shown} (${Array.isArray(value) ? "array" : typeof value})`
189
191
  }
190
192
 
@@ -43,7 +43,7 @@ export function enqueueAsk(owner, key, ask) {
43
43
  return chain
44
44
  }
45
45
 
46
- // Async pool limits per role domain (AGENT-LOOP-SUBAGENT.md §6.10 — R14, 2026-09-06):
46
+ // Async pool limits per role domain (AGENT-LOOP-ASYNC-POOL.md §6.10 — R14, 2026-09-06):
47
47
  // the old single cap (ASYNC_SUBAGENT_LIMIT = 4, §15 D-A4) evolved into two
48
48
  // independent pools — eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
49
49
  // 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
@@ -57,8 +57,8 @@ export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
57
57
 
58
58
  /**
59
59
  * Role → pool domain (single source of truth — §11.1 修正 #8): the CLI role
60
- * enum/assembly table is subagent.mjs's ROLES = { explore, plan, coder, eng-coder }
61
- * (mode-filtered: normal → explore/plan/coder, engineering → explore/plan/eng-coder).
60
+ * whitelist is subagent.mjs's ROLES = { explore, plan, coder, eng-coder, eng-designer }
61
+ * (mode-filtered: normal → explore/plan/coder, engineering → explore/eng-designer/eng-coder).
62
62
  * eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
63
63
  * → other pool. escalate spawns its expert internally (role "coder" — other pool).
64
64
  */
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * subagent-spawn.mjs — spawn 路径装配(2026-09-05 module-split:subagent.mjs 726
3
- * > 500 硬限——spawn 前置 helpers(summarizeEngTaskBook/effectiveSubagentModel/
4
- * resolveDesignSlot)+ §20 调度参数准入(prepareScheduling)+ child 装配
3
+ * > 500 硬限——spawn 前置 helpers(effectiveSubagentModel/resolveDesignSlot——审计块
4
+ * 构造器 2026-09-22 起另立 ./audit-block.mjs)+ §20 调度参数准入(prepareScheduling)+ child 装配
5
5
  * (buildSpawnChild)verbatim 迁入(仅闭包变量参数化),语义零变;executeAsyncSpawn
6
6
  * 另在 subagent-run.mjs。subagent.mjs execute 经本文件 import 调用。
7
7
  * 2026-09-07:executeConsumeDesignAction 消费执行器(token 链终消费制——与 spawn 侧
8
8
  * slot 族同域——ENGINEERING-MODE.md §2.6 F1——removeDesignTokenSlot 自 token-ttl.mjs)。
9
+ * 2026-09-22(台账 #23 · AGENT-LOOP-SUBAGENT.md §6.26):审计块构造器(summarizeEngTaskBook
10
+ * + buildAuditBlock)外提 `./audit-block.mjs`;spawn 级固定机制性指令改走 system 固块
11
+ * (`child._spawnSystemBlock` 单点绑定——消费点 = 核 `prepareRun`),`input` 只留任务书。
9
12
  */
10
13
 
11
14
  import { resolve } from "node:path"
@@ -27,57 +30,9 @@ import {
27
30
  // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):任务书六强制字段校验本体落
28
31
  // spawn-gates.mjs(纯谓词零依赖)——本处只加 import 调用。
29
32
  import { validateTaskBookFields } from "./spawn-gates.mjs"
30
-
31
- /**
32
- * §18.7 D-TS5 (A2): mechanically summarize the parent spawn task book for the
33
- * audit spawn — the three audit-relevant elements VERBATIM (design doc paths /
34
- * affected-file list / acceptance criteria); verbose context/background is
35
- * dropped (the auditor can read the design docs themselves — they stay
36
- * available outside this input). Independence preserved: the input is
37
- * _engTaskInput (mechanically kept by the parent spawn) — never the
38
- * eng-coder's self-report. Sections are located by header marker, prioritizing
39
- * header lines (structured task books: "## 文件清单 …") and falling back to
40
- * inline markers (flat one-line task books); a section runs to the next header
41
- * of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
42
- * Marker not found → the section is reported as missing (never fabricate).
43
- */
44
- function summarizeEngTaskBook(taskInput) {
45
- if (!taskInput) return "(unavailable)"
46
- const SECTIONS = [
47
- { name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/] },
48
- { name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/] },
49
- { name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
50
- ]
51
- const lines = taskInput.split("\n")
52
- const headerLevel = (l) => {
53
- const m = l.match(/^\s*(#{1,6})\s/)
54
- return m ? m[1].length : 0
55
- }
56
- const headerIdx = lines.map((l, i) => (headerLevel(l) > 0 ? i : -1)).filter((i) => i >= 0)
57
- const boundsFor = (from, level) => {
58
- for (const j of headerIdx) {
59
- if (j > from && (level === 0 || headerLevel(lines[j]) <= level)) return j
60
- }
61
- return lines.length
62
- }
63
- const out = []
64
- for (const { name, markers } of SECTIONS) {
65
- let from = -1
66
- let level = 0
67
- for (const i of headerIdx) {
68
- if (markers.some((m) => m.test(lines[i]))) { from = i; level = headerLevel(lines[i]); break }
69
- }
70
- if (from === -1) {
71
- for (let i = 0; i < lines.length; i++) {
72
- if (markers.some((m) => m.test(lines[i]))) { from = i; level = 0; break }
73
- }
74
- }
75
- if (from === -1) { out.push(`${name}: (not found in the parent task book)`); continue }
76
- const body = lines.slice(from, boundsFor(from, level)).join("\n").trim()
77
- out.push(body || `${name}: (empty section)`)
78
- }
79
- return out.join("\n\n")
80
- }
33
+ // 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-3):审计块构造器外提(`summarizeEngTaskBook`
34
+ // + `buildAuditBlock` 纯函数)——spawn 档只做「收集 + 绑定」;块文本逐字搬移(D23-4)。
35
+ import { buildAuditBlock } from "./audit-block.mjs"
81
36
 
82
37
  /**
83
38
  * Effective subagent model override for a role (CLI parity shared with VS Code):
@@ -238,8 +193,8 @@ export function prepareScheduling(parent, filesRaw, dependsRaw, wantAsync) {
238
193
  /**
239
194
  * §20 准入通过后的 child 装配(2026-09-05 module-split——自 execute 参数化提取,
240
195
  * 原 318-488 段 verbatim——语义零变)。副作用保留:relay 取号(sync 支 allocRelay——
241
- * `[model]` 出生声明归 SYNC-CANCEL 单点,#133)/ 子代理 _logId / _engTaskInput 携带
242
- * 全部在此发生。返回阻塞/异步两路径共用的
196
+ * `[model]` 出生声明归 SYNC-CANCEL 单点,#133)/ 子代理 _logId / _engTaskInput 携带 /
197
+ * _spawnSystemBlock 绑定(台账 #23——spawn 固块单点)全部在此发生。返回阻塞/异步两路径共用的
243
198
  * { child, input, childOpts, childRunOpts, relayPrefix, childProvider }。
244
199
  */
245
200
  export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, dependsOn, engAuditAttempt) {
@@ -386,60 +341,33 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
386
341
  // git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
387
342
  // setup.mjs depth===0——D-AG7 范围边界)。
388
343
  let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
389
- // §2.11 1 点(FR16 载体 + FR17 铁律 #5 的机械面):批次档**绝对路径**随任务输入
390
- // 下发给工程角色(eng-coder 实现 / eng-designer 写稿)——追加一行,任务文本本身不动
391
- // (批次档 §2 才是任务书本体;本行让子代理"拿到本档路径")。行文不含
392
- // summarizeEngTaskBook 的三组段 marker(Docs involved / Files list / Acceptance
393
- // criteria)——段匹配不受影响。仅工程角色注入。
394
- if (engineeringRole) input += `\n\nBatch record (batchDoc): ${batchDocAbs}`
344
+ // 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-1):**spawn 级固定机制性指令改走 system 固块**——
345
+ // 两处(批次档路径行 + 审计模板块)原随 user 首条下发,压缩把首条并入摘要即丢(长任务中后期
346
+ // 既看不到批次档路径、也看不到审计模板 过度勘察的真病根);固块值在 child 生命周期内恒定
347
+ // run system 前缀逐字节稳定(前缀缓存不破),且不在 `agent.history` 压缩结构上吞不掉。
348
+ // 收集序 = 原拼接序(批次档行 → 审计块);空数组 ⇒ 不绑定(depth-0 / explore / plan / consult
349
+ // system 逐字节同改前——§6.26 缓存契约兼容证明)。
350
+ const spawnBlocks = []
351
+ // §2.11 第 1 点(FR16 载体 + FR17 铁律 #5 的机械面):批次档**绝对路径**下发给工程角色
352
+ // (eng-coder 实现 / eng-designer 写稿)——任务文本本身不动(批次档 §2 才是任务书本体;
353
+ // 本行让子代理"拿到本档路径")。行文不含 summarizeEngTaskBook 的三组段 marker
354
+ // (Docs involved / Files list / Acceptance criteria)——段匹配不受影响。仅工程角色注入。
355
+ if (engineeringRole) spawnBlocks.push(`Batch record (batchDoc): ${batchDocAbs}`)
395
356
  // §18 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
396
357
  // the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
397
358
  // parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /
398
359
  // affected-file list / acceptance criteria verbatim, verbose context dropped)
399
360
  // — ∪ the mechanically tracked _touchedFiles — NEVER the eng-coder's
400
361
  // self-written list: a self-report could omit exactly the out-of-scope file
401
- // the audit must catch.
402
- if (engAuditAttempt !== null) {
403
- const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
404
- input += `\n\n[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
405
- // §18.7 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
406
- // 不再靠模型自悟;范围限制是 §18.5 D-AG3 声明(下方 Zero-git scope authority)
407
- // 的同源一句指注,不重复声明。
408
- `[Audit instructions — mechanical template:]\n` +
409
- `You are auditing an eng-coder delivery against its approved design — audit for EXACTLY these four deviation categories:\n` +
410
- `- PARTIAL: an acceptance criterion implemented partially or not at all;\n` +
411
- `- SILENT-SIMPLIFICATION: a "simpler approximation" of a specified behavior substituted for the spec;\n` +
412
- `- DOC-DRIFT: code changed without the owning design-doc section (module map / affected-files table) updated in the same delivery;\n` +
413
- `- OUT-OF-LIST: changes outside the approved file list.\n` +
414
- `Audit scope = _touchedFiles above UNION the files confirmed by the parent task book (single source — the Zero-git scope authority note below; NOT a second copy): ` +
415
- `workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
416
- `Scope discipline (F-TS6 A1): read ONLY the audited files and the design-doc sections relevant to this delivery — do NOT re-read whole documents.\n` +
417
- `Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
418
- // §18.7 D-TS5 A2:任务书从全量 verbatim 改机械摘要块(三要素逐字——排除冗长上下文)。
419
- `[Parent spawn task book — mechanical summary: design docs + affected-file list + acceptance criteria verbatim; verbose context/background dropped — the design docs are still available for reading outside this input:]\n` +
420
- `${summarizeEngTaskBook(ctx.agent._engTaskInput)}\n` +
421
- `Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
422
- // §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
423
- // git 上下文——§18.5 全角色零 git);_touchedFiles 为审计范围;工作区未列于
424
- // _touchedFiles 的改动与本任务无关,不作超清单依据(VS Code auditTaskBook 同款措辞)。
425
- "Zero-git scope authority: this audit task receives NO git context — nothing is injected. " +
426
- "The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
427
- "Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
428
- // §18.13 D-A1.2:审计预算句——A1 指令模板 + A2 摘要块之后、A3 报告模板之前(定序——评审 #7)。
429
- // 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
430
- // 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
431
- `\n[Audit budget — mechanical]: read ONLY the touched files listed above and the design-doc sections the parent task book names (affected-files table, acceptance criteria, status line). Do NOT read whole documents. Budget = 10 tool rounds max — if you cannot conclude within it, report PROBLEM (inconclusive) rather than continuing to explore.\n` +
432
- // §18.7 D-TS6 A3:审计输出报告格式模板(三态——字段化行——不让模型自由发挥)。
433
- `\n[Audit report format — mechanical template:]\n` +
434
- `Report EXACTLY one of three states:\n` +
435
- `- CLEAN — no deviation across the four categories: reply the line "Four deviation categories: none found." (四类偏差均未发现);\n` +
436
- `- DEVIATIONS — one row per deviation, every row fieldized: | category | file:line | design reference | severity | evidence |;\n` +
437
- `- PROBLEM — the audit itself could not run / inconclusive: state what blocked it.\n`
438
- }
362
+ // the audit must catch. 块文本(含模板逐字)住 `./audit-block.mjs`(D23-3 外提)。
363
+ if (engAuditAttempt !== null) spawnBlocks.push(buildAuditBlock(ctx))
439
364
  // The child's own task input rides the child object: an eng-coder's audit
440
365
  // spawns reuse it as the task-book SOURCE — injected as the D-TS5 A2
441
- // mechanical summary, not verbatim (see above).
366
+ // mechanical summary, not verbatim (see above). D23-6:语义 = **纯任务书**(不含固块)。
442
367
  if (role === "eng-coder") child._engTaskInput = input
368
+ // spawn 固块单点绑定(sync / async 同点——buildSpawnChild 唯一调用点):此后只读 ⇒ 同一
369
+ // child 的任意 run(resume 续跑 / 报告追问重跑)逐字节恒定;消费点 = 核 `prepareRun`。
370
+ if (spawnBlocks.length > 0) child._spawnSystemBlock = spawnBlocks.join("\n\n")
443
371
 
444
372
  // Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
445
373
  // pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
@@ -466,7 +394,7 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
466
394
  // LOGGING(LOGGING.md):子代理内部事件(子内 llm:*/tool:*)以 childId 归属——
467
395
  // agent._logId 随 runAgent 的 logCtx 透出(主文件单文件全记、按 childId grep)。
468
396
  child._logId = relayPrefix.slice(0, -1)
469
- // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-SUBAGENT.md §6.27.4 W1——spawn 主路径,sync + async
397
+ // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 W1——spawn 主路径,sync + async
470
398
  // 共用):子 → 父在飞通道装配单点(label = relay 前缀去尾;`sync` 供工具返回注分形)。
471
399
  child._upstream = { parent, label: relayPrefix.slice(0, -1), sync: !wantAsync }
472
400
  const childOpts = {
@@ -1,3 +1,6 @@
1
+ // F-CC4(CONTEXT-COMPACTION.md §6.16.5):task 列表变更 ⇒ 方向转换轻推(本档只调用;文案/去重单源 = agent-tools/context.mjs)
2
+ import { pushContextNudge } from "./context.mjs"
3
+
1
4
  /** Common synonyms LLMs tend to use — normalize to canonical values */
2
5
  const STATUS_ALIASES = {
3
6
  completed: "done",
@@ -54,6 +57,13 @@ export const taskTool = {
54
57
  },
55
58
  readonly: true,
56
59
  async execute(args, ctx) {
60
+ // F10(工程模式机械停用——双层门的 **execute 层兜底**;先例 = escalate `subagent-actions.mjs:349`):
61
+ // 前置位 = 本行以下全部副作用之前(alias 归一 / `_taskPushbacks` 归零 / `_onTaskUpdate` / 轻推)
62
+ // ⇒ 零副作用(列表不变);装配层已摘(`agent/family-tools.mjs` 工程分支不入表,KD8)。
63
+ // 普通模式(engineering 非真)逐字走原路径。
64
+ if (ctx?.agent?.config?.agent?.engineering) {
65
+ return "Error: engineering mode is ON — task is unavailable (the batch record + the ledger are the tracking authority in engineering mode). Update progress in the batch record (§5/§6) or the ledger (`/ledger`); task lists go stale in this mode's parallel structure."
66
+ }
57
67
  // Keep only non-done items + the 3 most recently completed (for context reference), max 20 to prevent accumulation
58
68
  const warnings = []
59
69
  const raw = (args.items ?? []).map((it) => {
@@ -78,6 +88,7 @@ export const taskTool = {
78
88
  ctx.agent.tasks = items
79
89
  ctx.agent._taskPushbacks = 0 // task list changed — the completion gate earns a fresh reminder
80
90
  ctx.agent._onTaskUpdate?.(items)
91
+ if (ctx.depth === 0) pushContextNudge(ctx.agent) // F-CC4 轻推(§6.16.5——depth-0 门:子代理拿不到 context 工具)
81
92
  const done = items.filter((i) => i.status === "done").length
82
93
  const open = items.length - done
83
94
  const warningText = warnings.length > 0 ? ` ⚠️ ${warnings.join("; ")}` : ""
package/agent-tools.mjs CHANGED
@@ -14,6 +14,9 @@ export { timerTool } from "./agent-tools/timer.mjs"
14
14
  export { advisorTool } from "./agent-tools/advisor.mjs"
15
15
  export { engTool } from "./agent-tools/eng.mjs"
16
16
  export { readHistoryTool } from "./agent-tools/read-history.mjs"
17
+ // 模型主动整理上下文(CONTEXT-COMPACTION.md §6.16 · F-CC5):单工具三操作 stats / prune / compact
18
+ // ——depth-0 段挂载(子代理面裁见 §6.16.6 `family-tools.mjs` `depthOnly`)。
19
+ export { contextTool } from "./agent-tools/context.mjs"
17
20
  export { batchTool } from "./agent-tools/batch.mjs"
18
21
  // 批次档生命周期工具(BATCH-RECORD §4.3 挂载表——主名 `batch` 单工具四 action:depth-0 主 agent
19
22
  // create/close + append §1/§4/§6 + status §1(轮 2 裁定②:§4/§6 状态面走普通文档写)(D-BR18);
@@ -22,6 +25,6 @@ export { batchTool } from "./agent-tools/batch.mjs"
22
25
  // CORE-UNIFICATION TOOLS #83(统一登记册——VSC `agent-tools/index.mjs:15` 含 consult 家族;
23
26
  // CLI 原把 consult 另挂 `agent/setup.mjs:173` ⇒ 归位:登记册即单一来源,装配方只读本档)。
24
27
  export { consultStartTool, consultStopTool } from "./agent-tools/consult.mjs"
25
- // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-SUBAGENT.md §6.27.4 装配接线):子代理上行通道工具
28
+ // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 装配接线):子代理上行通道工具
26
29
  // (depth>0 段装配——`agent/family-tools.mjs`;depth-0 / consult 不装配)。
27
30
  export { parentChannelTool } from "./agent-tools/parent-channel.mjs"
package/agent.mjs CHANGED
@@ -26,8 +26,9 @@ import {
26
26
  DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
27
27
  MIN_REPORT_CHARS, REPORT_CONTINUATION,
28
28
  AUTO_TURN_DIGEST_DOMAIN,
29
+ AUTO_TURN_DIGEST_DOMAIN_ENG, // §6.15.3(F10 第三面):工程模式 digest 基座变体(task 指针改批次档 + 台账)
29
30
  UPSTREAM_TURN_DOMAIN, // §6.27.12.8:上行唤醒轮域文本(手动档——ask 轮不沿用 digest 域文本)
30
- restoreGuard, // §17 D-S6 读侧单点(P2 机制层端差批 §2.18——键清单归核)
31
+ restoreGuard, // digest D-S6 读侧单点(AGENT-LOOP-ASYNC-POOL.md §6.8;P2 机制层端差批 §2.18——键清单归核)
31
32
  } from "./agent/helpers.mjs"
32
33
  // ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
33
34
  // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
@@ -159,13 +160,15 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
159
160
  agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
160
161
  agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
161
162
  agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
163
+ // F-CC2(§6.16.2):模型主动压缩的排队槽也是回合级——上一回合末尾未被安全点消费的请求不得跨回合生效
164
+ agent._pendingCompact = null
162
165
  }
163
166
  }
164
- // §17 D-S6 manual tier: digest action-domain reminder (system-driven turn — organize only).
167
+ // digest D-S6 manual tier(AGENT-LOOP-ASYNC-POOL.md §6.8): action-domain reminder (system-driven turn — organize only).
165
168
  // §6.27.12.4 ②: an up-stream wake turn answers a RUNNING subagent waiting for the reply — it
166
169
  // must not reuse the digest text ("no one is waiting" is the opposite of the truth).
167
170
  if ((autoTurn || upstreamTurn) && !agent.autoApprove) {
168
- agent.history.push({ role: "user", content: upstreamTurn ? UPSTREAM_TURN_DOMAIN : AUTO_TURN_DIGEST_DOMAIN, transient: true })
171
+ agent.history.push({ role: "user", content: upstreamTurn ? UPSTREAM_TURN_DOMAIN : (agent.config?.agent?.engineering === true ? AUTO_TURN_DIGEST_DOMAIN_ENG : AUTO_TURN_DIGEST_DOMAIN), transient: true })
169
172
  }
170
173
  // eng-coder authorization (_engDesignReviewed) is eng-coder-only: set by subagent-spawn.mjs
171
174
  // (spawn gate) / design-token.mjs (design review pass) BEFORE the child runAgent — the
@@ -192,8 +195,11 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
192
195
  // context.mjs compressIfNeeded 经 extras 透出到 logCtx)
193
196
  traceDepth: depth,
194
197
  }
198
+ // F-CC1(§6.16.4 阈值单源接线):本回合压缩判定所用的阈值 + 固定开销面暂存——
199
+ // `context` 工具的 stats 报**同一口径**(不自行重算第二口径;VSC checkAndCompact 同款暂存)。
200
+ agent._ctxBasis = { threshold, overhead: compactionOverhead }
195
201
 
196
- // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-SUBAGENT.md §6.27.4 消费点):子 → 父在飞消息的
202
+ // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
197
203
  // 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
198
204
  // (先例 = 上方 injectAsyncResult :113-117)。
199
205
  const { drainChildUpstream } = await import("./agent-tools/parent-channel.mjs")
package/config.mjs CHANGED
@@ -49,7 +49,7 @@ export const DEFAULTS = {
49
49
  advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
50
50
  autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
51
51
  engineering: false, // strict methodology enforcement — design-before-code (design review + user approval before code)
52
- // Async pool limits (AGENT-LOOP-SUBAGENT.md §6.10 D-24a/R14 + R13 — POOL-CONFIG-
52
+ // Async pool limits (AGENT-LOOP-ASYNC-POOL.md §6.10 D-24a/R14 + R13 — POOL-CONFIG-
53
53
  // UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
54
54
  // pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
55
55
  // 4 路" + advisor 评审池并入同一可配体系——三池统一默认 4)。engCoder/other