cowork-flow 0.0.4 → 0.0.5
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/README.md +17 -0
- package/package.json +6 -4
- package/src/lib/copy-template.js +9 -9
- package/src/lib/package-info.js +8 -2
- package/template/.agent/skills/agent-team-execution/SKILL.md +26 -0
- package/template/{.superpowers → .agent/skills}/brainstorming/scripts/start-server.sh +0 -0
- package/template/{.superpowers → .agent/skills}/brainstorming/scripts/stop-server.sh +0 -0
- package/template/.agent/skills/start/SKILL.md +1 -0
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/agent-team/adapters.yaml +15 -0
- package/template/.cowork-flow/agent-team/agents.yaml +68 -0
- package/template/.cowork-flow/agent-team/policy.yaml +23 -0
- package/template/.cowork-flow/config.yaml +9 -0
- package/template/.cowork-flow/run +4 -0
- package/template/.cowork-flow/run.cmd +7 -0
- package/template/.cowork-flow/scripts/agent_team.py +508 -0
- package/template/.cowork-flow/scripts/common/agent_team.py +241 -0
- package/template/.cowork-flow/scripts/common/config.py +18 -0
- package/template/.cowork-flow/scripts/common/git_context.py +17 -0
- package/template/.cowork-flow/scripts/task.py +1 -1
- package/template/.cowork-flow/workflow.md +43 -159
- package/template/.superpowers/executing-plans/SKILL.md +2 -0
- package/template/AGENTS.md +3 -2
- /package/template/{.superpowers → .agent/skills}/brainstorming/SKILL.md +0 -0
- /package/template/{.superpowers → .agent/skills}/brainstorming/scripts/frame-template.html +0 -0
- /package/template/{.superpowers → .agent/skills}/brainstorming/scripts/helper.js +0 -0
- /package/template/{.superpowers → .agent/skills}/brainstorming/scripts/server.cjs +0 -0
- /package/template/{.superpowers → .agent/skills}/brainstorming/spec-document-reviewer-prompt.md +0 -0
- /package/template/{.superpowers → .agent/skills}/brainstorming/visual-companion.md +0 -0
- /package/template/{.superpowers → .agent/skills}/requesting-code-review/SKILL.md +0 -0
- /package/template/{.superpowers → .agent/skills}/requesting-code-review/code-reviewer.md +0 -0
- /package/template/{.superpowers → .agent/skills}/subagent-driven-development/SKILL.md +0 -0
- /package/template/{.superpowers → .agent/skills}/subagent-driven-development/code-quality-reviewer-prompt.md +0 -0
- /package/template/{.superpowers → .agent/skills}/subagent-driven-development/implementer-prompt.md +0 -0
- /package/template/{.superpowers → .agent/skills}/subagent-driven-development/spec-reviewer-prompt.md +0 -0
- /package/template/{.superpowers → .agent/skills}/writing-plans/SKILL.md +0 -0
- /package/template/{.superpowers → .agent/skills}/writing-plans/plan-document-reviewer-prompt.md +0 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Agent team runtime commands for cowork-flow."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from common.agent_team import (
|
|
14
|
+
build_dispatch_plan,
|
|
15
|
+
build_initial_metrics,
|
|
16
|
+
build_initial_status,
|
|
17
|
+
parse_plan,
|
|
18
|
+
render_assignment_prompt,
|
|
19
|
+
render_dispatch_plan,
|
|
20
|
+
write_json,
|
|
21
|
+
)
|
|
22
|
+
from common.config import get_agent_team_enabled
|
|
23
|
+
from common.paths import DIR_WORKFLOW, get_repo_root
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
DEFAULT_CONFIGS = {
|
|
27
|
+
"agents.yaml": """# Agent Team Registry
|
|
28
|
+
# Project teams may customize this file after initialization.
|
|
29
|
+
|
|
30
|
+
default_adapter: codex
|
|
31
|
+
|
|
32
|
+
agents:
|
|
33
|
+
implementer:
|
|
34
|
+
codex_type: worker
|
|
35
|
+
capabilities:
|
|
36
|
+
- implementation
|
|
37
|
+
- test-writing
|
|
38
|
+
preferred_task_types:
|
|
39
|
+
- code
|
|
40
|
+
- test
|
|
41
|
+
file_patterns:
|
|
42
|
+
- "src/**"
|
|
43
|
+
- "template/**"
|
|
44
|
+
- "tests/**"
|
|
45
|
+
- "test/**"
|
|
46
|
+
risk_limits:
|
|
47
|
+
max_parallel_write_conflicts: 0
|
|
48
|
+
|
|
49
|
+
spec-reviewer:
|
|
50
|
+
codex_type: default
|
|
51
|
+
capabilities:
|
|
52
|
+
- spec-review
|
|
53
|
+
- acceptance-check
|
|
54
|
+
preferred_task_types:
|
|
55
|
+
- review
|
|
56
|
+
- documentation
|
|
57
|
+
file_patterns:
|
|
58
|
+
- ".cowork-flow/changes/**"
|
|
59
|
+
- ".cowork-flow/plans/**"
|
|
60
|
+
- "README.md"
|
|
61
|
+
- "template/**"
|
|
62
|
+
risk_limits:
|
|
63
|
+
max_parallel_write_conflicts: 0
|
|
64
|
+
|
|
65
|
+
quality-reviewer:
|
|
66
|
+
codex_type: default
|
|
67
|
+
capabilities:
|
|
68
|
+
- code-quality-review
|
|
69
|
+
- test-review
|
|
70
|
+
preferred_task_types:
|
|
71
|
+
- review
|
|
72
|
+
- verification
|
|
73
|
+
file_patterns:
|
|
74
|
+
- "src/**"
|
|
75
|
+
- "template/**"
|
|
76
|
+
- "tests/**"
|
|
77
|
+
- "test/**"
|
|
78
|
+
risk_limits:
|
|
79
|
+
max_parallel_write_conflicts: 0
|
|
80
|
+
|
|
81
|
+
docs-agent:
|
|
82
|
+
codex_type: worker
|
|
83
|
+
capabilities:
|
|
84
|
+
- documentation
|
|
85
|
+
- workflow-writing
|
|
86
|
+
preferred_task_types:
|
|
87
|
+
- docs
|
|
88
|
+
file_patterns:
|
|
89
|
+
- "README.md"
|
|
90
|
+
- "AGENTS.md"
|
|
91
|
+
- ".cowork-flow/**/*.md"
|
|
92
|
+
- ".agent/skills/**"
|
|
93
|
+
risk_limits:
|
|
94
|
+
max_parallel_write_conflicts: 0
|
|
95
|
+
""",
|
|
96
|
+
"adapters.yaml": """# Agent Team Adapters
|
|
97
|
+
|
|
98
|
+
default: codex
|
|
99
|
+
fallback: manual
|
|
100
|
+
|
|
101
|
+
adapters:
|
|
102
|
+
codex:
|
|
103
|
+
mode: coordinator-dispatched
|
|
104
|
+
description: 主 agent 使用 Codex 子 agent 工具执行脚本生成的 assignments。
|
|
105
|
+
output_file: adapters/codex.json
|
|
106
|
+
|
|
107
|
+
manual:
|
|
108
|
+
mode: prompt-only
|
|
109
|
+
description: 生成可复制的 assignment prompt,由人或其他宿主执行并回写结果。
|
|
110
|
+
output_file: adapters/manual.json
|
|
111
|
+
""",
|
|
112
|
+
"policy.yaml": """# Agent Team Policy
|
|
113
|
+
|
|
114
|
+
parallel:
|
|
115
|
+
allow_parallel_batches: true
|
|
116
|
+
disallow_file_write_conflicts: true
|
|
117
|
+
require_coordinator_review: true
|
|
118
|
+
|
|
119
|
+
reviews:
|
|
120
|
+
require_spec_review: true
|
|
121
|
+
require_quality_review: true
|
|
122
|
+
chain:
|
|
123
|
+
- implementer
|
|
124
|
+
- spec-reviewer
|
|
125
|
+
- quality-reviewer
|
|
126
|
+
|
|
127
|
+
retry:
|
|
128
|
+
max_attempts: 3
|
|
129
|
+
retry_on:
|
|
130
|
+
- needs_context
|
|
131
|
+
- failed_verification
|
|
132
|
+
- review_rejected
|
|
133
|
+
- adapter_failed
|
|
134
|
+
escalation: needs-coordinator-decision
|
|
135
|
+
""",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
TERMINAL_STATUSES = {"done", "approved"}
|
|
139
|
+
MAX_ATTEMPTS = 3
|
|
140
|
+
GATED_COMMANDS = {
|
|
141
|
+
"prepare",
|
|
142
|
+
"status",
|
|
143
|
+
"next",
|
|
144
|
+
"record-result",
|
|
145
|
+
"record-review",
|
|
146
|
+
"retry",
|
|
147
|
+
"complete",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _agent_team_config_dir(repo_root: Path) -> Path:
|
|
152
|
+
return repo_root / DIR_WORKFLOW / "agent-team"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _write_default_if_missing(path: Path, content: str) -> str:
|
|
156
|
+
if path.exists():
|
|
157
|
+
return "preserved"
|
|
158
|
+
|
|
159
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
path.write_text(content, encoding="utf-8")
|
|
161
|
+
return "created"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _resolve_path(repo_root: Path, value: str) -> Path:
|
|
165
|
+
path = Path(value)
|
|
166
|
+
if path.is_absolute():
|
|
167
|
+
return path
|
|
168
|
+
return repo_root / path
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _task_ready(task_dir: Path) -> list[str]:
|
|
172
|
+
missing = []
|
|
173
|
+
for name in ("prd.md", "implement.jsonl", "check.jsonl", "debug.jsonl"):
|
|
174
|
+
path = task_dir / name
|
|
175
|
+
if not path.is_file() or not path.read_text(encoding="utf-8").strip():
|
|
176
|
+
missing.append(name)
|
|
177
|
+
return missing
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _runtime_dir(task_dir: Path) -> Path:
|
|
181
|
+
return task_dir / "agent-team"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _load_json(path: Path) -> dict[str, object]:
|
|
185
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _save_json(path: Path, data: dict[str, object]) -> None:
|
|
189
|
+
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _load_status(task_dir: Path) -> dict[str, object]:
|
|
193
|
+
status_path = _runtime_dir(task_dir) / "status.json"
|
|
194
|
+
if not status_path.is_file():
|
|
195
|
+
raise FileNotFoundError(f"agent-team status not found: {status_path}")
|
|
196
|
+
return _load_json(status_path)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _load_metrics(task_dir: Path) -> dict[str, object]:
|
|
200
|
+
metrics_path = _runtime_dir(task_dir) / "metrics.json"
|
|
201
|
+
if not metrics_path.is_file():
|
|
202
|
+
raise FileNotFoundError(f"agent-team metrics not found: {metrics_path}")
|
|
203
|
+
return _load_json(metrics_path)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _unlock_ready_assignments(status_data: dict[str, object]) -> None:
|
|
207
|
+
assignments = status_data["assignments"]
|
|
208
|
+
for assignment in assignments.values():
|
|
209
|
+
if assignment.get("status") != "pending":
|
|
210
|
+
continue
|
|
211
|
+
depends_on = assignment.get("depends_on", [])
|
|
212
|
+
if all(assignments[dependency]["status"] in TERMINAL_STATUSES for dependency in depends_on):
|
|
213
|
+
assignment["status"] = "ready"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _copy_payload(source: str | None, destination_dir: Path, assignment_id: str, attempt: int) -> None:
|
|
217
|
+
if not source:
|
|
218
|
+
return
|
|
219
|
+
source_path = Path(source)
|
|
220
|
+
if not source_path.is_file():
|
|
221
|
+
raise FileNotFoundError(f"payload file not found: {source}")
|
|
222
|
+
destination_dir.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
shutil.copyfile(source_path, destination_dir / f"{assignment_id}-attempt-{attempt}.json")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _record_assignment(
|
|
227
|
+
task_dir: Path,
|
|
228
|
+
assignment_id: str,
|
|
229
|
+
status: str,
|
|
230
|
+
payload_file: str | None,
|
|
231
|
+
*,
|
|
232
|
+
review: bool,
|
|
233
|
+
) -> int:
|
|
234
|
+
status_data = _load_status(task_dir)
|
|
235
|
+
metrics = _load_metrics(task_dir)
|
|
236
|
+
assignments = status_data["assignments"]
|
|
237
|
+
if assignment_id not in assignments:
|
|
238
|
+
print(f"Error: assignment not found: {assignment_id}", file=sys.stderr)
|
|
239
|
+
return 1
|
|
240
|
+
|
|
241
|
+
assignment = assignments[assignment_id]
|
|
242
|
+
assignment["attempts"] = int(assignment.get("attempts", 0)) + 1
|
|
243
|
+
assignment["status"] = status
|
|
244
|
+
attempt = int(assignment["attempts"])
|
|
245
|
+
|
|
246
|
+
destination = _runtime_dir(task_dir) / ("reviews" if review else "results")
|
|
247
|
+
_copy_payload(payload_file, destination, assignment_id, attempt)
|
|
248
|
+
|
|
249
|
+
metrics["attempts"] = int(metrics.get("attempts", 0)) + 1
|
|
250
|
+
if status in TERMINAL_STATUSES:
|
|
251
|
+
metrics["successfulAssignments"] = int(metrics.get("successfulAssignments", 0)) + 1
|
|
252
|
+
else:
|
|
253
|
+
metrics["failedAssignments"] = int(metrics.get("failedAssignments", 0)) + 1
|
|
254
|
+
if review:
|
|
255
|
+
metrics["reviewReworks"] = int(metrics.get("reviewReworks", 0)) + 1
|
|
256
|
+
|
|
257
|
+
_unlock_ready_assignments(status_data)
|
|
258
|
+
_save_json(_runtime_dir(task_dir) / "status.json", status_data)
|
|
259
|
+
_save_json(_runtime_dir(task_dir) / "metrics.json", metrics)
|
|
260
|
+
print(f"recorded {assignment_id} status={status} attempt={attempt}")
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
265
|
+
repo_root = get_repo_root()
|
|
266
|
+
config_dir = _agent_team_config_dir(repo_root)
|
|
267
|
+
outcomes = {
|
|
268
|
+
name: _write_default_if_missing(config_dir / name, content)
|
|
269
|
+
for name, content in DEFAULT_CONFIGS.items()
|
|
270
|
+
}
|
|
271
|
+
created = sum(1 for outcome in outcomes.values() if outcome == "created")
|
|
272
|
+
preserved = sum(1 for outcome in outcomes.values() if outcome == "preserved")
|
|
273
|
+
print(f"initialized agent-team config created={created} preserved={preserved}")
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def cmd_prepare(args: argparse.Namespace) -> int:
|
|
278
|
+
repo_root = get_repo_root()
|
|
279
|
+
task_dir = _resolve_path(repo_root, args.task_dir)
|
|
280
|
+
plan_file = _resolve_path(repo_root, args.plan)
|
|
281
|
+
|
|
282
|
+
if not task_dir.is_dir():
|
|
283
|
+
print(f"Error: task directory not found: {task_dir}", file=sys.stderr)
|
|
284
|
+
return 1
|
|
285
|
+
if not plan_file.is_file():
|
|
286
|
+
print(f"Error: plan file not found: {plan_file}", file=sys.stderr)
|
|
287
|
+
return 1
|
|
288
|
+
|
|
289
|
+
missing = _task_ready(task_dir)
|
|
290
|
+
if missing:
|
|
291
|
+
print(f"Error: task context is incomplete: {', '.join(missing)}", file=sys.stderr)
|
|
292
|
+
return 1
|
|
293
|
+
|
|
294
|
+
tasks = parse_plan(plan_file.read_text(encoding="utf-8"))
|
|
295
|
+
if not tasks:
|
|
296
|
+
print(f"Error: unable to parse plan tasks from: {plan_file}", file=sys.stderr)
|
|
297
|
+
return 1
|
|
298
|
+
|
|
299
|
+
runtime_dir = task_dir / "agent-team"
|
|
300
|
+
assignments_dir = runtime_dir / "assignments"
|
|
301
|
+
adapters_dir = runtime_dir / "adapters"
|
|
302
|
+
for directory in (
|
|
303
|
+
runtime_dir,
|
|
304
|
+
assignments_dir,
|
|
305
|
+
runtime_dir / "results",
|
|
306
|
+
runtime_dir / "reviews",
|
|
307
|
+
runtime_dir / "blockers",
|
|
308
|
+
adapters_dir,
|
|
309
|
+
):
|
|
310
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
311
|
+
|
|
312
|
+
dispatch_plan = build_dispatch_plan(tasks)
|
|
313
|
+
(runtime_dir / "dispatch-plan.yaml").write_text(
|
|
314
|
+
render_dispatch_plan(dispatch_plan),
|
|
315
|
+
encoding="utf-8",
|
|
316
|
+
)
|
|
317
|
+
write_json(runtime_dir / "status.json", build_initial_status(dispatch_plan))
|
|
318
|
+
write_json(runtime_dir / "metrics.json", build_initial_metrics(dispatch_plan))
|
|
319
|
+
write_json(
|
|
320
|
+
adapters_dir / "codex.json",
|
|
321
|
+
{
|
|
322
|
+
"adapter": "codex",
|
|
323
|
+
"mode": "coordinator-dispatched",
|
|
324
|
+
"assignmentCount": len(dispatch_plan["assignments"]),
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
for assignment in dispatch_plan["assignments"]:
|
|
329
|
+
prompt_path = assignments_dir / f"{assignment['id']}.md"
|
|
330
|
+
prompt_path.write_text(render_assignment_prompt(assignment), encoding="utf-8")
|
|
331
|
+
|
|
332
|
+
print(f"prepared agent-team runtime assignments={len(dispatch_plan['assignments'])}")
|
|
333
|
+
return 0
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
337
|
+
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
338
|
+
try:
|
|
339
|
+
status_data = _load_status(task_dir)
|
|
340
|
+
except FileNotFoundError as error:
|
|
341
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
342
|
+
return 1
|
|
343
|
+
|
|
344
|
+
counts: dict[str, int] = {}
|
|
345
|
+
for assignment in status_data["assignments"].values():
|
|
346
|
+
assignment_status = assignment.get("status", "unknown")
|
|
347
|
+
counts[assignment_status] = counts.get(assignment_status, 0) + 1
|
|
348
|
+
|
|
349
|
+
for status_name in sorted(counts):
|
|
350
|
+
print(f"{status_name}: {counts[status_name]}")
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def cmd_next(args: argparse.Namespace) -> int:
|
|
355
|
+
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
356
|
+
try:
|
|
357
|
+
status_data = _load_status(task_dir)
|
|
358
|
+
except FileNotFoundError as error:
|
|
359
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
360
|
+
return 1
|
|
361
|
+
|
|
362
|
+
ready = [
|
|
363
|
+
(assignment_id, assignment)
|
|
364
|
+
for assignment_id, assignment in status_data["assignments"].items()
|
|
365
|
+
if assignment.get("status") == "ready"
|
|
366
|
+
]
|
|
367
|
+
if not ready:
|
|
368
|
+
print("No ready assignments")
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
for assignment_id, assignment in ready:
|
|
372
|
+
print(
|
|
373
|
+
f"{assignment_id}\trole={assignment['role']}\t"
|
|
374
|
+
f"agent={assignment['recommended_agent']}\tcodex_type={assignment['codex_type']}"
|
|
375
|
+
)
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def cmd_record_result(args: argparse.Namespace) -> int:
|
|
380
|
+
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
381
|
+
try:
|
|
382
|
+
return _record_assignment(task_dir, args.assignment, args.status, args.file, review=False)
|
|
383
|
+
except FileNotFoundError as error:
|
|
384
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
385
|
+
return 1
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def cmd_record_review(args: argparse.Namespace) -> int:
|
|
389
|
+
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
390
|
+
try:
|
|
391
|
+
return _record_assignment(task_dir, args.assignment, args.status, args.file, review=True)
|
|
392
|
+
except FileNotFoundError as error:
|
|
393
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
394
|
+
return 1
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def cmd_retry(args: argparse.Namespace) -> int:
|
|
398
|
+
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
399
|
+
try:
|
|
400
|
+
status_data = _load_status(task_dir)
|
|
401
|
+
metrics = _load_metrics(task_dir)
|
|
402
|
+
except FileNotFoundError as error:
|
|
403
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
404
|
+
return 1
|
|
405
|
+
|
|
406
|
+
assignments = status_data["assignments"]
|
|
407
|
+
if args.assignment not in assignments:
|
|
408
|
+
print(f"Error: assignment not found: {args.assignment}", file=sys.stderr)
|
|
409
|
+
return 1
|
|
410
|
+
|
|
411
|
+
assignment = assignments[args.assignment]
|
|
412
|
+
assignment["attempts"] = int(assignment.get("attempts", 0)) + 1
|
|
413
|
+
assignment["last_retry_reason"] = args.reason
|
|
414
|
+
if int(assignment["attempts"]) >= MAX_ATTEMPTS:
|
|
415
|
+
assignment["status"] = "needs-coordinator-decision"
|
|
416
|
+
else:
|
|
417
|
+
assignment["status"] = "ready"
|
|
418
|
+
|
|
419
|
+
metrics["attempts"] = int(metrics.get("attempts", 0)) + 1
|
|
420
|
+
metrics["failedAssignments"] = int(metrics.get("failedAssignments", 0)) + 1
|
|
421
|
+
_save_json(_runtime_dir(task_dir) / "status.json", status_data)
|
|
422
|
+
_save_json(_runtime_dir(task_dir) / "metrics.json", metrics)
|
|
423
|
+
print(f"retry recorded {args.assignment} attempt={assignment['attempts']}")
|
|
424
|
+
return 0
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def cmd_complete(args: argparse.Namespace) -> int:
|
|
428
|
+
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
429
|
+
try:
|
|
430
|
+
status_data = _load_status(task_dir)
|
|
431
|
+
except FileNotFoundError as error:
|
|
432
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
433
|
+
return 1
|
|
434
|
+
|
|
435
|
+
pending = [
|
|
436
|
+
assignment_id
|
|
437
|
+
for assignment_id, assignment in status_data["assignments"].items()
|
|
438
|
+
if assignment.get("status") not in TERMINAL_STATUSES
|
|
439
|
+
]
|
|
440
|
+
if pending:
|
|
441
|
+
print(f"Error: pending assignments: {', '.join(pending)}", file=sys.stderr)
|
|
442
|
+
return 1
|
|
443
|
+
|
|
444
|
+
print("agent team complete")
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
449
|
+
parser = argparse.ArgumentParser(description="Manage Cowork Flow agent team runtime")
|
|
450
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
451
|
+
|
|
452
|
+
init_parser = subparsers.add_parser("init", help="Initialize agent team configuration")
|
|
453
|
+
init_parser.set_defaults(func=cmd_init)
|
|
454
|
+
|
|
455
|
+
prepare_parser = subparsers.add_parser("prepare", help="Prepare agent team runtime from a plan")
|
|
456
|
+
prepare_parser.add_argument("task_dir", help="Task directory")
|
|
457
|
+
prepare_parser.add_argument("--plan", required=True, help="Implementation plan file")
|
|
458
|
+
prepare_parser.set_defaults(func=cmd_prepare)
|
|
459
|
+
|
|
460
|
+
status_parser = subparsers.add_parser("status", help="Show agent team status")
|
|
461
|
+
status_parser.add_argument("task_dir", help="Task directory")
|
|
462
|
+
status_parser.set_defaults(func=cmd_status)
|
|
463
|
+
|
|
464
|
+
next_parser = subparsers.add_parser("next", help="Show ready assignments")
|
|
465
|
+
next_parser.add_argument("task_dir", help="Task directory")
|
|
466
|
+
next_parser.set_defaults(func=cmd_next)
|
|
467
|
+
|
|
468
|
+
record_result_parser = subparsers.add_parser("record-result", help="Record assignment result")
|
|
469
|
+
record_result_parser.add_argument("task_dir", help="Task directory")
|
|
470
|
+
record_result_parser.add_argument("--assignment", required=True, help="Assignment id")
|
|
471
|
+
record_result_parser.add_argument("--status", required=True, help="Result status")
|
|
472
|
+
record_result_parser.add_argument("--file", help="JSON payload file")
|
|
473
|
+
record_result_parser.set_defaults(func=cmd_record_result)
|
|
474
|
+
|
|
475
|
+
record_review_parser = subparsers.add_parser("record-review", help="Record assignment review")
|
|
476
|
+
record_review_parser.add_argument("task_dir", help="Task directory")
|
|
477
|
+
record_review_parser.add_argument("--assignment", required=True, help="Assignment id")
|
|
478
|
+
record_review_parser.add_argument("--status", required=True, help="Review status")
|
|
479
|
+
record_review_parser.add_argument("--file", help="JSON payload file")
|
|
480
|
+
record_review_parser.set_defaults(func=cmd_record_review)
|
|
481
|
+
|
|
482
|
+
retry_parser = subparsers.add_parser("retry", help="Record assignment retry")
|
|
483
|
+
retry_parser.add_argument("task_dir", help="Task directory")
|
|
484
|
+
retry_parser.add_argument("--assignment", required=True, help="Assignment id")
|
|
485
|
+
retry_parser.add_argument("--reason", required=True, help="Retry reason")
|
|
486
|
+
retry_parser.set_defaults(func=cmd_retry)
|
|
487
|
+
|
|
488
|
+
complete_parser = subparsers.add_parser("complete", help="Check whether agent team work is complete")
|
|
489
|
+
complete_parser.add_argument("task_dir", help="Task directory")
|
|
490
|
+
complete_parser.set_defaults(func=cmd_complete)
|
|
491
|
+
|
|
492
|
+
return parser
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def main() -> int:
|
|
496
|
+
parser = build_parser()
|
|
497
|
+
args = parser.parse_args()
|
|
498
|
+
if args.command in GATED_COMMANDS and not get_agent_team_enabled(get_repo_root()):
|
|
499
|
+
print(
|
|
500
|
+
"Error: agent-team is disabled. Set agent_team.enabled: true in .cowork-flow/config.yaml.",
|
|
501
|
+
file=sys.stderr,
|
|
502
|
+
)
|
|
503
|
+
return 1
|
|
504
|
+
return args.func(args)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
if __name__ == "__main__":
|
|
508
|
+
sys.exit(main())
|