cowork-flow 0.0.47 → 0.0.49
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/CHANGELOG.md +41 -0
- package/README.md +102 -23
- package/package.json +8 -3
- package/src/cli.js +6 -0
- package/src/commands/init.js +0 -83
- package/src/commands/source-refresh.js +40 -0
- package/src/commands/sync.js +6 -0
- package/src/commands/update.js +59 -6
- package/src/lib/copy-template.js +200 -50
- package/src/lib/host-assets.js +367 -64
- package/src/lib/plan-applier.js +47 -0
- package/template/.claude/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks/inject-workflow-state.py +7 -0
- package/template/.codex/hooks.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/config.yaml +0 -11
- package/template/.cowork-flow/scripts/adapters/cli/init_developer.py +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/subagent.py +7 -4
- package/template/.cowork-flow/scripts/adapters/cli/task.py +8 -484
- package/template/.cowork-flow/scripts/adapters/cli/task_archive_commands.py +15 -134
- package/template/.cowork-flow/scripts/adapters/cli/task_context_commands.py +7 -1
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +388 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_navigation.py +163 -2
- package/template/.cowork-flow/scripts/adapters/git/git_context.py +16 -193
- package/template/.cowork-flow/scripts/adapters/host/host_manifest.py +170 -84
- package/template/.cowork-flow/scripts/infra/config.py +0 -17
- package/template/.cowork-flow/scripts/infra/developer_profile.py +0 -95
- package/template/.cowork-flow/scripts/infra/paths.py +0 -79
- package/template/.cowork-flow/scripts/infra/skill_manifest.py +23 -3
- package/template/.cowork-flow/scripts/infra/storage/operation_log.py +40 -0
- package/template/.cowork-flow/scripts/infra/storage/state_store.py +356 -1
- package/template/.cowork-flow/scripts/kernel/workflow_route.py +15 -15
- package/template/.cowork-flow/scripts/run.py +0 -5
- package/template/.cowork-flow/scripts/services/context_discovery.py +156 -0
- package/template/.cowork-flow/scripts/services/context_jsonl.py +119 -0
- package/template/.cowork-flow/scripts/services/context_paths.py +186 -0
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +186 -69
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +128 -0
- package/template/.cowork-flow/scripts/services/plan_binding.py +146 -0
- package/template/.cowork-flow/scripts/services/readiness.py +3 -170
- package/template/.cowork-flow/scripts/services/task_archive.py +115 -24
- package/template/.cowork-flow/scripts/services/task_context.py +103 -365
- package/template/.cowork-flow/scripts/services/task_creation.py +40 -5
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +155 -18
- package/template/.cowork-flow/scripts/services/task_routing.py +80 -67
- package/template/.cowork-flow/scripts/services/task_utils.py +0 -146
- package/template/.cowork-flow/scripts/services/workflow_runtime.py +1 -1
- package/template/.cowork-flow/spec/contracts/capabilities.md +23 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +2 -2
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +8 -0
- package/template/.cowork-flow/spec/contracts/index.md +1 -0
- package/template/.cowork-flow/spec/contracts/party-mode-v2-board.md +20 -0
- package/template/.cowork-flow/spec/contracts/plan-binding.md +50 -0
- package/template/.cowork-flow/spec/contracts/skill-owned-actions.md +31 -7
- package/template/.cowork-flow/spec/guides/index.md +3 -1
- package/template/.cowork-flow/spec/index.md +5 -4
- package/template/.cowork-flow/spec/references/definition-of-done.md +2 -3
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
- package/template/.cowork-flow/spec/runtime/contract-registry.json +14 -0
- package/template/.cowork-flow/spec/runtime/host-assets.json +73 -3
- package/template/.cowork-flow/spec/runtime/index.md +1 -1
- package/template/.cowork-flow/spec/schemas/host-assets.schema.json +150 -8
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/skills/batch-execution/SKILL.md +37 -3
- package/template/skills/batch-execution/scripts/batch_action.py +4 -0
- package/template/skills/batch-execution/scripts/batch_execution.py +120 -499
- package/template/skills/batch-execution/scripts/batch_recovery.py +94 -0
- package/template/skills/batch-execution/scripts/batch_state_machine.py +201 -0
- package/template/skills/batch-execution/scripts/batch_verification.py +326 -0
- package/template/skills/brainstorming/SKILL.md +8 -7
- package/template/skills/cowork-flow/SKILL.md +3 -0
- package/template/skills/party-mode/scripts/party_action_contract.py +113 -0
- package/template/skills/party-mode/scripts/party_actions.py +95 -0
- package/template/skills/party-mode/scripts/party_board_store.py +148 -0
- package/template/skills/party-mode/scripts/party_mode_v2.py +345 -317
- package/template/skills/party-mode/scripts/party_reports.py +177 -0
- package/template/skills/party-mode/scripts/party_state_machine.py +189 -0
- package/template/skills/runtime-health/SKILL.md +14 -2
- package/template/skills/runtime-health/scripts/doctor.py +278 -41
- package/template/skills/task-planning/SKILL.md +105 -61
- package/template/skills/task-review/SKILL.md +4 -4
- package/template/skills/task-review/scripts/review_check.py +277 -20
- package/template/skills/test-first/SKILL.md +18 -3
- package/template/skills/test-first/manifest.json +8 -4
- package/template/.cowork-flow/changes/archive/.gitkeep +0 -1
- package/template/.cowork-flow/scripts/adapters/cli/add_session.py +0 -465
- package/template/.cowork-flow/scripts/adapters/cli/change.py +0 -412
- package/template/.cowork-flow/workspace/index.md +0 -102
- package/template/.zcode/scaffold/AGENTS.md +0 -145
- package/template/.zcode/scaffold/CLAUDE.md +0 -5
|
@@ -17,14 +17,12 @@ from adapters.cli.task_support import (
|
|
|
17
17
|
from adapters.cli.task_tree_commands import cmd_list
|
|
18
18
|
from infra.paths import (
|
|
19
19
|
DIR_ARCHIVE,
|
|
20
|
-
DIR_CHANGES,
|
|
21
20
|
DIR_TASKS,
|
|
22
21
|
DIR_WORKFLOW,
|
|
23
22
|
FILE_TASK_JSON,
|
|
24
23
|
get_repo_root,
|
|
25
24
|
)
|
|
26
25
|
from adapters.git.git_context import _run_git_command
|
|
27
|
-
from infra.archive_utils import archive_directory_resumable
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
from kernel.task_state import DONE_STATUSES # noqa: F401
|
|
@@ -38,127 +36,10 @@ def is_git_dirty(repo_root) -> bool:
|
|
|
38
36
|
return rc != 0 or bool(stdout.strip())
|
|
39
37
|
|
|
40
38
|
|
|
41
|
-
def linked_active_changes_for_task(repo_root, task_dir) -> list[str]:
|
|
42
|
-
from adapters.cli.change import linked_active_changes_for_task as find_changes
|
|
43
|
-
|
|
44
|
-
return find_changes(repo_root, (task_dir,))
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def linked_changes_ready_for_archive(repo_root, slugs: list[str]) -> bool:
|
|
48
|
-
from adapters.cli.change import validate_change
|
|
49
|
-
|
|
50
|
-
ready = True
|
|
51
|
-
for slug in slugs:
|
|
52
|
-
if not validate_change(repo_root, slug, quiet=True):
|
|
53
|
-
print(
|
|
54
|
-
colored(
|
|
55
|
-
f"Error: Linked change is not ready to archive: {slug}",
|
|
56
|
-
Colors.RED,
|
|
57
|
-
),
|
|
58
|
-
file=sys.stderr,
|
|
59
|
-
)
|
|
60
|
-
ready = False
|
|
61
|
-
return ready
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _linked_change_paths(repo_root, slug: str):
|
|
65
|
-
source = repo_root / DIR_WORKFLOW / DIR_CHANGES / slug
|
|
66
|
-
month = datetime.now().astimezone().strftime("%Y-%m")
|
|
67
|
-
destination = (
|
|
68
|
-
repo_root
|
|
69
|
-
/ DIR_WORKFLOW
|
|
70
|
-
/ DIR_CHANGES
|
|
71
|
-
/ DIR_ARCHIVE
|
|
72
|
-
/ month
|
|
73
|
-
/ slug
|
|
74
|
-
)
|
|
75
|
-
return source, destination
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _restore_linked_changes(
|
|
79
|
-
repo_root,
|
|
80
|
-
slugs: list[str],
|
|
81
|
-
metadata: dict[str, bytes],
|
|
82
|
-
) -> bool:
|
|
83
|
-
restored = True
|
|
84
|
-
for slug in reversed(slugs):
|
|
85
|
-
source, destination = _linked_change_paths(repo_root, slug)
|
|
86
|
-
if destination.is_dir():
|
|
87
|
-
result = archive_directory_resumable(destination, source)
|
|
88
|
-
if not result.ok:
|
|
89
|
-
print(
|
|
90
|
-
colored(
|
|
91
|
-
f"Error: Failed to restore linked change: {slug}: "
|
|
92
|
-
f"{result.message}",
|
|
93
|
-
Colors.RED,
|
|
94
|
-
),
|
|
95
|
-
file=sys.stderr,
|
|
96
|
-
)
|
|
97
|
-
restored = False
|
|
98
|
-
continue
|
|
99
|
-
if source.is_dir():
|
|
100
|
-
try:
|
|
101
|
-
(source / "change.yaml").write_bytes(metadata[slug])
|
|
102
|
-
except OSError as error:
|
|
103
|
-
print(
|
|
104
|
-
colored(
|
|
105
|
-
f"Error: Failed to restore linked change metadata: "
|
|
106
|
-
f"{slug}: {error}",
|
|
107
|
-
Colors.RED,
|
|
108
|
-
),
|
|
109
|
-
file=sys.stderr,
|
|
110
|
-
)
|
|
111
|
-
restored = False
|
|
112
|
-
return restored
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def archive_linked_changes(repo_root, slugs: list[str]) -> bool:
|
|
116
|
-
from adapters.cli.change import archive_change_by_slug
|
|
117
|
-
|
|
118
|
-
metadata: dict[str, bytes] = {}
|
|
119
|
-
try:
|
|
120
|
-
for slug in slugs:
|
|
121
|
-
source, _ = _linked_change_paths(repo_root, slug)
|
|
122
|
-
metadata[slug] = (source / "change.yaml").read_bytes()
|
|
123
|
-
except OSError as error:
|
|
124
|
-
print(
|
|
125
|
-
colored(
|
|
126
|
-
f"Error: Failed to snapshot linked change metadata: {error}",
|
|
127
|
-
Colors.RED,
|
|
128
|
-
),
|
|
129
|
-
file=sys.stderr,
|
|
130
|
-
)
|
|
131
|
-
return False
|
|
132
|
-
|
|
133
|
-
archived: list[str] = []
|
|
134
|
-
for slug in slugs:
|
|
135
|
-
if archive_change_by_slug(repo_root, slug) is None:
|
|
136
|
-
print(
|
|
137
|
-
colored(
|
|
138
|
-
f"Error: Failed to archive linked change: {slug}",
|
|
139
|
-
Colors.RED,
|
|
140
|
-
),
|
|
141
|
-
file=sys.stderr,
|
|
142
|
-
)
|
|
143
|
-
_restore_linked_changes(repo_root, slugs, metadata)
|
|
144
|
-
return False
|
|
145
|
-
archived.append(slug)
|
|
146
|
-
|
|
147
|
-
for slug in archived:
|
|
148
|
-
print(
|
|
149
|
-
colored(f"Archived linked change: {slug}", Colors.GREEN),
|
|
150
|
-
file=sys.stderr,
|
|
151
|
-
)
|
|
152
|
-
return True
|
|
153
|
-
|
|
154
|
-
|
|
155
39
|
def auto_commit_archive(task_name: str, repo_root) -> None:
|
|
156
40
|
archive_rels = [
|
|
157
41
|
relative
|
|
158
|
-
for relative in (
|
|
159
|
-
f"{DIR_WORKFLOW}/{DIR_TASKS}",
|
|
160
|
-
f"{DIR_WORKFLOW}/{DIR_CHANGES}",
|
|
161
|
-
)
|
|
42
|
+
for relative in (f"{DIR_WORKFLOW}/{DIR_TASKS}",)
|
|
162
43
|
if (repo_root / relative).exists()
|
|
163
44
|
]
|
|
164
45
|
_run_git_command(["add", "-A", *archive_rels], cwd=repo_root)
|
|
@@ -213,20 +94,27 @@ def _archive_error_message(task_name: str, error: TaskArchiveError) -> str:
|
|
|
213
94
|
return f"Failed to archive task: {error.detail}"
|
|
214
95
|
|
|
215
96
|
|
|
216
|
-
def
|
|
97
|
+
def _print_rollback_issues(error: TaskArchiveError) -> None:
|
|
98
|
+
if not error.rollback_issues:
|
|
99
|
+
return
|
|
100
|
+
print("Rollback issues:", file=sys.stderr)
|
|
101
|
+
for issue in error.rollback_issues:
|
|
102
|
+
print(
|
|
103
|
+
f"- {issue.stage}: {issue.path} ? {issue.detail}",
|
|
104
|
+
file=sys.stderr,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _archive_task(repo_root, task_name: str, task_dir):
|
|
217
109
|
try:
|
|
218
110
|
return TaskArchiveService(repo_root).archive(
|
|
219
111
|
task_dir,
|
|
220
112
|
archived_at=datetime.now().strftime("%Y-%m-%d"),
|
|
221
|
-
finalize=(
|
|
222
|
-
lambda: archive_linked_changes(repo_root, linked_changes)
|
|
223
|
-
)
|
|
224
|
-
if linked_changes
|
|
225
|
-
else None,
|
|
226
113
|
)
|
|
227
114
|
except TaskArchiveError as error:
|
|
228
115
|
message = _archive_error_message(task_name, error)
|
|
229
116
|
print(colored(f"Error: {message}", Colors.RED), file=sys.stderr)
|
|
117
|
+
_print_rollback_issues(error)
|
|
230
118
|
return None
|
|
231
119
|
|
|
232
120
|
|
|
@@ -254,13 +142,6 @@ def cmd_archive(args) -> int:
|
|
|
254
142
|
if task_dir is None:
|
|
255
143
|
return 1
|
|
256
144
|
|
|
257
|
-
linked_changes = linked_active_changes_for_task(repo_root, task_dir)
|
|
258
|
-
if linked_changes and not linked_changes_ready_for_archive(
|
|
259
|
-
repo_root,
|
|
260
|
-
linked_changes,
|
|
261
|
-
):
|
|
262
|
-
return 1
|
|
263
|
-
|
|
264
145
|
if is_git_dirty(repo_root):
|
|
265
146
|
print(
|
|
266
147
|
colored(
|
|
@@ -271,7 +152,7 @@ def cmd_archive(args) -> int:
|
|
|
271
152
|
file=sys.stderr,
|
|
272
153
|
)
|
|
273
154
|
|
|
274
|
-
result = _archive_task(repo_root, task_name, task_dir
|
|
155
|
+
result = _archive_task(repo_root, task_name, task_dir)
|
|
275
156
|
if result is None:
|
|
276
157
|
return 1
|
|
277
158
|
|
|
@@ -8,7 +8,6 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
from services.task_context import (
|
|
10
10
|
CONTEXT_JSONL_FILES,
|
|
11
|
-
PLANNED_FILE_HINT,
|
|
12
11
|
TaskContextError,
|
|
13
12
|
TaskContextService,
|
|
14
13
|
)
|
|
@@ -16,6 +15,13 @@ from adapters.cli.task_support import Colors, colored, resolve_task_dir
|
|
|
16
15
|
from infra.paths import get_repo_root
|
|
17
16
|
|
|
18
17
|
|
|
18
|
+
PLANNED_FILE_HINT = (
|
|
19
|
+
"If this is a planned new file, add "
|
|
20
|
+
'"type": "planned-file"; if this is an already-deleted file in scope, add '
|
|
21
|
+
'"type": "deleted-file" before running `task next --run`.'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
19
25
|
def _report_jsonl_skip(path: Path, reason: str) -> None:
|
|
20
26
|
print(f" {colored('[SKIP]', Colors.YELLOW)} {path.name}: {reason}")
|
|
21
27
|
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Lifecycle task CLI command handlers."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
if __package__:
|
|
12
|
+
from . import _bootstrap as _bootstrap # noqa: F401
|
|
13
|
+
else:
|
|
14
|
+
import _bootstrap # noqa: F401
|
|
15
|
+
|
|
16
|
+
from services.task_lifecycle import LifecycleResult, TaskLifecycleService
|
|
17
|
+
from adapters.cli.task_support import (
|
|
18
|
+
Colors,
|
|
19
|
+
colored,
|
|
20
|
+
resolve_task_dir as _resolve_task_dir,
|
|
21
|
+
run_hooks as _run_hooks,
|
|
22
|
+
)
|
|
23
|
+
from adapters.cli.execution_context_args import execution_context_from_namespace
|
|
24
|
+
from infra.paths import (
|
|
25
|
+
DIR_WORKFLOW,
|
|
26
|
+
FILE_TASK_JSON,
|
|
27
|
+
get_repo_root,
|
|
28
|
+
)
|
|
29
|
+
from runtime.session_state import clear_active_task, get_active_task
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _report_check_block(title: str, blockers: tuple[str, ...]) -> int:
|
|
33
|
+
print(colored(f"Error: {title}", Colors.RED), file=sys.stderr)
|
|
34
|
+
for blocker in blockers:
|
|
35
|
+
print(f" - {blocker}", file=sys.stderr)
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _print_transition_blockers(blockers: list[str]) -> None:
|
|
40
|
+
print(
|
|
41
|
+
colored("Error: Task state transition blocked", Colors.RED),
|
|
42
|
+
file=sys.stderr,
|
|
43
|
+
)
|
|
44
|
+
for blocker in blockers:
|
|
45
|
+
print(f" - {blocker}", file=sys.stderr)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _display_task_path(repo_root: Path, task_dir: Path) -> str:
|
|
49
|
+
try:
|
|
50
|
+
return task_dir.resolve().relative_to(repo_root.resolve()).as_posix()
|
|
51
|
+
except ValueError:
|
|
52
|
+
return str(task_dir)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_status_task_dir(
|
|
56
|
+
args: argparse.Namespace,
|
|
57
|
+
repo_root: Path,
|
|
58
|
+
) -> Path | None:
|
|
59
|
+
target_input = getattr(args, "dir", None)
|
|
60
|
+
if target_input:
|
|
61
|
+
task_dir = _resolve_task_dir(target_input, repo_root)
|
|
62
|
+
else:
|
|
63
|
+
active = get_active_task(repo_root)
|
|
64
|
+
if not active.context_key:
|
|
65
|
+
print(
|
|
66
|
+
colored(
|
|
67
|
+
"Error: Missing session context. Set "
|
|
68
|
+
"COWORK_FLOW_CONTEXT_ID or pass a task dir.",
|
|
69
|
+
Colors.RED,
|
|
70
|
+
),
|
|
71
|
+
file=sys.stderr,
|
|
72
|
+
)
|
|
73
|
+
return None
|
|
74
|
+
if not active.task_path:
|
|
75
|
+
print(
|
|
76
|
+
colored(
|
|
77
|
+
"Error: No active task set for this session",
|
|
78
|
+
Colors.RED,
|
|
79
|
+
),
|
|
80
|
+
file=sys.stderr,
|
|
81
|
+
)
|
|
82
|
+
return None
|
|
83
|
+
task_dir = repo_root / active.task_path
|
|
84
|
+
|
|
85
|
+
if not task_dir.is_dir():
|
|
86
|
+
print(
|
|
87
|
+
colored(
|
|
88
|
+
f"Error: Task not found: {target_input or task_dir}",
|
|
89
|
+
Colors.RED,
|
|
90
|
+
),
|
|
91
|
+
file=sys.stderr,
|
|
92
|
+
)
|
|
93
|
+
return None
|
|
94
|
+
if not (task_dir / FILE_TASK_JSON).is_file():
|
|
95
|
+
print(
|
|
96
|
+
colored(
|
|
97
|
+
f"Error: task.json not found: {task_dir}",
|
|
98
|
+
Colors.RED,
|
|
99
|
+
),
|
|
100
|
+
file=sys.stderr,
|
|
101
|
+
)
|
|
102
|
+
return None
|
|
103
|
+
return task_dir
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _report_lifecycle_preflight(
|
|
107
|
+
result: LifecycleResult,
|
|
108
|
+
) -> int:
|
|
109
|
+
print(colored(f"Error: {result.title}", Colors.RED), file=sys.stderr)
|
|
110
|
+
for blocker in result.blockers:
|
|
111
|
+
print(f" - {blocker}", file=sys.stderr)
|
|
112
|
+
if result.hint:
|
|
113
|
+
print(f"Hint: {result.hint}", file=sys.stderr)
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _report_lifecycle_repository_error(
|
|
118
|
+
result: LifecycleResult,
|
|
119
|
+
) -> int:
|
|
120
|
+
error = result.repository_error
|
|
121
|
+
action = (
|
|
122
|
+
"read"
|
|
123
|
+
if error is not None and error.code.startswith("TASK-LOAD-")
|
|
124
|
+
else "write"
|
|
125
|
+
)
|
|
126
|
+
task_json = result.task_dir / FILE_TASK_JSON
|
|
127
|
+
print(
|
|
128
|
+
colored(
|
|
129
|
+
f"Error: Failed to {action} task metadata: {task_json}",
|
|
130
|
+
Colors.RED,
|
|
131
|
+
),
|
|
132
|
+
file=sys.stderr,
|
|
133
|
+
)
|
|
134
|
+
return 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _resolve_start_task(
|
|
138
|
+
task_input: str,
|
|
139
|
+
repo_root: Path,
|
|
140
|
+
) -> Path | None:
|
|
141
|
+
full_path = _resolve_task_dir(task_input, repo_root)
|
|
142
|
+
if full_path.is_dir():
|
|
143
|
+
return full_path
|
|
144
|
+
print(
|
|
145
|
+
colored(
|
|
146
|
+
f"Error: Task not found: {task_input}",
|
|
147
|
+
Colors.RED,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
print(
|
|
151
|
+
"Hint: Use task name (e.g., 'my-task') or full path "
|
|
152
|
+
f"(e.g., '{DIR_WORKFLOW}/tasks/01-31-my-task')"
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _run_auto_start(
|
|
158
|
+
args: argparse.Namespace,
|
|
159
|
+
full_path: Path,
|
|
160
|
+
service: TaskLifecycleService,
|
|
161
|
+
) -> int:
|
|
162
|
+
readiness = service.start_readiness(full_path)
|
|
163
|
+
if not readiness.ok:
|
|
164
|
+
return _report_lifecycle_preflight(readiness)
|
|
165
|
+
from adapters.cli.batch_mode import run_batch_entry
|
|
166
|
+
|
|
167
|
+
return run_batch_entry(get_repo_root(), full_path, args)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _report_start_failure(
|
|
171
|
+
result: LifecycleResult,
|
|
172
|
+
) -> int:
|
|
173
|
+
if result.title:
|
|
174
|
+
return _report_lifecycle_preflight(result)
|
|
175
|
+
if result.code == "LIFECYCLE-TRANSITION-001":
|
|
176
|
+
_print_transition_blockers(list(result.blockers))
|
|
177
|
+
return 1
|
|
178
|
+
if result.code == "LIFECYCLE-CHECK-001":
|
|
179
|
+
return _report_check_block(
|
|
180
|
+
"Lifecycle checks blocked start_task action",
|
|
181
|
+
result.blockers,
|
|
182
|
+
)
|
|
183
|
+
if result.code == "LIFECYCLE-CONTEXT-001":
|
|
184
|
+
return _report_missing_session_context()
|
|
185
|
+
if result.repository_error is not None:
|
|
186
|
+
return _report_lifecycle_repository_error(result)
|
|
187
|
+
print(
|
|
188
|
+
colored("Error: Task lifecycle failed", Colors.RED),
|
|
189
|
+
file=sys.stderr,
|
|
190
|
+
)
|
|
191
|
+
return 1
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _report_missing_session_context() -> int:
|
|
195
|
+
print(
|
|
196
|
+
colored(
|
|
197
|
+
"Error: Missing session context. Set "
|
|
198
|
+
"COWORK_FLOW_CONTEXT_ID or run inside a "
|
|
199
|
+
"supported host session.",
|
|
200
|
+
Colors.RED,
|
|
201
|
+
),
|
|
202
|
+
file=sys.stderr,
|
|
203
|
+
)
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _report_start_success(
|
|
208
|
+
result: LifecycleResult,
|
|
209
|
+
repo_root: Path,
|
|
210
|
+
full_path: Path,
|
|
211
|
+
) -> None:
|
|
212
|
+
task_dir = result.active_task_path or _display_task_path(
|
|
213
|
+
repo_root,
|
|
214
|
+
full_path,
|
|
215
|
+
)
|
|
216
|
+
print(
|
|
217
|
+
colored(
|
|
218
|
+
f"[OK] Active session task set to: {task_dir}",
|
|
219
|
+
Colors.GREEN,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
print()
|
|
223
|
+
print(
|
|
224
|
+
colored(
|
|
225
|
+
"Fixed agents will load context from this task's jsonl files.",
|
|
226
|
+
Colors.BLUE,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_start(args: argparse.Namespace) -> int:
|
|
232
|
+
"""Set the active task for this session."""
|
|
233
|
+
repo_root = get_repo_root()
|
|
234
|
+
task_input = args.dir
|
|
235
|
+
if not task_input:
|
|
236
|
+
print(
|
|
237
|
+
colored(
|
|
238
|
+
"Error: task directory or name required",
|
|
239
|
+
Colors.RED,
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
return 1
|
|
243
|
+
|
|
244
|
+
full_path = _resolve_start_task(task_input, repo_root)
|
|
245
|
+
if full_path is None:
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
service = TaskLifecycleService(repo_root)
|
|
249
|
+
if getattr(args, "auto", False):
|
|
250
|
+
return _run_auto_start(args, full_path, service)
|
|
251
|
+
|
|
252
|
+
result = service.start(full_path)
|
|
253
|
+
if not result.ok:
|
|
254
|
+
return _report_start_failure(result)
|
|
255
|
+
|
|
256
|
+
_report_start_success(result, repo_root, full_path)
|
|
257
|
+
if "after_start" in getattr(result, "emitted_events", ()):
|
|
258
|
+
_run_hooks("after_start", full_path / FILE_TASK_JSON, repo_root)
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cmd_review(args: argparse.Namespace) -> int:
|
|
263
|
+
"""Mark a task ready for check."""
|
|
264
|
+
repo_root = get_repo_root()
|
|
265
|
+
execution_context = execution_context_from_namespace(args)
|
|
266
|
+
task_dir = _resolve_status_task_dir(args, repo_root)
|
|
267
|
+
if task_dir is None:
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
service = TaskLifecycleService(repo_root)
|
|
271
|
+
result = service.review(
|
|
272
|
+
task_dir,
|
|
273
|
+
execution_context=execution_context,
|
|
274
|
+
)
|
|
275
|
+
if not result.ok:
|
|
276
|
+
if result.code == "LIFECYCLE-TRANSITION-001":
|
|
277
|
+
_print_transition_blockers(list(result.blockers))
|
|
278
|
+
return 1
|
|
279
|
+
if result.code == "LIFECYCLE-CHECK-001":
|
|
280
|
+
return _report_check_block(
|
|
281
|
+
"Lifecycle checks blocked review",
|
|
282
|
+
result.blockers,
|
|
283
|
+
)
|
|
284
|
+
if result.repository_error is not None:
|
|
285
|
+
return _report_lifecycle_repository_error(result)
|
|
286
|
+
return 1
|
|
287
|
+
|
|
288
|
+
task_path = _display_task_path(repo_root, task_dir)
|
|
289
|
+
print(
|
|
290
|
+
colored(
|
|
291
|
+
f"[OK] Task marked for check: {task_path}",
|
|
292
|
+
Colors.GREEN,
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
print(f"Next: ./.cowork-flow/run task next {task_path}")
|
|
296
|
+
return 0
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cmd_complete(args: argparse.Namespace) -> int:
|
|
300
|
+
"""Mark a task completed after final check."""
|
|
301
|
+
repo_root = get_repo_root()
|
|
302
|
+
execution_context = execution_context_from_namespace(args)
|
|
303
|
+
task_dir = _resolve_status_task_dir(args, repo_root)
|
|
304
|
+
if task_dir is None:
|
|
305
|
+
return 1
|
|
306
|
+
|
|
307
|
+
service = TaskLifecycleService(repo_root)
|
|
308
|
+
result = service.complete(
|
|
309
|
+
task_dir,
|
|
310
|
+
execution_context=execution_context,
|
|
311
|
+
)
|
|
312
|
+
if not result.ok:
|
|
313
|
+
if result.code == "LIFECYCLE-TRANSITION-001":
|
|
314
|
+
_print_transition_blockers(list(result.blockers))
|
|
315
|
+
print(
|
|
316
|
+
"Hint: run ./.cowork-flow/run task next <task-dir> "
|
|
317
|
+
"--run --intent review to mark the task ready for check",
|
|
318
|
+
file=sys.stderr,
|
|
319
|
+
)
|
|
320
|
+
return 1
|
|
321
|
+
if result.code == "LIFECYCLE-CHECK-001":
|
|
322
|
+
return _report_check_block(
|
|
323
|
+
"Lifecycle checks blocked completion",
|
|
324
|
+
result.blockers,
|
|
325
|
+
)
|
|
326
|
+
if result.repository_error is not None:
|
|
327
|
+
return _report_lifecycle_repository_error(result)
|
|
328
|
+
return 1
|
|
329
|
+
|
|
330
|
+
task_path = _display_task_path(repo_root, task_dir)
|
|
331
|
+
print(
|
|
332
|
+
colored(
|
|
333
|
+
f"[OK] Task marked completed: {task_path}",
|
|
334
|
+
Colors.GREEN,
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
print(f"Next: ./.cowork-flow/run task next {task_path}")
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def cmd_finish(args: argparse.Namespace) -> int:
|
|
342
|
+
"""Clear the active task for this session."""
|
|
343
|
+
repo_root = get_repo_root()
|
|
344
|
+
active = get_active_task(repo_root)
|
|
345
|
+
if not active.task_path:
|
|
346
|
+
print(
|
|
347
|
+
colored(
|
|
348
|
+
"No active task set for this session",
|
|
349
|
+
Colors.YELLOW,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
task_json_path = repo_root / active.task_path / FILE_TASK_JSON
|
|
355
|
+
clear_active_task(repo_root)
|
|
356
|
+
print(
|
|
357
|
+
colored(
|
|
358
|
+
f"[OK] Cleared active session task (was: {active.task_path})",
|
|
359
|
+
Colors.GREEN,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
emitted_events = ("after_finish",) if task_json_path.is_file() else ()
|
|
363
|
+
if "after_finish" in emitted_events:
|
|
364
|
+
_run_hooks("after_finish", task_json_path, repo_root)
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def cmd_current(args: argparse.Namespace) -> int:
|
|
369
|
+
"""Show the active task for this session."""
|
|
370
|
+
repo_root = get_repo_root()
|
|
371
|
+
active = get_active_task(repo_root)
|
|
372
|
+
if not active.context_key:
|
|
373
|
+
print(
|
|
374
|
+
colored(
|
|
375
|
+
"Error: Missing session context. Set "
|
|
376
|
+
"COWORK_FLOW_CONTEXT_ID or run inside a "
|
|
377
|
+
"supported host session.",
|
|
378
|
+
Colors.RED,
|
|
379
|
+
),
|
|
380
|
+
file=sys.stderr,
|
|
381
|
+
)
|
|
382
|
+
return 1
|
|
383
|
+
if not active.task_path:
|
|
384
|
+
print("Active task: (none)")
|
|
385
|
+
return 0
|
|
386
|
+
print(f"Active task: {active.task_path}")
|
|
387
|
+
print(f"Source: {active.source}:{active.context_key}")
|
|
388
|
+
return 0
|