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/web/app.js CHANGED
@@ -24,8 +24,8 @@ function node(tag, attributes = {}, ...children) {
24
24
  return element;
25
25
  }
26
26
  function replace(id, ...children) { $(id).replaceChildren(...children.flat()); }
27
- function installed() { return typeof state.status?.installation === 'object' ? state.status.installation.status : state.status?.installation; }
28
- function activeMode() { return typeof state.status?.mode === 'object' ? state.status.mode : state.modes.find(mode => mode.id === state.status?.installation?.mode) ?? null; }
27
+ function installed() { return state.status?.installation; }
28
+ function activeMode() { return state.status?.mode ?? null; }
29
29
  function dirty() { return Boolean(state.saved && state.draft && !same(state.saved.levels, state.draft.levels)); }
30
30
  function current() { return state.selected === activeMode()?.id; }
31
31
  function custom() { return state.saved?.kind === 'custom'; }
@@ -37,12 +37,7 @@ function displayValue(value) { return value === null || value === undefined ? '
37
37
  class ActionError extends Error {
38
38
  constructor(code, message, response = null) { super(message); this.code = code; this.response = response; }
39
39
  }
40
- function modeCommand(selector) {
41
- return state.modes.find(mode => mode.id === selector)?.command ?? selector;
42
- }
43
40
  async function request(action, params = {}, allowPartial = false) {
44
- if (params.mode) params = { ...params, mode: modeCommand(params.mode) };
45
- if (params.from) params = { ...params, from: modeCommand(params.from) };
46
41
  let response, payload;
47
42
  try {
48
43
  response = await fetch('/api/actions', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-Delegation-Token': state.bootstrap.csrf_token }, body: JSON.stringify({ action, params }) });
@@ -78,7 +73,7 @@ function busy(value, text = '') {
78
73
  for (const button of $('dialog-actions').querySelectorAll('button')) button.disabled = value;
79
74
  }
80
75
  function writeBlocked() {
81
- const integrity = state.status?.integrity ?? state.status?.installation?.integrity;
76
+ const integrity = state.status?.integrity;
82
77
  return !state.connected || !state.revision || state.stale || integrity === 'drifted' || installed() === 'invalid' || (state.status?.diagnostics ?? []).some(item => item.status === 'fail' || ['RULES_SHADOWED', 'ADAPTER_UNSUPPORTED'].includes(item.code));
83
78
  }
84
79
 
@@ -88,9 +83,9 @@ async function readBatch(preferred) {
88
83
  const status = (await request('status', { details: true }, true)).report.data;
89
84
  const listing = (await request('list', {}, true)).report.data;
90
85
  if (!status || !Array.isArray(listing?.modes)) throw new ActionError('COMMAND_RESULT_INVALID', '配置查询缺少状态或档位列表。');
91
- const active = typeof status.mode === 'object' ? status.mode?.id : status.installation?.mode;
92
- const selected = listing.modes.some(mode => mode.id === preferred) ? preferred : active ?? listing.modes.find(mode => mode.id === 'balanced')?.id ?? listing.modes[0]?.id;
93
- const shown = selected ? (await request('show', { mode: listing.modes.find(mode => mode.id === selected)?.command ?? selected }, true)).report.data : null;
86
+ const active = status.mode?.id;
87
+ const selected = listing.modes.some(mode => mode.id === preferred) ? preferred : active ?? listing.modes.find(mode => mode.id === 'mid')?.id ?? listing.modes[0]?.id;
88
+ const shown = selected ? (await request('show', { mode: selected }, true)).report.data : null;
94
89
  const revision = status.configuration_revision;
95
90
  if (revision && revision === listing.configuration_revision && (!shown || revision === shown.configuration_revision)) {
96
91
  if (shown && (!shown.definition?.levels || LEVELS.some(level => !shown.definition.levels[level]))) throw new ActionError('COMMAND_RESULT_INVALID', '档位报告缺少完整 L1–L4 定义。');
@@ -140,8 +135,6 @@ async function refresh({ selected = state.selected, compare = false, preserve =
140
135
  }
141
136
 
142
137
  function commandArguments(action, params) {
143
- if (params.mode) params = { ...params, mode: modeCommand(params.mode) };
144
- if (params.from) params = { ...params, from: modeCommand(params.from) };
145
138
  const args = [];
146
139
  if (['set', 'create', 'configure'].includes(action)) args.push('mode', action);
147
140
  else if (action === 'models') args.push('models', 'check');
@@ -165,7 +158,7 @@ function commandText(argv) { return ['sub-d', ...argv.map(value => "'" + String(
165
158
  function nextWrite() {
166
159
  if (!state.saved) return null;
167
160
  const params = { mode: state.selected, expectRevision: state.revision };
168
- if (installed() === 'not_installed' || installed() === 'legacy') return { action: 'setup', params };
161
+ if (installed() === 'not_installed') return { action: 'setup', params };
169
162
  if (dirty()) return { action: 'configure', params: { ...params, definition: { levels: clone(state.draft.levels) } } };
170
163
  if (!current()) return { action: 'set', params };
171
164
  return null;
@@ -289,8 +282,8 @@ function showTab(tab) {
289
282
  function renderModePicker() {
290
283
  const focusedMode = $('mode-radios').contains(document.activeElement) ? document.activeElement.id : null;
291
284
  const isCustom = state.modes.find(mode => mode.id === state.selected)?.kind === 'custom';
292
- const descriptions = { economy: '简单任务,优先节省', balanced: '日常任务,兼顾质量', performance: '复杂任务,优先质量', custom: '自行设置模型与备用' };
293
- const radios = state.modes.filter(mode => mode.kind !== 'custom').map(mode => ({ value: mode.id, label: `${mode.name} · ${mode.command ?? mode.id}` }));
285
+ const descriptions = { low: '简单任务,优先节省', mid: '日常任务,兼顾质量', fire: '复杂任务,优先质量', custom: '自行设置模型与备用' };
286
+ const radios = state.modes.filter(mode => mode.kind !== 'custom').map(mode => ({ value: mode.id, label: `${mode.name} · ${mode.id}` }));
294
287
  radios.push({ value: 'custom', label: '自定义' });
295
288
  replace('mode-radios', radios.map(option => node('label', {}, node('input', { id: `mode-${option.value}`, type: 'radio', name: 'mode', value: option.value, 'aria-label': option.label, 'aria-describedby': `mode-${option.value}-note`, checked: option.value === 'custom' ? isCustom : state.selected === option.value, disabled: state.busy || !state.connected, on: { change: () => { if (option.value !== 'custom') selectMode(option.value); else { const target = state.modes.find(mode => mode.kind === 'custom'); if (target) selectMode(target.id); else { renderModePicker(); createMode(); } } } } }), node('span', { class: 'mode-name' }, option.label), node('span', { id: `mode-${option.value}-note`, class: 'mode-description' }, descriptions[option.value] ?? '查看模型分配'))));
296
289
  $('custom-picker').hidden = !isCustom;
@@ -359,7 +352,7 @@ function updateLevelSummary(level) {
359
352
  }
360
353
  function renderChanges() {
361
354
  if (!state.draft) { $('changes-count').textContent = '暂无可编辑的定义'; replace('change-list'); return; }
362
- const setup = installed() === 'not_installed' || installed() === 'legacy';
355
+ const setup = installed() === 'not_installed';
363
356
  const old = dirty() || current() ? state.saved : activeMode();
364
357
  const changes = LEVELS.filter(level => setup || !same(old?.levels?.[level], state.draft.levels[level]));
365
358
  $('changes-count').textContent = changes.length ? `${setup ? '将配置' : '变化涉及'} ${changes.length} 个任务等级` : current() ? '尚未修改' : '模型组合不变,将切换档位';
@@ -377,20 +370,18 @@ function renderControls() {
377
370
  const blocked = state.busy || writeBlocked();
378
371
  const operation = nextWrite();
379
372
  $('draft-label').hidden = !operation;
380
- $('apply').textContent = state.busy ? '请等待操作完成…' : installed() === 'not_installed' ? '配置 Codex' : installed() === 'legacy' ? '升级并配置 Codex' : dirty() ? current() ? '保存并应用' : '保存档位' : !current() && state.saved ? '设为当前档位' : '无修改';
373
+ $('apply').textContent = state.busy ? '请等待操作完成…' : installed() === 'not_installed' ? '配置 Codex' : dirty() ? current() ? '保存并应用' : '保存档位' : !current() && state.saved ? '设为当前档位' : '无修改';
381
374
  $('apply').disabled = blocked || !operation;
382
375
  $('preview').disabled = blocked || !operation;
383
- $('new-mode').disabled = blocked || !['installed', 'legacy'].includes(installed());
384
- $('copy-mode').disabled = blocked || !state.saved || !['installed', 'legacy'].includes(installed());
376
+ $('new-mode').disabled = blocked || installed() !== 'installed';
377
+ $('copy-mode').disabled = blocked || !state.saved || installed() !== 'installed';
385
378
  $('cancel-draft').hidden = !dirty(); $('cancel-draft').disabled = state.busy;
386
379
  $('cancel-selection').hidden = !activeMode() || current() || dirty(); $('cancel-selection').disabled = state.busy;
387
- const version = state.status?.configuration_version ?? state.status?.installation?.version;
388
- $('upgrade').hidden = !version || version === state.bootstrap?.cli_version || installed() !== 'installed'; $('upgrade').disabled = blocked;
389
380
  $('refresh').disabled = state.busy;
390
381
  for (const id of ['custom-mode', 'explain', 'explain-mode', 'explain-level', 'explain-role', 'check-models', 'verify', 'verify-model', 'verify-effort', 'close-preview', 'compare']) $(id).disabled = state.busy || !state.connected;
391
382
  for (const input of document.querySelectorAll('#levels input, #mode-radios input, #verify-form input[type=radio]')) input.disabled = state.busy || !state.connected;
392
383
  for (const link of document.querySelectorAll('.tabs a')) link.setAttribute('aria-disabled', String(state.busy));
393
- const restoration = state.status?.restoration?.status ?? state.status?.installation?.restoration?.status;
384
+ const restoration = state.status?.restoration?.status;
394
385
  $('restore-preview').disabled = state.busy || !state.connected || !state.revision || state.stale || !['available'].includes(restoration);
395
386
  $('restore-confirm').disabled = state.busy || !state.restorePreview || state.restorePreview.revision !== state.revision;
396
387
  }
@@ -411,15 +402,15 @@ function renderFiles(id, data) {
411
402
  function renderRules() {
412
403
  replace('role-list', (state.status?.roles ?? []).map(role => node('div', { class: 'role' }, node('h4', {}, `${ROLE_LABELS[role.name] ?? role.name} · ${role.name}`), node('p', {}, `${role.configured_model ?? role.model ?? '模型未固定'} / ${role.configured_effort ?? role.effort ?? '推理强度未固定'}`), node('p', {}, role.name === 'task_scan' || role.name === 'task_review' ? '只读职责;不获得项目写入权限。' : role.name === 'task_execute' ? '仅在本次明确范围内产出,写入范围须独占。' : '按照本次明确授权与原职责执行。'), role.source ? node('p', { class: 'path' }, `来源:${role.source}`) : null)));
413
404
  if (!state.status?.roles?.length) replace('role-list', node('p', { class: 'muted' }, '未安装本工具的角色。可在档位页先预览并配置。'));
414
- const summary = state.status?.rules?.summary ?? ['用户与宿主授权优先', '禁止递归委派', '只读 Git', '明确独占写入', '外部写入交回主代理', '局部阻断与最小充分验证', '保留专业规则、预算和高影响裁决边界'];
405
+ const summary = state.status?.rules?.summary ?? ['用户与宿主授权优先', '禁止递归委派', '明确独占写入', '外部写入交回主代理', '局部阻断与最小充分验证', '保留专业规则、预算和高影响裁决边界'];
415
406
  replace('rule-summary', node('ul', {}, summary.map(item => node('li', {}, typeof item === 'string' ? item : displayValue(item)))));
416
407
  replace('sources', node('p', { class: 'muted' }, `项目查询范围:${state.bootstrap?.project ?? '服务启动时的工作目录'}`), node('pre', {}, JSON.stringify({ rules: state.status?.rules, related_config: state.status?.related_config, inspection: state.status?.inspection, diagnostics: state.status?.diagnostics }, null, 2)));
417
408
  const previous = $('explain-mode').value;
418
409
  replace('explain-mode', state.modes.map(mode => node('option', { value: mode.id, selected: mode.id === (previous || state.selected) }, mode.name)));
419
410
  }
420
411
  function renderRestore() {
421
- const restoration = state.status?.restoration?.status ?? state.status?.installation?.restoration?.status;
422
- const messages = { available: '安装前恢复记录可用。仍需先预览并通过完整预检。', not_installed: '未安装本工具,无需恢复。', not_applicable: '未安装本工具,无需恢复。', legacy_unavailable: '旧版安装没有首次配置前的恢复基线。自动恢复不可用,请使用原备份或在本地人工核对。', invalid: '恢复记录无效。自动恢复已阻止,请核对诊断与原备份。' };
412
+ const restoration = state.status?.restoration?.status;
413
+ const messages = { available: '安装前恢复记录可用。仍需先预览并通过完整预检。', not_installed: '未安装本工具,无需恢复。', invalid: '恢复记录无效。自动恢复已阻止,请核对诊断与原备份。' };
423
414
  $('restore-capability').textContent = messages[restoration] ?? '恢复能力未确认,请查看状态诊断。';
424
415
  $('restore-target').textContent = `绑定 home:${state.bootstrap?.home ?? ''}`;
425
416
  const customs = state.modes.filter(mode => mode.kind === 'custom');
@@ -457,8 +448,8 @@ function renderModels() {
457
448
  function render() {
458
449
  const active = activeMode();
459
450
  $('configured').dataset.state = installed();
460
- $('versions').textContent = `工具 ${state.bootstrap?.cli_version ?? '未知'} · 已安装配置 ${state.status?.configuration_version ?? state.status?.installation?.version ?? '未安装'}`;
461
- $('configured').textContent = installed() === 'legacy' ? '旧版安装,档位未记录' : active ? `已配置:${active.name}` : '尚未配置本工具';
451
+ $('versions').textContent = `工具 ${state.bootstrap?.cli_version ?? '未知'} · 已安装配置 ${state.status?.configuration_version ?? '未安装'}`;
452
+ $('configured').textContent = active ? `已配置:${active.name}` : '尚未配置本工具';
462
453
  $('runtime').hidden = installed() !== 'installed';
463
454
  $('runtime').textContent = '使用已配置规则前请重启 Codex;运行生效未验证。';
464
455
  $('editing-title').textContent = state.saved ? `${custom() ? '正在编辑' : '正在查看'}:${state.saved.name}` : '四级模型预设';
@@ -481,7 +472,7 @@ async function createMode() {
481
472
  source.addEventListener('change', () => { preview.hidden = true; });
482
473
  const valid = () => {
483
474
  const normalized = name.value.trim().normalize('NFC');
484
- if (!validText(normalized, 80) || state.modes.some(mode => mode.id === normalized || mode.name === normalized || mode.command === normalized)) {
475
+ if (!validText(normalized, 80) || state.modes.some(mode => mode.id === normalized || mode.name === normalized)) {
485
476
  error.textContent = '名称须为唯一的 1–80 个字符,不能与内置命令同名或包含控制字符。'; error.hidden = false; name.focus(); return false;
486
477
  }
487
478
  return true;
@@ -558,13 +549,6 @@ $('preview').addEventListener('click', () => apply(true));
558
549
  $('cancel-draft').addEventListener('click', async () => { state.draft = clone(state.saved); state.preview = null; if (state.stale) await refresh({ preserve: false }); else render(); });
559
550
  $('cancel-selection').addEventListener('click', () => selectMode(activeMode().id));
560
551
  $('close-preview').addEventListener('click', () => { state.preview = null; renderPreview(); $('preview').focus(); });
561
- $('upgrade').addEventListener('click', async () => {
562
- if (dirty()) { showError(new ActionError('UNSAVED_DRAFT', '请先保存或取消草稿,再预览升级配置。')); return; }
563
- const params = { mode: activeMode()?.id ?? state.selected, expectRevision: state.revision };
564
- if (!await performWrite('setup', params, { preview: true })) return;
565
- const answer = await dialog({ title: '升级已安装配置?', description: '以下是 CLI 预览的文件变化。保留已选择的档位与自定义定义;升级后请重启 Codex。', content: node('pre', {}, JSON.stringify(state.preview.response.report.data?.changes ?? state.preview.response.report.data?.changed_files ?? [], null, 2)), choices: [{ label: '返回查看预览', value: null }, { label: '升级配置', value: true, class: 'primary' }] });
566
- if (answer) await performWrite('setup', params);
567
- });
568
552
  $('copy-definition').addEventListener('click', async () => {
569
553
  try { await navigator.clipboard.writeText(JSON.stringify({ levels: state.draft.levels }, null, 2)); $('copy-status').textContent = '完整四级 JSON 已复制。'; }
570
554
  catch { $('copy-status').textContent = '浏览器未允许复制,请展开以下 JSON 手动复制。'; $('command').textContent += `\n\n${JSON.stringify({ levels: state.draft.levels }, null, 2)}`; }
package/web/index.html CHANGED
@@ -61,7 +61,7 @@
61
61
  <div class="changes-heading"><h2 id="changes-title">本次变更</h2><span id="draft-label" class="draft-label" hidden>待确认</span></div>
62
62
  <p id="changes-count" class="muted">尚未修改</p><div id="change-list"></div>
63
63
  <p id="save-note" class="save-note"></p>
64
- <div class="actions"><button id="apply" type="button" class="primary" disabled>无修改</button><button id="cancel-draft" type="button" class="secondary" hidden>取消修改</button><button id="cancel-selection" type="button" class="secondary" hidden>取消选择</button><button id="preview" type="button" class="secondary" disabled>预览文件变更</button><button id="upgrade" type="button" class="secondary" hidden>升级配置</button></div>
64
+ <div class="actions"><button id="apply" type="button" class="primary" disabled>无修改</button><button id="cancel-draft" type="button" class="secondary" hidden>取消修改</button><button id="cancel-selection" type="button" class="secondary" hidden>取消选择</button><button id="preview" type="button" class="secondary" disabled>预览文件变更</button></div>
65
65
  <details id="command-details" class="technical"><summary>查看终端命令</summary><p id="command-label" class="muted">将执行的命令(尚未执行)</p><pre id="command"></pre><button id="copy-definition" type="button" class="secondary" hidden>复制完整四级 JSON</button><p id="copy-status" class="muted" role="status"></p></details>
66
66
  </aside>
67
67
  </div>
@@ -1,6 +0,0 @@
1
- interface:
2
- display_name: "Subagent Model Selection"
3
- short_description: "Choose subagent models by task difficulty, scope, and budget"
4
- default_prompt: "Use $subagent-model-selection to choose models and reasoning effort for authorized delegation while respecting my model preferences and budget."
5
- policy:
6
- allow_implicit_invocation: true
package/install.py DELETED
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Compatibility entry: delegate the original install arguments to the npm CLI."""
3
- import argparse
4
- from pathlib import Path
5
- import shutil
6
- import subprocess
7
- import sys
8
-
9
-
10
- def main():
11
- parser = argparse.ArgumentParser(description=__doc__)
12
- parser.add_argument('--agent', choices=['codex'], default='codex')
13
- parser.add_argument('--codex-home')
14
- parser.add_argument('--dry-run', action='store_true')
15
- args = parser.parse_args()
16
- node = shutil.which('node')
17
- if not node:
18
- print('需要 Node.js 22+;请先安装 Node,然后运行 npm ci。', file=sys.stderr)
19
- return 1
20
- command = [node, str(Path(__file__).resolve().parent / 'bin/subagent-delegation.mjs'), 'setup', '--agent=codex']
21
- if args.codex_home is not None:
22
- command.append('--codex-home=' + args.codex_home)
23
- if args.dry_run:
24
- command.append('--dry-run')
25
- try:
26
- return subprocess.run(command, check=False).returncode
27
- except OSError:
28
- print('无法启动 Node CLI;请检查安装路径和权限。', file=sys.stderr)
29
- return 1
30
-
31
-
32
- if __name__ == '__main__':
33
- raise SystemExit(main())