@thincoder/core 0.9.3 → 0.9.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.
- package/CHANGELOG.md +40 -0
- package/advisor/loop.mjs +2 -2
- package/advisor/run.mjs +1 -1
- package/agent/dispatch.mjs +21 -18
- package/agent/family-tools.mjs +6 -6
- package/agent/helpers.mjs +12 -2
- package/agent/run-stages.mjs +5 -5
- package/agent/suspension.mjs +1 -1
- package/agent-tools/advisor-async.mjs +2 -2
- package/agent-tools/advisor-settle.mjs +5 -5
- package/agent-tools/advisor.mjs +3 -3
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/batch-lifecycle.mjs +8 -16
- package/agent-tools/batch-paths.mjs +142 -0
- package/agent-tools/batch.mjs +10 -54
- package/agent-tools/consult.mjs +2 -2
- package/agent-tools/escalate-async.mjs +2 -2
- package/agent-tools/panel-blocks.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +21 -21
- package/agent-tools/subagent-async.mjs +11 -11
- package/agent-tools/subagent-panel.mjs +5 -5
- package/agent-tools/subagent-run.mjs +8 -8
- package/agent-tools/subagent-scheduler.mjs +1 -1
- package/agent-tools/subagent-spawn.mjs +13 -12
- package/agent-tools/subagent.mjs +13 -13
- package/agent.mjs +13 -7
- package/config-presets.mjs +3 -3
- package/git/checkpoint.mjs +4 -8
- package/ledger-db.mjs +25 -5
- package/ledger-migrate.mjs +299 -0
- package/ledger.mjs +2 -1
- package/model-specs.mjs +131 -60
- package/package.json +1 -1
- package/peer-claims.mjs +263 -0
- package/peer-domains.mjs +93 -60
- package/prompts/common.md +4 -0
- package/prompts/persona-coder.md +1 -1
- package/prompts/persona-eng-coder.md +2 -2
- package/prompts/persona-eng-designer.md +2 -2
- package/prompts/persona-engineering.md +9 -0
- package/prompts/persona-explore.md +1 -1
- package/prompts/persona-normal.md +9 -0
- package/prompts/persona-plan.md +3 -3
- package/session-gc.mjs +67 -6
- package/session-slots-manifest.mjs +5 -0
- package/tool-docs/question.md +1 -0
- package/tools/edit-batch.mjs +6 -7
- package/tools/edit-diff.mjs +29 -2
- package/tools/file.mjs +7 -3
- package/tools/glob-dialect.mjs +3 -3
- package/tools/search.mjs +2 -2
- package/tools/shared.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
All notable changes to the core package are documented here.
|
|
4
4
|
Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
|
|
5
5
|
|
|
6
|
+
## [0.9.5] — 2026-09-25
|
|
7
|
+
|
|
8
|
+
> 0.9.4 → 0.9.5(月内序号——发布时定号)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **规格表覆盖扩面(新名专行)**:`qwen-flash` / `qwen-vl-max`(实测输出上限 32_768——服务端直报 `[1, 32768]`;前者思考默认可开、后者不支持)、`claude-fable-5.1`(1_000_000 / 128_000)、`gemini-3.1-pro`(1_048_576 / 65_536)、`stepfun/step-3.7-flash`(262_144);kimi-code 平台三行(`kimi-for-coding` / `kimi-for-coding-highspeed` / `k3-256k`——含 effort 枚举与默认档)——掉兜底名不再退默认。
|
|
13
|
+
- **bench 受测 10 档规格行**:此前经泛前缀/兜底解析的受测模型补独立行 + 视觉声明分态登记。
|
|
14
|
+
- **跨实例意图认领层(P1)**:`peers/{sessionId}.json` 增 claims 意图域(TTL 30 分钟租约 · 进程死亡即失效);写前命中他实例活认领 ⇒ 软提示(不阻写;与写后足迹登记分开存)。
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **提示词面**:`question` 工具从子代理工具表排除(子代理无提问通道——可见但不可用,不再注入);子代理**在途提问通道**(问询即上抛,不等收尾)与「矛盾要求 ⇒ 立即 ask、不得自行解套」自止边界落档(common + 各人格面)。
|
|
19
|
+
- **`glm-5.3-flash` 行 `maxOutput` 128_000 → 131_072**(校验级实测;本体 `glm-5.3` 128_000 冻结零改)。
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
|
|
23
|
+
- **`cacheMode` 字段整体删除**:全仓零判据消费的死字段(假能力位——看着像能力声明、实际零行为);表键 / 行注 / `DEFAULT_SPEC` / 注释提及字面清零。
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **deepseek 预设 `maxTokens` 393_216 → 384_000**(对齐规格行——消 K×1024 vs 十进制口径差;预置 ≤ 规格不变式 + 历史超限白名单锁定)。
|
|
28
|
+
- **edit 工具非数组 `edits` 入参** ⇒ 成形错误(原裸 `TypeError` 外泄;core 与 ACP 桥同收)。
|
|
29
|
+
- **台账库键跨端归一**(盘符大小写 ⇒ 同项目双库)+ `ledger-migrate` 迁移引擎(备份两道 → dry-run → confirm → 记存根审计;源库进回收不删)。
|
|
30
|
+
- **batch 工具相对路径基底统一**(create / append / 评审门 / spawn 门四调用点单源解析 + 防嵌套 fail-closed)。
|
|
31
|
+
- **会话 GC 加固**:无主会话清扫(无活属主 + cwd 不可达/空 + 7 天窗收口)· 多处指针/文句收正。
|
|
32
|
+
|
|
33
|
+
## [0.9.4] — 2026-09-22
|
|
34
|
+
|
|
35
|
+
> 0.9.3 → 0.9.4(月内序号——发布时定号)
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **MiMo V2.6 三款规格行**(`mimo-v2.6-pro` / `mimo-v2.6-flash` / `mimo-v2.6-pro-ultraspeed`——同日上架):此前缺行落兜底(128K / 32K / 无视觉),现按真机实测登记——1M 上下文 · 131_072 输出 · 思考默认开(`thinking.type`)· 全模态 · 自动缓存。
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **MiMo v2.5 两行存量对齐**(同日实测):`maxOutput` 128_000 → 131_072 + 补自动缓存登记;族头注与 `assistantToolCallMessage` 文书句按 2026-09-22 复测改写(原「缺字段 → 400」句已不可复现——回显策略保持保守不变)。
|
|
44
|
+
- **MiMo 预设改指**:`mimo` / `mimoplan` 默认模型 → `mimo-v2.6-pro`(v2.5 官网标注即将下线;Token Plan 端点同平台推断)。
|
|
45
|
+
|
|
6
46
|
## [0.9.3] — 2026-09-22
|
|
7
47
|
|
|
8
48
|
> 0.9.2 → 0.9.3(月内序号——发布时定号)
|
package/advisor/loop.mjs
CHANGED
|
@@ -40,8 +40,8 @@ function advisorCodeSearchTool(agent) {
|
|
|
40
40
|
|
|
41
41
|
function advisorToolsFor(agent, reviewType = "code", batchDoc = null) {
|
|
42
42
|
const tools = [readTool, globTool, grepTool, lsTool, lspTool, advisorCodeSearchTool(agent)]
|
|
43
|
-
// §
|
|
44
|
-
// 逐字节不变(零 git +
|
|
43
|
+
// BATCH-RECORD.md §4.3(第 4 批):**只有绑定了批次档的设计评审**额外拿到写通道——代码评审工具集
|
|
44
|
+
// 逐字节不变(零 git + 只读不变量,BATCH-RECORD.md §4.7 #1);未绑定 → 不挂载(fail-closed)。
|
|
45
45
|
if (reviewType === "design" && batchDoc) tools.push(batchTool(batchDoc, { review: true }))
|
|
46
46
|
return { schemas: tools.map(toOpenAISchema), byName: new Map(tools.map((t) => [t.name, t])) }
|
|
47
47
|
}
|
package/advisor/run.mjs
CHANGED
|
@@ -122,7 +122,7 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// §
|
|
125
|
+
// BATCH-RECORD.md §4.3 批次档写通道的绑定(仅设计评审):同步路径 = 调用方(advisor 工具)传入的
|
|
126
126
|
// callbacks.batchDoc(即 resolved.run 的实例绑定);异步路径 = 本实例在跑池条目的
|
|
127
127
|
// run.batchDoc(同文档集实例键——各评审各取各档,不用单值会话态)。
|
|
128
128
|
const boundBatchDoc = reviewType === "design" ? batchDocForReview(agent, documents, callbacks) : null
|
package/agent/dispatch.mjs
CHANGED
|
@@ -6,9 +6,9 @@ import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
|
|
|
6
6
|
import { runHooks } from "../hooks.mjs"
|
|
7
7
|
import { snapshotForUndo } from "../undo-stack.mjs"
|
|
8
8
|
import { isCodePath, loadConventions } from "../conventions.mjs"
|
|
9
|
-
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):写工具钩子——peerCollabNote
|
|
10
|
-
//
|
|
11
|
-
import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites } from "../peer-domains.mjs"
|
|
9
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b / §4.4 D-MI17–D-MI21):写工具钩子——peerCollabNote
|
|
10
|
+
// (执行前冲突与认领命中检测——软提示不阻止)+ recordPeerWrites(成功后足迹与认领登记)。
|
|
11
|
+
import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites, markClaimNoted } from "../peer-domains.mjs"
|
|
12
12
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
13
13
|
import { join, resolve, relative } from "node:path"
|
|
14
14
|
import { homedir } from "node:os"
|
|
@@ -51,18 +51,18 @@ function logToolError(toolName, args, error) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* action-level classification (AGENT-LOOP-SUBAGENT.md §6.7 D-M1): the merged subagent
|
|
55
55
|
* tool expresses spawn (side effect) and status (read-only query) through
|
|
56
56
|
* its `action` parameter — the tool-level readonly flag can no longer express both.
|
|
57
57
|
* dispatch Phase-1/Phase-2 classifies per action: status behaves as readonly
|
|
58
58
|
* (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
|
|
59
59
|
* gates, escalate runs non-readonly AND serially (the retired escalate tool had no
|
|
60
60
|
* parallel flag — zero behavior change under the merged surface).
|
|
61
|
-
* §
|
|
61
|
+
* AGENT-LOOP-SUBAGENT.md §6.7.2 cancel (19.5.2b round2 #4): CONTROL-class exemption — cancel only
|
|
62
62
|
* stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
|
|
63
63
|
* readonly (planMode pass / no permission ask — never joins a batch approval
|
|
64
64
|
* group / no handler → not denied — digest 内 cancel 放行).
|
|
65
|
-
* §
|
|
65
|
+
* AGENT-LOOP-SUBAGENT.md §6.7.2 panel (round1 #5): view 面归只读类(同 status——planMode 放行、免
|
|
66
66
|
* 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
|
|
67
67
|
* 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
|
|
68
68
|
*/
|
|
@@ -83,11 +83,11 @@ function isSubagentReadonlyAction(toolName, args) {
|
|
|
83
83
|
}
|
|
84
84
|
if (toolName !== "subagent" || !args || typeof args !== "object") return false
|
|
85
85
|
const action = args.action
|
|
86
|
-
// §
|
|
86
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.5: check 动作已删除——只读面仅剩 status(planMode 放行/免权限审批/可批并行)
|
|
87
87
|
if (action === "status") return true
|
|
88
88
|
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行)
|
|
89
89
|
if (action === "observe") return true
|
|
90
|
-
// §
|
|
90
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
|
|
91
91
|
if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
|
|
92
92
|
return false
|
|
93
93
|
}
|
|
@@ -97,7 +97,7 @@ function isSubagentControlAction(toolName, args) {
|
|
|
97
97
|
// SUBAGENT-OBSERVE-SEND:send = 控制类豁免(同 cancel——父回合内显式调用即授权——
|
|
98
98
|
// 写子输入队列属父对子轻量引导,非产品代码写——免审批、planMode 放行、digest 内放行)
|
|
99
99
|
if (args?.action === "send") return true
|
|
100
|
-
// §
|
|
100
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
|
|
101
101
|
if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
|
|
102
102
|
return false
|
|
103
103
|
}
|
|
@@ -352,11 +352,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
352
352
|
const toolName = item.toolCall.name
|
|
353
353
|
logEvent("tool:call", { tool: toolName, child: agent?._logId })
|
|
354
354
|
try {
|
|
355
|
-
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b)
|
|
356
|
-
//
|
|
357
|
-
//
|
|
355
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b / §4.4.4):结构化写工具执行前查冲突与认领
|
|
356
|
+
// 命中(软提示——决策⑥ A 不阻止;一次目录 stat——N3 度量);足迹累积(D-L3a——"检测+
|
|
357
|
+
// 记录一次完成")与认领登记延后到执行成功(实际写过的文件)。
|
|
358
358
|
const isPeerWriteTool = PEER_WRITE_TOOLS.has(toolName)
|
|
359
|
-
const peerNote = isPeerWriteTool ? peerCollabNote(agent
|
|
359
|
+
const peerNote = isPeerWriteTool ? peerCollabNote(agent, item.tool, item.args) : null
|
|
360
360
|
// Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
|
|
361
361
|
if (!item.tool?.readonly && item.args) {
|
|
362
362
|
snapshotForUndo(agent, item.toolCall.name, item.args, agent.cwd)
|
|
@@ -367,7 +367,8 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
367
367
|
const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
|
|
368
368
|
if (routed?.handled) {
|
|
369
369
|
const routedOk = !String(routed.result).startsWith("Error:")
|
|
370
|
-
const routedResult = peerNote && routedOk ? `${routed.result}\n${peerNote}` : routed.result
|
|
370
|
+
const routedResult = peerNote && routedOk ? `${routed.result}\n${peerNote.text}` : routed.result
|
|
371
|
+
if (peerNote && routedOk) markClaimNoted(agent, peerNote.keys) // 提示已附加 ⇒ 落去重标记
|
|
371
372
|
if (isPeerWriteTool && routedOk) recordPeerWrites(agent, item.tool, item.args)
|
|
372
373
|
// §29 fix A:routed 写成功(客户端执行)同样执行期即刻记账(唯一记账点)
|
|
373
374
|
if (routedOk && FILE_MUTATORS.has(toolName)) noteExecutedMutation(agent, item.tool, item.args)
|
|
@@ -434,9 +435,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
434
435
|
const resultWithConsole = capturedConsole.length > 0
|
|
435
436
|
? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
436
437
|
: result
|
|
437
|
-
// R10 L3
|
|
438
|
-
const
|
|
439
|
-
|
|
438
|
+
// R10 L3:冲突 / 认领软提示附在工具结果末尾(模型可见——不阻止写;提示附加成功才落去重标记)
|
|
439
|
+
const notedOk = Boolean(peerNote) && !raw.startsWith("Error:")
|
|
440
|
+
if (notedOk) markClaimNoted(agent, peerNote.keys)
|
|
441
|
+
const resultForModel = notedOk
|
|
442
|
+
? `${resultWithConsole}\n${peerNote.text}`
|
|
440
443
|
: resultWithConsole
|
|
441
444
|
callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
|
|
442
445
|
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
@@ -478,7 +481,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
478
481
|
}
|
|
479
482
|
for (const item of prepared) {
|
|
480
483
|
// escalate action keeps the retired escalate tool's serial placement (no
|
|
481
|
-
// parallel flag): it flushes the batch and runs alone in call order (§
|
|
484
|
+
// parallel flag): it flushes the batch and runs alone in call order (AGENT-LOOP-SUBAGENT.md §6.7 —
|
|
482
485
|
// spawn stays parallel; status classifies as readonly and batch freely).
|
|
483
486
|
if (item.tool && !item.tool.readonly
|
|
484
487
|
&& (!item.tool.parallel || isSubagentEscalateAction(item.tool.name, item.args))) {
|
package/agent/family-tools.mjs
CHANGED
|
@@ -63,7 +63,7 @@ export async function assembleFamilyTools({
|
|
|
63
63
|
properties: {
|
|
64
64
|
...subagentTool.parameters.properties,
|
|
65
65
|
role: { ...subagentTool.parameters.properties.role, ...subagentRoles },
|
|
66
|
-
// §
|
|
66
|
+
// AGENT-LOOP-SUBAGENT.md §6.7: escalate 动作的候选池 = consultModels(缺省池首 / 指定 provider:model)。
|
|
67
67
|
// 池装饰挂在 action 属性描述(原 escalate 工具注册时 withPool 同款意图——模型
|
|
68
68
|
// 需要知道可选候选人)。escalate 在工程模式禁用——装饰只对正常模式有意义。
|
|
69
69
|
action: (consultModels.length && !engineering)
|
|
@@ -80,15 +80,15 @@ export async function assembleFamilyTools({
|
|
|
80
80
|
// §18 D-E3 + ENGINEERING-MODE.md §2.15 D(第 2 批——参数化复用,不并列第二个 IIFE):
|
|
81
81
|
// 工程子代理(depth>0;eng-coder = 偏差审计 / eng-designer = 自己勘察)get a restricted
|
|
82
82
|
// spawn channel — role enum limited to explore, NO async parameter (sync only) and action
|
|
83
|
-
// pinned to spawn
|
|
83
|
+
// pinned to spawn(AGENT-LOOP-SUBAGENT.md §6.7 D-M3 restricted-variant action gate——escalate/check/status are
|
|
84
84
|
// refused here at the schema level too;the mechanical re-check lives in subagent.mjs
|
|
85
|
-
// execute → the §
|
|
85
|
+
// execute → the AGENT-LOOP-SUBAGENT.md §6.7 action gate + gateEngCoderSpawn (spawn-child.mjs) — schema enums are
|
|
86
86
|
// advisory, providers don't enforce them)。描述文案按父角色分流(审计 vs 勘察)。
|
|
87
87
|
const engChildRole = depth > 0 && (role === "eng-coder" || role === "eng-designer") ? role : null
|
|
88
88
|
const engChildSubagent = engChildRole
|
|
89
89
|
? (() => {
|
|
90
90
|
const props = { ...subagentTool.parameters.properties }
|
|
91
|
-
// §
|
|
91
|
+
// AGENT-LOOP-SUBAGENT.md §6.7 review hygiene: the child channel is spawn-only sync explore — drop
|
|
92
92
|
// async, the check/status params (id/n), the eng-coder token params
|
|
93
93
|
// (designToken/designId are meaningless for a read-only spawn; the parent
|
|
94
94
|
// spawn already carried the token) and batchDoc (an audit child derives no
|
|
@@ -130,7 +130,7 @@ export async function assembleFamilyTools({
|
|
|
130
130
|
: null
|
|
131
131
|
|
|
132
132
|
// consult 工具仅在配置时注册(consultModels 空池时注册会让模型调用后吃一个错误回合)——
|
|
133
|
-
// §
|
|
133
|
+
// AGENT-LOOP-SUBAGENT.md §6.7: escalate 已并入常驻 subagent 的 action:"escalate"(无空池注册问题——动作在
|
|
134
134
|
// 池空时返回既有错误语义,工程模式 fail-closed 在 execute 内拒绝)。
|
|
135
135
|
// §25 D-R17a: consult_check 已退役(digest 自动注入是唯一消费通道)——consult 家族
|
|
136
136
|
// 只剩 2 工具(consult_start/consult_stop——setup 注册点与描述面同步清零)。
|
|
@@ -159,7 +159,7 @@ export async function assembleFamilyTools({
|
|
|
159
159
|
// eng-designer (§2.15 D): the survey-only subagent channel alone — no advisor
|
|
160
160
|
// (it does not fire reviews) and no verify (its deliverable is documents, not code).
|
|
161
161
|
// BATCH-RECORD §4.3 挂载表(批次档生命周期工具化批):eng 两分支各挂主名 `batch`
|
|
162
|
-
// (绑定段 append/status;目标 = spawn 绑定 `child._batchDoc
|
|
162
|
+
// (绑定段 append/status;目标 = spawn 绑定 `child._batchDoc`)——BATCH-RECORD.md §4.2 的 batch_segment
|
|
163
163
|
// 挂载形态已随单名化收口退役(过渡别名 §4.14 不入生产挂载面);depth-0 主 agent 同表挂载
|
|
164
164
|
// `batch`(D-BR18 扩权——create/close + append §1/§4/§6 + status §1(轮 2 裁定②:§4/§6
|
|
165
165
|
// 状态面走普通文档写),目标走可选 path / 在飞扫描)。
|
package/agent/helpers.mjs
CHANGED
|
@@ -210,7 +210,7 @@ export class ContinueError extends Error {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
/** §
|
|
213
|
+
/** AGENT-LOOP-ASYNC-POOL.md §6.8 D-S6 auto-turn guard 标记集(核内单源):快照/回填两 helper 共用本清单——
|
|
214
214
|
* 端侧宿主载体(`panel._guardCarry`)与核载体(`agent._inheritedGuard`)同清单。 */
|
|
215
215
|
export const INHERITED_GUARD_KEYS = [
|
|
216
216
|
"_mutatedThisRun", "_verifiedThisRun", "_verifyPassed", "_calledAdvisorThisRun",
|
|
@@ -231,7 +231,7 @@ export function restoreGuard(target, snap) {
|
|
|
231
231
|
for (const k of INHERITED_GUARD_KEYS) if (k in snap) target[k] = snap[k]
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
/** 跨段累计编号帧(TURN-CAP-CONTINUE.md §
|
|
234
|
+
/** 跨段累计编号帧(TURN-CAP-CONTINUE.md §4——第 19 批 TURN-ACROSS-SEGMENTS):
|
|
235
235
|
* 唯一计算点(纯函数)——把链内累计序数换算成面向消费面的编号载荷。
|
|
236
236
|
* - seq = 该轮链内累计序数(1 起——`agent._turnSeq` 每轮 +1,续跑不重置)
|
|
237
237
|
* - turn = 段内轮号(0 起——段内帽判定的循环变量)
|
|
@@ -337,6 +337,16 @@ export function readonlyToolNames(tools) {
|
|
|
337
337
|
return new Set(tools.filter((t) => t.readonly).map((t) => t.name))
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
/** Depth>0 tool exclusions — subagents never get the interactive main-session tools (`question`:
|
|
341
|
+
* no user to ask; the routing guidance already lives in every persona file). Single source for
|
|
342
|
+
* the repo: the shell-side depth>0 filter consumes this set (design = TOOLS.md §6.16 / D-TO11). */
|
|
343
|
+
export const SUBAGENT_TOOL_EXCLUSIONS = new Set(["question"])
|
|
344
|
+
|
|
345
|
+
/** Apply the depth>0 exclusion set — always returns a NEW array (parent table untouched). */
|
|
346
|
+
export function excludeSubagentTools(tools) {
|
|
347
|
+
return tools.filter((t) => !SUBAGENT_TOOL_EXCLUSIONS.has(t.name))
|
|
348
|
+
}
|
|
349
|
+
|
|
340
350
|
const MAX_INSTRUCTION_CHARS = 32_000
|
|
341
351
|
|
|
342
352
|
/** Load AGENTS.md / project_rules.md from user home and project root.
|
package/agent/run-stages.mjs
CHANGED
|
@@ -171,7 +171,7 @@ export async function finalizeAgentTurn(agent, ctx) {
|
|
|
171
171
|
// resumes); the Ctrl+C abort branch below is the only path that aborts them
|
|
172
172
|
// (cleanupConsultSessions — marked stopped → no digest), and the suspension
|
|
173
173
|
// driver aborts them on its own abort unwind.
|
|
174
|
-
// Async subagent turn-end handling (AGENT-LOOP.md §15 D-A3 + §
|
|
174
|
+
// Async subagent turn-end handling (AGENT-LOOP.md §15 D-A3 + AGENT-LOOP-ASYNC-POOL.md §6.8 D-S1). Lifecycle:
|
|
175
175
|
// - Ctrl+C (plain abort): children were aborted with the parent signal — discard dead
|
|
176
176
|
// entries (tombstone/out-of-pool/notice — AGENT-LOOP-ASYNC-POOL.md §6.20; no stale
|
|
177
177
|
// errors injected — user explicitly stopped); consultation sessions are cross-turn
|
|
@@ -217,7 +217,7 @@ export async function finalizeAgentTurn(agent, ctx) {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
agent._inAutoTurn = false
|
|
220
|
-
// §
|
|
220
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-S6: auto-turn guard marks survive into the next USER run (restored at its
|
|
221
221
|
// !resume reset above). Normal ends only — abort discards; ContinueError lets the
|
|
222
222
|
// auto-resumed run snapshot at its own end.
|
|
223
223
|
if (autoTurn && !(signal?.aborted && !signal?.reason?.interrupt) && !(thrownError instanceof ContinueError)) {
|
|
@@ -236,8 +236,8 @@ export async function finalizeAgentTurn(agent, ctx) {
|
|
|
236
236
|
* queued STAY — no allSettled wait. Results never lost without a session.
|
|
237
237
|
* - suspDriven=true (the interaction layer runs suspensionSession after this
|
|
238
238
|
* run): NO direct inject — settled entries STAY pooled (settled not consumed)
|
|
239
|
-
* for the session's first sweepSettledToPending → digest turn (§
|
|
240
|
-
* 条目留池等消化轮注入;status 在 sweep 前仍从池读——17.5.2
|
|
239
|
+
* for the session's first sweepSettledToPending → digest turn (AGENT-LOOP-ASYNC-POOL.md §6.8 — done
|
|
240
|
+
* 条目留池等消化轮注入;status 在 sweep 前仍从池读——17.5.2 不变面(AGENT-LOOP-SUBAGENT.md §6.7.5:check 已删——自动通道为唯一消费方))。
|
|
241
241
|
* maybeRefillAsync runs in both modes (starts queued heads whose slot freed).
|
|
242
242
|
* Single ownership: entries settled inside a suspension session were moved to
|
|
243
243
|
* _pendingAsyncResults by the settle callback, so this only sees user-turn
|
|
@@ -250,7 +250,7 @@ async function collectSettledAsync(agent, { suspDriven = false } = {}) {
|
|
|
250
250
|
if (maps.length === 0) return false
|
|
251
251
|
const { maybeRefillAsync, injectAsyncResult } = await import("../agent-tools/subagent.mjs")
|
|
252
252
|
maybeRefillAsync(agent) // start queued heads now that slots may have freed — no waiting
|
|
253
|
-
if (suspDriven) return false // §
|
|
253
|
+
if (suspDriven) return false // AGENT-LOOP-ASYNC-POOL.md §6.8: settled stays pooled — the suspension session digests it
|
|
254
254
|
let injectedAdvisor = false
|
|
255
255
|
for (const map of maps) {
|
|
256
256
|
for (const e of [...map.values()]) {
|
package/agent/suspension.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agent/suspension.mjs —
|
|
2
|
+
* agent/suspension.mjs — 挂起 / 唤醒驱动(核内形态——AGENT-LOOP.md §2.3 #184 定案;机制面 = AGENT-LOOP-ASYNC-POOL.md §6.8)。
|
|
3
3
|
*
|
|
4
4
|
* 语义(两端同源,逐字承两端现行挂起驱动——CLI 侧 tui 面 / VSC 侧 extension 面,同语义移植):
|
|
5
5
|
* 回合尾后台池仍 live → 挂起会话——池项 settle → 入 pending;pending 非空 **或存在未 drain 的
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The advisor tool runs reviews in a background pool at depth 0 (default async —
|
|
5
5
|
* ruling ②-3 A): the launch returns an ack, the turn ends naturally, the session
|
|
6
6
|
* suspends, the review settles into _pendingAsyncResults and a digest turn
|
|
7
|
-
* delivers the report (§
|
|
7
|
+
* delivers the report (AGENT-LOOP-ASYNC-POOL.md §6.8 consumption machinery — role-agnostic; entries carry
|
|
8
8
|
* role "advisor" and ride the subagent panel as a pseudo-role, ruling ②-4 A).
|
|
9
9
|
*
|
|
10
10
|
* This module owns:
|
|
@@ -51,7 +51,7 @@ import { normAbs, mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
|
|
|
51
51
|
export {
|
|
52
52
|
mutationSeqOf, noteMutations, reviewIsStale, settleAdvisorRun, inflightDesignReviewConflict,
|
|
53
53
|
} from "./advisor-settle.mjs"
|
|
54
|
-
// 第 33
|
|
54
|
+
// 第 33 批(评审失败护栏):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
|
|
55
55
|
// 本文件继续在实例解析 / 池 entries 上消费它。
|
|
56
56
|
import { docSetKey } from "./review-facts.mjs"
|
|
57
57
|
// F31(2026-09-18 顾问面治理批):拒回文案对象标识行单源。
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `reviewIsStale` 同族(同 docAbs / 同 normAbs)、仅扫 running 且未取消的设计条目
|
|
15
15
|
* (dispatch Phase-1 预闸消费)。
|
|
16
16
|
*
|
|
17
|
-
* 第 33 批(2026-09-11
|
|
17
|
+
* 第 33 批(2026-09-11——评审失败护栏):`normAbs` 本体迁 `review-facts.mjs`
|
|
18
18
|
* (原处 re-export 保面);**2026-09-18 顾问面治理批**:计数落账(`designReviewOutcome` →
|
|
19
19
|
* `noteDesignReviewOutcome`)随会话级计数器整体退场——改治**失败结论**:结算出口按
|
|
20
20
|
* `settlementCriterion`(纯函数单源,`advisor/notice.mjs`)追加结论块(**两轨共用**——
|
|
@@ -25,7 +25,7 @@ import { settleDesignReview, makeDesignTokenRegex, stripApprovedSuffix } from ".
|
|
|
25
25
|
import { looksLikeReviewOutput, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
|
|
26
26
|
import { isCodePath, loadConventions } from "../conventions.mjs"
|
|
27
27
|
import { logEvent } from "../log.mjs"
|
|
28
|
-
// 第 33
|
|
28
|
+
// 第 33 批(评审失败护栏):`normAbs` 迁 `review-facts.mjs`(中立模块——陈旧判定与事实面共用同一归一);
|
|
29
29
|
// 2026-09-18:结算分类 / 结论文案单源迁 `advisor/notice.mjs`(纯函数、零状态、零计数载体)。
|
|
30
30
|
import { normAbs } from "./review-facts.mjs"
|
|
31
31
|
import { settlementCriterion, buildSettlementConclusion, scopeSummary } from "../advisor/notice.mjs"
|
|
@@ -51,7 +51,7 @@ export function noteMutations(agent, paths) {
|
|
|
51
51
|
if (log.length > 200) log.splice(0, log.length - 200)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// 第 33
|
|
54
|
+
// 第 33 批(评审失败护栏):`normAbs` 本体迁 `review-facts.mjs`;原处 re-export 保既有 import 面
|
|
55
55
|
// (advisor-async / 测试)零变。
|
|
56
56
|
export { normAbs } from "./review-facts.mjs"
|
|
57
57
|
|
|
@@ -121,7 +121,7 @@ export function inflightDesignReviewConflict(agent, absPaths) {
|
|
|
121
121
|
* 第 11 批增补(A/F11):`incomplete` = 宿主尾族判定(单谓词)——design 结算未完成即不
|
|
122
122
|
* 签发(`settleDesignReview` 的 `opts.incomplete`);code 完成守卫的失败判定改用同谓词
|
|
123
123
|
* (六形态语义零丢——旧 `^` 锚正则退役)。
|
|
124
|
-
* 第 33
|
|
124
|
+
* 第 33 批增补(评审失败护栏):design 结算按分类单源落账到会话级护栏——**2026-09-18 撤**(零载体)。
|
|
125
125
|
* 现行:结算出口按判据名单源(`settlementCriterion`)追加失败结论块(两轨共用;F28/F29)——
|
|
126
126
|
* 判据名非空 ⇒ 块在位(块首行 = 对象标识行);可用判决 / 中断 / 拒发 ⇒ 零噪声(无块)。
|
|
127
127
|
* Cancelled / parent-aborted reviews consume nothing (the user dropped the
|
|
@@ -143,7 +143,7 @@ export function settleAdvisorRun(agent, entry) {
|
|
|
143
143
|
// 宿主尾族判定(单谓词——design 结算与 code 守卫共用;§14.3 消费点 1/2)。
|
|
144
144
|
const incomplete = result != null ? advisorIncompleteMarker(String(result)) : null
|
|
145
145
|
// 启动拒绝报告(未发起请求——无评审产出):第 33 批上移为本结算段**单点**——消费 = ① design
|
|
146
|
-
// 失败分类(neutral
|
|
146
|
+
// 失败分类(neutral——无尝试发生;`ADVISOR-GUARDS.md` §7 失败结算表行 1)② code 守卫 failureVerdict(既有语义零变)。
|
|
147
147
|
const launchRefused = result != null && String(result).startsWith(ADVISOR_LAUNCH_REFUSAL_PREFIX)
|
|
148
148
|
if (!stale) {
|
|
149
149
|
if (run.reviewType === "design" && entry.designToken && result) {
|
package/agent-tools/advisor.mjs
CHANGED
|
@@ -174,10 +174,10 @@ export const advisorTool = {
|
|
|
174
174
|
// scopes agent._advisorRound/_lastAdvisorOutput so the message builder and
|
|
175
175
|
// the run.mjs cap read THIS instance's round/prior (multi-review isolation).
|
|
176
176
|
const resolved = resolveAdvisorLaunch(agent, reviewType, { documents })
|
|
177
|
-
// §
|
|
177
|
+
// BATCH-RECORD.md §4.2 评审侧批次档门禁 + 实例键绑定(batch_segment 的唯一路径来源):
|
|
178
178
|
// 口径 = **「若传则须可读」**(空/不可读 → throw;不强制必传——无批次档的在途设计评审
|
|
179
179
|
// 零回归,N5);绑定落在 resolved.run(评审实例键,与 reviewType/round/designId 同族)——
|
|
180
|
-
//
|
|
180
|
+
// 并发设计评审各绑各档,不用单值会话态(BATCH-RECORD.md §4.7 #6)。
|
|
181
181
|
if (args.batchDoc !== undefined && args.batchDoc !== null) {
|
|
182
182
|
resolved.run.batchDoc = resolveBatchDocPath(agent.cwd, args.batchDoc)
|
|
183
183
|
}
|
|
@@ -246,7 +246,7 @@ export const advisorTool = {
|
|
|
246
246
|
|
|
247
247
|
// B 启动拒绝(§14.4 #2——稳定前缀):拒发登记(同池满 / cap 款——不置 called、不耗轮次),
|
|
248
248
|
// 可见报错照常返回(record-results 的 REFUSED 契约)。第 33 批:判定单点——同时供设计
|
|
249
|
-
// 失败分类复用(launchRefused ⇒ neutral
|
|
249
|
+
// 失败分类复用(launchRefused ⇒ neutral——无尝试发生;`ADVISOR-GUARDS.md` §7 失败结算表行 1)。
|
|
250
250
|
const launchRefused = String(result).startsWith(ADVISOR_LAUNCH_REFUSAL_PREFIX)
|
|
251
251
|
if (launchRefused && ctx._toolCallId !== undefined) {
|
|
252
252
|
(agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
@@ -232,7 +232,7 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
|
|
|
232
232
|
}
|
|
233
233
|
// ③ 分流
|
|
234
234
|
if (entry.cancelled) {
|
|
235
|
-
// §
|
|
235
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 cancelled settle 分支(D-M6):不入 pending、不参与回合尾直注入——清池 +
|
|
236
236
|
// 终态墓碑(dependsOn 取消语义)+ ⟦ev⟧stopped 冻结 + 族提醒(半成品警示对模型可见)。
|
|
237
237
|
pool?.delete(String(entry.id))
|
|
238
238
|
writeTombstone(parent, entry.id, "cancelled", role) // §20 D-SD5:单点写入(#94 载体吸收)
|
|
@@ -263,7 +263,7 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
|
|
|
263
263
|
// 族记账 hook(advisor 陈旧判定/token D1 落盘;escalate 三分类 merge 决策;subagent/
|
|
264
264
|
// escalate 腾槽补位)——可改写 entry.report(settle 分支输出——digest 原样进)。
|
|
265
265
|
onAccounting?.(parent, entry)
|
|
266
|
-
//
|
|
266
|
+
// 挂起分流(AGENT-LOOP-ASYNC-POOL.md §6.8 D-S8 + D-S3 记账——以读取时刻为准):挂起期 settle → 移交 pending
|
|
267
267
|
// 单容器(digest 注入)+ 出池 + ⟦ev⟧settled 驻留;回合内 → ⟦ev⟧done 立即冻结
|
|
268
268
|
// (条目留池——done-in-pool 统一表示——回合尾 collectSettledAsync 注入)。
|
|
269
269
|
// consult 族恒停靠(settle 即出会话池入 pending——无 TUI 冻结事件——子块各自冻结)。
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* depth-0 在飞批定位(findInFlightBatch——D-BR21)。判定字面全部单源自 batch-skeleton.mjs
|
|
6
6
|
* (SEGMENT_BY_ROLE / STATUS_WORDS / STATUS_LINE_RE / readBatchStatusLine / sectionHeaderRe /
|
|
7
7
|
* batchSkeleton / TEMPLATE_PLACEHOLDERS / findPlaceholderResidue / placeholderResidueError);
|
|
8
|
-
*
|
|
9
|
-
* 主档——**依赖单向(KD-4):
|
|
10
|
-
*
|
|
8
|
+
* 路径解析单源(`resolveBatchDocPath` / `batchDocBases` / `resolveBatchCreatePath`——住 `batch-paths.mjs`,
|
|
9
|
+
* 本档 import 取用)与 #84 记账缝住 batch.mjs 主档——**依赖单向(KD-4):paths ← skeleton ← lifecycle
|
|
10
|
+
* ← 主档**,主档把解析后的 cwd/bases/目标闭包传进来,本档不回 import 主档(防环)。
|
|
11
11
|
*
|
|
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
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* 状态面走普通文档写);depth-0 的 status/path 面按 D-BR21(可选 path,缺省 = 在飞批唯一时取)。
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
19
|
-
import { dirname,
|
|
19
|
+
import { dirname, resolve } from "node:path"
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
22
|
SEGMENT_BY_ROLE, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHeaderRe, batchSkeleton,
|
|
23
23
|
TEMPLATE_PLACEHOLDERS, findPlaceholderResidue, placeholderResidueError,
|
|
24
24
|
} from "./batch-skeleton.mjs"
|
|
25
|
+
import { resolveBatchCreatePath } from "./batch-paths.mjs"
|
|
25
26
|
|
|
26
27
|
/** 可读文件判据(存在且为文件——目录/缺失同判不可读;与主档同名 helper 同型——KD-4 单向
|
|
27
28
|
* 依赖下各自持有,三行谓词不构成第二权威源)。 */
|
|
@@ -87,21 +88,13 @@ export function findInFlightBatch(cwd, bases) {
|
|
|
87
88
|
return candidates[0]
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
/** create
|
|
91
|
-
function assertInsideBases(abs, bases, raw) {
|
|
92
|
-
const norm = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
|
|
93
|
-
const target = norm(abs)
|
|
94
|
-
for (const b of bases) {
|
|
95
|
-
const root = norm(b)
|
|
96
|
-
if (target === root || target.startsWith(root.endsWith(sep) ? root : root + sep)) return
|
|
97
|
-
}
|
|
98
|
-
throw new Error(`batch: create path resolves outside the batch-record base roots — a batch record must live under the declared docRoot.batches base (fail-closed). Path: ${raw}`)
|
|
99
|
-
}
|
|
91
|
+
/** create 路径越界判据已迁 `batch-paths.mjs`(`resolveBatchCreatePath` 内含——§4.15 单源)。 */
|
|
100
92
|
|
|
101
93
|
/**
|
|
102
94
|
* create——建档(§4.11,仅 depth-0)。六段骨架一次预齐(骨架模板单源 = batchSkeleton),
|
|
103
95
|
* 建档即过 gate(§1 占位状态行含「进行中」);fail-closed:非 .md / 越基底 / 目标已存在 ⇒ throw
|
|
104
96
|
* (不覆盖既有批档,BR-20);目录缺失 ⇒ mkdir -p 后落位(BR-25)。不代建台账条目。
|
|
97
|
+
* 路径解析 = `resolveBatchCreatePath`(§4.15 单源:候选序首个落基底内者 / 锚定串只解析项目根形)。
|
|
105
98
|
* F11-B:`source` **必填**(topic 同款空拒——骨架编制行实参化;不传 = 死占位残留必被 F11-C 拒
|
|
106
99
|
* ⇒ create 即拒,不留死锁);`prev` 传入值**幂等剥**「前情 = 」前缀(值规范化;默认值零变)。
|
|
107
100
|
* @returns {string} 成功消息(含落盘绝对路径)
|
|
@@ -119,8 +112,7 @@ export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
|
|
|
119
112
|
if (!primary) {
|
|
120
113
|
throw new Error("batch: no batch-record base root is available — fix PROJECT-MANIFEST.json docRoot.batches (or its default) before creating a record. Nothing was written.")
|
|
121
114
|
}
|
|
122
|
-
const abs =
|
|
123
|
-
assertInsideBases(abs, bases, raw)
|
|
115
|
+
const abs = resolveBatchCreatePath(cwd, raw, bases)
|
|
124
116
|
if (readableFile(abs)) {
|
|
125
117
|
throw new Error(`batch: create target already exists — refusing to overwrite an existing batch record (fail-closed, BR-20): ${raw}`)
|
|
126
118
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/batch-paths.mjs — 批次档相对路径解析**单源**(`BATCH-RECORD.md` §4.15 · 台账 #287 · 2026-09-25 批)。
|
|
3
|
+
*
|
|
4
|
+
* 四处调用点全部经本档:create(`resolveBatchCreatePath`)· append / status / close 与评审门
|
|
5
|
+
* (`resolveBatchDocPath`——读面抛错形)· spawn 门(`resolveBatchReadPath`——读面非抛形)。
|
|
6
|
+
*
|
|
7
|
+
* 契约(§4.15 逐字):**候选序** = cwd → 项目根 → 逐基底(基底 = `batchDocBases` 声明面单源);
|
|
8
|
+
* **判据分野** = 读面取首个**可读**文件 / create 取首个**落基底根内**者;**锚定防嵌套** = 串以任一基底的
|
|
9
|
+
* 「项目根相对前缀」打头且前缀后为**路径段边界** ⇒ 只解析项目根形(不二次拼接;不落基底内 ⇒ fail-closed);
|
|
10
|
+
* **零变面** = 绝对路径照旧、三个错误文案逐字、「参数在 + 路径可读」判据句。
|
|
11
|
+
*
|
|
12
|
+
* 依赖(KD-4 单向):本档为**叶档**——只 import `node:*` 与 `../manifest.mjs`;主档 re-export 保既有面。
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, statSync } from "node:fs"
|
|
15
|
+
import { isAbsolute, relative, resolve, sep } from "node:path"
|
|
16
|
+
|
|
17
|
+
import { docRootBase, docRootPaths, readManifest, resolveProjectRoot } from "../manifest.mjs"
|
|
18
|
+
|
|
19
|
+
/** 可读文件判据(存在且为文件——目录 / 缺失同判不可读)。 */
|
|
20
|
+
function readableFile(abs) {
|
|
21
|
+
try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 「项目根」取根单源(§4.15 条 1——归属 ∨ 发现,无 manifest ⇒ `resolve(cwd ?? ".")` 兜底且**单值**):
|
|
25
|
+
* 同式 = 台账库键 / 写门指针基准的同一子表达式(`design/LEDGER.md` §6.1 口径 2)。 */
|
|
26
|
+
export function batchProjectRoot(cwd) {
|
|
27
|
+
return resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** manifest **声明基底**(声明面;manifest 缺失 / 非法 / 读错 ⇒ 空集——v1 单基底语义零变)。 */
|
|
31
|
+
function declaredBases(cwd) {
|
|
32
|
+
const base = cwd ?? process.cwd()
|
|
33
|
+
let man = { ok: false }
|
|
34
|
+
try { man = readManifest(base) } catch { /* 读错 → 按无 manifest 处理(回退默认基底) */ }
|
|
35
|
+
return man.ok ? docRootPaths(man.manifest?.docRoot?.batches, base) : []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 批次档基底根数组(**声明面单源**——create / 在飞扫描 / 锚定判据共用):manifest `docRoot.batches`
|
|
39
|
+
* (串 | 多根数组)→ 声明基底;缺失 / 非法 ⇒ 回退默认基底 `"docs/batches"`(恒返回非空数组)。
|
|
40
|
+
* @param {string} cwd
|
|
41
|
+
* @returns {string[]} */
|
|
42
|
+
export function batchDocBases(cwd) {
|
|
43
|
+
const roots = declaredBases(cwd)
|
|
44
|
+
return roots.length ? roots : [resolve(docRootBase(cwd), "docs/batches")]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 基底的项目根相对前缀(归一 `/`;基底不落项目根下 / 与前缀重合 ⇒ 不产前缀——前缀不匹配者不做启发式改写)。 */
|
|
48
|
+
function basePrefixes(cwd, bases) {
|
|
49
|
+
const root = batchProjectRoot(cwd)
|
|
50
|
+
const out = []
|
|
51
|
+
for (const b of bases) {
|
|
52
|
+
const rel = relative(root, resolve(b)).replace(/\\/g, "/")
|
|
53
|
+
if (!rel || rel === ".." || rel.startsWith("../") || isAbsolute(rel)) continue
|
|
54
|
+
out.push(rel)
|
|
55
|
+
}
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 锚定判据(**按路径段**——前缀后须为 `/` 或串尾;`docs/batches-old/<x>.md` 不锚定,BR-35)。 */
|
|
60
|
+
function anchoredPrefix(p, prefixes) {
|
|
61
|
+
for (const pre of prefixes) if (p === pre || p.startsWith(pre + "/")) return pre
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 越基底判据(create 面):解析后须落在某一基底根之内;win32 大小写不敏感比较。 */
|
|
66
|
+
function insideBases(abs, bases) {
|
|
67
|
+
const norm = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
|
|
68
|
+
const target = norm(abs)
|
|
69
|
+
return bases.some((b) => {
|
|
70
|
+
const root = norm(b)
|
|
71
|
+
return target === root || target.startsWith(root.endsWith(sep) ? root : root + sep)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** create 越基底(原文案逐字——零变面)。 */
|
|
76
|
+
const outsideBasesError = (raw) => new Error(`batch: create path resolves outside the batch-record base roots — a batch record must live under the declared docRoot.batches base (fail-closed). Path: ${raw}`)
|
|
77
|
+
/** create 锚定但落基底外(§4.15 新文案逐字——不二次拼接)。 */
|
|
78
|
+
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}`)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* **读面**解析(append / status / close 与评审门 / spawn 门共用):首个**可读**候选胜;无 ⇒ null
|
|
82
|
+
* (调用方各自抛既有文案)。锚定串只解析项目根形(跳过 ①③——已含基底相对前缀者不再二次拼接)。
|
|
83
|
+
* @param {string} cwd 会话锚(缺省 → 进程 cwd)
|
|
84
|
+
* @param {string} given 参数原值(相对 / 绝对、`\` 与 `/` 混写均可)
|
|
85
|
+
* @returns {string|null} 可读文件的绝对路径 / null
|
|
86
|
+
*/
|
|
87
|
+
export function resolveBatchReadPath(cwd, given) {
|
|
88
|
+
const base = cwd ?? process.cwd()
|
|
89
|
+
const raw = typeof given === "string" ? given.trim() : ""
|
|
90
|
+
if (!raw) return null
|
|
91
|
+
const p = raw.replace(/\\/g, "/")
|
|
92
|
+
if (anchoredPrefix(p, basePrefixes(base, batchDocBases(base)))) {
|
|
93
|
+
const abs = resolve(batchProjectRoot(base), p)
|
|
94
|
+
return readableFile(abs) ? abs : null
|
|
95
|
+
}
|
|
96
|
+
const candidates = [resolve(base, p), resolve(batchProjectRoot(base), p), ...declaredBases(base).map((b) => resolve(b, p))]
|
|
97
|
+
for (const abs of candidates) if (readableFile(abs)) return abs
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 批次档路径门禁(评审侧 `BATCH-RECORD.md` §4.2 口径 = **「若传则须可读」**):空 / 非字符串 / 不可读 → throw。
|
|
103
|
+
* 非空且可读 → 返回绝对路径(`\` 归一——照 `files` / `batchDoc` spawn 门先例)。错误文案逐字零变(零回归硬约束)。
|
|
104
|
+
*/
|
|
105
|
+
export function resolveBatchDocPath(cwd, given) {
|
|
106
|
+
const base = cwd ?? process.cwd()
|
|
107
|
+
const raw = typeof given === "string" ? given.trim() : ""
|
|
108
|
+
if (!raw) {
|
|
109
|
+
throw new Error("batchDoc must be a non-empty path to the batch record — pass the batch record currently in flight, or omit the parameter entirely when no batch record is in flight.")
|
|
110
|
+
}
|
|
111
|
+
const abs = resolveBatchReadPath(base, raw)
|
|
112
|
+
if (abs) return abs
|
|
113
|
+
throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* **create 面**解析(§4.15 条 2/3):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
|
|
118
|
+
* 根内**者(新档不存在——可读性无判别力);锚定串只解析项目根形,不落基底内 ⇒ fail-closed 新文案。
|
|
119
|
+
* @param {string} cwd 会话锚
|
|
120
|
+
* @param {string} given create `path` 原值
|
|
121
|
+
* @param {string[]} bases 已解析的基底根数组(`batchDocBases` 单源;调用方传入以免重复读档)
|
|
122
|
+
* @returns {string} 落位绝对路径
|
|
123
|
+
*/
|
|
124
|
+
export function resolveBatchCreatePath(cwd, given, bases) {
|
|
125
|
+
const base = cwd ?? process.cwd()
|
|
126
|
+
const raw = String(given ?? "").trim()
|
|
127
|
+
const p = raw.replace(/\\/g, "/")
|
|
128
|
+
if (isAbsolute(raw)) {
|
|
129
|
+
const abs = resolve(raw)
|
|
130
|
+
if (!insideBases(abs, bases)) throw outsideBasesError(raw)
|
|
131
|
+
return abs
|
|
132
|
+
}
|
|
133
|
+
if (anchoredPrefix(p, basePrefixes(base, bases))) {
|
|
134
|
+
const abs = resolve(batchProjectRoot(base), p)
|
|
135
|
+
if (!insideBases(abs, bases)) throw anchoredEscapeError(raw)
|
|
136
|
+
return abs
|
|
137
|
+
}
|
|
138
|
+
for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p), ...bases.map((b) => resolve(b, p))]) {
|
|
139
|
+
if (insideBases(abs, bases)) return abs
|
|
140
|
+
}
|
|
141
|
+
throw outsideBasesError(raw)
|
|
142
|
+
}
|