@blxzer/cursor-trellis 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +13 -4
  3. package/bin/smart-search.js +2 -2
  4. package/dist/commands/update.d.ts.map +1 -1
  5. package/dist/commands/update.js +2 -1
  6. package/dist/commands/update.js.map +1 -1
  7. package/dist/configurators/workflow.d.ts.map +1 -1
  8. package/dist/configurators/workflow.js +8 -2
  9. package/dist/configurators/workflow.js.map +1 -1
  10. package/dist/migrations/manifests/0.2.2.json +9 -0
  11. package/dist/migrations/manifests/0.2.4.json +9 -0
  12. package/dist/migrations/manifests/0.2.5.json +9 -0
  13. package/dist/migrations/manifests/0.2.6.json +9 -0
  14. package/dist/migrations/manifests/0.2.7.json +9 -0
  15. package/dist/templates/common/skills/brainstorm.md +164 -163
  16. package/dist/templates/markdown/index.d.ts +1 -0
  17. package/dist/templates/markdown/index.d.ts.map +1 -1
  18. package/dist/templates/markdown/index.js +1 -0
  19. package/dist/templates/markdown/index.js.map +1 -1
  20. package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +43 -0
  21. package/dist/templates/markdown/spec/guides/index.md.txt +102 -101
  22. package/dist/templates/trellis/config/execution-strategy-rules.json +31 -0
  23. package/dist/templates/trellis/index.d.ts +2 -0
  24. package/dist/templates/trellis/index.d.ts.map +1 -1
  25. package/dist/templates/trellis/index.js +3 -0
  26. package/dist/templates/trellis/index.js.map +1 -1
  27. package/dist/templates/trellis/scripts/common/execution_strategy.py +268 -0
  28. package/dist/templates/trellis/scripts/common/task_store.py +1594 -1565
  29. package/dist/templates/trellis/scripts/task.py +906 -877
  30. package/dist/templates/trellis/workflow.md +805 -800
  31. package/package.json +2 -2
@@ -1,877 +1,906 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Task Management Script.
5
-
6
- Usage:
7
- python task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>]
8
- python task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
9
- python task.py validate <dir> # Validate jsonl files
10
- python task.py list-context <dir> # List jsonl entries
11
- python task.py dashboard # Show Task Dashboard
12
- python task.py select <dir> # Select task for this live session
13
- python task.py selected [--source] # Show selected task
14
- python task.py start-execution <dir> --approved # Start approved execution
15
- python task.py record-gate <dir> --transition <key> --gate <gate> --result PASS|FAIL|SKIPPED --reviewer <id> --evidence <ref> [--root-cause <cause>]
16
- python task.py exit # Clear selected task
17
- python task.py set-branch <dir> <branch> # Set git branch
18
- python task.py set-base-branch <dir> <branch> # Set PR target branch
19
- python task.py set-scope <dir> <scope> # Set scope for PR title
20
- python task.py archive <task-dir> [--check] [--archive-integrated-children] # Check or archive completed task
21
- python task.py prepare-archive-evidence <task-dir> [--dry-run] # Draft missing verify.md archive evidence
22
- python task.py prepare-learning-scaffold <task-dir> [--trigger <text>] # Print spec-capture checklist (stdout only)
23
- python task.py list # List active tasks
24
- python task.py list-archive [month] # List archived tasks
25
- python task.py add-subtask <parent-dir> <child-dir> # Link child to parent
26
- python task.py remove-subtask <parent-dir> <child-dir> # Unlink child from parent
27
- python task.py prepare-child-worktree <parent-dir> <child-dir> --branch <branch>
28
- python task.py set-child-state <parent-dir> <child-dir> <state> --evidence <ref>
29
- python task.py integrate-child <parent-dir> <child-dir> <state> --evidence <ref>
30
- python task.py generate-child-prompt <parent-dir> <child-dir> [--mode inline|subagent]
31
- python task.py generate-dispatch-prompt <task-dir> <role> [--scope TEXT] [--finish] [--max-chars N]
32
- python task.py parent-status <parent-dir>
33
- python task.py review-child <parent-dir> <child-dir> [--check] [--decision accept|changes|cancel|integrate-through]
34
- """
35
-
36
- from __future__ import annotations
37
-
38
- import argparse
39
- import sys
40
- from datetime import datetime, timezone
41
- from pathlib import Path
42
-
43
- from common.log import Colors, colored
44
- from common.paths import (
45
- DIR_WORKFLOW,
46
- DIR_TASKS,
47
- FILE_TASK_JSON,
48
- get_repo_root,
49
- get_developer,
50
- get_tasks_dir,
51
- )
52
- from common.active_task import (
53
- clear_selected_task,
54
- resolve_context_key,
55
- resolve_selected_task,
56
- set_selected_task,
57
- )
58
- from common.io import read_json, write_json
59
- from common.task_dashboard import render_task_dashboard
60
- from common.cli_environment import optional_capability_note
61
- from common.task_gates import (
62
- BASELINE_GATE,
63
- build_reviewer_gate_record,
64
- read_strategy_contract,
65
- start_execution_repair_hints,
66
- validate_start_execution,
67
- validate_start_execution_check,
68
- write_gate_record,
69
- )
70
- from common.task_utils import resolve_task_dir, run_task_hooks
71
- from common.tasks import (
72
- children_progress,
73
- format_child_task_display,
74
- iter_active_tasks,
75
- load_parent_child_integration_states,
76
- )
77
- from common.task_map import get_child_state
78
-
79
- # Import command handlers from split modules (also re-exports for plan.py compatibility)
80
- from common.task_store import (
81
- cmd_create,
82
- cmd_archive,
83
- cmd_prepare_archive_evidence,
84
- cmd_prepare_learning_scaffold,
85
- cmd_set_branch,
86
- cmd_set_base_branch,
87
- cmd_set_scope,
88
- cmd_add_subtask,
89
- cmd_remove_subtask,
90
- cmd_prepare_child_worktree,
91
- cmd_set_child_state,
92
- cmd_integrate_child,
93
- cmd_generate_child_prompt,
94
- cmd_generate_dispatch_prompt,
95
- cmd_parent_status,
96
- cmd_review_child,
97
- )
98
- from common.task_context import (
99
- cmd_add_context,
100
- cmd_validate,
101
- cmd_list_context,
102
- )
103
-
104
-
105
- # =============================================================================
106
- # Command: dashboard / select / selected / start-execution / record-gate / exit
107
- # =============================================================================
108
-
109
- def _repo_relative(path, repo_root) -> str:
110
- try:
111
- return path.relative_to(repo_root).as_posix()
112
- except ValueError:
113
- return str(path)
114
-
115
-
116
- def _resolve_existing_task(task_input: str, repo_root):
117
- full_path = resolve_task_dir(task_input, repo_root)
118
- if not full_path.is_dir():
119
- print(colored(f"Error: Task not found: {task_input}", Colors.RED), file=sys.stderr)
120
- print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.trellis/tasks/01-31-my-task')", file=sys.stderr)
121
- return None
122
- return full_path
123
-
124
-
125
- def cmd_dashboard(args: argparse.Namespace) -> int:
126
- """Show Task Dashboard without mutating selection or status."""
127
- _ = args
128
- print(render_task_dashboard(get_repo_root()))
129
- return 0
130
-
131
-
132
- def cmd_select(args: argparse.Namespace) -> int:
133
- """Select a task for this live session without changing task.status."""
134
- repo_root = get_repo_root()
135
- task_input = args.dir
136
- full_path = _resolve_existing_task(task_input, repo_root)
137
- if full_path is None:
138
- return 1
139
-
140
- if not resolve_context_key():
141
- print(
142
- colored("Error: session identity not available; selected_task was not persisted.", Colors.RED),
143
- file=sys.stderr,
144
- )
145
- print(
146
- "Hint: run inside an AI session that exposes session identity, or set TRELLIS_CONTEXT_ID before running task.py select.",
147
- file=sys.stderr,
148
- )
149
- return 1
150
-
151
- selected = set_selected_task(_repo_relative(full_path, repo_root), repo_root)
152
- if not selected:
153
- print(colored("Error: failed to select task", Colors.RED), file=sys.stderr)
154
- return 1
155
-
156
- print(colored(f"✓ Selected task: {selected.task_path}", Colors.GREEN))
157
- print(f"Source: {selected.source}")
158
- print("Task status unchanged.")
159
- return 0
160
-
161
-
162
- def _print_no_selected_task_guidance() -> None:
163
- """Explain why no task is selected and what to run next."""
164
- print(colored("No task selected for this live session.", Colors.YELLOW), file=sys.stderr)
165
- print("Next actions:", file=sys.stderr)
166
- print(
167
- " - Route work: python ./.trellis/scripts/task.py dashboard",
168
- file=sys.stderr,
169
- )
170
- print(
171
- " - Select a task: python ./.trellis/scripts/task.py select <task-dir>",
172
- file=sys.stderr,
173
- )
174
- print(
175
- " - List active tasks: python ./.trellis/scripts/task.py list",
176
- file=sys.stderr,
177
- )
178
- print(
179
- " - Persist selection in shells: set TRELLIS_CONTEXT_ID (or use your platform session hook)",
180
- file=sys.stderr,
181
- )
182
-
183
-
184
- def cmd_selected(args: argparse.Namespace) -> int:
185
- """Show selected task."""
186
- repo_root = get_repo_root()
187
- selected = resolve_selected_task(repo_root)
188
-
189
- if args.source:
190
- print(f"Selected task: {selected.task_path or '(none)'}")
191
- print(f"Source: {selected.source}")
192
- if selected.stale:
193
- print("State: stale")
194
- if not selected.task_path:
195
- _print_no_selected_task_guidance()
196
- return 0 if selected.task_path else 1
197
-
198
- if selected.task_path:
199
- print(selected.task_path)
200
- return 0
201
-
202
- _print_no_selected_task_guidance()
203
- return 1
204
-
205
-
206
- def cmd_exit(args: argparse.Namespace) -> int:
207
- """Clear selected task for this live session without changing task.status."""
208
- _ = args
209
- repo_root = get_repo_root()
210
- selected = clear_selected_task(repo_root)
211
- if not selected.task_path:
212
- print(colored("No selected task set", Colors.YELLOW))
213
- return 0
214
-
215
- print(colored(f"✓ Cleared selected task (was: {selected.task_path})", Colors.GREEN))
216
- print(f"Source: {selected.source}")
217
- print("Task status unchanged.")
218
- return 0
219
-
220
-
221
- def _print_guard_errors(items: list[str], stream=None) -> None:
222
- if stream is None:
223
- stream = sys.stdout
224
- for item in items:
225
- print(f" - {item}", file=stream)
226
-
227
-
228
- def cmd_start_execution(args: argparse.Namespace) -> int:
229
- """Start approved task execution after a non-mutating readiness check."""
230
- repo_root = get_repo_root()
231
- task_dir = _resolve_existing_task(args.dir, repo_root)
232
- if task_dir is None:
233
- return 1
234
-
235
- task_json_path = task_dir / FILE_TASK_JSON
236
- task_data = read_json(task_json_path) if task_json_path.is_file() else None
237
-
238
- if args.check:
239
- guard = validate_start_execution_check(task_dir, task_data)
240
- if not guard.ok:
241
- print(colored("Start-execution check: FAIL", Colors.RED))
242
- _print_guard_errors(guard.errors)
243
- for hint in start_execution_repair_hints(guard.errors, task_dir):
244
- print(f" Hint: {hint}")
245
- return 1
246
- print(colored("Start-execution check: PASS", Colors.GREEN))
247
- print(f"Contract fingerprint: {guard.contract_fingerprint}")
248
- baseline_fingerprint = guard.artifact_fingerprints.get(BASELINE_GATE)
249
- if baseline_fingerprint:
250
- print(f"Artifact fingerprint: {baseline_fingerprint}")
251
- if guard.required_gates:
252
- print(f"Required reviewer gates: {', '.join(guard.required_gates)}")
253
- if task_dir is not None:
254
- contract, _ = read_strategy_contract(task_dir)
255
- cap_note = optional_capability_note(contract.get("optional_capabilities"))
256
- if cap_note:
257
- print(colored(f"Note: {cap_note}", Colors.YELLOW))
258
- print("Artifact gates are ready. Ask the user for explicit execution approval before running `task.py start-execution <task> --approved`.")
259
- return 0
260
-
261
- if not args.approved:
262
- print(colored("Error: no action selected for start-execution.", Colors.RED), file=sys.stderr)
263
- print("Run with --check for non-mutating preflight or --approved after explicit user approval.", file=sys.stderr)
264
- return 1
265
-
266
- guard = validate_start_execution(task_dir, task_data, approved=True)
267
- if not guard.ok:
268
- print(colored("Error: cannot start execution; readiness check failed.", Colors.RED), file=sys.stderr)
269
- _print_guard_errors(guard.errors, stream=sys.stderr)
270
- return 1
271
-
272
- assert task_data is not None
273
- if guard.baseline_record:
274
- write_gate_record(task_data, "start-execution", BASELINE_GATE, guard.baseline_record)
275
- for gate, record in guard.auto_gate_records.items():
276
- write_gate_record(task_data, "start-execution", gate, record)
277
- task_data["execution_approval"] = {
278
- "schema_version": 1,
279
- "transition": "start-execution",
280
- "approved_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
281
- "approved_by": "user",
282
- "approval_source": "task.py start-execution --approved",
283
- "contract_fingerprint": guard.contract_fingerprint,
284
- "artifact_fingerprint": guard.artifact_fingerprints.get(BASELINE_GATE),
285
- }
286
- if task_data.get("status") == "planning":
287
- task_data["status"] = "in_progress"
288
- if not write_json(task_json_path, task_data):
289
- print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
290
- return 1
291
-
292
- print(colored(f"✓ Execution approved for: {_repo_relative(task_dir, repo_root)}", Colors.GREEN))
293
- print(f"Status: {task_data.get('status')}")
294
- run_task_hooks("after_start", task_json_path, repo_root)
295
- return 0
296
-
297
-
298
- def cmd_record_gate(args: argparse.Namespace) -> int:
299
- """Record a non-baseline reviewer gate result."""
300
- repo_root = get_repo_root()
301
- task_dir = _resolve_existing_task(args.dir, repo_root)
302
- if task_dir is None:
303
- return 1
304
-
305
- task_json_path = task_dir / FILE_TASK_JSON
306
- task_data = read_json(task_json_path) if task_json_path.is_file() else None
307
- if task_data is None:
308
- print(colored("Error: task.json not found or invalid", Colors.RED), file=sys.stderr)
309
- return 1
310
-
311
- record, errors, warnings = build_reviewer_gate_record(
312
- task_dir=task_dir,
313
- task_data=task_data,
314
- transition=args.transition,
315
- gate=args.gate,
316
- result=args.result,
317
- reviewer=args.reviewer,
318
- evidence=args.evidence,
319
- issue_fingerprint=args.issue_fingerprint,
320
- issue_summary=args.issue_summary,
321
- root_cause=args.root_cause,
322
- skip_approved_by=args.skip_approved_by,
323
- skip_reason=args.skip_reason,
324
- contract_fingerprint=args.contract_fingerprint,
325
- artifact_fingerprint=args.artifact_fingerprint,
326
- )
327
- if errors:
328
- print(colored("Error: cannot record quality gate.", Colors.RED), file=sys.stderr)
329
- _print_guard_errors(errors, stream=sys.stderr)
330
- return 1
331
-
332
- assert record is not None
333
- write_gate_record(task_data, args.transition, args.gate, record)
334
- if not write_json(task_json_path, task_data):
335
- print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
336
- return 1
337
-
338
- print(colored(f"✓ Recorded gate: {args.transition}/{args.gate} = {record['result']}", Colors.GREEN))
339
- print(f"Evidence: {record['evidence']}")
340
- print(f"Contract fingerprint: {record['contract_fingerprint']}")
341
- print(f"Artifact fingerprint: {record['artifact_fingerprint']}")
342
- if record.get("route"):
343
- print(f"Route: {record['route']}")
344
- for warning in warnings:
345
- print(colored(f"Warning: {warning}", Colors.YELLOW), file=sys.stderr)
346
- return 0
347
-
348
-
349
- # =============================================================================
350
- # Command: list
351
- # =============================================================================
352
-
353
- def cmd_list(args: argparse.Namespace) -> int:
354
- """List active tasks."""
355
- repo_root = get_repo_root()
356
- tasks_dir = get_tasks_dir(repo_root)
357
- selected_task = resolve_selected_task(repo_root).task_path
358
- developer = get_developer(repo_root)
359
- filter_mine = args.mine
360
- filter_status = args.status
361
-
362
- if filter_mine:
363
- if not developer:
364
- print(colored("Error: No developer set. Run init_developer.py first", Colors.RED), file=sys.stderr)
365
- return 1
366
- print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE))
367
- else:
368
- print(colored("All active tasks:", Colors.BLUE))
369
- print()
370
-
371
- # Single pass: collect all tasks via shared iterator
372
- all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
373
- all_statuses = {name: t.status for name, t in all_tasks.items()}
374
-
375
- # Display tasks hierarchically
376
- count = 0
377
-
378
- def _print_task(
379
- dir_name: str,
380
- indent: int = 0,
381
- parent_dir: Path | None = None,
382
- ) -> None:
383
- nonlocal count
384
- t = all_tasks[dir_name]
385
-
386
- # Apply --mine filter
387
- if filter_mine and (t.assignee or "-") != developer:
388
- return
389
-
390
- # Apply --status filter
391
- if filter_status and t.status != filter_status:
392
- return
393
-
394
- relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
395
- marker = ""
396
- if relative_path == selected_task:
397
- marker = f" {colored('<- selected', Colors.GREEN)}"
398
-
399
- integration_states = None
400
- if t.children:
401
- integration_states = load_parent_child_integration_states(
402
- t.directory, t.children
403
- )
404
- progress = children_progress(
405
- t.children, all_statuses, integration_states
406
- )
407
-
408
- integration_state = None
409
- if parent_dir is not None:
410
- integration_state = get_child_state(parent_dir, dir_name)
411
- status_display = format_child_task_display(t.status, integration_state)
412
-
413
- # Package tag
414
- pkg_tag = f" @{t.package}" if t.package else ""
415
-
416
- prefix = " " * indent + " - "
417
-
418
- if filter_mine:
419
- print(
420
- f"{prefix}{dir_name}/ ({status_display}){pkg_tag}{progress}{marker}"
421
- )
422
- else:
423
- print(
424
- f"{prefix}{dir_name}/ ({status_display}){pkg_tag}{progress} "
425
- f"[{colored(t.assignee or '-', Colors.CYAN)}]{marker}"
426
- )
427
- count += 1
428
-
429
- # Print children indented
430
- for child_name in t.children:
431
- if child_name in all_tasks:
432
- _print_task(child_name, indent + 1, parent_dir=t.directory)
433
-
434
- # Display only top-level tasks (those without a parent)
435
- for dir_name in sorted(all_tasks.keys()):
436
- if not all_tasks[dir_name].parent:
437
- _print_task(dir_name)
438
-
439
- if count == 0:
440
- if filter_mine:
441
- print(" (no tasks assigned to you)")
442
- else:
443
- print(" (no active tasks)")
444
-
445
- print()
446
- print(f"Total: {count} task(s)")
447
- return 0
448
-
449
-
450
- # =============================================================================
451
- # Command: list-archive
452
- # =============================================================================
453
-
454
- def cmd_list_archive(args: argparse.Namespace) -> int:
455
- """List archived tasks."""
456
- repo_root = get_repo_root()
457
- tasks_dir = get_tasks_dir(repo_root)
458
- archive_dir = tasks_dir / "archive"
459
- month = args.month
460
-
461
- print(colored("Archived tasks:", Colors.BLUE))
462
- print()
463
-
464
- if month:
465
- month_dir = archive_dir / month
466
- if month_dir.is_dir():
467
- print(f"[{month}]")
468
- for d in sorted(month_dir.iterdir()):
469
- if d.is_dir():
470
- print(f" - {d.name}/")
471
- else:
472
- print(f" No archives for {month}")
473
- else:
474
- if archive_dir.is_dir():
475
- for month_dir in sorted(archive_dir.iterdir()):
476
- if month_dir.is_dir():
477
- month_name = month_dir.name
478
- count = sum(1 for d in month_dir.iterdir() if d.is_dir())
479
- print(f"[{month_name}] - {count} task(s)")
480
-
481
- return 0
482
-
483
-
484
- # =============================================================================
485
- # Help
486
- # =============================================================================
487
-
488
- def show_usage() -> None:
489
- """Show usage help."""
490
- print("""Task Management Script
491
-
492
- Usage:
493
- python task.py create <title> Create new task directory
494
- python task.py create <title> --package <pkg> Create task for a specific package
495
- python task.py create <title> --parent <dir> Create task as child of parent
496
- python task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl
497
- python task.py validate <dir> Validate jsonl files
498
- python task.py list-context <dir> List jsonl entries
499
- python task.py dashboard Show Task Dashboard
500
- python task.py select <dir> Select task for this live session
501
- python task.py selected [--source] Show selected task
502
- python task.py start-execution <dir> --check Check execution readiness
503
- python task.py start-execution <dir> --approved Start approved execution
504
- python task.py record-gate <dir> --transition <key> --gate <gate> --result PASS|FAIL|SKIPPED --reviewer <id> --evidence <ref> [--root-cause <cause>]
505
- python task.py exit Clear selected task
506
- python task.py set-branch <dir> <branch> Set git branch
507
- python task.py set-base-branch <dir> <branch> Set PR target branch
508
- python task.py set-scope <dir> <scope> Set scope for PR title
509
- python task.py archive <task-dir> [--check] Check or archive completed task
510
- python task.py add-subtask <parent> <child> Link child task to parent
511
- python task.py remove-subtask <parent> <child> Unlink child from parent
512
- python task.py prepare-child-worktree <parent> <child> --branch <branch>
513
- python task.py set-child-state <parent> <child> <state> --evidence <ref>
514
- python task.py integrate-child <parent> <child> <state> --evidence <ref>
515
- python task.py list [--mine] [--status <status>] List tasks
516
- python task.py list-archive [YYYY-MM] List archived tasks
517
-
518
- Monorepo options:
519
- --package <pkg> Package name (validated against config.yaml packages)
520
-
521
- List options:
522
- --mine, -m Show only tasks assigned to current developer
523
- --status, -s <s> Filter by status (planning, in_progress, review, completed)
524
-
525
- Examples:
526
- python task.py create "Add login feature" --slug add-login
527
- python task.py create "Add login feature" --slug add-login --package cli
528
- python task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent
529
- python task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines"
530
- python task.py set-branch <dir> task/add-login
531
- python task.py dashboard
532
- python task.py select .trellis/tasks/01-21-add-login
533
- python task.py selected --source
534
- python task.py start-execution .trellis/tasks/01-21-add-login --check
535
- python task.py start-execution .trellis/tasks/01-21-add-login --approved
536
- python task.py record-gate .trellis/tasks/01-21-add-login --transition full-task-complete --gate code-review --result FAIL --reviewer codex --evidence verify.md --issue-fingerprint auth-branch-1 --root-cause implementation-defect
537
- python task.py exit
538
- python task.py archive add-login --check
539
- python task.py archive add-login
540
- python task.py add-subtask parent-task child-task # Link existing tasks
541
- python task.py remove-subtask parent-task child-task
542
- python task.py prepare-child-worktree parent-task child-task --branch child-task
543
- python task.py set-child-state parent-task child-task review --evidence verify.md
544
- python task.py integrate-child parent-task child-task accepted --evidence handoff.md --ref child-branch
545
- python task.py integrate-child parent-task child-task integrated --evidence task-map.md --ref child-branch --execute-merge
546
- python task.py generate-child-prompt parent-task child-task --mode inline
547
- python task.py parent-status parent-task
548
- python task.py review-child parent-task child-task --check
549
- python task.py review-child parent-task child-task --decision accept --ref child-branch
550
- python task.py list # List all active tasks
551
- python task.py list --mine # List my tasks only
552
- python task.py list --mine --status in_progress # List my in-progress tasks
553
- """)
554
-
555
-
556
- # =============================================================================
557
- # Main Entry
558
- # =============================================================================
559
-
560
- def main() -> int:
561
- """CLI entry point."""
562
- # Deprecation guard: `init-context` was removed in v0.5.0-beta.12.
563
- # Detect early so argparse doesn't mask the real reason with a generic
564
- # "invalid choice" error.
565
- if len(sys.argv) >= 2 and sys.argv[1] == "init-context":
566
- print(
567
- colored(
568
- "Error: `task.py init-context` was removed in v0.5.0-beta.12.",
569
- Colors.RED,
570
- ),
571
- file=sys.stderr,
572
- )
573
- print(
574
- "implement.jsonl / check.jsonl are now seeded on `task.py create` for",
575
- file=sys.stderr,
576
- )
577
- print(
578
- "sub-agent-capable platforms and curated by the AI during planning when needed.",
579
- file=sys.stderr,
580
- )
581
- print("See .trellis/workflow.md planning artifact guidance or run:", file=sys.stderr)
582
- print(
583
- " python ./.trellis/scripts/get_context.py --mode phase --step 1",
584
- file=sys.stderr,
585
- )
586
- print(
587
- "Use `task.py add-context <dir> implement|check <path> <reason>` to append entries.",
588
- file=sys.stderr,
589
- )
590
- return 2
591
-
592
- parser = argparse.ArgumentParser(
593
- description="Task Management Script",
594
- formatter_class=argparse.RawDescriptionHelpFormatter,
595
- )
596
- subparsers = parser.add_subparsers(dest="command", help="Commands")
597
-
598
- # create
599
- p_create = subparsers.add_parser("create", help="Create new task")
600
- p_create.add_argument("title", help="Task title")
601
- p_create.add_argument("--slug", "-s", help="Task slug")
602
- p_create.add_argument("--assignee", "-a", help="Assignee developer")
603
- p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
604
- p_create.add_argument("--description", "-d", help="Task description")
605
- p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
606
- p_create.add_argument("--package", help="Package name for monorepo projects")
607
-
608
- # add-context
609
- p_add = subparsers.add_parser("add-context", help="Add context entry")
610
- p_add.add_argument("dir", help="Task directory")
611
- p_add.add_argument("file", help="JSONL file (implement|check)")
612
- p_add.add_argument("path", help="File path to add")
613
- p_add.add_argument("reason", nargs="?", help="Reason for adding")
614
-
615
- # validate
616
- p_validate = subparsers.add_parser("validate", help="Validate context files")
617
- p_validate.add_argument("dir", help="Task directory")
618
-
619
- # list-context
620
- p_listctx = subparsers.add_parser("list-context", help="List context entries")
621
- p_listctx.add_argument("dir", help="Task directory")
622
-
623
- # dashboard
624
- subparsers.add_parser("dashboard", help="Show Task Dashboard")
625
-
626
- # select
627
- p_select = subparsers.add_parser("select", help="Select task for this live session")
628
- p_select.add_argument("dir", help="Task directory")
629
-
630
- # selected
631
- p_selected = subparsers.add_parser("selected", help="Show selected task")
632
- p_selected.add_argument("--source", action="store_true",
633
- help="Show selected task source")
634
-
635
- # start-execution
636
- p_start_execution = subparsers.add_parser("start-execution", help="Start approved task execution")
637
- p_start_execution.add_argument("dir", help="Task directory")
638
- p_start_execution.add_argument("--check", action="store_true",
639
- help="Run non-mutating execution readiness check")
640
- p_start_execution.add_argument("--approved", action="store_true",
641
- help="Record explicit approval and start execution")
642
-
643
- # record-gate
644
- p_record_gate = subparsers.add_parser("record-gate", help="Record reviewer quality gate")
645
- p_record_gate.add_argument("dir", help="Task directory")
646
- p_record_gate.add_argument("--transition", required=True, help="Transition key")
647
- p_record_gate.add_argument("--gate", required=True, help="Gate name")
648
- p_record_gate.add_argument("--result", required=True, help="PASS, FAIL, or SKIPPED")
649
- p_record_gate.add_argument("--reviewer", required=True, help="Reviewer identifier")
650
- p_record_gate.add_argument("--evidence", required=True, help="Short evidence reference")
651
- p_record_gate.add_argument("--issue-fingerprint", help="Required for FAIL")
652
- p_record_gate.add_argument("--issue-summary", help="Optional short issue summary for FAIL")
653
- p_record_gate.add_argument("--root-cause",
654
- help="Required for FAIL: implementation-defect, contract-changing-defect, or validation-environment-blocker")
655
- p_record_gate.add_argument("--skip-approved-by", help="Must be 'user' for SKIPPED")
656
- p_record_gate.add_argument("--skip-reason", help="Required reason for SKIPPED")
657
- p_record_gate.add_argument("--contract-fingerprint", help="Optional current contract fingerprint assertion")
658
- p_record_gate.add_argument("--artifact-fingerprint", help="Optional current artifact fingerprint assertion")
659
-
660
- # exit
661
- subparsers.add_parser("exit", help="Clear selected task")
662
-
663
- # set-branch
664
- p_branch = subparsers.add_parser("set-branch", help="Set git branch")
665
- p_branch.add_argument("dir", help="Task directory")
666
- p_branch.add_argument("branch", help="Branch name")
667
-
668
- # set-base-branch
669
- p_base = subparsers.add_parser("set-base-branch", help="Set PR target branch")
670
- p_base.add_argument("dir", help="Task directory")
671
- p_base.add_argument("base_branch", help="Base branch name (PR target)")
672
-
673
- # set-scope
674
- p_scope = subparsers.add_parser("set-scope", help="Set scope")
675
- p_scope.add_argument("dir", help="Task directory")
676
- p_scope.add_argument("scope", help="Scope name")
677
-
678
- # archive
679
- p_archive = subparsers.add_parser("archive", help="Archive task")
680
- p_archive.add_argument("name", help="Task directory or name")
681
- p_archive.add_argument("--check", action="store_true", help="Run non-mutating archive readiness check")
682
- p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive")
683
- p_archive.add_argument(
684
- "--archive-integrated-children",
685
- action="store_true",
686
- help="When archiving a parent, also archive integrated children that pass archive --check",
687
- )
688
-
689
- # prepare-archive-evidence
690
- p_prepare_archive = subparsers.add_parser(
691
- "prepare-archive-evidence",
692
- help="Append missing archive evidence sections to verify.md",
693
- )
694
- p_prepare_archive.add_argument("name", help="Task directory or name")
695
- p_prepare_archive.add_argument(
696
- "--dry-run",
697
- action="store_true",
698
- help="Show what would be appended without writing verify.md",
699
- )
700
-
701
- # prepare-learning-scaffold
702
- p_learning_scaffold = subparsers.add_parser(
703
- "prepare-learning-scaffold",
704
- help="Print durable-learning / spec-update checklist (does not edit specs)",
705
- )
706
- p_learning_scaffold.add_argument("name", help="Task directory or name")
707
- p_learning_scaffold.add_argument(
708
- "--trigger",
709
- help="Optional reason (e.g. parent review changes, repeated workflow bug)",
710
- )
711
-
712
- # list
713
- p_list = subparsers.add_parser("list", help="List tasks")
714
- p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only")
715
- p_list.add_argument("--status", "-s", help="Filter by status")
716
-
717
- # add-subtask
718
- p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent")
719
- p_addsub.add_argument("parent_dir", help="Parent task directory")
720
- p_addsub.add_argument("child_dir", help="Child task directory")
721
-
722
- # remove-subtask
723
- p_rmsub = subparsers.add_parser("remove-subtask", help="Unlink child task from parent")
724
- p_rmsub.add_argument("parent_dir", help="Parent task directory")
725
- p_rmsub.add_argument("child_dir", help="Child task directory")
726
-
727
- # prepare-child-worktree
728
- p_prepare_worktree = subparsers.add_parser("prepare-child-worktree", help="Create/register a Child git worktree")
729
- p_prepare_worktree.add_argument("parent_dir", help="Parent task directory")
730
- p_prepare_worktree.add_argument("child_dir", help="Child task directory")
731
- p_prepare_worktree.add_argument("--branch", required=True, help="Child git branch to create or checkout")
732
- p_prepare_worktree.add_argument("--base", help="Base ref for a new Child branch")
733
- p_prepare_worktree.add_argument("--path", help="Worktree path under .trellis/worktrees/")
734
- p_prepare_worktree.add_argument("--check", action="store_true", help="Run non-mutating worktree readiness check")
735
-
736
- # set-child-state
737
- p_child_state = subparsers.add_parser("set-child-state", help="Set Child Worker state in Parent task-map.md")
738
- p_child_state.add_argument("parent_dir", help="Parent task directory")
739
- p_child_state.add_argument("child_dir", help="Child task directory")
740
- p_child_state.add_argument("state", help="Child state")
741
- p_child_state.add_argument("--evidence", required=True, help="Short evidence reference")
742
- p_child_state.add_argument("--reason", help="Optional short reason")
743
-
744
- # integrate-child
745
- p_integrate_child = subparsers.add_parser("integrate-child", help="Set Parent-controlled Child integration state")
746
- p_integrate_child.add_argument("parent_dir", help="Parent task directory")
747
- p_integrate_child.add_argument("child_dir", help="Child task directory")
748
- p_integrate_child.add_argument("state", help="Parent-controlled Child state")
749
- p_integrate_child.add_argument("--evidence", required=True, help="Short evidence reference")
750
- p_integrate_child.add_argument("--ref", help="Child git ref or reviewed diff reference")
751
- p_integrate_child.add_argument("--reason", help="Optional short reason")
752
- p_integrate_child.add_argument("--execute-merge", action="store_true", help="Execute git merge --no-ff --no-commit for an integrated Child")
753
- p_integrate_child.add_argument("--check", action="store_true", help="Run non-mutating integration readiness check")
754
-
755
- # generate-dispatch-prompt
756
- p_dispatch_prompt = subparsers.add_parser(
757
- "generate-dispatch-prompt",
758
- help="Build full Task dispatch prompt (Agent-facing)",
759
- )
760
- p_dispatch_prompt.add_argument("task_dir", help="Task directory")
761
- p_dispatch_prompt.add_argument(
762
- "role",
763
- choices=["implement", "check", "research"],
764
- help="Subagent role",
765
- )
766
- p_dispatch_prompt.add_argument("--scope", help="One-line task instruction for subagent")
767
- p_dispatch_prompt.add_argument(
768
- "--finish",
769
- action="store_true",
770
- help="Use finish check context (role=check only)",
771
- )
772
- p_dispatch_prompt.add_argument(
773
- "--max-chars",
774
- type=int,
775
- help="Hard truncate embedded context block",
776
- )
777
-
778
- # generate-child-prompt
779
- p_gen_prompt = subparsers.add_parser(
780
- "generate-child-prompt",
781
- help="Generate child implementation prompt for parent orchestration",
782
- )
783
- p_gen_prompt.add_argument("parent_dir", help="Parent task directory")
784
- p_gen_prompt.add_argument("child_dir", help="Child task directory")
785
- p_gen_prompt.add_argument(
786
- "--mode",
787
- choices=["inline", "subagent"],
788
- default="inline",
789
- help="Delivery mode hint (inline manual handoff vs optional subagent)",
790
- )
791
- p_gen_prompt.add_argument(
792
- "--include-artifacts",
793
- action="store_true",
794
- help="Embed child artifact bodies in the generated prompt",
795
- )
796
- p_gen_prompt.add_argument("--output", "-o", help="Write prompt to file instead of stdout")
797
-
798
- # parent-status
799
- p_parent_status = subparsers.add_parser("parent-status", help="Show parent task-map orchestration status")
800
- p_parent_status.add_argument("parent_dir", help="Parent task directory")
801
-
802
- # review-child
803
- p_review_child = subparsers.add_parser(
804
- "review-child",
805
- help="Review child handoff and optionally advance integration states",
806
- )
807
- p_review_child.add_argument("parent_dir", help="Parent task directory")
808
- p_review_child.add_argument("child_dir", help="Child task directory")
809
- p_review_child.add_argument("--check", action="store_true", help="Non-mutating review readiness check")
810
- p_review_child.add_argument(
811
- "--decision",
812
- choices=["accept", "changes", "cancel", "integrate-through"],
813
- help="Parent review decision (runs integrate-child steps when valid)",
814
- )
815
- p_review_child.add_argument("--ref", help="Child git ref for accept / integrate-through")
816
- p_review_child.add_argument("--reason", help="Required for changes or cancel decisions")
817
- p_review_child.add_argument("--notes", help="Short parent review notes included in the report")
818
- p_review_child.add_argument(
819
- "--write-artifact",
820
- action="store_true",
821
- help="Also write review-<child>.md under the parent task directory",
822
- )
823
- p_review_child.add_argument(
824
- "--no-append-parent-verify",
825
- action="store_true",
826
- help="Do not append review notes to parent verify.md",
827
- )
828
-
829
- # list-archive
830
- p_listarch = subparsers.add_parser("list-archive", help="List archived tasks")
831
- p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)")
832
-
833
- args = parser.parse_args()
834
-
835
- if not args.command:
836
- show_usage()
837
- return 1
838
-
839
- commands = {
840
- "create": cmd_create,
841
- "add-context": cmd_add_context,
842
- "validate": cmd_validate,
843
- "list-context": cmd_list_context,
844
- "dashboard": cmd_dashboard,
845
- "select": cmd_select,
846
- "selected": cmd_selected,
847
- "start-execution": cmd_start_execution,
848
- "record-gate": cmd_record_gate,
849
- "exit": cmd_exit,
850
- "set-branch": cmd_set_branch,
851
- "set-base-branch": cmd_set_base_branch,
852
- "set-scope": cmd_set_scope,
853
- "archive": cmd_archive,
854
- "prepare-archive-evidence": cmd_prepare_archive_evidence,
855
- "prepare-learning-scaffold": cmd_prepare_learning_scaffold,
856
- "add-subtask": cmd_add_subtask,
857
- "remove-subtask": cmd_remove_subtask,
858
- "prepare-child-worktree": cmd_prepare_child_worktree,
859
- "set-child-state": cmd_set_child_state,
860
- "integrate-child": cmd_integrate_child,
861
- "generate-child-prompt": cmd_generate_child_prompt,
862
- "generate-dispatch-prompt": cmd_generate_dispatch_prompt,
863
- "parent-status": cmd_parent_status,
864
- "review-child": cmd_review_child,
865
- "list": cmd_list,
866
- "list-archive": cmd_list_archive,
867
- }
868
-
869
- if args.command in commands:
870
- return commands[args.command](args)
871
- else:
872
- show_usage()
873
- return 1
874
-
875
-
876
- if __name__ == "__main__":
877
- sys.exit(main())
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Task Management Script.
5
+
6
+ Usage:
7
+ python task.py create "<title>" [--slug <name>] [--assignee <dev>] [--priority P0|P1|P2|P3] [--parent <dir>] [--package <pkg>]
8
+ python task.py add-context <dir> <file> <path> [reason] # Add jsonl entry
9
+ python task.py validate <dir> # Validate jsonl files
10
+ python task.py list-context <dir> # List jsonl entries
11
+ python task.py dashboard # Show Task Dashboard
12
+ python task.py select <dir> # Select task for this live session
13
+ python task.py selected [--source] # Show selected task
14
+ python task.py start-execution <dir> --approved # Start approved execution
15
+ python task.py record-gate <dir> --transition <key> --gate <gate> --result PASS|FAIL|SKIPPED --reviewer <id> --evidence <ref> [--root-cause <cause>]
16
+ python task.py exit # Clear selected task
17
+ python task.py set-branch <dir> <branch> # Set git branch
18
+ python task.py set-base-branch <dir> <branch> # Set PR target branch
19
+ python task.py set-scope <dir> <scope> # Set scope for PR title
20
+ python task.py archive <task-dir> [--check] [--archive-integrated-children] # Check or archive completed task
21
+ python task.py prepare-archive-evidence <task-dir> [--dry-run] # Draft missing verify.md archive evidence
22
+ python task.py prepare-learning-scaffold <task-dir> [--trigger <text>] # Print spec-capture checklist (stdout only)
23
+ python task.py list # List active tasks
24
+ python task.py list-archive [month] # List archived tasks
25
+ python task.py add-subtask <parent-dir> <child-dir> # Link child to parent
26
+ python task.py remove-subtask <parent-dir> <child-dir> # Unlink child from parent
27
+ python task.py prepare-child-worktree <parent-dir> <child-dir> --branch <branch>
28
+ python task.py set-child-state <parent-dir> <child-dir> <state> --evidence <ref>
29
+ python task.py integrate-child <parent-dir> <child-dir> <state> --evidence <ref>
30
+ python task.py generate-child-prompt <parent-dir> <child-dir> [--mode inline|subagent]
31
+ python task.py generate-dispatch-prompt <task-dir> <role> [--scope TEXT] [--finish] [--max-chars N]
32
+ python task.py parent-status <parent-dir>
33
+ python task.py review-child <parent-dir> <child-dir> [--check] [--decision accept|changes|cancel|integrate-through]
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import argparse
39
+ import sys
40
+ from datetime import datetime, timezone
41
+ from pathlib import Path
42
+
43
+ from common.log import Colors, colored
44
+ from common.paths import (
45
+ DIR_WORKFLOW,
46
+ DIR_TASKS,
47
+ FILE_TASK_JSON,
48
+ get_repo_root,
49
+ get_developer,
50
+ get_tasks_dir,
51
+ )
52
+ from common.active_task import (
53
+ clear_selected_task,
54
+ resolve_context_key,
55
+ resolve_selected_task,
56
+ set_selected_task,
57
+ )
58
+ from common.io import read_json, write_json
59
+ from common.task_dashboard import render_task_dashboard
60
+ from common.cli_environment import optional_capability_note
61
+ from common.task_gates import (
62
+ BASELINE_GATE,
63
+ build_reviewer_gate_record,
64
+ read_strategy_contract,
65
+ start_execution_repair_hints,
66
+ validate_start_execution,
67
+ validate_start_execution_check,
68
+ write_gate_record,
69
+ )
70
+ from common.task_utils import resolve_task_dir, run_task_hooks
71
+ from common.tasks import (
72
+ children_progress,
73
+ format_child_task_display,
74
+ iter_active_tasks,
75
+ load_parent_child_integration_states,
76
+ )
77
+ from common.task_map import get_child_state
78
+
79
+ # Import command handlers from split modules (also re-exports for plan.py compatibility)
80
+ from common.task_store import (
81
+ cmd_create,
82
+ cmd_archive,
83
+ cmd_prepare_archive_evidence,
84
+ cmd_prepare_learning_scaffold,
85
+ cmd_set_branch,
86
+ cmd_set_base_branch,
87
+ cmd_set_scope,
88
+ cmd_add_subtask,
89
+ cmd_remove_subtask,
90
+ cmd_prepare_child_worktree,
91
+ cmd_set_child_state,
92
+ cmd_integrate_child,
93
+ cmd_generate_child_prompt,
94
+ cmd_generate_dispatch_prompt,
95
+ cmd_suggest_execution_strategy,
96
+ cmd_parent_status,
97
+ cmd_review_child,
98
+ )
99
+ from common.task_context import (
100
+ cmd_add_context,
101
+ cmd_validate,
102
+ cmd_list_context,
103
+ )
104
+
105
+
106
+ # =============================================================================
107
+ # Command: dashboard / select / selected / start-execution / record-gate / exit
108
+ # =============================================================================
109
+
110
+ def _repo_relative(path, repo_root) -> str:
111
+ try:
112
+ return path.relative_to(repo_root).as_posix()
113
+ except ValueError:
114
+ return str(path)
115
+
116
+
117
+ def _resolve_existing_task(task_input: str, repo_root):
118
+ full_path = resolve_task_dir(task_input, repo_root)
119
+ if not full_path.is_dir():
120
+ print(colored(f"Error: Task not found: {task_input}", Colors.RED), file=sys.stderr)
121
+ print("Hint: Use task name (e.g., 'my-task') or full path (e.g., '.trellis/tasks/01-31-my-task')", file=sys.stderr)
122
+ return None
123
+ return full_path
124
+
125
+
126
+ def cmd_dashboard(args: argparse.Namespace) -> int:
127
+ """Show Task Dashboard without mutating selection or status."""
128
+ _ = args
129
+ print(render_task_dashboard(get_repo_root()))
130
+ return 0
131
+
132
+
133
+ def cmd_select(args: argparse.Namespace) -> int:
134
+ """Select a task for this live session without changing task.status."""
135
+ repo_root = get_repo_root()
136
+ task_input = args.dir
137
+ full_path = _resolve_existing_task(task_input, repo_root)
138
+ if full_path is None:
139
+ return 1
140
+
141
+ if not resolve_context_key():
142
+ print(
143
+ colored("Error: session identity not available; selected_task was not persisted.", Colors.RED),
144
+ file=sys.stderr,
145
+ )
146
+ print(
147
+ "Hint: run inside an AI session that exposes session identity, or set TRELLIS_CONTEXT_ID before running task.py select.",
148
+ file=sys.stderr,
149
+ )
150
+ return 1
151
+
152
+ selected = set_selected_task(_repo_relative(full_path, repo_root), repo_root)
153
+ if not selected:
154
+ print(colored("Error: failed to select task", Colors.RED), file=sys.stderr)
155
+ return 1
156
+
157
+ print(colored(f"✓ Selected task: {selected.task_path}", Colors.GREEN))
158
+ print(f"Source: {selected.source}")
159
+ print("Task status unchanged.")
160
+ return 0
161
+
162
+
163
+ def _print_no_selected_task_guidance() -> None:
164
+ """Explain why no task is selected and what to run next."""
165
+ print(colored("No task selected for this live session.", Colors.YELLOW), file=sys.stderr)
166
+ print("Next actions:", file=sys.stderr)
167
+ print(
168
+ " - Route work: python ./.trellis/scripts/task.py dashboard",
169
+ file=sys.stderr,
170
+ )
171
+ print(
172
+ " - Select a task: python ./.trellis/scripts/task.py select <task-dir>",
173
+ file=sys.stderr,
174
+ )
175
+ print(
176
+ " - List active tasks: python ./.trellis/scripts/task.py list",
177
+ file=sys.stderr,
178
+ )
179
+ print(
180
+ " - Persist selection in shells: set TRELLIS_CONTEXT_ID (or use your platform session hook)",
181
+ file=sys.stderr,
182
+ )
183
+
184
+
185
+ def cmd_selected(args: argparse.Namespace) -> int:
186
+ """Show selected task."""
187
+ repo_root = get_repo_root()
188
+ selected = resolve_selected_task(repo_root)
189
+
190
+ if args.source:
191
+ print(f"Selected task: {selected.task_path or '(none)'}")
192
+ print(f"Source: {selected.source}")
193
+ if selected.stale:
194
+ print("State: stale")
195
+ if not selected.task_path:
196
+ _print_no_selected_task_guidance()
197
+ return 0 if selected.task_path else 1
198
+
199
+ if selected.task_path:
200
+ print(selected.task_path)
201
+ return 0
202
+
203
+ _print_no_selected_task_guidance()
204
+ return 1
205
+
206
+
207
+ def cmd_exit(args: argparse.Namespace) -> int:
208
+ """Clear selected task for this live session without changing task.status."""
209
+ _ = args
210
+ repo_root = get_repo_root()
211
+ selected = clear_selected_task(repo_root)
212
+ if not selected.task_path:
213
+ print(colored("No selected task set", Colors.YELLOW))
214
+ return 0
215
+
216
+ print(colored(f"✓ Cleared selected task (was: {selected.task_path})", Colors.GREEN))
217
+ print(f"Source: {selected.source}")
218
+ print("Task status unchanged.")
219
+ return 0
220
+
221
+
222
+ def _print_guard_errors(items: list[str], stream=None) -> None:
223
+ if stream is None:
224
+ stream = sys.stdout
225
+ for item in items:
226
+ print(f" - {item}", file=stream)
227
+
228
+
229
+ def cmd_start_execution(args: argparse.Namespace) -> int:
230
+ """Start approved task execution after a non-mutating readiness check."""
231
+ repo_root = get_repo_root()
232
+ task_dir = _resolve_existing_task(args.dir, repo_root)
233
+ if task_dir is None:
234
+ return 1
235
+
236
+ task_json_path = task_dir / FILE_TASK_JSON
237
+ task_data = read_json(task_json_path) if task_json_path.is_file() else None
238
+
239
+ if args.check:
240
+ guard = validate_start_execution_check(task_dir, task_data)
241
+ if not guard.ok:
242
+ print(colored("Start-execution check: FAIL", Colors.RED))
243
+ _print_guard_errors(guard.errors)
244
+ for hint in start_execution_repair_hints(guard.errors, task_dir):
245
+ print(f" Hint: {hint}")
246
+ return 1
247
+ print(colored("Start-execution check: PASS", Colors.GREEN))
248
+ print(f"Contract fingerprint: {guard.contract_fingerprint}")
249
+ baseline_fingerprint = guard.artifact_fingerprints.get(BASELINE_GATE)
250
+ if baseline_fingerprint:
251
+ print(f"Artifact fingerprint: {baseline_fingerprint}")
252
+ if guard.required_gates:
253
+ print(f"Required reviewer gates: {', '.join(guard.required_gates)}")
254
+ if task_dir is not None:
255
+ contract, _ = read_strategy_contract(task_dir)
256
+ cap_note = optional_capability_note(contract.get("optional_capabilities"))
257
+ if cap_note:
258
+ print(colored(f"Note: {cap_note}", Colors.YELLOW))
259
+ from common.execution_strategy import (
260
+ contract_drift_warnings,
261
+ validate_strategy_pair,
262
+ )
263
+
264
+ if contract:
265
+ for item in contract_drift_warnings(
266
+ repo_root, task_dir, task_data or {}, contract
267
+ ):
268
+ print(f"[execution-strategy] WARN: {item}", file=sys.stderr)
269
+ mode = contract.get("execution_mode")
270
+ iso = contract.get("isolation")
271
+ if isinstance(mode, str) and isinstance(iso, str):
272
+ for item in validate_strategy_pair(mode, iso):
273
+ print(f"[execution-strategy] WARN: {item}", file=sys.stderr)
274
+ print("Artifact gates are ready. Ask the user for explicit execution approval before running `task.py start-execution <task> --approved`.")
275
+ return 0
276
+
277
+ if not args.approved:
278
+ print(colored("Error: no action selected for start-execution.", Colors.RED), file=sys.stderr)
279
+ print("Run with --check for non-mutating preflight or --approved after explicit user approval.", file=sys.stderr)
280
+ return 1
281
+
282
+ guard = validate_start_execution(task_dir, task_data, approved=True)
283
+ if not guard.ok:
284
+ print(colored("Error: cannot start execution; readiness check failed.", Colors.RED), file=sys.stderr)
285
+ _print_guard_errors(guard.errors, stream=sys.stderr)
286
+ return 1
287
+
288
+ assert task_data is not None
289
+ if guard.baseline_record:
290
+ write_gate_record(task_data, "start-execution", BASELINE_GATE, guard.baseline_record)
291
+ for gate, record in guard.auto_gate_records.items():
292
+ write_gate_record(task_data, "start-execution", gate, record)
293
+ task_data["execution_approval"] = {
294
+ "schema_version": 1,
295
+ "transition": "start-execution",
296
+ "approved_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
297
+ "approved_by": "user",
298
+ "approval_source": "task.py start-execution --approved",
299
+ "contract_fingerprint": guard.contract_fingerprint,
300
+ "artifact_fingerprint": guard.artifact_fingerprints.get(BASELINE_GATE),
301
+ }
302
+ if task_data.get("status") == "planning":
303
+ task_data["status"] = "in_progress"
304
+ if not write_json(task_json_path, task_data):
305
+ print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
306
+ return 1
307
+
308
+ print(colored(f" Execution approved for: {_repo_relative(task_dir, repo_root)}", Colors.GREEN))
309
+ print(f"Status: {task_data.get('status')}")
310
+ run_task_hooks("after_start", task_json_path, repo_root)
311
+ return 0
312
+
313
+
314
+ def cmd_record_gate(args: argparse.Namespace) -> int:
315
+ """Record a non-baseline reviewer gate result."""
316
+ repo_root = get_repo_root()
317
+ task_dir = _resolve_existing_task(args.dir, repo_root)
318
+ if task_dir is None:
319
+ return 1
320
+
321
+ task_json_path = task_dir / FILE_TASK_JSON
322
+ task_data = read_json(task_json_path) if task_json_path.is_file() else None
323
+ if task_data is None:
324
+ print(colored("Error: task.json not found or invalid", Colors.RED), file=sys.stderr)
325
+ return 1
326
+
327
+ record, errors, warnings = build_reviewer_gate_record(
328
+ task_dir=task_dir,
329
+ task_data=task_data,
330
+ transition=args.transition,
331
+ gate=args.gate,
332
+ result=args.result,
333
+ reviewer=args.reviewer,
334
+ evidence=args.evidence,
335
+ issue_fingerprint=args.issue_fingerprint,
336
+ issue_summary=args.issue_summary,
337
+ root_cause=args.root_cause,
338
+ skip_approved_by=args.skip_approved_by,
339
+ skip_reason=args.skip_reason,
340
+ contract_fingerprint=args.contract_fingerprint,
341
+ artifact_fingerprint=args.artifact_fingerprint,
342
+ )
343
+ if errors:
344
+ print(colored("Error: cannot record quality gate.", Colors.RED), file=sys.stderr)
345
+ _print_guard_errors(errors, stream=sys.stderr)
346
+ return 1
347
+
348
+ assert record is not None
349
+ write_gate_record(task_data, args.transition, args.gate, record)
350
+ if not write_json(task_json_path, task_data):
351
+ print(colored("Error: failed to write task.json", Colors.RED), file=sys.stderr)
352
+ return 1
353
+
354
+ print(colored(f" Recorded gate: {args.transition}/{args.gate} = {record['result']}", Colors.GREEN))
355
+ print(f"Evidence: {record['evidence']}")
356
+ print(f"Contract fingerprint: {record['contract_fingerprint']}")
357
+ print(f"Artifact fingerprint: {record['artifact_fingerprint']}")
358
+ if record.get("route"):
359
+ print(f"Route: {record['route']}")
360
+ for warning in warnings:
361
+ print(colored(f"Warning: {warning}", Colors.YELLOW), file=sys.stderr)
362
+ return 0
363
+
364
+
365
+ # =============================================================================
366
+ # Command: list
367
+ # =============================================================================
368
+
369
+ def cmd_list(args: argparse.Namespace) -> int:
370
+ """List active tasks."""
371
+ repo_root = get_repo_root()
372
+ tasks_dir = get_tasks_dir(repo_root)
373
+ selected_task = resolve_selected_task(repo_root).task_path
374
+ developer = get_developer(repo_root)
375
+ filter_mine = args.mine
376
+ filter_status = args.status
377
+
378
+ if filter_mine:
379
+ if not developer:
380
+ print(colored("Error: No developer set. Run init_developer.py first", Colors.RED), file=sys.stderr)
381
+ return 1
382
+ print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE))
383
+ else:
384
+ print(colored("All active tasks:", Colors.BLUE))
385
+ print()
386
+
387
+ # Single pass: collect all tasks via shared iterator
388
+ all_tasks = {t.dir_name: t for t in iter_active_tasks(tasks_dir)}
389
+ all_statuses = {name: t.status for name, t in all_tasks.items()}
390
+
391
+ # Display tasks hierarchically
392
+ count = 0
393
+
394
+ def _print_task(
395
+ dir_name: str,
396
+ indent: int = 0,
397
+ parent_dir: Path | None = None,
398
+ ) -> None:
399
+ nonlocal count
400
+ t = all_tasks[dir_name]
401
+
402
+ # Apply --mine filter
403
+ if filter_mine and (t.assignee or "-") != developer:
404
+ return
405
+
406
+ # Apply --status filter
407
+ if filter_status and t.status != filter_status:
408
+ return
409
+
410
+ relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
411
+ marker = ""
412
+ if relative_path == selected_task:
413
+ marker = f" {colored('<- selected', Colors.GREEN)}"
414
+
415
+ integration_states = None
416
+ if t.children:
417
+ integration_states = load_parent_child_integration_states(
418
+ t.directory, t.children
419
+ )
420
+ progress = children_progress(
421
+ t.children, all_statuses, integration_states
422
+ )
423
+
424
+ integration_state = None
425
+ if parent_dir is not None:
426
+ integration_state = get_child_state(parent_dir, dir_name)
427
+ status_display = format_child_task_display(t.status, integration_state)
428
+
429
+ # Package tag
430
+ pkg_tag = f" @{t.package}" if t.package else ""
431
+
432
+ prefix = " " * indent + " - "
433
+
434
+ if filter_mine:
435
+ print(
436
+ f"{prefix}{dir_name}/ ({status_display}){pkg_tag}{progress}{marker}"
437
+ )
438
+ else:
439
+ print(
440
+ f"{prefix}{dir_name}/ ({status_display}){pkg_tag}{progress} "
441
+ f"[{colored(t.assignee or '-', Colors.CYAN)}]{marker}"
442
+ )
443
+ count += 1
444
+
445
+ # Print children indented
446
+ for child_name in t.children:
447
+ if child_name in all_tasks:
448
+ _print_task(child_name, indent + 1, parent_dir=t.directory)
449
+
450
+ # Display only top-level tasks (those without a parent)
451
+ for dir_name in sorted(all_tasks.keys()):
452
+ if not all_tasks[dir_name].parent:
453
+ _print_task(dir_name)
454
+
455
+ if count == 0:
456
+ if filter_mine:
457
+ print(" (no tasks assigned to you)")
458
+ else:
459
+ print(" (no active tasks)")
460
+
461
+ print()
462
+ print(f"Total: {count} task(s)")
463
+ return 0
464
+
465
+
466
+ # =============================================================================
467
+ # Command: list-archive
468
+ # =============================================================================
469
+
470
+ def cmd_list_archive(args: argparse.Namespace) -> int:
471
+ """List archived tasks."""
472
+ repo_root = get_repo_root()
473
+ tasks_dir = get_tasks_dir(repo_root)
474
+ archive_dir = tasks_dir / "archive"
475
+ month = args.month
476
+
477
+ print(colored("Archived tasks:", Colors.BLUE))
478
+ print()
479
+
480
+ if month:
481
+ month_dir = archive_dir / month
482
+ if month_dir.is_dir():
483
+ print(f"[{month}]")
484
+ for d in sorted(month_dir.iterdir()):
485
+ if d.is_dir():
486
+ print(f" - {d.name}/")
487
+ else:
488
+ print(f" No archives for {month}")
489
+ else:
490
+ if archive_dir.is_dir():
491
+ for month_dir in sorted(archive_dir.iterdir()):
492
+ if month_dir.is_dir():
493
+ month_name = month_dir.name
494
+ count = sum(1 for d in month_dir.iterdir() if d.is_dir())
495
+ print(f"[{month_name}] - {count} task(s)")
496
+
497
+ return 0
498
+
499
+
500
+ # =============================================================================
501
+ # Help
502
+ # =============================================================================
503
+
504
+ def show_usage() -> None:
505
+ """Show usage help."""
506
+ print("""Task Management Script
507
+
508
+ Usage:
509
+ python task.py create <title> Create new task directory
510
+ python task.py create <title> --package <pkg> Create task for a specific package
511
+ python task.py create <title> --parent <dir> Create task as child of parent
512
+ python task.py add-context <dir> <jsonl> <path> [reason] Add entry to jsonl
513
+ python task.py validate <dir> Validate jsonl files
514
+ python task.py list-context <dir> List jsonl entries
515
+ python task.py dashboard Show Task Dashboard
516
+ python task.py select <dir> Select task for this live session
517
+ python task.py selected [--source] Show selected task
518
+ python task.py start-execution <dir> --check Check execution readiness
519
+ python task.py start-execution <dir> --approved Start approved execution
520
+ python task.py record-gate <dir> --transition <key> --gate <gate> --result PASS|FAIL|SKIPPED --reviewer <id> --evidence <ref> [--root-cause <cause>]
521
+ python task.py exit Clear selected task
522
+ python task.py set-branch <dir> <branch> Set git branch
523
+ python task.py set-base-branch <dir> <branch> Set PR target branch
524
+ python task.py set-scope <dir> <scope> Set scope for PR title
525
+ python task.py archive <task-dir> [--check] Check or archive completed task
526
+ python task.py add-subtask <parent> <child> Link child task to parent
527
+ python task.py remove-subtask <parent> <child> Unlink child from parent
528
+ python task.py prepare-child-worktree <parent> <child> --branch <branch>
529
+ python task.py set-child-state <parent> <child> <state> --evidence <ref>
530
+ python task.py integrate-child <parent> <child> <state> --evidence <ref>
531
+ python task.py list [--mine] [--status <status>] List tasks
532
+ python task.py list-archive [YYYY-MM] List archived tasks
533
+
534
+ Monorepo options:
535
+ --package <pkg> Package name (validated against config.yaml packages)
536
+
537
+ List options:
538
+ --mine, -m Show only tasks assigned to current developer
539
+ --status, -s <s> Filter by status (planning, in_progress, review, completed)
540
+
541
+ Examples:
542
+ python task.py create "Add login feature" --slug add-login
543
+ python task.py create "Add login feature" --slug add-login --package cli
544
+ python task.py create "Child task" --slug child --parent .trellis/tasks/01-21-parent
545
+ python task.py add-context <dir> implement .trellis/spec/cli/backend/auth.md "Auth guidelines"
546
+ python task.py set-branch <dir> task/add-login
547
+ python task.py dashboard
548
+ python task.py select .trellis/tasks/01-21-add-login
549
+ python task.py selected --source
550
+ python task.py start-execution .trellis/tasks/01-21-add-login --check
551
+ python task.py start-execution .trellis/tasks/01-21-add-login --approved
552
+ python task.py record-gate .trellis/tasks/01-21-add-login --transition full-task-complete --gate code-review --result FAIL --reviewer codex --evidence verify.md --issue-fingerprint auth-branch-1 --root-cause implementation-defect
553
+ python task.py exit
554
+ python task.py archive add-login --check
555
+ python task.py archive add-login
556
+ python task.py add-subtask parent-task child-task # Link existing tasks
557
+ python task.py remove-subtask parent-task child-task
558
+ python task.py prepare-child-worktree parent-task child-task --branch child-task
559
+ python task.py set-child-state parent-task child-task review --evidence verify.md
560
+ python task.py integrate-child parent-task child-task accepted --evidence handoff.md --ref child-branch
561
+ python task.py integrate-child parent-task child-task integrated --evidence task-map.md --ref child-branch --execute-merge
562
+ python task.py generate-child-prompt parent-task child-task --mode inline
563
+ python task.py parent-status parent-task
564
+ python task.py review-child parent-task child-task --check
565
+ python task.py review-child parent-task child-task --decision accept --ref child-branch
566
+ python task.py list # List all active tasks
567
+ python task.py list --mine # List my tasks only
568
+ python task.py list --mine --status in_progress # List my in-progress tasks
569
+ """)
570
+
571
+
572
+ # =============================================================================
573
+ # Main Entry
574
+ # =============================================================================
575
+
576
+ def main() -> int:
577
+ """CLI entry point."""
578
+ # Deprecation guard: `init-context` was removed in v0.5.0-beta.12.
579
+ # Detect early so argparse doesn't mask the real reason with a generic
580
+ # "invalid choice" error.
581
+ if len(sys.argv) >= 2 and sys.argv[1] == "init-context":
582
+ print(
583
+ colored(
584
+ "Error: `task.py init-context` was removed in v0.5.0-beta.12.",
585
+ Colors.RED,
586
+ ),
587
+ file=sys.stderr,
588
+ )
589
+ print(
590
+ "implement.jsonl / check.jsonl are now seeded on `task.py create` for",
591
+ file=sys.stderr,
592
+ )
593
+ print(
594
+ "sub-agent-capable platforms and curated by the AI during planning when needed.",
595
+ file=sys.stderr,
596
+ )
597
+ print("See .trellis/workflow.md planning artifact guidance or run:", file=sys.stderr)
598
+ print(
599
+ " python ./.trellis/scripts/get_context.py --mode phase --step 1",
600
+ file=sys.stderr,
601
+ )
602
+ print(
603
+ "Use `task.py add-context <dir> implement|check <path> <reason>` to append entries.",
604
+ file=sys.stderr,
605
+ )
606
+ return 2
607
+
608
+ parser = argparse.ArgumentParser(
609
+ description="Task Management Script",
610
+ formatter_class=argparse.RawDescriptionHelpFormatter,
611
+ )
612
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
613
+
614
+ # create
615
+ p_create = subparsers.add_parser("create", help="Create new task")
616
+ p_create.add_argument("title", help="Task title")
617
+ p_create.add_argument("--slug", "-s", help="Task slug")
618
+ p_create.add_argument("--assignee", "-a", help="Assignee developer")
619
+ p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
620
+ p_create.add_argument("--description", "-d", help="Task description")
621
+ p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
622
+ p_create.add_argument("--package", help="Package name for monorepo projects")
623
+
624
+ # add-context
625
+ p_add = subparsers.add_parser("add-context", help="Add context entry")
626
+ p_add.add_argument("dir", help="Task directory")
627
+ p_add.add_argument("file", help="JSONL file (implement|check)")
628
+ p_add.add_argument("path", help="File path to add")
629
+ p_add.add_argument("reason", nargs="?", help="Reason for adding")
630
+
631
+ # validate
632
+ p_validate = subparsers.add_parser("validate", help="Validate context files")
633
+ p_validate.add_argument("dir", help="Task directory")
634
+
635
+ # list-context
636
+ p_listctx = subparsers.add_parser("list-context", help="List context entries")
637
+ p_listctx.add_argument("dir", help="Task directory")
638
+
639
+ # dashboard
640
+ subparsers.add_parser("dashboard", help="Show Task Dashboard")
641
+
642
+ # select
643
+ p_select = subparsers.add_parser("select", help="Select task for this live session")
644
+ p_select.add_argument("dir", help="Task directory")
645
+
646
+ # selected
647
+ p_selected = subparsers.add_parser("selected", help="Show selected task")
648
+ p_selected.add_argument("--source", action="store_true",
649
+ help="Show selected task source")
650
+
651
+ # start-execution
652
+ p_start_execution = subparsers.add_parser("start-execution", help="Start approved task execution")
653
+ p_start_execution.add_argument("dir", help="Task directory")
654
+ p_start_execution.add_argument("--check", action="store_true",
655
+ help="Run non-mutating execution readiness check")
656
+ p_start_execution.add_argument("--approved", action="store_true",
657
+ help="Record explicit approval and start execution")
658
+
659
+ # record-gate
660
+ p_record_gate = subparsers.add_parser("record-gate", help="Record reviewer quality gate")
661
+ p_record_gate.add_argument("dir", help="Task directory")
662
+ p_record_gate.add_argument("--transition", required=True, help="Transition key")
663
+ p_record_gate.add_argument("--gate", required=True, help="Gate name")
664
+ p_record_gate.add_argument("--result", required=True, help="PASS, FAIL, or SKIPPED")
665
+ p_record_gate.add_argument("--reviewer", required=True, help="Reviewer identifier")
666
+ p_record_gate.add_argument("--evidence", required=True, help="Short evidence reference")
667
+ p_record_gate.add_argument("--issue-fingerprint", help="Required for FAIL")
668
+ p_record_gate.add_argument("--issue-summary", help="Optional short issue summary for FAIL")
669
+ p_record_gate.add_argument("--root-cause",
670
+ help="Required for FAIL: implementation-defect, contract-changing-defect, or validation-environment-blocker")
671
+ p_record_gate.add_argument("--skip-approved-by", help="Must be 'user' for SKIPPED")
672
+ p_record_gate.add_argument("--skip-reason", help="Required reason for SKIPPED")
673
+ p_record_gate.add_argument("--contract-fingerprint", help="Optional current contract fingerprint assertion")
674
+ p_record_gate.add_argument("--artifact-fingerprint", help="Optional current artifact fingerprint assertion")
675
+
676
+ # exit
677
+ subparsers.add_parser("exit", help="Clear selected task")
678
+
679
+ # set-branch
680
+ p_branch = subparsers.add_parser("set-branch", help="Set git branch")
681
+ p_branch.add_argument("dir", help="Task directory")
682
+ p_branch.add_argument("branch", help="Branch name")
683
+
684
+ # set-base-branch
685
+ p_base = subparsers.add_parser("set-base-branch", help="Set PR target branch")
686
+ p_base.add_argument("dir", help="Task directory")
687
+ p_base.add_argument("base_branch", help="Base branch name (PR target)")
688
+
689
+ # set-scope
690
+ p_scope = subparsers.add_parser("set-scope", help="Set scope")
691
+ p_scope.add_argument("dir", help="Task directory")
692
+ p_scope.add_argument("scope", help="Scope name")
693
+
694
+ # archive
695
+ p_archive = subparsers.add_parser("archive", help="Archive task")
696
+ p_archive.add_argument("name", help="Task directory or name")
697
+ p_archive.add_argument("--check", action="store_true", help="Run non-mutating archive readiness check")
698
+ p_archive.add_argument("--no-commit", action="store_true", help="Skip auto git commit after archive")
699
+ p_archive.add_argument(
700
+ "--archive-integrated-children",
701
+ action="store_true",
702
+ help="When archiving a parent, also archive integrated children that pass archive --check",
703
+ )
704
+
705
+ # prepare-archive-evidence
706
+ p_prepare_archive = subparsers.add_parser(
707
+ "prepare-archive-evidence",
708
+ help="Append missing archive evidence sections to verify.md",
709
+ )
710
+ p_prepare_archive.add_argument("name", help="Task directory or name")
711
+ p_prepare_archive.add_argument(
712
+ "--dry-run",
713
+ action="store_true",
714
+ help="Show what would be appended without writing verify.md",
715
+ )
716
+
717
+ # prepare-learning-scaffold
718
+ p_learning_scaffold = subparsers.add_parser(
719
+ "prepare-learning-scaffold",
720
+ help="Print durable-learning / spec-update checklist (does not edit specs)",
721
+ )
722
+ p_learning_scaffold.add_argument("name", help="Task directory or name")
723
+ p_learning_scaffold.add_argument(
724
+ "--trigger",
725
+ help="Optional reason (e.g. parent review changes, repeated workflow bug)",
726
+ )
727
+
728
+ # list
729
+ p_list = subparsers.add_parser("list", help="List tasks")
730
+ p_list.add_argument("--mine", "-m", action="store_true", help="My tasks only")
731
+ p_list.add_argument("--status", "-s", help="Filter by status")
732
+
733
+ # add-subtask
734
+ p_addsub = subparsers.add_parser("add-subtask", help="Link child task to parent")
735
+ p_addsub.add_argument("parent_dir", help="Parent task directory")
736
+ p_addsub.add_argument("child_dir", help="Child task directory")
737
+
738
+ # remove-subtask
739
+ p_rmsub = subparsers.add_parser("remove-subtask", help="Unlink child task from parent")
740
+ p_rmsub.add_argument("parent_dir", help="Parent task directory")
741
+ p_rmsub.add_argument("child_dir", help="Child task directory")
742
+
743
+ # prepare-child-worktree
744
+ p_prepare_worktree = subparsers.add_parser("prepare-child-worktree", help="Create/register a Child git worktree")
745
+ p_prepare_worktree.add_argument("parent_dir", help="Parent task directory")
746
+ p_prepare_worktree.add_argument("child_dir", help="Child task directory")
747
+ p_prepare_worktree.add_argument("--branch", required=True, help="Child git branch to create or checkout")
748
+ p_prepare_worktree.add_argument("--base", help="Base ref for a new Child branch")
749
+ p_prepare_worktree.add_argument("--path", help="Worktree path under .trellis/worktrees/")
750
+ p_prepare_worktree.add_argument("--check", action="store_true", help="Run non-mutating worktree readiness check")
751
+
752
+ # set-child-state
753
+ p_child_state = subparsers.add_parser("set-child-state", help="Set Child Worker state in Parent task-map.md")
754
+ p_child_state.add_argument("parent_dir", help="Parent task directory")
755
+ p_child_state.add_argument("child_dir", help="Child task directory")
756
+ p_child_state.add_argument("state", help="Child state")
757
+ p_child_state.add_argument("--evidence", required=True, help="Short evidence reference")
758
+ p_child_state.add_argument("--reason", help="Optional short reason")
759
+
760
+ # integrate-child
761
+ p_integrate_child = subparsers.add_parser("integrate-child", help="Set Parent-controlled Child integration state")
762
+ p_integrate_child.add_argument("parent_dir", help="Parent task directory")
763
+ p_integrate_child.add_argument("child_dir", help="Child task directory")
764
+ p_integrate_child.add_argument("state", help="Parent-controlled Child state")
765
+ p_integrate_child.add_argument("--evidence", required=True, help="Short evidence reference")
766
+ p_integrate_child.add_argument("--ref", help="Child git ref or reviewed diff reference")
767
+ p_integrate_child.add_argument("--reason", help="Optional short reason")
768
+ p_integrate_child.add_argument("--execute-merge", action="store_true", help="Execute git merge --no-ff --no-commit for an integrated Child")
769
+ p_integrate_child.add_argument("--check", action="store_true", help="Run non-mutating integration readiness check")
770
+
771
+ # suggest-execution-strategy
772
+ p_suggest_strategy = subparsers.add_parser(
773
+ "suggest-execution-strategy",
774
+ help="Suggest execution_mode and isolation for Development Strategy Contract",
775
+ )
776
+ p_suggest_strategy.add_argument("task_dir", help="Task directory")
777
+ p_suggest_strategy.add_argument(
778
+ "--json",
779
+ action="store_true",
780
+ help="Emit machine-readable JSON",
781
+ )
782
+
783
+ # generate-dispatch-prompt
784
+ p_dispatch_prompt = subparsers.add_parser(
785
+ "generate-dispatch-prompt",
786
+ help="Build full Task dispatch prompt (Agent-facing)",
787
+ )
788
+ p_dispatch_prompt.add_argument("task_dir", help="Task directory")
789
+ p_dispatch_prompt.add_argument(
790
+ "role",
791
+ choices=["implement", "check", "research"],
792
+ help="Subagent role",
793
+ )
794
+ p_dispatch_prompt.add_argument("--scope", help="One-line task instruction for subagent")
795
+ p_dispatch_prompt.add_argument(
796
+ "--finish",
797
+ action="store_true",
798
+ help="Use finish check context (role=check only)",
799
+ )
800
+ p_dispatch_prompt.add_argument(
801
+ "--max-chars",
802
+ type=int,
803
+ help="Hard truncate embedded context block",
804
+ )
805
+
806
+ # generate-child-prompt
807
+ p_gen_prompt = subparsers.add_parser(
808
+ "generate-child-prompt",
809
+ help="Generate child implementation prompt for parent orchestration",
810
+ )
811
+ p_gen_prompt.add_argument("parent_dir", help="Parent task directory")
812
+ p_gen_prompt.add_argument("child_dir", help="Child task directory")
813
+ p_gen_prompt.add_argument(
814
+ "--mode",
815
+ choices=["inline", "subagent"],
816
+ default="inline",
817
+ help="Delivery mode hint (inline manual handoff vs optional subagent)",
818
+ )
819
+ p_gen_prompt.add_argument(
820
+ "--include-artifacts",
821
+ action="store_true",
822
+ help="Embed child artifact bodies in the generated prompt",
823
+ )
824
+ p_gen_prompt.add_argument("--output", "-o", help="Write prompt to file instead of stdout")
825
+
826
+ # parent-status
827
+ p_parent_status = subparsers.add_parser("parent-status", help="Show parent task-map orchestration status")
828
+ p_parent_status.add_argument("parent_dir", help="Parent task directory")
829
+
830
+ # review-child
831
+ p_review_child = subparsers.add_parser(
832
+ "review-child",
833
+ help="Review child handoff and optionally advance integration states",
834
+ )
835
+ p_review_child.add_argument("parent_dir", help="Parent task directory")
836
+ p_review_child.add_argument("child_dir", help="Child task directory")
837
+ p_review_child.add_argument("--check", action="store_true", help="Non-mutating review readiness check")
838
+ p_review_child.add_argument(
839
+ "--decision",
840
+ choices=["accept", "changes", "cancel", "integrate-through"],
841
+ help="Parent review decision (runs integrate-child steps when valid)",
842
+ )
843
+ p_review_child.add_argument("--ref", help="Child git ref for accept / integrate-through")
844
+ p_review_child.add_argument("--reason", help="Required for changes or cancel decisions")
845
+ p_review_child.add_argument("--notes", help="Short parent review notes included in the report")
846
+ p_review_child.add_argument(
847
+ "--write-artifact",
848
+ action="store_true",
849
+ help="Also write review-<child>.md under the parent task directory",
850
+ )
851
+ p_review_child.add_argument(
852
+ "--no-append-parent-verify",
853
+ action="store_true",
854
+ help="Do not append review notes to parent verify.md",
855
+ )
856
+
857
+ # list-archive
858
+ p_listarch = subparsers.add_parser("list-archive", help="List archived tasks")
859
+ p_listarch.add_argument("month", nargs="?", help="Month (YYYY-MM)")
860
+
861
+ args = parser.parse_args()
862
+
863
+ if not args.command:
864
+ show_usage()
865
+ return 1
866
+
867
+ commands = {
868
+ "create": cmd_create,
869
+ "add-context": cmd_add_context,
870
+ "validate": cmd_validate,
871
+ "list-context": cmd_list_context,
872
+ "dashboard": cmd_dashboard,
873
+ "select": cmd_select,
874
+ "selected": cmd_selected,
875
+ "start-execution": cmd_start_execution,
876
+ "record-gate": cmd_record_gate,
877
+ "exit": cmd_exit,
878
+ "set-branch": cmd_set_branch,
879
+ "set-base-branch": cmd_set_base_branch,
880
+ "set-scope": cmd_set_scope,
881
+ "archive": cmd_archive,
882
+ "prepare-archive-evidence": cmd_prepare_archive_evidence,
883
+ "prepare-learning-scaffold": cmd_prepare_learning_scaffold,
884
+ "add-subtask": cmd_add_subtask,
885
+ "remove-subtask": cmd_remove_subtask,
886
+ "prepare-child-worktree": cmd_prepare_child_worktree,
887
+ "set-child-state": cmd_set_child_state,
888
+ "integrate-child": cmd_integrate_child,
889
+ "generate-child-prompt": cmd_generate_child_prompt,
890
+ "generate-dispatch-prompt": cmd_generate_dispatch_prompt,
891
+ "suggest-execution-strategy": cmd_suggest_execution_strategy,
892
+ "parent-status": cmd_parent_status,
893
+ "review-child": cmd_review_child,
894
+ "list": cmd_list,
895
+ "list-archive": cmd_list_archive,
896
+ }
897
+
898
+ if args.command in commands:
899
+ return commands[args.command](args)
900
+ else:
901
+ show_usage()
902
+ return 1
903
+
904
+
905
+ if __name__ == "__main__":
906
+ sys.exit(main())