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.
Files changed (37) hide show
  1. package/README.md +17 -0
  2. package/package.json +6 -4
  3. package/src/lib/copy-template.js +9 -9
  4. package/src/lib/package-info.js +8 -2
  5. package/template/.agent/skills/agent-team-execution/SKILL.md +26 -0
  6. package/template/{.superpowers → .agent/skills}/brainstorming/scripts/start-server.sh +0 -0
  7. package/template/{.superpowers → .agent/skills}/brainstorming/scripts/stop-server.sh +0 -0
  8. package/template/.agent/skills/start/SKILL.md +1 -0
  9. package/template/.cowork-flow/.version +1 -1
  10. package/template/.cowork-flow/agent-team/adapters.yaml +15 -0
  11. package/template/.cowork-flow/agent-team/agents.yaml +68 -0
  12. package/template/.cowork-flow/agent-team/policy.yaml +23 -0
  13. package/template/.cowork-flow/config.yaml +9 -0
  14. package/template/.cowork-flow/run +4 -0
  15. package/template/.cowork-flow/run.cmd +7 -0
  16. package/template/.cowork-flow/scripts/agent_team.py +508 -0
  17. package/template/.cowork-flow/scripts/common/agent_team.py +241 -0
  18. package/template/.cowork-flow/scripts/common/config.py +18 -0
  19. package/template/.cowork-flow/scripts/common/git_context.py +17 -0
  20. package/template/.cowork-flow/scripts/task.py +1 -1
  21. package/template/.cowork-flow/workflow.md +43 -159
  22. package/template/.superpowers/executing-plans/SKILL.md +2 -0
  23. package/template/AGENTS.md +3 -2
  24. /package/template/{.superpowers → .agent/skills}/brainstorming/SKILL.md +0 -0
  25. /package/template/{.superpowers → .agent/skills}/brainstorming/scripts/frame-template.html +0 -0
  26. /package/template/{.superpowers → .agent/skills}/brainstorming/scripts/helper.js +0 -0
  27. /package/template/{.superpowers → .agent/skills}/brainstorming/scripts/server.cjs +0 -0
  28. /package/template/{.superpowers → .agent/skills}/brainstorming/spec-document-reviewer-prompt.md +0 -0
  29. /package/template/{.superpowers → .agent/skills}/brainstorming/visual-companion.md +0 -0
  30. /package/template/{.superpowers → .agent/skills}/requesting-code-review/SKILL.md +0 -0
  31. /package/template/{.superpowers → .agent/skills}/requesting-code-review/code-reviewer.md +0 -0
  32. /package/template/{.superpowers → .agent/skills}/subagent-driven-development/SKILL.md +0 -0
  33. /package/template/{.superpowers → .agent/skills}/subagent-driven-development/code-quality-reviewer-prompt.md +0 -0
  34. /package/template/{.superpowers → .agent/skills}/subagent-driven-development/implementer-prompt.md +0 -0
  35. /package/template/{.superpowers → .agent/skills}/subagent-driven-development/spec-reviewer-prompt.md +0 -0
  36. /package/template/{.superpowers → .agent/skills}/writing-plans/SKILL.md +0 -0
  37. /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())