移除 herdr 编排(用户决策:不再使用) #5
@@ -50,7 +50,6 @@
|
|||||||
| 用错误码定位失败 | [错误码目录](system-design/2026-09-28-error-code-catalog.md) |
|
| 用错误码定位失败 | [错误码目录](system-design/2026-09-28-error-code-catalog.md) |
|
||||||
| 测试报告 | [test-reports/](test-reports/) |
|
| 测试报告 | [test-reports/](test-reports/) |
|
||||||
| 历史功能设计 | [archive/design/superpowers/](archive/design/superpowers/) |
|
| 历史功能设计 | [archive/design/superpowers/](archive/design/superpowers/) |
|
||||||
| Herdr 角色、审批与备援 | [ops/2026-09-13-herdr-agent-roster.md](ops/2026-09-13-herdr-agent-roster.md) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -80,3 +80,6 @@ __pycache__/
|
|||||||
.doc/test-reports/assets/**/playwright-output-*/
|
.doc/test-reports/assets/**/playwright-output-*/
|
||||||
.doc/test-reports/assets/**/playwright-report/
|
.doc/test-reports/assets/**/playwright-report/
|
||||||
.doc/test-reports/assets/**/playwright-report-*/
|
.doc/test-reports/assets/**/playwright-report-*/
|
||||||
|
|
||||||
|
# 多仓迁移工作区本地组件克隆(U5 前);gitlink 随实际消费按 KTD3 引入
|
||||||
|
modules/
|
||||||
|
|||||||
@@ -49,6 +49,3 @@
|
|||||||
|
|
||||||
推送模型(2026-09-29 起):dev/test/main 受 Gitea 保护——用户白名单身份可直推;编码 Agent 以 `agent-bot` 身份推送功能分支并建 PR,不直推主干。凭证查找 `${HOME}/.secrets/gitea`(weichen)与 `${HOME}/.secrets/agent-bot`(agent-bot),所需字段为 `ACCESS_TOKEN`。不向用户索要值,不把凭证写入仓库/git config;缺失只记录路径与缺字段。[git-push-dev.sh](./scripts/git-push-dev.sh) 仅适用于用户白名单直推场景。
|
推送模型(2026-09-29 起):dev/test/main 受 Gitea 保护——用户白名单身份可直推;编码 Agent 以 `agent-bot` 身份推送功能分支并建 PR,不直推主干。凭证查找 `${HOME}/.secrets/gitea`(weichen)与 `${HOME}/.secrets/agent-bot`(agent-bot),所需字段为 `ACCESS_TOKEN`。不向用户索要值,不把凭证写入仓库/git config;缺失只记录路径与缺字段。[git-push-dev.sh](./scripts/git-push-dev.sh) 仅适用于用户白名单直推场景。
|
||||||
|
|
||||||
## Herdr 工程协作
|
|
||||||
|
|
||||||
仅在明确使用 Herdr 编排/启动/派发时读 [角色、审批与备援 SSOT](./.doc/ops/2026-09-13-herdr-agent-roster.md);使用 [启动脚本](./scripts/herdr-agent-start.sh) / [派发脚本](./scripts/herdr-agent-dispatch.sh)。此时按 v1.2 资源预算与角色路由执行;普通 Codex/Claude/Cursor 会话不套用该分工。`context_pi` 是协作终端的 PI CLI,不能替代产品 PI Agent Runner 验收。实际客户端/模型/在线状态记录在本次运行证据中。
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
exec python3 -B "${SCRIPT_DIR}/herdr_agents.py" dispatch "$@"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
exec python3 -B "${SCRIPT_DIR}/herdr_agents.py" start "$@"
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Herdr-only routing; local CLI entrypoints do not change global client settings."""
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
import sqlite3
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
from agentctl import Repo, task_row, Denied
|
|
||||||
|
|
||||||
PROFILES = {
|
|
||||||
'claude': ('reviewer_glm', 'claude', ['--dangerously-skip-permissions', '--model', 'opus']),
|
|
||||||
'codex': ('orchestrator_codex', 'codex', ['--ask-for-approval', 'never', '--sandbox', 'danger-full-access']),
|
|
||||||
'cursor': ('implementer_cursor', 'cursor', ['--force', '--sandbox', 'disabled', '--approve-mcps', '--trust']),
|
|
||||||
'antigravity': ('verifier_antigravity', 'agy', ['--dangerously-skip-permissions']),
|
|
||||||
'pi-max': ('qwen_max', 'pi', ['--approve', '--provider', 'qwen-token-plan-cn', '--model', 'qwen3.8-max']),
|
|
||||||
'pi-flash': ('qwen_flash', 'pi', ['--approve', '--provider', 'qwen-token-plan-cn', '--model', 'qwen3.8-flash']),
|
|
||||||
'pi-glm': ('glm_pi', 'pi', ['--approve', '--provider', 'zai-coding-cn', '--model', 'glm-5.3']),
|
|
||||||
'pi': ('context_pi', 'pi', ['--approve', '--provider', 'minimax-cn', '--model', 'MiniMax-M3']),
|
|
||||||
'kimi': ('context_kimi', 'kimi', ['--auto']),
|
|
||||||
}
|
|
||||||
ROUTES = {
|
|
||||||
'supervision': ['qwen_flash', 'qwen_max', 'verifier_antigravity'],
|
|
||||||
'analysis': ['qwen_max', 'qwen_flash'],
|
|
||||||
'planning': ['reviewer_glm', 'qwen_max', 'glm_pi'],
|
|
||||||
'execution': ['reviewer_glm', 'qwen_max'],
|
|
||||||
'implementation': ['implementer_cursor'],
|
|
||||||
'ui': ['implementer_cursor'],
|
|
||||||
'code-review': ['reviewer_glm', 'context_pi'],
|
|
||||||
'test': ['qwen_flash', 'verifier_antigravity'],
|
|
||||||
'test-analysis': ['context_pi', 'reviewer_glm'],
|
|
||||||
'report': ['qwen_max', 'qwen_flash'],
|
|
||||||
'report-review': ['reviewer_glm'],
|
|
||||||
'critical-review': ['orchestrator_codex', 'reviewer_glm'],
|
|
||||||
'report-final-check': ['context_kimi'],
|
|
||||||
'quota-emergency': ['context_kimi'],
|
|
||||||
'fast-check': ['verifier_antigravity', 'qwen_flash'],
|
|
||||||
'long-context': ['qwen_max', 'glm_pi'],
|
|
||||||
'governance': ['reviewer_glm', 'qwen_max'],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def fail(message, code=1):
|
|
||||||
print(json.dumps({'status': 'refused', 'message': message}, ensure_ascii=False), file=sys.stderr)
|
|
||||||
raise SystemExit(code)
|
|
||||||
|
|
||||||
|
|
||||||
def matches(name, base):
|
|
||||||
return bool(re.fullmatch(r'[a-z][a-z0-9_-]{0,31}', name)) and (name == base or name.startswith(base + '__'))
|
|
||||||
|
|
||||||
|
|
||||||
def context(task_id):
|
|
||||||
if os.environ.get('HERDR_ENV') != '1':
|
|
||||||
fail('Not in Herdr; no session inspection or control is allowed')
|
|
||||||
repo = Repo('.')
|
|
||||||
task = task_row(repo, task_id)
|
|
||||||
if str(repo.current) != task['worktree'] or repo.current.parent != repo.main / '.worktrees':
|
|
||||||
fail('Use the registered task worktree')
|
|
||||||
return repo, task
|
|
||||||
|
|
||||||
|
|
||||||
def reserve_kimi(repo, task_id, reason):
|
|
||||||
"""Reserve before sending; uncertain sends stay spent, including across restarts."""
|
|
||||||
if not reason.strip():
|
|
||||||
fail('Kimi requires a bounded final-check or quota-emergency reason')
|
|
||||||
if (repo.root / 'herdr-kimi.disabled').exists():
|
|
||||||
fail('Kimi is reserved for personal use; Herdr Kimi disabled')
|
|
||||||
with repo.db(True) as db:
|
|
||||||
db.execute('CREATE TABLE IF NOT EXISTS herdr_kimi_uses (task TEXT PRIMARY KEY, reason TEXT NOT NULL, created REAL NOT NULL)')
|
|
||||||
try:
|
|
||||||
db.execute('INSERT INTO herdr_kimi_uses VALUES (?,?,?)', (task_id, reason, time.time()))
|
|
||||||
except sqlite3.IntegrityError:
|
|
||||||
fail('Kimi limit reached: at most one dispatch per logical task')
|
|
||||||
|
|
||||||
|
|
||||||
def herdr(*args):
|
|
||||||
return subprocess.run(['herdr', *args], capture_output=True, text=True)
|
|
||||||
|
|
||||||
|
|
||||||
def data(result):
|
|
||||||
if result.returncode:
|
|
||||||
fail(result.stderr or 'Herdr call failed', 75)
|
|
||||||
try:
|
|
||||||
return json.loads(result.stdout)['result']
|
|
||||||
except (ValueError, KeyError, TypeError):
|
|
||||||
fail('Unknown Herdr response; do not repeat an uncertain submission', 75)
|
|
||||||
|
|
||||||
|
|
||||||
def start(args):
|
|
||||||
base, kind, native = PROFILES[args.profile]
|
|
||||||
if not matches(args.name, base):
|
|
||||||
fail('Expected ' + base + ' or ' + base + '__<short-task> (32 chars max)', 2)
|
|
||||||
if not re.fullmatch(r'w[0-9A-Za-z]+:p[0-9A-Za-z]+', args.pane):
|
|
||||||
fail('Invalid explicit pane id', 2)
|
|
||||||
command = ['herdr', 'agent', 'start', args.name, '--kind', kind, '--pane', args.pane, '--', *native]
|
|
||||||
if args.dry_run:
|
|
||||||
print(shlex.join(command)); return
|
|
||||||
if not args.task:
|
|
||||||
fail('Live start requires --task', 2)
|
|
||||||
repo, _ = context(args.task)
|
|
||||||
panes = data(herdr('pane', 'list', '--workspace', args.pane.split(':')[0]))['panes']
|
|
||||||
pane = next((p for p in panes if p.get('pane_id') == args.pane), None)
|
|
||||||
if not pane or Path(pane.get('cwd', '')).resolve() != repo.current:
|
|
||||||
fail('Target pane must use the registered worktree')
|
|
||||||
# Herdr itself checks the shell is available before startup.
|
|
||||||
result = subprocess.run(command)
|
|
||||||
raise SystemExit(result.returncode)
|
|
||||||
|
|
||||||
|
|
||||||
def dispatch(args):
|
|
||||||
route = list(ROUTES[args.lane])
|
|
||||||
if args.implementation_fallback:
|
|
||||||
if args.lane not in ('implementation', 'ui'):
|
|
||||||
fail('Implementation fallback applies only to implementation/ui', 2)
|
|
||||||
route += ['reviewer_glm', 'qwen_max']
|
|
||||||
if args.explain:
|
|
||||||
print('\n'.join(route)); return
|
|
||||||
if not args.task or not args.prompt:
|
|
||||||
fail('Live dispatch requires --task and prompt', 2)
|
|
||||||
repo, _ = context(args.task)
|
|
||||||
if args.lane in ('report-final-check', 'quota-emergency') and not args.kimi_reason:
|
|
||||||
fail('Optional Kimi lane requires --kimi-reason', 2)
|
|
||||||
agents = data(herdr('agent', 'list'))['agents']
|
|
||||||
dispatch_id = uuid.uuid4().hex
|
|
||||||
prefix = 'HERDR_RETRY:' + dispatch_id
|
|
||||||
protocol = ('\n[Herdr v1.2 task=' + args.task + ' lane=' + args.lane + '] '
|
|
||||||
'遵守 Herdr roster 与 agentctl;写入前逐命令取 path 租约,提交另持 git:index;'
|
|
||||||
'禁止在 pane 间复制 supervisor 凭据。规划/执行计划只调度,实施/UI 才写产品代码。'
|
|
||||||
'已授权步骤自主完成,不询问工具批准;确有缺失授权或信息时写 blocker。'
|
|
||||||
'报告 flash 仅初稿,max 定稿再由 GLM 独立复核。测试结论须来自实际命令。'
|
|
||||||
'只有未写文件且未执行副作用命令、因 quota/auth/client 无法开始时,独占一行返回 '
|
|
||||||
+ prefix + ':quota 或 :auth 或 :client;已开始则写 handoff,禁止冒用重试标记。')
|
|
||||||
if args.implementation_fallback:
|
|
||||||
protocol += '\n已登记实施接管理由:' + args.implementation_fallback
|
|
||||||
if args.kimi_reason:
|
|
||||||
protocol += '\n本任务唯一 Kimi 调用范围:' + args.kimi_reason + ';结束后不派生额外 Kimi 调用。'
|
|
||||||
for base in route:
|
|
||||||
candidates = [a for a in agents if matches(a.get('name', ''), base)
|
|
||||||
and a.get('cwd') == str(repo.current) and a.get('agent_status') in ('idle', 'done')]
|
|
||||||
if len(candidates) > 1:
|
|
||||||
fail('Ambiguous same-role agents in task worktree; select one before dispatch', 75)
|
|
||||||
if not candidates:
|
|
||||||
continue
|
|
||||||
target = candidates[0]['name']
|
|
||||||
# Names alone cannot prove model routing; startup receipts must be checked by coordinator.
|
|
||||||
if base == 'context_kimi':
|
|
||||||
reserve_kimi(repo, args.task, args.kimi_reason)
|
|
||||||
print(json.dumps({'lane': args.lane, 'target': target, 'dispatch_id': dispatch_id}), file=sys.stderr)
|
|
||||||
result = herdr('agent', 'prompt', target, args.prompt + protocol, '--wait', '--timeout', str(args.timeout_ms))
|
|
||||||
if result.returncode:
|
|
||||||
# No textual error heuristic: a transport failure can occur after delivery.
|
|
||||||
print(result.stderr or result.stdout, file=sys.stderr)
|
|
||||||
fail('Delivery uncertain; inspect pane/handoff before another dispatch', 75)
|
|
||||||
settled = data(herdr('agent', 'get', target))
|
|
||||||
agent = settled.get('agent', settled)
|
|
||||||
if agent.get('agent_status') not in ('idle', 'done'):
|
|
||||||
fail('Agent not settled; inspect without re-submitting', 75)
|
|
||||||
transcript = herdr('agent', 'read', target, '--source', 'recent-unwrapped', '--lines', '120')
|
|
||||||
if transcript.returncode:
|
|
||||||
fail('Cannot read outcome; no automatic retry', 75)
|
|
||||||
if re.search(r'^' + re.escape(prefix) + r':(quota|auth|client)$', transcript.stdout, re.M):
|
|
||||||
agents = data(herdr('agent', 'list'))['agents']
|
|
||||||
continue
|
|
||||||
print(result.stdout, end=''); return
|
|
||||||
fail('No eligible instance; start a standard agent in this task worktree', 69)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
sub = parser.add_subparsers(dest='action', required=True)
|
|
||||||
s = sub.add_parser('start'); s.add_argument('--dry-run', action='store_true'); s.add_argument('--task')
|
|
||||||
s.add_argument('profile', choices=PROFILES); s.add_argument('name'); s.add_argument('pane')
|
|
||||||
d = sub.add_parser('dispatch'); d.add_argument('--explain', action='store_true'); d.add_argument('--task')
|
|
||||||
d.add_argument('--timeout-ms', type=int, default=60000); d.add_argument('--kimi-reason', default='')
|
|
||||||
d.add_argument('--implementation-fallback', default=''); d.add_argument('lane', choices=ROUTES); d.add_argument('prompt', nargs='?')
|
|
||||||
args = parser.parse_args()
|
|
||||||
if args.action == 'dispatch' and not 1 <= args.timeout_ms <= 60000:
|
|
||||||
fail('Use a bounded 1..60000 ms wait; observe long tasks separately', 2)
|
|
||||||
try:
|
|
||||||
start(args) if args.action == 'start' else dispatch(args)
|
|
||||||
except Denied as exc:
|
|
||||||
fail(exc.code + ': ' + exc.message)
|
|
||||||
except (OSError, KeyError, TypeError, sqlite3.Error) as exc:
|
|
||||||
fail(str(exc), 75)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
"""Real Git/SQLite tasks with a fake Herdr transport; never starts paid agents."""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
import test_agentctl as fixtures
|
|
||||||
|
|
||||||
SCRIPT = Path(__file__).resolve().parents[1] / 'herdr_agents.py'
|
|
||||||
sys.path.insert(0, str(SCRIPT.parent))
|
|
||||||
from herdr_agents import PROFILES, ROUTES
|
|
||||||
|
|
||||||
|
|
||||||
class HerdrTests(unittest.TestCase):
|
|
||||||
git = fixtures.AgentctlTests.git
|
|
||||||
cli = fixtures.AgentctlTests.cli
|
|
||||||
initialized_task = fixtures.AgentctlTests.initialized_task
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
fixtures.AgentctlTests.setUp(self)
|
|
||||||
self.task = self.initialized_task()
|
|
||||||
self.bin = Path(self.temp.name) / 'bin'; self.bin.mkdir()
|
|
||||||
self.log = Path(self.temp.name) / 'calls.jsonl'
|
|
||||||
# Resolve so macOS /var → /private/var matches agentctl Repo.current strings.
|
|
||||||
task_cwd = str(self.task.resolve())
|
|
||||||
self.env = dict(os.environ, HERDR_ENV='1', PATH=str(self.bin)+os.pathsep+os.environ['PATH'],
|
|
||||||
MOCK_LOG=str(self.log), MOCK_CWD=task_cwd, MOCK_MODE='ok')
|
|
||||||
self.env['MOCK_AGENTS'] = json.dumps(['qwen_max__alpha','qwen_flash__alpha','context_kimi__alpha','implementer_cursor__alpha','reviewer_glm__alpha'])
|
|
||||||
fake = self.bin / 'herdr'
|
|
||||||
fake.write_text("#!/usr/bin/env python3\n" + r'''import json,os,re,sys
|
|
||||||
from pathlib import Path
|
|
||||||
a=sys.argv[1:]; log=Path(os.environ['MOCK_LOG'])
|
|
||||||
with log.open('a') as f:f.write(json.dumps(a)+'\n')
|
|
||||||
mode=os.environ['MOCK_MODE']; cwd=os.environ['MOCK_CWD']
|
|
||||||
if a[:2]==['agent','list']:
|
|
||||||
result={'agents':[{'name':n,'cwd':cwd,'agent_status':'idle'} for n in json.loads(os.environ['MOCK_AGENTS'])]}
|
|
||||||
elif a[:2]==['agent','get']:
|
|
||||||
result={'agent':{'agent_status':'blocked' if mode=='blocked' else 'done'}}
|
|
||||||
elif a[:2]==['pane','list']:
|
|
||||||
result={'panes':[{'pane_id':'w1:p2','cwd':cwd}]}
|
|
||||||
elif a[:2]==['agent','prompt']:
|
|
||||||
if mode=='timeout':print('timeout agent_blocked quota',file=sys.stderr);sys.exit(1)
|
|
||||||
result={'status':'done'}
|
|
||||||
elif a[:2]==['agent','read']:
|
|
||||||
if mode=='retry':
|
|
||||||
prompts=[json.loads(l) for l in log.read_text().splitlines() if json.loads(l)[:2]==['agent','prompt']]
|
|
||||||
if len(prompts)==1:print(re.search(r'HERDR_RETRY:[a-f0-9]+',prompts[-1][3])[0]+':quota');sys.exit(0)
|
|
||||||
print('Finished with evidence');sys.exit(0)
|
|
||||||
else:result={'status':'started'}
|
|
||||||
print(json.dumps({'result':result}))
|
|
||||||
''')
|
|
||||||
fake.chmod(0o755)
|
|
||||||
|
|
||||||
def run_cli(self, *args, env=None, cwd=None):
|
|
||||||
return subprocess.run([sys.executable,'-B',str(SCRIPT),*args],cwd=cwd or self.task,
|
|
||||||
env=env or self.env,capture_output=True,text=True,timeout=15)
|
|
||||||
|
|
||||||
def calls(self, group):
|
|
||||||
if not self.log.exists():return []
|
|
||||||
return [a for a in map(json.loads,self.log.read_text().splitlines()) if a[:2]==['agent',group]]
|
|
||||||
|
|
||||||
def test_all_profiles_have_noninteractive_flags_and_explicit_pi_routes(self):
|
|
||||||
for profile,(name,_,native) in PROFILES.items():
|
|
||||||
with self.subTest(profile=profile):
|
|
||||||
p=self.run_cli('start','--dry-run',profile,name+'__a','w1:p2',env=dict(self.env,HERDR_ENV=''))
|
|
||||||
self.assertEqual(p.returncode,0,p.stderr)
|
|
||||||
for arg in native:self.assertIn(arg,p.stdout)
|
|
||||||
self.assertFalse(self.log.exists())
|
|
||||||
self.assertIn('qwen3.8-max',PROFILES['pi-max'][2])
|
|
||||||
self.assertIn('--auto',PROFILES['kimi'][2])
|
|
||||||
self.assertIn('danger-full-access',PROFILES['codex'][2])
|
|
||||||
|
|
||||||
def test_outside_herdr_and_wrong_task_cwd_never_contact_transport(self):
|
|
||||||
for env,cwd in [(dict(self.env,HERDR_ENV=''),self.task),(self.env,self.root)]:
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','report','draft',env=env,cwd=cwd)
|
|
||||||
self.assertNotEqual(p.returncode,0)
|
|
||||||
self.assertFalse(self.log.exists())
|
|
||||||
|
|
||||||
def test_main_or_wrong_target_pane_cannot_start(self):
|
|
||||||
p=self.run_cli('start','--task','alpha','pi-max','qwen_max__a','w1:p2',env=dict(self.env,MOCK_CWD=str(self.root)))
|
|
||||||
self.assertNotEqual(p.returncode,0);self.assertFalse(self.calls('start'))
|
|
||||||
p=self.run_cli('start','--task','alpha','pi-max','qwen_max__a','w1:p2')
|
|
||||||
self.assertEqual(p.returncode,0,p.stderr);self.assertEqual(len(self.calls('start')),1)
|
|
||||||
|
|
||||||
def test_report_routes_to_max_not_kimi_or_minimax(self):
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','report','draft')
|
|
||||||
self.assertEqual(p.returncode,0,p.stderr)
|
|
||||||
self.assertEqual(self.calls('prompt')[0][2],'qwen_max__alpha')
|
|
||||||
self.assertTrue(all('context_kimi' not in v for k,v in ROUTES.items() if k not in ('report-final-check','quota-emergency')))
|
|
||||||
|
|
||||||
def test_implementation_fallback_is_explicit(self):
|
|
||||||
self.env['MOCK_AGENTS']=json.dumps(['reviewer_glm__alpha'])
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','implementation','change')
|
|
||||||
self.assertEqual(p.returncode,69);self.assertFalse(self.calls('prompt'))
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','--implementation-fallback','Cursor quota exhausted, no writes','implementation','change')
|
|
||||||
self.assertEqual(p.returncode,0,p.stderr)
|
|
||||||
self.assertEqual(self.calls('prompt')[0][2],'reviewer_glm__alpha')
|
|
||||||
|
|
||||||
def test_uncertain_transport_error_and_blocked_do_not_fallback(self):
|
|
||||||
for mode in ['timeout','blocked']:
|
|
||||||
self.log.unlink(missing_ok=True)
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','report','draft',env=dict(self.env,MOCK_MODE=mode))
|
|
||||||
self.assertEqual(p.returncode,75,p.stderr);self.assertEqual(len(self.calls('prompt')),1)
|
|
||||||
|
|
||||||
def test_explicit_prewrite_retry_reaches_flash(self):
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','report','draft',env=dict(self.env,MOCK_MODE='retry'))
|
|
||||||
self.assertEqual(p.returncode,0,p.stderr)
|
|
||||||
self.assertEqual([a[2] for a in self.calls('prompt')],['qwen_max__alpha','qwen_flash__alpha'])
|
|
||||||
|
|
||||||
def test_same_role_ambiguity_and_foreign_cwd_do_not_dispatch(self):
|
|
||||||
for env in [dict(self.env,MOCK_AGENTS=json.dumps(['qwen_max__a','qwen_max__b'])),dict(self.env,MOCK_CWD=str(self.root))]:
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','report','draft',env=env)
|
|
||||||
self.assertNotEqual(p.returncode,0);self.assertFalse(self.calls('prompt'))
|
|
||||||
|
|
||||||
def test_kimi_reason_disabled_marker_and_cross_lane_budget(self):
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','report-final-check','check')
|
|
||||||
self.assertNotEqual(p.returncode,0);self.assertFalse(self.calls('prompt'))
|
|
||||||
marker=self.root/'.locks/herdr-kimi.disabled';marker.touch()
|
|
||||||
args=['dispatch','--task','alpha','--kimi-reason','final report','report-final-check','check']
|
|
||||||
self.assertNotEqual(self.run_cli(*args).returncode,0);self.assertFalse(self.calls('prompt'))
|
|
||||||
marker.unlink()
|
|
||||||
p=self.run_cli(*args);self.assertEqual(p.returncode,0,p.stderr)
|
|
||||||
p=self.run_cli('dispatch','--task','alpha','--kimi-reason','outage','quota-emergency','check')
|
|
||||||
self.assertNotEqual(p.returncode,0);self.assertEqual(len(self.calls('prompt')),1)
|
|
||||||
|
|
||||||
def test_kimi_atomic_limit_across_eight_processes(self):
|
|
||||||
args=[sys.executable,'-B',str(SCRIPT),'dispatch','--task','alpha','--kimi-reason','final report','report-final-check','check']
|
|
||||||
processes=[subprocess.Popen(args,cwd=self.task,env=self.env,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) for _ in range(8)]
|
|
||||||
for p in processes:p.communicate(timeout=20)
|
|
||||||
self.assertEqual(sum(p.returncode==0 for p in processes),1)
|
|
||||||
self.assertEqual(len(self.calls('prompt')),1)
|
|
||||||
|
|
||||||
def test_kimi_uncertain_send_consumes_budget(self):
|
|
||||||
args=['dispatch','--task','alpha','--kimi-reason','final report','report-final-check','check']
|
|
||||||
self.assertEqual(self.run_cli(*args,env=dict(self.env,MOCK_MODE='timeout')).returncode,75)
|
|
||||||
self.assertNotEqual(self.run_cli(*args).returncode,0)
|
|
||||||
self.assertEqual(len(self.calls('prompt')),1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__=='__main__':unittest.main()
|
|
||||||
Reference in New Issue
Block a user