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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-flow",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "CLI for installing and maintaining cowork-flow collaboration templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 Orchestration Prompt
27
-
28
- - Ask Codex explicitly to spawn subagents. Use this wording for each ready batch: `Spawn one <agent_type> agent per ready assignment, wait for all requested results, and summarize each result by assignment id.`
29
- - Codex handles spawning, routing follow-up instructions, waiting for results, and closing completed agent threads. Do not look for an `agent-team` spawn subcommand; the Python script only prepares state and prompts.
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
- - Do not request a full-history fork when you also specify `agent_type`. In Codex hosts that expose real subagent dispatch tools, full-history fork cannot be combined with `agent_type` and the spawn call will be rejected.
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
- - When the host offers fork controls, prefer a fresh child thread with explicit prompt context over inherited full history. Put the assignment Markdown, write boundary, and any needed task facts directly in the worker prompt.
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.14
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(description="Manage Cowork Flow agent team runtime")
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"# {assignment['id']}",
557
+ f"# {prompt_title}",
473
558
  "",
474
- f"Role: {assignment['role']}",
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"Spawn one {agent_type} agent for this assignment. Use this file as the complete worker prompt.",
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.append("You are not alone in this codebase. Respect the write boundary, do not revert other agents' edits, and report changed files.")
491
- lines.append("")
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
- args = list(sys.argv[1:] if argv is None else argv)
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
- return run_script(script_name, rest)
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,