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