@thincoder/core 0.9.5 → 0.10.2
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 +49 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
|
@@ -43,7 +43,8 @@ import { refillAdvisorQueue } from "./advisor-async.mjs"
|
|
|
43
43
|
/**
|
|
44
44
|
* 载体字段读取吸收(#94——「VSC 的跨 runAgent 存活载体面按核内结构归一」):
|
|
45
45
|
* §2.3 载体口径 = `_asyncSubagents` · `_asyncAdvisors` · `_consultSessions` ·
|
|
46
|
-
* `_pendingAsyncResults` · `_suspended`(AGENT-LOOP.md §2.3 seam
|
|
46
|
+
* `_pendingAsyncResults` · `_suspended`(AGENT-LOOP.md §2.3 seam 表);#9 后台任务池
|
|
47
|
+
* `_bgTasks` 同列(写侧父字段 + 借用/别名 history——见 `bash-async.mjs` `ensureBgPool`)。
|
|
47
48
|
* CLI 形:字段直接挂 agent(跨 run 存活);VSC 形:字段挂 depth-0 `history`
|
|
48
49
|
* (agent per-run 重建——run 起始把 history 容器绑到 agent 字段,绑定不变式)。
|
|
49
50
|
* 本函数吸收两形为同一读取:父对象字段优先,缺字段回退 `parent.history`。
|
|
@@ -91,7 +92,7 @@ export function tombstoneOf(parent, id) {
|
|
|
91
92
|
/**
|
|
92
93
|
* 条目 controller 链到基信号单点(#98 VSC 侧并入——interrupt 豁免面;原 subagent-run /
|
|
93
94
|
* advisor-async / escalate-async / consult 四处逐字同构):
|
|
94
|
-
* - 基信号**已 aborted** 且非 interrupt ⇒ 立即 abort(reason
|
|
95
|
+
* - 基信号**已 aborted** 且非 interrupt ⇒ 立即 abort(reason 逐跳保留——AGENT-LOOP-SUBAGENT.md §6.12 站点 #10);
|
|
95
96
|
* - interrupt aborted(Ctrl+I——中断消息注入后同回合续跑)⇒ **不逐链中止**(池保留;
|
|
96
97
|
* 子代理 / 评审继续跑完,settle 照常注入——「Ctrl+I keeps the pool」意图对齐);
|
|
97
98
|
* - 未来 abort ⇒ 非 interrupt 才逐链传播(Stop / 会话中止全停语义不变)。
|
|
@@ -111,11 +112,12 @@ export function bindChildController(ctrl, baseSignal) {
|
|
|
111
112
|
// ─── D1 池 accessor(吸收双池)────────────────────────────────────────────────
|
|
112
113
|
|
|
113
114
|
/** 池 accessor(D1):role "advisor" → `_asyncAdvisors`(独立评审池——无队列独立调度)·
|
|
114
|
-
* role "consult" → `_consultSessions`(会话池)·
|
|
115
|
-
*
|
|
116
|
-
* `?? new Map()` / 可选链处置。 */
|
|
115
|
+
* role "consult" → `_consultSessions`(会话池)· role "bg" → `_bgTasks`(#9 后台 bash
|
|
116
|
+
* 任务池)· 其余 → `_asyncSubagents`(子代理/飞刀共享槽位队列池)。载体经 `carrierField`
|
|
117
|
+
* 吸收(#94);未初始化返 null——调用方以 `?? new Map()` / 可选链处置。 */
|
|
117
118
|
export function getAsyncPool(parent, role) {
|
|
118
|
-
const field = role === "advisor" ? "_asyncAdvisors" : role === "consult" ? "_consultSessions"
|
|
119
|
+
const field = role === "advisor" ? "_asyncAdvisors" : role === "consult" ? "_consultSessions"
|
|
120
|
+
: role === "bg" ? "_bgTasks" : "_asyncSubagents"
|
|
119
121
|
return carrierField(parent, field) ?? null
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -155,7 +157,7 @@ export function buildChildSignal(parent, ctx) {
|
|
|
155
157
|
// ─── D3 settle 共享 helper ───────────────────────────────────────────────────
|
|
156
158
|
|
|
157
159
|
/**
|
|
158
|
-
* §
|
|
160
|
+
* AGENT-LOOP.md §6.15 消化窗口持有释放(TUI-OOM-ROOTCAUSE——D-SM2 表 2 候选 1):注入完成后置空条目
|
|
159
161
|
* 对子代理对象的引用(`childAgent`/`report`——条目前此从不显式释放,挂起期 = 分钟级驻留)。
|
|
160
162
|
* 幂等(重入不抛);池内/挂起未消化窗口语义零变(settle 时刻不释放——表 2 候选 2 否决:
|
|
161
163
|
* status/observe 在窗口内仍读 child 摘要)。三消费点(回合尾收集 / run 起始 pending 注入 /
|
|
@@ -178,10 +180,10 @@ export function releaseSettledEntry(entry) {
|
|
|
178
180
|
* ③ 分流:cancelled(出池 + cancelled 墓碑 + ⟦ev⟧stopped + 族提醒——不入 pending)→
|
|
179
181
|
* parentAborted(守卫抑制——不落事件不入流——中止清池在回合尾/挂起中止统一做)→
|
|
180
182
|
* settled(族 onAccounting hook 记账 → 挂起期(或 consult 族恒停靠)移交 pending
|
|
181
|
-
* 单容器 +
|
|
183
|
+
* 单容器 + 出池,非挂起期留池 done:true;两态一律 ⟦ev⟧settled——⟦ev⟧done = 消费面补发);
|
|
182
184
|
* ④ 公共尾部:settleSeq 递增 + `_settle` 唤醒 waiter + 腾槽补位(subagent/escalate 族
|
|
183
185
|
* 恒补——settle/cancel 释放槽 → maybeRefillAsync + refreshQueuedTokens——AGENT-LOOP.md
|
|
184
|
-
* §
|
|
186
|
+
* AGENT-LOOP-SUBAGENT.md §6.9 "settle/cancel 释放槽后…启动到槽满";advisor 经 refillAdvisorQueue 补位
|
|
185
187
|
* (ED-4——评审池有排队语义后不再豁免);consult(会话池)豁免)。
|
|
186
188
|
* opts(设计签名 `{pool, onAccounting}` + 实现参数 ctx):
|
|
187
189
|
* - pool:条目所在池 Map(出池 delete 目标;consult 传 null——会话池无条目)。
|
|
@@ -193,7 +195,7 @@ export function releaseSettledEntry(entry) {
|
|
|
193
195
|
* 设计偏差注(advisor code review 发现——交付报告同步):设计 D3 把 maybeRefillAsync
|
|
194
196
|
* 归入 onAccounting hook(仅 settled 分支执行)——running 取消的 cancelled 分支将不再
|
|
195
197
|
* 补位(槽释放但 queued 头停滞——挂起会话无回合尾补位覆盖),违背既有不变式
|
|
196
|
-
* (AGENT-LOOP.md §
|
|
198
|
+
* (AGENT-LOOP-SUBAGENT.md §6.9 + subagent-scheduler maybeRefillAsync 文档 "called from every
|
|
197
199
|
* settle")与 VSC 镜像(无条件 refill)。修正:腾槽补位移入公共尾部(subagent/escalate
|
|
198
200
|
* 族恒补——旧行为零回归;advisor/consult 豁免——同 VSC refill!==false 语义)。
|
|
199
201
|
*/
|
|
@@ -235,7 +237,7 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
|
|
|
235
237
|
// AGENT-LOOP-SUBAGENT.md §6.7.2 cancelled settle 分支(D-M6):不入 pending、不参与回合尾直注入——清池 +
|
|
236
238
|
// 终态墓碑(dependsOn 取消语义)+ ⟦ev⟧stopped 冻结 + 族提醒(半成品警示对模型可见)。
|
|
237
239
|
pool?.delete(String(entry.id))
|
|
238
|
-
writeTombstone(parent, entry.id, "cancelled", role) // §
|
|
240
|
+
writeTombstone(parent, entry.id, "cancelled", role) // AGENT-LOOP-SUBAGENT.md §6.9 D-SD5:单点写入(#94 载体吸收)
|
|
239
241
|
ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
|
|
240
242
|
if (role === "advisor") {
|
|
241
243
|
// ②-6b:评审取消——token 未签发提醒(settleAdvisorRun 不消费预算)。
|
|
@@ -249,7 +251,7 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
|
|
|
249
251
|
content: `[System reminder: async escalate #${entry.id} (${entry.tag}) cancelled by user — partial changes not merged/audited]`,
|
|
250
252
|
})
|
|
251
253
|
} else {
|
|
252
|
-
// §
|
|
254
|
+
// §6.9 D-SD5:running 依赖取消——提醒列出依赖者(供模型决策)。
|
|
253
255
|
const dependents = dependentLabels(parent, String(entry.id))
|
|
254
256
|
const autoNote = parent.autoApprove
|
|
255
257
|
? " — AUTO session: they auto-start on slot availability (round2 #3)"
|
|
@@ -264,22 +266,23 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
|
|
|
264
266
|
// escalate 腾槽补位)——可改写 entry.report(settle 分支输出——digest 原样进)。
|
|
265
267
|
onAccounting?.(parent, entry)
|
|
266
268
|
// 挂起分流(AGENT-LOOP-ASYNC-POOL.md §6.8 D-S8 + D-S3 记账——以读取时刻为准):挂起期 settle → 移交 pending
|
|
267
|
-
// 单容器(digest 注入)+
|
|
268
|
-
//
|
|
269
|
+
// 单容器(digest 注入)+ 出池;非挂起期 ⇒ 留池 done-in-pool(回合尾 collectSettledAsync 注入兜底 ∥
|
|
270
|
+
// 挂起会话 sweep 消费链照旧)。
|
|
269
271
|
// consult 族恒停靠(settle 即出会话池入 pending——无 TUI 冻结事件——子块各自冻结)。
|
|
270
272
|
if (suspended || role === "consult") {
|
|
271
273
|
parkAsyncPending(parent, entry)
|
|
272
274
|
pool?.delete(String(entry.id))
|
|
273
|
-
ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
|
|
274
|
-
} else {
|
|
275
|
-
ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
276
275
|
}
|
|
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`)
|
|
277
280
|
}
|
|
278
281
|
// ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
|
|
279
282
|
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
280
283
|
entry._settle?.()
|
|
281
284
|
wakeAsyncWaiters(parent) // §6.27.12.4 ①:挂起驱动唤醒单点(settle 尾 / 上行 ask 入队尾两处调用)
|
|
282
|
-
// §
|
|
285
|
+
// §6.9 D-SD4 释放点:settle/cancel 释放槽 + 依赖终态转移 → 补位(依赖满足者/域冲突
|
|
283
286
|
// 解除者自动启动——槽 ≤4)→ 排队态面板刷新(等待块头标注随终态更新——dependency
|
|
284
287
|
// cancelled / 位置前移)。subagent/escalate 族恒补(旧行为零回归——cancelled 分支
|
|
285
288
|
// 同样补位);advisor(ED-4——独立评审池的排队语义)经 refillAdvisorQueue 补位;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* §
|
|
12
|
+
* AGENT-LOOP-SUBAGENT.md §6.7.6 D-TS5 (A2): mechanically summarize the parent spawn task book for the
|
|
13
13
|
* audit spawn — the three audit-relevant elements VERBATIM (design doc paths /
|
|
14
14
|
* affected-file list / acceptance criteria); verbose context/background is
|
|
15
15
|
* dropped (the auditor can read the design docs themselves — they stay
|
|
@@ -70,8 +70,8 @@ export function summarizeEngTaskBook(taskInput) {
|
|
|
70
70
|
export function buildAuditBlock(ctx) {
|
|
71
71
|
const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
|
|
72
72
|
return `[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
|
|
73
|
-
// §
|
|
74
|
-
// 不再靠模型自悟;范围限制是 §
|
|
73
|
+
// §6.7.6 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
|
|
74
|
+
// 不再靠模型自悟;范围限制是 §6.7.4 D-AG3 声明(下方 Zero-git scope authority)
|
|
75
75
|
// 的同源一句指注,不重复声明。
|
|
76
76
|
`[Audit instructions — mechanical template:]\n` +
|
|
77
77
|
`You are auditing an eng-coder delivery against its approved design — audit for EXACTLY these four deviation categories:\n` +
|
|
@@ -83,21 +83,21 @@ export function buildAuditBlock(ctx) {
|
|
|
83
83
|
`workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
|
|
84
84
|
`Scope discipline (F-TS6 A1): read ONLY the audited files and the design-doc sections relevant to this delivery — do NOT re-read whole documents.\n` +
|
|
85
85
|
`Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
|
|
86
|
-
// §
|
|
86
|
+
// §6.7.6 D-TS5 A2:任务书从全量 verbatim 改机械摘要块(三要素逐字——排除冗长上下文)。
|
|
87
87
|
`[Parent spawn task book — mechanical summary: design docs + affected-file list + acceptance criteria verbatim; verbose context/background dropped — the design docs are still available for reading outside this input:]\n` +
|
|
88
88
|
`${summarizeEngTaskBook(ctx.agent._engTaskInput)}\n` +
|
|
89
89
|
`Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
|
|
90
90
|
// §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
|
|
91
|
-
// git 上下文——§
|
|
91
|
+
// git 上下文——§6.7.4 全角色零 git);_touchedFiles 为审计范围;工作区未列于
|
|
92
92
|
// _touchedFiles 的改动与本任务无关,不作超清单依据(VS Code auditTaskBook 同款措辞)。
|
|
93
93
|
"Zero-git scope authority: this audit task receives NO git context — nothing is injected. " +
|
|
94
94
|
"The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
|
|
95
95
|
"Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
|
|
96
|
-
// §
|
|
96
|
+
// §6.7.6 D-A1.2:审计预算句——A1 指令模板 + A2 摘要块之后、A3 报告模板之前(定序——评审 #7)。
|
|
97
97
|
// 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
|
|
98
98
|
// 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
|
|
99
99
|
`\n[Audit budget — mechanical]: read ONLY the touched files listed above and the design-doc sections the parent task book names (affected-files table, acceptance criteria, status line). Do NOT read whole documents. Budget = 10 tool rounds max — if you cannot conclude within it, report PROBLEM (inconclusive) rather than continuing to explore.\n` +
|
|
100
|
-
// §
|
|
100
|
+
// §6.7.6 D-TS6 A3:审计输出报告格式模板(三态——字段化行——不让模型自由发挥)。
|
|
101
101
|
`\n[Audit report format — mechanical template:]\n` +
|
|
102
102
|
`Report EXACTLY one of three states:\n` +
|
|
103
103
|
`- CLEAN — no deviation across the four categories: reply the line "Four deviation categories: none found." (四类偏差均未发现);\n` +
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash-async.mjs — #9 `bash` 后台任务池(异步 ∥ 后台执行;设计 = 批档
|
|
3
|
+
* `docs/batches/2026-09-29-tools-carryover.md` §2.1——口径同源 = consult ∥ escalate 族,
|
|
4
|
+
* AGENT-LOOP-ASYNC-POOL.md §6.8 ∥ §6.10)。
|
|
5
|
+
*
|
|
6
|
+
* 本档 = 后台任务单点(池 ∥ 起跑 ∥ 结算 ∥ 杀):
|
|
7
|
+
* - 池:分域池 `_bgTasks`(模式照 `_asyncAdvisors`——独立池 + 载体吸收:写侧父字段、读侧
|
|
8
|
+
* `carrierField`——缺省借用/别名 `history` 侧,VSC 形跨 runAgent 存活)。
|
|
9
|
+
* - 起跑:id 沿 `nextSubagentId` 共用命名空间(`bash#N` 前缀区分域);帽 `BG_TASK_MAX = 4`
|
|
10
|
+
* (对齐 `ASYNC_POOL_LIMITS` 单域 4)——超限**显式拒**(零静默丢;无排队面)。
|
|
11
|
+
* - 输出面:全量流式落盘 `configDir/tool-results/<ts>-bash-<id>.log`(轮转复用
|
|
12
|
+
* `cleanupOldToolResults`);内存尾部环形缓冲(注入面取尾截:≤120 行 ∧ ≤8KB 双帽 +
|
|
13
|
+
* 截尾注记;全量恒在 log——不经预算 offload)。
|
|
14
|
+
* - 送达:进程退出/被杀 ⇒ 结算 ⇒ 停靠 pending 单容器(consult 族同式「恒出池」)⇒ 唤醒挂起
|
|
15
|
+
* 驱动(`wakeAsyncWaiters`)⇒ 消化轮 run-start 注入(`injectBgResult`——子代理族注入器
|
|
16
|
+
* `injectAsyncResult` 按 role="bg" 分发到本档)。
|
|
17
|
+
* - 超时:async **不收**同步径默认 120s(长任务 = 本形目的);显式 `timeout` 到点杀树
|
|
18
|
+
* (结算状态 `killed: timeout`)。
|
|
19
|
+
* - 收尾两档(D9-7):回合中断(Ctrl+I)**不杀**——`bindChildController` interrupt 豁免,
|
|
20
|
+
* 池保留(后续经 wait_for ∥ settle 消费)∥ 会话中止(Ctrl+C ∥ Stop)⇒ 条目控制器中止 ⇒
|
|
21
|
+
* `async-discard.mjs` 收尾面逐条杀树 + 出池 + 墓碑(单不变量「杀 ⟺ 控制器已中止」)。
|
|
22
|
+
* - 边界(v1):不做可见面板块 ∥ 状态行段;不做跨会话存活(会话结束即清——进程退出径无钩子)。
|
|
23
|
+
*
|
|
24
|
+
* 模块图:→ agent-tools/async-settle.mjs(池 accessor ∥ pending 停靠 ∥ 墓碑 ∥ 唤醒 ∥ 控制器
|
|
25
|
+
* 链结)· subagent-scheduler.mjs(取号/令牌/入池键守卫)· tools/process-tree.mjs(树杀)·
|
|
26
|
+
* tools/shared.mjs(解码/清洗——与同步径同源)· tools/bash.mjs(`buildBashEnv` 单源)·
|
|
27
|
+
* agent/helpers.mjs(转义 ∥ 轮转清理)· config.mjs(configDir)· context.mjs(pushReal)
|
|
28
|
+
* ——叶子向、无环。
|
|
29
|
+
* 入径 = `tools/bash.mjs` / `tools/ops.mjs` **动态 import**(W8 契约②:本档静态链含
|
|
30
|
+
* agent-tools 族 ⇒ 端壳静态闭包不得经工具档命中 node:sqlite)。
|
|
31
|
+
*/
|
|
32
|
+
import { createWriteStream } from "node:fs"
|
|
33
|
+
import { mkdir } from "node:fs/promises"
|
|
34
|
+
import { join } from "node:path"
|
|
35
|
+
import { spawn } from "node:child_process"
|
|
36
|
+
import { configDir } from "../config.mjs"
|
|
37
|
+
import { cleanupOldToolResults, escapeXml } from "../agent/helpers.mjs"
|
|
38
|
+
import { pushReal } from "../context.mjs"
|
|
39
|
+
import { makeDecoder, sanitizeOutput } from "../tools/shared.mjs"
|
|
40
|
+
import { killProcessTree } from "../tools/process-tree.mjs"
|
|
41
|
+
import { buildBashEnv } from "../tools/bash.mjs"
|
|
42
|
+
import {
|
|
43
|
+
bindChildController, buildChildSignal, carrierField, getAsyncPool, parkAsyncPending,
|
|
44
|
+
tombstoneOf, wakeAsyncWaiters, writeTombstone,
|
|
45
|
+
} from "./async-settle.mjs"
|
|
46
|
+
import { assertPoolKeyFree, consumeSubagentToken, nextSubagentId } from "./subagent-scheduler.mjs"
|
|
47
|
+
|
|
48
|
+
/** 后台任务帽(对齐 `ASYNC_POOL_LIMITS` 单域 4——AGENT-LOOP-ASYNC-POOL.md §6.10)。超限显式拒。 */
|
|
49
|
+
export const BG_TASK_MAX = 4
|
|
50
|
+
|
|
51
|
+
/** 注入面双帽(批档 §2.1.3 送达面):≤120 行 ∧ ≤8KB;全量恒在 log。 */
|
|
52
|
+
const DIGEST_TAIL_MAX_LINES = 120
|
|
53
|
+
const DIGEST_TAIL_MAX_CHARS = 8 * 1024
|
|
54
|
+
/** 内存尾部环形缓冲保留量(缓冲仅需覆盖注入帽——多留一成余量)。 */
|
|
55
|
+
const TAIL_KEEP_CHARS = 32 * 1024
|
|
56
|
+
/** exit→close 宽限(同同步径后台注记面:孙进程握管道时 close 不来——`bash.mjs` 1500ms 同值)。 */
|
|
57
|
+
const EXIT_GRACE_MS = 1500
|
|
58
|
+
|
|
59
|
+
/** 测试缝:log 目录重定向(单测沙箱——生产从不调用;`_setDigestOffloadDirForTest` 同式)。 */
|
|
60
|
+
let _bgTaskLogDir = null
|
|
61
|
+
export function _setBgTaskLogDirForTest(dir) { _bgTaskLogDir = dir }
|
|
62
|
+
function bgTaskLogDir() { return _bgTaskLogDir ?? join(configDir, "tool-results") }
|
|
63
|
+
|
|
64
|
+
/** 池写侧(绑定不变式:主容器落父字段;缺省借用 `history` 侧 + 载体别名——`writeTombstone`
|
|
65
|
+
* 借用法同式,VSC 形跨 runAgent 存活;CLI 形父字段恒在场——零差分)。 */
|
|
66
|
+
function ensureBgPool(parent) {
|
|
67
|
+
if (parent._bgTasks instanceof Map) return parent._bgTasks
|
|
68
|
+
const borrowed = carrierField(parent, "_bgTasks")
|
|
69
|
+
if (borrowed instanceof Map) { parent._bgTasks = borrowed; return borrowed }
|
|
70
|
+
parent._bgTasks = new Map()
|
|
71
|
+
if (parent?.history && typeof parent.history === "object") parent.history._bgTasks = parent._bgTasks
|
|
72
|
+
return parent._bgTasks
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 条目读取(键形单源:写侧 `set(String(id))` ⇒ 读一律 String 归一;双形键兜底照子代理族)。 */
|
|
76
|
+
export function bgTaskEntry(parent, id) {
|
|
77
|
+
const pool = getAsyncPool(parent, "bg")
|
|
78
|
+
if (!(pool instanceof Map)) return null
|
|
79
|
+
const key = String(id)
|
|
80
|
+
return pool.get(key) ?? pool.get(Number(key)) ?? null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `wait_for` 条件 `bash id:N done` 判据(同式 `subagent id:N done`):池内 done ∥ **出池即 done**
|
|
84
|
+
* (结算恒出池——pending 停靠/丢弃/消费皆已结束,无可等待物;未知 id 亦按 done-by-vacuity)。 */
|
|
85
|
+
export function bgTaskDone(parent, id) {
|
|
86
|
+
const entry = bgTaskEntry(parent, id)
|
|
87
|
+
if (!entry) return true
|
|
88
|
+
return entry.done === true || entry.status === "done"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 杀单点:条目进程树(process kill ∥ discard 收尾面共用——树杀函数单源 = `tools/process-tree.mjs`)。 */
|
|
92
|
+
export function killBgTree(entry) {
|
|
93
|
+
if (entry?.child) killProcessTree(entry.child)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 起跑(ack 即返)。失败面 = 显式 `{ error }`(帽/深度/上下文——零静默丢)。
|
|
98
|
+
* @returns {{ id: number, ack: string, entry: object } | { error: string }}
|
|
99
|
+
*/
|
|
100
|
+
export async function launchBgTask(parent, ctx, { command, timeout = null, shell = null } = {}) {
|
|
101
|
+
if (!parent) return { error: "no agent context — background tasks run in a depth-0 session only" }
|
|
102
|
+
if ((ctx?.depth ?? 0) > 0) {
|
|
103
|
+
return { error: "bash async is depth-0 only — a child agent has no background task pool (run the command synchronously)" }
|
|
104
|
+
}
|
|
105
|
+
const pool = ensureBgPool(parent)
|
|
106
|
+
const running = [...pool.values()].filter((e) => e.done !== true)
|
|
107
|
+
if (running.length >= BG_TASK_MAX) {
|
|
108
|
+
return {
|
|
109
|
+
error: `background task cap reached (${running.length}/${BG_TASK_MAX} running: ${running.map((e) => `bash#${e.id}`).join(", ")})` +
|
|
110
|
+
` — wait for one to settle (wait_for "bash id:N done") or kill one (process action='kill' id:N)`,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const dir = bgTaskLogDir()
|
|
114
|
+
await cleanupOldToolResults(dir)
|
|
115
|
+
await mkdir(dir, { recursive: true })
|
|
116
|
+
// ED-5:取号 → 消费同步配对(无 await 间隙)+ 入池键守卫(同 id 二次入池 = 静默覆写 ⇒ 抛)
|
|
117
|
+
// ——守卫前置于 spawn:命中即抛时零副作用(无孤儿进程/无半开 log)。
|
|
118
|
+
const id = nextSubagentId(parent)
|
|
119
|
+
consumeSubagentToken(parent, id, "bash async launch", "bash")
|
|
120
|
+
assertPoolKeyFree(pool, id, "bash")
|
|
121
|
+
const logPath = join(dir, `${Date.now()}-bash-${id}.log`)
|
|
122
|
+
const cwd = ctx?.cwd ?? parent.cwd ?? process.cwd()
|
|
123
|
+
// UTF-8 码页前缀 = 同步径同款(win32 + 默认 shell);解码走 `makeDecoder`(同源)。
|
|
124
|
+
const effectiveCommand = process.platform === "win32" && !shell ? `chcp 65001 >nul && ${command}` : command
|
|
125
|
+
const child = spawn(effectiveCommand, [], {
|
|
126
|
+
cwd,
|
|
127
|
+
shell: shell ?? true,
|
|
128
|
+
windowsHide: true,
|
|
129
|
+
detached: process.platform !== "win32",
|
|
130
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
131
|
+
env: buildBashEnv(),
|
|
132
|
+
})
|
|
133
|
+
const entry = {
|
|
134
|
+
id, role: "bg", command, child, logPath,
|
|
135
|
+
status: "running", done: false, cancelled: false, discarded: false,
|
|
136
|
+
startedAt: Date.now(), endedAt: null, exit: null, killed: null, error: null,
|
|
137
|
+
report: null, tail: "", tailDropped: false, bytes: 0,
|
|
138
|
+
timeoutMs: null, timer: null, graceTimer: null, controller: null,
|
|
139
|
+
logStream: null, _settle: null, promise: null,
|
|
140
|
+
}
|
|
141
|
+
entry.promise = new Promise((res) => { entry._settle = res })
|
|
142
|
+
entry.logStream = createWriteStream(logPath, { flags: "a" })
|
|
143
|
+
entry.logStream.on("error", () => { /* log 写失败不进结算语义——尽力面 */ })
|
|
144
|
+
const outDecoder = makeDecoder()
|
|
145
|
+
const errDecoder = makeDecoder()
|
|
146
|
+
const appendChunk = (s) => {
|
|
147
|
+
if (!s) return
|
|
148
|
+
entry.bytes += s.length
|
|
149
|
+
try { entry.logStream.write(s) } catch { /* 同上——尽力面 */ }
|
|
150
|
+
entry.tail += s
|
|
151
|
+
if (entry.tail.length > TAIL_KEEP_CHARS) {
|
|
152
|
+
entry.tail = entry.tail.slice(entry.tail.length - TAIL_KEEP_CHARS)
|
|
153
|
+
entry.tailDropped = true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
child.stdout.on("data", (d) => appendChunk(sanitizeOutput(outDecoder(d))))
|
|
157
|
+
child.stderr.on("data", (d) => appendChunk(sanitizeOutput(errDecoder(d))))
|
|
158
|
+
// 会话/回合信号链(#98 链结单点——interrupt 豁免内建):非 interrupt 中止 ⇒ 控制器中止 ⇒
|
|
159
|
+
// discard 收尾面(杀树 + 出池 + 墓碑);回合中断(Ctrl+I)⇒ 不动(F2 同款豁免)。
|
|
160
|
+
const ctrl = new AbortController()
|
|
161
|
+
entry.controller = ctrl
|
|
162
|
+
bindChildController(ctrl, buildChildSignal(parent, ctx))
|
|
163
|
+
// 显式 timeout(async 缺省无超时——本形目的):到点杀树,结算状态 `killed: timeout`。
|
|
164
|
+
if (Number.isFinite(timeout) && timeout > 0) {
|
|
165
|
+
entry.timeoutMs = timeout
|
|
166
|
+
entry.timer = setTimeout(() => {
|
|
167
|
+
entry.killed = "timeout"
|
|
168
|
+
killBgTree(entry)
|
|
169
|
+
}, timeout)
|
|
170
|
+
}
|
|
171
|
+
child.on("error", (error) => {
|
|
172
|
+
entry.error = error?.message ?? String(error)
|
|
173
|
+
finalizeBgEntry(parent, entry, { code: null, signal: null })
|
|
174
|
+
})
|
|
175
|
+
child.on("exit", (code, exitSignal) => {
|
|
176
|
+
if (entry.done) return
|
|
177
|
+
// 宽限窗(同步径同判据):孙进程握管道 ⇒ close 不来——到点按「可能仍在跑」收尾。
|
|
178
|
+
entry.graceTimer = setTimeout(() => {
|
|
179
|
+
finalizeBgEntry(parent, entry, { code, signal: exitSignal, background: true })
|
|
180
|
+
}, EXIT_GRACE_MS)
|
|
181
|
+
})
|
|
182
|
+
child.on("close", (code, exitSignal) => {
|
|
183
|
+
finalizeBgEntry(parent, entry, { code, signal: exitSignal })
|
|
184
|
+
})
|
|
185
|
+
pool.set(String(id), entry)
|
|
186
|
+
return { id, ack: `bash#${id} started (running) — log: ${logPath}`, entry }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 结算单点(close ∥ exit 宽限 ∥ spawn error 三径合流;幂等:entry.done 即返)。
|
|
191
|
+
* 杀/丢弃两态(报告不可达)⇒ 只落终态与唤醒;正常退出 ⇒ 停靠 pending + 唤醒挂起驱动。
|
|
192
|
+
*/
|
|
193
|
+
function finalizeBgEntry(parent, entry, { code = null, signal = null, background = false } = {}) {
|
|
194
|
+
if (entry.done) return
|
|
195
|
+
entry.done = true
|
|
196
|
+
entry.status = "done"
|
|
197
|
+
entry.endedAt = Date.now()
|
|
198
|
+
entry.exit = { code, signal, background }
|
|
199
|
+
if (entry.graceTimer) { clearTimeout(entry.graceTimer); entry.graceTimer = null }
|
|
200
|
+
if (entry.timer) { clearTimeout(entry.timer); entry.timer = null }
|
|
201
|
+
try { entry.logStream?.end() } catch { /* 尽力面 */ }
|
|
202
|
+
getAsyncPool(parent, "bg")?.delete(String(entry.id))
|
|
203
|
+
if (entry.cancelled === true || entry.discarded === true) {
|
|
204
|
+
entry._settle?.() // 定向杀 ∥ 会话中止收尾——不注入(状态/墓碑已由杀点落定)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
entry.report = renderTail(entry).text
|
|
208
|
+
parkAsyncPending(parent, entry)
|
|
209
|
+
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
210
|
+
entry._settle?.()
|
|
211
|
+
wakeAsyncWaiters(parent)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 注入面尾截(双帽 + 截尾判据:行帽/字符帽/环形缓冲丢头三径合一)。 */
|
|
215
|
+
function renderTail(entry) {
|
|
216
|
+
let text = String(entry.tail ?? "")
|
|
217
|
+
let truncated = entry.tailDropped === true
|
|
218
|
+
const lines = text.split("\n")
|
|
219
|
+
if (lines.length > DIGEST_TAIL_MAX_LINES) {
|
|
220
|
+
text = lines.slice(-DIGEST_TAIL_MAX_LINES).join("\n")
|
|
221
|
+
truncated = true
|
|
222
|
+
}
|
|
223
|
+
if (text.length > DIGEST_TAIL_MAX_CHARS) {
|
|
224
|
+
text = text.slice(-DIGEST_TAIL_MAX_CHARS)
|
|
225
|
+
truncated = true
|
|
226
|
+
}
|
|
227
|
+
return { text, truncated }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 注入器(digest 单点族成员——`injectAsyncResult` 按 role="bg" 分发;消费即墓碑)。 */
|
|
231
|
+
export async function injectBgResult(parent, entry) {
|
|
232
|
+
const exit = entry.exit ?? {}
|
|
233
|
+
const status = entry.killed === "timeout"
|
|
234
|
+
? "killed: timeout"
|
|
235
|
+
: entry.error != null ? `spawn failed: ${entry.error}`
|
|
236
|
+
: exit.background ? `exit code ${exit.code} (a child process still holds the output pipe — output may be incomplete; it may still be running)`
|
|
237
|
+
: exit.signal ? `killed: ${exit.signal}`
|
|
238
|
+
: `exit code ${exit.code}`
|
|
239
|
+
const seconds = (Math.max(0, (entry.endedAt ?? Date.now()) - entry.startedAt) / 1000).toFixed(1)
|
|
240
|
+
const head = `[System reminder: background bash#${entry.id} finished — \`${entry.command}\` (${status}, ${seconds}s) — full log: ${entry.logPath}]`
|
|
241
|
+
const { text, truncated } = renderTail(entry)
|
|
242
|
+
const body = text.trim() ? escapeXml(text) : "(no output)"
|
|
243
|
+
const note = truncated ? `\n[... tail truncated — the full output is in the log: ${entry.logPath}]` : ""
|
|
244
|
+
pushReal(parent, { role: "user", content: `${head}\n${body}${note}` })
|
|
245
|
+
writeTombstone(parent, entry.id, entry.error != null ? "failed" : "consumed", "bg")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 杀单点(id 靶——`process` action='kill'):杀树 + 出池 + 墓碑(cancelled);重复 = 幂等
|
|
250
|
+
* (经终态墓碑返回同一确认——`cancelAsyncAdvisor` 同式)。
|
|
251
|
+
* @returns {{ id: string, status: "cancelled" | "error", error?: string, logPath?: string }}
|
|
252
|
+
*/
|
|
253
|
+
export function killBgTask(parent, id) {
|
|
254
|
+
const key = String(id)
|
|
255
|
+
if (!key || key === "undefined" || key === "null") {
|
|
256
|
+
return { id: key, status: "error", error: "process kill id: requires a background bash task id (bash#N from the async ack)" }
|
|
257
|
+
}
|
|
258
|
+
const pool = getAsyncPool(parent, "bg")
|
|
259
|
+
const entry = bgTaskEntry(parent, key)
|
|
260
|
+
if (!entry) {
|
|
261
|
+
const tomb = tombstoneOf(parent, key)
|
|
262
|
+
if (tomb?.status === "cancelled" && tomb.role === "bg") return { id: key, status: "cancelled" } // 幂等确认
|
|
263
|
+
return { id: key, status: "error", error: `unknown background bash task id: ${key} — it has finished, was killed, or was never started` }
|
|
264
|
+
}
|
|
265
|
+
if (entry.done) return { id: key, status: "error", error: `bash#${key} has already finished — nothing to kill` }
|
|
266
|
+
if (entry.cancelled) return { id: key, status: "cancelled", logPath: entry.logPath } // 杀已在途——幂等
|
|
267
|
+
entry.cancelled = true
|
|
268
|
+
killBgTree(entry)
|
|
269
|
+
pool?.delete(key)
|
|
270
|
+
writeTombstone(parent, key, "cancelled", "bg")
|
|
271
|
+
return { id: key, status: "cancelled", logPath: entry.logPath }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 杀单点(pid 靶——`process` action='kill'):`killProcessTree` 同款(win32 `taskkill /T /F` ∥
|
|
275
|
+
* POSIX 组杀 + 直杀兜底);pid 形态非法 ⇒ false(调用方给显式错误)。 */
|
|
276
|
+
export function killBgPid(pid) {
|
|
277
|
+
const n = Number(pid)
|
|
278
|
+
if (!Number.isInteger(n) || n <= 0) return false
|
|
279
|
+
killProcessTree({ pid: n, kill: (sig) => process.kill(n, sig) })
|
|
280
|
+
return true
|
|
281
|
+
}
|
|
@@ -19,7 +19,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
|
|
|
19
19
|
import { dirname, resolve } from "node:path"
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
-
SEGMENT_BY_ROLE, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHeaderRe, batchSkeleton,
|
|
22
|
+
SEGMENT_BY_ROLE, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHasStatusLine, sectionHeaderRe, batchSkeleton,
|
|
23
23
|
TEMPLATE_PLACEHOLDERS, findPlaceholderResidue, placeholderResidueError,
|
|
24
24
|
} from "./batch-skeleton.mjs"
|
|
25
25
|
import { resolveBatchCreatePath } from "./batch-paths.mjs"
|
|
@@ -56,8 +56,22 @@ function assertMainAgentOnly(ctx, action, review) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** 递归收集目录下全部 `*.md`(#369 · KD-4:显式嵌套路径为合法 create 面 ⇒ 顶层平扫会欠计
|
|
60
|
+
* ⇒ 复数情形漏判、默认定位可落非预期档)。只认真目录(`dirent.isDirectory()`——symlink 不入,
|
|
61
|
+
* 防环);单目录读错按无候选处理(尽力面,同旧平扫)。 */
|
|
62
|
+
function collectMarkdownFiles(dir, out = []) {
|
|
63
|
+
let entries = []
|
|
64
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
const abs = resolve(dir, e.name)
|
|
67
|
+
if (e.isDirectory()) collectMarkdownFiles(abs, out)
|
|
68
|
+
else if (e.isFile() && e.name.endsWith(".md")) out.push(abs)
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
/**
|
|
60
|
-
* depth-0 目标定位(D-BR21):扫描全部基底根下 `*.md
|
|
74
|
+
* depth-0 目标定位(D-BR21):扫描全部基底根下 `*.md`(**递归**——#369),逐档读 §1 状态行判定 open
|
|
61
75
|
* (readBatchStatusLine === "open")。0 个 ⇒ throw(无在飞批);恰 1 个 ⇒ 返回该档绝对路径;
|
|
62
76
|
* ≥2 ⇒ throw(复数在飞批——必传 path,错误消息列出候选)。
|
|
63
77
|
* 基底根数组由主档传入(batchDocBases——声明面单源,恒非空);单根目录读错按无候选处理。
|
|
@@ -69,11 +83,7 @@ export function findInFlightBatch(cwd, bases) {
|
|
|
69
83
|
void cwd
|
|
70
84
|
const candidates = []
|
|
71
85
|
for (const root of bases ?? []) {
|
|
72
|
-
|
|
73
|
-
try { entries = readdirSync(root) } catch { continue }
|
|
74
|
-
for (const name of entries) {
|
|
75
|
-
if (!name.endsWith(".md")) continue
|
|
76
|
-
const abs = resolve(root, name)
|
|
86
|
+
for (const abs of collectMarkdownFiles(root)) {
|
|
77
87
|
let src
|
|
78
88
|
try { src = readFileSync(abs, "utf8") } catch { continue }
|
|
79
89
|
if (readBatchStatusLine(src) === "open") candidates.push(abs)
|
|
@@ -140,9 +150,10 @@ export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
|
|
|
140
150
|
|
|
141
151
|
/**
|
|
142
152
|
* 段内状态行单行改写(status/close 共用——append-only 的唯一豁免,域限状态行):
|
|
143
|
-
* 定位 `## §N` 段(sectionHeaderRe
|
|
144
|
-
*
|
|
145
|
-
*
|
|
153
|
+
* 定位 `## §N` 段(sectionHeaderRe 单源),段内逐行扫描 STATUS_LINE_RE 首命中行原位替换
|
|
154
|
+
* (行尾随命中行自身);缺失则标题行后偏移插入状态行 + 空行(插入块行尾 = 标题行自身
|
|
155
|
+
* 行尾——锚行后首二字节判读)。段界外字节零变(行切分 = /\r?\n/:EOL 形态无关)。
|
|
156
|
+
* 段标题缺失 ⇒ throw(create 是骨架唯一权威入口)。
|
|
146
157
|
*/
|
|
147
158
|
function updateSectionStatusLine(src, seg, value) {
|
|
148
159
|
const hdr = sectionHeaderRe(seg).exec(src)
|
|
@@ -153,13 +164,34 @@ function updateSectionStatusLine(src, seg, value) {
|
|
|
153
164
|
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
154
165
|
const next = nextRe.exec(src)
|
|
155
166
|
const endIdx = next ? next.index : src.length
|
|
156
|
-
const eol = src.includes("\r\n") ? "\r\n" : "\n"
|
|
157
|
-
const lines = src.slice(hdr.index, endIdx).split(eol)
|
|
158
167
|
const line = `**状态行**:${value}`
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
const parts = src.slice(hdr.index, endIdx).split(/(\r?\n)/) // 保留分隔符(奇数下标 = 行尾)——替换只动命中行
|
|
169
|
+
const idx = parts.findIndex((p) => STATUS_LINE_RE.test(p))
|
|
170
|
+
if (idx >= 0) { parts[idx] = line } else {
|
|
171
|
+
const eol = parts[1] === "\r\n" ? "\r\n" : "\n" // 缺行插入块行尾 = 标题行自身行尾
|
|
172
|
+
if (parts.length === 1) parts.splice(1, 0, eol, line, eol, "", eol, "")
|
|
173
|
+
else parts.splice(2, 0, line, eol, "", eol, "", eol)
|
|
174
|
+
}
|
|
175
|
+
return src.slice(0, hdr.index) + parts.join("") + src.slice(endIdx)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** §6 段体非空白内容判据(#422①——close 前闸:段头行余文(标题文本)不计入内容;
|
|
179
|
+
* §6 段头缺失同判空——create 是骨架唯一权威入口)。 */
|
|
180
|
+
function sectionSixHasContent(src) {
|
|
181
|
+
const hdr = sectionHeaderRe(6).exec(src)
|
|
182
|
+
if (!hdr) return false
|
|
183
|
+
const nextRe = /^## §\d/gm
|
|
184
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
185
|
+
const next = nextRe.exec(src)
|
|
186
|
+
const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
|
|
187
|
+
const nl = body.indexOf("\n") // 跳过段头行余文(`## §6 …` 的标题部分)
|
|
188
|
+
return (nl >= 0 ? body.slice(nl + 1) : "").trim() !== ""
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** §6 空闸拒绝句(#422①——空 ⇒ 拒 + 提示先写 §6;防「append 被拒 ⇒ close 照常 ⇒ §6 永久为空」
|
|
192
|
+
* 复发——2026-09-26 dispatch-sizing 批实证)。 */
|
|
193
|
+
function emptySectionSixError() {
|
|
194
|
+
return "batch: §6 收口段为空(non-empty gate)— close refuses: the closeout section (§6) has no content, and closing would freeze the record with that section permanently empty. Write the section first (append §6: the closeout / verification summary), then call close again. Nothing was written."
|
|
163
195
|
}
|
|
164
196
|
|
|
165
197
|
/** 冻结门(§4.9——append/status 同门;close 视为写同样过门):closed/unknown 的错误串与
|
|
@@ -170,7 +202,7 @@ function assertGateOpen(src) {
|
|
|
170
202
|
throw new Error("batch: 已收口档不回改 — the record's §1 status line contains 「已收口」, so the record is frozen: its body is never written to again (整档冻结;改 = 新批新档). Nothing was written.")
|
|
171
203
|
}
|
|
172
204
|
if (gate === "unknown") {
|
|
173
|
-
throw new Error("batch: 状态行不可解析或缺失 — the record has no §1 `**状态行**:` line whose value contains 已收口 or 进行中 (fail-closed: the write is refused as if frozen). Nothing was written.")
|
|
205
|
+
throw new Error("batch: 状态行不可解析或缺失 — the record has no §1 `**状态行**:` line whose value contains 已收口 or 进行中 (fail-closed: the write is refused as if frozen). Nothing was written. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close).")
|
|
174
206
|
}
|
|
175
207
|
}
|
|
176
208
|
|
|
@@ -278,13 +310,20 @@ export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
|
|
|
278
310
|
* close——收口冻结(§4.13,仅 depth-0)。§1 状态行 →「已收口 <YYYY-MM-DD>」;此后该档
|
|
279
311
|
* append/status 全拒(冻结判据唯一真值 = §1 行——§4.9 零语义变)。对已收口档再 close ⇒ 拒
|
|
280
312
|
* (close 本身是对冻结档的写)。不代写 §6 内容、不替代台账核销事务。
|
|
313
|
+
* 前提三规则(#422):① §6 非空闸(空 ⇒ 拒 + 提示先写 §6);② 拒句含「失败 ⇒ 勿 close」
|
|
314
|
+
* (append/status 面——见 batch.mjs 与上方 assertGateOpen);③ 无状态行档 ⇒ 自建机读位
|
|
315
|
+
* (updateSectionStatusLine 缺失即插——同路收口;「有行但不可解析」仍 fail-closed)。
|
|
281
316
|
* @returns {string} 成功消息
|
|
282
317
|
*/
|
|
283
318
|
export function closeBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
|
|
284
319
|
assertMainAgentOnly(ctx, "close", review)
|
|
285
320
|
const abs = pickTarget(args?.path)
|
|
286
321
|
const src = readFileSync(abs, "utf8")
|
|
287
|
-
|
|
322
|
+
// #422③:§1 无状态行档 ⇒ 自建机读位(下行走 updateSectionStatusLine 缺失即插——同路收口);
|
|
323
|
+
// 「有行但不可解析」维持 fail-closed(assertGateOpen 拒——防误冻结)。
|
|
324
|
+
if (readBatchStatusLine(src) !== "open" && sectionHasStatusLine(src, 1)) assertGateOpen(src)
|
|
325
|
+
// #422①:§6 非空闸(空 ⇒ 拒 + 提示先写 §6)——防「append 被拒 ⇒ close 照常 ⇒ §6 永久为空」复发。
|
|
326
|
+
if (!sectionSixHasContent(src)) throw new Error(emptySectionSixError())
|
|
288
327
|
const date = todayLocal()
|
|
289
328
|
const written = updateSectionStatusLine(src, 1, `已收口 ${date}`)
|
|
290
329
|
writeFileSync(abs, written)
|