cowork-flow 0.0.14 → 0.0.16
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/package.json +1 -1
- package/template/.agent/skills/agent-team-execution/SKILL.md +15 -8
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/agent-team/agents.yaml +7 -0
- package/template/.cowork-flow/scripts/agent_team.py +106 -9
- package/template/.cowork-flow/scripts/common/agent_team.py +140 -9
- package/template/.cowork-flow/scripts/common/execution_context.py +253 -0
- package/template/.cowork-flow/scripts/resume.py +25 -1
- package/template/.cowork-flow/scripts/run.py +34 -2
- package/template/.cowork-flow/scripts/task.py +28 -0
package/package.json
CHANGED
|
@@ -23,11 +23,20 @@ Use this skill after a task is active and an approved `.cowork-flow/plans/*.md`
|
|
|
23
23
|
|
|
24
24
|
Do not invoke `superpowers:subagent-driven-development` from this skill. Agent-team has its own persisted state machine; use Codex subagent orchestration directly and keep `agent-team` as the source of truth for ready assignments, results, reviews, retries, and completion.
|
|
25
25
|
|
|
26
|
-
### Codex
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
26
|
+
### Codex Tool Dispatch
|
|
27
|
+
|
|
28
|
+
- In Codex hosts that expose real multi-agent tools, dispatch workers with `spawn_agent`, wait for all requested results with `wait_agent`, and close completed child threads with `close_agent`.
|
|
29
|
+
- For each ready assignment, read `agent-team/assignments/<assignment-id>.md` and use that file's body as the child `message`.
|
|
30
|
+
- When `agent-team/adapters/codex.json` provides `suggestedTaskName`, pass that value as the child `task_name` so the Codex App can start from a readable label instead of a raw assignment id.
|
|
31
|
+
- After each `spawn_agent` call returns, immediately capture the host-provided `nickname` and canonical `task_name`, then persist them with `./.cowork-flow/run agent-team record-spawn <task-dir> --assignment <id> --task-name <returned-task-name> [--nickname <returned-nickname>]`.
|
|
32
|
+
- `prepare` also emits `agent-team/assignments/<assignment-id>.context.json`. If a worker needs cowork-flow recovery, route it through `./.cowork-flow/run --context-file <that-file> resume` instead of plain `resume`.
|
|
33
|
+
- Set `agent_type` from the assignment and pass `fork_turns: none` so the worker starts as a fresh child thread instead of inheriting coordinator history.
|
|
34
|
+
- The child `message` must be the assignment prompt body only. Do not prepend coordinator dispatch wording such as `Spawn one ... agent`, because that wording can leak into the child thread and make it think it is the coordinator.
|
|
35
|
+
- Treat `recommended_agent` as the registry match and prompt source, not as the Codex spawn target unless it also names a real Codex custom agent.
|
|
36
|
+
- Prefer the host `nickname` for human-facing display once it is available. Do not invent your own alias when the host already returned one.
|
|
37
|
+
- After dispatching a ready batch, wait for all requested results, summarize each result by assignment id, then record outcomes through `agent-team`.
|
|
30
38
|
- Use `/agent` in interactive CLI sessions when you need to inspect, steer, stop, or close active agent threads.
|
|
39
|
+
- Only fall back to manual or another host-level dispatch path if the current Codex host does not expose real subagent tools.
|
|
31
40
|
|
|
32
41
|
### Subagent Evidence Gate
|
|
33
42
|
|
|
@@ -39,11 +48,9 @@ Do not invoke `superpowers:subagent-driven-development` from this skill. Agent-t
|
|
|
39
48
|
### Fresh Worker Per Assignment
|
|
40
49
|
|
|
41
50
|
- Spawn one agent per ready assignment. Use the assignment's `agent_type` as the Codex spawn target; built-in Codex agent types include `default`, `worker`, and `explorer`, and project or personal custom agents may add more.
|
|
42
|
-
-
|
|
43
|
-
- Treat `recommended_agent` as the agent-team registry match and prompt source, not as the Codex spawn target unless it also names a real Codex custom agent.
|
|
51
|
+
- Keep `fork_turns: none`; do not request a full-history fork for these worker dispatches.
|
|
44
52
|
- Spawn a fresh worker for each ready assignment. Do not reuse the main agent context as the worker context.
|
|
45
|
-
-
|
|
46
|
-
- Use `agent-team/assignments/<assignment-id>.md` as the worker prompt body.
|
|
53
|
+
- Put the assignment Markdown, write boundary, and any needed task facts directly in the worker prompt body.
|
|
47
54
|
- Add this scene-setting to every worker prompt: it is not alone in the codebase, it must respect the listed write boundary, it must not revert other agents' edits, and it must report changed files plus exact verification commands.
|
|
48
55
|
- Dispatch multiple ready assignments in parallel only after confirming their write files do not overlap. If they overlap, dispatch them sequentially or repair the dependency graph.
|
|
49
56
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.16
|
|
@@ -16,6 +16,7 @@ agents:
|
|
|
16
16
|
risk_limits:
|
|
17
17
|
max_parallel_write_conflicts: 0
|
|
18
18
|
prompt: |
|
|
19
|
+
Use the assignment brief as the source of truth; do not broaden scope.
|
|
19
20
|
Implement the smallest change that satisfies the approved plan and tests.
|
|
20
21
|
Write or update focused regression tests first, keep edits scoped, and report exact verification commands.
|
|
21
22
|
|
|
@@ -31,6 +32,7 @@ agents:
|
|
|
31
32
|
risk_limits:
|
|
32
33
|
max_parallel_write_conflicts: 0
|
|
33
34
|
prompt: |
|
|
35
|
+
Use the assignment brief as the source of truth; do not broaden scope.
|
|
34
36
|
Design tests that fail for the broken behavior and pass for the intended behavior.
|
|
35
37
|
Prefer clear behavior assertions over implementation details, and include the command that proves the result.
|
|
36
38
|
|
|
@@ -47,6 +49,7 @@ agents:
|
|
|
47
49
|
risk_limits:
|
|
48
50
|
max_parallel_write_conflicts: 0
|
|
49
51
|
prompt: |
|
|
52
|
+
Use the assignment brief as the source of truth; do not broaden scope.
|
|
50
53
|
Find the root cause before changing code. Reproduce the failure, compare with nearby working patterns,
|
|
51
54
|
then make the smallest fix and verify the original symptom.
|
|
52
55
|
|
|
@@ -62,6 +65,7 @@ agents:
|
|
|
62
65
|
risk_limits:
|
|
63
66
|
max_parallel_write_conflicts: 0
|
|
64
67
|
prompt: |
|
|
68
|
+
Use the assignment brief as the source of truth; review only the requested scope.
|
|
65
69
|
Review the proposal, spec, plan, and task PRD for contradictions, missing acceptance criteria,
|
|
66
70
|
unclear scope, and behavior that is not covered by verification.
|
|
67
71
|
|
|
@@ -78,6 +82,7 @@ agents:
|
|
|
78
82
|
risk_limits:
|
|
79
83
|
max_parallel_write_conflicts: 0
|
|
80
84
|
prompt: |
|
|
85
|
+
Use the assignment brief as the source of truth; review only the requested scope.
|
|
81
86
|
Review the diff for correctness, maintainability, focused scope, and meaningful tests.
|
|
82
87
|
Check that verification evidence matches the behavior being claimed.
|
|
83
88
|
|
|
@@ -93,6 +98,7 @@ agents:
|
|
|
93
98
|
risk_limits:
|
|
94
99
|
max_parallel_write_conflicts: 0
|
|
95
100
|
prompt: |
|
|
101
|
+
Use the assignment brief as the source of truth; do not broaden scope.
|
|
96
102
|
Write concise project documentation that reflects actual commands, files, and workflow behavior.
|
|
97
103
|
Avoid aspirational text that is not backed by the implementation.
|
|
98
104
|
|
|
@@ -109,5 +115,6 @@ agents:
|
|
|
109
115
|
risk_limits:
|
|
110
116
|
max_parallel_write_conflicts: 0
|
|
111
117
|
prompt: |
|
|
118
|
+
Use the assignment brief as the source of truth; review only the requested scope.
|
|
112
119
|
Review release-facing changes for versioning, packaging, documentation, and command safety.
|
|
113
120
|
Confirm that generated artifacts and publish steps are intentional.
|
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from common.agent_team import (
|
|
14
|
+
build_adapter_payload,
|
|
14
15
|
build_dispatch_plan,
|
|
15
16
|
build_initial_metrics,
|
|
16
17
|
build_initial_status,
|
|
@@ -21,6 +22,11 @@ from common.agent_team import (
|
|
|
21
22
|
write_json,
|
|
22
23
|
)
|
|
23
24
|
from common.config import get_agent_team_enabled
|
|
25
|
+
from common.execution_context import (
|
|
26
|
+
build_internal_execution_context_parser,
|
|
27
|
+
execution_context_from_namespace,
|
|
28
|
+
worker_command_block_message,
|
|
29
|
+
)
|
|
24
30
|
from common.paths import DIR_WORKFLOW, get_repo_root
|
|
25
31
|
|
|
26
32
|
|
|
@@ -216,6 +222,7 @@ GATED_COMMANDS = {
|
|
|
216
222
|
"prepare",
|
|
217
223
|
"status",
|
|
218
224
|
"next",
|
|
225
|
+
"record-spawn",
|
|
219
226
|
"record-result",
|
|
220
227
|
"record-review",
|
|
221
228
|
"retry",
|
|
@@ -298,6 +305,16 @@ def _copy_payload(source: str | None, destination_dir: Path, assignment_id: str,
|
|
|
298
305
|
shutil.copyfile(source_path, destination_dir / f"{assignment_id}-attempt-{attempt}.json")
|
|
299
306
|
|
|
300
307
|
|
|
308
|
+
def _assignment_display_label(assignment_id: str, assignment: dict[str, object]) -> str:
|
|
309
|
+
nickname = assignment.get("spawn_nickname")
|
|
310
|
+
if isinstance(nickname, str) and nickname.strip():
|
|
311
|
+
return nickname.strip()
|
|
312
|
+
task_name = assignment.get("spawn_task_name")
|
|
313
|
+
if isinstance(task_name, str) and task_name.strip():
|
|
314
|
+
return task_name.strip()
|
|
315
|
+
return assignment_id
|
|
316
|
+
|
|
317
|
+
|
|
301
318
|
def _record_assignment(
|
|
302
319
|
task_dir: Path,
|
|
303
320
|
assignment_id: str,
|
|
@@ -386,24 +403,40 @@ def cmd_prepare(args: argparse.Namespace) -> int:
|
|
|
386
403
|
|
|
387
404
|
registry = load_agent_registry(_agent_team_config_dir(repo_root) / "agents.yaml")
|
|
388
405
|
dispatch_plan = build_dispatch_plan(tasks, registry)
|
|
406
|
+
try:
|
|
407
|
+
task_dir_relative = task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
|
|
408
|
+
except ValueError:
|
|
409
|
+
task_dir_relative = str(task_dir)
|
|
410
|
+
for assignment in dispatch_plan["assignments"]:
|
|
411
|
+
assignment_id = str(assignment["id"])
|
|
412
|
+
assignment["worker_context_file"] = (
|
|
413
|
+
f"{task_dir_relative}/agent-team/assignments/{assignment_id}.context.json"
|
|
414
|
+
)
|
|
415
|
+
assignment["worker_prompt_file"] = (
|
|
416
|
+
f"{task_dir_relative}/agent-team/assignments/{assignment_id}.md"
|
|
417
|
+
)
|
|
389
418
|
(runtime_dir / "dispatch-plan.yaml").write_text(
|
|
390
419
|
render_dispatch_plan(dispatch_plan),
|
|
391
420
|
encoding="utf-8",
|
|
392
421
|
)
|
|
393
422
|
write_json(runtime_dir / "status.json", build_initial_status(dispatch_plan))
|
|
394
423
|
write_json(runtime_dir / "metrics.json", build_initial_metrics(dispatch_plan))
|
|
395
|
-
write_json(
|
|
396
|
-
adapters_dir / f"{dispatch_plan['adapter']}.json",
|
|
397
|
-
{
|
|
398
|
-
"adapter": dispatch_plan["adapter"],
|
|
399
|
-
"mode": "coordinator-dispatched",
|
|
400
|
-
"assignmentCount": len(dispatch_plan["assignments"]),
|
|
401
|
-
},
|
|
402
|
-
)
|
|
424
|
+
write_json(adapters_dir / f"{dispatch_plan['adapter']}.json", build_adapter_payload(dispatch_plan))
|
|
403
425
|
|
|
404
426
|
for assignment in dispatch_plan["assignments"]:
|
|
405
427
|
prompt_path = assignments_dir / f"{assignment['id']}.md"
|
|
428
|
+
context_path = assignments_dir / f"{assignment['id']}.context.json"
|
|
406
429
|
prompt_path.write_text(render_assignment_prompt(assignment), encoding="utf-8")
|
|
430
|
+
write_json(
|
|
431
|
+
context_path,
|
|
432
|
+
{
|
|
433
|
+
"version": 1,
|
|
434
|
+
"mode": "worker",
|
|
435
|
+
"taskDir": task_dir_relative,
|
|
436
|
+
"assignment": assignment["id"],
|
|
437
|
+
"promptFile": assignment["worker_prompt_file"],
|
|
438
|
+
},
|
|
439
|
+
)
|
|
407
440
|
|
|
408
441
|
print(f"prepared agent-team runtime assignments={len(dispatch_plan['assignments'])}")
|
|
409
442
|
return 0
|
|
@@ -424,6 +457,23 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
424
457
|
|
|
425
458
|
for status_name in sorted(counts):
|
|
426
459
|
print(f"{status_name}: {counts[status_name]}")
|
|
460
|
+
if getattr(args, "verbose", False):
|
|
461
|
+
for assignment_id in sorted(status_data["assignments"]):
|
|
462
|
+
assignment = status_data["assignments"][assignment_id]
|
|
463
|
+
display_label = _assignment_display_label(assignment_id, assignment)
|
|
464
|
+
parts = [
|
|
465
|
+
assignment_id,
|
|
466
|
+
f"status={assignment.get('status', 'unknown')}",
|
|
467
|
+
f"role={assignment.get('role', 'unknown')}",
|
|
468
|
+
f"label={display_label}",
|
|
469
|
+
]
|
|
470
|
+
spawn_task_name = assignment.get("spawn_task_name")
|
|
471
|
+
if isinstance(spawn_task_name, str) and spawn_task_name.strip():
|
|
472
|
+
parts.append(f"task_name={spawn_task_name.strip()}")
|
|
473
|
+
spawn_nickname = assignment.get("spawn_nickname")
|
|
474
|
+
if isinstance(spawn_nickname, str) and spawn_nickname.strip():
|
|
475
|
+
parts.append(f"nickname={spawn_nickname.strip()}")
|
|
476
|
+
print("\t".join(parts))
|
|
427
477
|
return 0
|
|
428
478
|
|
|
429
479
|
|
|
@@ -452,6 +502,31 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
452
502
|
return 0
|
|
453
503
|
|
|
454
504
|
|
|
505
|
+
def cmd_record_spawn(args: argparse.Namespace) -> int:
|
|
506
|
+
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
507
|
+
try:
|
|
508
|
+
status_data = _load_status(task_dir)
|
|
509
|
+
except FileNotFoundError as error:
|
|
510
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
511
|
+
return 1
|
|
512
|
+
|
|
513
|
+
assignments = status_data["assignments"]
|
|
514
|
+
if args.assignment not in assignments:
|
|
515
|
+
print(f"Error: assignment not found: {args.assignment}", file=sys.stderr)
|
|
516
|
+
return 1
|
|
517
|
+
|
|
518
|
+
assignment = assignments[args.assignment]
|
|
519
|
+
assignment["spawn_task_name"] = args.task_name
|
|
520
|
+
assignment["spawn_nickname"] = args.nickname.strip() if args.nickname else None
|
|
521
|
+
_save_json(_runtime_dir(task_dir) / "status.json", status_data)
|
|
522
|
+
|
|
523
|
+
label = _assignment_display_label(args.assignment, assignment)
|
|
524
|
+
print(
|
|
525
|
+
f"recorded spawn {args.assignment} task_name={args.task_name} label={label}"
|
|
526
|
+
)
|
|
527
|
+
return 0
|
|
528
|
+
|
|
529
|
+
|
|
455
530
|
def cmd_record_result(args: argparse.Namespace) -> int:
|
|
456
531
|
task_dir = _resolve_path(get_repo_root(), args.task_dir)
|
|
457
532
|
try:
|
|
@@ -522,7 +597,10 @@ def cmd_complete(args: argparse.Namespace) -> int:
|
|
|
522
597
|
|
|
523
598
|
|
|
524
599
|
def build_parser() -> argparse.ArgumentParser:
|
|
525
|
-
parser = argparse.ArgumentParser(
|
|
600
|
+
parser = argparse.ArgumentParser(
|
|
601
|
+
description="Manage Cowork Flow agent team runtime",
|
|
602
|
+
parents=[build_internal_execution_context_parser()],
|
|
603
|
+
)
|
|
526
604
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
527
605
|
|
|
528
606
|
init_parser = subparsers.add_parser("init", help="Initialize agent team configuration")
|
|
@@ -535,12 +613,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
535
613
|
|
|
536
614
|
status_parser = subparsers.add_parser("status", help="Show agent team status")
|
|
537
615
|
status_parser.add_argument("task_dir", help="Task directory")
|
|
616
|
+
status_parser.add_argument("--verbose", action="store_true", help="Show per-assignment details")
|
|
538
617
|
status_parser.set_defaults(func=cmd_status)
|
|
539
618
|
|
|
540
619
|
next_parser = subparsers.add_parser("next", help="Show ready assignments")
|
|
541
620
|
next_parser.add_argument("task_dir", help="Task directory")
|
|
542
621
|
next_parser.set_defaults(func=cmd_next)
|
|
543
622
|
|
|
623
|
+
record_spawn_parser = subparsers.add_parser("record-spawn", help="Record spawned agent details")
|
|
624
|
+
record_spawn_parser.add_argument("task_dir", help="Task directory")
|
|
625
|
+
record_spawn_parser.add_argument("--assignment", required=True, help="Assignment id")
|
|
626
|
+
record_spawn_parser.add_argument("--task-name", required=True, help="Canonical task_name returned by spawn_agent")
|
|
627
|
+
record_spawn_parser.add_argument("--nickname", help="Host-provided nickname returned by spawn_agent")
|
|
628
|
+
record_spawn_parser.set_defaults(func=cmd_record_spawn)
|
|
629
|
+
|
|
544
630
|
record_result_parser = subparsers.add_parser("record-result", help="Record assignment result")
|
|
545
631
|
record_result_parser.add_argument("task_dir", help="Task directory")
|
|
546
632
|
record_result_parser.add_argument("--assignment", required=True, help="Assignment id")
|
|
@@ -571,6 +657,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
571
657
|
def main() -> int:
|
|
572
658
|
parser = build_parser()
|
|
573
659
|
args = parser.parse_args()
|
|
660
|
+
execution_context = execution_context_from_namespace(args)
|
|
661
|
+
if execution_context.is_worker:
|
|
662
|
+
print(
|
|
663
|
+
worker_command_block_message(
|
|
664
|
+
execution_context,
|
|
665
|
+
f"agent-team {args.command}",
|
|
666
|
+
"Workers must not inspect or mutate agent-team coordinator state.",
|
|
667
|
+
),
|
|
668
|
+
file=sys.stderr,
|
|
669
|
+
)
|
|
670
|
+
return 2
|
|
574
671
|
if args.command in GATED_COMMANDS and not get_agent_team_enabled(get_repo_root()):
|
|
575
672
|
print(
|
|
576
673
|
"Error: agent-team is disabled. Set agent_team.enabled: true in .cowork-flow/config.yaml.",
|
|
@@ -140,6 +140,18 @@ ROLE_TASK_TYPES = {
|
|
|
140
140
|
"quality-reviewer": {"review", "verification"},
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
ROLE_PROMPT_TITLES = {
|
|
144
|
+
"implementer": "Implement",
|
|
145
|
+
"spec-reviewer": "Spec review",
|
|
146
|
+
"quality-reviewer": "Quality review",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ROLE_TASK_NAME_PREFIXES = {
|
|
150
|
+
"implementer": "implement",
|
|
151
|
+
"spec-reviewer": "spec_review",
|
|
152
|
+
"quality-reviewer": "quality_review",
|
|
153
|
+
}
|
|
154
|
+
|
|
143
155
|
|
|
144
156
|
def _role_config(registry: dict[str, object] | None, role: str) -> dict[str, object]:
|
|
145
157
|
agents = registry.get("agents", {}) if registry else {}
|
|
@@ -334,6 +346,37 @@ def _select_agent(role: str, task: dict[str, object], registry: dict[str, object
|
|
|
334
346
|
prompt = best_config.get("prompt") if isinstance(best_config, dict) else ""
|
|
335
347
|
return best_name, agent_type, prompt if isinstance(prompt, str) else ""
|
|
336
348
|
|
|
349
|
+
|
|
350
|
+
def _slugify_task_title(title: object) -> str:
|
|
351
|
+
if not isinstance(title, str):
|
|
352
|
+
return "assignment"
|
|
353
|
+
tokens = re.findall(r"[a-z0-9]+", title.lower())
|
|
354
|
+
return "_".join(tokens) if tokens else "assignment"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _assignment_prompt_title(assignment: dict[str, object]) -> str:
|
|
358
|
+
role = str(assignment.get("role", ""))
|
|
359
|
+
title = str(assignment.get("title", "")).strip() or str(assignment.get("id", "Assignment"))
|
|
360
|
+
prefix = ROLE_PROMPT_TITLES.get(role, "Assignment")
|
|
361
|
+
return f"{prefix}: {title}"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _assignment_task_name_seed(assignment: dict[str, object]) -> str:
|
|
365
|
+
role = str(assignment.get("role", ""))
|
|
366
|
+
prefix = ROLE_TASK_NAME_PREFIXES.get(role, _slugify_task_title(role))
|
|
367
|
+
title_slug = _slugify_task_title(assignment.get("title", ""))
|
|
368
|
+
return f"{prefix}_{title_slug}" if title_slug else prefix
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _apply_assignment_naming(assignments: list[dict[str, object]]) -> None:
|
|
372
|
+
seen: dict[str, int] = {}
|
|
373
|
+
for assignment in assignments:
|
|
374
|
+
assignment["prompt_title"] = _assignment_prompt_title(assignment)
|
|
375
|
+
seed = _assignment_task_name_seed(assignment)
|
|
376
|
+
count = seen.get(seed, 0) + 1
|
|
377
|
+
seen[seed] = count
|
|
378
|
+
assignment["suggested_task_name"] = seed if count == 1 else f"{seed}_{count}"
|
|
379
|
+
|
|
337
380
|
def build_dispatch_plan(
|
|
338
381
|
tasks: list[dict[str, object]],
|
|
339
382
|
registry: dict[str, object] | None = None,
|
|
@@ -372,6 +415,7 @@ def build_dispatch_plan(
|
|
|
372
415
|
"commands": task.get("commands", []),
|
|
373
416
|
}
|
|
374
417
|
)
|
|
418
|
+
_apply_assignment_naming(assignments)
|
|
375
419
|
|
|
376
420
|
return {
|
|
377
421
|
"version": 1,
|
|
@@ -438,6 +482,8 @@ def build_initial_status(dispatch_plan: dict[str, object]) -> dict[str, object]:
|
|
|
438
482
|
"task": assignment["task"],
|
|
439
483
|
"recommended_agent": assignment["recommended_agent"],
|
|
440
484
|
"agent_type": assignment["agent_type"],
|
|
485
|
+
"spawn_task_name": None,
|
|
486
|
+
"spawn_nickname": None,
|
|
441
487
|
"agent_prompt": assignment.get("agent_prompt", ""),
|
|
442
488
|
}
|
|
443
489
|
|
|
@@ -455,6 +501,42 @@ def build_initial_metrics(dispatch_plan: dict[str, object]) -> dict[str, object]
|
|
|
455
501
|
}
|
|
456
502
|
|
|
457
503
|
|
|
504
|
+
def build_adapter_payload(dispatch_plan: dict[str, object]) -> dict[str, object]:
|
|
505
|
+
payload: dict[str, object] = {
|
|
506
|
+
"adapter": dispatch_plan["adapter"],
|
|
507
|
+
"mode": "coordinator-dispatched",
|
|
508
|
+
"assignmentCount": len(dispatch_plan["assignments"]),
|
|
509
|
+
}
|
|
510
|
+
if dispatch_plan["adapter"] != "codex":
|
|
511
|
+
return payload
|
|
512
|
+
|
|
513
|
+
payload["spawnDefaults"] = {
|
|
514
|
+
"spawnAgent": True,
|
|
515
|
+
"forkTurns": "none",
|
|
516
|
+
"promptSource": "assignment-file",
|
|
517
|
+
}
|
|
518
|
+
payload["spawnResult"] = {
|
|
519
|
+
"captureFields": ["task_name", "nickname"],
|
|
520
|
+
"displayNamePreference": ["nickname", "task_name", "assignmentId"],
|
|
521
|
+
"recordSpawnCommandTemplate": (
|
|
522
|
+
"./.cowork-flow/run agent-team record-spawn {taskDir} "
|
|
523
|
+
"--assignment {assignmentId} --task-name {task_name} [--nickname {nickname}]"
|
|
524
|
+
),
|
|
525
|
+
}
|
|
526
|
+
payload["assignments"] = [
|
|
527
|
+
{
|
|
528
|
+
"assignmentId": assignment["id"],
|
|
529
|
+
"agentType": assignment["agent_type"],
|
|
530
|
+
"recommendedAgent": assignment["recommended_agent"],
|
|
531
|
+
"suggestedTaskName": assignment.get("suggested_task_name"),
|
|
532
|
+
"promptFile": f"assignments/{assignment['id']}.md",
|
|
533
|
+
"contextFile": f"assignments/{assignment['id']}.context.json",
|
|
534
|
+
}
|
|
535
|
+
for assignment in dispatch_plan["assignments"]
|
|
536
|
+
]
|
|
537
|
+
return payload
|
|
538
|
+
|
|
539
|
+
|
|
458
540
|
def _render_named_items(title: str, items: object) -> list[str]:
|
|
459
541
|
lines = [f"## {title}", ""]
|
|
460
542
|
if isinstance(items, list) and items:
|
|
@@ -468,28 +550,77 @@ def _render_named_items(title: str, items: object) -> list[str]:
|
|
|
468
550
|
|
|
469
551
|
def render_assignment_prompt(assignment: dict[str, object]) -> str:
|
|
470
552
|
agent_type = assignment["agent_type"]
|
|
553
|
+
assignment_id = assignment["id"]
|
|
554
|
+
task_title = assignment.get("title", "")
|
|
555
|
+
prompt_title = assignment.get("prompt_title", assignment_id)
|
|
471
556
|
lines = [
|
|
472
|
-
f"# {
|
|
557
|
+
f"# {prompt_title}",
|
|
473
558
|
"",
|
|
474
|
-
f"
|
|
475
|
-
f"Recommended agent: {assignment['recommended_agent']}",
|
|
476
|
-
f"Agent type: {agent_type}",
|
|
477
|
-
f"Spawn target agent type: {agent_type}",
|
|
478
|
-
f"Task: {assignment.get('title', '')}",
|
|
559
|
+
f"Assignment ID: {assignment_id}",
|
|
479
560
|
"",
|
|
480
|
-
f"
|
|
561
|
+
f"You are implementing assignment {assignment_id}: {task_title}",
|
|
481
562
|
"",
|
|
482
563
|
"You are already the dispatched worker for this assignment.",
|
|
564
|
+
"This Markdown file is the worker brief that should be sent as the child thread's initial message.",
|
|
565
|
+
"If you can see any outer transport text such as `Spawn one ... agent`, ignore it. That text is for the coordinator, not for you.",
|
|
483
566
|
"Do not run the project start-session workflow or try to spawn another worker from this prompt.",
|
|
567
|
+
"Do not rerun `agent-team-execution` or `subagent-driven-development` from this prompt.",
|
|
484
568
|
"Treat this file as the complete worker brief unless you are blocked and need one specific missing fact.",
|
|
485
569
|
"",
|
|
570
|
+
"## Assignment context",
|
|
571
|
+
"",
|
|
572
|
+
f"- Role: {assignment['role']}",
|
|
573
|
+
f"- Recommended agent: {assignment['recommended_agent']}",
|
|
574
|
+
f"- Agent type: {agent_type}",
|
|
575
|
+
f"- Spawn target agent type: {agent_type}",
|
|
576
|
+
f"- Task: {task_title}",
|
|
577
|
+
"",
|
|
486
578
|
]
|
|
579
|
+
worker_context_file = assignment.get("worker_context_file")
|
|
487
580
|
agent_prompt = assignment.get("agent_prompt")
|
|
488
581
|
if isinstance(agent_prompt, str) and agent_prompt.strip():
|
|
489
582
|
lines.extend(["## Agent prompt", "", agent_prompt.strip(), ""])
|
|
490
|
-
lines.
|
|
491
|
-
|
|
583
|
+
lines.extend(
|
|
584
|
+
[
|
|
585
|
+
"## Before you begin",
|
|
586
|
+
"",
|
|
587
|
+
"- Read the files, steps, and commands below before editing.",
|
|
588
|
+
"- If the assignment boundary or acceptance criteria are unclear, ask now or report NEEDS_CONTEXT before changing code.",
|
|
589
|
+
"- Do not guess, broaden scope, or switch into coordinator behavior from this worker brief.",
|
|
590
|
+
(
|
|
591
|
+
"- If you need cowork-flow recovery, use the scoped command "
|
|
592
|
+
f"`./.cowork-flow/run --context-file {worker_context_file} resume`."
|
|
593
|
+
if isinstance(worker_context_file, str) and worker_context_file.strip()
|
|
594
|
+
else "- If you need cowork-flow recovery, ask for the assignment context file instead of running unscoped workflow commands."
|
|
595
|
+
),
|
|
596
|
+
"- Do not run unscoped cowork-flow workflow commands such as `./.cowork-flow/run resume`, `task start`, or `agent-team next`.",
|
|
597
|
+
"",
|
|
598
|
+
]
|
|
599
|
+
)
|
|
600
|
+
lines.extend(
|
|
601
|
+
[
|
|
602
|
+
"## Your job",
|
|
603
|
+
"",
|
|
604
|
+
"- Implement exactly this assignment and nothing outside its write boundary.",
|
|
605
|
+
"- Follow the listed steps and run the listed verification commands when applicable.",
|
|
606
|
+
"- You are not alone in this codebase. Respect other agents' edits and never revert work you did not make.",
|
|
607
|
+
"- If the brief is missing a required fact, report NEEDS_CONTEXT with the specific missing fact instead of guessing.",
|
|
608
|
+
"",
|
|
609
|
+
]
|
|
610
|
+
)
|
|
492
611
|
lines.extend(_render_named_items("Files", assignment.get("files")))
|
|
493
612
|
lines.extend(_render_named_items("Steps", assignment.get("steps")))
|
|
494
613
|
lines.extend(_render_named_items("Commands", assignment.get("commands")))
|
|
614
|
+
lines.extend(
|
|
615
|
+
[
|
|
616
|
+
"## Report format",
|
|
617
|
+
"",
|
|
618
|
+
"- Status: DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT",
|
|
619
|
+
"- What changed, or what was attempted if blocked",
|
|
620
|
+
"- Files changed",
|
|
621
|
+
"- Exact verification commands and results",
|
|
622
|
+
"- Concerns or follow-up needed",
|
|
623
|
+
"",
|
|
624
|
+
]
|
|
625
|
+
)
|
|
495
626
|
return "\n".join(lines)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Execution context helpers for coordinator and worker scoped cowork-flow commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .paths import DIR_WORKFLOW, get_repo_root
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
MODE_COORDINATOR = "coordinator"
|
|
14
|
+
MODE_WORKER = "worker"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExecutionContextError(ValueError):
|
|
18
|
+
"""Raised when execution context flags are invalid."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ExecutionContext:
|
|
23
|
+
mode: str = MODE_COORDINATOR
|
|
24
|
+
assignment: str | None = None
|
|
25
|
+
task_dir: str | None = None
|
|
26
|
+
prompt_file: str | None = None
|
|
27
|
+
context_file: str | None = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_worker(self) -> bool:
|
|
31
|
+
return self.mode == MODE_WORKER
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def is_default(self) -> bool:
|
|
35
|
+
return (
|
|
36
|
+
self.mode == MODE_COORDINATOR
|
|
37
|
+
and self.assignment is None
|
|
38
|
+
and self.task_dir is None
|
|
39
|
+
and self.prompt_file is None
|
|
40
|
+
and self.context_file is None
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _strip(value: object) -> str | None:
|
|
45
|
+
if not isinstance(value, str):
|
|
46
|
+
return None
|
|
47
|
+
stripped = value.strip()
|
|
48
|
+
return stripped or None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_context_file(path: str) -> dict[str, object]:
|
|
52
|
+
context_path = Path(path)
|
|
53
|
+
try:
|
|
54
|
+
return json.loads(context_path.read_text(encoding="utf-8"))
|
|
55
|
+
except FileNotFoundError as error:
|
|
56
|
+
raise ExecutionContextError(f"context file not found: {path}") from error
|
|
57
|
+
except json.JSONDecodeError as error:
|
|
58
|
+
raise ExecutionContextError(f"context file is not valid JSON: {path}") from error
|
|
59
|
+
except OSError as error:
|
|
60
|
+
raise ExecutionContextError(f"failed to read context file: {path}") from error
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_parser(
|
|
64
|
+
*,
|
|
65
|
+
add_help: bool,
|
|
66
|
+
mode_flag: str,
|
|
67
|
+
assignment_flag: str,
|
|
68
|
+
task_dir_flag: str,
|
|
69
|
+
prompt_file_flag: str,
|
|
70
|
+
context_file_flag: str,
|
|
71
|
+
) -> argparse.ArgumentParser:
|
|
72
|
+
parser = argparse.ArgumentParser(add_help=add_help)
|
|
73
|
+
parser.add_argument(mode_flag, choices=[MODE_COORDINATOR, MODE_WORKER], help=argparse.SUPPRESS)
|
|
74
|
+
parser.add_argument(assignment_flag, help=argparse.SUPPRESS)
|
|
75
|
+
parser.add_argument(task_dir_flag, help=argparse.SUPPRESS)
|
|
76
|
+
parser.add_argument(prompt_file_flag, help=argparse.SUPPRESS)
|
|
77
|
+
parser.add_argument(context_file_flag, help=argparse.SUPPRESS)
|
|
78
|
+
return parser
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_public_execution_context_parser(add_help: bool = False) -> argparse.ArgumentParser:
|
|
82
|
+
return _build_parser(
|
|
83
|
+
add_help=add_help,
|
|
84
|
+
mode_flag="--mode",
|
|
85
|
+
assignment_flag="--assignment",
|
|
86
|
+
task_dir_flag="--task-dir",
|
|
87
|
+
prompt_file_flag="--prompt-file",
|
|
88
|
+
context_file_flag="--context-file",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_internal_execution_context_parser(add_help: bool = False) -> argparse.ArgumentParser:
|
|
93
|
+
return _build_parser(
|
|
94
|
+
add_help=add_help,
|
|
95
|
+
mode_flag="--execution-mode",
|
|
96
|
+
assignment_flag="--execution-assignment",
|
|
97
|
+
task_dir_flag="--execution-task-dir",
|
|
98
|
+
prompt_file_flag="--execution-prompt-file",
|
|
99
|
+
context_file_flag="--execution-context-file",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _context_from_values(
|
|
104
|
+
*,
|
|
105
|
+
mode: str | None,
|
|
106
|
+
assignment: str | None,
|
|
107
|
+
task_dir: str | None,
|
|
108
|
+
prompt_file: str | None,
|
|
109
|
+
context_file: str | None,
|
|
110
|
+
) -> ExecutionContext:
|
|
111
|
+
file_data: dict[str, object] = {}
|
|
112
|
+
if context_file:
|
|
113
|
+
file_data = _load_context_file(context_file)
|
|
114
|
+
|
|
115
|
+
resolved_mode = _strip(mode) or _strip(file_data.get("mode")) or MODE_COORDINATOR
|
|
116
|
+
if resolved_mode not in {MODE_COORDINATOR, MODE_WORKER}:
|
|
117
|
+
raise ExecutionContextError(f"unsupported execution mode: {resolved_mode}")
|
|
118
|
+
|
|
119
|
+
resolved_assignment = _strip(assignment) or _strip(file_data.get("assignment"))
|
|
120
|
+
resolved_task_dir = _strip(task_dir) or _strip(file_data.get("taskDir"))
|
|
121
|
+
resolved_prompt_file = _strip(prompt_file) or _strip(file_data.get("promptFile"))
|
|
122
|
+
|
|
123
|
+
if resolved_mode == MODE_WORKER:
|
|
124
|
+
if not resolved_task_dir or not resolved_assignment:
|
|
125
|
+
raise ExecutionContextError(
|
|
126
|
+
"worker mode requires task dir and assignment (pass --context-file or --task-dir with --assignment)"
|
|
127
|
+
)
|
|
128
|
+
if not resolved_prompt_file:
|
|
129
|
+
resolved_prompt_file = (
|
|
130
|
+
f"{resolved_task_dir}/agent-team/assignments/{resolved_assignment}.md"
|
|
131
|
+
)
|
|
132
|
+
elif resolved_assignment or resolved_task_dir or resolved_prompt_file:
|
|
133
|
+
raise ExecutionContextError("assignment-scoped execution fields require worker mode")
|
|
134
|
+
|
|
135
|
+
return ExecutionContext(
|
|
136
|
+
mode=resolved_mode,
|
|
137
|
+
assignment=resolved_assignment,
|
|
138
|
+
task_dir=resolved_task_dir,
|
|
139
|
+
prompt_file=resolved_prompt_file,
|
|
140
|
+
context_file=_strip(context_file),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def parse_public_execution_context_args(argv: list[str]) -> tuple[ExecutionContext, list[str]]:
|
|
145
|
+
parser = build_public_execution_context_parser(add_help=False)
|
|
146
|
+
leading: list[str] = []
|
|
147
|
+
remaining = list(argv)
|
|
148
|
+
index = 0
|
|
149
|
+
while index < len(argv):
|
|
150
|
+
token = argv[index]
|
|
151
|
+
if token in {"--mode", "--assignment", "--task-dir", "--prompt-file", "--context-file"}:
|
|
152
|
+
if index + 1 >= len(argv):
|
|
153
|
+
raise ExecutionContextError(f"missing value for {token}")
|
|
154
|
+
leading.extend([token, argv[index + 1]])
|
|
155
|
+
index += 2
|
|
156
|
+
continue
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
remaining = argv[index:]
|
|
160
|
+
namespace, leftover = parser.parse_known_args(leading)
|
|
161
|
+
if leftover:
|
|
162
|
+
raise ExecutionContextError(f"unsupported execution context arguments: {' '.join(leftover)}")
|
|
163
|
+
context = _context_from_values(
|
|
164
|
+
mode=getattr(namespace, "mode", None),
|
|
165
|
+
assignment=getattr(namespace, "assignment", None),
|
|
166
|
+
task_dir=getattr(namespace, "task_dir", None),
|
|
167
|
+
prompt_file=getattr(namespace, "prompt_file", None),
|
|
168
|
+
context_file=getattr(namespace, "context_file", None),
|
|
169
|
+
)
|
|
170
|
+
return context, remaining
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def execution_context_from_namespace(namespace: argparse.Namespace) -> ExecutionContext:
|
|
174
|
+
return _context_from_values(
|
|
175
|
+
mode=getattr(namespace, "execution_mode", None),
|
|
176
|
+
assignment=getattr(namespace, "execution_assignment", None),
|
|
177
|
+
task_dir=getattr(namespace, "execution_task_dir", None),
|
|
178
|
+
prompt_file=getattr(namespace, "execution_prompt_file", None),
|
|
179
|
+
context_file=getattr(namespace, "execution_context_file", None),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def context_to_internal_cli_args(context: ExecutionContext) -> list[str]:
|
|
184
|
+
if context.is_default:
|
|
185
|
+
return []
|
|
186
|
+
if context.context_file:
|
|
187
|
+
return ["--execution-context-file", context.context_file]
|
|
188
|
+
|
|
189
|
+
args = ["--execution-mode", context.mode]
|
|
190
|
+
if context.task_dir:
|
|
191
|
+
args.extend(["--execution-task-dir", context.task_dir])
|
|
192
|
+
if context.assignment:
|
|
193
|
+
args.extend(["--execution-assignment", context.assignment])
|
|
194
|
+
if context.prompt_file:
|
|
195
|
+
args.extend(["--execution-prompt-file", context.prompt_file])
|
|
196
|
+
return args
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def worker_command_block_message(
|
|
200
|
+
context: ExecutionContext,
|
|
201
|
+
command: str,
|
|
202
|
+
reason: str,
|
|
203
|
+
) -> str:
|
|
204
|
+
assignment = context.assignment or "unknown-assignment"
|
|
205
|
+
return f"Blocked: worker mode cannot run `{command}` for assignment {assignment}. {reason}"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def build_worker_resume_text(
|
|
209
|
+
context: ExecutionContext,
|
|
210
|
+
repo_root: Path | None = None,
|
|
211
|
+
) -> str:
|
|
212
|
+
if repo_root is None:
|
|
213
|
+
repo_root = get_repo_root()
|
|
214
|
+
|
|
215
|
+
lines = [
|
|
216
|
+
"========================================",
|
|
217
|
+
"WORKER CONTEXT",
|
|
218
|
+
"========================================",
|
|
219
|
+
"",
|
|
220
|
+
"## EXECUTION MODE",
|
|
221
|
+
f"Mode: {context.mode}",
|
|
222
|
+
f"Assignment: {context.assignment or 'unknown'}",
|
|
223
|
+
f"Task directory: {context.task_dir or 'unknown'}",
|
|
224
|
+
]
|
|
225
|
+
if context.prompt_file:
|
|
226
|
+
lines.append(f"Prompt file: {context.prompt_file}")
|
|
227
|
+
if context.context_file:
|
|
228
|
+
lines.append(f"Context file: {context.context_file}")
|
|
229
|
+
lines.extend(
|
|
230
|
+
[
|
|
231
|
+
"",
|
|
232
|
+
"## READ FIRST",
|
|
233
|
+
f"- Read worker brief: {context.prompt_file or '(missing prompt file)'}",
|
|
234
|
+
]
|
|
235
|
+
)
|
|
236
|
+
if context.task_dir and (repo_root / context.task_dir / "prd.md").is_file():
|
|
237
|
+
lines.append(f"- Read task PRD: {context.task_dir}/prd.md")
|
|
238
|
+
lines.extend(
|
|
239
|
+
[
|
|
240
|
+
"- Follow only the files, steps, and commands named in the worker brief.",
|
|
241
|
+
"",
|
|
242
|
+
"## RULES",
|
|
243
|
+
"- Do not switch into coordinator behavior from this worker-scoped context.",
|
|
244
|
+
"- Do not run unscoped cowork-flow workflow commands from this worker thread.",
|
|
245
|
+
"- If you are blocked, report NEEDS_CONTEXT instead of activating tasks or coordinating other workers.",
|
|
246
|
+
"",
|
|
247
|
+
f"Use scoped cowork-flow commands like: ./{DIR_WORKFLOW}/run --context-file "
|
|
248
|
+
f"{context.context_file or '<assignment-context.json>'} resume",
|
|
249
|
+
"",
|
|
250
|
+
"========================================",
|
|
251
|
+
]
|
|
252
|
+
)
|
|
253
|
+
return "\n".join(lines)
|
|
@@ -3,10 +3,34 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
from common.execution_context import (
|
|
9
|
+
build_internal_execution_context_parser,
|
|
10
|
+
build_worker_resume_text,
|
|
11
|
+
execution_context_from_namespace,
|
|
12
|
+
)
|
|
6
13
|
from common.git_context import get_context_text
|
|
7
14
|
|
|
8
15
|
|
|
9
|
-
def main() -> int:
|
|
16
|
+
def main(argv: list[str] | None = None) -> int:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
description="Resume cowork-flow session context",
|
|
19
|
+
parents=[build_internal_execution_context_parser()],
|
|
20
|
+
)
|
|
21
|
+
args = parser.parse_args(argv)
|
|
22
|
+
context = execution_context_from_namespace(args)
|
|
23
|
+
|
|
24
|
+
if context.is_worker:
|
|
25
|
+
print("========================================")
|
|
26
|
+
print("COWORK-FLOW WORKER RESUME")
|
|
27
|
+
print("========================================")
|
|
28
|
+
print("Use this only when a dispatched worker needs assignment-scoped recovery.")
|
|
29
|
+
print("Do not switch back into the coordinator workflow from this entrypoint.")
|
|
30
|
+
print("")
|
|
31
|
+
print(build_worker_resume_text(context))
|
|
32
|
+
return 0
|
|
33
|
+
|
|
10
34
|
print("========================================")
|
|
11
35
|
print("COWORK-FLOW RESUME")
|
|
12
36
|
print("========================================")
|
|
@@ -8,6 +8,12 @@ import subprocess
|
|
|
8
8
|
import sys
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
+
from common.execution_context import (
|
|
12
|
+
ExecutionContextError,
|
|
13
|
+
context_to_internal_cli_args,
|
|
14
|
+
parse_public_execution_context_args,
|
|
15
|
+
)
|
|
16
|
+
|
|
11
17
|
COMMAND_SCRIPTS = {
|
|
12
18
|
"resume": "resume.py",
|
|
13
19
|
"task": "task.py",
|
|
@@ -24,11 +30,15 @@ COMMAND_SCRIPTS = {
|
|
|
24
30
|
"agent_team": "agent_team.py",
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
CONTEXT_AWARE_COMMANDS = {"resume", "task", "agent-team"}
|
|
34
|
+
|
|
27
35
|
|
|
28
36
|
def print_usage() -> None:
|
|
29
37
|
print(
|
|
30
38
|
"""Usage:
|
|
31
39
|
./.cowork-flow/run <command> [args...]
|
|
40
|
+
./.cowork-flow/run --context-file <assignment-context.json> <command> [args...]
|
|
41
|
+
./.cowork-flow/run --mode worker --task-dir <task-dir> --assignment <id> <command> [args...]
|
|
32
42
|
./.cowork-flow/run python [python-args...]
|
|
33
43
|
|
|
34
44
|
Common commands:
|
|
@@ -62,7 +72,16 @@ def run_script(script_name: str, args: list[str]) -> int:
|
|
|
62
72
|
|
|
63
73
|
|
|
64
74
|
def main(argv: list[str] | None = None) -> int:
|
|
65
|
-
|
|
75
|
+
raw_args = list(sys.argv[1:] if argv is None else argv)
|
|
76
|
+
if not raw_args:
|
|
77
|
+
print_usage()
|
|
78
|
+
return 2
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
context, args = parse_public_execution_context_args(raw_args)
|
|
82
|
+
except ExecutionContextError as error:
|
|
83
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
84
|
+
return 2
|
|
66
85
|
if not args:
|
|
67
86
|
print_usage()
|
|
68
87
|
return 2
|
|
@@ -73,6 +92,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
73
92
|
print_usage()
|
|
74
93
|
return 0
|
|
75
94
|
if command == "python":
|
|
95
|
+
if not context.is_default:
|
|
96
|
+
print(
|
|
97
|
+
"Error: execution context flags are not supported with the `python` passthrough command.",
|
|
98
|
+
file=sys.stderr,
|
|
99
|
+
)
|
|
100
|
+
return 2
|
|
76
101
|
return run_python(rest)
|
|
77
102
|
|
|
78
103
|
script_name = COMMAND_SCRIPTS.get(command)
|
|
@@ -85,7 +110,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
85
110
|
print_usage()
|
|
86
111
|
return 2
|
|
87
112
|
|
|
88
|
-
|
|
113
|
+
if not context.is_default and command not in CONTEXT_AWARE_COMMANDS:
|
|
114
|
+
print(
|
|
115
|
+
f"Error: execution context flags are only supported for: {', '.join(sorted(CONTEXT_AWARE_COMMANDS))}",
|
|
116
|
+
file=sys.stderr,
|
|
117
|
+
)
|
|
118
|
+
return 2
|
|
119
|
+
|
|
120
|
+
return run_script(script_name, [*context_to_internal_cli_args(context), *rest])
|
|
89
121
|
|
|
90
122
|
|
|
91
123
|
if __name__ == "__main__":
|
|
@@ -65,6 +65,11 @@ from common.task_utils import (
|
|
|
65
65
|
archive_task_complete,
|
|
66
66
|
)
|
|
67
67
|
from common.config import get_hooks
|
|
68
|
+
from common.execution_context import (
|
|
69
|
+
build_internal_execution_context_parser,
|
|
70
|
+
execution_context_from_namespace,
|
|
71
|
+
worker_command_block_message,
|
|
72
|
+
)
|
|
68
73
|
|
|
69
74
|
CONTEXT_JSONL_FILES = ["implement.jsonl", "check.jsonl", "debug.jsonl"]
|
|
70
75
|
DONE_STATUSES = ("completed", "done")
|
|
@@ -1357,6 +1362,7 @@ def main() -> int:
|
|
|
1357
1362
|
parser = argparse.ArgumentParser(
|
|
1358
1363
|
description="Task Management Script for cowork-flow workflow",
|
|
1359
1364
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1365
|
+
parents=[build_internal_execution_context_parser()],
|
|
1360
1366
|
)
|
|
1361
1367
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
1362
1368
|
|
|
@@ -1421,11 +1427,33 @@ def main() -> int:
|
|
|
1421
1427
|
p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)")
|
|
1422
1428
|
|
|
1423
1429
|
args = parser.parse_args()
|
|
1430
|
+
execution_context = execution_context_from_namespace(args)
|
|
1424
1431
|
|
|
1425
1432
|
if not args.command:
|
|
1426
1433
|
show_usage()
|
|
1427
1434
|
return 1
|
|
1428
1435
|
|
|
1436
|
+
worker_blocked_commands = {
|
|
1437
|
+
"create",
|
|
1438
|
+
"init-context",
|
|
1439
|
+
"add-context",
|
|
1440
|
+
"start",
|
|
1441
|
+
"finish",
|
|
1442
|
+
"archive",
|
|
1443
|
+
"add-subtask",
|
|
1444
|
+
"remove-subtask",
|
|
1445
|
+
}
|
|
1446
|
+
if execution_context.is_worker and args.command in worker_blocked_commands:
|
|
1447
|
+
print(
|
|
1448
|
+
worker_command_block_message(
|
|
1449
|
+
execution_context,
|
|
1450
|
+
f"task {args.command}",
|
|
1451
|
+
"Workers must not activate, archive, or mutate cowork-flow task state.",
|
|
1452
|
+
),
|
|
1453
|
+
file=sys.stderr,
|
|
1454
|
+
)
|
|
1455
|
+
return 2
|
|
1456
|
+
|
|
1429
1457
|
commands = {
|
|
1430
1458
|
"create": cmd_create,
|
|
1431
1459
|
"init-context": cmd_init_context,
|