@blxzer/cursor-trellis 0.1.1 → 0.1.3
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/README.md +2 -0
- package/dist/migrations/manifests/0.1.1.json +9 -0
- package/dist/migrations/manifests/0.1.2.json +9 -0
- package/dist/templates/cursor/rules/retrieval-routing.mdc +9 -7
- package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +2 -1
- package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +16 -3
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +11 -6
- package/dist/templates/shared-hooks/inject-subagent-context.py +31 -567
- package/dist/templates/trellis/index.d.ts +2 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +4 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +78 -29
- package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +92 -0
- package/dist/templates/trellis/scripts/common/retrieval_adapter_metadata.py +100 -9
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +76 -31
- package/dist/templates/trellis/scripts/common/retrieval_tool_classification.py +18 -3
- package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +19 -0
- package/dist/templates/trellis/scripts/common/smart_search_evidence.py +5 -2
- package/dist/templates/trellis/scripts/common/subagent_dispatch.py +527 -0
- package/dist/templates/trellis/scripts/common/task_store.py +32 -0
- package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +396 -0
- package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +300 -0
- package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +126 -0
- package/dist/templates/trellis/scripts/task.py +26 -0
- package/dist/templates/trellis/workflow.md +5 -22
- package/dist/utils/codebase-retrieval-router.d.ts +5 -0
- package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
- package/dist/utils/codebase-retrieval-router.js +48 -28
- package/dist/utils/codebase-retrieval-router.js.map +1 -1
- package/dist/utils/cursor-retrieval-env.d.ts +28 -0
- package/dist/utils/cursor-retrieval-env.d.ts.map +1 -0
- package/dist/utils/cursor-retrieval-env.js +89 -0
- package/dist/utils/cursor-retrieval-env.js.map +1 -0
- package/dist/utils/project-capabilities.d.ts.map +1 -1
- package/dist/utils/project-capabilities.js +22 -15
- package/dist/utils/project-capabilities.js.map +1 -1
- package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
- package/dist/utils/retrieval-agent-instructions.js +37 -21
- package/dist/utils/retrieval-agent-instructions.js.map +1 -1
- package/dist/utils/retrieval-tool-classification.d.ts +2 -0
- package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
- package/dist/utils/retrieval-tool-classification.js +10 -2
- package/dist/utils/retrieval-tool-classification.js.map +1 -1
- package/dist/utils/semantic-plan-gate.d.ts +8 -0
- package/dist/utils/semantic-plan-gate.d.ts.map +1 -0
- package/dist/utils/semantic-plan-gate.js +42 -0
- package/dist/utils/semantic-plan-gate.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Subagent dispatch prompt builder — single source for hook + CLI Layer 2.
|
|
4
|
+
|
|
5
|
+
Agent-facing only; not documented in user README.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
from .io import read_json
|
|
16
|
+
from .paths import FILE_TASK_JSON
|
|
17
|
+
|
|
18
|
+
DIR_WORKFLOW = ".trellis"
|
|
19
|
+
DIR_SPEC = "spec"
|
|
20
|
+
|
|
21
|
+
AGENT_IMPLEMENT = "trellis-implement"
|
|
22
|
+
AGENT_CHECK = "trellis-check"
|
|
23
|
+
AGENT_RESEARCH = "trellis-research"
|
|
24
|
+
|
|
25
|
+
AGENTS_ALL = (AGENT_IMPLEMENT, AGENT_CHECK, AGENT_RESEARCH)
|
|
26
|
+
AGENTS_REQUIRE_TASK = (AGENT_IMPLEMENT, AGENT_CHECK)
|
|
27
|
+
|
|
28
|
+
INJECTION_MARKER = "<!-- trellis-hook-injected -->"
|
|
29
|
+
|
|
30
|
+
DispatchRole = Literal["implement", "check", "research"]
|
|
31
|
+
|
|
32
|
+
DEFAULT_SCOPES: dict[str, str] = {
|
|
33
|
+
"implement": (
|
|
34
|
+
"Implement per prd.md and implement.md; run project lint/typecheck/tests."
|
|
35
|
+
),
|
|
36
|
+
"check": (
|
|
37
|
+
"Review against prd/spec; fix in-contract defects; record evidence in verify.md."
|
|
38
|
+
),
|
|
39
|
+
"research": "Research and persist to {TASK}/research/.",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def prompt_has_injection_marker(text: str) -> bool:
|
|
44
|
+
return INJECTION_MARKER in text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_file_content(base_path: str, file_path: str) -> str | None:
|
|
48
|
+
full_path = os.path.join(base_path, file_path)
|
|
49
|
+
if os.path.exists(full_path) and os.path.isfile(full_path):
|
|
50
|
+
try:
|
|
51
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
52
|
+
return f.read()
|
|
53
|
+
except OSError:
|
|
54
|
+
return None
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_directory_contents(
|
|
59
|
+
base_path: str, dir_path: str, max_files: int = 20
|
|
60
|
+
) -> list[tuple[str, str]]:
|
|
61
|
+
full_path = os.path.join(base_path, dir_path)
|
|
62
|
+
if not os.path.exists(full_path) or not os.path.isdir(full_path):
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
results: list[tuple[str, str]] = []
|
|
66
|
+
try:
|
|
67
|
+
md_files = sorted(
|
|
68
|
+
f
|
|
69
|
+
for f in os.listdir(full_path)
|
|
70
|
+
if f.endswith(".md") and os.path.isfile(os.path.join(full_path, f))
|
|
71
|
+
)
|
|
72
|
+
for filename in md_files[:max_files]:
|
|
73
|
+
file_full_path = os.path.join(full_path, filename)
|
|
74
|
+
relative_path = os.path.join(dir_path, filename)
|
|
75
|
+
try:
|
|
76
|
+
with open(file_full_path, "r", encoding="utf-8") as f:
|
|
77
|
+
results.append((relative_path, f.read()))
|
|
78
|
+
except OSError:
|
|
79
|
+
continue
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
return results
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_jsonl_entries(
|
|
86
|
+
base_path: str,
|
|
87
|
+
jsonl_path: str,
|
|
88
|
+
*,
|
|
89
|
+
warn_prefix: str = "subagent-dispatch",
|
|
90
|
+
) -> list[tuple[str, str]]:
|
|
91
|
+
full_path = os.path.join(base_path, jsonl_path)
|
|
92
|
+
if not os.path.exists(full_path):
|
|
93
|
+
print(
|
|
94
|
+
f"[{warn_prefix}] WARN: {jsonl_path} not found — "
|
|
95
|
+
"sub-agent will receive only task artifacts",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
results: list[tuple[str, str]] = []
|
|
101
|
+
saw_real_entry = False
|
|
102
|
+
try:
|
|
103
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
104
|
+
for line in f:
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if not line:
|
|
107
|
+
continue
|
|
108
|
+
try:
|
|
109
|
+
item = json.loads(line)
|
|
110
|
+
file_path = item.get("file") or item.get("path")
|
|
111
|
+
entry_type = item.get("type", "file")
|
|
112
|
+
if not file_path:
|
|
113
|
+
continue
|
|
114
|
+
saw_real_entry = True
|
|
115
|
+
if entry_type == "directory":
|
|
116
|
+
results.extend(read_directory_contents(base_path, file_path))
|
|
117
|
+
else:
|
|
118
|
+
content = read_file_content(base_path, file_path)
|
|
119
|
+
if content:
|
|
120
|
+
results.append((file_path, content))
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
continue
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
if not saw_real_entry:
|
|
127
|
+
print(
|
|
128
|
+
f"[{warn_prefix}] WARN: {jsonl_path} has no curated entries "
|
|
129
|
+
"(only seed / empty) — sub-agent will receive only task artifacts.",
|
|
130
|
+
file=sys.stderr,
|
|
131
|
+
)
|
|
132
|
+
return results
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str:
|
|
136
|
+
context_parts: list[str] = []
|
|
137
|
+
agent_jsonl = f"{task_dir}/{agent_type}.jsonl"
|
|
138
|
+
for file_path, content in read_jsonl_entries(repo_root, agent_jsonl):
|
|
139
|
+
context_parts.append(f"=== {file_path} ===\n{content}")
|
|
140
|
+
return "\n\n".join(context_parts)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_implement_context(repo_root: str, task_dir: str) -> str:
|
|
144
|
+
context_parts: list[str] = []
|
|
145
|
+
base_context = get_agent_context(repo_root, task_dir, "implement")
|
|
146
|
+
if base_context:
|
|
147
|
+
context_parts.append(base_context)
|
|
148
|
+
|
|
149
|
+
prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
|
|
150
|
+
if prd_content:
|
|
151
|
+
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
|
|
152
|
+
|
|
153
|
+
design_content = read_file_content(repo_root, f"{task_dir}/design.md")
|
|
154
|
+
if design_content:
|
|
155
|
+
context_parts.append(
|
|
156
|
+
f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
implement_plan_content = read_file_content(repo_root, f"{task_dir}/implement.md")
|
|
160
|
+
if implement_plan_content:
|
|
161
|
+
context_parts.append(
|
|
162
|
+
f"=== {task_dir}/implement.md (Execution Plan) ===\n{implement_plan_content}"
|
|
163
|
+
)
|
|
164
|
+
return "\n\n".join(context_parts)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_check_context(repo_root: str, task_dir: str) -> str:
|
|
168
|
+
context_parts: list[str] = []
|
|
169
|
+
for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"):
|
|
170
|
+
context_parts.append(f"=== {file_path} ===\n{content}")
|
|
171
|
+
|
|
172
|
+
prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
|
|
173
|
+
if prd_content:
|
|
174
|
+
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
|
|
175
|
+
|
|
176
|
+
design_content = read_file_content(repo_root, f"{task_dir}/design.md")
|
|
177
|
+
if design_content:
|
|
178
|
+
context_parts.append(
|
|
179
|
+
f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
implement_plan_content = read_file_content(repo_root, f"{task_dir}/implement.md")
|
|
183
|
+
if implement_plan_content:
|
|
184
|
+
context_parts.append(
|
|
185
|
+
f"=== {task_dir}/implement.md (Execution Plan) ===\n{implement_plan_content}"
|
|
186
|
+
)
|
|
187
|
+
return "\n\n".join(context_parts)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_finish_context(repo_root: str, task_dir: str) -> str:
|
|
191
|
+
return get_check_context(repo_root, task_dir)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_research_context(repo_root: str, task_dir: str | None) -> str:
|
|
195
|
+
_ = task_dir
|
|
196
|
+
spec_path = f"{DIR_WORKFLOW}/{DIR_SPEC}"
|
|
197
|
+
spec_root = Path(repo_root) / DIR_WORKFLOW / DIR_SPEC
|
|
198
|
+
|
|
199
|
+
tree_lines = [f"{spec_path}/"]
|
|
200
|
+
if spec_root.is_dir():
|
|
201
|
+
pkg_dirs = sorted(d for d in spec_root.iterdir() if d.is_dir())
|
|
202
|
+
for i, pkg_dir in enumerate(pkg_dirs):
|
|
203
|
+
is_last = i == len(pkg_dirs) - 1
|
|
204
|
+
prefix = "└── " if is_last else "├── "
|
|
205
|
+
layers = sorted(d.name for d in pkg_dir.iterdir() if d.is_dir())
|
|
206
|
+
layer_info = f" ({', '.join(layers)})" if layers else ""
|
|
207
|
+
tree_lines.append(f"{prefix}{pkg_dir.name}/{layer_info}")
|
|
208
|
+
|
|
209
|
+
project_structure = f"""## Project Spec Directory Structure
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
{chr(10).join(tree_lines)}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
To get structured package info, run: `python ./{DIR_WORKFLOW}/scripts/get_context.py --mode packages`
|
|
216
|
+
|
|
217
|
+
## Search Tips
|
|
218
|
+
|
|
219
|
+
- Spec files: `{spec_path}/**/*.md`
|
|
220
|
+
- Code search: Use Glob and Grep tools
|
|
221
|
+
- External facts / docs: load `smart-search-cli` skill and use Bash (`smart-search` CLI), not Cursor WebSearch/WebFetch by default"""
|
|
222
|
+
return project_structure
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def build_implement_prompt(original_prompt: str, context: str) -> str:
|
|
226
|
+
return f"""{INJECTION_MARKER}
|
|
227
|
+
# Implement Agent Task
|
|
228
|
+
|
|
229
|
+
You are the Implement Agent in the Multi-Agent Pipeline.
|
|
230
|
+
|
|
231
|
+
## Your Context
|
|
232
|
+
|
|
233
|
+
All the information you need has been prepared for you:
|
|
234
|
+
|
|
235
|
+
{context}
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Your Task
|
|
240
|
+
|
|
241
|
+
{original_prompt}
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Workflow
|
|
246
|
+
|
|
247
|
+
1. **Understand specs** - All dev specs are injected above, understand them
|
|
248
|
+
2. **Understand task artifacts** - Read requirements, technical design if present, and execution plan if present
|
|
249
|
+
3. **Implement feature** - Implement following specs and task artifacts
|
|
250
|
+
4. **Self-check** - Ensure code quality against check specs
|
|
251
|
+
|
|
252
|
+
## Important Constraints
|
|
253
|
+
|
|
254
|
+
- Do NOT execute git commit, only code modifications
|
|
255
|
+
- Follow all dev specs injected above
|
|
256
|
+
- Report list of modified/created files when done"""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def build_check_prompt(original_prompt: str, context: str) -> str:
|
|
260
|
+
return f"""{INJECTION_MARKER}
|
|
261
|
+
# Check Agent Task
|
|
262
|
+
|
|
263
|
+
You are the Check Agent in the Multi-Agent Pipeline (code and cross-layer checker).
|
|
264
|
+
|
|
265
|
+
## Your Context
|
|
266
|
+
|
|
267
|
+
All check specs and dev specs you need:
|
|
268
|
+
|
|
269
|
+
{context}
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Your Task
|
|
274
|
+
|
|
275
|
+
{original_prompt}
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Workflow
|
|
280
|
+
|
|
281
|
+
1. **Get changes** - Run `git diff --name-only` and `git diff` to get code changes
|
|
282
|
+
2. **Check against specs** - Check item by item against specs above
|
|
283
|
+
3. **Self-fix** - Fix issues directly, don't just report
|
|
284
|
+
4. **Run verification** - Run project's lint and typecheck commands
|
|
285
|
+
|
|
286
|
+
## Important Constraints
|
|
287
|
+
|
|
288
|
+
- Fix issues yourself, don't just report
|
|
289
|
+
- Must execute complete checklist in check specs
|
|
290
|
+
- Pay special attention to impact radius analysis (L1-L5)"""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def build_finish_prompt(original_prompt: str, context: str) -> str:
|
|
294
|
+
return f"""{INJECTION_MARKER}
|
|
295
|
+
# Finish Agent Task
|
|
296
|
+
|
|
297
|
+
You are performing the final check before creating a PR.
|
|
298
|
+
|
|
299
|
+
## Your Context
|
|
300
|
+
|
|
301
|
+
Finish checklist and requirements:
|
|
302
|
+
|
|
303
|
+
{context}
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Your Task
|
|
308
|
+
|
|
309
|
+
{original_prompt}
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Workflow
|
|
314
|
+
|
|
315
|
+
1. **Review changes** - Run `git diff --name-only` to see all changed files
|
|
316
|
+
2. **Verify task artifacts** - Check requirements in prd.md and, when present, design.md / implement.md
|
|
317
|
+
3. **Spec sync** - Analyze whether changes introduce new patterns, contracts, or conventions
|
|
318
|
+
4. **Run final checks** - Execute lint and typecheck
|
|
319
|
+
5. **Confirm ready** - Ensure code is ready for PR
|
|
320
|
+
|
|
321
|
+
## Important Constraints
|
|
322
|
+
|
|
323
|
+
- You MAY update spec files when gaps are detected (use update-spec.md as guide)
|
|
324
|
+
- MUST read the target spec file BEFORE editing (avoid duplicating existing content)
|
|
325
|
+
- Do NOT update specs for trivial changes (typos, formatting, obvious fixes)
|
|
326
|
+
- Verify all acceptance criteria in prd.md are met"""
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def build_research_prompt(original_prompt: str, context: str) -> str:
|
|
330
|
+
return f"""{INJECTION_MARKER}
|
|
331
|
+
# Research Agent Task
|
|
332
|
+
|
|
333
|
+
You are the Trellis Research Agent.
|
|
334
|
+
|
|
335
|
+
## Core Principle
|
|
336
|
+
|
|
337
|
+
**You do one thing: find, explain, and PERSIST information.**
|
|
338
|
+
|
|
339
|
+
Conversations get compacted; files do not. Every research topic MUST be written under `{{TASK_DIR}}/research/`. Chat-only findings are a failure.
|
|
340
|
+
|
|
341
|
+
## Dispatch contract
|
|
342
|
+
|
|
343
|
+
- External facts: load `smart-search-cli` skill + Bash — default **not** Cursor `WebSearch`/`WebFetch`.
|
|
344
|
+
- Do NOT spawn nested `trellis-implement` / `trellis-check` / `trellis-research` sub-agents.
|
|
345
|
+
|
|
346
|
+
## Project Info
|
|
347
|
+
|
|
348
|
+
{context}
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Your Task
|
|
353
|
+
|
|
354
|
+
{original_prompt}
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Workflow
|
|
359
|
+
|
|
360
|
+
1. **Resolve task** — ensure `{{TASK_DIR}}/research/` exists
|
|
361
|
+
2. **Classify** — internal / external / mixed
|
|
362
|
+
3. **Search** — Glob/Grep/Read for repo; `smart-search-cli` + CLI for external
|
|
363
|
+
4. **Persist** — Write each topic to `{{TASK_DIR}}/research/<topic-slug>.md`
|
|
364
|
+
5. **Report** — Reply with file paths + one-line summaries only
|
|
365
|
+
|
|
366
|
+
## Write ALLOWED
|
|
367
|
+
|
|
368
|
+
- `{{TASK_DIR}}/research/*.md` only
|
|
369
|
+
|
|
370
|
+
## Write FORBIDDEN
|
|
371
|
+
|
|
372
|
+
- Code, `.trellis/spec/`, platform config, git operations"""
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _repo_relative(task_dir: Path, repo_root: Path) -> str:
|
|
376
|
+
try:
|
|
377
|
+
return task_dir.relative_to(repo_root).as_posix()
|
|
378
|
+
except ValueError:
|
|
379
|
+
return task_dir.as_posix()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _truncate_context(context: str, max_chars: int | None, warnings: list[str]) -> str:
|
|
383
|
+
if max_chars is None or len(context) <= max_chars:
|
|
384
|
+
return context
|
|
385
|
+
warnings.append(f"context truncated at {max_chars} chars")
|
|
386
|
+
return context[:max_chars] + "\n...[truncated]..."
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _resolve_scope(role: DispatchRole, scope: str | None, task_rel: str) -> str:
|
|
390
|
+
if scope and scope.strip():
|
|
391
|
+
return scope.strip()
|
|
392
|
+
default = DEFAULT_SCOPES[role]
|
|
393
|
+
if role == "research":
|
|
394
|
+
return default.replace("{TASK}", task_rel)
|
|
395
|
+
return default
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _compose_task_prompt(task_rel: str, scope: str) -> str:
|
|
399
|
+
return f"Selected task: {task_rel}\n\n{scope}"
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def build_dispatch_prompt(
|
|
403
|
+
repo_root: Path,
|
|
404
|
+
task_dir: Path,
|
|
405
|
+
role: str,
|
|
406
|
+
*,
|
|
407
|
+
scope: str | None = None,
|
|
408
|
+
finish: bool = False,
|
|
409
|
+
max_chars: int | None = None,
|
|
410
|
+
original_prompt: str | None = None,
|
|
411
|
+
require_in_progress: bool = True,
|
|
412
|
+
) -> tuple[str | None, list[str], list[str]]:
|
|
413
|
+
"""
|
|
414
|
+
Build a full subagent dispatch prompt.
|
|
415
|
+
|
|
416
|
+
Returns (prompt, warnings, errors). prompt is None on hard failure.
|
|
417
|
+
When original_prompt is set (hook path), it is used as the task section as-is.
|
|
418
|
+
"""
|
|
419
|
+
warnings: list[str] = []
|
|
420
|
+
errors: list[str] = []
|
|
421
|
+
|
|
422
|
+
if role not in ("implement", "check", "research"):
|
|
423
|
+
errors.append(f"invalid role: {role!r}")
|
|
424
|
+
return None, warnings, errors
|
|
425
|
+
|
|
426
|
+
repo_root_str = str(repo_root)
|
|
427
|
+
if not task_dir.is_dir():
|
|
428
|
+
errors.append(f"task directory not found: {task_dir}")
|
|
429
|
+
return None, warnings, errors
|
|
430
|
+
|
|
431
|
+
task_rel = _repo_relative(task_dir, repo_root)
|
|
432
|
+
prd_path = task_dir / "prd.md"
|
|
433
|
+
if role in ("implement", "check"):
|
|
434
|
+
if not prd_path.is_file():
|
|
435
|
+
errors.append(f"prd.md missing under {task_rel}")
|
|
436
|
+
return None, warnings, errors
|
|
437
|
+
elif not prd_path.is_file():
|
|
438
|
+
warnings.append(f"prd.md missing under {task_rel}")
|
|
439
|
+
|
|
440
|
+
if role in ("implement", "check") and require_in_progress:
|
|
441
|
+
task_json_path = task_dir / FILE_TASK_JSON
|
|
442
|
+
task_data = read_json(task_json_path) if task_json_path.is_file() else None
|
|
443
|
+
status = (task_data or {}).get("status")
|
|
444
|
+
if status != "in_progress":
|
|
445
|
+
errors.append(f"task status must be in_progress, got {status!r}")
|
|
446
|
+
|
|
447
|
+
if original_prompt is not None:
|
|
448
|
+
task_prompt = original_prompt
|
|
449
|
+
else:
|
|
450
|
+
resolved_scope = _resolve_scope(role, scope, task_rel) # type: ignore[arg-type]
|
|
451
|
+
task_prompt = _compose_task_prompt(task_rel, resolved_scope)
|
|
452
|
+
|
|
453
|
+
if role == "implement":
|
|
454
|
+
context = get_implement_context(repo_root_str, task_rel)
|
|
455
|
+
context = _truncate_context(context, max_chars, warnings)
|
|
456
|
+
if not context:
|
|
457
|
+
errors.append("no implement context available")
|
|
458
|
+
return None, warnings, errors
|
|
459
|
+
prompt = build_implement_prompt(task_prompt, context)
|
|
460
|
+
elif role == "check":
|
|
461
|
+
context = get_finish_context(repo_root_str, task_rel) if finish else get_check_context(
|
|
462
|
+
repo_root_str, task_rel
|
|
463
|
+
)
|
|
464
|
+
context = _truncate_context(context, max_chars, warnings)
|
|
465
|
+
if not context:
|
|
466
|
+
errors.append("no check context available")
|
|
467
|
+
return None, warnings, errors
|
|
468
|
+
prompt = (
|
|
469
|
+
build_finish_prompt(task_prompt, context)
|
|
470
|
+
if finish
|
|
471
|
+
else build_check_prompt(task_prompt, context)
|
|
472
|
+
)
|
|
473
|
+
else:
|
|
474
|
+
context = get_research_context(repo_root_str, task_rel)
|
|
475
|
+
context = _truncate_context(context, max_chars, warnings)
|
|
476
|
+
if not context:
|
|
477
|
+
errors.append("no research context available")
|
|
478
|
+
return None, warnings, errors
|
|
479
|
+
prompt = build_research_prompt(task_prompt, context)
|
|
480
|
+
|
|
481
|
+
return prompt, warnings, errors
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def build_dispatch_prompt_for_agent(
|
|
485
|
+
repo_root: str,
|
|
486
|
+
task_dir: str,
|
|
487
|
+
subagent_type: str,
|
|
488
|
+
original_prompt: str,
|
|
489
|
+
*,
|
|
490
|
+
finish: bool = False,
|
|
491
|
+
max_chars: int | None = None,
|
|
492
|
+
) -> str | None:
|
|
493
|
+
"""Hook helper: map subagent type to role and build prompt."""
|
|
494
|
+
if subagent_type == AGENT_RESEARCH and not task_dir:
|
|
495
|
+
context = get_research_context(repo_root, None)
|
|
496
|
+
if max_chars is not None and len(context) > max_chars:
|
|
497
|
+
context = context[:max_chars] + "\n...[truncated]..."
|
|
498
|
+
if not context:
|
|
499
|
+
return None
|
|
500
|
+
return build_research_prompt(original_prompt, context)
|
|
501
|
+
|
|
502
|
+
role_map = {
|
|
503
|
+
AGENT_IMPLEMENT: "implement",
|
|
504
|
+
AGENT_CHECK: "check",
|
|
505
|
+
AGENT_RESEARCH: "research",
|
|
506
|
+
}
|
|
507
|
+
role = role_map.get(subagent_type)
|
|
508
|
+
if not role:
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
repo_path = Path(repo_root)
|
|
512
|
+
task_path = repo_path / task_dir.replace("\\", "/")
|
|
513
|
+
is_finish = finish or "[finish]" in original_prompt.lower()
|
|
514
|
+
prompt, _, errors = build_dispatch_prompt(
|
|
515
|
+
repo_path,
|
|
516
|
+
task_path,
|
|
517
|
+
role,
|
|
518
|
+
finish=is_finish if role == "check" else False,
|
|
519
|
+
max_chars=max_chars,
|
|
520
|
+
original_prompt=original_prompt,
|
|
521
|
+
require_in_progress=False,
|
|
522
|
+
)
|
|
523
|
+
if errors:
|
|
524
|
+
for item in errors:
|
|
525
|
+
print(f"[inject-subagent-context] {item}", file=sys.stderr)
|
|
526
|
+
return None
|
|
527
|
+
return prompt
|
|
@@ -1272,6 +1272,38 @@ def cmd_integrate_child(args: argparse.Namespace) -> int:
|
|
|
1272
1272
|
# Command: generate-child-prompt / parent-status / review-child
|
|
1273
1273
|
# =============================================================================
|
|
1274
1274
|
|
|
1275
|
+
def cmd_generate_dispatch_prompt(args: argparse.Namespace) -> int:
|
|
1276
|
+
"""Build a full Task dispatch prompt (Agent-facing CLI Layer 2)."""
|
|
1277
|
+
from .subagent_dispatch import build_dispatch_prompt
|
|
1278
|
+
|
|
1279
|
+
repo_root = get_repo_root()
|
|
1280
|
+
task_dir = resolve_task_dir(args.task_dir, repo_root)
|
|
1281
|
+
role = args.role
|
|
1282
|
+
scope = getattr(args, "scope", None)
|
|
1283
|
+
finish = bool(getattr(args, "finish", False))
|
|
1284
|
+
max_chars = getattr(args, "max_chars", None)
|
|
1285
|
+
|
|
1286
|
+
prompt, warnings, errors = build_dispatch_prompt(
|
|
1287
|
+
repo_root,
|
|
1288
|
+
task_dir,
|
|
1289
|
+
role,
|
|
1290
|
+
scope=scope,
|
|
1291
|
+
finish=finish,
|
|
1292
|
+
max_chars=max_chars,
|
|
1293
|
+
)
|
|
1294
|
+
for item in warnings:
|
|
1295
|
+
print(f"[generate-dispatch-prompt] WARN: {item}", file=sys.stderr)
|
|
1296
|
+
if errors:
|
|
1297
|
+
for item in errors:
|
|
1298
|
+
print(f"[generate-dispatch-prompt] Error: {item}", file=sys.stderr)
|
|
1299
|
+
return 1
|
|
1300
|
+
if prompt is None:
|
|
1301
|
+
print(colored("Error: could not build dispatch prompt", Colors.RED), file=sys.stderr)
|
|
1302
|
+
return 1
|
|
1303
|
+
print(prompt)
|
|
1304
|
+
return 0
|
|
1305
|
+
|
|
1306
|
+
|
|
1275
1307
|
def cmd_generate_child_prompt(args: argparse.Namespace) -> int:
|
|
1276
1308
|
"""Generate a child implementation prompt for parent orchestration."""
|
|
1277
1309
|
from .parent_orchestration import build_child_prompt
|