subagent-delegation 0.3.0 → 0.5.0

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.
Files changed (24) hide show
  1. package/README.md +55 -45
  2. package/VERSION +1 -1
  3. package/adapters/codex/modes.json +3 -3
  4. package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/SKILL.md +4 -4
  5. package/adapters/codex/skills/subagent-delegation/agents/openai.yaml +6 -0
  6. package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/assets/AGENTS.fragment.md +1 -1
  7. package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/assets/codex/agents/task-delegate.toml +2 -5
  8. package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/assets/codex/agents/task-execute.toml +2 -5
  9. package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/assets/codex/agents/task-review.toml +2 -5
  10. package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/assets/codex/agents/task-scan.toml +2 -5
  11. package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/references/configuration.md +3 -2
  12. package/bin/subagent-delegation.mjs +5 -6
  13. package/docs/README.md +4 -4
  14. package/docs/implementation-review.md +22 -72
  15. package/docs/usage.md +24 -24
  16. package/lib/codex.mjs +9 -18
  17. package/lib/install.mjs +33 -53
  18. package/package.json +3 -3
  19. package/web/app.js +20 -36
  20. package/web/index.html +1 -1
  21. package/adapters/codex/skills/subagent-model-selection/agents/openai.yaml +0 -6
  22. package/install.py +0 -33
  23. /package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/assets/codex/config.toml +0 -0
  24. /package/adapters/codex/skills/{subagent-model-selection → subagent-delegation}/references/failure-policy.md +0 -0
package/docs/usage.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # 使用说明
2
2
 
3
- `subagent-delegation` 0.3.0。首版支持 Codex;Node.js 22+。命令、网页和生成的 Skill 使用同一套档位数据。
3
+ `subagent-delegation` 0.5.0。首版支持 Codex;Node.js 22+。命令、网页和生成的 Skill 使用同一套档位数据。
4
4
 
5
- 短命令为 `sub-d`,与原命令 `subagent-delegation` 完全等价。升级到 0.3.0 后执行 `sub-d setup --dry-run` 检查,再执行 `sub-d setup` 更新部署;保留当前档位、自定义定义和首次恢复基线。指定独立 home 时两次命令都需带原路径,完成后重启 Codex。npm 包名仍为 `subagent-delegation`。
5
+ 终端命令为 `sub-d`,npm 包名为 `subagent-delegation`。内测版仅支持当前配置格式,不提供旧版本兼容、升级或迁移。更换版本前先用原版本 `restore`,再安装新版本并 `setup`;独立 home 全程使用原路径。
6
6
 
7
7
  ## 安装与配置
8
8
 
@@ -22,7 +22,9 @@ npm install -g subagent-delegation && sub-d setup --mode mid
22
22
 
23
23
  目标目录优先级为 `--codex-home PATH`、`CODEX_HOME`、用户 `~/.codex`。空格路径需要 shell 引号。命令不会设置主代理模型、全局子代理默认、供应商、审批、沙箱、并发或子代理启用开关。
24
24
 
25
- 首次 setup 默认适中;已安装时保留当前选择。setup 也用于安全升级。npm 安装没有 postinstall 配置动作。`--dry-run` 不创建目标 home、锁或状态文件。
25
+ Skill 名称为 `subagent-delegation`,调用为 `$subagent-delegation`,安装目录为 `skills/subagent-delegation`。npm 安装命令获取已发布版本;当前内测开发代码通过 `npm pack` 构建本地包。
26
+
27
+ 首次 setup 默认适中;同版本重复 setup 保留当前选择与首次恢复基线。npm 安装没有 postinstall 配置动作。`--dry-run` 不创建目标 home、锁或状态文件。
26
28
 
27
29
  所有配置部署变化都提示:**配置已写入,请重启 Codex 后使用新委派规则。** 运行中的任务继续使用原上下文;工具不自动重启、停止任务或保证动态加载。
28
30
 
@@ -32,7 +34,7 @@ npm install -g subagent-delegation && sub-d setup --mode mid
32
34
 
33
35
  | 命令 | 用途 |
34
36
  | --- | --- |
35
- | `setup [--mode MODE]` | 首次配置或升级,保留无关文件 |
37
+ | `setup [--mode MODE]` | 首次配置或同版本重新配置,保留无关文件 |
36
38
  | `mode set MODE` | 切换已安装 home 的档位 |
37
39
  | `mode list` | 列出内置、自定义及当前选择 |
38
40
  | `mode show MODE` | 查看四级主/备用、来源与定义版本,不激活 |
@@ -46,7 +48,7 @@ npm install -g subagent-delegation && sub-d setup --mode mid
46
48
  | `serve [--port PORT] [--open]` | 前台启动本地设置页面 |
47
49
  | `--help` / `--version` | 帮助及工具版本,可带 --json,不接受 home 参数 |
48
50
 
49
- `MODE` 使用 `low`(经济)、`mid`(适中,首次配置默认)、`fire`(性能);内置档位仅接受这三个名称,中文用于说明含义。旧安装记录中的历史内部 id 仅供读取与恢复,不作为命令参数。自定义档位可使用内部 id 或唯一名称。职责为 `task_scan`、`task_execute`、`task_review`、`task_delegate`,explain 省略职责时默认 task_delegate。默认 explain 使用当前有效档位;未安装时需明确 `--mode mid` 等内置预览。
51
+ `MODE` 使用 `low`(经济)、`mid`(适中,首次配置默认)、`fire`(性能);命令、JSON id、状态记录和网页使用同一值,中文用于说明含义。自定义档位可使用内部 id 或唯一名称。职责为 `task_scan`、`task_execute`、`task_review`、`task_delegate`,explain 省略职责时默认 task_delegate。默认 explain 使用当前有效档位;未安装时需明确 `--mode mid` 等内置预览。
50
52
 
51
53
  名称以 `-` 开头时,在全部选项后使用 `--`:
52
54
 
@@ -68,13 +70,13 @@ sub-d explain --level L4 --role task_review
68
70
 
69
71
  详细查询分开呈现已配置值、预设值、潜在覆盖与运行未验证。检查相关项目及祖先目录,不扫描全电脑;不输出账号凭据、私有指令、完整 TOML 或认证记录。范围外注册路径与未确定的 profile 只报告候选及未检查原因,不跟随任意文件。
70
72
 
71
- 同一档位重复切换不重写;仅保存非活动定义不要求重启。配置版本与 CLI 版本不同,先 setup 升级,不能直接跨版本切换。
73
+ 同一档位重复切换不重写;仅保存非活动定义不要求重启。配置版本或格式不匹配时,读取与写入都会被拒绝,不做自动转换。
72
74
 
73
75
  无需全局安装的 npm 查询,以及本地包查询:
74
76
 
75
77
  ```sh
76
- npm exec --package=subagent-delegation@0.3.0 -- sub-d status --details
77
- npm exec --package="/path/to/subagent-delegation-0.3.0.tgz" -- sub-d status --details
78
+ npm exec --package=subagent-delegation@0.5.0 -- sub-d status --details
79
+ npm exec --package="/path/to/subagent-delegation-0.5.0.tgz" -- sub-d status --details
78
80
  ```
79
81
 
80
82
  ## 任务等级与委派职责
@@ -97,7 +99,9 @@ npm exec --package="/path/to/subagent-delegation-0.3.0.tgz" -- sub-d status --de
97
99
 
98
100
  等级与职责互相独立。例如经济模式下复杂产出升为 L3 时,不能继续用固定 Luna 的 task_execute 覆盖选择;应使用 task_delegate 请求 Sol/medium,同时保留范围内产出的权限契约。继承需核对宿主全局默认,省略参数不证明继承。
99
101
 
100
- AGENTS 管理入口要求启动时读取 Skill,每次创建、重新委派或模型变化前按 Skill 检查。用户选择、预算、宿主和项目规则优先,工具不自动授权委派。主代理保留最终设计、共享裁决、Git 写操作、集成、关键验证及完成声明。子代理禁止递归、写 Git、外部环境和代理配置改动、图像生成;独占写入、局部适配/阻断、最小充分验证及 PARTIAL 返回在全部档位保留。
102
+ AGENTS 管理入口要求启动时读取 Skill,每次创建、重新委派或模型变化前按 Skill 检查。用户选择、预算、宿主和项目规则优先,工具不自动授权委派。主代理保留最终设计、共享裁决、集成、关键验证及完成声明。子代理禁止递归、外部环境和代理配置改动;独占写入、局部适配/阻断、最小充分验证及 PARTIAL 返回在全部档位保留。
103
+
104
+ 本工具不额外限制 Git 操作或图像生成,也不设置 `features.image_generation`。实际工具能力与操作授权遵循宿主、用户和项目规则。
101
105
 
102
106
  ## 备用与模型不可用
103
107
 
@@ -111,7 +115,7 @@ AGENTS 管理入口要求启动时读取 Skill,每次创建、重新委派或
111
115
 
112
116
  Astra 不可用:先区分认证、权限、额度、临时故障或能力不支持。允许备用且全部条件满足才用已配置 Sol/high;用户强制只用 Astra、预算不允许或旧 worker 停止未知时,受影响部分 BLOCKED,继续其他独立工作。缺资料或工具故障先补前提,不能靠强模型掩盖。
113
117
 
114
- 每任务最多三次父代理发起的委派,同一组合最多额外一次;累计等待不超过 30 秒并遵守 Retry-After。宿主已重试或次数未知时不追加。异常或部分结果标 PARTIAL,不自动撤销其他人的修改。完整分类见 [失败处理](../adapters/codex/skills/subagent-model-selection/references/failure-policy.md)。
118
+ 每任务最多三次父代理发起的委派,同一组合最多额外一次;累计等待不超过 30 秒并遵守 Retry-After。宿主已重试或次数未知时不追加。异常或部分结果标 PARTIAL,不自动撤销其他人的修改。完整分类见 [失败处理](../adapters/codex/skills/subagent-delegation/references/failure-policy.md)。
115
119
 
116
120
  ## 自定义档位
117
121
 
@@ -123,7 +127,7 @@ sub-d mode configure "我的研究方案" \
123
127
  sub-d mode set "我的研究方案"
124
128
  ```
125
129
 
126
- 新建自定义档位不能命名为 `low`、`mid`、`fire`。若旧配置已有同名自定义档位,使用 `mode list` 显示的 custom-* id 选择;low、mid、fire 始终指向内置档位,原自定义配置保留。
130
+ 自定义名称不能与内置档位的 id、中文名或现有自定义名称/id 重复;自定义档位可按名称或 custom-* id 选择。
127
131
 
128
132
  名称 trim + Unicode NFC 归一化,1–80 个码点,不能含控制字符、无效 Unicode 或与既有名称/id 混淆。名称只用于显示与查找,不拼文件路径;内部 id 为 UUID。模型 1–200、effort 1–40 个码点,拒绝无效 Unicode、控制字符和首尾空白。目录未知不等于标识无效。
129
133
 
@@ -162,7 +166,7 @@ setup、切换、活动自定义部署实际改变后,解锁再查询所选 ho
162
166
 
163
167
  分别查看:目录是否完整、effort 是否支持、账号实际请求权限、可信实际身份。目录列出模型不证明请求成功;目录缺项仍 unknown。桌面会话、启动参数、profile 与 CLI 上下文匹配未核验,检查结果不能冒充桌面账号能力。
164
168
 
165
- **0.3.0 的实际推理门禁继续关闭。** 已核验的 Codex CLI 0.156.1 无法证明禁用全部工具/委派及限定外部读取,因此 `--verify` 返回 exit 1、`PROBE_ISOLATION_UNSUPPORTED`,不创建线程或发送推理。没有有效当前档位且无显式目标时先报 `PROBE_TARGET_REQUIRED`。未来只有宿主强制隔离得到验证才能启用;页面仍保留显式额度确认,不默认调用。
169
+ **0.5.0 的实际推理门禁继续关闭。** 已核验的 Codex CLI 0.156.1 无法证明禁用全部工具/委派及限定外部读取,因此 `--verify` 返回 exit 1、`PROBE_ISOLATION_UNSUPPORTED`,不创建线程或发送推理。没有有效当前档位且无显式目标时先报 `PROBE_TARGET_REQUIRED`。未来只有宿主强制隔离得到验证才能启用;页面仍保留显式额度确认,不默认调用。
166
170
 
167
171
  ## 本地网页
168
172
 
@@ -205,13 +209,14 @@ JSON stdout 只有统一 envelope:
205
209
  - `RULES_SHADOWED`:非空 home AGENTS.override.md 遮蔽入口。工具不改 override;用户自行合并原规则并停用 override,或选择未遮蔽 home。
206
210
  - `MANAGED_FILE_DRIFT` / `MANAGED_RULES_DRIFT`:人工改动、删除或 chmod;保留用户内容,核对后重新操作,无 force 覆盖开关。
207
211
  - `BUSY`:有写入锁或读取持续变化。先确认其他命令已停止;不要自动删除锁。进程崩溃后可能需人工核对锁与残留。
208
- - `VERSION_MISMATCH`:先用当前工具 setup 升级;禁止旧工具降级覆盖。
209
- - `RESTORE_METADATA_INVALID` / `RESTORE_BASELINE_UNAVAILABLE`:恢复记录损坏或旧版没有原始基线,使用原备份或人工核对,不猜测恢复。
212
+ - `VERSION_MISMATCH`:配置版本与当前工具不一致;用原版本 restore 后再重新配置,当前工具不迁移或覆盖。
213
+ - `STATE_INVALID`:状态格式、路径或档位引用不受支持,保持文件不变。
214
+ - `RESTORE_METADATA_INVALID`:恢复记录缺失或损坏,使用原备份或人工核对,不猜测恢复。
210
215
  - `MODEL_ACCESS_UNKNOWN` / `MODEL_NOT_LISTED`:权限未知;不能当作可用或不可用的最终证明。
211
216
 
212
217
  配置文件采用逐文件原子替换、共同独占锁、完整观察复读、状态最后写入和普通失败回退。回退不覆盖并发编辑,残留单独报告。多文件在进程崩溃/断电时并非一个原子事务;不要在应用期间手工修改管理文件。
213
218
 
214
- ## 恢复、迁移与卸载
219
+ ## 恢复与卸载
215
220
 
216
221
  ```sh
217
222
  sub-d restore --dry-run
@@ -221,16 +226,11 @@ npm uninstall -g subagent-delegation
221
226
 
222
227
  restore 后重启 Codex。它不会卸载 npm 工具、删除整个 home、清理账号或重启应用。npm 卸载也不会代做 restore。
223
228
 
224
- 首次新版 setup 记录受影响文件的原存在状态与普通权限,仅允许接管同内容公开工具资产;AGENTS 只记录管理区块与校验元数据,不保存完整私有正文、config 或认证文件。切换/升级保留首次基线,新路径追加原值,退场路径保留原始恢复记录。
229
+ 首次 setup 记录受影响文件的原存在状态与普通权限,仅允许接管同内容公开工具资产;AGENTS 只记录管理区块与校验元数据,不保存完整私有正文、config 或认证文件。同版本档位切换与自定义配置保留首次基线。
225
230
 
226
231
  恢复清理本工具新增 Skill、四角色、规则区块、当前档位、全部自定义与元数据;保留用户原文件和后续标记外正文。只非递归删除本工具创建的空目录,home 与无关文件保留。冲突先完整预检,失败不猜测删除。
227
232
 
228
- 0.1.0 升级为 schema 2 时标 `legacy_unavailable`:摘要不能重建安装前内容,不把迁移当天配置冒充原始基线。旧 Python 命令现在是 Node setup 兼容入口,需要 Node 22+ 和已安装依赖:
229
-
230
- ```sh
231
- npm ci --ignore-scripts
232
- python3 install.py --codex-home "/path/to/codex-home" --dry-run
233
- ```
233
+ 工具统一使用 Node.js 22+ 和 npm,不需要 Python;安装与配置入口为 `sub-d setup`。
234
234
 
235
235
  ## 开发验证
236
236
 
@@ -240,7 +240,7 @@ npm test
240
240
  npm pack
241
241
  ```
242
242
 
243
- 原十项 Python 保护测试保存在测试 fixture 上,同时 CLI 用例检查新的兼容入口。可在具备 Python 3.11+ 时运行 `python3 -m unittest discover -s tests -v`。
243
+ `npm test` 验证安装、档位、查询、恢复和本地服务;安装保护用例覆盖用户配置保留、冲突、符号链接、失败回退及输出脱敏。
244
244
 
245
245
  浏览器集成脚本不安装额外依赖;使用开发环境已存在的 Playwright 和 Chrome:
246
246
 
@@ -248,4 +248,4 @@ npm pack
248
248
  PLAYWRIGHT_MODULE=/path/to/playwright CHROME_PATH=/path/to/chrome npm run test:browser
249
249
  ```
250
250
 
251
- 跨平台 API 设计不代替实机验收;Linux/Windows、其他 Codex 客户端及真实账号推理仍需单独验证。兼容依据和五轮审查见 [实施验收记录](implementation-review.md)。
251
+ 跨平台 API 设计不代替实机验收;Linux/Windows、其他 Codex 客户端及真实账号推理仍需单独验证。当前验证结果与边界见 [实施验收记录](implementation-review.md)。
package/lib/codex.mjs CHANGED
@@ -12,7 +12,6 @@ export const END = '<!-- subagent-delegation:end -->';
12
12
  const policy = JSON.parse(await readFile(new URL('../adapters/codex/modes.json', import.meta.url), 'utf8'));
13
13
  export const FALLBACK_CONDITIONS = policy.fallback_conditions;
14
14
  export const BUILTINS = policy.modes.map(mode => ({ ...mode, kind: 'builtin', fallback_conditions: FALLBACK_CONDITIONS }));
15
- export const MODE_COMMANDS = { low: 'economy', mid: 'balanced', fire: 'performance' };
16
15
 
17
16
  export function argumentError(message, code = 'INVALID_ARGUMENT') {
18
17
  return Object.assign(new Error(message), { code, exitCode: 2 });
@@ -46,22 +45,14 @@ export function validateDefinition(input) {
46
45
  }));
47
46
  }
48
47
 
49
- function checkedDefinition(matches) {
48
+ export function resolveDefinition(selector, customModes = []) {
49
+ const name = typeof selector === 'string' ? selector.trim().normalize('NFC') : selector;
50
+ const matches = [...BUILTINS.filter(mode => mode.id === selector), ...customModes.filter(mode => mode.id === selector || mode.name === name)];
50
51
  if (matches.length !== 1) throw argumentError('档位不存在或名称不唯一;内置档位请用 low、mid、fire,自定义档位请用 mode list 显示的 id 或名称。', 'MODE_NOT_FOUND');
51
52
  const mode = matches[0];
52
53
  return { ...mode, levels: validateDefinition({ levels: mode.levels }), fallback_conditions: FALLBACK_CONDITIONS };
53
54
  }
54
55
 
55
- export function resolveDefinition(selector, customModes = []) {
56
- if (Object.hasOwn(MODE_COMMANDS, selector)) return checkedDefinition(BUILTINS.filter(mode => mode.id === MODE_COMMANDS[selector]));
57
- const name = typeof selector === 'string' ? selector.trim().normalize('NFC') : selector;
58
- return checkedDefinition(customModes.filter(mode => mode.id === selector || mode.name === name));
59
- }
60
-
61
- export function resolveStoredDefinition(id, customModes = []) {
62
- return checkedDefinition([...BUILTINS, ...customModes].filter(mode => mode.id === id));
63
- }
64
-
65
56
  export function explainSelection({ definition, level, role = 'task_delegate' }) {
66
57
  if (!LEVELS.includes(level) || !Object.hasOwn(ROLES, role)) throw argumentError('任务等级或角色无效。');
67
58
  const { model, effort, fallback } = definition.levels[level];
@@ -87,19 +78,19 @@ export function safeJsonData(value) {
87
78
  }
88
79
 
89
80
  export async function renderPayload(home, mode) {
90
- const skillRoot = path.join(PACKAGE_ROOT, 'adapters/codex/skills/subagent-model-selection');
81
+ const skillRoot = path.join(PACKAGE_ROOT, 'adapters/codex/skills/subagent-delegation');
91
82
  const files = new Map();
92
83
  async function visit(directory, prefix = '') {
93
84
  for (const entry of await readdir(directory, { withFileTypes: true })) {
94
85
  if (entry.isSymbolicLink()) throw Object.assign(new Error('发布资产不允许符号链接。'), { code: 'INVALID_PACKAGE' });
95
86
  const relative = prefix + entry.name;
96
87
  if (entry.isDirectory()) await visit(path.join(directory, entry.name), relative + '/');
97
- else if (entry.isFile() && entry.name !== '.DS_Store') files.set(`skills/subagent-model-selection/${relative}`, await readFile(path.join(directory, entry.name)));
88
+ else if (entry.isFile() && entry.name !== '.DS_Store') files.set(`skills/subagent-delegation/${relative}`, await readFile(path.join(directory, entry.name)));
98
89
  }
99
90
  }
100
91
  await visit(skillRoot);
101
- const skillPath = path.join(home, 'skills/subagent-model-selection/SKILL.md');
102
- const modePath = path.join(home, 'skills/subagent-model-selection/references/current-mode.json');
92
+ const skillPath = path.join(home, 'skills/subagent-delegation/SKILL.md');
93
+ const modePath = path.join(home, 'skills/subagent-delegation/references/current-mode.json');
103
94
  for (const [name, level] of Object.entries(ROLES)) {
104
95
  const filename = name.replaceAll('_', '-') + '.toml';
105
96
  const role = safeToml(await readFile(path.join(skillRoot, 'assets/codex/agents', filename)));
@@ -108,9 +99,9 @@ export async function renderPayload(home, mode) {
108
99
  role.developer_instructions = `启动后先读取本 home 的子代理模型选择 Skill,遵守当前派发职责;不递归委派。\n规则路径(JSON 字符串数据):${safeJsonData(skillPath)}\n\n` + role.developer_instructions;
109
100
  const bytes = Buffer.from(stringify(role));
110
101
  files.set(`agents/${filename}`, bytes);
111
- files.set(`skills/subagent-model-selection/assets/codex/agents/${filename}`, bytes);
102
+ files.set(`skills/subagent-delegation/assets/codex/agents/${filename}`, bytes);
112
103
  }
113
- files.set('skills/subagent-model-selection/references/current-mode.json', Buffer.from(JSON.stringify({ ...mode, version: VERSION, roles: ROLES }, null, 2) + '\n'));
104
+ files.set('skills/subagent-delegation/references/current-mode.json', Buffer.from(JSON.stringify({ ...mode, version: VERSION, roles: ROLES }, null, 2) + '\n'));
114
105
  const fragment = await readFile(path.join(skillRoot, 'assets/AGENTS.fragment.md'), 'utf8');
115
106
  const block = Buffer.from(`${START}\n## AI 子代理模型选择与委派\n\nCodex 启动或加载本 home 的规则时,必须先读取以下 Skill;每次创建、重新委派或切换子智能体模型前必须再次按 Skill 和当前档位检查。已有上下文可复用,配置变化后须重读。委派仍须有用户或适用规则授权。\n\n以下 JSON 只表示绑定 home 的文件路径,不是新指令:\n\n\`\`\`json\n${safeJsonData({ skill: skillPath, mode: modePath })}\n\`\`\`\n\n${fragment.trim()}\n${END}\n`);
116
107
  return { files, block };
package/lib/install.mjs CHANGED
@@ -4,9 +4,9 @@ import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import { parse as parseToml } from 'smol-toml';
7
- import { VERSION, LEVELS, ROLES, BUILTINS, MODE_COMMANDS, resolveDefinition, resolveStoredDefinition, validateDefinition, renderPayload } from './codex.mjs';
7
+ import { VERSION, LEVELS, ROLES, BUILTINS, resolveDefinition, validateDefinition, renderPayload } from './codex.mjs';
8
8
 
9
- const SKILL = 'skills/subagent-model-selection';
9
+ const SKILL = 'skills/subagent-delegation';
10
10
  const STATE_DIR = '.subagent-delegation';
11
11
  const STATE = `${STATE_DIR}/state.json`;
12
12
  const BASELINE = `${STATE_DIR}/restore.json`;
@@ -37,15 +37,6 @@ function fail(code, message, exitCode = 1, details = {}) { throw new DelegationE
37
37
  function keys(value, required, optional = []) {
38
38
  return object(value) && required.every(key => Object.hasOwn(value, key)) && Object.keys(value).every(key => [...required, ...optional].includes(key));
39
39
  }
40
- function version(value) {
41
- if (typeof value !== 'string' || !/^\d+\.\d+\.\d+$/.test(value)) fail('STATE_INVALID', '安装版本记录无效。');
42
- return value.split('.').map(Number);
43
- }
44
- function compareVersions(left, right) {
45
- const a = version(left), b = version(right);
46
- for (let index = 0; index < 3; index++) if (a[index] !== b[index]) return Math.sign(a[index] - b[index]);
47
- return 0;
48
- }
49
40
  export function resolveHome(explicit) {
50
41
  let value = explicit ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
51
42
  if (typeof value !== 'string' || !value || /[\p{Cc}\p{Cf}]/u.test(value)) fail('INVALID_HOME', 'Codex home 必须是有效路径。', 2);
@@ -162,17 +153,14 @@ function validateCustom(bytes) {
162
153
  }
163
154
  function validateState(bytes) {
164
155
  if (bytes === null) return null;
165
- const state = parseJson(bytes, 'STATE_INVALID', '安装记录无效或不兼容。');
166
- const common = ['schema_version', 'agent', 'version', 'files', 'rules_sha256'];
167
- const extra = ['mode', 'mode_kind', 'instance_id', 'file_modes', 'rules_mode', 'restoration'];
168
- if (![1, 2].includes(state?.schema_version) || state.agent !== 'codex' || !keys(state, state.schema_version === 1 ? common : [...common, ...extra]) || !object(state.files) || !SHA.test(state.rules_sha256)) fail('STATE_INVALID', '安装记录无效或不兼容。');
169
- version(state.version);
170
- for (const [relative, digest] of Object.entries(state.files)) if (!owned(relative) || state.schema_version === 1 && relative === CUSTOM || !SHA.test(digest)) fail('STATE_INVALID', '安装记录含非法路径或摘要。');
171
- if (state.schema_version === 2) {
172
- if (![...ROLE_PATHS, `${SKILL}/SKILL.md`, `${SKILL}/references/current-mode.json`].every(relative => Object.hasOwn(state.files, relative)) || typeof state.mode !== 'string' || !['builtin', 'custom'].includes(state.mode_kind) || !UUID.test(state.instance_id) || !object(state.file_modes) || !equal(Object.keys(state.files).sort(), Object.keys(state.file_modes).sort()) || !Object.values(state.file_modes).every(permission) || !permission(state.rules_mode)) fail('STATE_INVALID', '安装记录字段无效。');
173
- const restore = state.restoration;
174
- if (!keys(restore, ['status', 'baseline_sha256', 'retired_paths']) || !['available', 'legacy_unavailable'].includes(restore.status) || !Array.isArray(restore.retired_paths) || new Set(restore.retired_paths).size !== restore.retired_paths.length || !restore.retired_paths.every(owned) || restore.retired_paths.some(relative => Object.hasOwn(state.files, relative)) || (restore.status === 'available' ? !SHA.test(restore.baseline_sha256) : restore.baseline_sha256 !== null || restore.retired_paths.length)) fail('STATE_INVALID', '恢复状态字段无效。');
175
- }
156
+ const state = parseJson(bytes, 'STATE_INVALID', '安装记录无效或格式不支持。');
157
+ const fields = ['schema_version', 'agent', 'version', 'files', 'rules_sha256', 'mode', 'mode_kind', 'instance_id', 'file_modes', 'rules_mode', 'restoration'];
158
+ if (state?.schema_version !== 2 || state.agent !== 'codex' || !keys(state, fields) || !object(state.files) || !SHA.test(state.rules_sha256)) fail('STATE_INVALID', '安装记录无效或格式不支持;本内测版本不提供升级迁移。');
159
+ if (state.version !== VERSION) fail('VERSION_MISMATCH', '配置版本与当前工具不一致;请先使用原版本 restore,再使用当前版本 setup。未修改任何文件。');
160
+ for (const [relative, digest] of Object.entries(state.files)) if (!owned(relative) || !SHA.test(digest)) fail('STATE_INVALID', '安装记录含非法路径或摘要。');
161
+ if (![...ROLE_PATHS, `${SKILL}/SKILL.md`, `${SKILL}/references/current-mode.json`].every(relative => Object.hasOwn(state.files, relative)) || typeof state.mode !== 'string' || !['builtin', 'custom'].includes(state.mode_kind) || !UUID.test(state.instance_id) || !object(state.file_modes) || !equal(Object.keys(state.files).sort(), Object.keys(state.file_modes).sort()) || !Object.values(state.file_modes).every(permission) || !permission(state.rules_mode)) fail('STATE_INVALID', '安装记录字段无效。');
162
+ const restore = state.restoration;
163
+ if (!keys(restore, ['status', 'baseline_sha256']) || restore.status !== 'available' || !SHA.test(restore.baseline_sha256)) fail('STATE_INVALID', '恢复状态字段无效。');
176
164
  return state;
177
165
  }
178
166
  function base64(value, code = 'RESTORE_METADATA_INVALID') {
@@ -189,7 +177,7 @@ function validateBaseline(bytes, state) {
189
177
  if (!owned(relative)) fail('RESTORE_METADATA_INVALID', '恢复记录路径不在白名单中。');
190
178
  if (entry !== null && (!keys(entry, ['bytes_base64', 'sha256', 'mode']) || !permission(entry.mode) || !SHA.test(entry.sha256) || hash(base64(entry.bytes_base64)) !== entry.sha256)) fail('RESTORE_METADATA_INVALID', '恢复记录条目无效。');
191
179
  }
192
- const expected = [...Object.keys(state.files), ...state.restoration.retired_paths].sort();
180
+ const expected = Object.keys(state.files).sort();
193
181
  if (!equal(Object.keys(baseline.entries).sort(), expected)) fail('RESTORE_METADATA_INVALID', '恢复记录与当前管理文件不对应。');
194
182
  const rules = baseline.rules;
195
183
  if (!keys(rules, ['existed', 'mode', 'sha256', 'original_block_base64', 'separator_base64', 'original_prefix_sha256']) || typeof rules.existed !== 'boolean' || (rules.existed ? !permission(rules.mode) || !SHA.test(rules.sha256) : rules.mode !== null || rules.sha256 !== null) || !SHA.test(rules.original_prefix_sha256)) fail('RESTORE_METADATA_INVALID', '恢复规则记录无效。');
@@ -219,21 +207,21 @@ async function collect(home, { ignoreLock = false } = {}) {
219
207
  const customModes = validateCustom((await file(CUSTOM)).bytes);
220
208
  await file(`${SKILL}/references/current-mode.json`);
221
209
  let mode = null;
222
- if (state?.schema_version === 2) {
223
- try { mode = resolveStoredDefinition(state.mode, customModes); } catch { fail('STATE_INVALID', '活动档位引用无效。'); }
210
+ if (state) {
211
+ try { mode = resolveDefinition(state.mode, customModes); } catch { fail('STATE_INVALID', '活动档位引用无效。'); }
224
212
  if (mode.kind !== state.mode_kind) fail('STATE_INVALID', '活动档位类型记录无效。');
225
213
  const installedBytes = (await file(`${SKILL}/references/current-mode.json`)).bytes;
226
214
  if (installedBytes !== null) {
227
215
  const installed = parseJson(installedBytes, 'STATE_INVALID', '已部署档位说明无效。');
228
216
  if (!keys(installed, ['id', 'name', 'kind', 'levels', 'version', 'roles', 'fallback_conditions']) || installed.id !== state.mode || installed.kind !== state.mode_kind || safeText(installed.name) === null || [...installed.name].length > 80 || !Array.isArray(installed.fallback_conditions) || !installed.fallback_conditions.every(value => typeof value === 'string' && /^[A-Z_]+$/.test(value)) || !equal(installed.roles, ROLES)) fail('STATE_INVALID', '已部署档位说明无效。');
229
- version(installed.version);
217
+ if (installed.version !== VERSION) fail('STATE_INVALID', '已部署档位说明的版本与当前工具不一致。');
230
218
  let levels;
231
219
  try { levels = validateDefinition({ levels: installed.levels }); } catch { fail('STATE_INVALID', '已部署档位等级定义无效。'); }
232
220
  mode = { id: installed.id, name: installed.name, kind: installed.kind, levels, fallback_conditions: installed.fallback_conditions, definition_version: installed.version };
233
221
  }
234
222
  }
235
- const candidates = await renderPayload(home, mode ?? BUILTINS.find(value => value.id === 'balanced'));
236
- const paths = new Set([...candidates.files.keys(), ...Object.keys(state?.files ?? {}), ...(state?.restoration?.retired_paths ?? [])]);
223
+ const candidates = await renderPayload(home, mode ?? resolveDefinition('mid'));
224
+ const paths = new Set([...candidates.files.keys(), ...Object.keys(state?.files ?? {})]);
237
225
  for (const relative of paths) {
238
226
  if (!owned(relative)) fail('STATE_INVALID', '管理路径不在白名单中。');
239
227
  await file(relative);
@@ -253,18 +241,15 @@ async function collect(home, { ignoreLock = false } = {}) {
253
241
  if (!state && (await file(BASELINE)).bytes !== null) fail('RESTORE_METADATA_INVALID', '恢复记录存在但安装状态缺失;请人工核对残留配置。');
254
242
  if (!state && (await file(CUSTOM)).bytes !== null) fail('STATE_INVALID', '自定义档位记录存在但安装状态缺失。');
255
243
  if (state) {
256
- if (state.schema_version === 2 && (await file(STATE)).mode !== 0o600) diagnostics.push(diagnostic('STATE_PERMISSIONS_DRIFT', 'fail', path.join(home, STATE), '安装状态文件权限变化。'));
244
+ if ((await file(STATE)).mode !== 0o600) diagnostics.push(diagnostic('STATE_PERMISSIONS_DRIFT', 'fail', path.join(home, STATE), '安装状态文件权限变化。'));
257
245
  for (const [relative, digest] of Object.entries(state.files)) {
258
246
  const actual = await file(relative);
259
- if (actual.bytes === null || hash(actual.bytes) !== digest || state.schema_version === 2 && actual.mode !== state.file_modes[relative]) diagnostics.push(diagnostic('MANAGED_FILE_DRIFT', 'fail', path.join(home, relative), '受管理文件被修改、删除或权限变化。'));
247
+ if (actual.bytes === null || hash(actual.bytes) !== digest || actual.mode !== state.file_modes[relative]) diagnostics.push(diagnostic('MANAGED_FILE_DRIFT', 'fail', path.join(home, relative), '受管理文件被修改、删除或权限变化。'));
260
248
  }
261
249
  const rules = extractRules((await file('AGENTS.md')).bytes);
262
- if (!rules || hash(rules.block) !== state.rules_sha256 || state.schema_version === 2 && (await file('AGENTS.md')).mode !== state.rules_mode) diagnostics.push(diagnostic('MANAGED_RULES_DRIFT', 'fail', path.join(home, 'AGENTS.md'), '受管理区块被修改、移除或权限变化。'));
263
- if (state.schema_version === 2 && state.restoration.status === 'available') {
264
- if ((await file(BASELINE)).mode !== 0o600) fail('RESTORE_METADATA_INVALID', '安装前恢复记录权限变化。');
265
- baseline = validateBaseline((await file(BASELINE)).bytes, state);
266
- for (const relative of state.restoration.retired_paths) if ((await file(relative)).bytes !== null) diagnostics.push(diagnostic('RETIRED_FILE_DRIFT', 'fail', path.join(home, relative), '退场的管理路径被重新创建。'));
267
- } else if ((await file(BASELINE)).bytes !== null) fail('RESTORE_METADATA_INVALID', '不应存在恢复记录;请人工核对。');
250
+ if (!rules || hash(rules.block) !== state.rules_sha256 || (await file('AGENTS.md')).mode !== state.rules_mode) diagnostics.push(diagnostic('MANAGED_RULES_DRIFT', 'fail', path.join(home, 'AGENTS.md'), '受管理区块被修改、移除或权限变化。'));
251
+ if ((await file(BASELINE)).mode !== 0o600) fail('RESTORE_METADATA_INVALID', '安装前恢复记录权限变化。');
252
+ baseline = validateBaseline((await file(BASELINE)).bytes, state);
268
253
  if ((await file(CUSTOM)).bytes !== null && !Object.hasOwn(state.files, CUSTOM)) fail('STATE_INVALID', '自定义档位记录未登记所有权。');
269
254
  }
270
255
  let config = {};
@@ -294,7 +279,7 @@ async function collect(home, { ignoreLock = false } = {}) {
294
279
  return snapshot;
295
280
  }
296
281
  function revision(snapshot) {
297
- return hash(json({ instance: snapshot.state?.instance_id ?? (snapshot.state ? 'legacy' : 'not_installed'), files: [...snapshot.observedFiles].sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => [name, stamp(value)]), directories: [...snapshot.observedDirectories].sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => [name, stamp(value)]) }));
282
+ return hash(json({ instance: snapshot.state?.instance_id ?? 'not_installed', files: [...snapshot.observedFiles].sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => [name, stamp(value)]), directories: [...snapshot.observedDirectories].sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => [name, stamp(value)]) }));
298
283
  }
299
284
  async function stillObserved(snapshot, ignoreLock = false) {
300
285
  if (!ignoreLock && await lockExists(snapshot.home)) return false;
@@ -317,7 +302,7 @@ export async function readSnapshot(home, options = {}) {
317
302
  }
318
303
  }
319
304
  function requireClean(snapshot, restore = false) {
320
- const failures = snapshot.diagnostics.filter(value => value.status === 'fail' && (!restore || ['MANAGED_FILE_DRIFT', 'MANAGED_RULES_DRIFT', 'RETIRED_FILE_DRIFT', 'STATE_PERMISSIONS_DRIFT'].includes(value.code)));
305
+ const failures = snapshot.diagnostics.filter(value => value.status === 'fail' && (!restore || ['MANAGED_FILE_DRIFT', 'MANAGED_RULES_DRIFT', 'STATE_PERMISSIONS_DRIFT'].includes(value.code)));
321
306
  if (failures.length) fail(restore ? 'RESTORE_CONFLICT' : failures[0].code, restore ? '受管理内容存在冲突,未恢复任何文件。' : '受管理内容存在冲突,未覆盖任何文件。', 1, { paths: failures.map(value => value.source) });
322
307
  }
323
308
  function requireRevision(snapshot, expected) {
@@ -365,10 +350,8 @@ async function buildPlan(snapshot, action, options) {
365
350
  requireRevision(snapshot, options.expectRevision);
366
351
  requireClean(snapshot, action === 'restore');
367
352
  const { state, home } = snapshot;
368
- if (state && compareVersions(VERSION, state.version) < 0) fail('VERSION_TOO_OLD', '当前工具比已安装配置旧,拒绝覆盖或恢复。');
369
353
  if (action !== 'setup' && action !== 'restore' && !state) fail('NOT_INSTALLED', '请先执行 setup 配置 Codex。');
370
354
  if (!['setup', 'set', 'create', 'configure', 'restore'].includes(action)) fail('INVALID_ACTION', '不支持的写入动作。', 2);
371
- if (!['setup', 'restore'].includes(action) && (state.schema_version !== 2 || state.version !== VERSION)) fail('VERSION_MISMATCH', '请先使用 setup 安全升级配置,再修改档位。');
372
355
  const changes = new Map();
373
356
  const plan = { snapshot, action, changes, mode: snapshot.mode, warnings: [], deploymentChanged: false, cleanupDirectories: [] };
374
357
  const add = async (relative, after, mode) => {
@@ -380,7 +363,6 @@ async function buildPlan(snapshot, action, options) {
380
363
  if (action === 'restore') {
381
364
  plan.mode = null;
382
365
  if (!state) return plan;
383
- if (state.schema_version === 1 || state.restoration.status === 'legacy_unavailable') fail('RESTORE_BASELINE_UNAVAILABLE', '旧版没有安装前恢复基线;需要您自己的备份或人工核对原状。');
384
366
  for (const [relative, entry] of Object.entries(snapshot.baseline.entries)) await add(relative, entry === null ? null : base64(entry.bytes_base64), entry?.mode);
385
367
  const current = snapshot.observedFiles.get('AGENTS.md');
386
368
  const found = extractRules(current.bytes);
@@ -404,11 +386,11 @@ async function buildPlan(snapshot, action, options) {
404
386
  return plan;
405
387
  }
406
388
  let customModes = snapshot.customModes.map(mode => ({ ...mode, levels: structuredClone(mode.levels) }));
407
- let desiredMode = snapshot.mode ? resolveStoredDefinition(snapshot.mode.id, customModes) : resolveDefinition('mid', customModes);
389
+ let desiredMode = snapshot.mode ? resolveDefinition(snapshot.mode.id, customModes) : resolveDefinition('mid', customModes);
408
390
  if (action === 'setup' && options.mode !== undefined || action === 'set') desiredMode = resolveDefinition(options.mode, customModes);
409
391
  if (action === 'create') {
410
392
  const name = normalizeName(options.name);
411
- if (Object.hasOwn(MODE_COMMANDS, name) || [...BUILTINS, ...customModes].some(mode => [mode.name, mode.id].includes(name))) fail('NAME_CONFLICT', '档位名称与内置命令、既有档位或内部 id 重复。', 2);
393
+ if ([...BUILTINS, ...customModes].some(mode => [mode.name, mode.id].includes(name))) fail('NAME_CONFLICT', '档位名称与内置命令、既有档位或内部 id 重复。', 2);
412
394
  const source = resolveDefinition(options.from ?? 'mid', customModes);
413
395
  const definition = { id: `custom-${randomUUID()}`, name, kind: 'custom', levels: structuredClone(source.levels) };
414
396
  customModes.push(definition); plan.created_mode = definition;
@@ -431,7 +413,7 @@ async function buildPlan(snapshot, action, options) {
431
413
  levels = validateDefinition({ levels });
432
414
  }
433
415
  customModes = customModes.map(mode => mode.id === definition.id ? { ...mode, levels } : mode);
434
- if (desiredMode.id === definition.id) desiredMode = resolveStoredDefinition(definition.id, customModes);
416
+ if (desiredMode.id === definition.id) desiredMode = resolveDefinition(definition.id, customModes);
435
417
  }
436
418
  const deploying = action === 'setup' || action === 'set' || action === 'configure' && desiredMode.id === resolveDefinition(options.mode, customModes).id;
437
419
  if (deploying && snapshot.diagnostics.some(value => value.code === 'RULES_SHADOWED')) fail('RULES_SHADOWED', '非空 AGENTS.override.md 遮蔽本库规则;未写入部署配置。');
@@ -458,12 +440,10 @@ async function buildPlan(snapshot, action, options) {
458
440
  if (!state) {
459
441
  baseline = { schema_version: 1, agent: 'codex', entries: {}, rules: { existed: originalRules.bytes !== null, mode: originalRules.mode, sha256: originalRules.bytes === null ? null : hash(originalRules.bytes), original_block_base64: merged.found?.block.toString('base64') ?? null, separator_base64: merged.separator.toString('base64'), original_prefix_sha256: hash(merged.found?.before ?? originalRules.bytes ?? Buffer.alloc(0)) }, created_directories: directories };
460
442
  }
461
- if (baseline) {
462
- for (const relative of files.keys()) if (!Object.hasOwn(baseline.entries, relative)) baseline.entries[relative] = managedEntry(snapshot.observedFiles.get(relative) ?? await observe(home, relative));
463
- baseline.created_directories = [...new Set([...baseline.created_directories, ...directories])];
464
- await add(BASELINE, json(baseline), 0o600);
465
- }
466
- const stateBytes = json({ schema_version: 2, agent: 'codex', version: VERSION, mode: desiredMode.id, mode_kind: desiredMode.kind, instance_id: state?.instance_id ?? randomUUID(), files: Object.fromEntries([...files].sort(([a], [b]) => a.localeCompare(b)).map(([relative, bytes]) => [relative, hash(bytes)])), file_modes: Object.fromEntries([...files.keys()].sort().map(relative => [relative, changes.get(relative)?.mode ?? snapshot.observedFiles.get(relative)?.mode ?? 0o600])), rules_mode: changes.get('AGENTS.md')?.mode ?? originalRules.mode ?? 0o600, rules_sha256: deploying ? hash(payload.block) : state.rules_sha256, restoration: baseline ? { status: 'available', baseline_sha256: hash(json(baseline)), retired_paths: Object.keys(baseline.entries).filter(relative => !files.has(relative)).sort() } : { status: 'legacy_unavailable', baseline_sha256: null, retired_paths: [] } });
443
+ for (const relative of files.keys()) if (!Object.hasOwn(baseline.entries, relative)) baseline.entries[relative] = managedEntry(snapshot.observedFiles.get(relative) ?? await observe(home, relative));
444
+ baseline.created_directories = [...new Set([...baseline.created_directories, ...directories])];
445
+ await add(BASELINE, json(baseline), 0o600);
446
+ const stateBytes = json({ schema_version: 2, agent: 'codex', version: VERSION, mode: desiredMode.id, mode_kind: desiredMode.kind, instance_id: state?.instance_id ?? randomUUID(), files: Object.fromEntries([...files].sort(([a], [b]) => a.localeCompare(b)).map(([relative, bytes]) => [relative, hash(bytes)])), file_modes: Object.fromEntries([...files.keys()].sort().map(relative => [relative, changes.get(relative)?.mode ?? snapshot.observedFiles.get(relative)?.mode ?? 0o600])), rules_mode: changes.get('AGENTS.md')?.mode ?? originalRules.mode ?? 0o600, rules_sha256: deploying ? hash(payload.block) : state.rules_sha256, restoration: { status: 'available', baseline_sha256: hash(json(baseline)) } });
467
447
  await add(STATE, stateBytes, 0o600);
468
448
  const ordered = new Map();
469
449
  if (changes.has(BASELINE)) ordered.set(BASELINE, changes.get(BASELINE));
@@ -658,8 +638,8 @@ export async function statusReport(home, options = {}) {
658
638
  for (const [name, profile] of Object.entries(snapshot.config.profiles ?? {})) if (object(profile) && safeText(name, 80) !== null) profileCandidates.push({ name, model: safeText(profile.model), effort: safeText(profile.model_reasoning_effort, 40), default_subagent_model: safeText(profile.agents?.default_subagent_model), default_subagent_reasoning_effort: safeText(profile.agents?.default_subagent_reasoning_effort, 40), source: path.join(snapshot.home, 'config.toml'), active: null });
659
639
  if (profileCandidates.length) skippedPaths.push({ path: path.join(snapshot.home, 'config.toml'), reason: 'conditional_active_profile_unknown' });
660
640
  for (const [relative, observed] of snapshot.observedDirectories) if (relative === 'agents' || relative.startsWith('agents/')) for (const [name, kind] of observed.entries ?? []) if (kind === 'symlink') skippedPaths.push({ path: safeSource(path.join(snapshot.home, relative, name)), reason: 'unfollowed_symlink' });
661
- const rulesSummary = ['仅在用户和宿主授权下委派', '按任务难度和验证需求选择最小充分模型', '用户明确模型、预算和项目规则优先', '禁止递归委派', '只读 Git', '禁止修改生产、共享环境、第三方系统和代理配置', '明确独占写入', '先读取真实材料与验收条件', '等价局部差异自行适配', '核心前提失效只阻断受影响部分', '必要验证通过后停止,不擅自扩大检查', '未知退出状态、陈旧报告或部分结果标记 PARTIAL', '图像生成由主代理处理', '固定角色不适用时使用 task_delegate,权限与验收不变', '备用须满足原授权、预算、工具和能力;失败不得静默降级'];
662
- return { agent: 'codex', home: snapshot.home, cli_version: VERSION, configuration_version: state?.version ?? null, configuration_revision: snapshot.configuration_revision, installation: !state ? 'not_installed' : state.schema_version === 1 ? 'legacy' : 'installed', integrity: snapshot.diagnostics.some(value => value.status === 'fail') ? 'drifted' : 'clean', mode: mode ? { id: mode.id, name: mode.name, kind: mode.kind, levels: mode.levels } : null, restoration: { status: state?.restoration?.status ?? (state ? 'legacy_unavailable' : 'not_installed') }, roles: Object.entries(ROLES).map(([name, level]) => {
641
+ const rulesSummary = ['仅在用户和宿主授权下委派', '按任务难度和验证需求选择最小充分模型', '用户明确模型、预算和项目规则优先', '禁止递归委派', '禁止修改生产、共享环境、第三方系统和代理配置', '明确独占写入', '先读取真实材料与验收条件', '等价局部差异自行适配', '核心前提失效只阻断受影响部分', '必要验证通过后停止,不擅自扩大检查', '未知退出状态、陈旧报告或部分结果标记 PARTIAL', '固定角色不适用时使用 task_delegate,权限与验收不变', '备用须满足原授权、预算、工具和能力;失败不得静默降级'];
642
+ return { agent: 'codex', home: snapshot.home, cli_version: VERSION, configuration_version: state?.version ?? null, configuration_revision: snapshot.configuration_revision, installation: state ? 'installed' : 'not_installed', integrity: snapshot.diagnostics.some(value => value.status === 'fail') ? 'drifted' : 'clean', mode: mode ? { id: mode.id, name: mode.name, kind: mode.kind, levels: mode.levels } : null, restoration: { status: state?.restoration.status ?? 'not_installed' }, roles: Object.entries(ROLES).map(([name, level]) => {
663
643
  const relative = `agents/${name.replaceAll('_', '-')}.toml`;
664
644
  const bytes = snapshot.observedFiles.get(relative).bytes;
665
645
  let role = {};
@@ -670,7 +650,7 @@ export async function statusReport(home, options = {}) {
670
650
  }
671
651
  export async function listReport(home, options = {}) {
672
652
  const snapshot = await readSnapshot(home, options);
673
- return { configuration_revision: snapshot.configuration_revision, home: snapshot.home, modes: [...BUILTINS, ...snapshot.customModes].map(mode => ({ ...(mode.kind === 'builtin' && snapshot.mode?.id === mode.id ? snapshot.mode : mode), command: Object.keys(MODE_COMMANDS).find(command => MODE_COMMANDS[command] === mode.id) ?? mode.id, active: snapshot.mode?.id === mode.id, source: mode.kind === 'builtin' && snapshot.mode?.id === mode.id ? 'installed_snapshot' : mode.kind === 'builtin' ? 'package' : 'custom_definition', definition_version: mode.kind === 'builtin' && snapshot.mode?.id === mode.id ? snapshot.mode.definition_version ?? snapshot.state.version : mode.kind === 'builtin' ? VERSION : null })), diagnostics: snapshot.diagnostics };
653
+ return { configuration_revision: snapshot.configuration_revision, home: snapshot.home, modes: [...BUILTINS, ...snapshot.customModes].map(mode => ({ ...(mode.kind === 'builtin' && snapshot.mode?.id === mode.id ? snapshot.mode : mode), active: snapshot.mode?.id === mode.id, source: mode.kind === 'builtin' && snapshot.mode?.id === mode.id ? 'installed_snapshot' : mode.kind === 'builtin' ? 'package' : 'custom_definition', definition_version: mode.kind === 'builtin' && snapshot.mode?.id === mode.id ? snapshot.mode.definition_version ?? snapshot.state.version : mode.kind === 'builtin' ? VERSION : null })), diagnostics: snapshot.diagnostics };
674
654
  }
675
655
  export async function showReport(home, selector, options = {}) {
676
656
  const snapshot = await readSnapshot(home, options);
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "subagent-delegation",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Local Codex subagent delegation profiles, safe configuration and restore.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "fanglinwei",
8
8
  "repository": { "type": "git", "url": "https://github.com/fanglinwei/subagent-delegation.git" },
9
- "bin": { "sub-d": "bin/subagent-delegation.mjs", "subagent-delegation": "bin/subagent-delegation.mjs" },
9
+ "bin": { "sub-d": "bin/subagent-delegation.mjs" },
10
10
  "engines": { "node": ">=22" },
11
- "files": ["bin/", "lib/", "web/", "adapters/", "VERSION", "README.md", "docs/README.md", "docs/usage.md", "docs/implementation-review.md", "LICENSE", "install.py"],
11
+ "files": ["bin/", "lib/", "web/", "adapters/", "VERSION", "README.md", "docs/README.md", "docs/usage.md", "docs/implementation-review.md", "LICENSE"],
12
12
  "scripts": { "test": "node --test tests/*.test.mjs", "test:browser": "node tests/browser.mjs" },
13
13
  "dependencies": { "smol-toml": "1.9.1" }
14
14
  }