@weotro/dx 0.1.10 → 0.1.12
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/agent-references/ship-issue-pr-core.md +46 -12
- package/package.json +1 -1
- package/skills/ask_cc/SKILL.md +4 -0
- package/skills/cc-ship-issue-pr/references/runtime.md +11 -22
- package/skills/delegate-cc/SKILL.md +8 -4
- package/skills/delegate-cc/scripts/__pycache__/delegate_cc.cpython-314.pyc +0 -0
- package/skills/delegate-cc/scripts/__pycache__/test_delegate_cc.cpython-314.pyc +0 -0
- package/skills/delegate-cc/scripts/delegate_cc.py +55 -12
- package/skills/delegate-cc/scripts/test_delegate_cc.py +83 -5
- package/skills/oo-ship-issue-pr/references/runtime.md +4 -2
|
@@ -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
|
每次派发外部专家前先过这道门禁。命中任一信号即**重改动**,走外部专家;一个都不命中即**轻改动**,主进程直接做。派发往返成本高于轻改动直改,门禁结果和命中/未命中的信号编号必须记入审查报告。
|
|
@@ -45,7 +54,7 @@
|
|
|
45
54
|
| 模板 | Issue/PR 正文结构一律取自 `.github/`,禁止自造结构 |
|
|
46
55
|
| Heredoc | 多行 Issue body、commit message、PR body、PR comment 必须用 heredoc |
|
|
47
56
|
| 暂存 | 按明确路径 `git add <path>`,禁止 `git add -A` / `git add .` |
|
|
48
|
-
| 验证 | 发 PR
|
|
57
|
+
| 验证 | 发 PR 前必须有覆盖当前版本的相关验证记录;按下方证据复用规则补跑失效或缺失项 |
|
|
49
58
|
| 正文质量 | 模板占位注释、`TODO`、`TBD`、「稍后补充」残留一律阻塞 |
|
|
50
59
|
| 合并 | 只能 `gh pr merge --squash --auto`,禁止 `--admin` |
|
|
51
60
|
| 真完成 | 必须轮询到 `mergedAt` 非空,并回访关联 Issue 状态 |
|
|
@@ -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
|
-
|
|
114
|
+
整段使用入口技能声明的后台执行方式运行。记录持锁任务与进程身份;等待过久时检查持锁者日志和进程状态。只有确认持锁者及其测试子进程已结束才清理遗留锁,不能仅凭锁目录 mtime 判死或抢占。
|
|
106
115
|
|
|
107
116
|
同时在跑的 follow-up subagent 最多 3 个。超出的由主进程记在待派清单里,等某条线的完成通知到达后再派下一个——排队没有别的执行者,主进程不记就等于丢了。
|
|
108
117
|
|
|
@@ -136,7 +145,7 @@ gh pr status
|
|
|
136
145
|
| 当前分支已有 OPEN PR | 阶段七 | 上一行全部,外加阶段六的 PR body 读回校验与依赖冲突检查 |
|
|
137
146
|
| 工作树干净、无分支差异、无 OPEN PR | 输出「无需交付」并结束 | — |
|
|
138
147
|
|
|
139
|
-
|
|
148
|
+
快捷入口只省掉已经做过的动作,不豁免任何门禁。先收集本次任务已有的验收、审查和验证记录,按下方证据复用规则核对。补齐前禁止进入目标阶段:没有 Issue ID 就先建 Issue,分支不合规就先改分支,验证缺失或失效就补跑,依赖 PR 未真合并就停下。
|
|
140
149
|
|
|
141
150
|
阻塞条件:不在 Git 仓库;`gh auth status` 不可用;当前分支为 `main` 且无法创建新分支。
|
|
142
151
|
|
|
@@ -236,9 +245,30 @@ git diff --stat
|
|
|
236
245
|
|
|
237
246
|
改动范围超出 Issue 目标、或引入未经说明的依赖时,主进程收敛回范围内,不带进提交。
|
|
238
247
|
|
|
248
|
+
## 验收、审查与验证证据复用
|
|
249
|
+
|
|
250
|
+
证据绑定代码和输入,不绑定「PR 创建前/后」或阶段编号。默认实现后做最低充分验证、提交并创建 PR,再在阶段七完成首次正式审查;进入本流程前已有的有效审查可以直接承接,无需为了发 PR 额外增加一轮。单纯 commit、push、创建 PR、更新正文或进入合并阶段不触发重审或重跑。
|
|
251
|
+
|
|
252
|
+
每次完成验收、审查或权威验证时记录以下信息;先保存在本次任务记录中,创建 PR 后将来源与复用结论写入审查报告:
|
|
253
|
+
|
|
254
|
+
- **版本与范围**:仓库、目标 base 分支及其 SHA、merge-base、被检查的 HEAD 与 tree SHA、覆盖的 diff/文件和 Issue 验收标准。未提交改动用包含相关新增文件的内容快照标识;提交后核对实际内容一致,再关联 commit,不能只记录当时的 HEAD。
|
|
255
|
+
- **审查证据**:实际 reviewer、是否独立于实现/修复、报告来源、findings 及处理状态。实现者自检、方案咨询和一句「已审查」不能代替独立审查报告。
|
|
256
|
+
- **验证证据**:实际执行者、命令与参数、结果及日志来源、影响结果的环境/工具链/依赖和配置标识(不记录密钥)。验证过程中格式化、代码生成或钩子改了输入时,记录最终版本并补跑受影响项。
|
|
257
|
+
|
|
258
|
+
复用前由主进程确认来源可读、范围充分、结论仍适用。主进程先前亲自执行并读取输出的验证可以复用;外部执行者的结果仍按入口运行时契约处理。混有无关工作树改动时,必须确认它们没有影响被验证的提交内容,否则在隔离的 worktree 验证目标版本。
|
|
259
|
+
|
|
260
|
+
| 当前状态 | 处理 |
|
|
261
|
+
|---|---|
|
|
262
|
+
| 内容、base、验收范围及相关验证输入均未变,证据完整 | 直接复用,记录来源;已有完整审查无需再次通读同一 diff |
|
|
263
|
+
| 只有 commit SHA 变化,tree、base 和其他输入一致 | 核对内容后将原证据关联新 HEAD,无需重跑 |
|
|
264
|
+
| 有新增修改、修复、验收标准变化,或 base/依赖/配置/环境变化 | 比较前后差异,复查增量及其影响的调用方、不变量和验收项;只重跑受影响的验证命令,保留未受影响证据并记录理由 |
|
|
265
|
+
| 缺少记录、无法确认版本一致或无法界定影响范围 | 补做缺失检查;无法界定影响时审查完整当前 diff,并执行阶段五适用的验证矩阵 |
|
|
266
|
+
|
|
267
|
+
审查与验证分别判断是否可复用;测试通过不能代替代码审查。当前 diff 仍须过复杂度门禁:重改动只有自审记录时,仍需补齐 7.3 的条件式独立审查或按入口契约记录降级,才算审查完整。历史失败和未处理 findings 继续保留,不能因复用而改记为通过。PR 前已完成的独立外部审查计入同一交付的「每 PR 最多一次」额度;历史额度与当前覆盖范围分别记录,后续新增改动由主进程补审。
|
|
268
|
+
|
|
239
269
|
## 阶段五:验证与提交
|
|
240
270
|
|
|
241
|
-
|
|
271
|
+
按改动范围确定最低充分验证,复用有效记录,仅执行缺失或失效项:
|
|
242
272
|
|
|
243
273
|
| 改动范围 | 必跑命令 |
|
|
244
274
|
|---|---|
|
|
@@ -277,6 +307,8 @@ MSG
|
|
|
277
307
|
|
|
278
308
|
标题必须是 Conventional Commit。既有问题的顺手修复单独用 `chore(precheck):` 提交,与本次功能改动分开。
|
|
279
309
|
|
|
310
|
+
提交后核对 commit 内容与验证快照一致;提交钩子或暂存范围造成内容差异时,按证据复用规则补验,再进入阶段六。
|
|
311
|
+
|
|
280
312
|
## 阶段六:PR 创建或更新
|
|
281
313
|
|
|
282
314
|
```bash
|
|
@@ -334,7 +366,7 @@ git merge --no-commit --no-ff origin/main
|
|
|
334
366
|
|
|
335
367
|
## 阶段七:审查修复循环
|
|
336
368
|
|
|
337
|
-
最多 3
|
|
369
|
+
最多 3 轮实际审查修复。首次进入先核对已有证据,每轮只补齐 Issue 验收、验证流水线、代码审查和逐项修复中的缺失或失效项。证据全部有效且满足终止条件时,发布复用报告后直接进入阶段八;复用不算新一轮审查。
|
|
338
370
|
|
|
339
371
|
### 7.1 Issue 验收(主进程)
|
|
340
372
|
|
|
@@ -343,7 +375,7 @@ gh issue view <ISSUE_ID> --json title,body,state
|
|
|
343
375
|
gh pr diff <PR_NUMBER>
|
|
344
376
|
```
|
|
345
377
|
|
|
346
|
-
|
|
378
|
+
先确认 Issue 标准及 PR diff 相对已有记录的变化。已有证据覆盖的验收项直接引用;对新增、变化或缺证据的标准逐条核对:
|
|
347
379
|
|
|
348
380
|
| 状态 | 后续 |
|
|
349
381
|
|---|---|
|
|
@@ -354,16 +386,16 @@ gh pr diff <PR_NUMBER>
|
|
|
354
386
|
|
|
355
387
|
### 7.2 验证流水线(主进程)
|
|
356
388
|
|
|
357
|
-
|
|
389
|
+
按证据复用规则补跑阶段五中缺失或受本轮变化影响的命令;阶段五或上一轮修复后已验证同一输入时直接引用结果。失败必须保留完整错误:文件、行号、命令、失败摘要。
|
|
358
390
|
|
|
359
391
|
### 7.3 代码审查
|
|
360
392
|
|
|
361
|
-
|
|
393
|
+
主进程先核对已有审查的版本、范围、reviewer 和问题处理状态。没有有效审查时通读 `gh pr diff` 全部改动;已有完整有效审查时直接复用,有后续修改时只审增量及受影响上下文。无法界定影响范围时恢复完整审查。核查 diff 涉及文件内的真实 bug,不主动扩大到无关模块。
|
|
362
394
|
|
|
363
395
|
外部专家独立审查是条件式补充,同时满足两条才派发:
|
|
364
396
|
|
|
365
397
|
1. 实际 diff 命中**复杂度门禁**任一信号(重改动)。
|
|
366
|
-
2.
|
|
398
|
+
2. 本次交付尚未用过外部专家审查——同一 PR 最多一次,含创建 PR 前对本次交付的独立审查;派发前查任务记录与本 PR 已有报告的 reviewer、独立性和覆盖范围,确认已有独立外部审查即只由主进程补审未覆盖的改动。轻改动 PR 的这次额度可以留到后续 diff 扩大首次命中信号时再用。
|
|
367
399
|
|
|
368
400
|
使用入口技能声明的 review 通道。需要挑战实现方案、状态机、不变量或安全边界时改用 adversarial review;需要对照 Issue 验收标准或排除已确认问题时,把定制上下文一并传入。
|
|
369
401
|
|
|
@@ -388,6 +420,8 @@ gh pr comment <PR_NUMBER> --body-file - <<'MSG'
|
|
|
388
420
|
- Critical:0 / Major:0 / Minor:0
|
|
389
421
|
- reviewer:主进程自审 / 外部专家 review / 外部专家 adversarial review
|
|
390
422
|
- 复杂度门禁:轻改动(未命中信号)/ 重改动(命中信号 N);外部专家额度:未用 / 本轮已用 / 此前已用
|
|
423
|
+
- 证据:base / merge-base / HEAD / tree;验收、审查与验证各自的原记录来源和覆盖范围
|
|
424
|
+
- 本次处理:复用 / 增量补查 / 首次完整检查;变化与失效原因(如有);复用报告沿用原轮次
|
|
391
425
|
|
|
392
426
|
### 验证流水线
|
|
393
427
|
|
|
@@ -413,7 +447,7 @@ MSG
|
|
|
413
447
|
|
|
414
448
|
修复默认主进程直接改;只有单条修复自身命中**复杂度门禁**信号(判定的是这次修复的改动面,不是原问题的严重级)才派外部专家,规则同阶段四。
|
|
415
449
|
|
|
416
|
-
|
|
450
|
+
修复后复查问题与受影响上下文,重跑受影响的验证并更新证据,再发布修复报告评论,列出已修复项及其 commit、拒绝项及理由、follow-up 项及 Issue 编号。下一轮直接承接这些证据。
|
|
417
451
|
|
|
418
452
|
### 循环终止
|
|
419
453
|
|
|
@@ -423,10 +457,10 @@ MSG
|
|
|
423
457
|
|
|
424
458
|
## 阶段八:合并
|
|
425
459
|
|
|
426
|
-
|
|
460
|
+
合并前读取 PR 的最新 `headRefOid`、base 并刷新目标分支,核对远端 head 与本地已检查提交一致、相关工作树输入与证据一致。若有变化,按证据复用规则回到阶段七补齐;全部有效时直接发布验证总结评论,引用已完成的实际命令与证据来源、审查轮数和问题统计。验证失败时禁止写「可合并」。
|
|
427
461
|
|
|
428
462
|
```bash
|
|
429
|
-
gh pr merge <PR_NUMBER> --squash --auto
|
|
463
|
+
gh pr merge <PR_NUMBER> --squash --auto --match-head-commit <已核对的HEAD_SHA>
|
|
430
464
|
gh pr view <PR_NUMBER> --json state,mergedAt,url
|
|
431
465
|
```
|
|
432
466
|
|
package/package.json
CHANGED
package/skills/ask_cc/SKILL.md
CHANGED
|
@@ -7,10 +7,14 @@ description: 仅在用户显式调用 $ask_cc 或明确要求使用 ask_cc 技
|
|
|
7
7
|
|
|
8
8
|
使用 [delegate-cc](../delegate-cc/SKILL.md) 的咨询流程获取答案或第二意见,默认不授予工具权限。
|
|
9
9
|
|
|
10
|
+
调用前完整读取该技能,按其中的活性监控、诊断和接续规则执行。咨询没有文件修改是正常现象,以本次会话的流式输出和进程状态辅助判断;静默只触发排查,不能仅因尚无答案就停止或换模型。
|
|
11
|
+
|
|
10
12
|
若用户明确要求修改代码或执行开发任务,转入该技能的 `task` 模式,按任务范围完成实现和验收。
|
|
11
13
|
|
|
12
14
|
旧命令 `scripts/ask_cc.py --prompt-file /绝对路径/问题.txt` 继续可用,仅运行咨询模式;模型顺序和降级逻辑复用 `delegate-cc`。
|
|
13
15
|
|
|
16
|
+
旧入口的 `--timeout` 同样表示无活动告警阈值(默认 120 秒),不是答复截止时间。通过宿主异步工具保留原会话并持续取结果;只有原调用已退出且结果无效或失败时,脚本才尝试下一个模型。
|
|
17
|
+
|
|
14
18
|
`dx initial` 会同时安装这两个技能。使用当前 `SKILL.md` 所在目录下的脚本;默认安装后的调用方式:
|
|
15
19
|
|
|
16
20
|
```bash
|
|
@@ -57,36 +57,25 @@ PY
|
|
|
57
57
|
|
|
58
58
|
定位到 job 后读取同目录 `jobs/<id>.json`。只有 `status=completed` 且 `phase=done` 才算返回,报告正文取 `result.rawOutput`。
|
|
59
59
|
|
|
60
|
-
##
|
|
60
|
+
## 等待与活性排查
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
所有通道(含 follow-up)遵守共享流程的「委派活性与接管」规则。等待期间工作树由 Codex 独占,主进程只做不写工作树的并行事项。
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
```
|
|
64
|
+
派发后保存本次 job/agent ID、worktree 和日志路径;上方按仓库列举 job 的脚本只用于发现候选,结合派发时间与任务信息确认后固定 ID,后续不得自动改查最新 job。保存派发基线,约每 2 分钟比较:
|
|
65
|
+
|
|
66
|
+
- 本次 job 的新增日志、工具事件、输出正文,以及 `status` / `phase` 的实际变化。状态文件 mtime 刷新但内容未变不算进展。
|
|
67
|
+
- 本次责任文件的内容变化:比较 `git diff --binary HEAD` 的内容指纹,并记录相关未跟踪文件的内容指纹;删除、新增、同一脏文件的再次修改都应被识别。只读审查不要求有文件修改。
|
|
68
|
+
- 当前子命令与进程状态、权限请求或外部依赖等待。观测失败记为未知,不能当作无活动。
|
|
69
|
+
|
|
70
|
+
连续多个观测窗口没有进展时,读取日志尾部和子命令状态,向原 agent 查询当前步骤及阻塞点;这个条件只触发排查,不触发停止或换执行者。重复错误、空心跳或无关文件变化不能证明任务在推进。
|
|
82
71
|
|
|
83
|
-
|
|
72
|
+
companion 的 `review --wait` / `adversarial-review --wait` 也通过宿主异步工具启动,保留会话 ID 并读取输出和本次 job;宿主单次等待结束后继续观察同一会话,不给前台命令套固定时长的自动终止器。只有确认失败或排查证实无法推进,才按共享接管规则处理;正常运行但尚未返回不算通道不可用。
|
|
84
73
|
|
|
85
74
|
## 阶段与报告映射
|
|
86
75
|
|
|
87
76
|
- 阶段四方案讨论:只读 Codex agent;主进程先写初步方案,最终拍板权在主进程。
|
|
88
77
|
- 阶段四实现与阶段 7.4 重修复:可写 Codex agent,prompt 要求不 commit、不 push、不碰 GitHub、不递归委派。
|
|
89
|
-
- 阶段 7.3
|
|
78
|
+
- 阶段 7.3:先按共享流程复用已有审查;复杂度门禁命中且本次交付尚未用过 Codex 独立审查时执行 review(创建 PR 前的审查也计数);需要挑战方案时执行 adversarial review。
|
|
90
79
|
- reviewer:`Claude Code 主进程自审 / codex native review / codex adversarial review / codex 只读 subagent`。
|
|
91
80
|
|
|
92
81
|
Codex 返回后主进程必须直读源码和 diff 核验,不采信转述。重改动实现通道不可用时阻塞;条件式独立审查不可用时由主进程完成本轮自审并在报告标明降级。
|
|
@@ -23,10 +23,10 @@ description: 仅在用户显式调用 $delegate-cc 或明确要求使用 delegat
|
|
|
23
23
|
运行当前 `SKILL.md` 所在目录下的 `scripts/delegate_cc.py`。以下路径适用于 `dx initial` 的默认安装位置;通过其他方式安装时使用实际技能目录:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
#
|
|
26
|
+
# 咨询:默认模式,无活动告警阈值默认 120 秒。
|
|
27
27
|
python3 "$HOME/.agents/skills/delegate-cc/scripts/delegate_cc.py" --prompt-file /绝对路径/问题.txt
|
|
28
28
|
|
|
29
|
-
#
|
|
29
|
+
# 执行:必须指定工作目录,无活动告警阈值默认 900 秒。
|
|
30
30
|
python3 "$HOME/.agents/skills/delegate-cc/scripts/delegate_cc.py" --mode task --cwd /绝对路径/工作区 --prompt-file /绝对路径/任务.txt
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -34,7 +34,11 @@ python3 "$HOME/.agents/skills/delegate-cc/scripts/delegate_cc.py" --mode task --
|
|
|
34
34
|
|
|
35
35
|
执行模式保留默认编码系统提示,开放 `Read,Glob,Grep,Edit,Write,Bash`,使用 `acceptEdits`,不能获批的工具请求自动拒绝。两种模式都关闭 hooks、MCP 和技能,避免额外任务链;主 agent 应把所需项目规则及技能要求写入任务书。本技能不使用 `bypassPermissions`,也不扩大任务至提交、推送、部署或对外发送消息。
|
|
36
36
|
|
|
37
|
-
通过工具异步运行,至少每 60
|
|
37
|
+
通过工具异步运行,至少每 60 秒向用户汇报进度,保留会话 ID。脚本读取流式输出,并比较工作目录中文件的大小和纳秒级修改/变更时间,能识别同一文件持续编辑;stderr 输出 PID、输出字节数、静默时长及观测可用性,不转发原始敏感日志。任务退出后 stdout 仍返回单个 JSON 结果。
|
|
38
|
+
|
|
39
|
+
`--timeout` 现表示无活动告警阈值,保留参数名和默认秒数;原先依赖它限制总运行时间的调用方应迁移为活性监控。达到阈值仅提示疑似停滞,脚本继续运行,不自动杀进程或换模型。输出或文件变化会重置静默计时;文件观测只覆盖 Git 跟踪及未忽略的新文件,忽略目录中的测试产物需由主进程另查。观测不可用不表示无活动,日志字节增长也需排除重复错误和空心跳。
|
|
40
|
+
|
|
41
|
+
静默告警后主进程检查原会话、日志和子命令,区分推理/测试、权限等待与实际故障;有进展则继续,无进展但证据不足则报告状态并保持任务。诊断证实无法推进时先尝试恢复原任务,必要时显式中断脚本并确认任务进程组已停止,再按下一节接续。宿主单次等待超时只继续读取同一会话,不设置固定总时长的自动终止器。完成条件:获得 JSON 结果或明确的执行失败,旧进程已结束。
|
|
38
42
|
|
|
39
43
|
## 3. 验收或接续
|
|
40
44
|
|
|
@@ -43,7 +47,7 @@ python3 "$HOME/.agents/skills/delegate-cc/scripts/delegate_cc.py" --mode task --
|
|
|
43
47
|
- `continuation_required`:任务可能已部分执行。先检查现有 diff、测试状态及外部副作用;更新任务书为“保留已完成部分,仅完成剩余工作”。若 `next_model` 有值,以 `--start-model` 传入该值接续;每个模型最多尝试一次。禁止将同一任务从头盲目重放。
|
|
44
48
|
- `fallback_required`、第二模型也失败或 CLI 无法运行:当前执行者已经是 sub-agent 时直接接手;主 agent 则委派一个继承当前模型的 sub-agent,环境不支持委派时自己接手。提供原任务、已完成改动和剩余工作,明确跳过本技能及 CLI,避免递归。权限拒绝不能靠切换模型绕过。
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
咨询模式只有在原调用已退出且失败或结果无效后,脚本才尝试下一个模型;执行模式每次只运行一个模型,由主 agent 检查现场后接续。尚未返回、静默告警和宿主等待超时均不触发降级。认证、额度和模型不可用也不等于任务完成。
|
|
47
51
|
|
|
48
52
|
完成条件:咨询得到实质答案,或执行任务的代码和验证满足验收标准;仍有缺口时明确列出,而非仅转述 CC 的成功声明。
|
|
49
53
|
|
|
@@ -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
|
-
|
|
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)
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
本入口只补充交付上下文与验收要求。CLI 参数、模型顺序、权限配置、等待与超时、状态解析、接续及降级统一按依赖技能执行,不在这里另写调用脚本或重试链。依赖文件缺失时先运行 `dx initial` 恢复;仍无法读取时报告缺失路径,不临时仿写该技能。
|
|
17
17
|
|
|
18
|
+
所有通道(含 follow-up)遵守共享流程的「委派活性与接管」规则。依赖脚本的 `--timeout` 是无活动告警阈值;读取其 PID、输出字节和文件观测摘要,疑似停滞时检查原会话与工作树。宿主单次等待结束后继续观察同一会话,不设置按总耗时自动终止的外层超时器,也不把尚未返回当作降级依据。
|
|
19
|
+
|
|
18
20
|
## 交接上下文
|
|
19
21
|
|
|
20
22
|
每次交接包含当前 worktree 绝对路径、阶段、Issue 原文与逐条验收标准、base,以及该阶段所需的方案、代码和验证证据。任务仅覆盖当前阶段,不运行完整 `oo-ship-issue-pr`;Git 历史、GitHub 操作、权威验证和最终事实核验由 Codex 主进程负责。
|
|
@@ -37,9 +39,9 @@
|
|
|
37
39
|
|
|
38
40
|
取得结果、等待停止、处理部分改动、接续或降级均按依赖技能执行。主进程取得最终结果后直读源码和 diff,核验结论、合并重复项并记录拒绝依据;仅派发成功或返回成功声明不算阶段完成。
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
外部执行者的验证仅作参考。交接材料附本阶段需补跑的清单,并要求报告未运行的验证及原因;权威验证由 Codex 主进程执行并阅读输出。主进程已有验证按共享流程的证据复用规则承接,后续阶段只补跑缺失或失效项。
|
|
41
43
|
|
|
42
|
-
阶段 7.3
|
|
44
|
+
阶段 7.3 仍服从共享流程的证据复用、复杂度门禁和每 PR 最多一次外部专家审查约束。创建 PR 前已有的独立外部审查也继续计数并按覆盖范围复用;同一阶段因证据不足补充材料或按依赖技能降级,不能被当作后续轮次重新派发审查的额度。
|
|
43
45
|
|
|
44
46
|
reviewer 按实际执行者记录为 `Codex 主进程自审`、`Claude Code(ask_cc)review/adversarial review` 或实际降级执行者,并记录依赖技能返回的请求模型及降级情况。实现与修复记录使用 `delegate-cc task` 及实际执行者;自身接管的审查记为自审,不计入外部专家审查。
|
|
45
47
|
|