pi-legion 4.0.0 → 5.0.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 +248 -1
- package/README.md +101 -44
- package/agents/centurion.md +20 -16
- package/agents/scout.md +3 -1
- package/agents/worker.md +5 -7
- package/docs/contracts/declarative-phase-machine-v5.md +128 -0
- package/docs/contracts/extension-inheritance-wiring-v1.md +428 -0
- package/docs/contracts/subagent-renewal-v1.md +260 -0
- package/docs/headless-child-contract.md +6 -3
- package/docs/inprocess-supervisor.md +70 -24
- package/docs/legion-v2-contract.md +9 -0
- package/docs/spawn-mode-visibility-v1.md +4 -1
- package/index.ts +598 -44
- package/package.json +2 -2
- package/src/agents/agents.ts +293 -36
- package/src/agents/overrides.ts +3 -1
- package/src/agents/settings.ts +6 -18
- package/src/extension/child-guard.ts +25 -1
- package/src/extension/legion-tools.ts +66 -21
- package/src/extension/notify.ts +136 -4
- package/src/extension/playbook.ts +2 -2
- package/src/extension/runtime.ts +56 -1
- package/src/extension/summary.ts +13 -4
- package/src/extension/timing.ts +8 -4
- package/src/legion/phase-conditions.ts +164 -0
- package/src/legion/phase-machine.ts +403 -0
- package/src/legion/state-machine.ts +219 -115
- package/src/runs/args.ts +49 -18
- package/src/runs/extension-inheritance.ts +463 -0
- package/src/runs/orchestrator.ts +137 -44
- package/src/runs/progress-signal.ts +137 -0
- package/src/runs/store-lookup.ts +270 -0
- package/src/shared/types.ts +88 -3
- package/src/supervisor/child-context.ts +19 -0
- package/src/supervisor/inprocess-supervisor.ts +44 -5
- package/src/supervisor/routing-supervisor.ts +16 -1
- package/src/supervisor/rpc-supervisor.ts +16 -2
- package/src/tui/tree-view.ts +56 -0
- package/src/tui/viz/palette.ts +0 -3
- package/agents/advisor.md +0 -110
- package/agents/designer.md +0 -87
- package/agents/planner.md +0 -53
- package/agents/prototype.md +0 -72
- package/agents/reviewer.md +0 -113
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Declarative phase machine (v5)
|
|
2
|
+
|
|
3
|
+
Status: draft, layer 3 (#298).
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
The legion phase machine is hard-coded in three places that must agree:
|
|
8
|
+
|
|
9
|
+
| Place | Hard-codes |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `src/legion/state-machine.ts:1` | The seven phase names and every transition rule |
|
|
12
|
+
| `src/agents/settings.ts:117` | A duplicate copy of the phase names, for `phaseTimeouts` validation |
|
|
13
|
+
| `src/extension/legion-tools.ts:47` | The `legion_phase` tool's `to` enum |
|
|
14
|
+
|
|
15
|
+
Worse, the *rules inside a phase* are keyed on **role names**:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
implementing → reviewing requires every `role === "worker"` to be settled
|
|
19
|
+
reviewing → verifying requires a settled `role === "reviewer"` with verdict ok:true
|
|
20
|
+
planning → implementing requires no `role === "planner"` still running
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
So a team cannot re-shape its workflow without editing source, and a role that
|
|
24
|
+
merely *exists* acquires workflow meaning. "worker" is not "a worker" — it is a
|
|
25
|
+
specific slot in a fixed pipeline.
|
|
26
|
+
|
|
27
|
+
## Goal
|
|
28
|
+
|
|
29
|
+
A centurion declares its team's phases and the conditions for leaving them.
|
|
30
|
+
Defaults preserve today's behaviour exactly, so an undeclared machine is the
|
|
31
|
+
four-phase pipeline above.
|
|
32
|
+
|
|
33
|
+
## Non-goals
|
|
34
|
+
|
|
35
|
+
- Arbitrary scripting. Conditions are a closed vocabulary of predicates, not code.
|
|
36
|
+
- Per-node machines. One machine per team root; children inherit it read-only.
|
|
37
|
+
- Changing how children are spawned. Phases gate *progress*, not dispatch.
|
|
38
|
+
|
|
39
|
+
## Where the declaration lives
|
|
40
|
+
|
|
41
|
+
Role frontmatter, on the team root card:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
---
|
|
45
|
+
name: centurion
|
|
46
|
+
phases:
|
|
47
|
+
- id: planning
|
|
48
|
+
require: [contract_file]
|
|
49
|
+
next: [implementing]
|
|
50
|
+
timeoutMs: 900000
|
|
51
|
+
- id: implementing
|
|
52
|
+
require: [children_settled]
|
|
53
|
+
next: [reviewing, failed]
|
|
54
|
+
- id: reviewing
|
|
55
|
+
require: [verdict_ok]
|
|
56
|
+
next: [verifying, implementing]
|
|
57
|
+
- id: verifying
|
|
58
|
+
require: [verification_passed]
|
|
59
|
+
next: [done]
|
|
60
|
+
---
|
|
61
|
+
```text
|
|
62
|
+
|
|
63
|
+
Kept on the card rather than in `settings` because the machine is a property of
|
|
64
|
+
*this team*, like its roster — two centurions in one settings file may run
|
|
65
|
+
different workflows.
|
|
66
|
+
|
|
67
|
+
Resolution order: card `phases` → `settings.legion.phases` → built-in default.
|
|
68
|
+
|
|
69
|
+
## Condition vocabulary
|
|
70
|
+
|
|
71
|
+
A condition answers "may this node leave phase X for phase Y?" over a snapshot of
|
|
72
|
+
the node and its children. Closed set, each with one meaning:
|
|
73
|
+
|
|
74
|
+
| Condition | True when |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `contract_file` | `contract_path` is set and the file exists and is non-empty |
|
|
77
|
+
| `children_settled` | every child of this node is terminal (settled/failed/retired) |
|
|
78
|
+
| `children_none_failed` | no child is `failed`, or every failed child is listed in `handled` |
|
|
79
|
+
| `verdict_ok` | some settled child's last turn parsed to `{ok: true}` |
|
|
80
|
+
| `verdict_not_ok` | some settled child's last turn parsed to `{ok: false}` |
|
|
81
|
+
| `verification_passed` | the node's stored verification result is `pass` |
|
|
82
|
+
| `verification_failed` | the node's stored verification result is `fail` |
|
|
83
|
+
| `rework_available` | `rework_count < maxRework` |
|
|
84
|
+
| `children_all_failed` | there is ≥1 child and all are `failed` |
|
|
85
|
+
| `always` | unconditional |
|
|
86
|
+
|
|
87
|
+
`require` is a conjunction: all listed conditions must hold.
|
|
88
|
+
`next` is the allowed successor set; a transition to anything outside it is
|
|
89
|
+
refused with `invalid_transition`.
|
|
90
|
+
|
|
91
|
+
**Role names do not appear anywhere.** `verdict_ok` asks "did any settled child
|
|
92
|
+
return ok:true", not "did the reviewer". That is what lets `worker` become a
|
|
93
|
+
general worker.
|
|
94
|
+
|
|
95
|
+
## Terminal phases
|
|
96
|
+
|
|
97
|
+
Exactly three are terminal and are always available regardless of `next`:
|
|
98
|
+
|
|
99
|
+
- `done` — only when the phase's `require` holds
|
|
100
|
+
- `failed` — parent decision, all children failed, or rework exhausted
|
|
101
|
+
- `aborted` — parent retire only
|
|
102
|
+
|
|
103
|
+
## Safety rails (unchanged)
|
|
104
|
+
|
|
105
|
+
These are invariants, not configuration:
|
|
106
|
+
|
|
107
|
+
- A new team starts in the machine's **first** phase.
|
|
108
|
+
- `rework_count` still caps at `maxRework` (default 2).
|
|
109
|
+
- `centurion`-only gates stay: a child cannot advance the team.
|
|
110
|
+
- Phase timeouts still only *report*; they never kill work.
|
|
111
|
+
- A declared machine is validated at load time. Unknown condition, empty `next`,
|
|
112
|
+
duplicate id, unreachable phase, or a terminal phase used as a start → the card
|
|
113
|
+
is rejected (same posture as unknown frontmatter).
|
|
114
|
+
|
|
115
|
+
## Migration
|
|
116
|
+
|
|
117
|
+
The default machine reproduces today's table, so no behaviour change without a
|
|
118
|
+
declaration. The role-name predicates become condition predicates:
|
|
119
|
+
|
|
120
|
+
| Old rule | New condition |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| all `worker` settled before reviewing | `children_settled` |
|
|
123
|
+
| settled `reviewer` with ok:true | `verdict_ok` |
|
|
124
|
+
| no `planner` still running | covered by `children_settled` |
|
|
125
|
+
| verification pass before done | `verification_passed` |
|
|
126
|
+
|
|
127
|
+
Once this lands, `planner` and `reviewer` stop being load-bearing and their
|
|
128
|
+
bundled cards can be deleted.
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# 冻结契约:extension inheritance 接线(#280 的漏接线修复)
|
|
2
|
+
|
|
3
|
+
状态:**FROZEN** — 实施中禁止偏离。发现矛盾立刻停下报告,不要自圆其说。
|
|
4
|
+
范围:implementation-ready。本文件冻结接口、落点、禁令与验收。
|
|
5
|
+
|
|
6
|
+
## 0. 目标与不做的事
|
|
7
|
+
|
|
8
|
+
**目标**:让 5.0.0 已声明的策略「children inherit every host extension by default
|
|
9
|
+
(#280)」在运行时真正生效。当前 `discoverHostExtensions()` 全仓零调用,
|
|
10
|
+
`discoveredHostExtensions()` 恒为 `undefined`,`inheritedExtensions` 恒收到空 host
|
|
11
|
+
列表,策略层存在但等于空转。
|
|
12
|
+
|
|
13
|
+
**顺带修掉两个独立缺陷**(它们被空 discovery 掩盖,接线后会立刻显形):
|
|
14
|
+
|
|
15
|
+
- `collectChildExtensions(agent, env)` 丢弃 `env`,导致 `PI_LEGION_CHILD_INHERIT_EXTENSIONS=0`
|
|
16
|
+
对继承集无效(实测:`inheritedExtensions(agent, HOST, envOff)` → 0 条,但
|
|
17
|
+
`collectChildExtensions(agent, envOff)` → 7 条)
|
|
18
|
+
- legion 自身可能被当作「宿主扩展」注入子会话,与 child-guard 形成双份加载
|
|
19
|
+
|
|
20
|
+
**non-goals(本契约明确不做)**:
|
|
21
|
+
|
|
22
|
+
- 不改 `role-tool-gates.ts:79` 的「inheritsExtensions 为 true 时静音」策略。接线成功后该策略
|
|
23
|
+
才真正成立;若宿主确实缺某扩展,静音仍是既有独立缺陷,另立 issue。
|
|
24
|
+
- 不改 doctor 的 `!inheritGlobal` 模型探测短路(`legion-doctor.ts:569/597/607`)。接线后
|
|
25
|
+
`inheritGlobal=true` 与「真的继承了」重新一致。
|
|
26
|
+
- 不修 `playbook.ts:14` / `docs/headless-child-contract.md` 等文案。文案与代码再次一致后
|
|
27
|
+
这些描述自动成立。
|
|
28
|
+
- 不动 `--no-extensions` + 显式 `--extension` 的既有形状。rpc/inprocess 字节级对齐是
|
|
29
|
+
冻结契约(`args.ts:249-255`、`inprocess-supervisor.ts:151-159`)。
|
|
30
|
+
|
|
31
|
+
## 1. 已确认事实(父会话已核对,**禁止重新侦察**)
|
|
32
|
+
|
|
33
|
+
| # | 事实 | 证据 |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| F1 | `discoverHostExtensions()` 全仓零调用;`resetExtensionDiscoveryCache()` 同样零调用 | `src/runs/extension-inheritance.ts:128`、`:70`(唯一定义处) |
|
|
36
|
+
| F2 | `discovered` 唯一赋值点在 `discoverHostExtensions` 内部 | `extension-inheritance.ts:144` |
|
|
37
|
+
| F3 | `discoveredHostExtensions()` 是纯 sync getter,返回模块级 `discovered` | `extension-inheritance.ts:156-158` |
|
|
38
|
+
| F4 | `collectChildExtensions` 把 `discoveredHostExtensions() ?? []` 传给 `inheritedExtensions`,**未传 env** | `src/runs/args.ts:217` |
|
|
39
|
+
| F5 | `childInheritsExtensions` 默认返回 `true`;env 只能降不能升(`=1` 是 no-op) | `extension-inheritance.ts:51-61`(实测三种卡片形态下 env=1 与默认完全相同) |
|
|
40
|
+
| F6 | 实测 `moduleCache:false` 下模块级 `let` **跨 jiti 实例不可见**,且**同实例重复 import 也重执行**(executions: 2) | jiti 探针 P1/P1h(见侦察报告) |
|
|
41
|
+
| F7 | 默认缓存下同一模块实例可跨 jiti 实例共享(`B 看到 A 的 6 条`) | jiti 探针 P1-default-cache |
|
|
42
|
+
| F8 | 本仓另三处**故意**用 `globalThis+Symbol.for` 对抗 jiti 重载 | `src/supervisor/child-context.ts:11-24`、`src/supervisor/inprocess-depth.ts:6-26`、`src/supervisor/admission-queue.ts:25-31` |
|
|
43
|
+
| F9 | `/reload` → `clearExtensionCache()` → 入口模块重新 import → `discovered` 归零 | `agent-session.js:3010` → `resource-loader.js:352-359` → `loader.js:89-93`、`:461-490`(实测每次 reload 重新 transpile) |
|
|
44
|
+
| F10 | 同 cwd 下新建子 loader **复用**父模块实例;cwd 变化才 `clearExtensionCache` | 探针 P3m(`A==B same cwd -> shared: true`、`A==C diff cwd -> re-imported`) |
|
|
45
|
+
| F11 | 安装副本形态下 guard 路径 === discovered legion 条目(`path.resolve` 后相等),dedupe 合并成 1 条 | 探针 P2(`dedupeKeys equal? true`、`collectChildExtensions count: 1`) |
|
|
46
|
+
| F12 | 源码树自举形态下两者不等,出现**两个 legion factory**并报 tool 冲突 | 探针 P2h(`legion factories: 2`、`Tool "subagent" conflicts with /root/code/...`) |
|
|
47
|
+
| F13 | `extensionDedupeKey` 只做 `path.resolve`(builtin 除外) | `src/runs/args.ts:181-183` |
|
|
48
|
+
| F14 | `matchesExcludedExtension` 支持包名形式,`pi-legion` 匹配安装副本与源码树两种路径 | `extension-inheritance.ts:170-195` |
|
|
49
|
+
| F15 | `launch()` 774→1072 只有 2 个 await(`:1005`、`:1072`),均在同步准入块 `:938-955` 之后 | 探针 P4 + 源码 |
|
|
50
|
+
| F16 | 契约禁令:禁止让 `launch()` 的快路径多出任何 await | `docs/contracts/launch-admission-queue-v1.md:829` |
|
|
51
|
+
| F17 | `LaunchInput` 无 `signal`/`abort` 字段;`launch()` 内无 abort 读取 | `orchestrator.ts:180-196`;P4 |
|
|
52
|
+
| F18 | `RoutingSupervisor.spawnChild` 是唯一分派到两个后端的点,且**本身 async** | `src/supervisor/routing-supervisor.ts:128-160` |
|
|
53
|
+
| F19 | 唯一 spawn 漏斗是 `Orchestrator.launch` → `supervisor.spawnChild`(`:1072`) | `orchestrator.ts:774`、`:1072` |
|
|
54
|
+
| F20 | 存在 3 类不在 tool execute 帧内的 spawn 出口:postman 定时器、启动期团队恢复 microtask、launch 队列泵 | `legion-tools.ts:106-110`→`:184`→`index.ts:544`;`index.ts:642-649`→`:683`;`orchestrator.ts:1488`/`:1449-1452` |
|
|
55
|
+
| F21 | `pi list` 实测 320–920 ms;有 project 资源时内部会加载全部扩展(309 条 jiti 记录) | 探针 P4-timing |
|
|
56
|
+
| F22 | `discoverHostExtensions` 只 `execFile("pi",["list"])`,不 spawn 子 agent、不写 legion.db | `extension-inheritance.ts:128-152` |
|
|
57
|
+
| F23 | 失败也会被缓存成 `[]`,且 `if (discovered)` 对空数组为真 → 永不重试 | `extension-inheritance.ts:129`、`:143-151` |
|
|
58
|
+
| F24 | 会因 discovery 接上而变红的现有测试 | `test/unit/args.test.ts:43,40,128,185`;`test/unit/legion-doctor.test.ts:261`(详见 §6) |
|
|
59
|
+
| F25 | 脚本:`npm run typecheck` / `npm test` / `npm run test:integration` | `package.json:52-54` |
|
|
60
|
+
|
|
61
|
+
## 2. 决策(D 编号,reviewer 必读)
|
|
62
|
+
|
|
63
|
+
### D1 — 缓存迁到 `globalThis[Symbol.for]`
|
|
64
|
+
|
|
65
|
+
**判定**:`discovered` / `discovery` 从模块级 `let` 迁到
|
|
66
|
+
`globalThis[Symbol.for("pi-legion.extension-discovery")]` 上的一个 holder 对象。
|
|
67
|
+
|
|
68
|
+
**理由**:F6 实测证明模块级 `let` 在 pi 的真实 loader 配置(`moduleCache:false`)下跨 jiti
|
|
69
|
+
实例不可见、同实例重 import 也归零。若继续用模块级变量,子会话(cwd 不同时触发
|
|
70
|
+
`clearExtensionCache`,F10)内的 spawn 会读到空的 `discovered`,「父预热一次惠及全树」不成立。
|
|
71
|
+
F8 是本仓库已验证的同因先例。
|
|
72
|
+
|
|
73
|
+
**失败模式**:不做迁移 → 探针 P1-noCache 的形态(`B.discovered: undefined`)在生产复现,
|
|
74
|
+
修复只对「第一次 spawn」有效。
|
|
75
|
+
|
|
76
|
+
**验收**:A1、A2。
|
|
77
|
+
|
|
78
|
+
#### D1-a — holder 形状与复位语义
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const STORE_KEY = Symbol.for("pi-legion.extension-discovery");
|
|
82
|
+
|
|
83
|
+
interface DiscoveryStore {
|
|
84
|
+
/** Resolved list, or undefined until discovery completes. */
|
|
85
|
+
discovered?: string[];
|
|
86
|
+
/** In-flight promise, so concurrent launches share one `pi list`. */
|
|
87
|
+
inFlight?: Promise<string[]>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type StoreHolder = { [key: symbol]: DiscoveryStore | undefined };
|
|
91
|
+
const store = ((globalThis as unknown as StoreHolder)[STORE_KEY] ??= {});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`resetExtensionDiscoveryCache()` 必须 **清空同一个 holder 的字段**(`delete store.discovered;
|
|
95
|
+
delete store.inFlight;`),不得重新赋值 holder —— 否则父会话与子会话会指向两个不同对象
|
|
96
|
+
(F6 的直接后果),`reset` 在测试外的语义也会静默失效。
|
|
97
|
+
|
|
98
|
+
### D2 — 预热落点:`RoutingSupervisor.spawnChild`,不是 `Orchestrator.launch`
|
|
99
|
+
|
|
100
|
+
**判定**:在 `RoutingSupervisor.spawnChild` 顶部(`resolveSpawnMode` 之后、分派之前)
|
|
101
|
+
`await ensureHostExtensions()`。**不得**在 `Orchestrator.launch` 顶部预热。
|
|
102
|
+
|
|
103
|
+
**理由**:
|
|
104
|
+
|
|
105
|
+
- F16 是**已冻结契约的禁令**:`launch()` 快路径不得多出 await。F15 显示这段只有 2 个
|
|
106
|
+
await 且都在同步准入块之后,插到准入块之前会破坏 A21(同 tick 8 步 fanout 的槽位预留)。
|
|
107
|
+
- F18:`RoutingSupervisor.spawnChild` 本身 async、已在 await 链上、且是唯一同时看见两后端的点。
|
|
108
|
+
- F19 + F20:`Orchestrator.launch` 是唯一 spawn 漏斗,`RoutingSupervisor.spawnChild` 是它唯一
|
|
109
|
+
的下游分派点 —— 三类后台出口(postman 定时器 / 团队恢复 microtask / 队列泵)全部经过它,
|
|
110
|
+
因此预热覆盖全部 spawn 路径。
|
|
111
|
+
- F21:预热最坏 920 ms 且只在**首次**发生(之后 holder 命中)。放在分派点不影响准入判定。
|
|
112
|
+
|
|
113
|
+
**失败模式**:放到 `launch()` → 违反 F16,A21 测试变红;放到 `index.ts` 的 tool execute →
|
|
114
|
+
漏掉 F20 的三类后台出口。
|
|
115
|
+
|
|
116
|
+
**验收**:A3、A4。
|
|
117
|
+
|
|
118
|
+
#### D2-a — `ensureHostExtensions` 契约
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the host's extension set once, for the whole process.
|
|
123
|
+
*
|
|
124
|
+
* Never throws and never rejects: discovery failure must not fail a launch
|
|
125
|
+
* (F22/F23). Returns the resolved list, or `[]` when discovery failed.
|
|
126
|
+
*/
|
|
127
|
+
export async function ensureHostExtensions(env?: NodeJS.ProcessEnv): Promise<string[]>;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- 幂等:并发调用共享同一个 in-flight promise(与既有 `discovery ??=` 语义一致)。
|
|
131
|
+
- 已缓存则立即返回缓存(不跑 `pi list`)。
|
|
132
|
+
- **失败缓存策略变更**:`pi list` 失败时**不缓存空数组**,只清 in-flight,让下一次 spawn 重试。
|
|
133
|
+
理由 F23:既有实现把 `[]` 永久缓存,一次瞬时失败会静默关闭继承直到进程重启。
|
|
134
|
+
- `env` 透传给 `execFile`(既有行为),但**缓存键不区分 env** —— 保持"进程内一份主机扩展集"语义。
|
|
135
|
+
|
|
136
|
+
**验收**:A5。
|
|
137
|
+
|
|
138
|
+
### D3 — env 透传修复
|
|
139
|
+
|
|
140
|
+
**判定**:`collectChildExtensions` 把 `env` 传给 `inheritedExtensions`。
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
...inheritedExtensions(agent, discoveredHostExtensions() ?? [], env),
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**理由**:F4 实测 `PI_LEGION_CHILD_INHERIT_EXTENSIONS=0` 目前对继承集无效。接线后 doctor 里
|
|
147
|
+
按 env 构造的场景(`legion-doctor.test.ts` 多处)会连锁失败。
|
|
148
|
+
|
|
149
|
+
**失败模式**:不修 → opt-out 在生产无效,用户设 `=0` 却仍拿到全部宿主扩展(接线后)。
|
|
150
|
+
|
|
151
|
+
**验收**:A6。
|
|
152
|
+
|
|
153
|
+
### D4 — legion 自身从继承集排除
|
|
154
|
+
|
|
155
|
+
**判定**:`collectChildExtensions` 在合并前把 legion 自身从 inherited 集合里滤掉:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const inherited = inheritedExtensions(agent, discoveredHostExtensions() ?? [], env)
|
|
159
|
+
.filter((ext) => !matchesExcludedExtension(ext, SELF_EXTENSION_NAME));
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
其中 `SELF_EXTENSION_NAME` 是包名常量(`"pi-legion"`)。child-guard 仍在
|
|
163
|
+
函数末尾**无条件**追加,因此本判定不影响 guard 的必然存在。
|
|
164
|
+
|
|
165
|
+
**理由**:F11 显示安装副本形态本来就会 dedupe 成 1 条;F12 显示自举形态会出现两个 factory
|
|
166
|
+
并报 tool 冲突(`Tool "subagent" conflicts with ...`)。自排除让两种形态都收敛到 1 条。
|
|
167
|
+
|
|
168
|
+
**不做的事**:不依赖 `extensionDedupeKey` 做路径归一化。F13 显示它只做 `path.resolve`,
|
|
169
|
+
无法识别"同一份代码的两个副本"(F12 实测 inode 不同)。
|
|
170
|
+
|
|
171
|
+
**验收**:A7。
|
|
172
|
+
|
|
173
|
+
### D5 — 不新增 await 到 `launch()`,不碰 doctor 策略
|
|
174
|
+
|
|
175
|
+
见 non-goals 与 F16。`legion-doctor.ts` 的 `extensionsForRole` 占位串
|
|
176
|
+
`"(all host-discovered extensions)"` 接线后重新成立,**不改**。
|
|
177
|
+
|
|
178
|
+
**验收**:A8(`launch()` 的 await 数量与位置不变)。
|
|
179
|
+
|
|
180
|
+
## 3. 文件归属(单 writer,禁止越界)
|
|
181
|
+
|
|
182
|
+
| 文件 | 允许的改动 |
|
|
183
|
+
| --- | --- |
|
|
184
|
+
| `src/runs/extension-inheritance.ts` | D1(holder 迁移)、D2-a(新增 `ensureHostExtensions`);`discoverHostExtensions` 失败不缓存空数组 |
|
|
185
|
+
| `src/runs/args.ts` | D3(第 217 行补 env)、D4(自排除 + 常量) |
|
|
186
|
+
| `src/supervisor/routing-supervisor.ts` | D2(`spawnChild` 顶部 `await ensureHostExtensions()`) |
|
|
187
|
+
| `test/unit/extension-inheritance.test.ts` | A1、A2、A5 新增用例 |
|
|
188
|
+
| `test/unit/args.test.ts` | A6、A7 新增用例;修 F24 的 4 条 |
|
|
189
|
+
| `test/unit/legion-doctor.test.ts` | 修 F24 的 1 条(`:261`) |
|
|
190
|
+
|
|
191
|
+
**禁止改动**:`src/runs/orchestrator.ts`、`src/supervisor/inprocess-supervisor.ts`、
|
|
192
|
+
`src/supervisor/rpc-supervisor.ts`、`src/extension/legion-doctor.ts`(生产代码)、
|
|
193
|
+
`src/runs/role-tool-gates.ts`、`docs/contracts/launch-admission-queue-v1.md`。
|
|
194
|
+
|
|
195
|
+
## 4. 验收清单
|
|
196
|
+
|
|
197
|
+
| # | 断言 | 方式 |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| A1 | 迁移后 `discoveredHostExtensions()` 仍能读到 `ensureHostExtensions()` 的结果 | 单测:`await ensureHostExtensions()` 后 `discoveredHostExtensions()` 非 undefined |
|
|
200
|
+
| A2 | `resetExtensionDiscoveryCache()` 清空的是**同一个** holder(不是换新对象) | 单测:reset 前拿到 holder 引用,reset 后 `discoveredHostExtensions()` 为 undefined 且引用不变 |
|
|
201
|
+
| A3 | `RoutingSupervisor.spawnChild` 在分派前 await 预热 | 单测:stub `ensureHostExtensions`,断言调用发生在 backend.spawnChild 之前 |
|
|
202
|
+
| A4 | 预热幂等:连续 2 次 spawn 只跑一次 `pi list` | 单测:计数 stub,断言恰好 1 次 |
|
|
203
|
+
| A5 | 预热失败不抛、不缓存空结果(第二次会重试) | 单测:stub execFile 先失败后成功,断言第 2 次调用拿到结果 |
|
|
204
|
+
| A6 | `collectChildExtensions(agent, {PI_LEGION_CHILD_INHERIT_EXTENSIONS:"0"})` 不含 inherited 条目 | 单测:primed discovery 后断言 |
|
|
205
|
+
| A7 | inherited 集合不含 legion 自身,但 child-guard 仍在末尾 | 单测:断言 `!list.includes(selfPath) && list.includes(childGuardExtensionPath())` |
|
|
206
|
+
| A8 | `orchestrator.ts` 的 774→1072 区间 await 仍只有 `:1005`(内容可漂)与 `spawnChild` 那一处 | `grep -c await` 对照 + 契约禁令 5 复核 |
|
|
207
|
+
| A9 | 全量单测绿 | `npm run typecheck && npm test && npm run test:integration` |
|
|
208
|
+
| A10 | 真实端到端:primed 后 `collectChildExtensions` 含宿主扩展(pi-lens 等) | 内联 `node -e` 探针,输出与 F11 形状一致 |
|
|
209
|
+
|
|
210
|
+
## 5. 实施顺序
|
|
211
|
+
|
|
212
|
+
1. D1 holder 迁移(不改变行为,先让缓存可跨实例)
|
|
213
|
+
2. D2-a `ensureHostExtensions`(含失败不缓存)
|
|
214
|
+
3. D3 env 透传 → D4 自排除(两处都在 `args.ts`,一次改完)
|
|
215
|
+
4. D2 预热点接入
|
|
216
|
+
5. 修 F24 的 5 条测试 + 新增 A1–A7
|
|
217
|
+
|
|
218
|
+
第 1 步之后跑一次 `npm test` 确认零红,再继续 —— 否则后续失败无法归因。
|
|
219
|
+
|
|
220
|
+
## 6. 已知会变红的测试(F24,必须一并处理)
|
|
221
|
+
|
|
222
|
+
| 测试 | 位置 | 变红原因 |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| `buildPiArgs injects the child-guard extension` | `args.test.ts:30`,断言 `:43-45` | `--extension` 计数从 1 变 7;第一个值不再是 guard |
|
|
225
|
+
| 同上 `:39-41` | 取第一个 `--extension` 值 | 同上 |
|
|
226
|
+
| `#43 children get --no-extensions by default` | `args.test.ts:124`,`deepEqual` `:128` | 全量数组顺序敏感 |
|
|
227
|
+
| `#89 collectChildExtensions dedupes explicit builtin:codemode` | `args.test.ts:178`,`deepEqual` `:185-188` | 同上 |
|
|
228
|
+
| `#70 bundled worker does not promise lens tools` | `legion-doctor.test.ts:230`,断言 `:261` | discovered 含 `pi-lens/dist/index.js` |
|
|
229
|
+
|
|
230
|
+
**修复方式**:这些测试断言的是「discovery 为空时」的列表形状。改为**显式控制 discovery
|
|
231
|
+
状态**:在 `withEnv` 里额外 `resetExtensionDiscoveryCache()`,并对需要完整列表的用例断言
|
|
232
|
+
「inherited 条目存在 + guard 在末尾」而不是全量 `deepEqual`。
|
|
233
|
+
|
|
234
|
+
**禁止**:用 `excludeExtensions: ["*"]` 或伪造空 host 来让旧断言继续通过 —— 那是把 F24
|
|
235
|
+
的假设重新固化成契约。
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 7. 修订 R1(review 后追加,2026-10-26)
|
|
240
|
+
|
|
241
|
+
本节由 reviewer 审查 `fix/280-extension-inheritance-wiring` 后追加。**修订原因是实施中发现了
|
|
242
|
+
F1–F25 未能预见的两条真实缺陷**;F 事实表保持不变,A 清单在其后增量。
|
|
243
|
+
|
|
244
|
+
### R1-a — 目录形态的 `pi.extensions` 条目必须展开成可加载文件(critical)
|
|
245
|
+
|
|
246
|
+
**新事实**:
|
|
247
|
+
|
|
248
|
+
| # | 事实 | 证据 |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| F26 | `pi-diff-review` 的 `package.json` 声明 `pi.extensions: ["./extensions"]`,该目录内只有 `review.ts`、无 `package.json`/`index.ts` | `node -e require('.../pi-diff-review/package.json').pi` + `ls` |
|
|
251
|
+
| F27 | `resolveExtensionEntries` 对声明条目只做 `existsSync` 过滤,**不过滤目录** | `extension-inheritance.ts:130-134`(接线后实测 `非文件条目: [".../pi-diff-review/extensions"]`) |
|
|
252
|
+
| F28 | 真实 `pi -e <该目录> --print` → **exit 1**(`Failed to load extension: Cannot find module`);换成 `-e <该目录>/review.ts` → **exit 0** | 实测两次退出码 |
|
|
253
|
+
| F29 | in-process 后端不致命但静默丢弃:`DefaultResourceLoader({additionalExtensionPaths:[该目录]})` → `errors: 1, loaded: 0`,且 `inprocess-supervisor.ts` 只 warn `pendingProviderRegistrations`,读不到 `getExtensions().errors` | SDK 探针 |
|
|
254
|
+
|
|
255
|
+
**判定**:`resolveExtensionEntries` 必须把目录条目展开成其内可加载的 `*.ts|*.js`,
|
|
256
|
+
镜像 pi 自己的 loader 行为;展开为空则丢弃该条目。
|
|
257
|
+
|
|
258
|
+
**为什么这不违反 non-goal 5**:non-goal 5 指「不修 `pi.extensions` 写成目录而非文件」这个
|
|
259
|
+
**拼写选择**。本节不改变那个选择,只保证交给 `-e` 的东西可加载。接线前该缺陷不可达(F3),
|
|
260
|
+
接线后它变成「每个用 pi-diff-review 的 rpc 子会话启动失败」,属改动自身引入的回归。
|
|
261
|
+
|
|
262
|
+
**新增验收**:
|
|
263
|
+
|
|
264
|
+
| # | 断言 |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| A11 | `pi.extensions: ["./extensions"]` 且目录内只有 `review.ts`(无 index/package.json)→ 解析结果是 `.../extensions/review.ts`,不是目录 |
|
|
267
|
+
| A12 | `pi.extensions` 指向空目录 → 该条目被丢弃,不出现在 argv |
|
|
268
|
+
| A13 | `collectChildExtensions` 返回值中**不存在**非文件路径(用 `statSync().isFile()` 全量断言) |
|
|
269
|
+
|
|
270
|
+
### R1-b — 预热点必须覆盖直连 `RpcSupervisor` 的公开路径(major)
|
|
271
|
+
|
|
272
|
+
**新事实**:
|
|
273
|
+
|
|
274
|
+
| # | 事实 | 证据 |
|
|
275
|
+
| --- | --- | --- |
|
|
276
|
+
| F30 | `api.ts:13` 的文档示例是 `new Orchestrator({ supervisor: new RpcSupervisor(), ... })`,绕过 `RoutingSupervisor` | 读 `src/api.ts:8-20` |
|
|
277
|
+
| F31 | `Orchestrator` 构造函数的 supervisor 默认值也是裸 `RpcSupervisor` | `orchestrator.ts:453` |
|
|
278
|
+
| F32 | `index.ts` 的 5 处 `new Orchestrator` **全部**传 `input.supervisor`(来自 `RoutingSupervisor`),故自举形态覆盖正常 | `grep -A6 "new Orchestrator" index.ts`(736/1832/1930/2422/3116) |
|
|
279
|
+
|
|
280
|
+
**判定**:预热点从 `RoutingSupervisor.spawnChild` **下沉**到两个后端各自的
|
|
281
|
+
`spawnChild` 顶部(`InProcessSupervisor.spawnChild`、`RpcSupervisor.spawnChild`)。
|
|
282
|
+
理由:这是无论调用方走 router 还是直连后端都必须经过的点,且两者本身都是 async。
|
|
283
|
+
`RoutingSupervisor.spawnChild` 的预热**保留**——它能在参数校验失败前就暖好缓存,
|
|
284
|
+
且 router 路径下第一次调用会命中已暖的 holder(幂等,无额外 `pi list`)。
|
|
285
|
+
|
|
286
|
+
**为什么不放 `Orchestrator.launch`**:F16 禁令不变,`launch()` 快路径不得新增 await。
|
|
287
|
+
|
|
288
|
+
**新增验收**:
|
|
289
|
+
|
|
290
|
+
| # | 断言 |
|
|
291
|
+
| --- | --- |
|
|
292
|
+
| A14 | 直接 `new RpcSupervisor()` + `new Orchestrator({supervisor})` 后 `launch()`,`discoveredHostExtensions()` 非 undefined |
|
|
293
|
+
| A15 | 同上,in-process 后端 |
|
|
294
|
+
| A16 | 幂等不变:router 路径下连续 2 次 spawn 仍只 1 次 `pi list` |
|
|
295
|
+
|
|
296
|
+
### R1-c — 驳回的 finding(记录以免重复争议)
|
|
297
|
+
|
|
298
|
+
reviewer 报的 minor「自排除会误吃 `acme/src/legion/index.ts`」**不成立**:
|
|
299
|
+
自排除常量是 `"pi-legion"`,`matchesExcludedExtension` 的 `dir === target` /
|
|
300
|
+
`file === target.ts` 兜底只在目录/文件名恰为 `pi-legion` 时触发。实测 5 条路径,
|
|
301
|
+
仅真正的 `node_modules/pi-legion/index.ts` 被丢:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
kept /host/node_modules/acme/src/legion/index.ts
|
|
305
|
+
kept /host/node_modules/acme/extensions/legion.ts
|
|
306
|
+
kept /host/node_modules/acme-legion/index.ts
|
|
307
|
+
kept /host/node_modules/acme/legion-ext/index.ts
|
|
308
|
+
DROPPED /host/node_modules/pi-legion/index.ts
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
reviewer 用错了包名(拿 `legion` 当 target)。**不修。**
|
|
312
|
+
|
|
313
|
+
### R1-d — 修订后的文件归属
|
|
314
|
+
|
|
315
|
+
在 §3 基础上**新增允许改动**:
|
|
316
|
+
|
|
317
|
+
| 文件 | 允许的改动 |
|
|
318
|
+
| --- | --- |
|
|
319
|
+
| `src/runs/extension-inheritance.ts` | R1-a 的目录展开(`resolveExtensionEntries`) |
|
|
320
|
+
| `src/supervisor/inprocess-supervisor.ts` | **仅** R1-b:`spawnChild` 顶部 `await ensureHostExtensions()` |
|
|
321
|
+
| `src/supervisor/rpc-supervisor.ts` | **仅** R1-b:`spawnChild` 顶部 `await ensureHostExtensions()` |
|
|
322
|
+
| `test/unit/extension-inheritance.test.ts` | A11–A13 |
|
|
323
|
+
| `test/unit/routing-supervisor*.test.ts` 或 `test/unit/orchestrator*` 就近文件 | A14–A16 |
|
|
324
|
+
|
|
325
|
+
§3 的其余禁区不变(`orchestrator.ts` 仍禁止改,含 `launch()`)。
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 8. 修订 R2(第二次 review 后追加,2026-10-26)
|
|
330
|
+
|
|
331
|
+
由 `rev-r1` 复审 `8cf2aba` 后追加。R1 的两条修复方向正确,但**镜像 pi loader 的目标未达到**,
|
|
332
|
+
且三处预热点在失败路径上互相叠加。以下为增量事实与决策。
|
|
333
|
+
|
|
334
|
+
### R2-a — 目录展开必须镜像 `collectAutoExtensionEntries`(major)
|
|
335
|
+
|
|
336
|
+
| # | 事实 | 证据 |
|
|
337
|
+
| --- | --- | --- |
|
|
338
|
+
| F33 | pi 处理 manifest 声明的目录走 `collectAutoExtensionEntries`,对每个**子目录**再调 `resolveExtensionEntries`,因此 `extensions/<name>/index.ts` 是官方文档化布局 | `package-manager.js:407-462`;`loader.js:623-631` 注释「规则 2: Subdirectory with index」 |
|
|
339
|
+
| F34 | 本实现在同一 fixture 上返回 `[]`:`extensions/alpha/index.ts` + `extensions/beta/index.ts` 全部静默丢失 | 实测 `resolveExtensionEntries("/tmp/extprobe/pkg")` → `[]` |
|
|
340
|
+
| F35 | `discoverExtensionFiles` 的注释自称「Deliberately one level」,理由是「嵌套包不需要,递归会把错拼路径变成无界遍历」 | `extension-inheritance.ts:160-168` |
|
|
341
|
+
| F36 | 同一实现把 `extensions/.eslintrc.js` **收进**结果;pi 的 `collectAutoExtensionEntries` 显式跳过 `.` 开头与 `node_modules` | `extension-inheritance.ts:176`;`package-manager.js:407-462` |
|
|
342
|
+
| F37 | 但 `pi -e <点文件>` 实测 **exit 0**(不是 exit 1),故 F36 的后果是「多传一条无用条目」而非启动失败 | 实测 `.eslintrc.js` → EXIT=0;`alpha/index.ts` → EXIT=0 |
|
|
343
|
+
|
|
344
|
+
**判定(R2-a)**:`discoverExtensionFiles` 改为与 pi 同形:
|
|
345
|
+
|
|
346
|
+
- 跳过 `.` 开头的条目与 `node_modules`
|
|
347
|
+
- 文件且 `*.ts|*.js` → 收
|
|
348
|
+
- **子目录** → 调 `resolveExtensionEntries(child, false)`,收到结果就收,收不到就跳过
|
|
349
|
+
|
|
350
|
+
F35 的「无界遍历」担忧用**深度上限**解决:`resolveExtensionEntries(dir, allowNestedManifest)` 的
|
|
351
|
+
第二个参数(已存在)作为递归闸门,嵌套层只允许一层子目录解析,子目录内部不再继续递归。
|
|
352
|
+
这与 pi 的行为一致(pi 也只递归一层:`collectAutoExtensionEntries` → `resolveExtensionEntries`
|
|
353
|
+
→ 不再往下)。
|
|
354
|
+
|
|
355
|
+
**新增验收**:
|
|
356
|
+
|
|
357
|
+
| # | 断言 |
|
|
358
|
+
| --- | --- |
|
|
359
|
+
| A17 | `extensions/alpha/index.ts` + `extensions/beta/index.ts`(无 manifest/index 于 extensions 根)→ 两条结果都出现 |
|
|
360
|
+
| A18 | `extensions/.eslintrc.js` 不被收进结果 |
|
|
361
|
+
| A19 | 子目录内还是目录(`extensions/x/y/index.ts`)→ 不递归收,结果为空(或只收 `x` 层能解析到的) |
|
|
362
|
+
|
|
363
|
+
### R2-b — 预热点在失败路径上必须幂等(major)
|
|
364
|
+
|
|
365
|
+
| # | 事实 | 证据 |
|
|
366
|
+
| --- | --- | --- |
|
|
367
|
+
| F38 | `ensureHostExtensions` 失败时不缓存(F23 的刻意设计,保留),故每次调用都会重跑 `pi list` | `extension-inheritance.ts:292-313` |
|
|
368
|
+
| F39 | 三处预热点(router + rpc 后端 + inprocess 后端)叠加后,router 路径一次 spawn 会 warm 两次:router 一次(失败)+ 后端一次(再失败) | 实测三次连续 `ensureHostExtensions` → `[[],[],[]]` |
|
|
369
|
+
| F40 | 失败时每个 spawn 恒付 2× 探测成本(真实 0.3–1.8s;`pi list` 超时时最坏 +30s),且发生在内存门之前 | `rev-r1` 实测(fake `pi` 500ms → spawn#1 1035ms / spawn#2 1026ms) |
|
|
370
|
+
| F41 | A16 的字面断言「router 路径连续 2 次 spawn 仍只 1 次 `pi list`」只在成功路径成立 | 同上 |
|
|
371
|
+
|
|
372
|
+
**判定(R2-b)**:引入**同 spawn 短路标记**,不动「失败不缓存跨 spawn 可重试」的语义:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
/**
|
|
376
|
+
* Mark the current spawn attempt as already-warmed, so the backend's warm call
|
|
377
|
+
* does not repeat a FAILED `pi list` that the router just paid for.
|
|
378
|
+
*
|
|
379
|
+
* Scope: one spawn attempt. Reset when the outermost warm returns.
|
|
380
|
+
*/
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
实现方式(选一,实施者择简):
|
|
384
|
+
|
|
385
|
+
- **(a) 传入标记**:`ensureHostExtensions(env, { spawnScoped?: boolean })`;`RoutingSupervisor`
|
|
386
|
+
调用后设置一个同步可见的 `recentlyWarmed` 时间戳/布尔,两个后端在**同一 tick 内**看到就短路。
|
|
387
|
+
- **(b) 失败短 TTL**:失败时把 `failedAt = Date.now()` 记进 holder,`ensureHostExtensions` 在
|
|
388
|
+
`FAILURE_TTL_MS`(如 5s)内直接返回 `[]` 而不重跑。跨 spawn 的最坏情况变成「首次失败后 5s 内的
|
|
389
|
+
spawn 都不重试」,比现状好,且仍满足 F23「不是永久关闭」。
|
|
390
|
+
|
|
391
|
+
**倾向 (b)**:实现最小(holder 加一个字段),无需在 router 与后端之间传状态,也不改变
|
|
392
|
+
`ensureHostExtensions` 的签名(A14/A15 的测试不用重写)。TTL 值必须是命名常量并注释理由。
|
|
393
|
+
|
|
394
|
+
**新增验收**:
|
|
395
|
+
|
|
396
|
+
| # | 断言 |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| A20 | `pi list` 失败下,router 路径一次 spawn 只跑 **1** 次 `pi list`(不是 2) |
|
|
399
|
+
| A21 | 失败后超过 `FAILURE_TTL_MS`,下一次 spawn 会**重试**(可恢复性未被永久关闭) |
|
|
400
|
+
| A22 | 成功路径行为不变:连续 2 次 spawn 仍只 1 次 `pi list`(既有 A16 保持绿) |
|
|
401
|
+
|
|
402
|
+
### R2-c — 测试注释必须如实(minor)
|
|
403
|
+
|
|
404
|
+
`test/unit/extension-inheritance.test.ts:601-604` 的注释声称断言集合是「fake host **PLUS the
|
|
405
|
+
live `pi list`**」,但 `withPathEnv` 执行的是 `process.env.PATH = pathValue`(**完全替换**,
|
|
406
|
+
`test/unit/extension-inheritance.test.ts:235`),真实 `pi list` 从未运行。
|
|
407
|
+
|
|
408
|
+
**判定**:删掉「PLUS the live `pi list`」这半句。**不得**为此新增不做 PATH 遮蔽的用例 ——
|
|
409
|
+
那会让测试依赖本机安装的包,不可移植。
|
|
410
|
+
|
|
411
|
+
### R2-d — 修订后的文件归属增量
|
|
412
|
+
|
|
413
|
+
在 §7 R1-d 基础上,**新增允许改动**:
|
|
414
|
+
|
|
415
|
+
| 文件 | 允许的改动 |
|
|
416
|
+
| --- | --- |
|
|
417
|
+
| `src/runs/extension-inheritance.ts` | R2-a 的 sweep 重写、R2-b 的失败 TTL |
|
|
418
|
+
| `test/unit/extension-inheritance.test.ts` | A17–A21、R2-c 的注释删除 |
|
|
419
|
+
| `test/unit/routing-supervisor*.test.ts` 或就近文件 | A20 |
|
|
420
|
+
|
|
421
|
+
`src/runs/orchestrator.ts`、`src/extension/legion-doctor.ts`、`src/runs/role-tool-gates.ts`、
|
|
422
|
+
`docs/contracts/launch-admission-queue-v1.md` 仍为禁区。
|
|
423
|
+
|
|
424
|
+
### R2-e — 降级与驳回记录
|
|
425
|
+
|
|
426
|
+
- **降级**:`rev-r1` 的 minor「点文件导致 `pi -e` exit 1」论据不成立(实测 EXIT=0)。
|
|
427
|
+
但「点文件不该被收」仍成立(pi 自己会跳过,多传一条无用条目)→ 并入 R2-a 的 A18。
|
|
428
|
+
- **保持驳回**:§7 R1-c 的「自排除误吃 `acme/src/legion/index.ts`」仍不成立,勿再报。
|