cowork-flow 0.0.38 → 0.0.40

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 (22) hide show
  1. package/package.json +1 -1
  2. package/template/.cowork-flow/.version +1 -1
  3. package/template/.cowork-flow/scripts/application/task_archive.py +148 -21
  4. package/template/.cowork-flow/scripts/application/task_context.py +134 -25
  5. package/template/.cowork-flow/scripts/application/task_lifecycle.py +3 -1
  6. package/template/.cowork-flow/scripts/commands/add_session.py +137 -124
  7. package/template/.cowork-flow/scripts/commands/task.py +172 -110
  8. package/template/.cowork-flow/scripts/commands/task_archive_commands.py +135 -49
  9. package/template/.cowork-flow/scripts/commands/task_context_commands.py +18 -1
  10. package/template/.cowork-flow/scripts/commands/task_create_command.py +51 -24
  11. package/template/.cowork-flow/scripts/commands/task_navigation.py +145 -81
  12. package/template/.cowork-flow/scripts/commands/task_parser.py +23 -0
  13. package/template/.cowork-flow/scripts/common/core/paths.py +4 -1
  14. package/template/.cowork-flow/scripts/common/gates/gates.py +114 -108
  15. package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
  16. package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +50 -28
  17. package/template/.cowork-flow/scripts/common/git/git_snapshot.py +14 -0
  18. package/template/.cowork-flow/scripts/common/task/active_task.py +36 -74
  19. package/template/.cowork-flow/spec/references/definition-of-done.md +1 -1
  20. package/template/.cowork-flow/workflow.md +6 -2
  21. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  22. package/template/.zcode/scaffold/.cowork-flow/workflow.md +6 -2
@@ -196,6 +196,42 @@ def generate_session_content(
196
196
  """
197
197
 
198
198
 
199
+ def _replace_index_section(
200
+ lines: list[str],
201
+ start_marker: str,
202
+ end_marker: str,
203
+ replacement: list[str],
204
+ ) -> list[str]:
205
+ rendered: list[str] = []
206
+ inside = False
207
+ for line in lines:
208
+ if start_marker in line:
209
+ rendered.extend((line, *replacement))
210
+ inside = True
211
+ elif end_marker in line:
212
+ rendered.append(line)
213
+ inside = False
214
+ elif not inside:
215
+ rendered.append(line)
216
+ return rendered
217
+
218
+
219
+ def _insert_session_history(lines: list[str], row: str) -> list[str]:
220
+ rendered: list[str] = []
221
+ inside = False
222
+ inserted = False
223
+ for line in lines:
224
+ if MARKER_SESSION_HISTORY_START in line:
225
+ inside = True
226
+ elif MARKER_SESSION_HISTORY_END in line:
227
+ inside = False
228
+ rendered.append(line)
229
+ if inside and re.match(r"^\|\s*-", line) and not inserted:
230
+ rendered.append(row)
231
+ inserted = True
232
+ return rendered
233
+
234
+
199
235
  def update_index(
200
236
  index_file: Path,
201
237
  dev_dir: Path,
@@ -207,8 +243,6 @@ def update_index(
207
243
  ) -> bool:
208
244
  """Update index.md with new session info."""
209
245
  commit_display = _format_commit_display(commit)
210
-
211
- # Get file number from active_file name
212
246
  match = re.search(r"(\d+)", active_file)
213
247
  active_num = int(match.group(1)) if match else 0
214
248
  files_table = count_journal_files(dev_dir, active_num)
@@ -225,69 +259,26 @@ def update_index(
225
259
  print("Error: index.md is missing auto-update markers. Please restore the template first.", file=sys.stderr)
226
260
  return False
227
261
 
228
- # Process sections
229
262
  lines = content.splitlines()
230
- new_lines = []
231
-
232
- in_current_status = False
233
- in_active_documents = False
234
- in_session_history = False
235
- header_written = False
236
-
237
- for line in lines:
238
- if MARKER_CURRENT_STATUS_START in line:
239
- new_lines.append(line)
240
- in_current_status = True
241
- new_lines.append(f"- **Current file**: `{active_file}`")
242
- new_lines.append(f"- **Total Sessions**: {new_session}")
243
- new_lines.append(f"- **Last Active**: {today}")
244
- continue
245
-
246
- if MARKER_CURRENT_STATUS_END in line:
247
- in_current_status = False
248
- new_lines.append(line)
249
- continue
250
-
251
- if MARKER_ACTIVE_DOCUMENTS_START in line:
252
- new_lines.append(line)
253
- in_active_documents = True
254
- new_lines.append("| File | Lines | Status |")
255
- new_lines.append("|------|------|------|")
256
- new_lines.append(files_table)
257
- continue
258
-
259
- if MARKER_ACTIVE_DOCUMENTS_END in line:
260
- in_active_documents = False
261
- new_lines.append(line)
262
- continue
263
-
264
- if MARKER_SESSION_HISTORY_START in line:
265
- new_lines.append(line)
266
- in_session_history = True
267
- header_written = False
268
- continue
269
-
270
- if MARKER_SESSION_HISTORY_END in line:
271
- in_session_history = False
272
- new_lines.append(line)
273
- continue
274
-
275
- if in_current_status:
276
- continue
277
-
278
- if in_active_documents:
279
- continue
280
-
281
- if in_session_history:
282
- new_lines.append(line)
283
- if re.match(r"^\|\s*-", line) and not header_written:
284
- new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} |")
285
- header_written = True
286
- continue
287
-
288
- new_lines.append(line)
289
-
290
- index_file.write_text("\n".join(new_lines), encoding="utf-8")
263
+ lines = _replace_index_section(
264
+ lines,
265
+ MARKER_CURRENT_STATUS_START,
266
+ MARKER_CURRENT_STATUS_END,
267
+ [
268
+ f"- **Current file**: `{active_file}`",
269
+ f"- **Total Sessions**: {new_session}",
270
+ f"- **Last Active**: {today}",
271
+ ],
272
+ )
273
+ lines = _replace_index_section(
274
+ lines,
275
+ MARKER_ACTIVE_DOCUMENTS_START,
276
+ MARKER_ACTIVE_DOCUMENTS_END,
277
+ ["| File | Lines | Status |", "|------|------|------|", files_table],
278
+ )
279
+ row = f"| {new_session} | {today} | {title} | {commit_display} |"
280
+ lines = _insert_session_history(lines, row)
281
+ index_file.write_text("\n".join(lines), encoding="utf-8")
291
282
  print("[OK] index.md updated.", file=sys.stderr)
292
283
  return True
293
284
 
@@ -326,87 +317,109 @@ def _auto_commit_workspace(repo_root: Path) -> None:
326
317
  print(f"[WARN] Metadata auto-commit failed: {commit_result.stderr.strip()}", file=sys.stderr)
327
318
 
328
319
 
329
- def add_session(
330
- title: str,
331
- commit: str = "-",
332
- summary: str = "(add summary)",
333
- extra_content: str = "(add details)",
334
- auto_commit: bool = False,
335
- ) -> int:
336
- """Add a new session."""
337
- repo_root = get_repo_root()
338
- ensure_developer(repo_root)
339
-
320
+ def _session_workspace(repo_root: Path):
340
321
  developer = get_developer(repo_root)
341
322
  if not developer:
342
323
  print("Error: developer identity has not been initialized.", file=sys.stderr)
343
- return 1
344
-
324
+ return None
345
325
  dev_dir = get_workspace_dir(repo_root)
346
- if not dev_dir:
326
+ if dev_dir is None or not dev_dir.is_dir():
347
327
  print("Error: workspace directory not found.", file=sys.stderr)
348
- return 1
349
-
350
- max_lines = get_max_journal_lines(repo_root)
351
-
328
+ return None
352
329
  index_file = dev_dir / "index.md"
353
- today = datetime.now().strftime("%Y-%m-%d")
354
-
355
330
  journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
356
- current_session = get_current_session(index_file)
357
- new_session = current_session + 1
331
+ if not index_file.is_file():
332
+ print(f"Error: workspace index is missing: {index_file}", file=sys.stderr)
333
+ return None
334
+ if journal_file is None:
335
+ print(f"Error: workspace journal is missing: {dev_dir}", file=sys.stderr)
336
+ return None
337
+ return developer, dev_dir, index_file, journal_file, current_num, current_lines
358
338
 
359
- session_content = generate_session_content(
360
- new_session, title, commit, summary, extra_content, today
361
- )
362
- content_lines = len(session_content.splitlines())
363
339
 
364
- print("========================================", file=sys.stderr)
365
- print("Add Session Record", file=sys.stderr)
366
- print("========================================", file=sys.stderr)
367
- print("", file=sys.stderr)
368
- print(f"Session: {new_session}", file=sys.stderr)
340
+ def _print_session_preview(
341
+ new_session: int,
342
+ title: str,
343
+ commit: str,
344
+ current_num: int,
345
+ current_lines: int,
346
+ content_lines: int,
347
+ ) -> None:
348
+ print("=" * 40, "Add Session Record", "=" * 40, sep="\n", file=sys.stderr)
349
+ print(f"\nSession: {new_session}", file=sys.stderr)
369
350
  print(f"Title: {title}", file=sys.stderr)
370
- print(f"Commit: {commit}", file=sys.stderr)
371
- print("", file=sys.stderr)
351
+ print(f"Commit: {commit}\n", file=sys.stderr)
372
352
  print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
373
353
  print(f"Current lines: {current_lines}", file=sys.stderr)
374
354
  print(f"New content lines: {content_lines}", file=sys.stderr)
375
- print(f"Total lines after append: {current_lines + content_lines}", file=sys.stderr)
376
- print("", file=sys.stderr)
377
-
378
- target_file = journal_file
379
- target_num = current_num
355
+ print(f"Total lines after append: {current_lines + content_lines}\n", file=sys.stderr)
380
356
 
381
- if current_lines + content_lines > max_lines:
382
- target_num = current_num + 1
383
- print(f"[!] Over {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
384
- target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines)
385
- print(f"Created: {target_file}", file=sys.stderr)
386
357
 
387
- # Append session content
388
- if target_file:
389
- with target_file.open("a", encoding="utf-8") as f:
390
- f.write(session_content)
391
- print(f"[OK] Appended session to {target_file.name}", file=sys.stderr)
392
-
393
- print("", file=sys.stderr)
358
+ def _session_target(
359
+ journal_file: Path,
360
+ current_num: int,
361
+ current_lines: int,
362
+ content_lines: int,
363
+ max_lines: int,
364
+ dev_dir: Path,
365
+ developer: str,
366
+ today: str,
367
+ ) -> tuple[Path, int]:
368
+ if current_lines + content_lines <= max_lines:
369
+ return journal_file, current_num
370
+ target_num = current_num + 1
371
+ print(f"[!] Over {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
372
+ target_file = create_new_journal_file(
373
+ dev_dir, target_num, developer, today, max_lines
374
+ )
375
+ print(f"Created: {target_file}", file=sys.stderr)
376
+ return target_file, target_num
394
377
 
395
- # Update index.md
396
- active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
397
- if not update_index(index_file, dev_dir, title, commit, new_session, active_file, today):
398
- return 1
399
378
 
400
- print("", file=sys.stderr)
401
- print("========================================", file=sys.stderr)
379
+ def _print_session_success(new_session: int, target_file: Path) -> None:
380
+ print("\n" + "=" * 40, file=sys.stderr)
402
381
  print(f"[OK] Session {new_session} recorded successfully.", file=sys.stderr)
403
- print("========================================", file=sys.stderr)
404
- print("", file=sys.stderr)
382
+ print("=" * 40 + "\n", file=sys.stderr)
405
383
  print("Updated files:", file=sys.stderr)
406
- print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr)
384
+ print(f" - {target_file.name}", file=sys.stderr)
407
385
  print(" - index.md", file=sys.stderr)
408
386
 
409
- # Auto-commit workspace changes only when explicitly requested.
387
+
388
+ def add_session(
389
+ title: str,
390
+ commit: str = "-",
391
+ summary: str = "(add summary)",
392
+ extra_content: str = "(add details)",
393
+ auto_commit: bool = False,
394
+ ) -> int:
395
+ """Add a new session."""
396
+ repo_root = get_repo_root()
397
+ ensure_developer(repo_root)
398
+ workspace = _session_workspace(repo_root)
399
+ if workspace is None:
400
+ return 1
401
+ developer, dev_dir, index_file, journal_file, current_num, current_lines = workspace
402
+ max_lines = get_max_journal_lines(repo_root)
403
+ today = datetime.now().strftime("%Y-%m-%d")
404
+ new_session = get_current_session(index_file) + 1
405
+ session_content = generate_session_content(
406
+ new_session, title, commit, summary, extra_content, today
407
+ )
408
+ content_lines = len(session_content.splitlines())
409
+ _print_session_preview(
410
+ new_session, title, commit, current_num, current_lines, content_lines
411
+ )
412
+ target_file, target_num = _session_target(
413
+ journal_file, current_num, current_lines, content_lines,
414
+ max_lines, dev_dir, developer, today,
415
+ )
416
+ with target_file.open("a", encoding="utf-8") as stream:
417
+ stream.write(session_content)
418
+ print(f"[OK] Appended session to {target_file.name}\n", file=sys.stderr)
419
+ active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
420
+ if not update_index(index_file, dev_dir, title, commit, new_session, active_file, today):
421
+ return 1
422
+ _print_session_success(new_session, target_file)
410
423
  if auto_commit:
411
424
  print("", file=sys.stderr)
412
425
  _auto_commit_workspace(repo_root)
@@ -16,6 +16,7 @@ else:
16
16
  import _bootstrap # noqa: F401
17
17
 
18
18
  from application.task_context import (
19
+ PLANNED_FILE_HINT,
19
20
  TaskContextService,
20
21
  detect_installed_platforms as _detect_installed_platforms,
21
22
  discover_spec_files as _discover_spec_files,
@@ -42,6 +43,7 @@ from application.batch_execution import (
42
43
  )
43
44
  from commands.task_archive_commands import cmd_archive
44
45
  from commands.task_context_commands import (
46
+ cmd_add_planned_file,
45
47
  cmd_add_context,
46
48
  cmd_init_context,
47
49
  cmd_list_context,
@@ -205,6 +207,15 @@ def _task_context_validation_issues(
205
207
  )
206
208
 
207
209
 
210
+ def _refresh_task_artifact_placeholders(
211
+ task_dir: Path,
212
+ repo_root: Path,
213
+ ) -> list[str]:
214
+ service = TaskContextService(repo_root)
215
+ service.ensure_task_artifact_placeholders(task_dir)
216
+ return _task_context_validation_issues(task_dir, repo_root)
217
+
218
+
208
219
  def _optional_readiness_blockers(
209
220
  repo_root: Path,
210
221
  task_dir: Path,
@@ -243,6 +254,8 @@ def _start_preflight(
243
254
  task_dir,
244
255
  repo_root,
245
256
  )
257
+ if validation_issues:
258
+ validation_issues = _refresh_task_artifact_placeholders(task_dir, repo_root)
246
259
  if validation_issues:
247
260
  return LifecyclePreflightFailure(
248
261
  code="TASK-CONTEXT-001",
@@ -250,7 +263,7 @@ def _start_preflight(
250
263
  blockers=tuple(validation_issues),
251
264
  hint=(
252
265
  "run ./.cowork-flow/run task validate <dir> and fix the "
253
- "reported issues"
266
+ f"reported issues. {PLANNED_FILE_HINT}"
254
267
  ),
255
268
  )
256
269
 
@@ -302,6 +315,102 @@ def _report_lifecycle_repository_error(
302
315
  return 1
303
316
 
304
317
 
318
+ def _resolve_start_task(
319
+ task_input: str,
320
+ repo_root: Path,
321
+ ) -> Path | None:
322
+ full_path = _resolve_task_dir(task_input, repo_root)
323
+ if full_path.is_dir():
324
+ return full_path
325
+ print(
326
+ colored(
327
+ f"Error: Task not found: {task_input}",
328
+ Colors.RED,
329
+ )
330
+ )
331
+ print(
332
+ "Hint: Use task name (e.g., 'my-task') or full path "
333
+ f"(e.g., '{DIR_WORKFLOW}/tasks/01-31-my-task')"
334
+ )
335
+ return None
336
+
337
+
338
+ def _run_auto_start(
339
+ args: argparse.Namespace,
340
+ full_path: Path,
341
+ preflight,
342
+ ) -> int:
343
+ failure = preflight(full_path)
344
+ if failure is not None:
345
+ return _report_lifecycle_preflight(failure)
346
+ from common.task.batch_mode import run_batch_entry
347
+
348
+ return run_batch_entry(get_repo_root(), full_path, args)
349
+
350
+
351
+ def _report_start_failure(
352
+ result: LifecycleResult,
353
+ service: TaskLifecycleService,
354
+ ) -> int:
355
+ if result.title:
356
+ return _report_lifecycle_preflight(result)
357
+ if result.code == "LIFECYCLE-TRANSITION-001":
358
+ _print_transition_blockers(list(result.blockers))
359
+ return 1
360
+ if result.code == "LIFECYCLE-GATE-001":
361
+ return _report_gate_block(
362
+ "Spec enforcement blocked task start",
363
+ result.gate_result,
364
+ service.gate_runner,
365
+ )
366
+ if result.code == "LIFECYCLE-CONTEXT-001":
367
+ return _report_missing_session_context()
368
+ if result.repository_error is not None:
369
+ return _report_lifecycle_repository_error(result)
370
+ print(
371
+ colored("Error: Task lifecycle failed", Colors.RED),
372
+ file=sys.stderr,
373
+ )
374
+ return 1
375
+
376
+
377
+ def _report_missing_session_context() -> int:
378
+ print(
379
+ colored(
380
+ "Error: Missing session context. Set "
381
+ "COWORK_FLOW_CONTEXT_ID or run inside a "
382
+ "supported host session.",
383
+ Colors.RED,
384
+ ),
385
+ file=sys.stderr,
386
+ )
387
+ return 1
388
+
389
+
390
+ def _report_start_success(
391
+ result: LifecycleResult,
392
+ repo_root: Path,
393
+ full_path: Path,
394
+ ) -> None:
395
+ task_dir = result.active_task_path or _display_task_path(
396
+ repo_root,
397
+ full_path,
398
+ )
399
+ print(
400
+ colored(
401
+ f"[OK] Active session task set to: {task_dir}",
402
+ Colors.GREEN,
403
+ )
404
+ )
405
+ print()
406
+ print(
407
+ colored(
408
+ "Fixed agents will load context from this task's jsonl files.",
409
+ Colors.BLUE,
410
+ )
411
+ )
412
+
413
+
305
414
  def cmd_start(args: argparse.Namespace) -> int:
306
415
  """Set the active task for this session."""
307
416
  repo_root = get_repo_root()
@@ -315,18 +424,8 @@ def cmd_start(args: argparse.Namespace) -> int:
315
424
  )
316
425
  return 1
317
426
 
318
- full_path = _resolve_task_dir(task_input, repo_root)
319
- if not full_path.is_dir():
320
- print(
321
- colored(
322
- f"Error: Task not found: {task_input}",
323
- Colors.RED,
324
- )
325
- )
326
- print(
327
- "Hint: Use task name (e.g., 'my-task') or full path "
328
- f"(e.g., '{DIR_WORKFLOW}/tasks/01-31-my-task')"
329
- )
427
+ full_path = _resolve_start_task(task_input, repo_root)
428
+ if full_path is None:
330
429
  return 1
331
430
 
332
431
  def preflight(
@@ -335,63 +434,14 @@ def cmd_start(args: argparse.Namespace) -> int:
335
434
  return _start_preflight(task_dir, repo_root)
336
435
 
337
436
  if getattr(args, "auto", False):
338
- failure = preflight(full_path)
339
- if failure is not None:
340
- return _report_lifecycle_preflight(failure)
341
- from common.task.batch_mode import run_batch_entry
342
-
343
- return run_batch_entry(repo_root, full_path, args)
437
+ return _run_auto_start(args, full_path, preflight)
344
438
 
345
439
  service = TaskLifecycleService(repo_root)
346
440
  result = service.start(full_path, preflight=preflight)
347
441
  if not result.ok:
348
- if result.title:
349
- return _report_lifecycle_preflight(result)
350
- if result.code == "LIFECYCLE-TRANSITION-001":
351
- _print_transition_blockers(list(result.blockers))
352
- return 1
353
- if result.code == "LIFECYCLE-GATE-001":
354
- return _report_gate_block(
355
- "Spec enforcement blocked task start",
356
- result.gate_result,
357
- service.gate_runner,
358
- )
359
- if result.code == "LIFECYCLE-CONTEXT-001":
360
- print(
361
- colored(
362
- "Error: Missing session context. Set "
363
- "COWORK_FLOW_CONTEXT_ID or run inside a "
364
- "supported host session.",
365
- Colors.RED,
366
- ),
367
- file=sys.stderr,
368
- )
369
- return 1
370
- if result.repository_error is not None:
371
- return _report_lifecycle_repository_error(result)
372
- print(
373
- colored("Error: Task lifecycle failed", Colors.RED),
374
- file=sys.stderr,
375
- )
376
- return 1
442
+ return _report_start_failure(result, service)
377
443
 
378
- task_dir = result.active_task_path or _display_task_path(
379
- repo_root,
380
- full_path,
381
- )
382
- print(
383
- colored(
384
- f"[OK] Active session task set to: {task_dir}",
385
- Colors.GREEN,
386
- )
387
- )
388
- print()
389
- print(
390
- colored(
391
- "Fixed agents will load context from this task's jsonl files.",
392
- Colors.BLUE,
393
- )
394
- )
444
+ _report_start_success(result, repo_root, full_path)
395
445
  _run_hooks("after_start", full_path / FILE_TASK_JSON, repo_root)
396
446
  return 0
397
447
 
@@ -511,6 +561,7 @@ def cmd_complete(args: argparse.Namespace) -> int:
511
561
  result = service.complete(
512
562
  task_dir,
513
563
  completed_at=datetime.now().strftime("%Y-%m-%d"),
564
+ allow_spec_file_modifications=is_main_session(repo_root),
514
565
  )
515
566
  if not result.ok:
516
567
  if result.code == "LIFECYCLE-TRANSITION-001":
@@ -592,6 +643,60 @@ def cmd_current(args: argparse.Namespace) -> int:
592
643
  return 0
593
644
 
594
645
 
646
+ WORKER_BLOCKED_COMMANDS = frozenset((
647
+ "create",
648
+ "init-context",
649
+ "add-context",
650
+ "add-planned-file",
651
+ "start",
652
+ "batch-resume",
653
+ "batch-record-result",
654
+ "review",
655
+ "complete",
656
+ "finish",
657
+ "archive",
658
+ "add-subtask",
659
+ "remove-subtask",
660
+ ))
661
+
662
+ COMMANDS = {
663
+ "create": cmd_create,
664
+ "init-context": cmd_init_context,
665
+ "add-context": cmd_add_context,
666
+ "add-planned-file": cmd_add_planned_file,
667
+ "validate": cmd_validate,
668
+ "list-context": cmd_list_context,
669
+ "start": cmd_start,
670
+ "batch-resume": cmd_batch_resume,
671
+ "batch-record-result": cmd_batch_record_result,
672
+ "current": cmd_current,
673
+ "review": cmd_review,
674
+ "complete": cmd_complete,
675
+ "next": cmd_next,
676
+ "finish": cmd_finish,
677
+ "archive": cmd_archive,
678
+ "add-subtask": cmd_add_subtask,
679
+ "remove-subtask": cmd_remove_subtask,
680
+ "list": cmd_list,
681
+ "list-archive": cmd_list_archive,
682
+ }
683
+
684
+
685
+ def _worker_command_blocked(execution_context, command: str) -> bool:
686
+ if not execution_context.is_worker or command not in WORKER_BLOCKED_COMMANDS:
687
+ return False
688
+ print(
689
+ worker_command_block_message(
690
+ execution_context,
691
+ f"task {command}",
692
+ "Workers must not activate, archive, or mutate "
693
+ "cowork-flow task state.",
694
+ ),
695
+ file=sys.stderr,
696
+ )
697
+ return True
698
+
699
+
595
700
  def main() -> int:
596
701
  """CLI entry point."""
597
702
  args = build_parser().parse_args()
@@ -600,53 +705,10 @@ def main() -> int:
600
705
  show_usage()
601
706
  return 1
602
707
 
603
- worker_blocked_commands = {
604
- "create",
605
- "init-context",
606
- "add-context",
607
- "start",
608
- "batch-resume",
609
- "batch-record-result",
610
- "review",
611
- "complete",
612
- "finish",
613
- "archive",
614
- "add-subtask",
615
- "remove-subtask",
616
- }
617
- if execution_context.is_worker and args.command in worker_blocked_commands:
618
- print(
619
- worker_command_block_message(
620
- execution_context,
621
- f"task {args.command}",
622
- "Workers must not activate, archive, or mutate "
623
- "cowork-flow task state.",
624
- ),
625
- file=sys.stderr,
626
- )
708
+ if _worker_command_blocked(execution_context, args.command):
627
709
  return 2
628
710
 
629
- commands = {
630
- "create": cmd_create,
631
- "init-context": cmd_init_context,
632
- "add-context": cmd_add_context,
633
- "validate": cmd_validate,
634
- "list-context": cmd_list_context,
635
- "start": cmd_start,
636
- "batch-resume": cmd_batch_resume,
637
- "batch-record-result": cmd_batch_record_result,
638
- "current": cmd_current,
639
- "review": cmd_review,
640
- "complete": cmd_complete,
641
- "next": cmd_next,
642
- "finish": cmd_finish,
643
- "archive": cmd_archive,
644
- "add-subtask": cmd_add_subtask,
645
- "remove-subtask": cmd_remove_subtask,
646
- "list": cmd_list,
647
- "list-archive": cmd_list_archive,
648
- }
649
- command = commands.get(args.command)
711
+ command = COMMANDS.get(args.command)
650
712
  if command is None:
651
713
  show_usage()
652
714
  return 1