cowork-flow 0.0.37 → 0.0.39

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 (111) hide show
  1. package/README.md +27 -14
  2. package/package.json +1 -1
  3. package/src/commands/init.js +39 -28
  4. package/src/commands/sync.js +2 -2
  5. package/src/lib/asset-plan.js +127 -0
  6. package/src/lib/copy-template.js +101 -141
  7. package/src/lib/host-assets.js +231 -0
  8. package/src/lib/plan-applier.js +329 -0
  9. package/src/lib/platforms.js +15 -76
  10. package/src/lib/skill-registry.js +503 -0
  11. package/template/.claude/agents/cowork-check.md +10 -1
  12. package/template/.claude/agents/cowork-implement.md +11 -2
  13. package/template/.claude/hooks/inject-workflow-state.py +51 -254
  14. package/template/.codex/agents/cowork-check.toml +12 -3
  15. package/template/.codex/agents/cowork-implement.toml +11 -2
  16. package/template/.codex/agents/default.toml +3 -2
  17. package/template/.codex/agents/explorer.toml +3 -2
  18. package/template/.codex/agents/worker.toml +2 -1
  19. package/template/.codex/hooks/inject-workflow-state.py +46 -265
  20. package/template/.cowork-flow/.version +1 -1
  21. package/template/.cowork-flow/scripts/application/__init__.py +1 -0
  22. package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
  23. package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
  24. package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
  25. package/template/.cowork-flow/scripts/application/task_context.py +765 -0
  26. package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
  27. package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
  28. package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
  29. package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
  30. package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
  31. package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
  32. package/template/.cowork-flow/scripts/commands/task.py +535 -1953
  33. package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
  34. package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
  35. package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
  36. package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
  37. package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
  38. package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
  39. package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
  40. package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
  41. package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
  42. package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
  43. package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
  44. package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
  45. package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
  46. package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
  47. package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
  48. package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
  49. package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
  50. package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
  51. package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
  52. package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
  53. package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
  54. package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
  55. package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
  56. package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
  57. package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
  58. package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
  59. package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
  60. package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
  61. package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
  62. package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
  63. package/template/.cowork-flow/scripts/run.py +24 -0
  64. package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  65. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  66. package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
  67. package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
  68. package/template/.cowork-flow/spec/protocols/review.md +34 -0
  69. package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
  70. package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
  71. package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
  72. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  73. package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
  74. package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
  75. package/template/.cowork-flow/spec/runtime/index.md +8 -0
  76. package/template/.cowork-flow/spec/runtime/rules.json +61 -10
  77. package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
  78. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
  79. package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  80. package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
  81. package/template/.cowork-flow/workflow.md +18 -13
  82. package/template/.opencode/agents/cowork-check.md +11 -0
  83. package/template/.opencode/agents/cowork-implement.md +16 -1
  84. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  85. package/template/.zcode/hooks/inject-context.js +3 -85
  86. package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
  87. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  88. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  89. package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
  90. package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  91. package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
  92. package/template/.zcode/scaffold/AGENTS.md +18 -8
  93. package/template/AGENTS.md +1 -1
  94. package/template/skills/batch-mode/SKILL.md +140 -76
  95. package/template/skills/brainstorming/SKILL.md +2 -2
  96. package/template/skills/cowork-flow/SKILL.md +42 -0
  97. package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
  98. package/template/skills/party-mode/SKILL.md +48 -119
  99. package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
  100. package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
  101. package/template/skills/before-dev/SKILL.md +0 -41
  102. package/template/skills/check/SKILL.md +0 -53
  103. package/template/skills/check/simplification-guide.md +0 -47
  104. package/template/skills/continue/SKILL.md +0 -29
  105. package/template/skills/doubt-review/SKILL.md +0 -118
  106. package/template/skills/finish-work/SKILL.md +0 -32
  107. package/template/skills/party-mode-v2/SKILL.md +0 -75
  108. package/template/skills/start/SKILL.md +0 -78
  109. package/template/skills/tdd/SKILL.md +0 -74
  110. package/template/skills/update-spec/SKILL.md +0 -39
  111. package/template/skills/using-cowork-flow/SKILL.md +0 -80
@@ -0,0 +1,364 @@
1
+ #!/usr/bin/env python3
2
+ """Argument parser and usage rendering for task commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+
9
+ from common.core.execution_context import (
10
+ build_internal_execution_context_parser,
11
+ )
12
+
13
+
14
+ def _add_create_command(subparsers: argparse._SubParsersAction) -> None:
15
+ create = subparsers.add_parser("create", help="Create new task")
16
+ create.add_argument("title", help="Task title")
17
+ create.add_argument("--slug", "-s", help="Task slug")
18
+ create.add_argument("--assignee", "-a", help="Assignee developer")
19
+ create.add_argument(
20
+ "--priority",
21
+ "-p",
22
+ default="P2",
23
+ help="Priority (P0-P3)",
24
+ )
25
+ create.add_argument("--description", "-d", help="Task description")
26
+ create.add_argument(
27
+ "--parent",
28
+ help="Parent task directory (establishes subtask link)",
29
+ )
30
+ create.add_argument(
31
+ "--from-plan",
32
+ "-f",
33
+ help="Path to plan file (auto-generate decision-anchor skeleton)",
34
+ )
35
+
36
+
37
+ USAGE_TEXT = """Task Management Script for cowork-flow workflow
38
+
39
+ Usage:
40
+ ./.cowork-flow/run task create <title> Create new task directory
41
+ ./.cowork-flow/run task create <title> --parent <dir> Create task as child of parent
42
+ ./.cowork-flow/run task init-context <dir> <dev_type> Initialize jsonl files
43
+ ./.cowork-flow/run task add-context <dir> <jsonl> <path> [reason] [--type TYPE]
44
+ Add entry to jsonl
45
+ ./.cowork-flow/run task add-planned-file <dir> <jsonl> <path> [reason]
46
+ Add planned file entry
47
+ ./.cowork-flow/run task validate <dir> Validate jsonl files
48
+ ./.cowork-flow/run task list-context <dir> List jsonl entries
49
+ ./.cowork-flow/run task start <dir> Set active session task
50
+ ./.cowork-flow/run task review [dir] Mark task ready for check
51
+ ./.cowork-flow/run task complete [dir] Mark task completed
52
+ ./.cowork-flow/run task finish Clear active session task
53
+ ./.cowork-flow/run task next [dir] [--json] [--intent I] Show next safe workflow action
54
+ ./.cowork-flow/run task archive <task-name> Archive completed task and linked changes
55
+ ./.cowork-flow/run task add-subtask <parent> <child> Link child task to parent
56
+ ./.cowork-flow/run task remove-subtask <parent> <child> Unlink child from parent
57
+ ./.cowork-flow/run task list [--mine] [--status <status>] List tasks
58
+ ./.cowork-flow/run task list-archive [YYYY-MM] List archived tasks
59
+
60
+ Arguments:
61
+ dev_type: backend | frontend | fullstack | test | docs
62
+
63
+ List options:
64
+ --mine, -m Show only tasks assigned to current developer
65
+ --status, -s <s> Filter by status (planning, in_progress, review, completed)
66
+
67
+ Examples:
68
+ ./.cowork-flow/run task create "Add login feature" --slug add-login
69
+ ./.cowork-flow/run task create "Child task" --slug child --parent .cowork-flow/tasks/01-21-parent
70
+ ./.cowork-flow/run task init-context .cowork-flow/tasks/01-21-add-login backend
71
+ ./.cowork-flow/run task next
72
+ ./.cowork-flow/run task add-context <dir> implement .cowork-flow/spec/backend/auth.md "Auth guidelines"
73
+ ./.cowork-flow/run task add-context <dir> implement src/new.py "Planned source" --type planned-file
74
+ ./.cowork-flow/run task add-planned-file <dir> implement src/new.py "Planned source"
75
+ ./.cowork-flow/run task start .cowork-flow/tasks/01-21-add-login
76
+ ./.cowork-flow/run task review
77
+ ./.cowork-flow/run task complete
78
+ ./.cowork-flow/run task finish
79
+ ./.cowork-flow/run task archive add-login
80
+ ./.cowork-flow/run task add-subtask parent-task child-task
81
+ ./.cowork-flow/run task remove-subtask parent-task child-task
82
+ ./.cowork-flow/run task list
83
+ ./.cowork-flow/run task list --mine
84
+ ./.cowork-flow/run task list --mine --status in_progress
85
+ """
86
+
87
+
88
+ def _add_init_context_command(subparsers: argparse._SubParsersAction) -> None:
89
+ init_context = subparsers.add_parser(
90
+ "init-context",
91
+ help="Initialize context files",
92
+ )
93
+ init_context.add_argument("dir", help="Task directory")
94
+ init_context.add_argument(
95
+ "type",
96
+ help="Dev type: backend|frontend|fullstack|test|docs",
97
+ )
98
+
99
+
100
+ def _add_add_context_command(subparsers: argparse._SubParsersAction) -> None:
101
+ add_context = subparsers.add_parser(
102
+ "add-context",
103
+ help="Add context entry",
104
+ )
105
+ add_context.add_argument("dir", help="Task directory")
106
+ add_context.add_argument(
107
+ "file",
108
+ help="JSONL file (implement|check|debug)",
109
+ )
110
+ add_context.add_argument("path", help="File path to add")
111
+ add_context.add_argument(
112
+ "reason",
113
+ nargs="?",
114
+ help="Reason for adding",
115
+ )
116
+ add_context.add_argument(
117
+ "--type",
118
+ dest="entry_type",
119
+ choices=("file", "directory", "planned-file"),
120
+ default=None,
121
+ help="Context entry type; planned-file may target a file not created yet",
122
+ )
123
+
124
+
125
+ def _add_add_planned_file_command(subparsers: argparse._SubParsersAction) -> None:
126
+ add_planned_file = subparsers.add_parser(
127
+ "add-planned-file",
128
+ help="Add planned file context entry",
129
+ )
130
+ add_planned_file.add_argument("dir", help="Task directory")
131
+ add_planned_file.add_argument(
132
+ "file",
133
+ help="JSONL file (implement|check|debug)",
134
+ )
135
+ add_planned_file.add_argument("path", help="Planned file path to add")
136
+ add_planned_file.add_argument(
137
+ "reason",
138
+ nargs="?",
139
+ help="Reason for adding",
140
+ )
141
+
142
+
143
+ def _add_context_validation_commands(subparsers: argparse._SubParsersAction) -> None:
144
+ validate = subparsers.add_parser(
145
+ "validate",
146
+ help="Validate context files",
147
+ )
148
+ validate.add_argument("dir", help="Task directory")
149
+
150
+ list_context = subparsers.add_parser(
151
+ "list-context",
152
+ help="List context entries",
153
+ )
154
+ list_context.add_argument("dir", help="Task directory")
155
+
156
+
157
+ def _add_context_commands(subparsers: argparse._SubParsersAction) -> None:
158
+ _add_init_context_command(subparsers)
159
+ _add_add_context_command(subparsers)
160
+ _add_add_planned_file_command(subparsers)
161
+ _add_context_validation_commands(subparsers)
162
+
163
+
164
+ def _add_start_and_batch_commands(subparsers: argparse._SubParsersAction) -> None:
165
+ start = subparsers.add_parser(
166
+ "start",
167
+ help="Set active session task",
168
+ )
169
+ start.add_argument("dir", help="Task directory")
170
+ start.add_argument(
171
+ "--auto",
172
+ action="store_true",
173
+ help="Enable batch mode (requires --approved)",
174
+ )
175
+ start.add_argument(
176
+ "--approved",
177
+ action="store_true",
178
+ help="User has approved the plan",
179
+ )
180
+
181
+ batch_resume = subparsers.add_parser(
182
+ "batch-resume",
183
+ help="Resume a paused Batch operation",
184
+ )
185
+ batch_resume.add_argument(
186
+ "operation_id",
187
+ help="Batch operation id",
188
+ )
189
+
190
+ batch_result = subparsers.add_parser(
191
+ "batch-record-result",
192
+ help="Record one Host action result and advance Batch",
193
+ )
194
+ batch_result.add_argument(
195
+ "operation_id",
196
+ help="Batch operation id",
197
+ )
198
+ batch_result.add_argument(
199
+ "--file",
200
+ type=Path,
201
+ required=True,
202
+ help="UTF-8 JSON Host action result",
203
+ )
204
+
205
+
206
+ def _add_optional_task_dir(parser: argparse.ArgumentParser) -> None:
207
+ parser.add_argument(
208
+ "dir",
209
+ nargs="?",
210
+ help="Task directory or name",
211
+ )
212
+
213
+
214
+ def _add_review_command(subparsers: argparse._SubParsersAction) -> None:
215
+ review = subparsers.add_parser(
216
+ "review",
217
+ help="Mark task ready for check",
218
+ )
219
+ _add_optional_task_dir(review)
220
+
221
+
222
+ def _add_complete_command(subparsers: argparse._SubParsersAction) -> None:
223
+ complete = subparsers.add_parser(
224
+ "complete",
225
+ help="Mark task completed",
226
+ )
227
+ _add_optional_task_dir(complete)
228
+
229
+
230
+ def _add_next_command(subparsers: argparse._SubParsersAction) -> None:
231
+ next_parser = subparsers.add_parser(
232
+ "next",
233
+ help="Show next safe workflow action",
234
+ )
235
+ _add_optional_task_dir(next_parser)
236
+ next_parser.add_argument(
237
+ "--json",
238
+ action="store_true",
239
+ help="Render the stable machine-readable navigation contract",
240
+ )
241
+ next_parser.add_argument(
242
+ "--intent",
243
+ choices=(
244
+ "question",
245
+ "clarify",
246
+ "plan",
247
+ "implement",
248
+ "review",
249
+ "debug",
250
+ "discuss",
251
+ "batch",
252
+ ),
253
+ help="Classified user intent for structured routing",
254
+ )
255
+
256
+
257
+ def _add_lifecycle_commands(subparsers: argparse._SubParsersAction) -> None:
258
+ subparsers.add_parser(
259
+ "current",
260
+ help="Show active session task",
261
+ )
262
+ _add_review_command(subparsers)
263
+ _add_complete_command(subparsers)
264
+ _add_next_command(subparsers)
265
+ subparsers.add_parser(
266
+ "finish",
267
+ help="Clear active session task",
268
+ )
269
+
270
+
271
+ def _add_archive_command(subparsers: argparse._SubParsersAction) -> None:
272
+ archive = subparsers.add_parser("archive", help="Archive task")
273
+ archive.add_argument("name", help="Task name")
274
+ archive.add_argument(
275
+ "--commit",
276
+ action="store_true",
277
+ help="Auto git commit after archive",
278
+ )
279
+
280
+
281
+ def _add_list_command(subparsers: argparse._SubParsersAction) -> None:
282
+ list_parser = subparsers.add_parser("list", help="List tasks")
283
+ list_parser.add_argument(
284
+ "--mine",
285
+ "-m",
286
+ action="store_true",
287
+ help="My tasks only",
288
+ )
289
+ list_parser.add_argument(
290
+ "--status",
291
+ "-s",
292
+ help="Filter by status",
293
+ )
294
+
295
+
296
+ def _add_subtask_commands(subparsers: argparse._SubParsersAction) -> None:
297
+ add_subtask = subparsers.add_parser(
298
+ "add-subtask",
299
+ help="Link child task to parent",
300
+ )
301
+ add_subtask.add_argument(
302
+ "parent_dir",
303
+ help="Parent task directory",
304
+ )
305
+ add_subtask.add_argument(
306
+ "child_dir",
307
+ help="Child task directory",
308
+ )
309
+
310
+ remove_subtask = subparsers.add_parser(
311
+ "remove-subtask",
312
+ help="Unlink child task from parent",
313
+ )
314
+ remove_subtask.add_argument(
315
+ "parent_dir",
316
+ help="Parent task directory",
317
+ )
318
+ remove_subtask.add_argument(
319
+ "child_dir",
320
+ help="Child task directory",
321
+ )
322
+
323
+
324
+ def _add_list_archive_command(subparsers: argparse._SubParsersAction) -> None:
325
+ list_archive = subparsers.add_parser(
326
+ "list-archive",
327
+ help="List archived tasks",
328
+ )
329
+ list_archive.add_argument(
330
+ "month",
331
+ nargs="?",
332
+ help="Month (YYYY-MM)",
333
+ )
334
+
335
+
336
+ def _add_archive_and_listing_commands(subparsers: argparse._SubParsersAction) -> None:
337
+ _add_archive_command(subparsers)
338
+ _add_list_command(subparsers)
339
+ _add_subtask_commands(subparsers)
340
+ _add_list_archive_command(subparsers)
341
+
342
+
343
+ def build_parser() -> argparse.ArgumentParser:
344
+ parser = argparse.ArgumentParser(
345
+ description="Task Management Script for cowork-flow workflow",
346
+ formatter_class=argparse.RawDescriptionHelpFormatter,
347
+ parents=[build_internal_execution_context_parser()],
348
+ )
349
+ subparsers = parser.add_subparsers(
350
+ dest="command",
351
+ help="Commands",
352
+ )
353
+
354
+ _add_create_command(subparsers)
355
+ _add_context_commands(subparsers)
356
+ _add_start_and_batch_commands(subparsers)
357
+ _add_lifecycle_commands(subparsers)
358
+ _add_archive_and_listing_commands(subparsers)
359
+ return parser
360
+
361
+
362
+ def show_usage() -> None:
363
+ """Show usage help."""
364
+ print(USAGE_TEXT)
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """Shared delivery helpers for task commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import re
8
+ import shlex
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from common.core.config import get_hooks
14
+ from common.task.task_repository import TaskRepository
15
+
16
+
17
+ class Colors:
18
+ RED = "\033[0;31m"
19
+ GREEN = "\033[0;32m"
20
+ YELLOW = "\033[1;33m"
21
+ BLUE = "\033[0;34m"
22
+ CYAN = "\033[0;36m"
23
+ NC = "\033[0m"
24
+
25
+
26
+ def colored(text: str, color: str) -> str:
27
+ return f"{color}{text}{Colors.NC}"
28
+
29
+
30
+ def run_hooks(event: str, task_json_path: Path, repo_root: Path) -> None:
31
+ """Run configured lifecycle hooks with UTF-8 subprocess decoding."""
32
+ commands = get_hooks(event, repo_root)
33
+ if not commands:
34
+ return
35
+
36
+ env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)}
37
+ for command in commands:
38
+ try:
39
+ result = subprocess.run(
40
+ shlex.split(command),
41
+ cwd=repo_root,
42
+ env=env,
43
+ capture_output=True,
44
+ text=True,
45
+ encoding="utf-8",
46
+ errors="replace",
47
+ )
48
+ if result.returncode != 0:
49
+ print(
50
+ colored(
51
+ f"[WARN] Hook failed ({event}): {command}",
52
+ Colors.YELLOW,
53
+ ),
54
+ file=sys.stderr,
55
+ )
56
+ if result.stderr.strip():
57
+ print(f" {result.stderr.strip()}", file=sys.stderr)
58
+ except Exception as error:
59
+ print(
60
+ colored(
61
+ f"[WARN] Hook error ({event}): {command} - {error}",
62
+ Colors.YELLOW,
63
+ ),
64
+ file=sys.stderr,
65
+ )
66
+
67
+
68
+ def slugify(title: str) -> str:
69
+ result = title.lower()
70
+ result = re.sub(r"[^a-z0-9]", "-", result)
71
+ result = re.sub(r"-+", "-", result)
72
+ return result.strip("-")
73
+
74
+
75
+ def resolve_task_dir(target_dir: str | Path, repo_root: Path) -> Path:
76
+ return TaskRepository(repo_root).resolve(target_dir)
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env python3
2
+ """Delivery adapters for task tree commands."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+
8
+ from application.task_tree import TaskTreeError, TaskTreeService
9
+ from commands.task_support import Colors, colored, resolve_task_dir
10
+ from common.core.paths import (
11
+ DIR_TASKS,
12
+ DIR_WORKFLOW,
13
+ get_developer,
14
+ get_repo_root,
15
+ )
16
+ from common.task.active_task import get_active_task
17
+
18
+
19
+ def cmd_add_subtask(args) -> int:
20
+ repo_root = get_repo_root()
21
+ parent_dir = resolve_task_dir(args.parent_dir, repo_root)
22
+ child_dir = resolve_task_dir(args.child_dir, repo_root)
23
+ try:
24
+ TaskTreeService(repo_root).link(parent_dir, child_dir)
25
+ except TaskTreeError as error:
26
+ if error.code == "TASK-TREE-PARENT-LOAD-001":
27
+ message = f"Parent task.json not found: {args.parent_dir}"
28
+ elif error.code == "TASK-TREE-CHILD-LOAD-001":
29
+ message = f"Child task.json not found: {args.child_dir}"
30
+ elif error.code == "TASK-TREE-PARENT-001":
31
+ existing_parent = error.detail.removeprefix(
32
+ "child already has parent "
33
+ )
34
+ message = f"Child task already has a parent: {existing_parent}"
35
+ else:
36
+ message = "Failed to update task.json"
37
+ print(colored(f"Error: {message}", Colors.RED), file=sys.stderr)
38
+ return 1
39
+ print(
40
+ colored(
41
+ f"Linked: {child_dir.name} -> {parent_dir.name}",
42
+ Colors.GREEN,
43
+ ),
44
+ file=sys.stderr,
45
+ )
46
+ return 0
47
+
48
+
49
+ def cmd_remove_subtask(args) -> int:
50
+ repo_root = get_repo_root()
51
+ parent_dir = resolve_task_dir(args.parent_dir, repo_root)
52
+ child_dir = resolve_task_dir(args.child_dir, repo_root)
53
+ try:
54
+ TaskTreeService(repo_root).unlink(parent_dir, child_dir)
55
+ except TaskTreeError as error:
56
+ if error.code == "TASK-TREE-PARENT-LOAD-001":
57
+ message = f"Parent task.json not found: {args.parent_dir}"
58
+ elif error.code == "TASK-TREE-CHILD-LOAD-001":
59
+ message = f"Child task.json not found: {args.child_dir}"
60
+ else:
61
+ message = "Failed to update task.json"
62
+ print(colored(f"Error: {message}", Colors.RED), file=sys.stderr)
63
+ return 1
64
+ print(
65
+ colored(
66
+ f"Unlinked: {child_dir.name} from {parent_dir.name}",
67
+ Colors.GREEN,
68
+ ),
69
+ file=sys.stderr,
70
+ )
71
+ return 0
72
+
73
+
74
+ def cmd_list(args) -> int:
75
+ repo_root = get_repo_root()
76
+ active_task = get_active_task(repo_root).task_path
77
+ developer = get_developer(repo_root)
78
+ filter_mine = args.mine
79
+ filter_status = args.status
80
+
81
+ if filter_mine:
82
+ if not developer:
83
+ print(
84
+ colored(
85
+ "Error: No developer set. Run init_developer.py first",
86
+ Colors.RED,
87
+ ),
88
+ file=sys.stderr,
89
+ )
90
+ return 1
91
+ print(colored(f"My tasks (assignee: {developer}):", Colors.BLUE))
92
+ else:
93
+ print(colored("All active tasks:", Colors.BLUE))
94
+ print()
95
+
96
+ service = TaskTreeService(repo_root)
97
+ all_tasks = service.active_nodes()
98
+ count = 0
99
+
100
+ def print_task(dir_name: str, indent: int = 0) -> None:
101
+ nonlocal count
102
+ info = all_tasks[dir_name]
103
+ if filter_mine and info.assignee != developer:
104
+ return
105
+ if filter_status and info.status != filter_status:
106
+ return
107
+
108
+ relative_path = f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
109
+ marker = (
110
+ f" {colored('<- active', Colors.GREEN)}"
111
+ if relative_path == active_task
112
+ else ""
113
+ )
114
+ done, total = service.children_progress(info.children, all_tasks)
115
+ progress = f" [{done}/{total} done]" if info.children else ""
116
+ prefix = " " * indent + " - "
117
+ if filter_mine:
118
+ print(f"{prefix}{dir_name}/ ({info.status}){progress}{marker}")
119
+ else:
120
+ print(
121
+ f"{prefix}{dir_name}/ ({info.status}){progress} "
122
+ f"[{colored(info.assignee, Colors.CYAN)}]{marker}"
123
+ )
124
+ count += 1
125
+ for child_name in info.children:
126
+ if child_name in all_tasks:
127
+ print_task(child_name, indent + 1)
128
+
129
+ for dir_name in service.root_names(all_tasks):
130
+ print_task(dir_name)
131
+
132
+ if count == 0:
133
+ print(
134
+ " (no tasks assigned to you)"
135
+ if filter_mine
136
+ else " (no active tasks)"
137
+ )
138
+ print()
139
+ print(f"Total: {count} task(s)")
140
+ return 0
141
+
142
+
143
+ def cmd_list_archive(args) -> int:
144
+ repo_root = get_repo_root()
145
+ month = args.month
146
+ archives = TaskTreeService(repo_root).archived_tasks(month)
147
+
148
+ print(colored("Archived tasks:", Colors.BLUE))
149
+ print()
150
+ if month:
151
+ if month in archives:
152
+ print(f"[{month}]")
153
+ for task_name in archives[month]:
154
+ print(f" - {task_name}/")
155
+ else:
156
+ print(f" No archives for {month}")
157
+ else:
158
+ for month_name, task_names in archives.items():
159
+ print(f"[{month_name}] - {len(task_names)} task(s)")
160
+ return 0