@jahanxu/trellis 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +58 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +17 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +19 -0
- package/dist/constants/paths.js.map +1 -1
- package/dist/templates/claude/commands/trellis/handoff.md +90 -387
- package/dist/templates/claude/commands/trellis/pick-task.md +74 -444
- package/dist/templates/claude/hooks/inject-subagent-context.py +17 -101
- package/dist/templates/claude/hooks/ralph-loop.py +1 -0
- package/dist/templates/claude/hooks/session-start.py +170 -54
- package/dist/templates/iflow/commands/trellis/handoff.md +148 -0
- package/dist/templates/iflow/commands/trellis/pick-task.md +145 -0
- package/dist/templates/iflow/hooks/inject-subagent-context.py +1 -0
- package/dist/templates/iflow/hooks/ralph-loop.py +1 -0
- package/dist/templates/iflow/hooks/session-start.py +171 -0
- package/dist/templates/markdown/index.d.ts +9 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +10 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/roles/designer/index.md.txt +57 -0
- package/dist/templates/markdown/spec/roles/designer/mock-data-standards.md.txt +63 -0
- package/dist/templates/markdown/spec/roles/designer/prototype-guidelines.md.txt +49 -0
- package/dist/templates/markdown/spec/roles/frontend-impl/api-integration.md.txt +63 -0
- package/dist/templates/markdown/spec/roles/frontend-impl/index.md.txt +57 -0
- package/dist/templates/markdown/spec/roles/frontend-impl/prototype-to-production.md.txt +57 -0
- package/dist/templates/markdown/spec/roles/pm/index.md.txt +45 -0
- package/dist/templates/markdown/spec/roles/pm/prd-template.md.txt +64 -0
- package/dist/templates/markdown/spec/roles/pm/requirement-checklist.md.txt +43 -0
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +3 -2
- package/dist/templates/trellis/scripts/common/cli_adapter.py +4 -3
- package/dist/templates/trellis/scripts/common/developer.py +4 -3
- package/dist/templates/trellis/scripts/common/git_context.py +7 -7
- package/dist/templates/trellis/scripts/common/paths.py +64 -14
- package/dist/templates/trellis/scripts/common/phase.py +2 -2
- package/dist/templates/trellis/scripts/common/registry.py +16 -15
- package/dist/templates/trellis/scripts/common/task_queue.py +10 -10
- package/dist/templates/trellis/scripts/common/task_utils.py +5 -4
- package/dist/templates/trellis/scripts/common/worktree.py +8 -7
- package/dist/templates/trellis/scripts/pool.py +214 -265
- package/dist/templates/trellis/scripts/task.py +3 -116
- package/package.json +3 -3
- package/dist/templates/claude/commands/trellis/before-role-work.md +0 -364
- package/dist/templates/trellis/VERSION +0 -1
- package/dist/templates/trellis/deliverables/README.md +0 -51
- package/dist/templates/trellis/paths.README.md +0 -277
- package/dist/templates/trellis/paths.yaml +0 -41
- package/dist/templates/trellis/pool/implementations.json +0 -5
- package/dist/templates/trellis/pool/prototypes.json +0 -5
- package/dist/templates/trellis/pool/requirements.json +0 -5
- package/dist/templates/trellis/scripts/common/project_paths.py +0 -189
- package/dist/templates/trellis/scripts/handoff_generator.py +0 -380
- package/dist/templates/trellis/spec/roles/designer/index.md +0 -243
- package/dist/templates/trellis/spec/roles/designer/mock-data-standards.md +0 -481
- package/dist/templates/trellis/spec/roles/designer/prototype-guidelines.md +0 -429
- package/dist/templates/trellis/spec/roles/frontend-impl/api-integration.md +0 -565
- package/dist/templates/trellis/spec/roles/frontend-impl/index.md +0 -321
- package/dist/templates/trellis/spec/roles/frontend-impl/state-management.md +0 -599
- package/dist/templates/trellis/spec/roles/pm/index.md +0 -112
- package/dist/templates/trellis/spec/roles/pm/prd-template.md +0 -124
|
@@ -1,38 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Manages three role-based task pools:
|
|
7
|
-
- requirements.json (PM outputs)
|
|
8
|
-
- prototypes.json (Designer outputs)
|
|
9
|
-
- implementations.json (Frontend outputs)
|
|
4
|
+
Pool Management Script for Three-Role Collaboration Pipeline.
|
|
10
5
|
|
|
11
6
|
Usage:
|
|
12
|
-
python3 pool.py
|
|
13
|
-
python3 pool.py
|
|
14
|
-
python3 pool.py
|
|
15
|
-
python3 pool.py
|
|
7
|
+
python3 pool.py init [pool-name...] # Create pool JSON files
|
|
8
|
+
python3 pool.py add <pool> <id> <title> <path> [--handoff <path>] # Add deliverable
|
|
9
|
+
python3 pool.py list [pool] # List available items
|
|
10
|
+
python3 pool.py status <pool> <id> # Get item status
|
|
11
|
+
python3 pool.py consume <pool> <id> <consumed-by> # Mark as consumed
|
|
16
12
|
"""
|
|
17
13
|
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
# IMPORTANT: Force stdout to use UTF-8 on Windows
|
|
19
|
+
# This fixes UnicodeEncodeError when outputting non-ASCII characters
|
|
20
|
+
if sys.platform == "win32":
|
|
21
|
+
import io as _io
|
|
22
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
23
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
24
|
+
elif hasattr(sys.stdout, "detach"):
|
|
25
|
+
sys.stdout = _io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
26
|
+
|
|
18
27
|
import argparse
|
|
19
28
|
import json
|
|
29
|
+
import os
|
|
20
30
|
import sys
|
|
21
|
-
from datetime import datetime
|
|
31
|
+
from datetime import datetime, timezone
|
|
22
32
|
from pathlib import Path
|
|
23
|
-
from typing import Optional, Tuple, Dict
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# Constants
|
|
32
|
-
# =============================================================================
|
|
34
|
+
from common.paths import (
|
|
35
|
+
get_repo_root,
|
|
36
|
+
get_developer,
|
|
37
|
+
get_pool_dir,
|
|
38
|
+
get_pool_file,
|
|
39
|
+
)
|
|
33
40
|
|
|
34
|
-
POOL_DIR = ".trellis/pool"
|
|
35
|
-
POOL_TYPES = ["requirements", "prototypes", "implementations"]
|
|
36
41
|
|
|
37
42
|
# =============================================================================
|
|
38
43
|
# Colors
|
|
@@ -48,280 +53,246 @@ class Colors:
|
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
def colored(text: str, color: str) -> str:
|
|
56
|
+
"""Apply color to text."""
|
|
51
57
|
return f"{color}{text}{Colors.NC}"
|
|
52
58
|
|
|
53
59
|
|
|
54
60
|
# =============================================================================
|
|
55
|
-
#
|
|
61
|
+
# Constants
|
|
56
62
|
# =============================================================================
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
"""Get pool JSON file path."""
|
|
60
|
-
if pool_type not in POOL_TYPES:
|
|
61
|
-
raise ValueError(f"Invalid pool type: {pool_type}. Must be one of {POOL_TYPES}")
|
|
62
|
-
return repo_root / POOL_DIR / f"{pool_type}.json"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def read_pool(pool_file: Path) -> dict:
|
|
66
|
-
"""Read pool JSON file."""
|
|
67
|
-
if not pool_file.exists():
|
|
68
|
-
return {"available": [], "consumed": [], "last_updated": None}
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
return json.loads(pool_file.read_text(encoding="utf-8"))
|
|
72
|
-
except (json.JSONDecodeError, OSError) as e:
|
|
73
|
-
print(colored(f"Error reading pool file: {e}", Colors.RED), file=sys.stderr)
|
|
74
|
-
return {"available": [], "consumed": [], "last_updated": None}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def write_pool_atomic(pool_file: Path, data: dict) -> None:
|
|
78
|
-
"""Write pool JSON with atomic operation (no complex locking needed).
|
|
79
|
-
|
|
80
|
-
Uses temp file + rename for atomicity.
|
|
81
|
-
Based on risk analysis: low concurrency risk for most pools.
|
|
82
|
-
"""
|
|
83
|
-
# Update timestamp
|
|
84
|
-
data["last_updated"] = datetime.now().isoformat()
|
|
85
|
-
|
|
86
|
-
# Write to temp file
|
|
87
|
-
tmp_file = pool_file.with_suffix(".tmp")
|
|
88
|
-
tmp_file.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
64
|
+
DEFAULT_POOLS = ["requirements", "prototypes", "implementations"]
|
|
89
65
|
|
|
90
|
-
|
|
91
|
-
tmp_file.replace(pool_file)
|
|
66
|
+
EMPTY_POOL = {"available": []}
|
|
92
67
|
|
|
93
68
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
(status, task_data) where status is "available" or "consumed"
|
|
99
|
-
"""
|
|
100
|
-
for task in pool_data.get("available", []):
|
|
101
|
-
if task.get("id") == task_id:
|
|
102
|
-
return ("available", task)
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# Pool I/O (atomic writes)
|
|
71
|
+
# =============================================================================
|
|
103
72
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
73
|
+
def _read_pool(pool_path: Path) -> dict:
|
|
74
|
+
"""Read pool JSON file. Auto-create if missing, backup if corrupted."""
|
|
75
|
+
if not pool_path.is_file():
|
|
76
|
+
return {"available": []}
|
|
107
77
|
|
|
108
|
-
|
|
78
|
+
try:
|
|
79
|
+
data = json.loads(pool_path.read_text(encoding="utf-8"))
|
|
80
|
+
if not isinstance(data, dict) or "available" not in data:
|
|
81
|
+
raise ValueError("Invalid pool schema")
|
|
82
|
+
return data
|
|
83
|
+
except (json.JSONDecodeError, ValueError):
|
|
84
|
+
# Backup corrupted file
|
|
85
|
+
bak_path = pool_path.with_suffix(".json.bak")
|
|
86
|
+
try:
|
|
87
|
+
pool_path.rename(bak_path)
|
|
88
|
+
print(colored(f" Warning: Corrupted pool backed up to {bak_path.name}", Colors.YELLOW))
|
|
89
|
+
except OSError:
|
|
90
|
+
pass
|
|
91
|
+
return {"available": []}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _write_pool(pool_path: Path, data: dict) -> bool:
|
|
95
|
+
"""Write pool JSON atomically (write .tmp then os.replace)."""
|
|
96
|
+
tmp_path = pool_path.with_suffix(".json.tmp")
|
|
97
|
+
try:
|
|
98
|
+
pool_path.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
tmp_path.write_text(
|
|
100
|
+
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
101
|
+
encoding="utf-8",
|
|
102
|
+
)
|
|
103
|
+
os.replace(str(tmp_path), str(pool_path))
|
|
104
|
+
return True
|
|
105
|
+
except (OSError, IOError) as e:
|
|
106
|
+
print(colored(f" Error writing pool: {e}", Colors.RED))
|
|
107
|
+
# Clean up tmp if it exists
|
|
108
|
+
try:
|
|
109
|
+
tmp_path.unlink(missing_ok=True)
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
return False
|
|
109
113
|
|
|
110
114
|
|
|
111
115
|
# =============================================================================
|
|
112
|
-
#
|
|
116
|
+
# Subcommands
|
|
113
117
|
# =============================================================================
|
|
114
118
|
|
|
115
|
-
def
|
|
116
|
-
"""
|
|
119
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
120
|
+
"""Create pool JSON files."""
|
|
117
121
|
repo_root = get_repo_root()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
pool_dir = get_pool_dir(repo_root)
|
|
123
|
+
pool_dir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
pool_names = args.pools if args.pools else DEFAULT_POOLS
|
|
126
|
+
|
|
127
|
+
for name in pool_names:
|
|
128
|
+
pool_path = pool_dir / f"{name}.json"
|
|
129
|
+
if pool_path.is_file():
|
|
130
|
+
print(colored(f" Pool '{name}' already exists, skipping", Colors.YELLOW))
|
|
131
|
+
continue
|
|
132
|
+
if _write_pool(pool_path, EMPTY_POOL):
|
|
133
|
+
print(colored(f" Created pool: {name}", Colors.GREEN))
|
|
124
134
|
else:
|
|
125
|
-
|
|
126
|
-
task_file = Path(args.task_data)
|
|
127
|
-
if not task_file.exists():
|
|
128
|
-
print(colored(f"Error: File not found: {args.task_data}", Colors.RED))
|
|
129
|
-
return 1
|
|
130
|
-
task_data = json.loads(task_file.read_text(encoding="utf-8"))
|
|
131
|
-
except json.JSONDecodeError as e:
|
|
132
|
-
print(colored(f"Error: Invalid JSON: {e}", Colors.RED))
|
|
133
|
-
return 1
|
|
134
|
-
|
|
135
|
-
# Validate required fields
|
|
136
|
-
required_fields = ["id", "title", "path", "completed_by", "handoff_doc"]
|
|
137
|
-
for field in required_fields:
|
|
138
|
-
if field not in task_data:
|
|
139
|
-
print(colored(f"Error: Missing required field: {field}", Colors.RED))
|
|
135
|
+
print(colored(f" Failed to create pool: {name}", Colors.RED))
|
|
140
136
|
return 1
|
|
141
137
|
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
print(colored("Pool initialization complete.", Colors.GREEN))
|
|
139
|
+
return 0
|
|
144
140
|
|
|
145
|
-
# Check if task already exists
|
|
146
|
-
status, existing = find_task_in_pool(pool_data, task_data["id"])
|
|
147
|
-
if status:
|
|
148
|
-
print(colored(f"Warning: Task '{task_data['id']}' already exists in {status} list", Colors.YELLOW))
|
|
149
|
-
if not args.force:
|
|
150
|
-
print("Use --force to overwrite")
|
|
151
|
-
return 1
|
|
152
141
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
def cmd_add(args: argparse.Namespace) -> int:
|
|
143
|
+
"""Add a deliverable to a pool."""
|
|
144
|
+
repo_root = get_repo_root()
|
|
145
|
+
pool_path = get_pool_file(args.pool, repo_root)
|
|
146
|
+
|
|
147
|
+
data = _read_pool(pool_path)
|
|
148
|
+
|
|
149
|
+
# Check for duplicate ID
|
|
150
|
+
for item in data["available"]:
|
|
151
|
+
if item["id"] == args.id:
|
|
152
|
+
print(colored(f" Warning: ID '{args.id}' already exists in pool '{args.pool}', skipping", Colors.YELLOW))
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
developer = get_developer(repo_root) or "unknown"
|
|
156
|
+
|
|
157
|
+
entry = {
|
|
158
|
+
"id": args.id,
|
|
159
|
+
"title": args.title,
|
|
160
|
+
"path": args.path,
|
|
161
|
+
"completed_by": developer,
|
|
162
|
+
"completed_at": datetime.now(timezone.utc).isoformat(),
|
|
163
|
+
"handoff_doc": args.handoff if args.handoff else f"{args.path}/HANDOFF.md",
|
|
164
|
+
"status": "available",
|
|
165
|
+
}
|
|
158
166
|
|
|
159
|
-
|
|
160
|
-
task_data["status"] = "available"
|
|
161
|
-
task_data["added_at"] = datetime.now().isoformat()
|
|
162
|
-
pool_data["available"].append(task_data)
|
|
167
|
+
data["available"].append(entry)
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
if _write_pool(pool_path, data):
|
|
170
|
+
print(colored(f" Added '{args.id}' to pool '{args.pool}'", Colors.GREEN))
|
|
171
|
+
return 0
|
|
172
|
+
return 1
|
|
166
173
|
|
|
167
|
-
print(colored(f"✓ Added task '{task_data['id']}' to {pool_type} pool", Colors.GREEN))
|
|
168
|
-
print(f" Path: {task_data['path']}")
|
|
169
|
-
print(f" HANDOFF: {task_data['handoff_doc']}")
|
|
170
174
|
|
|
171
|
-
|
|
175
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
176
|
+
"""List available items in pools."""
|
|
177
|
+
repo_root = get_repo_root()
|
|
178
|
+
pool_dir = get_pool_dir(repo_root)
|
|
172
179
|
|
|
180
|
+
if not pool_dir.is_dir():
|
|
181
|
+
print(colored("No pool directory found. Run 'pool.py init' first.", Colors.YELLOW))
|
|
182
|
+
return 0
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
pools_to_list = [args.pool] if args.pool else [
|
|
185
|
+
p.stem for p in sorted(pool_dir.glob("*.json"))
|
|
186
|
+
]
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
pool_type = args.pool_type
|
|
182
|
-
filter_status = args.status
|
|
188
|
+
if not pools_to_list:
|
|
189
|
+
print(colored("No pools found.", Colors.YELLOW))
|
|
190
|
+
return 0
|
|
183
191
|
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
for pool_name in pools_to_list:
|
|
193
|
+
pool_path = get_pool_file(pool_name, repo_root)
|
|
194
|
+
data = _read_pool(pool_path)
|
|
186
195
|
|
|
187
|
-
|
|
188
|
-
|
|
196
|
+
available = [i for i in data["available"] if i.get("status") == "available"]
|
|
197
|
+
consumed = [i for i in data["available"] if i.get("status") == "consumed"]
|
|
189
198
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
available = pool_data.get("available", [])
|
|
193
|
-
print(colored(f"Available ({len(available)}):", Colors.GREEN))
|
|
194
|
-
if available:
|
|
195
|
-
for task in available:
|
|
196
|
-
print(f" • {colored(task['id'], Colors.CYAN)}: {task['title']}")
|
|
197
|
-
print(f" Completed by: {task['completed_by']}")
|
|
198
|
-
print(f" Path: {task['path']}")
|
|
199
|
-
print()
|
|
200
|
-
else:
|
|
201
|
-
print(" (none)")
|
|
202
|
-
print()
|
|
199
|
+
print(colored(f"\n=== {pool_name} ===", Colors.CYAN))
|
|
200
|
+
print(f" Available: {len(available)} | Consumed: {len(consumed)}")
|
|
203
201
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
if available:
|
|
203
|
+
print(colored(" Available items:", Colors.GREEN))
|
|
204
|
+
for item in available:
|
|
205
|
+
print(f" - {item['id']}: {item['title']} (by {item.get('completed_by', '?')})")
|
|
208
206
|
if consumed:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
print(f"
|
|
213
|
-
print()
|
|
214
|
-
else:
|
|
215
|
-
print(" (none)")
|
|
216
|
-
print()
|
|
217
|
-
|
|
218
|
-
# Show summary
|
|
219
|
-
last_updated = pool_data.get("last_updated", "never")
|
|
220
|
-
print(colored(f"Last updated: {last_updated}", Colors.BLUE))
|
|
207
|
+
print(colored(" Consumed items:", Colors.YELLOW))
|
|
208
|
+
for item in consumed:
|
|
209
|
+
consumed_by = item.get("consumed_by", "?")
|
|
210
|
+
print(f" - {item['id']}: {item['title']} (consumed by {consumed_by})")
|
|
221
211
|
|
|
212
|
+
print()
|
|
222
213
|
return 0
|
|
223
214
|
|
|
224
215
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
# =============================================================================
|
|
228
|
-
|
|
229
|
-
def cmd_consume(args: argparse.Namespace) -> int:
|
|
230
|
-
"""Mark task as consumed."""
|
|
216
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
217
|
+
"""Get status of a specific item."""
|
|
231
218
|
repo_root = get_repo_root()
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
consumed_by = args.consumed_by
|
|
235
|
-
|
|
236
|
-
pool_file = get_pool_file(pool_type, repo_root)
|
|
237
|
-
pool_data = read_pool(pool_file)
|
|
238
|
-
|
|
239
|
-
# Find task in available list
|
|
240
|
-
task = None
|
|
241
|
-
for t in pool_data.get("available", []):
|
|
242
|
-
if t["id"] == task_id:
|
|
243
|
-
task = t
|
|
244
|
-
break
|
|
245
|
-
|
|
246
|
-
if not task:
|
|
247
|
-
# Check if already consumed
|
|
248
|
-
for t in pool_data.get("consumed", []):
|
|
249
|
-
if t["id"] == task_id:
|
|
250
|
-
print(colored(f"Task '{task_id}' is already consumed", Colors.YELLOW))
|
|
251
|
-
print(f" Consumed by: {t.get('consumed_by', 'unknown')}")
|
|
252
|
-
print(f" Consumed at: {t.get('consumed_at', 'unknown')}")
|
|
253
|
-
return 1
|
|
219
|
+
pool_path = get_pool_file(args.pool, repo_root)
|
|
220
|
+
data = _read_pool(pool_path)
|
|
254
221
|
|
|
255
|
-
|
|
256
|
-
|
|
222
|
+
for item in data["available"]:
|
|
223
|
+
if item["id"] == args.id:
|
|
224
|
+
print(json.dumps(item, indent=2, ensure_ascii=False))
|
|
225
|
+
return 0
|
|
257
226
|
|
|
258
|
-
|
|
259
|
-
|
|
227
|
+
print(colored(f" Item '{args.id}' not found in pool '{args.pool}'", Colors.RED))
|
|
228
|
+
return 1
|
|
260
229
|
|
|
261
|
-
task["status"] = "consumed"
|
|
262
|
-
task["consumed_by"] = consumed_by
|
|
263
|
-
task["consumed_at"] = datetime.now().isoformat()
|
|
264
230
|
|
|
265
|
-
|
|
231
|
+
def cmd_consume(args: argparse.Namespace) -> int:
|
|
232
|
+
"""Mark an item as consumed."""
|
|
233
|
+
repo_root = get_repo_root()
|
|
234
|
+
pool_path = get_pool_file(args.pool, repo_root)
|
|
235
|
+
data = _read_pool(pool_path)
|
|
236
|
+
|
|
237
|
+
for item in data["available"]:
|
|
238
|
+
if item["id"] == args.id:
|
|
239
|
+
if item.get("status") == "consumed":
|
|
240
|
+
consumed_by = item.get("consumed_by", "?")
|
|
241
|
+
print(colored(f" Item '{args.id}' already consumed by {consumed_by}", Colors.YELLOW))
|
|
242
|
+
return 1
|
|
266
243
|
|
|
267
|
-
|
|
268
|
-
|
|
244
|
+
item["status"] = "consumed"
|
|
245
|
+
item["consumed_by"] = args.consumed_by
|
|
246
|
+
item["consumed_at"] = datetime.now(timezone.utc).isoformat()
|
|
269
247
|
|
|
270
|
-
|
|
271
|
-
|
|
248
|
+
if _write_pool(pool_path, data):
|
|
249
|
+
print(colored(f" Marked '{args.id}' as consumed by {args.consumed_by}", Colors.GREEN))
|
|
250
|
+
return 0
|
|
251
|
+
return 1
|
|
272
252
|
|
|
273
|
-
|
|
253
|
+
print(colored(f" Item '{args.id}' not found in pool '{args.pool}'", Colors.RED))
|
|
254
|
+
return 1
|
|
274
255
|
|
|
275
256
|
|
|
276
257
|
# =============================================================================
|
|
277
|
-
#
|
|
258
|
+
# CLI Parser
|
|
278
259
|
# =============================================================================
|
|
279
260
|
|
|
280
|
-
def
|
|
281
|
-
"""
|
|
282
|
-
|
|
283
|
-
|
|
261
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
262
|
+
"""Build argument parser."""
|
|
263
|
+
parser = argparse.ArgumentParser(
|
|
264
|
+
description="Pool management for three-role collaboration pipeline",
|
|
265
|
+
)
|
|
266
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
284
267
|
|
|
285
|
-
|
|
286
|
-
|
|
268
|
+
# init
|
|
269
|
+
p_init = subparsers.add_parser("init", help="Create pool JSON files")
|
|
270
|
+
p_init.add_argument("pools", nargs="*", help="Pool names (default: requirements, prototypes, implementations)")
|
|
287
271
|
|
|
288
|
-
|
|
289
|
-
|
|
272
|
+
# add
|
|
273
|
+
p_add = subparsers.add_parser("add", help="Add deliverable to pool")
|
|
274
|
+
p_add.add_argument("pool", help="Pool name (e.g., requirements)")
|
|
275
|
+
p_add.add_argument("id", help="Deliverable ID (e.g., user-login)")
|
|
276
|
+
p_add.add_argument("title", help="Deliverable title")
|
|
277
|
+
p_add.add_argument("path", help="Path to deliverable directory")
|
|
278
|
+
p_add.add_argument("--handoff", help="Path to HANDOFF.md (default: <path>/HANDOFF.md)")
|
|
290
279
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# Check available tasks
|
|
295
|
-
for task in pool_data.get("available", []):
|
|
296
|
-
task_id = task.get("id", "unknown")
|
|
297
|
-
|
|
298
|
-
# Check required fields
|
|
299
|
-
for field in ["id", "title", "path", "handoff_doc"]:
|
|
300
|
-
if field not in task:
|
|
301
|
-
print(colored(f"✗ Task '{task_id}': Missing field '{field}'", Colors.RED))
|
|
302
|
-
errors += 1
|
|
303
|
-
|
|
304
|
-
# Check if path exists
|
|
305
|
-
if "path" in task:
|
|
306
|
-
path = repo_root / task["path"]
|
|
307
|
-
if not path.exists():
|
|
308
|
-
print(colored(f"✗ Task '{task_id}': Path not found: {task['path']}", Colors.RED))
|
|
309
|
-
errors += 1
|
|
310
|
-
|
|
311
|
-
# Check if HANDOFF exists
|
|
312
|
-
if "handoff_doc" in task:
|
|
313
|
-
handoff = repo_root / task["handoff_doc"]
|
|
314
|
-
if not handoff.exists():
|
|
315
|
-
print(colored(f"⚠ Task '{task_id}': HANDOFF not found: {task['handoff_doc']}", Colors.YELLOW))
|
|
316
|
-
warnings += 1
|
|
280
|
+
# list
|
|
281
|
+
p_list = subparsers.add_parser("list", help="List available items")
|
|
282
|
+
p_list.add_argument("pool", nargs="?", help="Pool name (omit for all)")
|
|
317
283
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
print(colored(f"✗ {errors} error(s), {warnings} warning(s)", Colors.RED if errors > 0 else Colors.YELLOW))
|
|
284
|
+
# status
|
|
285
|
+
p_status = subparsers.add_parser("status", help="Get item status")
|
|
286
|
+
p_status.add_argument("pool", help="Pool name")
|
|
287
|
+
p_status.add_argument("id", help="Item ID")
|
|
323
288
|
|
|
324
|
-
|
|
289
|
+
# consume
|
|
290
|
+
p_consume = subparsers.add_parser("consume", help="Mark item as consumed")
|
|
291
|
+
p_consume.add_argument("pool", help="Pool name")
|
|
292
|
+
p_consume.add_argument("id", help="Item ID")
|
|
293
|
+
p_consume.add_argument("consumed_by", help="Consumer identity (e.g., designer-bob)")
|
|
294
|
+
|
|
295
|
+
return parser
|
|
325
296
|
|
|
326
297
|
|
|
327
298
|
# =============================================================================
|
|
@@ -329,30 +300,7 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|
|
329
300
|
# =============================================================================
|
|
330
301
|
|
|
331
302
|
def main() -> int:
|
|
332
|
-
parser =
|
|
333
|
-
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
334
|
-
|
|
335
|
-
# add
|
|
336
|
-
p_add = subparsers.add_parser("add", help="Add task to pool")
|
|
337
|
-
p_add.add_argument("pool_type", choices=POOL_TYPES, help="Pool type")
|
|
338
|
-
p_add.add_argument("task_data", help="Task data JSON or file path")
|
|
339
|
-
p_add.add_argument("--force", "-f", action="store_true", help="Overwrite existing task")
|
|
340
|
-
|
|
341
|
-
# list
|
|
342
|
-
p_list = subparsers.add_parser("list", help="List tasks in pool")
|
|
343
|
-
p_list.add_argument("pool_type", choices=POOL_TYPES, help="Pool type")
|
|
344
|
-
p_list.add_argument("--status", "-s", choices=["available", "consumed"], help="Filter by status")
|
|
345
|
-
|
|
346
|
-
# consume
|
|
347
|
-
p_consume = subparsers.add_parser("consume", help="Mark task as consumed")
|
|
348
|
-
p_consume.add_argument("pool_type", choices=POOL_TYPES, help="Pool type")
|
|
349
|
-
p_consume.add_argument("task_id", help="Task ID")
|
|
350
|
-
p_consume.add_argument("consumed_by", help="Developer who consumed the task")
|
|
351
|
-
|
|
352
|
-
# check
|
|
353
|
-
p_check = subparsers.add_parser("check", help="Check pool integrity")
|
|
354
|
-
p_check.add_argument("pool_type", choices=POOL_TYPES, help="Pool type")
|
|
355
|
-
|
|
303
|
+
parser = build_parser()
|
|
356
304
|
args = parser.parse_args()
|
|
357
305
|
|
|
358
306
|
if not args.command:
|
|
@@ -360,10 +308,11 @@ def main() -> int:
|
|
|
360
308
|
return 1
|
|
361
309
|
|
|
362
310
|
commands = {
|
|
311
|
+
"init": cmd_init,
|
|
363
312
|
"add": cmd_add,
|
|
364
313
|
"list": cmd_list,
|
|
314
|
+
"status": cmd_status,
|
|
365
315
|
"consume": cmd_consume,
|
|
366
|
-
"check": cmd_check,
|
|
367
316
|
}
|
|
368
317
|
|
|
369
318
|
return commands[args.command](args)
|