@blxzer/cursor-trellis 0.2.6 → 0.2.8

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