cowork-flow 0.0.39 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/application/task_archive.py +148 -21
- package/template/.cowork-flow/scripts/application/task_lifecycle.py +3 -1
- package/template/.cowork-flow/scripts/commands/add_session.py +137 -124
- package/template/.cowork-flow/scripts/commands/task.py +1 -0
- package/template/.cowork-flow/scripts/commands/task_archive_commands.py +135 -49
- package/template/.cowork-flow/scripts/commands/task_navigation.py +145 -81
- package/template/.cowork-flow/scripts/commands/task_parser.py +1 -0
- package/template/.cowork-flow/scripts/common/core/paths.py +4 -1
- package/template/.cowork-flow/scripts/common/gates/gates.py +114 -108
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +50 -28
- package/template/.cowork-flow/scripts/common/git/git_snapshot.py +14 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +1 -1
- package/template/.cowork-flow/workflow.md +6 -2
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +6 -2
|
@@ -24,19 +24,18 @@ from common.core.paths import (
|
|
|
24
24
|
get_repo_root,
|
|
25
25
|
)
|
|
26
26
|
from common.git.git_context import _run_git_command
|
|
27
|
+
from common.task.archive_utils import archive_directory_resumable
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
DONE_STATUSES = ("completed", "done")
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def is_git_dirty(repo_root) -> bool:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
except (OSError, Exception):
|
|
39
|
-
return False
|
|
34
|
+
rc, stdout, _ = _run_git_command(
|
|
35
|
+
["status", "--porcelain"],
|
|
36
|
+
cwd=repo_root,
|
|
37
|
+
)
|
|
38
|
+
return rc != 0 or bool(stdout.strip())
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def linked_active_changes_for_task(repo_root, task_dir) -> list[str]:
|
|
@@ -62,9 +61,76 @@ def linked_changes_ready_for_archive(repo_root, slugs: list[str]) -> bool:
|
|
|
62
61
|
return ready
|
|
63
62
|
|
|
64
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
|
+
|
|
65
115
|
def archive_linked_changes(repo_root, slugs: list[str]) -> bool:
|
|
66
116
|
from commands.change import archive_change_by_slug
|
|
67
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] = []
|
|
68
134
|
for slug in slugs:
|
|
69
135
|
if archive_change_by_slug(repo_root, slug) is None:
|
|
70
136
|
print(
|
|
@@ -74,7 +140,11 @@ def archive_linked_changes(repo_root, slugs: list[str]) -> bool:
|
|
|
74
140
|
),
|
|
75
141
|
file=sys.stderr,
|
|
76
142
|
)
|
|
143
|
+
_restore_linked_changes(repo_root, slugs, metadata)
|
|
77
144
|
return False
|
|
145
|
+
archived.append(slug)
|
|
146
|
+
|
|
147
|
+
for slug in archived:
|
|
78
148
|
print(
|
|
79
149
|
colored(f"Archived linked change: {slug}", Colors.GREEN),
|
|
80
150
|
file=sys.stderr,
|
|
@@ -111,15 +181,13 @@ def auto_commit_archive(task_name: str, repo_root) -> None:
|
|
|
111
181
|
print(f"[WARN] Auto-commit failed: {error.strip()}", file=sys.stderr)
|
|
112
182
|
|
|
113
183
|
|
|
114
|
-
def
|
|
115
|
-
repo_root = get_repo_root()
|
|
116
|
-
task_name = args.name
|
|
184
|
+
def _resolve_archive_task(repo_root, task_name: str):
|
|
117
185
|
if not task_name:
|
|
118
186
|
print(
|
|
119
187
|
colored("Error: Task name is required", Colors.RED),
|
|
120
188
|
file=sys.stderr,
|
|
121
189
|
)
|
|
122
|
-
return
|
|
190
|
+
return None
|
|
123
191
|
|
|
124
192
|
task_dir = resolve_task_dir(task_name, repo_root)
|
|
125
193
|
if not task_dir.is_dir():
|
|
@@ -129,46 +197,40 @@ def cmd_archive(args) -> int:
|
|
|
129
197
|
)
|
|
130
198
|
print("Active tasks:", file=sys.stderr)
|
|
131
199
|
cmd_list(argparse.Namespace(mine=False, status=None))
|
|
132
|
-
return
|
|
200
|
+
return None
|
|
201
|
+
return task_dir
|
|
133
202
|
|
|
134
|
-
linked_changes = linked_active_changes_for_task(repo_root, task_dir)
|
|
135
|
-
if linked_changes and not linked_changes_ready_for_archive(
|
|
136
|
-
repo_root,
|
|
137
|
-
linked_changes,
|
|
138
|
-
):
|
|
139
|
-
return 1
|
|
140
203
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
204
|
+
def _archive_error_message(task_name: str, error: TaskArchiveError) -> str:
|
|
205
|
+
if error.code == "TASK-ARCHIVE-LOAD-001":
|
|
206
|
+
return f"Task '{task_name}' task.json is unreadable — refusing archive."
|
|
207
|
+
if error.code == "TASK-ARCHIVE-STATUS-001":
|
|
208
|
+
status = error.detail.removeprefix("task status is ")
|
|
209
|
+
return (
|
|
210
|
+
f"Task '{task_name}' is in status '{status}', not in "
|
|
211
|
+
f"{DONE_STATUSES}. Run `task complete` first, then retry archive."
|
|
149
212
|
)
|
|
213
|
+
return f"Failed to archive task: {error.detail}"
|
|
214
|
+
|
|
150
215
|
|
|
216
|
+
def _archive_task(repo_root, task_name: str, task_dir, linked_changes: list[str]):
|
|
151
217
|
try:
|
|
152
|
-
|
|
218
|
+
return TaskArchiveService(repo_root).archive(
|
|
153
219
|
task_dir,
|
|
154
220
|
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,
|
|
155
226
|
)
|
|
156
227
|
except TaskArchiveError as error:
|
|
157
|
-
|
|
158
|
-
message = (
|
|
159
|
-
f"Task '{task_name}' task.json is unreadable — refusing archive."
|
|
160
|
-
)
|
|
161
|
-
elif error.code == "TASK-ARCHIVE-STATUS-001":
|
|
162
|
-
status = error.detail.removeprefix("task status is ")
|
|
163
|
-
message = (
|
|
164
|
-
f"Task '{task_name}' is in status '{status}', not in "
|
|
165
|
-
f"{DONE_STATUSES}. Run `task complete` first, then retry archive."
|
|
166
|
-
)
|
|
167
|
-
else:
|
|
168
|
-
message = f"Failed to archive task: {error.detail}"
|
|
228
|
+
message = _archive_error_message(task_name, error)
|
|
169
229
|
print(colored(f"Error: {message}", Colors.RED), file=sys.stderr)
|
|
170
|
-
return
|
|
230
|
+
return None
|
|
231
|
+
|
|
171
232
|
|
|
233
|
+
def _print_archive_result(result, repo_root) -> None:
|
|
172
234
|
archive_dest = result.destination
|
|
173
235
|
year_month = archive_dest.parent.name
|
|
174
236
|
print(
|
|
@@ -178,18 +240,42 @@ def cmd_archive(args) -> int:
|
|
|
178
240
|
),
|
|
179
241
|
file=sys.stderr,
|
|
180
242
|
)
|
|
181
|
-
if linked_changes and not archive_linked_changes(repo_root, linked_changes):
|
|
182
|
-
return 1
|
|
183
|
-
if getattr(args, "commit", False):
|
|
184
|
-
auto_commit_archive(result.task_name, repo_root)
|
|
185
|
-
|
|
186
243
|
print(
|
|
187
244
|
f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/"
|
|
188
245
|
f"{year_month}/{result.task_name}"
|
|
189
246
|
)
|
|
190
|
-
run_hooks(
|
|
191
|
-
|
|
192
|
-
|
|
247
|
+
run_hooks("after_archive", archive_dest / FILE_TASK_JSON, repo_root)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def cmd_archive(args) -> int:
|
|
251
|
+
repo_root = get_repo_root()
|
|
252
|
+
task_name = args.name
|
|
253
|
+
task_dir = _resolve_archive_task(repo_root, task_name)
|
|
254
|
+
if task_dir is None:
|
|
255
|
+
return 1
|
|
256
|
+
|
|
257
|
+
linked_changes = linked_active_changes_for_task(repo_root, task_dir)
|
|
258
|
+
if linked_changes and not linked_changes_ready_for_archive(
|
|
193
259
|
repo_root,
|
|
194
|
-
|
|
260
|
+
linked_changes,
|
|
261
|
+
):
|
|
262
|
+
return 1
|
|
263
|
+
|
|
264
|
+
if is_git_dirty(repo_root):
|
|
265
|
+
print(
|
|
266
|
+
colored(
|
|
267
|
+
"Warning: Uncommitted changes detected. Archive the task first, "
|
|
268
|
+
"then commit the archived result.",
|
|
269
|
+
Colors.YELLOW,
|
|
270
|
+
),
|
|
271
|
+
file=sys.stderr,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
result = _archive_task(repo_root, task_name, task_dir, linked_changes)
|
|
275
|
+
if result is None:
|
|
276
|
+
return 1
|
|
277
|
+
|
|
278
|
+
if getattr(args, "commit", False):
|
|
279
|
+
auto_commit_archive(result.task_name, repo_root)
|
|
280
|
+
_print_archive_result(result, repo_root)
|
|
195
281
|
return 0
|
|
@@ -25,6 +25,7 @@ USER_INTENTS = (
|
|
|
25
25
|
"clarify",
|
|
26
26
|
"plan",
|
|
27
27
|
"implement",
|
|
28
|
+
"archive",
|
|
28
29
|
"review",
|
|
29
30
|
"debug",
|
|
30
31
|
"discuss",
|
|
@@ -35,6 +36,7 @@ INTENT_REGISTRY_KEYS = {
|
|
|
35
36
|
"clarify": "clarify_requirement",
|
|
36
37
|
"plan": "write_plan",
|
|
37
38
|
"implement": "route_workflow",
|
|
39
|
+
"archive": "route_workflow",
|
|
38
40
|
"review": "route_workflow",
|
|
39
41
|
"debug": "analyze_repeated_failure",
|
|
40
42
|
"discuss": "discuss_options",
|
|
@@ -45,11 +47,10 @@ INTENT_OPERATIONS = {
|
|
|
45
47
|
"clarify": {"edit_planning_artifacts"},
|
|
46
48
|
"plan": {"edit_planning_artifacts"},
|
|
47
49
|
"implement": {
|
|
48
|
-
"create_task",
|
|
49
|
-
"start_task",
|
|
50
50
|
"implement_change",
|
|
51
51
|
"execute_delegated_work",
|
|
52
52
|
},
|
|
53
|
+
"archive": {"archive_task"},
|
|
53
54
|
"review": {"request_review", "verify_change", "complete_task"},
|
|
54
55
|
"debug": {"debug_failure"},
|
|
55
56
|
"discuss": {"discuss_options"},
|
|
@@ -204,12 +205,39 @@ def route_request(
|
|
|
204
205
|
if context not in {"main", "delegated"}:
|
|
205
206
|
raise ValueError(f"unsupported workflow context: {context}")
|
|
206
207
|
|
|
208
|
+
route_blockers, operations, intent_allowed, matches = _resolve_route(
|
|
209
|
+
registry,
|
|
210
|
+
status=status,
|
|
211
|
+
intent=intent,
|
|
212
|
+
context=context,
|
|
213
|
+
blockers=blockers,
|
|
214
|
+
active_target=active_target,
|
|
215
|
+
)
|
|
216
|
+
protocols = _route_protocols(status, intent, intent_allowed)
|
|
217
|
+
return {
|
|
218
|
+
"status": status,
|
|
219
|
+
"allowedOperations": operations,
|
|
220
|
+
"requiredArtifacts": _required_artifacts(status),
|
|
221
|
+
"recommendedSkill": matches[0] if len(matches) == 1 else None,
|
|
222
|
+
"internalProtocols": protocols,
|
|
223
|
+
"blockers": route_blockers,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _resolve_route(
|
|
228
|
+
registry: SkillRegistry,
|
|
229
|
+
*,
|
|
230
|
+
status: str,
|
|
231
|
+
intent: str,
|
|
232
|
+
context: str,
|
|
233
|
+
blockers: tuple[str, ...] | list[str],
|
|
234
|
+
active_target: bool,
|
|
235
|
+
) -> tuple[list[str], list[str], bool, list[str]]:
|
|
207
236
|
route_blockers = [str(blocker) for blocker in blockers]
|
|
208
|
-
blocker_tuple = tuple(route_blockers)
|
|
209
237
|
operations = _allowed_operations(
|
|
210
238
|
status,
|
|
211
239
|
context,
|
|
212
|
-
|
|
240
|
+
tuple(route_blockers),
|
|
213
241
|
active_target,
|
|
214
242
|
)
|
|
215
243
|
intent_allowed = _intent_is_allowed(intent, operations)
|
|
@@ -233,24 +261,22 @@ def route_request(
|
|
|
233
261
|
)
|
|
234
262
|
if intent == "batch" and not matches:
|
|
235
263
|
route_blockers.append("batch-mode is disabled")
|
|
264
|
+
return route_blockers, operations, intent_allowed, matches
|
|
236
265
|
|
|
237
|
-
|
|
266
|
+
|
|
267
|
+
def _route_protocols(
|
|
268
|
+
status: str,
|
|
269
|
+
intent: str,
|
|
270
|
+
intent_allowed: bool,
|
|
271
|
+
) -> list[str]:
|
|
238
272
|
if intent_allowed and intent == "implement" and status in {
|
|
239
273
|
"in_progress",
|
|
240
274
|
"delegated_subtask",
|
|
241
275
|
}:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
"status": status,
|
|
248
|
-
"allowedOperations": operations,
|
|
249
|
-
"requiredArtifacts": _required_artifacts(status),
|
|
250
|
-
"recommendedSkill": matches[0] if len(matches) == 1 else None,
|
|
251
|
-
"internalProtocols": protocols,
|
|
252
|
-
"blockers": route_blockers,
|
|
253
|
-
}
|
|
276
|
+
return ["tdd"]
|
|
277
|
+
if intent_allowed and intent == "review" and status in CHECK_STATUSES:
|
|
278
|
+
return ["check"]
|
|
279
|
+
return []
|
|
254
280
|
|
|
255
281
|
|
|
256
282
|
def _default_intent(
|
|
@@ -266,6 +292,8 @@ def _default_intent(
|
|
|
266
292
|
return "implement"
|
|
267
293
|
if status in CHECK_STATUSES:
|
|
268
294
|
return "review"
|
|
295
|
+
if status in DONE_STATUSES:
|
|
296
|
+
return "archive"
|
|
269
297
|
if status == "delegated_subtask":
|
|
270
298
|
return "implement"
|
|
271
299
|
if active_target:
|
|
@@ -308,70 +336,60 @@ def _print_json_route(
|
|
|
308
336
|
print(json.dumps(payload, ensure_ascii=False, sort_keys=False))
|
|
309
337
|
|
|
310
338
|
|
|
311
|
-
def
|
|
312
|
-
repo_root = get_repo_root()
|
|
339
|
+
def _navigation_target(args, repo_root: Path, structured: bool):
|
|
313
340
|
target = getattr(args, "dir", None)
|
|
314
|
-
active_target = False
|
|
315
|
-
structured = bool(getattr(args, "json", False))
|
|
316
|
-
if not structured:
|
|
317
|
-
print("Workflow Next")
|
|
318
341
|
if target:
|
|
319
342
|
task_dir = resolve_task_dir(target, repo_root)
|
|
320
343
|
task_path = _display(repo_root, task_dir)
|
|
321
|
-
|
|
322
|
-
else:
|
|
323
|
-
active = get_active_task(repo_root)
|
|
324
|
-
source = f"{active.source}:{active.context_key or '-'}"
|
|
325
|
-
if not active.task_path:
|
|
326
|
-
if structured:
|
|
327
|
-
_print_json_route(
|
|
328
|
-
args=args,
|
|
329
|
-
status="no_task",
|
|
330
|
-
blockers=[],
|
|
331
|
-
active_target=False,
|
|
332
|
-
)
|
|
333
|
-
return 0
|
|
334
|
-
print("Status: no_task")
|
|
335
|
-
print(f"Source: {source}")
|
|
336
|
-
print("Next action: create or start a task before repository changes")
|
|
337
|
-
print('Command: ./.cowork-flow/run task create "<title>" --slug <task-name>')
|
|
338
|
-
print("Then: ./.cowork-flow/run task start <task-dir>")
|
|
339
|
-
print("Runtime-context subagent state is injected by hook/plugin; do not infer it from prompt text.")
|
|
340
|
-
return 0
|
|
341
|
-
task_path = active.task_path
|
|
342
|
-
task_dir = repo_root / task_path
|
|
343
|
-
active_target = True
|
|
344
|
+
return task_dir, task_path, "argument", False
|
|
344
345
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
346
|
+
active = get_active_task(repo_root)
|
|
347
|
+
source = f"{active.source}:{active.context_key or '-'}"
|
|
348
|
+
if active.task_path:
|
|
349
|
+
return repo_root / active.task_path, active.task_path, source, True
|
|
350
|
+
if structured:
|
|
351
|
+
_print_json_route(
|
|
352
|
+
args=args,
|
|
353
|
+
status="no_task",
|
|
354
|
+
blockers=[],
|
|
355
|
+
active_target=False,
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
print("Status: no_task")
|
|
357
359
|
print(f"Source: {source}")
|
|
358
|
-
print("Next action:
|
|
359
|
-
print(
|
|
360
|
-
print("
|
|
361
|
-
print(
|
|
362
|
-
|
|
360
|
+
print("Next action: create or start a task before repository changes")
|
|
361
|
+
print('Command: ./.cowork-flow/run task create "<title>" --slug <task-name>')
|
|
362
|
+
print("Then: ./.cowork-flow/run task start <task-dir>")
|
|
363
|
+
print("Runtime-context subagent state is injected by hook/plugin; do not infer it from prompt text.")
|
|
364
|
+
return None
|
|
363
365
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if
|
|
366
|
+
|
|
367
|
+
def _print_stale_route(args, task_path: str, source: str, active_target: bool) -> None:
|
|
368
|
+
if bool(getattr(args, "json", False)):
|
|
367
369
|
_print_json_route(
|
|
368
370
|
args=args,
|
|
369
|
-
status=
|
|
370
|
-
blockers=
|
|
371
|
+
status="stale",
|
|
372
|
+
blockers=[f"task directory not found: {task_path}"],
|
|
371
373
|
active_target=active_target,
|
|
372
374
|
)
|
|
373
|
-
return
|
|
375
|
+
return
|
|
376
|
+
print("Status: stale")
|
|
377
|
+
print(f"Source: {source}")
|
|
378
|
+
print("Next action: clear or replace the missing active task")
|
|
379
|
+
print("Command: ./.cowork-flow/run task list")
|
|
380
|
+
print("Blockers:")
|
|
381
|
+
print(f" - task directory not found: {task_path}")
|
|
382
|
+
|
|
374
383
|
|
|
384
|
+
def _print_text_route(
|
|
385
|
+
repo_root: Path,
|
|
386
|
+
task_path: str,
|
|
387
|
+
task_dir: Path,
|
|
388
|
+
status: str,
|
|
389
|
+
source: str,
|
|
390
|
+
blockers: list[str],
|
|
391
|
+
active_target: bool,
|
|
392
|
+
) -> None:
|
|
375
393
|
print(f"Status: {status}")
|
|
376
394
|
print(f"Source: {source}")
|
|
377
395
|
if status == "planning":
|
|
@@ -387,21 +405,67 @@ def cmd_next(args) -> int:
|
|
|
387
405
|
elif status == "in_progress":
|
|
388
406
|
_implementation(task_path)
|
|
389
407
|
elif status in CHECK_STATUSES:
|
|
390
|
-
|
|
391
|
-
print(f"Command: ./.cowork-flow/run subagent init --role check --agent-type cowork-check --execution-task-dir {task_path} --title \"Check {Path(task_path).name}\"")
|
|
392
|
-
print("Then: pass cowork_runtime_context_id and cowork_host_context_key through the active Host Adapter or run equivalent inline check")
|
|
393
|
-
print("Then: child first step runs ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>")
|
|
394
|
-
print("Then: verify status=bound and bound_context_key before accepting output")
|
|
395
|
-
print(f"Then: ./.cowork-flow/run task complete {task_path}")
|
|
408
|
+
_print_check_route(task_path)
|
|
396
409
|
elif status in DONE_STATUSES:
|
|
397
|
-
|
|
398
|
-
print("Command: git status --short")
|
|
399
|
-
changes = linked_active_changes_for_task(repo_root, task_dir)
|
|
400
|
-
print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
|
|
401
|
-
for slug in changes:
|
|
402
|
-
print(f"Then: ./.cowork-flow/run change archive {slug} (handled by task archive)")
|
|
410
|
+
_print_archive_route(repo_root, task_path, task_dir)
|
|
403
411
|
else:
|
|
404
412
|
print("Next action: inspect task status and repair workflow state")
|
|
405
413
|
print(f"Command: ./.cowork-flow/run task validate {task_path}")
|
|
406
414
|
_print_blockers(blockers)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _print_check_route(task_path: str) -> None:
|
|
418
|
+
print("Next action: verify implementation")
|
|
419
|
+
print(f"Command: ./.cowork-flow/run subagent init --role check --agent-type cowork-check --execution-task-dir {task_path} --title \"Check {Path(task_path).name}\"")
|
|
420
|
+
print("Then: pass cowork_runtime_context_id and cowork_host_context_key through the active Host Adapter or run equivalent inline check")
|
|
421
|
+
print("Then: child first step runs ./.cowork-flow/run subagent bind <runtime_context_id> <host_context_key>")
|
|
422
|
+
print("Then: verify status=bound and bound_context_key before accepting output")
|
|
423
|
+
print(f"Then: ./.cowork-flow/run task complete {task_path}")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _print_archive_route(repo_root: Path, task_path: str, task_dir: Path) -> None:
|
|
427
|
+
print("Next action: finalize, archive, commit, and record session")
|
|
428
|
+
print("Command: git status --short")
|
|
429
|
+
changes = linked_active_changes_for_task(repo_root, task_dir)
|
|
430
|
+
print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
|
|
431
|
+
for slug in changes:
|
|
432
|
+
print(f"Then: ./.cowork-flow/run change archive {slug} (handled by task archive)")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def cmd_next(args) -> int:
|
|
436
|
+
repo_root = get_repo_root()
|
|
437
|
+
structured = bool(getattr(args, "json", False))
|
|
438
|
+
if not structured:
|
|
439
|
+
print("Workflow Next")
|
|
440
|
+
target = _navigation_target(args, repo_root, structured)
|
|
441
|
+
if target is None:
|
|
442
|
+
return 0
|
|
443
|
+
task_dir, task_path, source, active_target = target
|
|
444
|
+
|
|
445
|
+
if not structured:
|
|
446
|
+
print(f"Task: {task_path}")
|
|
447
|
+
if not task_dir.is_dir():
|
|
448
|
+
_print_stale_route(args, task_path, source, active_target)
|
|
449
|
+
return 0
|
|
450
|
+
|
|
451
|
+
status = _status(repo_root, task_dir)
|
|
452
|
+
blockers = _blockers(repo_root, task_dir) if status == "planning" else []
|
|
453
|
+
if structured:
|
|
454
|
+
_print_json_route(
|
|
455
|
+
args=args,
|
|
456
|
+
status=status,
|
|
457
|
+
blockers=blockers,
|
|
458
|
+
active_target=active_target,
|
|
459
|
+
)
|
|
460
|
+
return 0
|
|
461
|
+
|
|
462
|
+
_print_text_route(
|
|
463
|
+
repo_root,
|
|
464
|
+
task_path,
|
|
465
|
+
task_dir,
|
|
466
|
+
status,
|
|
467
|
+
source,
|
|
468
|
+
blockers,
|
|
469
|
+
active_target,
|
|
470
|
+
)
|
|
407
471
|
return 0
|
|
@@ -89,7 +89,10 @@ def get_developer(repo_root: Path | None = None) -> str | None:
|
|
|
89
89
|
content = dev_file.read_text(encoding="utf-8")
|
|
90
90
|
for line in content.splitlines():
|
|
91
91
|
if line.startswith("name="):
|
|
92
|
-
|
|
92
|
+
name = line.split("=", 1)[1].strip()
|
|
93
|
+
if not name or (name.startswith("<") and name.endswith(">")):
|
|
94
|
+
return None
|
|
95
|
+
return name
|
|
93
96
|
except (OSError, IOError):
|
|
94
97
|
pass
|
|
95
98
|
|