@thincoder/core 0.10.3 → 0.10.5

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 (172) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +1 -1
  3. package/advisor/citations.mjs +29 -3
  4. package/advisor/compaction.mjs +31 -7
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +2 -1
  8. package/advisor/run.mjs +9 -5
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/assemble.mjs +1 -1
  12. package/agent/chat-call.mjs +1 -1
  13. package/agent/child-marks.mjs +2 -2
  14. package/agent/completion.mjs +3 -1
  15. package/agent/digest-account.mjs +189 -0
  16. package/agent/dispatch-gates.mjs +8 -0
  17. package/agent/dispatch.mjs +39 -18
  18. package/agent/family-tools.mjs +27 -16
  19. package/agent/helpers.mjs +12 -3
  20. package/agent/light-round.mjs +66 -0
  21. package/agent/post-turn.mjs +8 -4
  22. package/agent/record-results.mjs +4 -0
  23. package/agent/run-stages.mjs +32 -7
  24. package/agent/run-start.mjs +50 -3
  25. package/agent/setup-reminders.mjs +1 -1
  26. package/agent/setup.mjs +18 -3
  27. package/agent/subagent-event.mjs +1 -1
  28. package/agent/suspension.mjs +37 -12
  29. package/agent/timers.mjs +56 -5
  30. package/agent/turn-loop.mjs +30 -5
  31. package/agent/write-gate.mjs +20 -32
  32. package/agent-tools/advisor-async.mjs +15 -7
  33. package/agent-tools/advisor-settle.mjs +2 -2
  34. package/agent-tools/advisor.mjs +34 -33
  35. package/agent-tools/async-discard.mjs +34 -0
  36. package/agent-tools/async-settle.mjs +32 -7
  37. package/agent-tools/audit-block.mjs +1 -1
  38. package/agent-tools/bash-async.mjs +1 -1
  39. package/agent-tools/batch-lifecycle.mjs +59 -17
  40. package/agent-tools/batch-paths.mjs +24 -4
  41. package/agent-tools/batch-skeleton.mjs +59 -43
  42. package/agent-tools/batch.mjs +39 -18
  43. package/agent-tools/browser-async.mjs +198 -0
  44. package/agent-tools/checkpoint.mjs +1 -1
  45. package/agent-tools/design-slots.mjs +232 -0
  46. package/agent-tools/design-token.mjs +20 -0
  47. package/agent-tools/digest-budget.mjs +1 -1
  48. package/agent-tools/escalate-async.mjs +4 -2
  49. package/agent-tools/manifest.mjs +193 -0
  50. package/agent-tools/parent-channel.mjs +7 -4
  51. package/agent-tools/review-facts.mjs +143 -16
  52. package/agent-tools/settings.mjs +1 -1
  53. package/agent-tools/skill.mjs +1 -1
  54. package/agent-tools/subagent-actions-query.mjs +13 -6
  55. package/agent-tools/subagent-async.mjs +31 -15
  56. package/agent-tools/subagent-panel.mjs +2 -1
  57. package/agent-tools/subagent-scheduler.mjs +8 -5
  58. package/agent-tools/subagent-spawn.mjs +30 -56
  59. package/agent-tools/subagent.mjs +21 -8
  60. package/agent-tools/timer.mjs +186 -12
  61. package/agent-tools/zero-write-watch.mjs +77 -0
  62. package/agent-tools.mjs +2 -1
  63. package/agent.mjs +2 -2
  64. package/attachments.mjs +1 -1
  65. package/config-io.mjs +18 -13
  66. package/config-migrate.mjs +13 -29
  67. package/config-presets.mjs +33 -29
  68. package/config.mjs +68 -25
  69. package/context-degrade.mjs +1 -1
  70. package/context-echo.mjs +1 -1
  71. package/context-push.mjs +2 -2
  72. package/context.mjs +1 -1
  73. package/conventions.mjs +28 -8
  74. package/declaration.mjs +18 -2
  75. package/escape.mjs +1 -1
  76. package/file-links.mjs +1 -1
  77. package/file-refs.mjs +1 -1
  78. package/history-window.mjs +1 -1
  79. package/i18n.mjs +7 -0
  80. package/ledger-cmd.mjs +62 -16
  81. package/ledger-db.mjs +6 -0
  82. package/ledger-surface.mjs +49 -5
  83. package/ledger-tools.mjs +158 -87
  84. package/ledger-variant-notice.mjs +47 -0
  85. package/ledger.mjs +11 -18
  86. package/log.mjs +1 -1
  87. package/manifest-discovery.mjs +1 -1
  88. package/manifest-schema.mjs +7 -5
  89. package/manifest.mjs +5 -3
  90. package/mcp/transport-stdio.mjs +1 -1
  91. package/mcp.mjs +1 -1
  92. package/memory/code-index.mjs +1 -1
  93. package/memory/code-search.mjs +1 -1
  94. package/memory/code-sync.mjs +1 -1
  95. package/memory/docs.mjs +6 -2
  96. package/memory/file-list.mjs +1 -1
  97. package/memory/file-walk.mjs +17 -5
  98. package/memory/maintain.mjs +410 -0
  99. package/memory/memory-tool.mjs +21 -6
  100. package/memory/schema.mjs +19 -7
  101. package/memory/sweep.mjs +8 -5
  102. package/memory/sync-tail.mjs +16 -10
  103. package/model-ref.mjs +16 -13
  104. package/model-specs.mjs +21 -9
  105. package/package.json +1 -1
  106. package/peer-instances.mjs +1 -1
  107. package/permission.mjs +33 -0
  108. package/process-probe.mjs +2 -2
  109. package/prompts/advisor-design.md +4 -2
  110. package/prompts/advisor-round1.md +1 -1
  111. package/prompts/advisor-round2.md +1 -1
  112. package/prompts/advisor-round3.md +1 -1
  113. package/prompts/common.md +38 -4
  114. package/prompts/consult-base.md +1 -1
  115. package/prompts/discipline-engineering.md +32 -15
  116. package/prompts/discipline-normal.md +46 -2
  117. package/prompts/persona-eng-designer.md +6 -6
  118. package/prompts/persona-engineering.md +18 -9
  119. package/prompts/persona-normal.md +2 -0
  120. package/prompts/subagent-base.md +3 -0
  121. package/provider/anthropic.mjs +14 -2
  122. package/provider/core.mjs +6 -0
  123. package/provider/errors.mjs +28 -3
  124. package/provider/google.mjs +22 -10
  125. package/provider/list-models.mjs +3 -2
  126. package/provider/responses-request.mjs +3 -1
  127. package/provider/responses.mjs +38 -4
  128. package/provider/sse.mjs +3 -0
  129. package/provider-flows.mjs +14 -15
  130. package/proxy-chunked.mjs +102 -0
  131. package/proxy-target.mjs +54 -0
  132. package/proxy-transport.mjs +298 -0
  133. package/proxy.mjs +17 -248
  134. package/session-gc.mjs +2 -2
  135. package/session-lifecycle.mjs +18 -14
  136. package/session-slot-claims.mjs +2 -2
  137. package/session-slots-manifest.mjs +1 -1
  138. package/session-slots.mjs +3 -3
  139. package/shell-identity.mjs +107 -0
  140. package/team-ledger.mjs +282 -0
  141. package/team.mjs +223 -0
  142. package/tool-docs/advisor.md +1 -1
  143. package/tool-docs/bash.md +2 -1
  144. package/tool-docs/batch.md +1 -1
  145. package/tool-docs/browser.md +48 -0
  146. package/tool-docs/delete.md +7 -5
  147. package/tool-docs/ledger.md +6 -0
  148. package/tool-docs/manifest.md +16 -0
  149. package/tool-docs/memory.md +1 -0
  150. package/tool-docs/notify_parent.md +5 -0
  151. package/tool-docs/process.md +4 -4
  152. package/tool-docs/question.md +1 -0
  153. package/tool-docs/subagent.md +14 -3
  154. package/tool-docs/team_ledger.md +9 -0
  155. package/tool-docs/timer.md +6 -0
  156. package/tool-docs/wait_for.md +1 -0
  157. package/tools/bash.mjs +9 -4
  158. package/tools/browser.mjs +161 -0
  159. package/tools/git.mjs +1 -1
  160. package/tools/index.mjs +4 -4
  161. package/tools/ops.mjs +26 -10
  162. package/tools/patch.mjs +8 -2
  163. package/tools/repomap.mjs +1 -1
  164. package/tools/write-path.mjs +8 -6
  165. package/traces/trace-cleanup.mjs +1 -1
  166. package/traces/trace-store.mjs +1 -1
  167. package/vision-reader.mjs +10 -16
  168. package/tool-docs/ledger_add.md +0 -1
  169. package/tool-docs/ledger_close.md +0 -1
  170. package/tool-docs/ledger_count.md +0 -1
  171. package/tool-docs/ledger_query.md +0 -1
  172. package/tool-docs/ledger_update.md +0 -1
@@ -31,7 +31,7 @@
31
31
  * non-stale code review marks _calledAdvisorThisRun; a passing design review
32
32
  * issues its token under the designId slot at settle time.
33
33
  * 2026-09-11 第 11 批拆分:settle 记账 + 变更日志 + 陈旧/冻结判定迁 `advisor-settle.mjs`
34
- * (本文件 500 行 = 硬帽在册);既有 import 面经本文件 re-export 保持不变。
34
+ * (本文件 500 行——2026-09-11 拆分时);既有 import 面经本文件 re-export 保持不变。
35
35
  *
36
36
  * The sync path (depth>0 eng-coder self-review / explicit async:false) reuses
37
37
  * the same instance resolution (rounds/prior/cap) — its accounting still lands
@@ -47,13 +47,13 @@ export {
47
47
  // 第 11 批拆分(2026-09-11):settle 记账 + 变更日志(noteMutations)+ 陈旧判定
48
48
  // (reviewIsStale)+ 冻结冲突(inflightDesignReviewConflict)迁 advisor-settle.mjs——
49
49
  // 既有 import 面(dispatch / subagent-async / escalate-async / 测试)经此 re-export 不变。
50
- import { normAbs, mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
50
+ import { mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
51
51
  export {
52
52
  mutationSeqOf, noteMutations, reviewIsStale, settleAdvisorRun, inflightDesignReviewConflict,
53
53
  } from "./advisor-settle.mjs"
54
54
  // 第 33 批(评审失败护栏):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
55
55
  // 本文件继续在实例解析 / 池 entries 上消费它。
56
- import { docSetKey } from "./review-facts.mjs"
56
+ import { docSetKey, resolveReviewDocPaths, scopeAdvisorMirror } from "./review-facts.mjs"
57
57
  // F31(2026-09-18 顾问面治理批):拒回文案对象标识行单源。
58
58
  import { withIdentityLine, scopeSummary } from "../advisor/notice.mjs"
59
59
  import { runAdvisorReview, resolveAdvisorProvider, ADVISOR_THINKING_PLACEHOLDER } from "../advisor/run.mjs"
@@ -65,6 +65,8 @@ import { deathLine } from "../abort-provenance.mjs"
65
65
  // ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
66
66
  import { bindChildController, buildChildSignal, carrierField, getAsyncPool, settleAsyncEntry, tombstoneOf, writeTombstone } from "./async-settle.mjs"
67
67
  import { nextSubagentId, consumeSubagentToken, assertPoolKeyFree, entryTerminal } from "./subagent-scheduler.mjs"
68
+ // #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.6 触发接线②):评审族推送单源(叶档——恒零抛)。
69
+ import { pushAdvisorAlert } from "./zero-write-watch.mjs"
68
70
 
69
71
  // ─────────────────────────────────────────────────────────────────────────────
70
72
  // Review-instance registry (_advisorRuns — per-review rounds/prior/cap)
@@ -161,9 +163,9 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
161
163
  runs.set(reviewId, run)
162
164
  }
163
165
  }
164
- // Scope the legacy mirror fields (message builder + run.mjs cap + displays).
165
- agent._advisorRound = run.priorOutput ? run.round : 0
166
- agent._lastAdvisorOutput = run.priorOutput
166
+ // Scope the mirror fields to THIS instance (message builder + run.mjs cap +
167
+ // displays; #949 ① — the queued-launch window is re-secured at entry.start).
168
+ scopeAdvisorMirror(agent, run)
167
169
  return { run, isNew: run.round === 0 && !run.priorOutput, reviewId: run.reviewId, designId: run.designId }
168
170
  }
169
171
 
@@ -390,8 +392,11 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
390
392
  id, role: "advisor", reviewType, run,
391
393
  reviewId: run.reviewId, designId, designToken, documents, paths, object,
392
394
  launchSeq: mutationSeqOf(parent),
395
+ // docAbs 产出面同源归一(2026-10-04 · #921 · A 随件):四分支单源 resolved(裸形/前缀形
396
+ // 命中候选项目根即归一为绝对路径)——冻结窗/陈旧判定对**裸形受理档**照护成立(否则 A
397
+ // 开冻结窗保护洞);绝对形与旧 normAbs 面逐字等价。
393
398
  docAbs: reviewType === "design" && Array.isArray(documents)
394
- ? documents.filter((d) => typeof d === "string").map((d) => normAbs(d, parent.cwd))
399
+ ? [...resolveReviewDocPaths(documents, parent.cwd).resolved.values()]
395
400
  : [],
396
401
  relayPrefix: `advisor#${id}/`,
397
402
  status: queued ? "queued" : "running", position: undefined,
@@ -420,9 +425,12 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
420
425
  // Async mark + [model] — the TUI block opens at ACTUAL start (⏹ gating reads it).
421
426
  ctx?.callbacks?.onToken?.(entry.relayPrefix + "⟦ev⟧async\x1e")
422
427
  ctx?.callbacks?.onToken?.(entry.relayPrefix + "[model]" + (entry.model ?? ""))
428
+ // #949 ①:排队 / 延迟窗口封死——构建前按本评审实例复核镜像(消费点定域;幂等)。
429
+ scopeAdvisorMirror(parent, run)
423
430
  runAdvisorReview(parent, reviewType, {
424
431
  onOutput: (chunk) => relayAdvisorOutput(ctx?.callbacks, entry.relayPrefix, chunk),
425
432
  signal: entry.controller.signal,
433
+ onStall: (rounds) => pushAdvisorAlert(parent, `advisor#${id}`, rounds), // #934:连续无产出越阈 ⇒ 自动上行提醒(只推——§6.32)
426
434
  }, designToken, documents, paths, object, designId)
427
435
  .then((report) => { entry.report = report })
428
436
  // §6.12 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * advisor-settle.mjs — advisor settle accounting + mutation log + stale / freeze
3
- * determination(2026-09-11 第 11 批自 advisor-async.mjs 拆出——500 行 = 硬帽在册,
4
- * 任何新增必越;既有 import 面经 advisor-async.mjs **re-export 保持不变**
3
+ * determination(2026-09-11 第 11 批自 advisor-async.mjs 拆出;
4
+ * 既有 import 面经 advisor-async.mjs **re-export 保持不变**
5
5
  * (noteMutations / mutationSeqOf / reviewIsStale / settleAdvisorRun)。
6
6
  *
7
7
  * 内容 = 迁入(逐字搬移含注释)+ 本批两处接线:
@@ -9,12 +9,11 @@
9
9
  */
10
10
  import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
11
11
  import { resolveBatchDocPath } from "./batch.mjs"
12
- // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——复用 M4 的
13
- // write-gate.mjs 单一权威源(KD-M6-1),替代 v1 的逐档分类面(isDocPath);
14
- // normAbs 同源 re-export(指针非副本)。不 import dispatch.mjs(簇间回边,环风险)。
15
- import { resolveReviewTargetPaths, resolveReviewRootsFor, normAbs } from "../agent/write-gate.mjs"
16
- import { owningProject } from "../manifest-discovery.mjs"
17
- import { sep } from "node:path"
12
+ // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——单一权威源 =
13
+ // review-facts.mjs 的 resolveReviewDocPaths(2026-10-04 · #921 · KD-2——分类四分支 + B 文案
14
+ // 诊断数据单源;write-gate 同名再出口保 normAbs 面)。不 import dispatch.mjs(簇间回边,环风险)。
15
+ import { normAbs } from "../agent/write-gate.mjs"
16
+ import { resolveReviewDocPaths } from "./review-facts.mjs"
18
17
  import {
19
18
  generateDesignToken,
20
19
  settleDesignReview,
@@ -63,11 +62,11 @@ export const advisorTool = {
63
62
  documents: {
64
63
  type: "array",
65
64
  items: { type: "string" },
66
- description: "Doc paths to review (design / requirements / referenced docs). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
65
+ description: "Doc paths to review (design / requirements / referenced docs). Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/…, or absolute). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
67
66
  },
68
67
  batchDoc: {
69
68
  type: "string",
70
- description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Omit when no batch record is in flight (no write channel — zero regression).",
69
+ description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/batches/…, or absolute). Omit when no batch record is in flight (no write channel — zero regression).",
71
70
  },
72
71
  },
73
72
  required: ["type"],
@@ -112,34 +111,36 @@ export const advisorTool = {
112
111
  )
113
112
  }
114
113
 
115
- // Design review: the review scope must be documentation files. Classification
116
- // comes from the manifest-declared review-target roots (M4 write-gate.mjs single
117
- // authority — FR12: no directory-name hardcoding; a project whose docs live
118
- // elsewhere declares them in PROJECT-MANIFEST.json docRoot).
114
+ // Design review: the review scope must be documentation files. Classification =
115
+ // resolveReviewDocPaths (2026-10-04 · #921 · the four-leg single authority in
116
+ // review-facts.mjs): legs ① session-root set / ② owning-project root set keep the
117
+ // legacy behavior verbatim; leg ③ probes candidate project roots for bare
118
+ // project-root-relative forms; leg ④ disambiguates by readable-file uniqueness
119
+ // (fail-closed — criterion=scope-doc-ambiguous when ≥2 candidates are readable).
119
120
  if (reviewType === "design" && documents) {
120
- const roots = resolveReviewTargetPaths(agent).map((r) => r.replace(/[\\/]/g, sep))
121
- // 按用点解析(2026-09-21 之裁收口 · 台账 #828):逐文档按**其所属项目**(最近带档祖先)
122
- // 的 docRoot 判定;无主档文档回退会话根集判定(原行为零变)。
123
- const ownerRootsCache = new Map()
124
- const ownRoots = (abs) => {
125
- const owner = owningProject(abs)
126
- if (!owner) return []
127
- let r = ownerRootsCache.get(owner)
128
- if (!r) {
129
- r = resolveReviewRootsFor(owner).map((x) => x.replace(/[\\/]/g, sep))
130
- ownerRootsCache.set(owner, r)
131
- }
132
- return r
133
- }
134
- const invalidDocs = documents.filter((doc) => {
135
- const n = normAbs(doc, agent.cwd).replace(/[\\/]/g, sep)
136
- const under = (r) => n === r || n.startsWith(r + sep)
137
- return !roots.some(under) && !ownRoots(n).some(under)
138
- })
139
- if (invalidDocs.length > 0) {
121
+ const { resolved, invalid, ambiguous, candidates } = resolveReviewDocPaths(documents, agent.cwd)
122
+ if (invalid.length > 0 || ambiguous) {
140
123
  if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
124
+ if (ambiguous) {
125
+ // 歧义拒(KD-4 独立判据——不混入 not-doc;列全部可读命中——fail-closed 不静默挑一)。
126
+ return withIdentityLine(
127
+ `Advisor: design review document is ambiguous — "${ambiguous.doc}" matches several candidate projects: ${ambiguous.hits.join(", ")}. Pass a project-root-relative or absolute path.`,
128
+ { type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-doc-ambiguous" },
129
+ )
130
+ }
131
+ // not-doc 拒:既有稳定前缀逐字(行首)+ 解析诊断尾(B 面根治——逐文档「解析后绝对
132
+ // 路径 ← 基面 = 会话 cwd」+ 候选项目根提示 cap 5 + 重试指引——纠正「误报因」缺陷)。
133
+ const invalidList = invalid.map((v) => v.doc).join(", ")
134
+ const attemptedList = invalid
135
+ .map((v) => `${v.doc} → ${v.attempted.length > 0 ? v.attempted.join(" | ") : normAbs(v.doc, agent.cwd)}`)
136
+ .join("; ")
137
+ const shown = candidates.slice(0, 5)
138
+ const more = candidates.length - shown.length
139
+ const candidateNote = shown.length > 0
140
+ ? `(candidates: ${shown.join(", ")}${more > 0 ? ` +${more} more` : ""}) — pass project-root-relative (e.g. ${shown[0]}/docs/…) or absolute paths`
141
+ : "— pass project-root-relative or absolute paths"
141
142
  return withIdentityLine(
142
- `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidDocs.join(", ")}`,
143
+ `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidList}\nDiagnosis: resolved against session cwd ${agent.cwd} → ${attemptedList}; no candidate project docRoot contains it ${candidateNote}.`,
143
144
  { type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-not-doc" },
144
145
  )
145
146
  }
@@ -17,6 +17,8 @@
17
17
  * ——其控制器链只作**判据**(杀 ⟺ 控制器已中止),杀树动作经本收尾面 `spec.dispose` 钩子落
18
18
  * `bash-async.mjs` 杀单点(`killBgTree`);回合中断(Ctrl+I)经 `bindChildController`
19
19
  * interrupt 豁免不中止控制器 ⇒ 本面零动作(F2 同款豁免——池保留)。
20
+ * 批 browser-async-fix(2026-10-07):browser 后台动作族同面并入(`BROWSER_SPEC`——dispose = 条目
21
+ * 控制器 abort ⇒ 动作展开 / 排队丢队;**不杀浏览器**)。
20
22
  *
21
23
  * 判据口径(D-AD6):两接线点**均不传 `ctx`** ⇒ 实走 controller 支(`parentAborted(null, entry)`)
22
24
  * ——与对侧有效判据同判(对侧传入的 `ctx` 为死参);`ctx` 保留为签名备用面(直调用例 / 对侧收敛)。
@@ -59,6 +61,11 @@ const bgReminderText = (n, list) =>
59
61
  `[System reminder: ${n} background bash task(s) were killed by the user's Stop — their processes are gone and no digest will arrive: ${list}. ` +
60
62
  "Full output logs remain on disk (paths were given in the start ack).]"
61
63
 
64
+ /** browser 后台动作族提醒模板(同族形态——取消无摘要:终止凭据 = 墓碑 + 杀点确认;浏览器本体不杀)。 */
65
+ const browserReminderText = (n, list) =>
66
+ `[System reminder: ${n} background browser task(s) were cancelled by the user's Stop — no digest will arrive: ${list}. ` +
67
+ "The browser session itself was not killed (run `close` to reset it).]"
68
+
62
69
  /** 列表词(wasStatus 数据源):queued → "(was queued — never started)";其余 "(was running)"。 */
63
70
  const wasPhrase = (wasStatus) => (wasStatus === "queued" ? " (was queued — never started)" : " (was running)")
64
71
 
@@ -151,6 +158,21 @@ const BG_SPEC = {
151
158
  },
152
159
  }
153
160
 
161
+ /** browser 后台动作族 spec(批 browser-async-fix——收尾档同面):dispose = 中止在飞(条目控制器
162
+ * abort ⇒ running 动作展开 / queued 丢队从未运行)+ 标记 discarded(settle 面据此不注入——摘要不可达);
163
+ * 无队列面(帽 = 起跑显式拒)。**不杀浏览器**(会话重置是独立动作 `close`)。 */
164
+ const BROWSER_SPEC = {
165
+ pool: "browser",
166
+ describe: (entry) => ({ id: entry.id, role: "browser", action: entry.action }),
167
+ roleOf: () => "browser",
168
+ listPhrase: (d) => `browser#${d.id} (${d.action})`,
169
+ reminder: browserReminderText,
170
+ dispose: (entry) => {
171
+ entry.discarded = true
172
+ entry.controller?.abort?.({ abortTrigger: "discard" })
173
+ },
174
+ }
175
+
154
176
  /**
155
177
  * 中止收尾(F1–F3):只清已死子代理条目——写 `discarded` 墓碑 + 出池 + 队列剔除 + 整批一次提醒。
156
178
  * @param parent agent 形态(载体双形经 `getAsyncPool` / `writeTombstone` 吸收)
@@ -184,3 +206,15 @@ export function discardAbortedAdvisors(parent, ctx = null) {
184
206
  export function discardAbortedBgTasks(parent, ctx = null) {
185
207
  return discardRole(parent, BG_SPEC, ctx)
186
208
  }
209
+
210
+ /**
211
+ * 中止收尾(browser 后台动作族——同面并入):只清已死条目——逐条中止在飞(running ⇒ 动作展开;
212
+ * queued ⇒ 丢队从未运行)+ `discarded` 墓碑 + 出池 + 整批一次提醒;回合中断(Ctrl+I)不中止控制器
213
+ * ⇒ 本面零动作(池保留——与 bg 同豁免)。
214
+ * @param parent agent 形态
215
+ * @param ctx 判据 ctx(接线点不传——controller 支,同上)
216
+ * @returns {{discarded: Array<{id, role: "browser", action}>, kept: number}}
217
+ */
218
+ export function discardAbortedBrowserTasks(parent, ctx = null) {
219
+ return discardRole(parent, BROWSER_SPEC, ctx)
220
+ }
@@ -113,11 +113,12 @@ export function bindChildController(ctrl, baseSignal) {
113
113
 
114
114
  /** 池 accessor(D1):role "advisor" → `_asyncAdvisors`(独立评审池——无队列独立调度)·
115
115
  * role "consult" → `_consultSessions`(会话池)· role "bg" → `_bgTasks`(#9 后台 bash
116
- * 任务池)· 其余 → `_asyncSubagents`(子代理/飞刀共享槽位队列池)。载体经 `carrierField`
116
+ * 任务池)· role "browser" → `_browserTasks`(批 browser-async-fix 后台浏览器动作池)·
117
+ * 其余 → `_asyncSubagents`(子代理/飞刀共享槽位队列池)。载体经 `carrierField`
117
118
  * 吸收(#94);未初始化返 null——调用方以 `?? new Map()` / 可选链处置。 */
118
119
  export function getAsyncPool(parent, role) {
119
120
  const field = role === "advisor" ? "_asyncAdvisors" : role === "consult" ? "_consultSessions"
120
- : role === "bg" ? "_bgTasks" : "_asyncSubagents"
121
+ : role === "bg" ? "_bgTasks" : role === "browser" ? "_browserTasks" : "_asyncSubagents"
121
122
  return carrierField(parent, field) ?? null
122
123
  }
123
124
 
@@ -134,6 +135,17 @@ export function parkAsyncPending(parent, entry) {
134
135
  pend.push(entry)
135
136
  }
136
137
 
138
+ /**
139
+ * §6.31.5 升级账本写助手(消化账务批):`_unsettledDigests` 追加一行
140
+ * (`{ id, role, failures, report, at }`)。载体吸收(父字段优先 / history 借用——形态同
141
+ * `parkAsyncPending`);行序 = 升级序(读面 = `digest-account.mjs` `unsettledRows`)。
142
+ */
143
+ export function appendUnsettledDigest(parent, row) {
144
+ const existing = carrierField(parent, "_unsettledDigests")
145
+ const list = (parent._unsettledDigests ??= Array.isArray(existing) ? existing : [])
146
+ list.push(row)
147
+ }
148
+
137
149
  // ─── D4 守卫统一(严格版)+ D6 信号兜底单点 ──────────────────────────────────
138
150
 
139
151
  /** D4 settle 守卫统一(escalate 严格版):父侧中止 = 回合 signal aborted(#98 VSC 侧
@@ -161,7 +173,9 @@ export function buildChildSignal(parent, ctx) {
161
173
  * 对子代理对象的引用(`childAgent`/`report`——条目前此从不显式释放,挂起期 = 分钟级驻留)。
162
174
  * 幂等(重入不抛);池内/挂起未消化窗口语义零变(settle 时刻不释放——表 2 候选 2 否决:
163
175
  * status/observe 在窗口内仍读 child 摘要)。三消费点(回合尾收集 / run 起始 pending 注入 /
164
- * 挂起残差注入)在注入完成后调本 helper。
176
+ * 挂起残差注入)——fallback 面(无 `_daSession`)注入完成后调本 helper(逐字沿用);会话态
177
+ * (`_daSession`)注入后改调 `releaseChildHold`(仅释放 `childAgent`——`report` 保留至销账 /
178
+ * 升级;§6.31.4 ∥ KD-DA7)。
165
179
  */
166
180
  export function releaseSettledEntry(entry) {
167
181
  if (!entry || typeof entry !== "object") return
@@ -169,6 +183,16 @@ export function releaseSettledEntry(entry) {
169
183
  entry.report = null
170
184
  }
171
185
 
186
+ /**
187
+ * §6.31.4 投递面(消化账务批 · 2026-10-05 · 台账 #930):会话态注入后**仅**释放 `childAgent`——
188
+ * `report` 保留至销账 / 升级(重投需原文;驻留增量以报告量级 ∥ 重投上限封顶)。fallback 面零调用
189
+ * (既有 `releaseSettledEntry` 逐字沿用——注入即释放)。
190
+ */
191
+ export function releaseChildHold(entry) {
192
+ if (!entry || typeof entry !== "object") return
193
+ entry.childAgent = null
194
+ }
195
+
172
196
  /**
173
197
  * D3 settle 共享 helper——四族 settle 回调公共收尾单点(ASYNC-RESULT-CONTAINER.md
174
198
  * AC1:settle 记账单点,无逐字重复)。流程:
@@ -273,10 +297,11 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
273
297
  parkAsyncPending(parent, entry)
274
298
  pool?.delete(String(entry.id))
275
299
  }
276
- // 发射单点(块到达时点归位批 · #746|§6.8「settle 延迟冻结(两态统一)」):settle 一律 ⟦ev⟧settled
277
- // (区块头恒中间态「done · awaiting digestion」驻留面板);⟦ev⟧done = 消费面补发(消费窗清单单源
278
- // = §6.8)⇒ 冻结 ∥ 归档恒落消费时点——回合运行中零归档触发器(「S 不夹运行中回合」结构不变式)。
279
- ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
300
+ // 发射单点(块到达时点归位批 · #746|§6.8「settle 延迟冻结(两态统一)」):settle 一律 ⟦ev⟧settled
301
+ // (区块头恒中间态「done · awaiting digestion」驻留面板);⟦ev⟧done = 消费面补发(消费窗清单单源
302
+ // = §6.8)⇒ 冻结 ∥ 归档恒落投递时点(消化账务批 · 2026-10-05——归档 ≠ 销账;销账后移见账 = §6.31);
303
+ // 回合运行中零归档触发器(「S 不夹运行中回合」结构不变式)。
304
+ ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
280
305
  }
281
306
  // ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
282
307
  entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
@@ -2,7 +2,7 @@
2
2
  * audit-block.mjs — 审计块构造器(AGENT-LOOP-SUBAGENT.md §6.26 · 台账 #23)。
3
3
  *
4
4
  * 自 `subagent-spawn.mjs` **逐字搬移**(D23-3 外提:spawn 档越 300 软线 + 块构造成纯函数
5
- * ⇒ 判据面可直测;D23-4:块内文本零改——含块内自指词,整块搬移不改真值)。本档承载
5
+ * ⇒ 判据可直测;D23-4:块内文本零改——含块内自指词,整块搬移不改真值)。本档承载
6
6
  * spawn 级固定机制性指令的第二块(审计模板 + A2 任务书摘要 + touched 快照):
7
7
  * 装配侧只收集(`thincoder-core/agent-tools/subagent-spawn.mjs`),下发面 = system 固块
8
8
  * (`child._spawnSystemBlock` → 核 `prepareRun` 拼接——`thincoder-core/agent/setup.mjs`)。
@@ -56,7 +56,7 @@ const TAIL_KEEP_CHARS = 32 * 1024
56
56
  /** exit→close 宽限(同同步径后台注记面:孙进程握管道时 close 不来——`bash.mjs` 1500ms 同值)。 */
57
57
  const EXIT_GRACE_MS = 1500
58
58
 
59
- /** 测试缝:log 目录重定向(单测沙箱——生产从不调用;`_setDigestOffloadDirForTest` 同式)。 */
59
+ /** 测试缝:log 目录重定向(单元测试沙箱——生产从不调用;`_setDigestOffloadDirForTest` 同式)。 */
60
60
  let _bgTaskLogDir = null
61
61
  export function _setBgTaskLogDirForTest(dir) { _bgTaskLogDir = dir }
62
62
  function bgTaskLogDir() { return _bgTaskLogDir ?? join(configDir, "tool-results") }
@@ -12,14 +12,15 @@
12
12
  * 身份判据(D-BR17/D-BR18/D-BR21):主 agent 的工具调用 ctx 带 `depth === 0`(dispatch 装配);
13
13
  * eng 子代理 `ctx.depth > 0`;评审实例 ctx 无 depth 但工具以 `review: true` 绑定。create/close
14
14
  * 仅 depth-0 放行(BR-19/BR-24「… is main-agent-only」);status 写域 = 调用者自己段
15
- * (eng-designer → §2 · 评审 → §3 · eng-coder → §5 · 主 agent → §1——轮 2 #3 裁定②,§4/§6
16
- * 状态面走普通文档写);depth-0 的 status/path 面按 D-BR21(可选 path,缺省 = 在飞批唯一时取)。
15
+ * (eng-designer → §2 · 评审 → §3 · eng-coder → §5 · coder → §5【普通模式分支增表】· 主 agent →
16
+ * 工程 §1 ∥ 普通 §1/§2/§5【取段——§4.16】——§4/§6 状态面走普通文档写);depth-0 的 status/path 面
17
+ * 按 D-BR21(可选 path,缺省 = 在飞批唯一时取)。
17
18
  */
18
19
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
19
20
  import { dirname, resolve } from "node:path"
20
21
 
21
22
  import {
22
- SEGMENT_BY_ROLE, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHasStatusLine, sectionHeaderRe, batchSkeleton,
23
+ SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHasStatusLine, sectionHeaderRe, batchSkeleton,
23
24
  TEMPLATE_PLACEHOLDERS, findPlaceholderResidue, placeholderResidueError,
24
25
  } from "./batch-skeleton.mjs"
25
26
  import { resolveBatchCreatePath } from "./batch-paths.mjs"
@@ -49,6 +50,18 @@ function segmentNumber(raw) {
49
50
  return m ? Number(m[1]) : null
50
51
  }
51
52
 
53
+ /** 档头两可选参归一(`BATCH-RECORD.md` §4.11 契约——逐字文本面 = `TOOLS.md` §6.20):trim ∥
54
+ * 单行(拒句逐字)∥ 空串 ≡ 未给(null);`ledger` 前导 `#` 补齐(board 无补齐)。给参 ⇒ 建即填
55
+ * 台账行(batchSkeleton 实参面);未给(任一)⇒ 占位留存 + 回执填法句。 */
56
+ function normalizeHeaderParam(name, raw) {
57
+ const v = typeof raw === "string" ? raw.trim() : ""
58
+ if (!v) return null
59
+ if (/\r?\n/.test(v)) {
60
+ throw new Error("batch: create ledger / board must be single lines — a multi-line value would break the header's 台账 line. Nothing was written.")
61
+ }
62
+ return name === "ledger" && !v.startsWith("#") ? `#${v}` : v
63
+ }
64
+
52
65
  /** main-agent-only 门(BR-19/BR-24):create/close 仅 depth-0 放行(eng 子代理 / 评审皆拒)。 */
53
66
  function assertMainAgentOnly(ctx, action, review) {
54
67
  if (review || !isDepthZero(ctx)) {
@@ -108,6 +121,10 @@ export function findInFlightBatch(cwd, bases) {
108
121
  * 路径解析 = `resolveBatchCreatePath`(§4.15 单源:候选序首个落基底内者 / 锚定串只解析项目根形)。
109
122
  * F11-B:`source` **必填**(topic 同款空拒——骨架编制行实参化;不传 = 死占位残留必被 F11-C 拒
110
123
  * ⇒ create 即拒,不留死锁);`prev` 传入值**幂等剥**「前情 = 」前缀(值规范化;默认值零变)。
124
+ * 2026-10-05(A-2):`ledger` / `board` 可选参**建即填**台账行(归一契约 = §4.11;回执两态 =
125
+ * 两参皆给 ⇒ 标注已填 ∥ 未给(任一)⇒ 填法句)。
126
+ * 2026-10-09(§4.16):骨架**按模式位取形**(`batchSkeleton({…, mode})`——执行期实读;普通面
127
+ * 作者标签四处替换,工程面骨架产出逐字不变)。
111
128
  * @returns {string} 成功消息(含落盘绝对路径)
112
129
  */
113
130
  export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten }) {
@@ -138,15 +155,24 @@ export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
138
155
  if (/\r?\n/.test(source)) {
139
156
  throw new Error("batch: create source must be a single line — a multi-line source would break the header's 编制 line. Nothing was written.")
140
157
  }
158
+ // 档头两可选参(§4.11 契约——给则建即填台账行;未给任一 ⇒ 占位留存 + 回执填法句)
159
+ const ledger = normalizeHeaderParam("ledger", args?.ledger)
160
+ const board = normalizeHeaderParam("board", args?.board)
141
161
  const date = typeof args?.date === "string" && args.date.trim() ? args.date.trim() : todayLocal()
142
162
  // F11-B:prev 幂等 strip——调用方自带「前情 = 」前缀(单或重复)一并剥净(`+` 量词;归一化目的
143
163
  // = 值规范化,无前缀值原样通过)。默认值形态「无(独立批)」零变。
144
164
  const prevRaw = typeof args?.prev === "string" ? args.prev.trim() : ""
145
165
  const prev = prevRaw ? (prevRaw.replace(/^(?:前情\s*[=::]\s*)+/g, "").trim() || "无(独立批)") : "无(独立批)"
146
166
  mkdirSync(dirname(abs), { recursive: true })
147
- writeFileSync(abs, batchSkeleton({ date, topic, source, prev }))
167
+ // 模式位执行期实读(§4.16——骨架取形时点 = 建档即固;翻转不回写既有骨架)
168
+ const mode = ctx?.agent?.config?.agent?.engineering ? "engineering" : "normal"
169
+ writeFileSync(abs, batchSkeleton({ date, topic, source, prev, ledger, board, mode }))
148
170
  onWritten?.(ctx?.agent ?? {}, abs)
149
- return `batch: created ${abs} — six-section skeleton written (§1 status line carries the gate-legal 「进行中」 placeholder; register the ledger entry yourself — create does not).`
171
+ // 回执两态(`TOOLS.md` §6.20 逐字):两参皆给 ⇒ 标注已填;未给(任一)⇒ 填法句(填法 = 文件编辑)
172
+ const headerNote = ledger !== null && board !== null
173
+ ? `header 台账 line filled (${ledger} · ${board})`
174
+ : "header 台账 placeholder(s) remain — fill them by file edit before the record's first append/status opens"
175
+ return `batch: created ${abs} — six-section skeleton written (§1 status line carries the gate-legal 「进行中」 placeholder; register the ledger entry yourself — create does not). ${headerNote}`
150
176
  }
151
177
 
152
178
  /**
@@ -260,24 +286,40 @@ function assertStatusNote(note) {
260
286
  }
261
287
 
262
288
  /**
263
- * status——状态行流转(§4.12,段属主)。写域 = 调用者自己段内 `**状态行**:` 行(eng-designer →
264
- * §2 · 评审 → §3 · eng-coder → §5 · 主 agent → §1——轮 2 #3 裁定②);值域 = STATUS_WORDS 该段项
265
- * 恰一词(**余核 = 关键词**——F11-A 谓词收紧),散文说明走独立 `note` 字段(落状态行括注);
266
- * 冻结真值不变(gate 只读 §1)。path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path
267
- * ⇒ 拒(目标 = spawn/实例注入,语法上写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):
268
- * 档头 / 目标段含骨架死占位 ⇒ 拒(close 不拦——收口是主 agent 终态动作)。
289
+ * status——状态行流转(§4.12,段属主;§4.16 模式感知)。写域 = 调用者自己段内 `**状态行**:`
290
+ * 行(评审 → §3 · eng-designer → §2 · eng-coder / coder → §5 · 主 agent:工程 §1 ∥ 普通
291
+ * §1/§2/§5 取段——缺省 §1);值域 = STATUS_WORDS 该段项恰一词(**余核 = 关键词**——F11-A
292
+ * 谓词收紧),散文说明走独立 `note` 字段(落状态行括注);冻结真值不变(gate 只读 §1)。
293
+ * path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path ⇒ 拒(目标 = spawn/实例注入,语法上
294
+ * 写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):档头结构行含骨架死占位 ⇒ 拒
295
+ * (段体引用豁免;close 不拦——收口是主 agent 终态动作)。
269
296
  * @returns {string} 成功消息
270
297
  */
271
298
  export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
272
299
  const depth0 = isDepthZero(ctx)
273
- const seg = review ? 3 : depth0 ? 1 : SEGMENT_BY_ROLE[ctx?.agent?._role] ?? null
300
+ // 模式位执行期实读(§4.16——同款先例 = task.mjs execute 门;翻转即时生效——D-BR26)。
301
+ // 写域:评审 → §3 · 子代理 → 身份段(普通模式分支合并 coder 增表——工程模式分支不查)·
302
+ // depth-0 工程 → §1 ∥ 普通 → §1/§2/§5(取段——缺省 §1)。
303
+ const engineering = !!ctx?.agent?.config?.agent?.engineering
304
+ let seg = review ? 3 : depth0 ? 1 : SEGMENT_BY_ROLE[ctx?.agent?._role] ?? (engineering ? null : SEGMENT_BY_ROLE_NORMAL[ctx?.agent?._role] ?? null)
274
305
  if (seg === null) {
275
306
  throw new Error("batch: no segment is writable by this caller — status writes the caller's OWN section (eng-designer → §2, design review → §3, eng-coder → §5, main agent → §1).")
276
307
  }
277
- // 声明段核对(BR-22「eng-designer 对 §1 调 status ⇒ 段白名单拒绝」——镜像 append 面:
278
- // segment 可声明,但声明段 ≠ 身份写域段 ⇒ 拒;身份写域段可省略——身份即写域)。
308
+ // 段域判据(BR-22「eng-designer 对 §1 调 status ⇒ 段白名单拒」——镜像 append 面):
309
+ // depth-0 普通面(§4.16)= **取段**——写域集 §1/§2/§5、缺省 §1、越出集合 ⇒ 拒(模式化新串);
310
+ // depth-0 工程面 ∥ 子代理 ∥ 评审 = **声明段核对**(声明段 ≠ 身份写域段 ⇒ 拒;身份写域段可省略)。
279
311
  const declared = args?.segment === undefined || args?.segment === null ? null : segmentNumber(args.segment)
280
- if (declared !== null && declared !== seg) {
312
+ // 传值而解析不可得 ⇒ 显式拒(#1116——镜像 append 面「unknown segment」串形;未传 = 缺省语义零变)。
313
+ if (declared === null && args?.segment !== undefined && args?.segment !== null) {
314
+ throw new Error(`batch: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${seg}"), or omit it. Nothing was written.`)
315
+ }
316
+ if (depth0 && !review && !engineering) {
317
+ const target = declared ?? 1
318
+ if (!DEPTH0_SEGMENTS.status.normal.includes(target)) {
319
+ throw new Error(`batch: §${declared} is not yours to write — in normal mode status writes §1/§2/§5 only (一段一作者: §3 = design review; §4/§6 carry no status word list). Nothing was written.`)
320
+ }
321
+ seg = target
322
+ } else if (declared !== null && declared !== seg) {
281
323
  throw new Error(`batch: §${declared} is not yours to write — status writes YOUR OWN section §${seg} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5, main agent → §1). Nothing was written.`)
282
324
  }
283
325
  if (args?.path !== undefined && args?.path !== null && !depth0) {
@@ -298,8 +340,8 @@ export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
298
340
  const abs = pickTarget(args?.path)
299
341
  const src = readFileSync(abs, "utf8")
300
342
  assertGateOpen(src)
301
- // F11-C(写盘前):档头 + 本段死占位残留 ⇒ 拒(他段占位不归本段作者管)
302
- const residues = findPlaceholderResidue(src, seg)
343
+ // F11-C(写盘前):档头结构行死占位残留 ⇒ 拒(段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20)
344
+ const residues = findPlaceholderResidue(src)
303
345
  if (residues.length) throw new Error(placeholderResidueError(residues))
304
346
  const written = updateSectionStatusLine(src, seg, lineValue)
305
347
  writeFileSync(abs, written)
@@ -7,9 +7,10 @@
7
7
  * 契约(§4.15 逐字):**候选序** = cwd → 项目根 → 逐基底(基底 = `batchDocBases` 声明面单源);
8
8
  * **判据分野** = 读面取首个**可读**文件 / create 取首个**落基底根内**者;**锚定防嵌套** = 串以任一基底的
9
9
  * 「项目根相对前缀」打头且前缀后为**路径段边界** ⇒ 只解析项目根形(不二次拼接;不落基底内 ⇒ fail-closed);
10
- * **歧义锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属显式拒;读面候选腿追加;
10
+ * **歧义锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属显式拒;读面候选分支追加;
11
11
  * 在飞 ∥ 写门基底集 = 候选并集(`batchDocBases`);**零变面** = 绝对路径照旧、三个错误文案逐字、
12
12
  * 「参数在 + 路径可读」判据句、`ok` / `none` 两态。
13
+ * **基底自有项目根分支 + 基底相对单段收窄**(条 6 · 2026-10-05 · #942)= 逐基底 `owningProject` 恰一落点收 ∥ 多落点显式拒(`ambiguousLandingError`);基底相对分支限单段(多段串不二次拼接)。
13
14
  *
14
15
  * 依赖(KD-4 单向):本档为**叶档**——只 import `node:*` 与 `../manifest.mjs`;主档 re-export 保既有面。
15
16
  */
@@ -98,8 +99,9 @@ const outsideBasesError = (raw) => new Error(`batch: create path resolves outsid
98
99
  const anchoredEscapeError = (raw) => new Error(`batch: create path is anchored at a batch base root but does not resolve under the declared docRoot.batches root(s) — refusing to nest it (fail-closed). Path: ${raw}`)
99
100
  /** 歧义锚 create 拒面(**文案逐字** = `BATCH-RECORD.md` §4.15 条 5——机检锚 `ambiguous session anchor`)。 */
100
101
  const ambiguousCreateError = (raw, candidates) => new Error(`batch: ambiguous session anchor — create target "${raw}" belongs to no candidate project; pass an explicit project path (e.g. "${candidates[0]}/docs/batches/<file>.md"). Candidates: ${candidates.join(", ")}`)
102
+ const ambiguousLandingError = (raw, landings) => new Error(`batch: create path resolves under more than one declared batch-record base — refusing to pick one (fail-closed); pass an explicit path (absolute preferred). Path: ${raw}. Candidates: ${landings.join(", ")}`)
101
103
 
102
- /** 歧义锚读面候选腿(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
104
+ /** 歧义锚读面候选分支(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
103
105
  * (候选按名排序、并集保序——「首个可读」在此即为确定判读)。 */
104
106
  function ambiguousReadLegs(cwd, p) {
105
107
  const amb = ambiguousCandidates(cwd)
@@ -149,7 +151,7 @@ export function resolveBatchDocPath(cwd, given) {
149
151
  }
150
152
 
151
153
  /**
152
- * **create 面**解析(§4.15 条 2/3):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
154
+ * **create 面**解析(§4.15 条 2/3/6):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
153
155
  * 根内**者(新档不存在——可读性无判别力);锚定串只解析项目根形,不落基底内 ⇒ fail-closed 新文案。
154
156
  * **歧义锚**(条 5):目标所属项目(`owningProject`)∈ 锚候选集 ⇒ 基底 = 该项目声明面(落其基底内;
155
157
  * 越出 ⇒ 既有 fail-closed);无所属 ⇒ **显式拒**(列候选 + 显式路径指引——不静默落锚)。
@@ -179,8 +181,26 @@ export function resolveBatchCreatePath(cwd, given, bases) {
179
181
  if (!insideBases(abs, roots)) throw anchoredEscapeError(raw)
180
182
  return abs
181
183
  }
182
- for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p), ...roots.map((b) => resolve(b, p))]) {
184
+ for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p)]) {
183
185
  if (insideBases(abs, roots)) return abs
184
186
  }
187
+ // ③ 基底自有项目根形(#942 条 6):owningProject 可判者逐基底取值;distinct 落点 ≥2 ⇒ 拒
188
+ const owned = []
189
+ for (const b of roots) {
190
+ const ownRoot = owningProject(b)
191
+ if (!ownRoot) continue
192
+ const abs = resolve(ownRoot, p)
193
+ if (insideBases(abs, roots) && !owned.some((x) => samePath(x, abs))) owned.push(abs)
194
+ }
195
+ if (owned.length > 1) throw ambiguousLandingError(raw, owned)
196
+ if (owned.length === 1) return owned[0]
197
+ // ④ 基底相对形收窄单段(归一后无 `/` 才走原基底分支——多段串不二次拼接)
198
+ const leafRel = roots.length ? relative(roots[0], resolve(roots[0], p)) : ""
199
+ if (leafRel && !leafRel.startsWith("..") && !leafRel.includes(sep)) {
200
+ for (const b of roots) {
201
+ const abs = resolve(b, p)
202
+ if (insideBases(abs, roots)) return abs
203
+ }
204
+ }
185
205
  throw outsideBasesError(raw)
186
206
  }