cowork-flow 0.0.20 → 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.
- package/README.md +65 -20
- package/package.json +1 -1
- package/src/cli.js +109 -2
- package/src/commands/init.js +61 -4
- package/src/commands/sync.js +10 -1
- package/src/lib/copy-template.js +38 -7
- package/src/lib/platforms.js +73 -0
- package/template/.agent/skills/brainstorming/SKILL.md +13 -9
- package/template/.agent/skills/finish-work/SKILL.md +3 -3
- package/template/.agent/skills/meta/SKILL.md +1 -1
- package/template/.agent/skills/start/SKILL.md +35 -16
- package/template/.agent/skills/writing-plans/SKILL.md +1 -1
- package/template/.claude/agents/cowork-check.md +41 -0
- package/template/.claude/agents/cowork-implement.md +40 -0
- package/template/.claude/agents/cowork-research.md +40 -0
- package/template/.claude/commands/cowork-check.md +27 -0
- package/template/.claude/commands/cowork-implement.md +27 -0
- package/template/.claude/commands/cowork-research.md +27 -0
- package/template/.claude/hooks/inject-workflow-state.py +298 -0
- package/template/.claude/settings.json +24 -0
- package/template/.claude/skills/before-dev/SKILL.md +31 -0
- package/template/.claude/skills/brainstorming/SKILL.md +32 -0
- package/template/.claude/skills/break-loop/SKILL.md +26 -0
- package/template/.claude/skills/check/SKILL.md +29 -0
- package/template/.claude/skills/continue/SKILL.md +29 -0
- package/template/.claude/skills/finish-work/SKILL.md +32 -0
- package/template/.claude/skills/meta/SKILL.md +30 -0
- package/template/.claude/skills/python-design/SKILL.md +27 -0
- package/template/.claude/skills/start/SKILL.md +78 -0
- package/template/.claude/skills/update-spec/SKILL.md +39 -0
- package/template/.claude/skills/writing-plans/SKILL.md +70 -0
- package/template/.codex/agents/cowork-check.toml +17 -22
- package/template/.codex/agents/cowork-implement.toml +18 -23
- package/template/.codex/agents/cowork-research.toml +14 -19
- package/template/.codex/agents/default.toml +27 -0
- package/template/.codex/agents/explorer.toml +28 -0
- package/template/.codex/agents/worker.toml +29 -0
- package/template/.codex/hooks/inject-workflow-state.py +161 -108
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/adapters/claude-code/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/codex/adapter.yaml +35 -0
- package/template/.cowork-flow/adapters/opencode/adapter.yaml +33 -0
- package/template/.cowork-flow/config.yaml +4 -6
- package/template/.cowork-flow/scripts/add_session.py +20 -7
- package/template/.cowork-flow/scripts/common/active_task.py +212 -1
- package/template/.cowork-flow/scripts/common/config.py +0 -17
- package/template/.cowork-flow/scripts/common/entry_classifier.py +122 -0
- package/template/.cowork-flow/scripts/common/execution_context.py +10 -17
- package/template/.cowork-flow/scripts/common/readiness.py +249 -0
- package/template/.cowork-flow/scripts/doctor.py +418 -40
- package/template/.cowork-flow/scripts/project_context.py +230 -0
- package/template/.cowork-flow/scripts/run.py +3 -0
- package/template/.cowork-flow/scripts/subagent.py +152 -238
- package/template/.cowork-flow/scripts/task.py +330 -17
- package/template/.cowork-flow/spec/adapter.schema.json +132 -0
- package/template/.cowork-flow/spec/capabilities.md +34 -0
- package/template/.cowork-flow/spec/entry-contract.md +45 -0
- package/template/.cowork-flow/spec/registry.json +54 -0
- package/template/.cowork-flow/spec/subagent-dispatch.md +77 -0
- package/template/.cowork-flow/spec/workflow-state-templates.md +66 -0
- package/template/.cowork-flow/workflow.md +158 -203
- package/template/.opencode/agents/cowork-check.md +46 -0
- package/template/.opencode/agents/cowork-implement.md +45 -0
- package/template/.opencode/agents/cowork-research.md +46 -0
- package/template/.opencode/commands/cowork-check.md +26 -0
- package/template/.opencode/commands/cowork-implement.md +26 -0
- package/template/.opencode/commands/cowork-research.md +26 -0
- package/template/.opencode/plugins/cowork-flow.js +317 -0
- package/template/CLAUDE.md +5 -0
- 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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
|
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("--
|
|
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`.
|