cowork-flow 0.0.19 → 0.0.21

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 (69) hide show
  1. package/README.md +65 -20
  2. package/package.json +1 -1
  3. package/src/cli.js +109 -2
  4. package/src/commands/init.js +61 -4
  5. package/src/commands/sync.js +10 -1
  6. package/src/lib/copy-template.js +38 -7
  7. package/src/lib/platforms.js +73 -0
  8. package/template/.agent/skills/brainstorming/SKILL.md +13 -9
  9. package/template/.agent/skills/finish-work/SKILL.md +3 -3
  10. package/template/.agent/skills/meta/SKILL.md +1 -1
  11. package/template/.agent/skills/start/SKILL.md +38 -14
  12. package/template/.agent/skills/writing-plans/SKILL.md +1 -1
  13. package/template/.claude/agents/cowork-check.md +41 -0
  14. package/template/.claude/agents/cowork-implement.md +40 -0
  15. package/template/.claude/agents/cowork-research.md +40 -0
  16. package/template/.claude/commands/cowork-check.md +27 -0
  17. package/template/.claude/commands/cowork-implement.md +27 -0
  18. package/template/.claude/commands/cowork-research.md +27 -0
  19. package/template/.claude/hooks/inject-workflow-state.py +298 -0
  20. package/template/.claude/settings.json +24 -0
  21. package/template/.claude/skills/before-dev/SKILL.md +31 -0
  22. package/template/.claude/skills/brainstorming/SKILL.md +32 -0
  23. package/template/.claude/skills/break-loop/SKILL.md +26 -0
  24. package/template/.claude/skills/check/SKILL.md +29 -0
  25. package/template/.claude/skills/continue/SKILL.md +29 -0
  26. package/template/.claude/skills/finish-work/SKILL.md +32 -0
  27. package/template/.claude/skills/meta/SKILL.md +30 -0
  28. package/template/.claude/skills/python-design/SKILL.md +27 -0
  29. package/template/.claude/skills/start/SKILL.md +78 -0
  30. package/template/.claude/skills/update-spec/SKILL.md +39 -0
  31. package/template/.claude/skills/writing-plans/SKILL.md +70 -0
  32. package/template/.codex/agents/cowork-check.toml +17 -22
  33. package/template/.codex/agents/cowork-implement.toml +18 -23
  34. package/template/.codex/agents/cowork-research.toml +14 -19
  35. package/template/.codex/agents/default.toml +27 -0
  36. package/template/.codex/agents/explorer.toml +28 -0
  37. package/template/.codex/agents/worker.toml +29 -0
  38. package/template/.codex/hooks/inject-workflow-state.py +172 -90
  39. package/template/.cowork-flow/.version +1 -1
  40. package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
  41. package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
  42. package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
  43. package/template/.cowork-flow/config.yaml +4 -1
  44. package/template/.cowork-flow/scripts/add_session.py +20 -7
  45. package/template/.cowork-flow/scripts/common/active_task.py +212 -1
  46. package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
  47. package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
  48. package/template/.cowork-flow/scripts/common/readiness.py +249 -0
  49. package/template/.cowork-flow/scripts/doctor.py +417 -29
  50. package/template/.cowork-flow/scripts/project_context.py +230 -0
  51. package/template/.cowork-flow/scripts/run.py +3 -0
  52. package/template/.cowork-flow/scripts/subagent.py +152 -238
  53. package/template/.cowork-flow/scripts/task.py +330 -17
  54. package/template/.cowork-flow/spec/adapter.schema.json +132 -0
  55. package/template/.cowork-flow/spec/capabilities.md +34 -0
  56. package/template/.cowork-flow/spec/entry-contract.md +45 -0
  57. package/template/.cowork-flow/spec/registry.json +54 -0
  58. package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
  59. package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
  60. package/template/.cowork-flow/workflow.md +154 -190
  61. package/template/.opencode/agents/cowork-check.md +46 -0
  62. package/template/.opencode/agents/cowork-implement.md +45 -0
  63. package/template/.opencode/agents/cowork-research.md +46 -0
  64. package/template/.opencode/commands/cowork-check.md +26 -0
  65. package/template/.opencode/commands/cowork-implement.md +26 -0
  66. package/template/.opencode/commands/cowork-research.md +26 -0
  67. package/template/.opencode/plugins/cowork-flow.js +317 -0
  68. package/template/CLAUDE.md +5 -0
  69. package/template/.agent/skills/entry-boundary/SKILL.md +0 -81
@@ -10,6 +10,8 @@ Usage:
10
10
  ./.cowork-flow/run task validate <dir> # Validate jsonl files
11
11
  ./.cowork-flow/run task list-context <dir> # List jsonl entries
12
12
  ./.cowork-flow/run task start <dir> # Set active session task
13
+ ./.cowork-flow/run task review [dir] # Mark task ready for check
14
+ ./.cowork-flow/run task complete [dir] # Mark task completed
13
15
  ./.cowork-flow/run task current # Show active session task
14
16
  ./.cowork-flow/run task finish # Clear active session task
15
17
  ./.cowork-flow/run task archive <task-name> # Archive completed task
@@ -67,6 +69,7 @@ from common.execution_context import (
67
69
 
68
70
  CONTEXT_JSONL_FILES = ["implement.jsonl", "check.jsonl", "debug.jsonl"]
69
71
  DONE_STATUSES = ("completed", "done")
72
+ CHECK_STATUSES = ("review", "checking")
70
73
 
71
74
 
72
75
  # =============================================================================
@@ -355,7 +358,24 @@ def get_implement_frontend() -> list[dict]:
355
358
  ]
356
359
 
357
360
 
358
- def _skill_path(name: str) -> str:
361
+ def _detect_installed_platforms(repo_root: Path | None = None) -> list[str]:
362
+ """Detect installed host platform assets in the current project."""
363
+ root = repo_root or get_repo_root()
364
+ platforms: list[str] = []
365
+ if (root / ".codex").is_dir():
366
+ platforms.append("codex")
367
+ if (root / ".opencode").is_dir():
368
+ platforms.append("opencode")
369
+ if (root / ".claude").is_dir() or (root / "CLAUDE.md").is_file():
370
+ platforms.append("claude-code")
371
+ return platforms
372
+
373
+ def _use_claude_skill_context(repo_root: Path | None = None) -> bool:
374
+ return _detect_installed_platforms(repo_root) == ["claude-code"]
375
+
376
+ def _skill_path(name: str, repo_root: Path | None = None) -> str:
377
+ if _use_claude_skill_context(repo_root):
378
+ return f".claude/skills/{name}/SKILL.md"
359
379
  return f"{DIR_AGENT}/skills/{name}/SKILL.md"
360
380
 
361
381
 
@@ -413,7 +433,11 @@ def _task_start_blockers(task_dir: Path) -> list[str]:
413
433
  return blockers
414
434
 
415
435
 
416
- def _task_context_validation_issues(task_dir: Path, repo_root: Path) -> list[str]:
436
+ def _task_context_validation_issues(
437
+ task_dir: Path,
438
+ repo_root: Path,
439
+ quiet: bool = False,
440
+ ) -> list[str]:
417
441
  """返回任务上下文文件的校验问题。"""
418
442
  issues: list[str] = []
419
443
 
@@ -422,7 +446,7 @@ def _task_context_validation_issues(task_dir: Path, repo_root: Path) -> list[str
422
446
  if not _read_text_if_present(jsonl_file):
423
447
  continue
424
448
 
425
- error_count = _validate_jsonl(jsonl_file, repo_root)
449
+ error_count = _validate_jsonl(jsonl_file, repo_root, quiet=quiet)
426
450
  if error_count > 0:
427
451
  issues.append(f"{jsonl_name} has {error_count} validation error(s)")
428
452
 
@@ -733,13 +757,14 @@ def cmd_validate(args: argparse.Namespace) -> int:
733
757
  return 1
734
758
 
735
759
 
736
- def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
760
+ def _validate_jsonl(jsonl_file: Path, repo_root: Path, quiet: bool = False) -> int:
737
761
  """Validate a single JSONL file."""
738
762
  file_name = jsonl_file.name
739
763
  errors = 0
740
764
 
741
765
  if not jsonl_file.is_file():
742
- print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
766
+ if not quiet:
767
+ print(f" {colored(f'{file_name}: not found (skipped)', Colors.YELLOW)}")
743
768
  return 0
744
769
 
745
770
  line_num = 0
@@ -752,7 +777,8 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
752
777
  try:
753
778
  data = json.loads(line)
754
779
  except json.JSONDecodeError:
755
- print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}")
780
+ if not quiet:
781
+ print(f" {colored(f'{file_name}:{line_num}: Invalid JSON', Colors.RED)}")
756
782
  errors += 1
757
783
  continue
758
784
 
@@ -760,24 +786,28 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
760
786
  entry_type = data.get("type", "file")
761
787
 
762
788
  if not file_path:
763
- print(f" {colored(f'{file_name}:{line_num}: Missing file field', Colors.RED)}")
789
+ if not quiet:
790
+ print(f" {colored(f'{file_name}:{line_num}: Missing file field', Colors.RED)}")
764
791
  errors += 1
765
792
  continue
766
793
 
767
794
  full_path = repo_root / file_path
768
795
  if entry_type == "directory":
769
796
  if not full_path.is_dir():
770
- print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
797
+ if not quiet:
798
+ print(f" {colored(f'{file_name}:{line_num}: Directory not found: {file_path}', Colors.RED)}")
771
799
  errors += 1
772
800
  else:
773
801
  if not full_path.is_file():
774
- print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
802
+ if not quiet:
803
+ print(f" {colored(f'{file_name}:{line_num}: File not found: {file_path}', Colors.RED)}")
775
804
  errors += 1
776
805
 
777
- if errors == 0:
778
- print(f" {colored(f'{file_name}: [OK] ({entry_count} entries)', Colors.GREEN)}")
779
- else:
780
- print(f" {colored(f'{file_name}: [FAIL] ({errors} errors)', Colors.RED)}")
806
+ if not quiet:
807
+ if errors == 0:
808
+ print(f" {colored(f'{file_name}: [OK] ({entry_count} entries)', Colors.GREEN)}")
809
+ else:
810
+ print(f" {colored(f'{file_name}: [FAIL] ({errors} errors)', Colors.RED)}")
781
811
 
782
812
  return errors
783
813
 
@@ -874,6 +904,22 @@ def cmd_start(args: argparse.Namespace) -> int:
874
904
  )
875
905
  return 1
876
906
 
907
+ readiness_blockers = _optional_readiness_blockers(repo_root, full_path)
908
+ if readiness_blockers:
909
+ print(colored("Error: Task readiness failed", Colors.RED), file=sys.stderr)
910
+ for blocker in readiness_blockers:
911
+ print(f" - {blocker}", file=sys.stderr)
912
+ print(
913
+ "Hint: run ./.cowork-flow/run task next <dir> and complete the reported readiness artifacts",
914
+ file=sys.stderr,
915
+ )
916
+ return 1
917
+
918
+ task_json_path = full_path / FILE_TASK_JSON
919
+ task_data = _load_task_data_or_report(full_path)
920
+ if task_data is None:
921
+ return 1
922
+
877
923
  # Convert to relative path for storage
878
924
  try:
879
925
  task_dir = full_path.relative_to(repo_root).as_posix()
@@ -891,15 +937,52 @@ def cmd_start(args: argparse.Namespace) -> int:
891
937
  )
892
938
  return 1
893
939
 
940
+ task_data["status"] = "in_progress"
941
+ if not _write_json_or_report(task_json_path, task_data, "task metadata"):
942
+ clear_active_task(repo_root)
943
+ return 1
944
+
894
945
  print(colored(f"[OK] Active session task set to: {task_dir}", Colors.GREEN))
895
946
  print()
896
947
  print(colored("Fixed agents will load context from this task's jsonl files.", Colors.BLUE))
897
948
 
898
- task_json_path = full_path / FILE_TASK_JSON
899
949
  _run_hooks("after_start", task_json_path, repo_root)
900
950
  return 0
901
951
 
902
952
 
953
+ def cmd_review(args: argparse.Namespace) -> int:
954
+ """Mark a task ready for check/review."""
955
+ repo_root = get_repo_root()
956
+ task_dir = _resolve_status_task_dir(args, repo_root)
957
+ if task_dir is None:
958
+ return 1
959
+
960
+ if not _set_task_status(task_dir, "review"):
961
+ return 1
962
+
963
+ task_path = _display_task_path(repo_root, task_dir)
964
+ print(colored(f"[OK] Task marked for check: {task_path}", Colors.GREEN))
965
+ print(f"Next: ./.cowork-flow/run task next {task_path}")
966
+ return 0
967
+
968
+
969
+ def cmd_complete(args: argparse.Namespace) -> int:
970
+ """Mark a task completed after final check."""
971
+ repo_root = get_repo_root()
972
+ task_dir = _resolve_status_task_dir(args, repo_root)
973
+ if task_dir is None:
974
+ return 1
975
+
976
+ today = datetime.now().strftime("%Y-%m-%d")
977
+ if not _set_task_status(task_dir, "completed", completed_at=today):
978
+ return 1
979
+
980
+ task_path = _display_task_path(repo_root, task_dir)
981
+ print(colored(f"[OK] Task marked completed: {task_path}", Colors.GREEN))
982
+ print(f"Next: ./.cowork-flow/run task next {task_path}")
983
+ return 0
984
+
985
+
903
986
  def cmd_finish(args: argparse.Namespace) -> int:
904
987
  """Clear active task for this session."""
905
988
  repo_root = get_repo_root()
@@ -941,6 +1024,208 @@ def cmd_current(args: argparse.Namespace) -> int:
941
1024
  return 0
942
1025
 
943
1026
 
1027
+ def _display_task_path(repo_root: Path, task_dir: Path) -> str:
1028
+ try:
1029
+ return task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
1030
+ except ValueError:
1031
+ return str(task_dir)
1032
+
1033
+
1034
+ def _load_task_status(task_dir: Path) -> str:
1035
+ task_json = task_dir / FILE_TASK_JSON
1036
+ try:
1037
+ data = json.loads(task_json.read_text(encoding="utf-8"))
1038
+ except (OSError, json.JSONDecodeError):
1039
+ return "stale"
1040
+ status = data.get("status") if isinstance(data, dict) else None
1041
+ return status.strip() if isinstance(status, str) and status.strip() else "unknown"
1042
+
1043
+
1044
+ def _load_task_data_or_report(task_dir: Path) -> dict | None:
1045
+ task_json = task_dir / FILE_TASK_JSON
1046
+ data = _read_json_file(task_json)
1047
+ if data is None:
1048
+ print(colored(f"Error: Failed to read task metadata: {task_json}", Colors.RED), file=sys.stderr)
1049
+ return None
1050
+ return data
1051
+
1052
+
1053
+ def _set_task_status(task_dir: Path, status: str, completed_at: str | None = None) -> bool:
1054
+ task_json = task_dir / FILE_TASK_JSON
1055
+ data = _load_task_data_or_report(task_dir)
1056
+ if data is None:
1057
+ return False
1058
+
1059
+ data["status"] = status
1060
+ if status in DONE_STATUSES:
1061
+ data["completedAt"] = completed_at or datetime.now().strftime("%Y-%m-%d")
1062
+
1063
+ return _write_json_or_report(task_json, data, "task metadata")
1064
+
1065
+
1066
+ def _resolve_status_task_dir(args: argparse.Namespace, repo_root: Path) -> Path | None:
1067
+ target_input = getattr(args, "dir", None)
1068
+ if target_input:
1069
+ task_dir = _resolve_task_dir(target_input, repo_root)
1070
+ else:
1071
+ active = get_active_task(repo_root)
1072
+ if not active.context_key:
1073
+ print(
1074
+ colored(
1075
+ "Error: Missing session context. Set COWORK_FLOW_CONTEXT_ID or pass a task dir.",
1076
+ Colors.RED,
1077
+ ),
1078
+ file=sys.stderr,
1079
+ )
1080
+ return None
1081
+ if not active.task_path:
1082
+ print(colored("Error: No active task set for this session", Colors.RED), file=sys.stderr)
1083
+ return None
1084
+ task_dir = repo_root / active.task_path
1085
+
1086
+ if not task_dir.is_dir():
1087
+ print(colored(f"Error: Task not found: {target_input or task_dir}", Colors.RED), file=sys.stderr)
1088
+ return None
1089
+ if not (task_dir / FILE_TASK_JSON).is_file():
1090
+ print(colored(f"Error: task.json not found: {task_dir}", Colors.RED), file=sys.stderr)
1091
+ return None
1092
+ return task_dir
1093
+
1094
+
1095
+ def _optional_readiness_blockers(repo_root: Path, task_dir: Path) -> list[str]:
1096
+ try:
1097
+ from common.readiness import task_readiness_blockers # type: ignore[import-not-found]
1098
+ except Exception:
1099
+ return []
1100
+ try:
1101
+ blockers = task_readiness_blockers(repo_root, task_dir)
1102
+ except Exception:
1103
+ return ["readiness check failed; run task validate and inspect linked change"]
1104
+ return [str(blocker) for blocker in blockers if str(blocker).strip()]
1105
+
1106
+
1107
+ def _task_next_blockers(repo_root: Path, task_dir: Path) -> list[str]:
1108
+ blockers = _task_start_blockers(task_dir)
1109
+ blockers.extend(_task_context_validation_issues(task_dir, repo_root, quiet=True))
1110
+ blockers.extend(_optional_readiness_blockers(repo_root, task_dir))
1111
+ return blockers
1112
+
1113
+
1114
+ def _print_blockers(blockers: list[str]) -> None:
1115
+ if not blockers:
1116
+ print("Blockers: none")
1117
+ return
1118
+ print("Blockers:")
1119
+ for blocker in blockers:
1120
+ print(f" - {blocker}")
1121
+
1122
+
1123
+ def cmd_next(args: argparse.Namespace) -> int:
1124
+ """Print the next safe workflow action without mutating state."""
1125
+ repo_root = get_repo_root()
1126
+ target_input = getattr(args, "dir", None)
1127
+ is_active_task = False
1128
+
1129
+ print("Workflow Next")
1130
+ if target_input:
1131
+ task_dir = _resolve_task_dir(target_input, repo_root)
1132
+ task_path = _display_task_path(repo_root, task_dir)
1133
+ source = "argument"
1134
+ else:
1135
+ active = get_active_task(repo_root)
1136
+ source = f"{active.source}:{active.context_key or '-'}"
1137
+ if not active.task_path:
1138
+ print("Status: no_task")
1139
+ print(f"Source: {source}")
1140
+ print("Next action: create or start a task before repository changes")
1141
+ print('Command: ./.cowork-flow/run task create "<title>" --slug <task-name>')
1142
+ print("Then: ./.cowork-flow/run task start <task-dir>")
1143
+ print(
1144
+ "Delegated subtask: execute the delegated prompt directly; do not start or resume workflow."
1145
+ )
1146
+ return 0
1147
+ task_path = active.task_path
1148
+ task_dir = repo_root / task_path
1149
+ is_active_task = True
1150
+
1151
+ print(f"Task: {task_path}")
1152
+ if not task_dir.is_dir():
1153
+ print("Status: stale")
1154
+ print(f"Source: {source}")
1155
+ print("Next action: clear or replace the missing active task")
1156
+ print("Command: ./.cowork-flow/run task list")
1157
+ print("Blockers:")
1158
+ print(f" - task directory not found: {task_path}")
1159
+ return 0
1160
+
1161
+ status = _load_task_status(task_dir)
1162
+ blockers = _task_next_blockers(repo_root, task_dir)
1163
+ print(f"Status: {status}")
1164
+ print(f"Source: {source}")
1165
+
1166
+ if status == "planning":
1167
+ if blockers:
1168
+ print("Next action: finish planning prerequisites before starting task")
1169
+ print(f"Command: ./.cowork-flow/run task init-context {task_path} <dev_type>")
1170
+ print(f"Then: ./.cowork-flow/run task start {task_path}")
1171
+ elif is_active_task:
1172
+ print("Next action: execute implementation plan")
1173
+ print(
1174
+ f"Command: ./.cowork-flow/run subagent init --role implement "
1175
+ f"--agent-type cowork-implement --execution-task-dir {task_path} "
1176
+ f"--title \"Implement {Path(task_path).name}\""
1177
+ )
1178
+ print("Then: pass cowork_runtime_context_id and cowork_host_context_key through the active Host Adapter")
1179
+ print("Then: child first step runs ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>")
1180
+ print("Then: verify status=bound and bound_context_key before accepting output")
1181
+ print(f"Then: wait, verify output, close runtime context, then ./.cowork-flow/run task review {task_path}")
1182
+ else:
1183
+ print("Next action: start task")
1184
+ print(f"Command: ./.cowork-flow/run task start {task_path}")
1185
+ _print_blockers(blockers)
1186
+ return 0
1187
+
1188
+ if status == "in_progress":
1189
+ print("Next action: execute implementation plan")
1190
+ print(
1191
+ f"Command: ./.cowork-flow/run subagent init --role implement "
1192
+ f"--agent-type cowork-implement --execution-task-dir {task_path} "
1193
+ f"--title \"Implement {Path(task_path).name}\""
1194
+ )
1195
+ print("Then: pass cowork_runtime_context_id and cowork_host_context_key through the active Host Adapter")
1196
+ print("Then: child first step runs ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>")
1197
+ print("Then: verify status=bound and bound_context_key before accepting output")
1198
+ print(f"Then: wait, verify output, close runtime context, then ./.cowork-flow/run task review {task_path}")
1199
+ _print_blockers(blockers)
1200
+ return 0
1201
+
1202
+ if status in CHECK_STATUSES:
1203
+ print("Next action: verify implementation")
1204
+ print(
1205
+ f"Command: ./.cowork-flow/run subagent init --role check "
1206
+ f"--agent-type cowork-check --execution-task-dir {task_path} "
1207
+ f"--title \"Check {Path(task_path).name}\""
1208
+ )
1209
+ print("Then: pass cowork_runtime_context_id and cowork_host_context_key through the active Host Adapter or run equivalent inline check")
1210
+ print("Then: child first step runs ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>")
1211
+ print("Then: verify status=bound and bound_context_key before accepting output")
1212
+ print(f"Then: ./.cowork-flow/run task complete {task_path}")
1213
+ _print_blockers(blockers)
1214
+ return 0
1215
+
1216
+ if status in DONE_STATUSES:
1217
+ print("Next action: finalize, commit, archive, and record session")
1218
+ print("Command: git status --short")
1219
+ print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
1220
+ _print_blockers(blockers)
1221
+ return 0
1222
+
1223
+ print("Next action: inspect task status and repair workflow state")
1224
+ print(f"Command: ./.cowork-flow/run task validate {task_path}")
1225
+ _print_blockers(blockers)
1226
+ return 0
1227
+
1228
+
944
1229
  # =============================================================================
945
1230
  # Command: archive
946
1231
  # =============================================================================
@@ -999,8 +1284,8 @@ def cmd_archive(args: argparse.Namespace) -> int:
999
1284
  year_month = archive_dest.parent.name
1000
1285
  print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
1001
1286
 
1002
- # Auto-commit unless --no-commit
1003
- if not getattr(args, "no_commit", False):
1287
+ # Auto-commit only when explicitly requested.
1288
+ if getattr(args, "commit", False) and not getattr(args, "no_commit", False):
1004
1289
  _auto_commit_archive(dir_name, repo_root)
1005
1290
 
1006
1291
  # Return the archive path
@@ -1275,7 +1560,10 @@ Usage:
1275
1560
  ./.cowork-flow/run task validate <dir> Validate jsonl files
1276
1561
  ./.cowork-flow/run task list-context <dir> List jsonl entries
1277
1562
  ./.cowork-flow/run task start <dir> Set active session task
1563
+ ./.cowork-flow/run task review [dir] Mark task ready for check
1564
+ ./.cowork-flow/run task complete [dir] Mark task completed
1278
1565
  ./.cowork-flow/run task finish Clear active session task
1566
+ ./.cowork-flow/run task next [dir] Show next safe workflow action
1279
1567
  ./.cowork-flow/run task archive <task-name> Archive completed task
1280
1568
  ./.cowork-flow/run task add-subtask <parent> <child> Link child task to parent
1281
1569
  ./.cowork-flow/run task remove-subtask <parent> <child> Unlink child from parent
@@ -1293,8 +1581,11 @@ Examples:
1293
1581
  ./.cowork-flow/run task create "Add login feature" --slug add-login
1294
1582
  ./.cowork-flow/run task create "Child task" --slug child --parent .cowork-flow/tasks/01-21-parent
1295
1583
  ./.cowork-flow/run task init-context .cowork-flow/tasks/01-21-add-login backend
1584
+ ./.cowork-flow/run task next
1296
1585
  ./.cowork-flow/run task add-context <dir> implement .cowork-flow/spec/backend/auth.md "Auth guidelines"
1297
1586
  ./.cowork-flow/run task start .cowork-flow/tasks/01-21-add-login
1587
+ ./.cowork-flow/run task review
1588
+ ./.cowork-flow/run task complete
1298
1589
  ./.cowork-flow/run task finish
1299
1590
  ./.cowork-flow/run task archive add-login
1300
1591
  ./.cowork-flow/run task add-subtask parent-task child-task # Link existing tasks
@@ -1354,13 +1645,30 @@ def main() -> int:
1354
1645
  # current
1355
1646
  subparsers.add_parser("current", help="Show active session task")
1356
1647
 
1648
+ # review
1649
+ p_review = subparsers.add_parser("review", help="Mark task ready for check")
1650
+ p_review.add_argument("dir", nargs="?", help="Task directory or name")
1651
+
1652
+ # complete
1653
+ p_complete = subparsers.add_parser("complete", help="Mark task completed")
1654
+ p_complete.add_argument("dir", nargs="?", help="Task directory or name")
1655
+
1656
+ # next
1657
+ p_next = subparsers.add_parser("next", help="Show next safe workflow action")
1658
+ p_next.add_argument("dir", nargs="?", help="Task directory or name")
1659
+
1357
1660
  # finish
1358
1661
  subparsers.add_parser("finish", help="Clear active session task")
1359
1662
 
1360
1663
  # archive
1361
1664
  p_archive = subparsers.add_parser("archive", help="Archive task")
1362
1665
  p_archive.add_argument("name", help="Task name")
1363
- p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive")
1666
+ p_archive.add_argument("--commit", action="store_true", help="Auto git commit after archive")
1667
+ p_archive.add_argument(
1668
+ "--no-commit",
1669
+ action="store_true",
1670
+ help="Deprecated no-op; archive no longer commits by default",
1671
+ )
1364
1672
 
1365
1673
  # list
1366
1674
  p_list = subparsers.add_parser("list", help="List tasks")
@@ -1393,6 +1701,8 @@ def main() -> int:
1393
1701
  "init-context",
1394
1702
  "add-context",
1395
1703
  "start",
1704
+ "review",
1705
+ "complete",
1396
1706
  "finish",
1397
1707
  "archive",
1398
1708
  "add-subtask",
@@ -1417,6 +1727,9 @@ def main() -> int:
1417
1727
  "list-context": cmd_list_context,
1418
1728
  "start": cmd_start,
1419
1729
  "current": cmd_current,
1730
+ "review": cmd_review,
1731
+ "complete": cmd_complete,
1732
+ "next": cmd_next,
1420
1733
  "finish": cmd_finish,
1421
1734
  "archive": cmd_archive,
1422
1735
  "add-subtask": cmd_add_subtask,
@@ -0,0 +1,132 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cowork-flow.local/spec/adapter.schema.json",
4
+ "title": "Cowork Flow Host Adapter",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "host", "displayName", "capabilities", "dispatch", "runtimeContext", "contracts", "fallback"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "schemaVersion": {
10
+ "type": "integer",
11
+ "const": 1
12
+ },
13
+ "host": {
14
+ "type": "string",
15
+ "pattern": "^[a-z][a-z0-9-]*$"
16
+ },
17
+ "displayName": {
18
+ "type": "string",
19
+ "minLength": 1
20
+ },
21
+ "capabilities": {
22
+ "type": "object",
23
+ "required": [
24
+ "dispatchSubagent",
25
+ "freshChildContext",
26
+ "sendFollowup",
27
+ "waitChild",
28
+ "listChildren",
29
+ "cancelChild",
30
+ "stateInjection",
31
+ "backgroundChild",
32
+ "runtimeContextDispatch",
33
+ "runtimeContextBinding",
34
+ "runtimeContextCleanup"
35
+ ],
36
+ "additionalProperties": {
37
+ "$ref": "#/$defs/capability"
38
+ },
39
+ "properties": {
40
+ "dispatchSubagent": { "$ref": "#/$defs/capability" },
41
+ "freshChildContext": { "$ref": "#/$defs/capability" },
42
+ "sendFollowup": { "$ref": "#/$defs/capability" },
43
+ "waitChild": { "$ref": "#/$defs/capability" },
44
+ "listChildren": { "$ref": "#/$defs/capability" },
45
+ "cancelChild": { "$ref": "#/$defs/capability" },
46
+ "stateInjection": { "$ref": "#/$defs/capability" },
47
+ "backgroundChild": { "$ref": "#/$defs/capability" },
48
+ "runtimeContextDispatch": { "$ref": "#/$defs/capability" },
49
+ "runtimeContextBinding": { "$ref": "#/$defs/capability" },
50
+ "runtimeContextCleanup": { "$ref": "#/$defs/capability" }
51
+ }
52
+ },
53
+ "dispatch": {
54
+ "type": "object",
55
+ "required": ["primitive"],
56
+ "additionalProperties": {
57
+ "type": ["string", "boolean", "number"]
58
+ },
59
+ "properties": {
60
+ "primitive": {
61
+ "type": "string",
62
+ "minLength": 1
63
+ }
64
+ }
65
+ },
66
+ "runtimeContext": {
67
+ "type": "object",
68
+ "required": ["transport", "promptKey", "envKey", "metadataKey"],
69
+ "additionalProperties": {
70
+ "type": ["string", "boolean", "number"]
71
+ },
72
+ "properties": {
73
+ "transport": {
74
+ "type": "string",
75
+ "enum": ["prompt", "env", "metadata", "plugin"]
76
+ },
77
+ "promptKey": {
78
+ "type": "string",
79
+ "const": "cowork_runtime_context_id"
80
+ },
81
+ "envKey": {
82
+ "type": "string",
83
+ "const": "COWORK_FLOW_RUNTIME_CONTEXT_ID"
84
+ },
85
+ "metadataKey": {
86
+ "type": "string",
87
+ "const": "cowork_runtime_context_id"
88
+ }
89
+ }
90
+ },
91
+ "contracts": {
92
+ "type": "object",
93
+ "required": ["entry", "dispatch", "leafExecutor"],
94
+ "additionalProperties": false,
95
+ "properties": {
96
+ "entry": {
97
+ "type": "string",
98
+ "const": "COWORK_ENTRY_CONTRACT_V1"
99
+ },
100
+ "dispatch": {
101
+ "type": "string",
102
+ "const": "RUNTIME_CONTEXT_DISPATCH_V2"
103
+ },
104
+ "leafExecutor": {
105
+ "type": "boolean",
106
+ "const": true
107
+ }
108
+ }
109
+ },
110
+ "fallback": {
111
+ "type": "object",
112
+ "required": ["whenRequiredCapabilityMissing"],
113
+ "additionalProperties": false,
114
+ "properties": {
115
+ "whenRequiredCapabilityMissing": {
116
+ "type": "string",
117
+ "enum": ["inline_or_manual", "fail_closed"]
118
+ },
119
+ "whenRuntimeContextMissing": {
120
+ "type": "string",
121
+ "enum": ["fail_closed"]
122
+ }
123
+ }
124
+ }
125
+ },
126
+ "$defs": {
127
+ "capability": {
128
+ "type": "string",
129
+ "enum": ["native", "shim", "plugin", "external", "experimental", "unsupported"]
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,34 @@
1
+ # Host capabilities
2
+
3
+ `cowork-flow` uses capability declarations to decide whether a host can run formal fixed-agent workflow steps directly or needs a fallback. Capabilities are enums, not booleans, so an adapter can state how stable the support is.
4
+
5
+ ## Values
6
+
7
+ | Value | Meaning |
8
+ | --- | --- |
9
+ | `native` | Host exposes the capability as a first-class primitive. |
10
+ | `shim` | Adapter can emulate the capability with host primitives. |
11
+ | `plugin` | Capability requires a host plugin or hook. |
12
+ | `external` | Capability requires a separate command or service. |
13
+ | `experimental` | Capability exists but must not be default workflow behavior. |
14
+ | `unsupported` | Capability is unavailable. |
15
+
16
+ ## Required capabilities
17
+
18
+ - `dispatchSubagent`: create a child agent for formal workflow work.
19
+ - `freshChildContext`: start child with bounded context, not inherited full session history.
20
+ - `runtimeContextDispatch`: create a runtime context before child dispatch.
21
+ - `runtimeContextBinding`: bind the host child session to that runtime context
22
+ through a proven hook/plugin path or an explicit first-step shim.
23
+ - `runtimeContextCleanup`: close and clean child runtime session files.
24
+ - `sendFollowup`: send follow-up instructions when the host supports it.
25
+ - `waitChild`: wait for child result.
26
+ - `listChildren`: inspect active children.
27
+ - `cancelChild`: stop an incorrect or cancelled child.
28
+ - `stateInjection`: inject workflow or runtime state before task recovery.
29
+ - `backgroundChild`: run a child while the main session continues.
30
+
31
+ Formal `cowork-research`, `cowork-implement`, and `cowork-check` require
32
+ `dispatchSubagent`, `freshChildContext`, `waitChild`, `runtimeContextDispatch`,
33
+ `runtimeContextBinding`, and `runtimeContextCleanup`. If a required capability
34
+ is `unsupported`, the adapter must use `fallback.whenRequiredCapabilityMissing`.