@weotro/dx 0.1.11 → 0.1.13

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.
@@ -17,6 +17,15 @@
17
17
 
18
18
  外部专家永远不写 Git 历史和 GitHub。它只在工作树落改动或返回报告,最终结论由主进程核验。
19
19
 
20
+ ## 委派活性与接管
21
+
22
+ 适用于方案讨论、实现、审查、修复和 follow-up。派发时记录执行者 ID、工作目录、日志或输出位置及责任文件基线;后续始终观察同一任务。
23
+
24
+ - 新增有效日志、工具调用进展、输出内容或责任文件修改,任一出现就更新最后活动记录并继续等待。同一文件反复编辑也算变化;只读任务可以只有日志。总耗时、单次等待超时、尚无最终答复都不能作为终止或替换依据。
25
+ - 静默阈值只触发诊断:检查原任务日志、当前子命令、进程状态及权限/依赖等待,必要时向原执行者询问进度。静默推理、长测试和观测渠道不可用都不等于死亡;重复报错或空心跳也不等于有效进展。
26
+ - 明确失败退出,或持续无有效进展且诊断证实无法继续(例如死锁、失联且执行进程已不存在、不可恢复错误),才进入恢复或接管。能够解除阻塞时优先恢复原任务;证据不足时保留任务并报告未知状态。用户明确取消时按取消要求处理。
27
+ - 接管前记录诊断证据,确认原任务及其写入子进程已停止,再核对现有 diff、新文件和验证结果。交接仅包含剩余工作;保留已完成成果,不从头重放,也不让两个执行者同时写同一范围。
28
+
20
29
  ## 复杂度门禁
21
30
 
22
31
  每次派发外部专家前先过这道门禁。命中任一信号即**重改动**,走外部专家;一个都不命中即**轻改动**,主进程直接做。派发往返成本高于轻改动直改,门禁结果和命中/未命中的信号编号必须记入审查报告。
@@ -102,7 +111,7 @@ dx test e2e backend <file-or-dir>; rc=$?
102
111
  rmdir /tmp/ship-issue-pr-e2e.lock; exit $rc
103
112
  ```
104
113
 
105
- 整段使用入口技能声明的后台执行方式运行。锁目录 mtime 超过 30 分钟即认定上一条线已死,`rmdir` 后重抢。
114
+ 整段使用入口技能声明的后台执行方式运行。记录持锁任务与进程身份;等待过久时检查持锁者日志和进程状态。只有确认持锁者及其测试子进程已结束才清理遗留锁,不能仅凭锁目录 mtime 判死或抢占。
106
115
 
107
116
  同时在跑的 follow-up subagent 最多 3 个。超出的由主进程记在待派清单里,等某条线的完成通知到达后再派下一个——排队没有别的执行者,主进程不记就等于丢了。
108
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weotro/dx",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,48 @@
1
+ # Skills 执行规则审计
2
+
3
+ 本次覆盖 `skills/` 下全部 11 个技能、相关 Markdown 引用和调用元数据。以下引文来自修改前版本,可通过 Git diff 核对。这里判断的是规则在当前任务中的效果;仅凭文字不能证明其作者当初是为某一旧模型设计,也未进行 Astra 行为基准测试。
4
+
5
+ ## 统一结果
6
+
7
+ 在宿主系统与开发者指令约束内,采用当前具体任务及已有授权 > 项目适用的 AGENTS.md > 通用技能与引用流程。只读检索、本地草稿、可回滚编辑和常规测试自主推进;不可逆或难以回滚且未获具体授权的操作,先完成预览和验证再确认。既有授权跨轮有效;真正无法确定目标时仍需澄清,对外消息仍需明确授权。
8
+
9
+ 检索从相关文件和直接依赖开始,按证据扩展;复用有效验证。全部 description 收敛为显式调用条件与一句用途,保留原有显式调用策略,并为原来缺少 agents/openai.yaml 的 4 个技能补齐 allow_implicit_invocation: false。依赖技能的路由细节放在正文。
10
+
11
+ ## 逐技能原句与处理
12
+
13
+ | 技能 | 修改前原句 | 判断与处理 |
14
+ | --- | --- | --- |
15
+ | ask_cc | “调用前完整读取该技能,按其中的活性监控、诊断和接续规则执行。” | 咨询入口强制加载全部执行分支;改为读取咨询调用与结果处理,诊断、接续按需读取。保留脚本超时和模型降级的真实运行语义。 |
16
+ | cc-ship-issue-pr | “完整读取 `SHIP_CORE` 后执行全部阶段。” | 不区分任务终点与已完成阶段;改为按当前任务读取和执行,并明确共享流程低于任务和项目规则。 |
17
+ | delegate-cc | “执行前检查 `git status --short` 和已有 diff,记录涉及文件的初始状态及未跟踪文件。” | 未限定 diff 范围,可能把无关改动也拉入上下文;改为任务文件的 diff 和相关未跟踪文件。保留责任隔离及停止旧写进程后接管的必要约束。 |
18
+ | delivering-design-handoff | “If the user asks to save time by writing the plan while review or fixes are pending, decline that shortcut and finish this gate first.” | 明确要求拒绝用户的并行草稿安排,倒置优先级;移除强制串行审查门禁,允许草稿并行,交付前核对一致性。 |
19
+ | doctor | “所有项目完成后运行一次最终复检。” | 与每次修复后的复检叠加,单项修复也可能拉入全套环境检查;改为按任务范围检查、复用健康证据,只补失效或缺失项。 |
20
+ | gh-dependabot-cleanup | “Confirm target and scope in one sentence.” | Confirm 容易被解释成等待确认;改为从任务与 remote 推断并说明后继续取证。本地修复不强制 commit、push 和 PR。 |
21
+ | git-release | “若存在未提交变更,列出文件并终止流程。” | 无关草稿也阻断发布准备;改为保护改动、必要时隔离 worktree,继续本地准备。冲突 tag 仍阻止不安全发布。 |
22
+ | online-debug-guard | “当且仅当目标环境是 `production` 时,必须确认当前处于 Plan 模式。” | 把生产只读取证绑定交互模式;移除 Plan 门禁,按操作副作用与已有授权判断。 |
23
+ | oo-ship-issue-pr | “完整读取 `SHIP_CORE` 后执行全部阶段。” | 同 CC 入口;增加适配规则,避免小任务被固定阶段、外部专家和全量验证阻断。 |
24
+ | prune-git-repository | “Require explicit current-turn authorization for the exact cleanup categories and thresholds.” | 跨轮丢弃授权,预览前就要求确认;改为先自主 dry-run,已有授权覆盖预览删除清单即可执行,新增删除范围才确认。 |
25
+ | stagewise-ui-debugging | “若页面打不开、空白、或无法渲染,提示用户对应服务可能没启动或状态异常,让用户手动处理后再访问。” | 可本地修复的问题直接交回用户;改为自行检查端口、日志和依赖,启动或修复所需服务后重试。 |
26
+
27
+ ## 其他收敛项
28
+
29
+ - 设计交接原句:“Do not create or update a PR for the docs-only handoff branch as part of this workflow, even when the repository's normal implementation flow expects PRs.” 改为服从用户或项目对 PR 的要求,不把通用技能放在项目之上。固定三份文档、两轮 subagent 审查和多次同样的占位扫描改为按任务规模组织产物和检查。
30
+ - 在线调试原句:“先询问用户当前环境。”、“若无法确认,则默认 `development`”。改为先从任务、配置、SSH 映射核实;仍不唯一时才询问,未知环境不假定为开发环境。接受项目环境别名。
31
+ - CC runtime 原句:“Codex 沙箱不能绑本地端口、不能起 PostgreSQL 共享内存、不能写主仓 `.git` index、无网络下载。” 改为根据实际宿主能力判断,不复制历史沙箱限制及固定跨端补跑清单。
32
+ - CC runtime 原句:“等待期间工作树由 Codex 独占,主进程只做不写工作树的并行事项。” 改为独占分配的责任文件,其他不重叠工作可继续;文件观测限定责任路径。
33
+ - CC runtime 原句:“重改动实现通道不可用时阻塞”。改为主进程接手剩余工作并如实说明执行者。
34
+ - OO runtime 原句:“外部执行者的验证仅作参考。” 改为核对版本、环境、命令和可读输出后复用有效证据,只补缺失或失效验证。
35
+ - 清理原句:“require zero remaining candidates”、“Verify ... a clean current worktree”。改为验证本次授权范围,允许保留保护对象和无关脏文件,不为达到零候选扩大删除。
36
+ - 保留清理脚本的 dry-run、保护 refs、远端读回、精确 refspec 等实际数据保护,以及委派脚本的权限与接续契约;未修改可执行脚本。
37
+
38
+ ## 范围与验证
39
+
40
+ 目录外的 `agent-references/ship-issue-pr-core.md` 未修改。两个交付入口明确其适配优先级,旧共享流程中的硬门禁、固定验证矩阵和自动 follow-up 扩张不能覆盖当前任务。若有人绕过入口直接使用该共享文件,其旧文字仍存在。
41
+
42
+ 同步修改 `test/skill-metadata.test.js`:以 YAML 中实际的显式调用策略验证调用边界,不再依赖 description 中的固定句子。
43
+
44
+ - `pnpm test`:50 个测试套件、414 项测试全部通过。
45
+ - `git diff --check`:通过。
46
+ - `quick_validate.py`:10 个技能通过;`ask_cc` 仅因原有下划线名称未通过 hyphen-case 校验,保留兼容入口,未改名。
47
+ - 未触发技能的真实发布、远程清理或生产调试;文档规则的模型行为效果仍需后续实际使用验证。
48
+
@@ -1,16 +1,28 @@
1
1
  ---
2
2
  name: ask_cc
3
- description: 仅在用户显式调用 $ask_cc 或明确要求使用 ask_cc 技能,或用户已显式调用的 oo-ship-issue-pr 流程要求咨询时使用;不要通过关键词、任务类型或上下文自动触发。
3
+ description: 仅在显式调用 ask_cc 时使用:咨询 Claude Code 获取第二意见。
4
4
  ---
5
5
 
6
6
  # ask_cc
7
7
 
8
+ ## 执行边界与优先级
9
+
10
+ 在宿主系统与开发者指令约束内,冲突时依次采用:当前任务的具体要求与已有授权 > 目标项目适用的 AGENTS.md > 本技能及其引用的通用流程。
11
+ 只读检索、本地草稿、可回滚编辑和常规测试可自主推断并继续;从相关文件和直接依赖开始,仅在证据不足或影响跨模块时扩大范围,复用仍有效的验证。
12
+ 仅在不可逆或难以回滚的修改缺少具体授权时,先完成预览、草稿与验证,再说明对象和影响并等待确认;会话中已有授权继续有效。缺少事实时先查证、标注假设并推进独立工作,确实无法确定操作目标时才询问。对外发送消息须有明确授权。
13
+
8
14
  使用 [delegate-cc](../delegate-cc/SKILL.md) 的咨询流程获取答案或第二意见,默认不授予工具权限。
9
15
 
16
+ 已显式调用的 `oo-ship-issue-pr` 需要咨询时可进入本技能,沿用原任务范围与授权。
17
+
18
+ 调用前读取该技能的咨询调用与结果处理部分,按需查看诊断和接续规则。咨询没有文件修改是正常现象,以本次会话的流式输出和进程状态辅助判断;静默只触发排查,不能仅因尚无答案就停止或换模型。
19
+
10
20
  若用户明确要求修改代码或执行开发任务,转入该技能的 `task` 模式,按任务范围完成实现和验收。
11
21
 
12
22
  旧命令 `scripts/ask_cc.py --prompt-file /绝对路径/问题.txt` 继续可用,仅运行咨询模式;模型顺序和降级逻辑复用 `delegate-cc`。
13
23
 
24
+ 旧入口的 `--timeout` 同样表示无活动告警阈值(默认 120 秒),不是答复截止时间。通过宿主异步工具保留原会话并持续取结果;只有原调用已退出且结果无效或失败时,脚本才尝试下一个模型。
25
+
14
26
  `dx initial` 会同时安装这两个技能。使用当前 `SKILL.md` 所在目录下的脚本;默认安装后的调用方式:
15
27
 
16
28
  ```bash
@@ -1,12 +1,27 @@
1
1
  ---
2
2
  name: cc-ship-issue-pr
3
- description: 仅在用户显式调用 $cc-ship-issue-pr、/cc-ship-issue-pr 或明确要求使用 cc-ship-issue-pr 技能时使用,派发 follow-up 交付的 subagent 也从这里进入;不要通过关键词、任务类型或上下文自动触发。
3
+ description: 仅在显式调用 cc-ship-issue-pr 时使用:通过 Claude Code Codex 交付 Issue/PR。
4
4
  ---
5
5
 
6
6
  # CC Ship Issue PR
7
7
 
8
+ ## 执行边界与优先级
9
+
10
+ 在宿主系统与开发者指令约束内,冲突时依次采用:当前任务的具体要求与已有授权 > 目标项目适用的 AGENTS.md > 本技能及其引用的通用流程。
11
+ 只读检索、本地草稿、可回滚编辑和常规测试可自主推断并继续;从相关文件和直接依赖开始,仅在证据不足或影响跨模块时扩大范围,复用仍有效的验证。
12
+ 仅在不可逆或难以回滚的修改缺少具体授权时,先完成预览、草稿与验证,再说明对象和影响并等待确认;会话中已有授权继续有效。缺少事实时先查证、标注假设并推进独立工作,确实无法确定操作目标时才询问。对外发送消息须有明确授权。
13
+
8
14
  Claude Code 入口:主进程执行轻改动,Codex 承接重改动的方案讨论、实现与独立审查。
9
15
 
16
+ ## 共享流程适配
17
+
18
+ 共享文件是通用参考,不覆盖本入口的执行边界。只读取当前阶段和必要约束;已完成的工作与验证直接复用。
19
+ - 轻改动按涉及文件推进;复杂度信号用于判断是否值得咨询,不自动触发全仓扫描或固定全量构建。命令、基线、模板和 CI 要求从目标项目确定。
20
+ - 当前任务优先于共享文件的硬门禁、阶段顺序和固定措辞;本地准备无需等 Issue、模板、外部专家或合并就绪。工具不可用时可自主接手并准确记录降级。
21
+ - 不明归属的已有改动保留,先在隔离范围继续工作;仅在必须覆盖且归属无法查明时询问。测试失败依靠基线或复现证据归因,不能仅凭未列入已知失败清单就认定是本次引入。
22
+ - Follow-up 先记为本地发现,只有任务授权包含创建 Issue、对外评论或委派后续交付时才执行这些动作。
23
+ - 提交、PR、合并和回访按本次约定的终点执行;合并等难以回滚动作缺少具体授权时,准备好可审阅的结果后确认。
24
+
10
25
  ## 步骤一:加载共享流程
11
26
 
12
27
  ```bash
@@ -15,13 +30,13 @@ SHIP_CORE="$AGENTS_ROOT/references/ship-issue-pr-core.md"
15
30
  test -f "$SHIP_CORE"
16
31
  ```
17
32
 
18
- 完整读取 `SHIP_CORE` 后执行全部阶段。运行时映射如下:
33
+ 读取 `SHIP_CORE` 中当前任务所需阶段,按本入口的适配规则执行。运行时映射如下:
19
34
 
20
35
  - 主进程:当前 Claude Code。
21
36
  - 外部专家:Codex。
22
37
  - 入口显示名:`CC Ship Issue PR`。
23
38
  - Follow-up:Claude Code `Agent(subagent_type: "general-purpose", run_in_background: true)`。
24
39
 
25
- 第一次进入外部专家或 follow-up 分支前,完整读取 [references/runtime.md](references/runtime.md)。该文件是本入口的平台调用契约;共享流程仍是交付语义的唯一真源。
40
+ 仅在进入外部专家或已授权的 follow-up 分支时读取 [references/runtime.md](references/runtime.md)。该文件是本入口的平台调用契约;共享流程提供交付参考,冲突按本入口的优先级解决。
26
41
 
27
- 只有共享流程全部完成且 `mergedAt` 非空、Issue 回访完成,才输出 `CC Ship Issue PR 完成`。
42
+ 按当前任务约定的终点报告完成;只有实际 `mergedAt` 非空并完成 Issue 回访时才声称已合并并回访。
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Codex 通道
6
6
 
7
- Codex 永远不碰 Git 历史和 GitHub。它把改动落在工作树,由 Claude Code 主进程核验、验证和分组提交。
7
+ 默认由主进程负责 Git 历史和 GitHub,除非当前任务明确另行分配责任。它把改动落在工作树,由 Claude Code 主进程核验、验证和分组提交。
8
8
 
9
9
  - **方案讨论 / 实现 / 重修复**:`Agent(subagent_type: "codex:codex-rescue")`。
10
10
  - **普通 review**:Codex companion 的 `review` 子命令。
@@ -33,9 +33,7 @@ CODEX_ROOT=$(/bin/ls -d ~/.claude/plugins/marketplaces/openai-codex/plugins/code
33
33
 
34
34
  ## 补跑清单
35
35
 
36
- Codex 沙箱不能绑本地端口、不能起 PostgreSQL 共享内存、不能写主仓 `.git` index、无网络下载。派发 prompt 原文附上以下主进程补跑项:
37
-
38
- `dx db migrate`(实 apply)、后端 E2E、`dx build front --dev`、`flutter test`、Dart OpenAPI 生成(要下 Maven)、`git commit`。
36
+ 根据实际宿主权限与项目命令判断哪些验证可运行,不预设 Codex 的沙箱能力。只列本次改动需要且外部执行者无法完成的补跑项;迁移实 apply 不属于常规测试,先确认隔离环境与授权。
39
37
 
40
38
  ## 取结果
41
39
 
@@ -57,30 +55,19 @@ PY
57
55
 
58
56
  定位到 job 后读取同目录 `jobs/<id>.json`。只有 `status=completed` 且 `phase=done` 才算返回,报告正文取 `result.rawOutput`。
59
57
 
60
- ## 等待与假死
58
+ ## 等待与活性排查
61
59
 
62
- 等待期间工作树由 Codex 独占,主进程只做不写工作树的并行事项。没别的工作时每 2 分钟取一次活性指纹:
60
+ 所有通道(含 follow-up)遵守共享流程的「委派活性与接管」规则。等待期间 Codex 独占分配的责任文件,主进程可继续不重叠的工作;责任无法隔离时使用独立 worktree。
63
61
 
64
- ```bash
65
- python3 - <<'LIVE'
66
- import glob, json, os, subprocess
67
- root = os.path.realpath(os.getcwd())
68
- rows = []
69
- for f in glob.glob(os.path.expanduser('~/.claude/plugins/data/codex-openai-codex/state/*/state.json')):
70
- for j in json.load(open(f)).get('jobs', []):
71
- if os.path.realpath(j.get('workspaceRoot', '')) == root:
72
- rows.append((j.get('createdAt'), j.get('id'), j.get('status'), j.get('phase'), f))
73
- if not rows:
74
- raise SystemExit('no job for this workspace')
75
- created, jid, status, phase, statefile = sorted(rows)[-1]
76
- jf = os.path.join(os.path.dirname(statefile), 'jobs', jid + '.json')
77
- size, mtime = (os.path.getsize(jf), int(os.path.getmtime(jf))) if os.path.exists(jf) else (0, 0)
78
- dirty = len([l for l in subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True).stdout.splitlines() if l.strip()])
79
- print(f'{jid} status={status} phase={phase} out={size}/{mtime} dirty={dirty}')
80
- LIVE
81
- ```
62
+ 派发后保存本次 job/agent ID、worktree 和日志路径;上方按仓库列举 job 的脚本只用于发现候选,结合派发时间与任务信息确认后固定 ID,后续不得自动改查最新 job。保存派发基线,约每 2 分钟比较:
63
+
64
+ - 本次 job 的新增日志、工具事件、输出正文,以及 `status` / `phase` 的实际变化。状态文件 mtime 刷新但内容未变不算进展。
65
+ - 本次责任文件的内容变化:比较责任文件的 `git diff --binary HEAD -- <paths>` 内容指纹,并记录相关未跟踪文件的内容指纹;删除、新增、同一脏文件的再次修改都应被识别。只读审查不要求有文件修改。
66
+ - 当前子命令与进程状态、权限请求或外部依赖等待。观测失败记为未知,不能当作无活动。
67
+
68
+ 连续多个观测窗口没有进展时,读取日志尾部和子命令状态,向原 agent 查询当前步骤及阻塞点;这个条件只触发排查,不触发停止或换执行者。重复错误、空心跳或无关文件变化不能证明任务在推进。
82
69
 
83
- 连续 3 次(约 6 分钟)三项全不变且仍为 running,按假死阻塞,报告 job id 与 phase。companion 的 `review --wait` / `adversarial-review --wait` 是前台阻塞调用,不走此轮询。
70
+ companion 的 `review --wait` / `adversarial-review --wait` 也通过宿主异步工具启动,保留会话 ID 并读取输出和本次 job;宿主单次等待结束后继续观察同一会话,不给前台命令套固定时长的自动终止器。只有确认失败或排查证实无法推进,才按共享接管规则处理;正常运行但尚未返回不算通道不可用。
84
71
 
85
72
  ## 阶段与报告映射
86
73
 
@@ -89,7 +76,7 @@ LIVE
89
76
  - 阶段 7.3:先按共享流程复用已有审查;复杂度门禁命中且本次交付尚未用过 Codex 独立审查时执行 review(创建 PR 前的审查也计数);需要挑战方案时执行 adversarial review。
90
77
  - reviewer:`Claude Code 主进程自审 / codex native review / codex adversarial review / codex 只读 subagent`。
91
78
 
92
- Codex 返回后主进程必须直读源码和 diff 核验,不采信转述。重改动实现通道不可用时阻塞;条件式独立审查不可用时由主进程完成本轮自审并在报告标明降级。
79
+ Codex 返回后主进程必须直读源码和 diff 核验,不采信转述。重改动实现通道不可用时由主进程接手剩余工作并注明实际执行者;条件式独立审查不可用时由主进程完成本轮自审并在报告标明降级。
93
80
 
94
81
  ## Follow-up
95
82
 
@@ -1,18 +1,26 @@
1
1
  ---
2
2
  name: delegate-cc
3
- description: 仅在用户显式调用 $delegate-cc 或明确要求使用 delegate-cc 技能,或已获用户调用授权的 ask_cc、oo-ship-issue-pr 流程要求委派时使用;不要通过关键词、任务类型或上下文自动触发。
3
+ description: 仅在显式调用 delegate-cc 时使用:委派 Claude Code 执行开发任务或咨询。
4
4
  ---
5
5
 
6
6
  # delegate-cc
7
7
 
8
+ ## 执行边界与优先级
9
+
10
+ 在宿主系统与开发者指令约束内,冲突时依次采用:当前任务的具体要求与已有授权 > 目标项目适用的 AGENTS.md > 本技能及其引用的通用流程。
11
+ 只读检索、本地草稿、可回滚编辑和常规测试可自主推断并继续;从相关文件和直接依赖开始,仅在证据不足或影响跨模块时扩大范围,复用仍有效的验证。
12
+ 仅在不可逆或难以回滚的修改缺少具体授权时,先完成预览、草稿与验证,再说明对象和影响并等待确认;会话中已有授权继续有效。缺少事实时先查证、标注假设并推进独立工作,确实无法确定操作目标时才询问。对外发送消息须有明确授权。
13
+
8
14
  把 Claude Code 当作协作者。模型顺序固定为 Fable 5.1(`claude-fable-5-1`)→ Opus 5(`claude-opus-5`)→ 当前 sub-agent。保留本机现有认证。
9
15
 
16
+ 已显式调用的 `ask_cc` 或 `oo-ship-issue-pr` 需要委派时可进入本技能,沿用原任务范围与授权。
17
+
10
18
  ## 1. 准备交接
11
19
 
12
20
  - **咨询 `consult`**:用户需要答案、决策建议或评审意见。提供问题、相关代码或事实、约束;CLI 无工具权限,只返回建议。
13
21
  - **执行 `task`**:用户要求实现、修复或把开发任务交给 CC。任务书必须包含工作目录、目标、可修改文件或模块、适用的项目指令、验收标准和仓库实际验证命令。明确告知 CC 还有其他协作者,保留他人的改动,只负责分配范围。
14
22
 
15
- 执行前检查 `git status --short` 和已有 diff,记录涉及文件的初始状态及未跟踪文件。分配互不重叠的文件;需要并发改同一文件时,先拆分责任或使用独立 worktree,指定由谁整合。worktree 不会自动携带未提交改动,必须提供任务所需的当前版本。CC 执行期间,主 agent 不同时修改其负责的文件。
23
+ 执行前检查 `git status --short` 和任务涉及文件的 diff,记录这些文件的初始状态及相关未跟踪文件。分配互不重叠的文件;需要并发改同一文件时,先拆分责任或使用独立 worktree,指定由谁整合。worktree 不会自动携带未提交改动,必须提供任务所需的当前版本。CC 执行期间,主 agent 不同时修改其负责的文件。
16
24
 
17
25
  将任务书写入临时 UTF-8 文件,只传必要上下文,排除凭据。完成条件:CC 无需当前对话即可理解目标、权限范围与完成标准。
18
26
 
@@ -23,18 +31,22 @@ description: 仅在用户显式调用 $delegate-cc 或明确要求使用 delegat
23
31
  运行当前 `SKILL.md` 所在目录下的 `scripts/delegate_cc.py`。以下路径适用于 `dx initial` 的默认安装位置;通过其他方式安装时使用实际技能目录:
24
32
 
25
33
  ```bash
26
- # 咨询:默认模式,单个模型默认超时 120 秒。
34
+ # 咨询:默认模式,无活动告警阈值默认 120 秒。
27
35
  python3 "$HOME/.agents/skills/delegate-cc/scripts/delegate_cc.py" --prompt-file /绝对路径/问题.txt
28
36
 
29
- # 执行:必须指定工作目录,单次默认超时 900 秒。
37
+ # 执行:必须指定工作目录,无活动告警阈值默认 900 秒。
30
38
  python3 "$HOME/.agents/skills/delegate-cc/scripts/delegate_cc.py" --mode task --cwd /绝对路径/工作区 --prompt-file /绝对路径/任务.txt
31
39
  ```
32
40
 
33
- 需要 CC 自行运行验证命令时,按本次授权追加 `--allow-command '仓库实际验证命令'`,可重复传入。使用具体命令而非开放全部 Bash;检查命令本身及其调用脚本的作用。现有 Claude Code 权限规则仍生效,`--allow-command` 只是增加允许规则,不是独立沙箱。文件责任范围通过任务书和验收控制,需要隔离时使用 worktree 或已有沙箱。
41
+ 需要 CC 自行运行验证命令时,按本次授权追加 `--allow-command '仓库实际验证命令'`,可重复传入。使用具体命令而非开放全部 Bash;根据命令和已知脚本判断副作用,仅对未知或可能破坏数据的脚本进一步检查。现有 Claude Code 权限规则仍生效,`--allow-command` 只是增加允许规则,不是独立沙箱。文件责任范围通过任务书和验收控制,需要隔离时使用 worktree 或已有沙箱。
34
42
 
35
43
  执行模式保留默认编码系统提示,开放 `Read,Glob,Grep,Edit,Write,Bash`,使用 `acceptEdits`,不能获批的工具请求自动拒绝。两种模式都关闭 hooks、MCP 和技能,避免额外任务链;主 agent 应把所需项目规则及技能要求写入任务书。本技能不使用 `bypassPermissions`,也不扩大任务至提交、推送、部署或对外发送消息。
36
44
 
37
- 通过工具异步运行,至少每 60 秒向用户汇报进度。`--timeout` 可按任务规模调整;超时会终止执行任务的进程组。完成条件:获得 JSON 结果或明确的执行失败,旧进程已结束。
45
+ 通过工具异步运行,至少每 60 秒向用户汇报进度,保留会话 ID。脚本读取流式输出,并比较工作目录中文件的大小和纳秒级修改/变更时间,能识别同一文件持续编辑;stderr 输出 PID、输出字节数、静默时长及观测可用性,不转发原始敏感日志。任务退出后 stdout 仍返回单个 JSON 结果。
46
+
47
+ `--timeout` 现表示无活动告警阈值,保留参数名和默认秒数;原先依赖它限制总运行时间的调用方应迁移为活性监控。达到阈值仅提示疑似停滞,脚本继续运行,不自动杀进程或换模型。输出或文件变化会重置静默计时;文件观测只覆盖 Git 跟踪及未忽略的新文件,忽略目录中的测试产物需由主进程另查。观测不可用不表示无活动,日志字节增长也需排除重复错误和空心跳。
48
+
49
+ 静默告警后主进程检查原会话、日志和子命令,区分推理/测试、权限等待与实际故障;有进展则继续,无进展但证据不足则报告状态并保持任务。诊断证实无法推进时先尝试恢复原任务,必要时显式中断脚本并确认任务进程组已停止,再按下一节接续。宿主单次等待超时只继续读取同一会话,不设置固定总时长的自动终止器。完成条件:获得 JSON 结果或明确的执行失败,旧进程已结束。
38
50
 
39
51
  ## 3. 验收或接续
40
52
 
@@ -43,7 +55,7 @@ python3 "$HOME/.agents/skills/delegate-cc/scripts/delegate_cc.py" --mode task --
43
55
  - `continuation_required`:任务可能已部分执行。先检查现有 diff、测试状态及外部副作用;更新任务书为“保留已完成部分,仅完成剩余工作”。若 `next_model` 有值,以 `--start-model` 传入该值接续;每个模型最多尝试一次。禁止将同一任务从头盲目重放。
44
56
  - `fallback_required`、第二模型也失败或 CLI 无法运行:当前执行者已经是 sub-agent 时直接接手;主 agent 则委派一个继承当前模型的 sub-agent,环境不支持委派时自己接手。提供原任务、已完成改动和剩余工作,明确跳过本技能及 CLI,避免递归。权限拒绝不能靠切换模型绕过。
45
57
 
46
- 咨询模式由脚本自动依次尝试两个模型;执行模式每次只运行一个模型,由主 agent 检查现场后接续。认证、额度、模型不可用和超时都不等于任务完成。
58
+ 咨询模式只有在原调用已退出且失败或结果无效后,脚本才尝试下一个模型;执行模式每次只运行一个模型,由主 agent 检查现场后接续。尚未返回、静默告警和宿主等待超时均不触发降级。认证、额度和模型不可用也不等于任务完成。
47
59
 
48
60
  完成条件:咨询得到实质答案,或执行任务的代码和验证满足验收标准;仍有缺口时明确列出,而非仅转述 CC 的成功声明。
49
61
 
@@ -10,18 +10,62 @@ import shutil
10
10
  import signal
11
11
  import subprocess
12
12
  import sys
13
+ import time
13
14
 
14
15
 
15
16
  MODELS = ("claude-fable-5-1", "claude-opus-5")
16
17
 
17
18
 
19
+ def workspace_activity(cwd):
20
+ """记录文件本身的变化,同一脏文件继续编辑也算活动。"""
21
+ if cwd is None:
22
+ return ()
23
+ try:
24
+ files = subprocess.run(
25
+ ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"],
26
+ cwd=cwd, capture_output=True, timeout=10, check=True,
27
+ ).stdout.split(b"\0")
28
+ rows = []
29
+ for name in sorted(set(files) - {b""}):
30
+ try:
31
+ stat = (Path(cwd) / os.fsdecode(name)).lstat()
32
+ rows.append((name, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns))
33
+ except FileNotFoundError:
34
+ rows.append((name, None))
35
+ return tuple(rows)
36
+ except (OSError, subprocess.SubprocessError):
37
+ return None
38
+
39
+
18
40
  def run_task(command, prompt, timeout, cwd):
19
- """超时或中断时终止整个任务进程组,避免后台写入与接续任务竞争。"""
41
+ """静默阈值只告警;仅显式中断或异常才回收任务进程组。"""
42
+ fingerprint = workspace_activity(cwd)
43
+ last_activity = last_notice = time.monotonic()
44
+ output_size = (0, 0)
20
45
  with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
21
46
  stderr=subprocess.PIPE, text=True, encoding="utf-8",
22
47
  errors="replace", cwd=cwd, start_new_session=True) as process:
23
48
  try:
24
- stdout, stderr = process.communicate(prompt, timeout=timeout)
49
+ pending_input = prompt
50
+ while True:
51
+ try:
52
+ stdout, stderr = process.communicate(pending_input, timeout=min(30, timeout))
53
+ break
54
+ except subprocess.TimeoutExpired as pending:
55
+ pending_input = None
56
+ now = time.monotonic()
57
+ current = workspace_activity(cwd)
58
+ sizes = (len(pending.output or b""), len(pending.stderr or b""))
59
+ if sizes != output_size or (current is not None and current != fingerprint):
60
+ last_activity = now
61
+ fingerprint, output_size = current, sizes
62
+ if now - last_notice >= min(60, timeout):
63
+ idle = now - last_activity
64
+ state = "疑似停滞,请检查日志、子进程或权限等待;保持运行" if idle >= timeout else "继续等待"
65
+ print(f"pid={process.pid} 静默={idle:.0f}s 输出字节={sizes} "
66
+ f"文件观测={'不可用' if current is None else '可用'};{state}",
67
+ file=sys.stderr, flush=True)
68
+ last_notice = now
25
69
  except BaseException:
26
70
  try:
27
71
  os.killpg(process.pid, signal.SIGKILL)
@@ -49,7 +93,8 @@ def ask(prompt, timeout=120, *, mode="consult", cwd=None, start_model=None, allo
49
93
  raise ValueError("模型不在委派链中")
50
94
  print(f"正在调用 {model}({mode})…", file=sys.stderr, flush=True)
51
95
  command = [
52
- cli, "--print", "--model", model, "--output-format", "json",
96
+ cli, "--print", "--model", model, "--output-format", "stream-json",
97
+ "--verbose", "--include-partial-messages",
53
98
  "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
54
99
  "--disable-slash-commands", "--no-session-persistence",
55
100
  "--settings", '{"disableAllHooks":true}',
@@ -69,18 +114,16 @@ def ask(prompt, timeout=120, *, mode="consult", cwd=None, start_model=None, allo
69
114
  if allow_commands:
70
115
  command += ["--allowedTools", *[f"Bash({item})" for item in allow_commands]]
71
116
  try:
72
- process = run_task(command, prompt, timeout, cwd) if mode == "task" else subprocess.run(
73
- command, input=prompt, text=True, encoding="utf-8",
74
- errors="replace", capture_output=True, timeout=timeout, cwd=cwd,
75
- )
76
- except subprocess.TimeoutExpired:
77
- attempts.append({"requested_model": model, "reason": "调用超时"})
78
- continue
117
+ process = run_task(command, prompt, timeout, cwd)
79
118
  except OSError as error:
80
119
  attempts.append({"requested_model": model, "reason": f"无法运行 CLI:{type(error).__name__}"})
81
120
  break
82
121
  try:
83
- payload = json.loads(process.stdout)
122
+ payload = {}
123
+ for line in process.stdout.splitlines():
124
+ event = json.loads(line)
125
+ if isinstance(event, dict) and event.get("type") == "result":
126
+ payload = event
84
127
  except (ValueError, TypeError):
85
128
  payload = {}
86
129
  if not isinstance(payload, dict):
@@ -118,7 +161,7 @@ def main(argv=None):
118
161
  parser.add_argument("--cwd", type=Path)
119
162
  parser.add_argument("--start-model", choices=MODELS)
120
163
  parser.add_argument("--allow-command", action="append", default=[])
121
- parser.add_argument("--timeout", type=float)
164
+ parser.add_argument("--timeout", type=float, help="无活动告警阈值(秒),不终止或切换模型")
122
165
  args = parser.parse_args(argv)
123
166
  if args.timeout is None:
124
167
  args.timeout = 900 if args.mode == "task" else 120
@@ -1,7 +1,10 @@
1
1
  """复现 CLI 模型校验失败,覆盖模型顺序及错误分类。"""
2
2
 
3
3
  import json
4
+ import io
5
+ import os
4
6
  import subprocess
7
+ import sys
5
8
  import unittest
6
9
  import tempfile
7
10
  from pathlib import Path
@@ -18,6 +21,44 @@ def result(answer, failed=False):
18
21
 
19
22
 
20
23
  class AskCCTest(unittest.TestCase):
24
+ def test_cli_entrypoints_do_not_switch_models_after_idle_threshold(self):
25
+ skills = Path(__file__).resolve().parents[2]
26
+ entries = [
27
+ (skills / "ask_cc/scripts/ask_cc.py", "consult"),
28
+ (skills / "delegate-cc/scripts/delegate_cc.py", "consult"),
29
+ (skills / "delegate-cc/scripts/delegate_cc.py", "task"),
30
+ ]
31
+ for entry, mode in entries:
32
+ with self.subTest(entry=entry, mode=mode), tempfile.TemporaryDirectory() as tmp:
33
+ root = Path(tmp)
34
+ fake_cli = root / "claude"
35
+ fake_cli.write_text(
36
+ f"#!{sys.executable}\n"
37
+ "import json, os, sys, time\n"
38
+ "from pathlib import Path\n"
39
+ "sys.stdin.read()\n"
40
+ "with Path(os.environ['DELEGATE_TEST_CALLS']).open('a') as log:\n"
41
+ " log.write(sys.argv[sys.argv.index('--model') + 1] + '\\n')\n"
42
+ "time.sleep(0.2)\n"
43
+ "print(json.dumps({'type': 'result', 'subtype': 'success', 'result': 'done'}))\n"
44
+ )
45
+ fake_cli.chmod(0o700)
46
+ prompt = root / "prompt.txt"
47
+ prompt.write_text("test")
48
+ calls = root / "calls.txt"
49
+ env = dict(os.environ, PATH=str(root) + os.pathsep + os.environ.get("PATH", ""),
50
+ DELEGATE_TEST_CALLS=str(calls))
51
+ command = [sys.executable, str(entry), "--prompt-file", str(prompt), "--timeout", "0.05"]
52
+ if mode == "task":
53
+ command += ["--mode", "task", "--cwd", tmp]
54
+ completed = subprocess.run(command, env=env, capture_output=True, text=True, timeout=10)
55
+ self.assertEqual(completed.returncode, 0, completed.stderr)
56
+ response = json.loads(completed.stdout)
57
+ self.assertEqual(response["status"], "review_required" if mode == "task" else "ok")
58
+ self.assertEqual(response["attempts"], [])
59
+ self.assertEqual(calls.read_text().splitlines(), [ask_cc.MODELS[0]])
60
+ self.assertIn("疑似停滞", completed.stderr)
61
+
21
62
  @patch.object(ask_cc.shutil, "which", return_value="/fake/claude")
22
63
  def test_task_permissions_and_review(self, _which):
23
64
  with tempfile.TemporaryDirectory() as cwd, patch.object(ask_cc, "run_task", return_value=result("完成")) as run:
@@ -33,7 +74,7 @@ class AskCCTest(unittest.TestCase):
33
74
  with tempfile.TemporaryDirectory() as cwd:
34
75
  def partial(*args):
35
76
  Path(cwd, "partial.txt").write_text("已完成部分")
36
- raise subprocess.TimeoutExpired("claude", 1)
77
+ raise OSError("CLI connection failed")
37
78
  with patch.object(ask_cc, "run_task", side_effect=partial) as run:
38
79
  response = ask_cc.ask("修改代码", mode="task", cwd=cwd)
39
80
  self.assertEqual(run.call_count, 1)
@@ -56,7 +97,7 @@ class AskCCTest(unittest.TestCase):
56
97
  def test_supported_model_ids_and_fallback(self, _which):
57
98
  seen = []
58
99
 
59
- def cli(command, **kwargs):
100
+ def cli(command, *args):
60
101
  model = command[command.index("--model") + 1]
61
102
  seen.append(model)
62
103
  if model == "claude-fable-5-1":
@@ -65,7 +106,7 @@ class AskCCTest(unittest.TestCase):
65
106
  return result("OK")
66
107
  return result(f"There's an issue with the selected model ({model}).", True)
67
108
 
68
- with patch.object(ask_cc.subprocess, "run", side_effect=cli):
109
+ with patch.object(ask_cc, "run_task", side_effect=cli):
69
110
  response = ask_cc.ask("仅回答 OK")
70
111
  self.assertEqual(response["status"], "ok")
71
112
  self.assertEqual(seen, ["claude-fable-5-1", "claude-opus-5"])
@@ -74,17 +115,54 @@ class AskCCTest(unittest.TestCase):
74
115
  @patch.object(ask_cc.shutil, "which", return_value="/fake/claude")
75
116
  def test_unrecognized_model_is_reported(self, _which):
76
117
  failure = result("There's an issue with the selected model (fable-5.1). It may not exist or you may not have access to it.", True)
77
- with patch.object(ask_cc.subprocess, "run", return_value=failure):
118
+ with patch.object(ask_cc, "run_task", return_value=failure):
78
119
  response = ask_cc.ask("问题")
79
120
  self.assertEqual(response["status"], "fallback_required")
80
121
  self.assertEqual(response["attempts"][0]["reason"], "模型名称无效或当前账户无权访问")
81
122
 
82
123
  @patch.object(ask_cc.shutil, "which", return_value="/fake/claude")
83
124
  def test_login_error_is_distinct(self, _which):
84
- with patch.object(ask_cc.subprocess, "run", return_value=result("Not logged in · Please run /login", True)):
125
+ with patch.object(ask_cc, "run_task", return_value=result("Not logged in · Please run /login", True)):
85
126
  response = ask_cc.ask("问题")
86
127
  self.assertEqual(response["attempts"][0]["reason"], "认证不可用")
87
128
 
129
+ def test_silent_process_survives_idle_warning(self):
130
+ command = [sys.executable, "-c", "import time; time.sleep(0.2); print('done')"]
131
+ with patch.object(ask_cc.sys, "stderr", new_callable=io.StringIO) as log:
132
+ completed = ask_cc.run_task(command, "", 0.05, None)
133
+ self.assertEqual(completed.returncode, 0)
134
+ self.assertEqual(completed.stdout.strip(), "done")
135
+ self.assertIn("疑似停滞", log.getvalue())
136
+
137
+ def test_streaming_process_survives_total_duration(self):
138
+ command = [sys.executable, "-u", "-c",
139
+ "import time\nfor i in range(8):\n print(i)\n time.sleep(0.04)"]
140
+ with patch.object(ask_cc.sys, "stderr", new_callable=io.StringIO):
141
+ completed = ask_cc.run_task(command, "", 0.1, None)
142
+ self.assertEqual(completed.returncode, 0)
143
+ self.assertEqual(completed.stdout.splitlines(), [str(i) for i in range(8)])
144
+
145
+ def test_repeated_same_size_file_edit_changes_activity(self):
146
+ with tempfile.TemporaryDirectory() as cwd:
147
+ subprocess.run(["git", "init", "-q", cwd], check=True)
148
+ file = Path(cwd, "work.txt")
149
+ file.write_text("before")
150
+ before = ask_cc.workspace_activity(cwd)
151
+ file.write_text("after!")
152
+ self.assertNotEqual(before, ask_cc.workspace_activity(cwd))
153
+ before = ask_cc.workspace_activity(cwd)
154
+ file.unlink()
155
+ self.assertNotEqual(before, ask_cc.workspace_activity(cwd))
156
+
157
+ @patch.object(ask_cc.shutil, "which", return_value="/fake/claude")
158
+ def test_stream_events_resolve_final_result(self, _which):
159
+ output = result("完成")
160
+ output.stdout = json.dumps({"type": "assistant", "message": "working"}) + "\n" + output.stdout
161
+ with patch.object(ask_cc, "run_task", return_value=output) as run:
162
+ response = ask_cc.ask("问题")
163
+ self.assertEqual(response["answer"], "完成")
164
+ self.assertIn("stream-json", run.call_args.args[0])
165
+
88
166
 
89
167
  if __name__ == "__main__":
90
168
  unittest.main()