@migor/cli 0.9.3 → 0.9.4

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 CHANGED
@@ -5,6 +5,99 @@
5
5
  (0.x 阶段:minor 可含破坏性变更,每个破坏性变更都在对应版本的「迁移」小节里写明)。
6
6
  决策的完整证据链在 `docs/spec.md` §10(带时间线的决策日志)。
7
7
 
8
+ ## [0.9.4] - 2026-09-26
9
+
10
+ > 本版主题(窗口 `0.9.3 → 0.9.4`):**成本与限制的账本对齐 + 「能不能发」有了判据**。
11
+ > 四块内容:① 成本口径补齐(缓存读/写单价,含 1h 档 = 2×;图片按**尺寸上界**估);
12
+ > ② 三个「旋钮设成 0 就静默失效」的洞改成**构造期响亮失败**;③ 大载荷不再灌进摘要器
13
+ > (`tool_result` **内嵌**图片块与 `tool_use` 参数两处,实测一张 200 000 字符 base64 的截图
14
+ > 此前渲染出 200 086 字符、估算 50 022 token ⇒ 修后 32 字符 / 3 139 token);
15
+ > ④ 评测即发布闸门(配方 + 可拷走的实现 + 守卫)。另含官网的 agent 可读面(每页 `.md` 变体 +
16
+ > `Accept: text/markdown` 内容协商)。
17
+ > **无破坏性变更**:框架公共 API 与脚手架模板形态逐字未变 —— 三个旋钮的 `0` 原本就
18
+ > 「静默不派发 / 拒掉一切 / 流活不过一帧」,现在改成启动期报错,**没有任何本来能工作的配置
19
+ > 会因此变坏**;既有使用者不需要任何动作。
20
+
21
+ ### 修复 · 大载荷不再灌进摘要器
22
+
23
+ - **`tool_result` **内嵌**图片块按尺寸上界估、只给占位**:工具返回截图是生产里最常见的入图路径,
24
+ 而此前的「渲染 / 摘要侧只给 `[image …]` 占位、不展开 base64」只覆盖**顶层**图片块。
25
+ 实测一张 200 000 字符 base64 的截图:渲染出 200 086 字符(含原始 base64)、估算 **50 022 token**
26
+ —— 修后 **32 字符 / 3 139 token**,与顶层图片同一条口径(内嵌与顶层的估算差 ≤ 3 token)。
27
+ - **`tool_use` 的参数同样有界**:同一类「大载荷灌进摘要器」的洞,两处各配回归用例
28
+ (含「小参数不许被截断」的阳性对照),变异验证 4/4 咬人。
29
+
30
+ ### 变更 · 三个旋钮的 `0` 从「静默失效」改成构造期报错
31
+
32
+ - **`Scheduler.every.maxInFlight: 0`**:闸门判据是 `inFlight.size >= maxInFlight`,`0` / 负数时
33
+ **恒真** ⇒ 每次 tick 都跳过,周期任务**永不派发且不报错**(实测 60 ms 内派发 0 次、无任何日志)。
34
+ 现在与 `AsyncRunner.concurrency` 同款在构造期抛 `TypeError`;`Infinity`(关闸门)与缺省 `1` 不受影响。
35
+ - **`HttpHandlerOptions.maxBodyBytes: 0`**:实测「每个带 body 的请求都 413」(3 条 POST 全部 413)。
36
+ - **`SseWriterOptions.maxBufferedBytes: 0`**:实测「流活不过一帧」。
37
+ - 三处都判 `invalid` + 构造期抛错,并接上 `limits.ts`(「限制旋钮的 0 是什么」的单一真源)的
38
+ `zeroClauseOf` 文案单源。⚠️ 还挡一类事故:`Number('') === 0` —— 空的环境变量会静默变成「拒绝一切」。
39
+
40
+ ### 新增 · 成本口径
41
+
42
+ - **`ModelPricing` 新增 `cacheRead` / `cacheWrite`**(缺省读 0.1×、写 1.25×;**1h 档写 2×** 由
43
+ `priceOverrides` 表达),非法乘数(负数 / NaN)同样构造期抛错。
44
+ - **未定价模型会「响」**:turn 上记 `usage.unpriced` 事件、指标有 `model_unpriced_turns_total`、
45
+ 可挂 `onUnpricedModel` 回调 —— 此前价格表外的模型成本恒为 0,`maxCostUsd` 这条护栏
46
+ **静默不触发**(现在失效本身是可观测的)。
47
+ - **图片块的 token 按尺寸上界估**:官方按**尺寸**计费(28×28 像素 = 1 visual token),与文件字节数
48
+ 无关;框架取「长边缩到 1568px」的上界 3136 token/块 —— 宁可高估(`maxTotalTokens` 提前拦)
49
+ 也不低估(护栏迟触发)。
50
+
51
+ ### 新增 · 评测即发布闸门(`docs/eval-gate.md` + `examples/eval-gate/`)
52
+
53
+ - 把「这一版能不能发」收成一条判据:**回归**(基线通过 → 这次失败)与**删用例**(基线里有 → 这次没跑)
54
+ 判不通过;「基线里本来就失败」**放行**(已知债不拦发布);退出码 `1`(回归)与 `2`(基线坏了)分两档。
55
+ - 配方文档 + 可直接拷走的实现 + 守卫(9 条纯函数断言 + 文档↔示例导出面双向覆盖 + e2e 真喂三份
56
+ 被改坏的基线)。**`src/` 零改动** —— 框架的职责到「产出结论」为止。
57
+
58
+ ### 新增 · 官网的 agent 可读面(GEO 档 C/D)
59
+
60
+ - **每页 `.md` 变体**:`packages/website/scripts/build-md-variants.mjs` 在构建末尾从**产物**
61
+ `dist/*.html`(与打分器同源)生成同名 `.md`,正文一个字不丢、剥壳集合与打分器一致。
62
+ - **`Accept: text/markdown` 内容协商**:`packages/website/public/_worker.js`(Cloudflare Pages
63
+ advanced mode)只改写带该头的**无扩展名页面路径**,其余(`/llms.txt`、`/robots.txt`、
64
+ `/sitemap.xml`、`/_astro/*`)原样透传,且**任何异常都退回 `env.ASSETS.fetch(request)`**
65
+ —— advanced mode 下所有请求都过它,兜底必须是最外层的那一个。
66
+
67
+ ### 仓库自身(不面向使用者)
68
+
69
+ - **`packages/cli/test/inspector.test.mjs` 新增 ⑫「选完文件夹 → 透传给 run 的就是所选目录」**:
70
+ 2026-09-24 真用户反馈「选完之后透传给 agent 的不是所选文件夹」,逐环核实后**链路是通的**
71
+ (`createApp` 把显式 `providers` 拼在 discover 结果**之后** ⇒ 模板的 `WORKDIR = opts.workdir`
72
+ 赢过目录里扫出来的无 deps 版本;面板 `readControls` 读 DOM 单源,重画只填空值不覆盖已选),
73
+ 但**当时没有任何守卫钉着这条链** —— `scripts/e2e-dev.ts` 第 15-ter 步只覆盖 `POST /run`
74
+ **直带** workdir,选择器那一跳(返回值 → 控件 → 请求体)全靠人读码。新用例钉两端接线:
75
+ ① **页面接线**:`/api/fs/pick` 的返回值必须写进工作目录控件,且 `readControls` 从**控件**
76
+ 取值(不是镜像 `state`);② **服务端接线**:把返回值原样喂给 `/run` ⇒ 钩子收到的 `workdir`
77
+ 就是它(且不是回落 `defaultWorkdir`)。反向验证三条各自变红(摘页面接线 / 摘
78
+ `parseRunRequest` 的 workdir 转发 / 把 `readControls` 换成镜像 `state`)。
79
+
80
+ - **`§7 已知边界`(77 行)立守卫**(`tests/docs/boundary-table.test.ts`):§7 是使用者判断
81
+ 「我能不能用这个框架」的唯一依据,此前**零守卫**且被 `usage-guide.test.ts` 的解析器结构性排除
82
+ (那条规则只收「首列恰好是一个反引号标识符」的行,§7 首列是散文 ⇒ 整表零采集)。四条判据:
83
+ 标识符不悬空 / 行集合↔登记表双向 / `pin={file,marker}` 可证伪 / 三态(`pin` 38 · `choice` 9 ·
84
+ `gap` 30)+ 下限。`gap` 的语义是「这句话今天为真、明天可能为假而没人会发现」,**不是功能缺失**。
85
+ 反向验证 8/8 咬人 —— 变异电池当场抓出第一版 A3 是**假守卫**(子串判定,标题加一个字照样绿)。
86
+ - **`killTree` 的平台分派抽成可注入的纯函数**(`packages/cli/src/dev-child.ts` 的 `killPlanFor`):
87
+ win32 那条 `taskkill /T /F` 此前**在任何平台都跑不到**(直接读 `process.platform`,而 CI 是
88
+ ubuntu、macOS 走 POSIX)。行为逐字不变,新增 6 条用例、变异 4/4 咬人。
89
+ - **守卫注册表补登记**:`packages/cli/test/native-pick.test.mjs` 此前没进 `guards.md §1`
90
+ (守卫在、登记缺),已登记并补两条实测反向验证读数。守卫注册表 §1 逐行审计修掉 3 个「假守卫」。
91
+ - **工具链自我描述 ↔ `ci.yml` 互为真值**(`tests/scripts/verify-all-wiring.test.ts`):步骤数 ==
92
+ CI job 名里写死的数字(那是分支保护依赖的必需检查)、每个 job 都被交代、副本不许各说各话。
93
+ - **CI 新增 `docker-image` job(非必需检查)**:`examples/deploy` 的镜像此前**没有任何门禁构建过**
94
+ (docker 在本仓 CI 出现 0 次、本机无 docker)⇒ 「服务有门禁、镜像只有这一次构建」如实写进
95
+ `examples/deploy/README.md` 与 `AGENTS.md`。
96
+ - **对外文章** `docs/articles/trace-first.md`:把核心卖点讲成一条可核对的故事(每个数字都是仓库
97
+ 自己能核对的事实:测试全绿数、三个包 dependencies 全空、8 步验证链、镜像只有一次构建)。
98
+ - **推广文案与仓库事实对齐**:修掉 6 类不符(指向别人包的安装命令、会腐烂的时长、硬编码计数、
99
+ 写错 scope 的 npm 链接、连不上的域名、**描述了不存在的功能**)。
100
+
8
101
  ## [0.9.3] - 2026-09-23
9
102
 
10
103
  > 本版主题(窗口 `0.9.2 → 0.9.3`):**dev 面板两个真用户反馈的落地** —— 原生文件夹选择器
@@ -1366,7 +1459,8 @@
1366
1459
  首个公开发布:`@migor/agentia` + `@migor/cli`(scope `@migor/*`),两包版本同步。
1367
1460
  框架本体单包;CLI 独立成包(workspaces)。
1368
1461
 
1369
- [Unreleased]: https://github.com/retrychx/agentia/compare/v0.9.3...HEAD
1462
+ [Unreleased]: https://github.com/retrychx/agentia/compare/v0.9.4...HEAD
1463
+ [0.9.4]: https://github.com/retrychx/agentia/releases/tag/v0.9.4
1370
1464
  [0.9.3]: https://github.com/retrychx/agentia/releases/tag/v0.9.3
1371
1465
  [0.9.2]: https://github.com/retrychx/agentia/releases/tag/v0.9.2
1372
1466
  [0.9.1]: https://github.com/retrychx/agentia/releases/tag/v0.9.1
package/dist/AGENTS.md CHANGED
@@ -288,7 +288,7 @@ token 的作用是挡住**本机其它进程**,别把它当网络边界:这
288
288
  | `onTraceEvent` | **增量记账出口的缺省值**:run **进行中**逐笔回调(`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`),给等不了收尾的消费者(面板 / SSE / 任务流)。与 `sinks` 是**两条缝**、可同时配;单次 run 给了自己的那个是**叠加**(应用级在前)而非覆盖。⚠️ **不保证送达**(宿主自己的流断了就断了),也不替代 `sinks` |
289
289
  | `maxTotalTokens` | 缺省成本硬管控:整条 run(**含子 agent / skill 子循环**,上限经 `ToolRunContext` 透传)累计 token 上限(可被单次 run 覆盖) |
290
290
  | `maxCostUsd` | 缺省成本硬管控:累计成本(美元)上限(**依赖模型在价格表内**,见 `priceOverrides`;未定价模型会留 `usage.unpriced` 事件,所以「护栏有没有真的生效」看得见) |
291
- | `priceOverrides` | 价格表覆盖/追加(`$/1M tokens`):覆盖内置同名项,或给非 Anthropic 模型定价(如 `{ 'deepseek-chat': { in: 0.27, out: 1.10 } }`)。**透传给子 agent/skill 的子循环** —— 不会「主 agent 有成本、子 agent 恒 0」。非法单价在 run 开始即抛错 |
291
+ | `priceOverrides` | 价格表覆盖/追加(`$/1M tokens`):覆盖内置同名项,或给非 Anthropic 模型定价(如 `{ 'deepseek-chat': { in: 0.27, out: 1.10 } }`)。⚠️ **模型名是精确匹配**:Anthropic 每个型号都发两个 id(不带日期的别名 + 带日期的快照,如 `claude-haiku-4-5` / `claude-haiku-4-5-20251001`),**互不相通** —— 用哪个 id 就得按哪个 id 定价,否则 `maxCostUsd` 不触发(会留 `usage.unpriced`,见下)。条目还可带**缓存乘数** `cacheRead` / `cacheWrite`(缺省 `0.1` / `1.25`,相对 `in`;写 `0` 就是**乘数为零** = 该模型的缓存读 / 写不花钱,**不是**「未设」—— 实现用 `??` 而非 `||`):缓存写缺省只对 **5 分钟** 档正确,用了 `cache_control: { ttl: '1h' }` 就写 `cacheWrite: 2`;缓存读的逐模型例外(Opus 5.5 = `0.05`、Fable 5.1 / Mythos 5.1 = `0.025`)同理。**透传给子 agent/skill 的子循环** —— 不会「主 agent 有成本、子 agent 恒 0」。非法单价与非法乘数都在**第一次 llm 调用之前**就失败:run 以 `status: error` 收口(错误记在 run 根 span 上、`totalUsage` 全 0、client 一次都不被调用)—— ⚠️ **不是抛异常给调用方**(`buildPricing` 的错被 `runAgent` 收成 result,别写成 `try/catch` 等它抛) |
292
292
  | `onUnpricedModel` | 遇到价格表外的模型时回调(`{ model, spanId }`,每个循环作用域内每模型一次);抛错被吞,**不改变 run 结局**(定价缺失是宿主配置问题)。用它接告警 |
293
293
  | `toolTimeoutMs` | 缺省单工具超时(毫秒);超时该条 tool_result 记 is_error,不杀 run |
294
294
  | `maxToolConcurrency` | 缺省同回合并行工具上限;不设 = 不限(全并行) |
@@ -602,9 +602,9 @@ const handler = createHttpHandler(callable, { runner });
602
602
  |---|---|
603
603
  | `authenticate` | 入口鉴权钩子:**除 `/healthz` 与 `/metrics` 外所有路径**都过它,且在**读 body 之前**(未通过就不收 body)。正常返回即通过;抛 `HttpException` 按其 `status`/`body` 回;抛别的错误回 401,原文只进服务端日志。框架**不实现策略**(不读 env、不碰凭据) |
604
604
  | `metrics` | 指标出口:给 `metricsSink()`(或任意 `{ render() }` / 返回字符串的闭包)后,`GET /metrics` 回它的 Prometheus 文本。**不鉴权**(拉取端在集群内网);要保护请放反代后面。不给则该路径 404。⚠️ 这只管**渲染** —— 数字要真的累计,必须把**同一个** sink 注册进 `createApp({ sinks: [metrics] })`(它靠 run 收尾投递,不自己埋点),否则 `/metrics` 恒为 0 **且不报错** |
605
- | `maxBodyBytes` | 请求 body 上限(字节),超限回 413;缺省 1 MiB |
605
+ | `maxBodyBytes` | 请求 body 上限(字节),超限回 413;缺省 1 MiB。构造期校验:必须 > 0 或 `Infinity` —— 0 会让**每个带 body 的请求**都 413(`POST /run` / `POST /tasks` / 审批全废),故直接抛错。⚠️ 还挡 `Number('') === 0`:从**空的环境变量**读出来的配置会静默变成「拒绝一切」 |
606
606
  | `maxConcurrentRuns` | 同时在跑的 `POST /run` 上限,超限回 503 + `Retry-After`;缺省 32(传 `Infinity` 恢复无上限)。构造期校验:必须 > 0 或 Infinity —— NaN 会让闸门静默失效、0/负数会全部 503,故直接抛错 |
607
- | `sseMaxBufferedBytes` | SSE 下游积压上限(字节,`res.writableLength` 超过即收口该 SSE 流并 **abort 对应 run** —— 客户端已经不消费了,继续逐 token 生成只是白烧 token);缺省 8 MiB |
607
+ | `sseMaxBufferedBytes` | SSE 下游积压上限(字节,`res.writableLength` 超过即收口该 SSE 流并 **abort 对应 run** —— 客户端已经不消费了,继续逐 token 生成只是白烧 token);缺省 8 MiB。构造期校验:必须 > 0 或 `Infinity` —— 判据是**写入前**的 `pending > 上限`,0 时第 1 帧照写、**第 2 帧必收口**(流活不过一帧),故直接抛错 |
608
608
  | `exposeErrors` | 是否把内部异常原文回给调用方;缺省 `false`(细节只进服务端日志) |
609
609
  | `runner` | 注入 `AsyncRunner`(共用 store / 并发上限 / `resumePending`);缺省内部 `new AsyncRunner(app)` |
610
610
 
@@ -666,9 +666,9 @@ process.on('SIGTERM', async () => {
666
666
  | `createBudgetPolicy` | 预算策略:超预算先 `trimToolPairs` 编辑,再 `compactMessages` 压缩(带滞回) |
667
667
  | `trimToolPairs` | context editing:丢旧 tool 对(按**对数**,`keepToolPairs`) |
668
668
  | `compactMessages` | compaction:旧前缀做摘要(摘要器由你注入,框架不替你造 token) |
669
- | `estimateMessages` | 估算一组消息的 token(预算决策用,不是精确记账) |
669
+ | `estimateMessages` | 估算一组消息的 token(预算决策用,不是精确记账)。⚠️ **图片块按上界估**:块里没有宽高,而官方图片计费按**尺寸**(28×28 像素 = 1 token,与文件字节数无关)⇒ 框架按「长边缩到 1568px」的**上界 3136 token/块**计(宁可高估让 `maxTotalTokens` 提前拦,也不要低估让它迟触发)。未知块按**未截断**负载估(同上理由) |
670
670
  | `defaultEstimateTokens` | 缺省的单文本估算函数(CJK 感知启发式:CJK ≈ 1.5 字/token、其余 ≈ 4 字符/token) |
671
- | `renderMessages` | 把 messages 渲染成纯文本 —— 喂给你注入的 compaction 摘要器(`summarize`)用 |
671
+ | `renderMessages` | 把 messages 渲染成纯文本 —— 喂给你注入的 compaction 摘要器(`summarize`)用。⚠️ **产物是有界的**:图片块只给 `[image image/png ~150000B]` 这类占位(**含 `tool_result` 正文里嵌套的图片** —— 工具返回截图是常见的入图路径)、未知块截断到 200 字符、**工具参数**截断到 2000 字符,两种截断都**留计数**(不静默丢)—— **绝不展开 base64**(否则等于把整段 payload 当输入 token 发给摘要模型,真金白银 + 摘要质量一起毁)。⚠️ **纯文本**(`text` 块与 `tool_result` 的字符串正文)**刻意不截断**:那是摘要器要读的内容,截掉等于让 compaction 永久丢掉历史 |
672
672
 
673
673
  **per-run 隔离(`ContextPolicy.forRun`)**:策略可能被配成应用级单例(`createApp({ contextPolicy })`)
674
674
  被所有 run 复用。带状态的实现(滞回计数、token 缓存等)应实现可选的 `forRun(): ContextPolicy` ——
@@ -1419,6 +1419,9 @@ const callable = {
1419
1419
  | 鉴权失败即断连 | 未通过鉴权时在读到 body 之前就回响应,连接**不可复用**(显式 `connection: close`);这是「不收body省资源」的代价 |
1420
1420
  | 预算护栏不是硬实时 | 一回合记账完才判,实际用量可能超上限一个回合的量;并行子循环(一回合多个子 agent)各自过闸,超支上限是「**每个在飞分支**各一个回合」而非「总共一个回合」;模型自然收尾的那回合超限**不算失败**(只留 `budget.exceeded` 事件) |
1421
1421
  | `maxCostUsd` 依赖价格表 | 模型不在价格表内(且未用 `priceOverrides` 覆盖)时成本恒为 0,这条护栏**不触发** —— 要无条件兜底用 `maxTotalTokens`。**失效会响**:turn 上会记 `usage.unpriced` 事件、指标有 `model_unpriced_turns_total`、可回调 `onUnpricedModel` |
1422
+ | 价格表按模型名**精确匹配** | 别名与带日期快照是两个不同的键:内置表收了 `claude-haiku-4-5`,那么 `claude-haiku-4-5-20251001` 就是**未定价**(成本算不出、`maxCostUsd` 不触发,会留 `usage.unpriced`)。照抄官方文档里的带日期 id 最容易踩这一条 —— 要么改用不带日期的别名,要么 `priceOverrides` 里按**你实际传的那个 id** 补一条 |
1423
+ | 缓存写的缺省乘数只对 **5 分钟** 档正确 | 官方是 5m 写 1.25×、**1h 写 2×**、读 0.1×。而 `Usage` 是四项**聚合**的,分不出这一回合的缓存写走的是哪个 TTL(SDK 的 `Usage.cache_creation` 有拆分,框架没把它带上)⇒ 你在块上用了 `cache_control: { ttl: '1h' }` 就必须在 `priceOverrides` 里写 `cacheWrite: 2`,否则成本**低估 37.5%**、`maxCostUsd` 会迟触发。框架自己的调用点都不设 ttl,所以不改也对 |
1424
+ | 图片块的 token 只能估、且按**上界**估 | 块里没有宽高(`source` 只有 base64 的 `data` 或 `url`),而官方图片计费按**尺寸**(28×28 像素 = 1 visual token),与文件字节数**无关** —— 一张 200KB 纯色 PNG 和一张 200KB 细节图的成本能差两个数量级。框架取「长边缩到 1568px」的**上界 3136 token/块**:宁可高估(`maxTotalTokens` 提前拦)也不低估(护栏迟触发)。渲染 / 摘要侧则只给 `[image …]` 占位,**不展开 base64** |
1422
1425
  | 工具超时**不强制取消**工具 | `AgentTool.run` 没有 signal 参数,超时首先是「不等了」;副作用可能已发生。想真停:监听 `ToolRunContext.abandoned`(引擎放弃等待时 abort 它)自行收尾。框架自带的 @SubAgent / @Skill 已这么做 —— 超时即中止子循环(在飞请求被掐、不再后台烧 token),capability span 立刻以 error 收尾 |
1423
1426
  | 会话只存对话轮次 | `SessionStore` 存「用户输入 + 最终回复」,run 内部的 tool 往返**不进历史**(要完整过程用 `traceToMessages`);且只有**跑成功**的轮次才回写 |
1424
1427
  | 同 session 并发 run 要自行串行化 | `SessionStore` 是 **append-only**:并发写不互相覆盖、不丢数据,但**不保证角色交替** —— 两个并发 run 共用同一 sessionId 时,各自追加的轮次可能交错成「连续两条 user」,下一轮 load 出来撞角色交替校验(400)。同一 session 的并发 run 请调用方自行串行化(每 session 一把锁 / 一条队列) |
package/dist/dev-child.js CHANGED
@@ -50,20 +50,30 @@ export function resolveTsxCli(root, fromCli) {
50
50
  }
51
51
  /** 子进程优雅退出的等待上限,超时补 SIGKILL(kill 必须连整棵树 —— 见 killTree) */
52
52
  export const KILL_GRACE_MS = 3_000;
53
+ /** 平台 → 杀树计划(缺省平台由调用方给进程平台) */
54
+ export function killPlanFor(platform, pid, signal) {
55
+ if (pid === undefined)
56
+ return { kind: 'none' };
57
+ if (platform === 'win32') {
58
+ // 不带 /T 就只杀直接子进程,`npx tsx` 的孙进程会留下孤儿 —— 「整棵树」全落在这个开关上
59
+ return { kind: 'taskkill', args: ['/pid', String(pid), '/T', '/F'] };
60
+ }
61
+ return { kind: 'process-group', pid, signal };
62
+ }
53
63
  /**
54
- * 杀掉**整棵树**。
64
+ * 杀掉**整棵**树(执行器;分派在 `killPlanFor`,行为与抽取前**逐字一致**)。
55
65
  *
56
66
  * `npx tsx` 自己还会 spawn 子进程,只 kill 直接子进程会留下孤儿(照搬 v0.7.0
57
67
  * 「`close()` 保证子进程已终止」的纪律)。POSIX 下靠 `detached: true` 把子进程
58
68
  * 变成进程组组长,再对**负 pid** 发信号;win32 没有进程组,用 `taskkill /T`。
59
69
  */
60
70
  export function killTree(child, signal) {
61
- const pid = child.pid;
62
- if (pid === undefined)
71
+ const plan = killPlanFor(process.platform, child.pid, signal);
72
+ if (plan.kind === 'none')
63
73
  return;
64
- if (process.platform === 'win32') {
74
+ if (plan.kind === 'taskkill') {
65
75
  try {
66
- spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' });
76
+ spawn('taskkill', plan.args, { stdio: 'ignore' });
67
77
  }
68
78
  catch {
69
79
  /* 已经退出了 */
@@ -71,7 +81,7 @@ export function killTree(child, signal) {
71
81
  return;
72
82
  }
73
83
  try {
74
- process.kill(-pid, signal); // 负 pid = 整个进程组
84
+ process.kill(-plan.pid, signal); // 负 pid = 整个进程组
75
85
  }
76
86
  catch {
77
87
  try {
@@ -9,10 +9,10 @@
9
9
  "typecheck": "tsc --noEmit -p tsconfig.json"
10
10
  },
11
11
  "dependencies": {
12
- "@migor/agentia": "^0.9.3"
12
+ "@migor/agentia": "^0.9.4"
13
13
  },
14
14
  "devDependencies": {
15
- "@migor/cli": "^0.9.3",
15
+ "@migor/cli": "^0.9.4",
16
16
  "tsx": "^4.19.0",
17
17
  "typescript": "^7.0.2",
18
18
  "@types/node": "^22.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@migor/cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Agentia 框架命令行工具:create 脚手架 + g 能力生成 + dev 本地可观测面板(trace inspector)",
5
5
  "type": "module",
6
6
  "bin": {