@thincoder/core 0.10.13 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +69 -48
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +3 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +32 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -367
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +275 -80
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/client-identity.mjs +22 -6
- package/config-presets.mjs +5 -3
- package/context.mjs +34 -3
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +68 -15
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +18 -5
- package/explore-distill.mjs +1 -1
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +19 -37
- package/ledger-cmd.mjs +71 -195
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +6 -6
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -68
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/core.mjs +2 -2
- package/provider/google.mjs +15 -5
- package/provider/rate.mjs +11 -3
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +9 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +90 -14
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
|
@@ -9,26 +9,36 @@
|
|
|
9
9
|
*
|
|
10
10
|
* - askpass 链:包装档(Win `.cmd` ∥ POSIX `.sh` 0700)→ `tools/ssh-askpass.mjs` → stdout;秘密只经
|
|
11
11
|
* ssh 子进程环境 `TC_SSH_SECRET`(零入参 / 零落盘 / 零回显;环境经 `nodeChildEnv` 单点补 Electron 旗标)。
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
12
|
+
* - 终端噪声:有状态剥离(CSI ∥ OSC ∥ 单字符控制三类 + CRLF 归一;状态跨 chunk 携带)——落点 = 传输入口,
|
|
13
|
+
* **先剥离后遮蔽**;日志面 = 模型面同源(D-TO23)。
|
|
14
|
+
* - 就绪判据:四门(待答标记 ∥ 提示符 ∥ 仍在吐 ∥ 硬超时 60 s);静默窗只作去抖(D-TO21)。
|
|
15
|
+
* - 遮蔽:写入的秘密对**捕获面与日志面**做字面移除(比较面 = 同一归一 + 按不可见处切段 ≥ 4)。
|
|
14
16
|
* - 失败分类族(auth ∥ unreachable ∥ hostkey)照服务端 `src/sandbox/ssh.mjs` 同源判据。
|
|
15
|
-
* 模块图:→ config.mjs(configDir 缺省)· tools/shared.mjs
|
|
16
|
-
* (nodeChildEnv
|
|
17
|
+
* 模块图:→ config.mjs(configDir 缺省)· tools/shared.mjs(解码)· text-budget.mjs(截断本体)·
|
|
18
|
+
* tools/node-child.mjs(nodeChildEnv)· expand-home.mjs(`~` 展开单源——`identityFile` 用)——叶子向、无环
|
|
19
|
+
* (本档不 import `ssh-session.mjs`)。
|
|
17
20
|
*/
|
|
18
21
|
import { chmodSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs"
|
|
19
22
|
import { spawn } from "node:child_process"
|
|
20
|
-
import { homedir } from "node:os"
|
|
21
23
|
import { fileURLToPath } from "node:url"
|
|
22
24
|
import { configDir } from "../config.mjs"
|
|
23
|
-
import {
|
|
25
|
+
import { expandHome } from "../expand-home.mjs"
|
|
26
|
+
import { makeDecoder } from "../tools/shared.mjs"
|
|
24
27
|
import { nodeChildEnv } from "../tools/node-child.mjs"
|
|
28
|
+
import { capText as capBudgetText, fillMarker } from "../text-budget.mjs"
|
|
25
29
|
|
|
26
|
-
/** 静默窗(§6.22
|
|
30
|
+
/** 静默窗(§6.22 ③):**只作去抖**——判尾部这半条标记到没到齐;不再单独构成「已就绪」。 */
|
|
27
31
|
export const SSH_SETTLE_MS = 800
|
|
28
|
-
/** 硬超时(§6.22
|
|
29
|
-
export const SSH_TIMEOUT_MS =
|
|
30
|
-
/**
|
|
32
|
+
/** 硬超时(§6.22 ③/⑥):`send` / `read` / `secret_input` 的本次等待;到点返回部分输出 + 可读句。 */
|
|
33
|
+
export const SSH_TIMEOUT_MS = 60_000
|
|
34
|
+
/** `connect` 首屏等待(§6.22 ③「适用范围」拆两常量——慢设备才拖不动连接)。 */
|
|
35
|
+
export const SSH_CONNECT_SETTLE_MS = 10_000
|
|
36
|
+
/** 单次返回帽(§6.22 ⑥):**返回窗口** = 保头 + 注记 + 保尾;省略中段回填未读缓冲。 */
|
|
31
37
|
export const SSH_RETURN_CAP = 8000
|
|
38
|
+
/** 帽窗头尾比例(§6.22 ⑥:头 60% ∥ 尾 40%——头段装接口 / ACL,尾段装结束证据)。 */
|
|
39
|
+
const CAP_HEAD_RATIO = 0.6
|
|
40
|
+
/** 遮蔽分段下限(§6.22 ③「`≥ 4` 为选定策略值」:3 字符以下与正常输出撞面过高)。 */
|
|
41
|
+
const MASK_SEG_MIN = 4
|
|
32
42
|
/** 保活缺省(§6.22 ④:`ServerAliveInterval=30`;测试态时间缝按 ms 传入后换算)。 */
|
|
33
43
|
export const SSH_KEEPALIVE_MS = 30_000
|
|
34
44
|
/** 秘密环境变量名(§6.22 ②「秘密传递」行)。 */
|
|
@@ -84,13 +94,6 @@ export function buildSshEnv({ wrapper, secret }) {
|
|
|
84
94
|
return nodeChildEnv(env)
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
/** `~` 展开(`identityFile` 记录 / 参数——`~/.ssh/id_ed25519` 同式;非 `~` 开头原样)。 */
|
|
88
|
-
function expandHome(file) {
|
|
89
|
-
const raw = String(file ?? "")
|
|
90
|
-
if (!/^~(?=$|[\\/])/.test(raw)) return raw
|
|
91
|
-
return `${homedir()}${raw.slice(1)}`
|
|
92
|
-
}
|
|
93
|
-
|
|
94
97
|
/** 会话命令形(§6.22 ⑤):`-tt` 强制远端 PTY + 保活三件 + 单提示;`port`/`user` 未给 ⇒ 不下发。
|
|
95
98
|
* `identityFile` 显式给出 / 记录有值 ⇒ `-i <路径>`(追加候选身份,不夺本机配置与 agent 的既有尝试序列)。
|
|
96
99
|
* 预探 = `NumberOfPasswordPrompts=0`(password ∥ keyboard-interactive 两径同被短路——
|
|
@@ -110,19 +113,111 @@ export function buildSshArgs({ host, user, port, probe, identityFile = null, kee
|
|
|
110
113
|
]
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
// ─── 终端噪声(§6.22 ③「终端噪声」· D-TO23)────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/** 不可见处(遮蔽比较面的切段器——与剥离同一类目:CSI ∥ ESC+1 ∥ 任一控制字符)。 */
|
|
119
|
+
const INVISIBLE_SPLIT = /\x1b\[[\x20-\x3f]*[\x40-\x7e]|\x1b[^\x1b]?|[\x00-\x1f\x7f]/
|
|
120
|
+
|
|
121
|
+
/** 有状态剥离器(§6.22 ③):三类序列 CSI ∥ OSC ∥ 单字符控制 + `\r\n`∥裸 `\r` 归 `\n`。
|
|
122
|
+
* 状态跨 chunk 携带(序列被 chunk 边界切开不残留成字面);三类之外的序列(DCS / APC 等)按
|
|
123
|
+
* 单字符控制处理——头字节剥掉,体内字节当可见文本如实交出(设计「表外序列的出口」)。
|
|
124
|
+
* `push` = 返回边界(状态保留);`end` = 收场(硬超时 ∥ close ∥ 会话终结:尾部半条序列丢弃并计一次)。 */
|
|
125
|
+
export function createStripper() {
|
|
126
|
+
let state = "ground" // ground ∥ esc ∥ csi ∥ osc ∥ oscEsc ∥ charset
|
|
127
|
+
let pendingCr = false // chunk 尾的 `\r`(下一片首字为 `\n` ⇒ 仍归一条——跨 chunk 的 CRLF)
|
|
128
|
+
const scan = (text) => {
|
|
129
|
+
const s = String(text ?? "")
|
|
130
|
+
let out = ""
|
|
131
|
+
let i = 0
|
|
132
|
+
while (i < s.length) {
|
|
133
|
+
const ch = s[i]
|
|
134
|
+
if (pendingCr) {
|
|
135
|
+
pendingCr = false
|
|
136
|
+
out += "\n"
|
|
137
|
+
if (ch === "\n") { i += 1; continue }
|
|
138
|
+
}
|
|
139
|
+
if (state === "esc") {
|
|
140
|
+
state = "ground"
|
|
141
|
+
if (ch === "[") state = "csi"
|
|
142
|
+
else if (ch === "]") state = "osc"
|
|
143
|
+
else if (ch === "(" || ch === ")" || ch === "*" || ch === "+") state = "charset"
|
|
144
|
+
else if (ch === "\x1b") state = "esc"
|
|
145
|
+
i += 1
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (state === "charset") { state = "ground"; i += 1; continue } // `ESC ( 0` 字符集指定(既有覆盖不回归)
|
|
149
|
+
if (state === "csi") {
|
|
150
|
+
const c = ch.charCodeAt(0)
|
|
151
|
+
if (c >= 0x20 && c <= 0x3f) { i += 1; continue } // 参数 ∥ 中间字节
|
|
152
|
+
if (c >= 0x40 && c <= 0x7e) { state = "ground"; i += 1; continue } // 终字节
|
|
153
|
+
state = "ground" // 类外字 ⇒ 序列作废,本字按常规面重判(不吞可见字节)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if (state === "osc") {
|
|
157
|
+
if (ch === "\x07") { state = "ground"; i += 1; continue }
|
|
158
|
+
if (ch === "\x1b") { state = "oscEsc"; i += 1; continue }
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (state === "oscEsc") {
|
|
163
|
+
state = "ground"
|
|
164
|
+
if (ch === "\\") { i += 1; continue } // `ESC \` = ST
|
|
165
|
+
state = "osc" // 非 ST ⇒ 本字仍在 OSC 体内(ESC 已丢弃,体内字节本就要丢)
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
if (ch === "\x1b") { state = "esc"; i += 1; continue }
|
|
169
|
+
if (ch === "\r") {
|
|
170
|
+
if (i + 1 >= s.length) { pendingCr = true; i += 1; continue }
|
|
171
|
+
out += "\n"
|
|
172
|
+
i += s[i + 1] === "\n" ? 2 : 1
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (ch === "\n" || ch === "\t") { out += ch; i += 1; continue }
|
|
176
|
+
const code = ch.charCodeAt(0)
|
|
177
|
+
if (code < 0x20 || code === 0x7f) { i += 1; continue } // BEL ∥ 退格 ∥ 其余 C0(保留 `\n` / `\t`)
|
|
178
|
+
out += ch
|
|
179
|
+
i += 1
|
|
180
|
+
}
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
/** 喂一片(返回可交出文本;半条序列留给下一片)。 */
|
|
185
|
+
feed(text) { return scan(text) },
|
|
186
|
+
/** 收场:尾部半条序列丢弃并计一次;其前后的可见字节照常交出。 */
|
|
187
|
+
end(text = "") {
|
|
188
|
+
let out = scan(text)
|
|
189
|
+
if (pendingCr) { out += "\n"; pendingCr = false }
|
|
190
|
+
const dropped = state === "ground" ? 0 : 1
|
|
191
|
+
state = "ground"
|
|
192
|
+
return { text: out, dropped }
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 同一归一(无状态旁路——遮蔽比较面用:秘密值先过控制字节剥离 + CRLF 归一)。 */
|
|
198
|
+
export function normalizeText(text) {
|
|
199
|
+
return createStripper().end(String(text ?? "")).text
|
|
200
|
+
}
|
|
201
|
+
|
|
113
202
|
// ─── 遮蔽(§6.22 ⑦)────────────────────────────────────────────────────────
|
|
114
203
|
|
|
115
|
-
/** 秘密遮蔽过滤器:字面移除 + `len-1` 尾缓冲(跨 chunk 切分不泄;尾至沉降冲涮)。
|
|
204
|
+
/** 秘密遮蔽过滤器:字面移除 + `len-1` 尾缓冲(跨 chunk 切分不泄;尾至沉降冲涮)。
|
|
205
|
+
* 比较面 = 与剥离同一归一(§6.22 ③):秘密值先归一、再按不可见处切段(段长 ≥ 4)——
|
|
206
|
+
* 裸字面匹配会漏掉夹控制字节 ∥ 被 chunk 切开的回显。 */
|
|
116
207
|
export function createMask() {
|
|
117
208
|
const secrets = []
|
|
118
209
|
let tail = ""
|
|
119
210
|
let keep = 0
|
|
120
211
|
return {
|
|
121
212
|
add(secret) {
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
124
|
-
|
|
125
|
-
|
|
213
|
+
const raw = String(secret ?? "")
|
|
214
|
+
if (!raw) return
|
|
215
|
+
const table = [normalizeText(raw), ...raw.split(INVISIBLE_SPLIT).filter((seg) => seg.length >= MASK_SEG_MIN)]
|
|
216
|
+
for (const s of table) {
|
|
217
|
+
if (!s || secrets.includes(s)) continue
|
|
218
|
+
secrets.push(s)
|
|
219
|
+
keep = Math.max(keep, s.length - 1)
|
|
220
|
+
}
|
|
126
221
|
},
|
|
127
222
|
push(text) {
|
|
128
223
|
let out = tail + String(text ?? "")
|
|
@@ -132,6 +227,8 @@ export function createMask() {
|
|
|
132
227
|
tail = out.slice(out.length - keep)
|
|
133
228
|
return emit
|
|
134
229
|
},
|
|
230
|
+
/** 尾缓冲现行内容(**判据面用**:门①② 要看到已达字节——尾在 drain 时释放,判据不得被它推迟)。 */
|
|
231
|
+
peek() { return tail },
|
|
135
232
|
/** 沉降时冲涮尾缓冲:**只放行非秘密前缀段**(尾内最长可疑后缀一律扣下——整尾 ∥ 移位前缀
|
|
136
233
|
* 都不出返回面;扣下的至多 `秘密长-1` 字符,代价 < 前缀外泄)。 */
|
|
137
234
|
flush() {
|
|
@@ -150,10 +247,9 @@ export function createMask() {
|
|
|
150
247
|
|
|
151
248
|
// ─── 子进程(起跑 ∥ 双流汇入 ∥ 退出)────────────────────────────────────────
|
|
152
249
|
|
|
153
|
-
/** 悬挂 chunk
|
|
250
|
+
/** 悬挂 chunk 双流汇入(已剥离已遮蔽 → log ∥ 缓冲 ∥ 面板 → 唤醒沉降等待者)。 */
|
|
154
251
|
function handleChunk(entry, text) {
|
|
155
252
|
if (!text) return
|
|
156
|
-
entry.bytes += text.length
|
|
157
253
|
try { entry.logStream?.write(text) } catch { /* log 写失败不进语义——尽力面 */ }
|
|
158
254
|
entry.buf += text
|
|
159
255
|
if (entry.buf.length > BUF_KEEP_CHARS) {
|
|
@@ -165,18 +261,33 @@ function handleChunk(entry, text) {
|
|
|
165
261
|
for (const w of [...entry._waiters]) w("data")
|
|
166
262
|
}
|
|
167
263
|
|
|
168
|
-
/** 子进程挂载(双流解码 ∥ 退出 ∥ spawn 错误——探连与会话同径)。
|
|
264
|
+
/** 子进程挂载(双流解码 ∥ 剥离 → 遮蔽 ∥ 退出 ∥ spawn 错误——探连与会话同径)。
|
|
265
|
+
* 顺序 = **先剥离后遮蔽**(§6.22 ③):剥离只删字节、不插入 ⇒ 切不断秘密字面;每条流一份剥离状态
|
|
266
|
+
* (两个流各自成序列,不共用一个半条状态)。到达计数在这之前记(§6.22 ③ 门④ 口径 = 「回了没有」,
|
|
267
|
+
* 不随遮蔽尾缓冲的后扣而变)。 */
|
|
169
268
|
export function attachChild(entry, child) {
|
|
170
269
|
entry.child = child
|
|
171
270
|
const outDecoder = makeDecoder()
|
|
172
271
|
const errDecoder = makeDecoder()
|
|
173
|
-
const
|
|
272
|
+
const outStrip = createStripper()
|
|
273
|
+
const errStrip = createStripper()
|
|
274
|
+
entry.strippers = [outStrip, errStrip]
|
|
275
|
+
const route = (decoder, strip) => (d, final = false) => {
|
|
276
|
+
const visible = strip.feed(decoder(d, final))
|
|
277
|
+
if (!visible) return
|
|
278
|
+
entry.lastDataAt = Date.now()
|
|
279
|
+
entry.bytes += visible.length // 到达量(剥离后、遮蔽前)——门④ 判「回了没有」用
|
|
280
|
+
handleChunk(entry, entry.mask.push(visible))
|
|
281
|
+
}
|
|
282
|
+
const outRoute = route(outDecoder, outStrip)
|
|
283
|
+
const errRoute = route(errDecoder, errStrip)
|
|
174
284
|
const flush = () => {
|
|
175
|
-
|
|
176
|
-
|
|
285
|
+
outRoute(Buffer.alloc(0), true)
|
|
286
|
+
errRoute(Buffer.alloc(0), true)
|
|
287
|
+
entry.escDropped = (entry.escDropped ?? 0) + flushEscapes(entry) // 收场:尾部半条序列丢弃并计一次
|
|
177
288
|
}
|
|
178
|
-
child.stdout.on("data", (d) =>
|
|
179
|
-
child.stderr.on("data", (d) =>
|
|
289
|
+
child.stdout.on("data", (d) => outRoute(d))
|
|
290
|
+
child.stderr.on("data", (d) => errRoute(d))
|
|
180
291
|
// stdin 无 error 监听 ⇒ 对端刚死时写入的异步 EPIPE 会以未处理 'error' 事件逸出(uncaughtException)。
|
|
181
292
|
// 同族先例:CLI `tui/clipboard.mjs` ∥ `mcp/transport-stdio.mjs` 同式吞 EPIPE。
|
|
182
293
|
child.stdin?.on("error", () => { /* 写失败由 send ∥ secret_input 的调用面回读;此处仅防未处理事件 */ })
|
|
@@ -210,17 +321,52 @@ export function spawnSession(entry, { user, port, secret, identityFile = null, k
|
|
|
210
321
|
return child
|
|
211
322
|
}
|
|
212
323
|
|
|
213
|
-
// ───
|
|
324
|
+
// ─── 就绪判据(§6.22 ③ 四门 · D-TO21)─────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/** 待答标记(门①——设备正等我发键;`--More--` 走空格、确认走 y/n)。 */
|
|
327
|
+
const ANSWER_MARKERS = ["--More--", "(y/n)", "Password:"]
|
|
328
|
+
/** 缺省设备提示符形(门②——行尾 `#` `>` `$` `%`,前接非空白词)。 */
|
|
329
|
+
const DEVICE_PROMPT = /[^\s][#$>%]\s*$/u
|
|
330
|
+
|
|
331
|
+
/** 末行(门① 只看末行——陈旧标记被后续输出顶走就不命中,B18)。 */
|
|
332
|
+
const tailLine = (text) => {
|
|
333
|
+
const s = String(text ?? "")
|
|
334
|
+
return s.slice(s.lastIndexOf("\n") + 1)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** 判据面文本 = **新到字节**(`base` 之后) + 遮蔽尾缓冲(尾在 drain 时释放,门不得被它推迟)。
|
|
338
|
+
* `base` 撇掉本轮之前就在缓冲里的存量(回填的中段不得充当本轮判据面——§6.22 ③「四门只判新到字节」)。 */
|
|
339
|
+
const watchText = (entry, base = 0) => `${entry.buf.slice(base)}${entry.mask?.peek?.() ?? ""}`
|
|
214
340
|
|
|
215
|
-
|
|
216
|
-
|
|
341
|
+
/** 门① 待答标记(**末行锚定**——只看最后一个换行之后的部分;"出现"故用 `includes`:`(y/n)?` ∥ `[y/n]` 同类形都能命中)。 */
|
|
342
|
+
export function markerHit(entry, base = 0) {
|
|
343
|
+
const line = tailLine(watchText(entry, base))
|
|
344
|
+
return ANSWER_MARKERS.some((m) => line.includes(m))
|
|
217
345
|
}
|
|
218
346
|
|
|
219
|
-
/**
|
|
220
|
-
export function
|
|
347
|
+
/** 门② 提示符:agent 所设 `promptPattern` 命中 ∥ 缺省设备形。 */
|
|
348
|
+
export function promptHit(entry, base = 0) {
|
|
349
|
+
const text = watchText(entry, base)
|
|
350
|
+
if (entry.promptRe && entry.promptRe.test(text)) return true
|
|
351
|
+
return DEVICE_PROMPT.test(tailLine(text))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** 硬超时失败标签三枚(门④——与门③ 两条子情形同文;计的是**到达字节**:剥离后、遮蔽前)。 */
|
|
355
|
+
function timeoutLabel(entry, startBytes) {
|
|
356
|
+
if (entry.bytes <= startBytes) return "not a single byte came back"
|
|
357
|
+
return Date.now() - entry.lastDataAt < SSH_SETTLE_MS ? "still producing output" : "no prompt detected"
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 等到就绪(返回 reason):`marker` ∥ `prompt` ∥ `timeout` ∥ `exited` ∥ `aborted`(`connect` 另有 `silence`)。
|
|
361
|
+
* 门①② 在**每个到达 chunk** 上求值;静默窗只判「尾部这半条标记到没到齐」(去抖)——不参与求值、不推迟
|
|
362
|
+
* 判断时点,也不再单独构成「已就绪」(D-TO21)。`settleOnSilence` = `connect` 首屏沿用旧口径
|
|
363
|
+
* (§6.22 ③「适用范围」:四门只判 `send` / `read` / `secret_input`)。 */
|
|
364
|
+
export function waitSettled(entry, { timeoutMs = SSH_TIMEOUT_MS, signal = null, settleOnSilence = false } = {}) {
|
|
221
365
|
return new Promise((resolve) => {
|
|
222
366
|
let done = false
|
|
223
367
|
let quietTimer = null
|
|
368
|
+
const startBytes = entry.bytes
|
|
369
|
+
const baseLen = entry.buf.length // 门①② 只判**新到字节**(存量中段不当本轮判据面)
|
|
224
370
|
const deadline = Date.now() + Math.max(1, Number(timeoutMs) || SSH_TIMEOUT_MS)
|
|
225
371
|
const finish = (reason) => {
|
|
226
372
|
if (done) return
|
|
@@ -229,15 +375,28 @@ export function waitSettled(entry, { timeoutMs = SSH_TIMEOUT_MS, signal = null }
|
|
|
229
375
|
clearTimeout(deadlineTimer)
|
|
230
376
|
signal?.removeEventListener("abort", onAbort)
|
|
231
377
|
entry._waiters.delete(onEvent)
|
|
378
|
+
if (reason === "timeout") {
|
|
379
|
+
entry.settleFailure = timeoutLabel(entry, startBytes)
|
|
380
|
+
entry.escDropped = (entry.escDropped ?? 0) + flushEscapes(entry) // 硬超时 = 收场边界(§6.22 ③ flush②)
|
|
381
|
+
}
|
|
232
382
|
resolve(reason)
|
|
233
383
|
}
|
|
384
|
+
const evaluate = () => {
|
|
385
|
+
if (markerHit(entry, baseLen)) { finish("marker"); return true }
|
|
386
|
+
if (promptHit(entry, baseLen)) { finish("prompt"); return true }
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
234
389
|
const arm = () => {
|
|
235
390
|
clearTimeout(quietTimer)
|
|
236
|
-
quietTimer = setTimeout(() =>
|
|
391
|
+
quietTimer = setTimeout(() => {
|
|
392
|
+
if (done) return
|
|
393
|
+
if (settleOnSilence) return finish(promptHit(entry, baseLen) ? "prompt" : "silence")
|
|
394
|
+
evaluate() // 去抖:尾部半条标记到齐后再求值一次;未命中 ⇒ 继续等到硬超时
|
|
395
|
+
}, SSH_SETTLE_MS)
|
|
237
396
|
}
|
|
238
397
|
const onEvent = (kind) => {
|
|
239
398
|
if (kind === "exit") return finish("exited")
|
|
240
|
-
if (
|
|
399
|
+
if (evaluate()) return undefined
|
|
241
400
|
arm()
|
|
242
401
|
}
|
|
243
402
|
const onAbort = () => finish("aborted")
|
|
@@ -246,15 +405,21 @@ export function waitSettled(entry, { timeoutMs = SSH_TIMEOUT_MS, signal = null }
|
|
|
246
405
|
if (signal?.aborted) return finish("aborted")
|
|
247
406
|
signal?.addEventListener("abort", onAbort, { once: true })
|
|
248
407
|
entry._waiters.add(onEvent)
|
|
249
|
-
onEvent("data") //
|
|
408
|
+
onEvent("data") // 起点:先查门①②,否则起去抖窗
|
|
250
409
|
})
|
|
251
410
|
}
|
|
252
411
|
|
|
253
|
-
/** 就绪结论注记(§6.22
|
|
412
|
+
/** 就绪结论注记(§6.22 ③):提示符 ∥ 待答标记 ∥ 硬超时(**点名失败门**)∥ 中断 ∥ 退出。 */
|
|
254
413
|
export function settleNote(entry, reason, waitedMs) {
|
|
255
414
|
if (reason === "prompt") return "settled (prompt matched)"
|
|
415
|
+
if (reason === "marker") return "settled (the device is waiting for input — the marker is shown above)"
|
|
256
416
|
if (reason === "silence") return "settled (silence window)"
|
|
257
|
-
if (reason === "timeout")
|
|
417
|
+
if (reason === "timeout") {
|
|
418
|
+
const label = entry.settleFailure ?? "no prompt detected"
|
|
419
|
+
entry.settleFailure = null
|
|
420
|
+
const hint = label === "no prompt detected" ? "; pass promptPattern on connect if this device's prompt does not look like a shell prompt" : ""
|
|
421
|
+
return `not settled after ${Math.round(waitedMs / 100) / 10}s (${idLabel(entry)} @ ${entry.host}) — partial output above (${label}${hint})`
|
|
422
|
+
}
|
|
258
423
|
if (reason === "aborted") return `(interrupted — partial output above; the session stays open: ${idLabel(entry)} @ ${entry.host})`
|
|
259
424
|
if (reason === "exited") return `${idLabel(entry)} @ ${entry.host} ${entry.state === "ready" ? "ended (the remote closed the connection)" : "has stopped"}`
|
|
260
425
|
return String(reason)
|
|
@@ -275,12 +440,78 @@ export function drain(entry) {
|
|
|
275
440
|
return { text, dropped }
|
|
276
441
|
}
|
|
277
442
|
|
|
278
|
-
/**
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
443
|
+
/** 回填未读缓冲(丢最旧、保住尾证据;§6.22 ⑥)。 */
|
|
444
|
+
function backfill(entry, middle) {
|
|
445
|
+
if (!middle || !entry) return
|
|
446
|
+
entry.buf = middle + entry.buf // 中段先于更新字节(回填顺序 = 原序)
|
|
447
|
+
if (entry.buf.length > BUF_KEEP_CHARS) {
|
|
448
|
+
entry.buf = entry.buf.slice(entry.buf.length - BUF_KEEP_CHARS)
|
|
449
|
+
entry.bufDropped = true
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** 返回帽窗(§6.22 ⑥ · D-TO22):**返回总长 ≤ 帽(注记计入帽内)**——头 60% ∥ 注记 ∥ 尾 40%,
|
|
454
|
+
* 切点按行边界取整、取整只在头段(头段可少不可多);省略中段**回填未读缓冲**(下一次 `read` 先交出)
|
|
455
|
+
* ⇒ 长配置按窗口连续取全,不靠翻日志。截断本体 = `text-budget.mjs` `capText`(零新截断实现)。 */
|
|
456
|
+
export function capWindow(entry, text, max = SSH_RETURN_CAP) {
|
|
457
|
+
const t = String(text ?? "")
|
|
458
|
+
if (!(max > 0) || t.length <= max) return t
|
|
459
|
+
if (max <= windowMarker(t.length, 0, 0).length) {
|
|
460
|
+
// 帽小到放不下注记 ⇒ 无法成窗:按帽交出前缀、余量回填缓冲(「总长 ≤ 帽」不变式先行;每次 read 都有进展)
|
|
461
|
+
backfill(entry, t.slice(max))
|
|
462
|
+
return t.slice(0, max)
|
|
463
|
+
}
|
|
464
|
+
let marker = windowMarker(t.length, 0, 0)
|
|
465
|
+
let head = 0
|
|
466
|
+
let tail = 0
|
|
467
|
+
for (let round = 0; round < 6; round++) {
|
|
468
|
+
const budget = Math.max(0, max - marker.length)
|
|
469
|
+
const nextHead = alignHead(t, Math.floor(budget * CAP_HEAD_RATIO))
|
|
470
|
+
const nextTail = Math.max(0, budget - Math.floor(budget * CAP_HEAD_RATIO))
|
|
471
|
+
const nextMarker = windowMarker(t.length - nextHead - nextTail, nextHead, nextTail)
|
|
472
|
+
const same = nextHead === head && nextTail === tail && nextMarker === marker
|
|
473
|
+
head = nextHead
|
|
474
|
+
tail = nextTail
|
|
475
|
+
marker = nextMarker
|
|
476
|
+
if (same) break
|
|
477
|
+
}
|
|
478
|
+
while (head > 0 && head + marker.length + tail > max) { // 收敛后的边角(省略数位数增长)——头段再收
|
|
479
|
+
head = Math.max(0, head - (head + marker.length + tail - max))
|
|
480
|
+
marker = windowMarker(t.length - head - tail, head, tail)
|
|
481
|
+
}
|
|
482
|
+
backfill(entry, t.slice(head, t.length - tail))
|
|
483
|
+
const markerOf = (omitted) => windowMarker(omitted, head, tail)
|
|
484
|
+
return capBudgetText(t, { max, keepHead: head, keepTail: tail, marker: markerOf })
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** 注记形(§6.22 ⑥):`N` 走 `text-budget.mjs` `fillMarker` 占位约定;`H` ∥ `T` = 实保头 ∥ 尾长。 */
|
|
488
|
+
const WINDOW_MARKER = "[… N chars omitted (head H / tail T) — the omitted middle is kept; call read again to collect it …]\n"
|
|
489
|
+
const windowMarker = (omitted, head, tail) =>
|
|
490
|
+
fillMarker(WINDOW_MARKER, omitted).replace("H", String(head)).replace("T", String(tail))
|
|
491
|
+
|
|
492
|
+
/** 头段切点按行边界取整(不切半行;找不到换行则保持原位——头段可少不可多)。 */
|
|
493
|
+
const alignHead = (text, n) => {
|
|
494
|
+
const nl = text.lastIndexOf("\n", Math.max(0, n - 1))
|
|
495
|
+
return nl >= 0 ? nl + 1 : n
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** 半条序列的收场(§6.22 ③ flush②):尾部半条丢弃并计一次;其前后的可见字节照常交出。 */
|
|
499
|
+
export function flushEscapes(entry) {
|
|
500
|
+
let dropped = 0
|
|
501
|
+
for (const strip of entry.strippers ?? []) {
|
|
502
|
+
const res = strip.end()
|
|
503
|
+
if (res.text) handleChunk(entry, entry.mask.push(res.text))
|
|
504
|
+
dropped += res.dropped
|
|
505
|
+
}
|
|
506
|
+
return dropped
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** 半条序列收场注记(§6.22 ③ flush②)——报告一次即清零。 */
|
|
510
|
+
export function escapeNote(entry) {
|
|
511
|
+
const n = entry.escDropped ?? 0
|
|
512
|
+
if (!n) return ""
|
|
513
|
+
entry.escDropped = 0
|
|
514
|
+
return `\n— ${n} incomplete escape sequence dropped`
|
|
284
515
|
}
|
|
285
516
|
|
|
286
517
|
/** 状态行冠形(§6.22 动作表末行):每返回首行。 */
|
|
@@ -30,7 +30,7 @@ import { unsettledRow, unsettledRows } from "../agent/digest-account.mjs"
|
|
|
30
30
|
* - id omitted → { overview: { running: [...], queued: [...], done: [...],
|
|
31
31
|
* unsettled: [{id, role, state, attempts, preview?}] } } — live queue positions (index in
|
|
32
32
|
* _asyncQueue + 1); the unsettled section lists every entry still awaiting settlement
|
|
33
|
-
* (awaiting-digest /
|
|
33
|
+
* (awaiting-digest / unaccounted) plus the upgraded ledger (§6.31.6).
|
|
34
34
|
* AGENT-LOOP-SUBAGENT.md §6.7.2: running 条目带 touched files 摘要(touchedFiles 前 5 + touchedMore 超出
|
|
35
35
|
* 计数——相对查询方 cwd;0 改动 → touched 占位);queued 条目带 touched 占位
|
|
36
36
|
* "—(未启动)";done/error/取消条目无 touched 字段(round3 #9)。
|
|
@@ -245,7 +245,7 @@ function touchedFilesNote(child, cwd) {
|
|
|
245
245
|
const touched = child?._touchedFiles ?? []
|
|
246
246
|
if (touched.length === 0) return ""
|
|
247
247
|
const shown = touched.map((f) => {
|
|
248
|
-
const r = relative(cwd
|
|
248
|
+
const r = relative(cwd, f)
|
|
249
249
|
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
250
250
|
})
|
|
251
251
|
return `\nTouched files: ${shown.join(", ")}`
|
|
@@ -63,8 +63,9 @@ export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Role → pool domain (single source of truth — §6.10 修正 #8): the CLI role
|
|
66
|
-
* whitelist is subagent.mjs's ROLES = { explore, plan, coder, eng-coder, eng-designer }
|
|
67
|
-
* (mode-filtered: normal → explore/plan/coder, engineering → explore/eng-designer/eng-coder
|
|
66
|
+
* whitelist is subagent.mjs's ROLES = { explore, plan, coder, eng-coder, eng-designer, tester }
|
|
67
|
+
* (mode-filtered: normal → explore/plan/coder/tester, engineering → explore/eng-designer/eng-coder/tester —
|
|
68
|
+
* tester is dispatchable in both modes).
|
|
68
69
|
* eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
|
|
69
70
|
* → other pool. escalate spawns its expert internally (role "coder" — other pool).
|
|
70
71
|
*/
|
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
* subagent-async.mjs 拆分——Module Split Policy——纯迁移零行为变化——
|
|
4
4
|
* AGENT-LOOP-SUBAGENT.md §6.9 D-SD1..SD5 + D-SL1 环形死锁修正)。
|
|
5
5
|
* 内容:normalizeFileList / filesOverlap / effectiveFiles(SCHEDULER-DYNAMIC-DOMAIN
|
|
6
|
-
* 动态域——声明 ∪ running touched)/ depInfo / describeBlockers /
|
|
6
|
+
* 动态域——声明 ∪ running touched)/ depInfo / describeBlockers / batchDocInFlight(
|
|
7
|
+
* AGENT-LOOP-SUBAGENT.md §6.58 批次档复用护栏——按档查在飞占用者)/ detectStall(
|
|
7
8
|
* P-SL2 停滞机械检测)/ queueRunnable / assertNoDepCycle / dependentLabels /
|
|
8
9
|
* refreshQueuedTokens / maybeRefillAsync。
|
|
9
10
|
* ASYNC_POOL_LIMITS/poolLimitsFor/runningPoolCount 回引自 subagent-async.mjs(主体保有——
|
|
10
11
|
* 仅调用期使用——与主体对此处调度符号的回引构成惰性环——无求值期依赖)。
|
|
11
12
|
*/
|
|
12
|
-
import { isAbsolute, relative
|
|
13
|
+
import { isAbsolute, relative } from "node:path"
|
|
13
14
|
import { existsSync, statSync } from "node:fs"
|
|
14
15
|
// zero-block 批 P1(TUI.md §6.8.3.2②):排队刷新 relay 异常留痕(禁静默——池状态仍不破坏)。
|
|
15
16
|
import { logEvent } from "../log.mjs"
|
|
16
17
|
// M5 F6(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):files 声明面拦截谓词本体落
|
|
17
18
|
// spawn-gates.mjs(纯谓词零依赖——本处只加 import 调用,无环)。
|
|
18
|
-
import {
|
|
19
|
+
import { rejectProcessDocPaths } from "./spawn-gates.mjs"
|
|
20
|
+
// `files` 落点单源 = 统一解析层(`docs/core/design/PATH-RESOLUTION.md` §2.5 第 4 行:`kind:"none"` /
|
|
21
|
+
// `order:"near-first"` ⇒ 恒解析出、只做调度、不校验存在性)。门判(目录声明 / 过程档)
|
|
22
|
+
// 仍吃**原始声明形状**——判据在转绝对之前跑,顺序零改。
|
|
23
|
+
import { resolvePathTarget } from "../path-resolve.mjs"
|
|
19
24
|
import { poolLimitsFor, runningPoolCount, ASYNC_POOL_LIMITS } from "./subagent-async.mjs"
|
|
20
25
|
// ASYNC-RESULT-CONTAINER.md D1:池 accessor——async-settle.mjs 反向 import 本模块
|
|
21
26
|
// (dependentLabels)构成同款惰性环(函数级绑定——无求值期依赖)。
|
|
@@ -43,7 +48,7 @@ import { carrierField, getAsyncPool, tombstoneOf } from "./async-settle.mjs"
|
|
|
43
48
|
* 大小写不敏感(todo.md/changelog.md 精确——路径任意层含 docs/、根、.thincoder/)——
|
|
44
49
|
* 命中 → throw(fail-closed——先于调度器准入——无排队残留——错误即工具结果);
|
|
45
50
|
* 提示列出全部违规条目——英文模板逐字定稿(AGENT-LOOP-SUBAGENT.md §6.9「父侧文件拦截(R26)」)。
|
|
46
|
-
* M5 F6(2026-09-17——2026-10-15 口径随正):黑名单随同迁入 spawn-gates.mjs
|
|
51
|
+
* M5 F6(2026-09-17——2026-10-15 口径随正):黑名单随同迁入 spawn-gates.mjs(族 = 过程档(CHANGELOG)+ 项目状态档(PROJECT-MANIFEST.json);老台账 md 族随 M2 退役)——本函数只留调用(错误文案逐字保留)。 */
|
|
47
52
|
export function normalizeFileList(files, cwd) {
|
|
48
53
|
const out = []
|
|
49
54
|
for (const f of Array.isArray(files) ? files : []) {
|
|
@@ -51,17 +56,19 @@ export function normalizeFileList(files, cwd) {
|
|
|
51
56
|
if (f.trimEnd().endsWith("/") || f.trimEnd().endsWith("\\")) { // 2.7 批:尾随空格目录声明("test/ ")同拒
|
|
52
57
|
throw new Error(`files must be file-level paths — directory declarations are not supported: ${f}`)
|
|
53
58
|
}
|
|
54
|
-
|
|
59
|
+
// 落点由统一解析层给(`kind:"none"` / `order:"near-first"` ⇒ 恒解析出、只做调度);
|
|
60
|
+
// **门判仍吃原始声明 `f`**:目录声明的尾分隔符判据按声明形状判——转成绝对路径后再判会漏。
|
|
61
|
+
const abs = resolvePathTarget(cwd, f, { kind: "none", order: "near-first" }).abs
|
|
55
62
|
if (existsSync(abs) && statSync(abs).isDirectory()) {
|
|
56
63
|
throw new Error(`files must be file-level paths — directory declarations are not supported: ${f}`)
|
|
57
64
|
}
|
|
58
65
|
if (!out.includes(abs)) out.push(abs)
|
|
59
66
|
}
|
|
60
|
-
// M5 F6(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2——2026-10-15
|
|
61
|
-
// 谓词本体落 spawn-gates.mjs
|
|
67
|
+
// M5 F6(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2——2026-10-15 口径随正):过程档 / 项目状态档拦截——
|
|
68
|
+
// 谓词本体落 spawn-gates.mjs(收集全部违规 + 抛合并错误);
|
|
62
69
|
// 循环内目录声明先行(错误序不变——目录错误先于过程档错误)。父侧维护文件
|
|
63
70
|
// basename 族(CHANGELOG——`ledger.db` 随 M2 退役)随同迁入谓词——错误文案逐字保留。
|
|
64
|
-
if (files !== undefined && files !== null)
|
|
71
|
+
if (files !== undefined && files !== null) rejectProcessDocPaths(files)
|
|
65
72
|
return out
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -79,7 +86,7 @@ export function filesOverlap(a, b) {
|
|
|
79
86
|
|
|
80
87
|
/** 冲突文件的显示形态(优先相对 cwd——面板/返回文本可读)。 */
|
|
81
88
|
function showFile(parent, key) {
|
|
82
|
-
const cwd = parent.cwd
|
|
89
|
+
const cwd = parent.cwd
|
|
83
90
|
const rel = relative(cwd, key)
|
|
84
91
|
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : key
|
|
85
92
|
}
|
|
@@ -176,6 +183,33 @@ export function describeBlockers(parent, entry) {
|
|
|
176
183
|
return { kind: "slot", detail: "" }
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
/**
|
|
187
|
+
* AGENT-LOOP-SUBAGENT.md §6.58 批次档复用护栏(2026-10-28 batchdoc-reuse-guard)——
|
|
188
|
+
* 按档查在飞占用者:池内条目 `_batchDoc` 与 `batchDocAbs` 按 `fileKey` 相等且**在飞**
|
|
189
|
+
* ⇒ `{ id, role, status }`;无 ⇒ `null`。池派生事实单源在此(与 `depInfo` /
|
|
190
|
+
* `describeBlockers` 同族),消费方单点 = `buildSpawnChild` 的 `batchDoc` 门。
|
|
191
|
+
* 三条判据各防一个具体的错:① 池**经访问器读**(`getAsyncPool`,读法同 `depInfo`)——
|
|
192
|
+
* 裸读 `parent._asyncSubagents` 在只携 `history` 的合成 parent 上读到空池 ⇒ 护栏不报错、
|
|
193
|
+
* 不生效、没人知道;② **按 status 过滤**(`done` / `cancelled` 皆假)——正常 settle 的条目
|
|
194
|
+
* 留在池里(`async-settle.mjs:295`)⇒ 只看「池里有同档条目」会把单席顺序交接全部误拒;
|
|
195
|
+
* ③ **占用判据只认 `_batchDoc`、不分角色**(普通面 coder 携档也算),而被检面只有工程角色的
|
|
196
|
+
* spawn、sync spawn 不入池也不是占用者(同档 §6.58.7 第 7 条)——**这个不对称勿「顺手统一」**。
|
|
197
|
+
* 入参 = 已解析的绝对路径(唯一消费方 `buildSpawnChild` 的工程分支,路径由 `resolveBatchReadPath`
|
|
198
|
+
* 保证非空);空入参返 `null` = 「无路径可比」——本谓词不替调用方判「该不该传路径」。
|
|
199
|
+
*/
|
|
200
|
+
export function batchDocInFlight(parent, batchDocAbs) {
|
|
201
|
+
if (!batchDocAbs) return null
|
|
202
|
+
const key = fileKey(batchDocAbs)
|
|
203
|
+
for (const e of getAsyncPool(parent, "subagent")?.values() ?? []) {
|
|
204
|
+
if (e.status !== "running" && e.status !== "queued") continue
|
|
205
|
+
if (e.done === true || e.cancelled === true) continue
|
|
206
|
+
if (!e._batchDoc || fileKey(e._batchDoc) !== key) continue
|
|
207
|
+
return { id: e.id, role: e.role, status: e.status }
|
|
208
|
+
}
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
179
213
|
/** P-SL2(D-SL2——混合边环形等待停滞机械检测——AGENT-LOOP-SUBAGENT.md §6.9):
|
|
180
214
|
* 判据(收窄——零误报优先——宁可漏报不可误打断):池内 running = 0 && queued ≥ 2 &&
|
|
181
215
|
* 无 dependency-cancelled 标记条目 && 每 queued 的 blocker(files 冲突者 + 未 settle
|