@weotro/dx 0.1.11 → 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 +10 -1
- package/package.json +1 -1
- package/skills/ask_cc/SKILL.md +4 -0
- package/skills/cc-ship-issue-pr/references/runtime.md +10 -21
- 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 +2 -0
|
@@ -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
|
-
|
|
114
|
+
整段使用入口技能声明的后台执行方式运行。记录持锁任务与进程身份;等待过久时检查持锁者日志和进程状态。只有确认持锁者及其测试子进程已结束才清理遗留锁,不能仅凭锁目录 mtime 判死或抢占。
|
|
106
115
|
|
|
107
116
|
同时在跑的 follow-up subagent 最多 3 个。超出的由主进程记在待派清单里,等某条线的完成通知到达后再派下一个——排队没有别的执行者,主进程不记就等于丢了。
|
|
108
117
|
|
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,30 +57,19 @@ 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
|
|
|
@@ -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 主进程负责。
|