@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.
Files changed (49) hide show
  1. package/README.md +2 -0
  2. package/dist/migrations/manifests/0.1.1.json +9 -0
  3. package/dist/migrations/manifests/0.1.2.json +9 -0
  4. package/dist/templates/cursor/rules/retrieval-routing.mdc +9 -7
  5. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +2 -1
  6. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +16 -3
  7. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +11 -6
  8. package/dist/templates/shared-hooks/inject-subagent-context.py +31 -567
  9. package/dist/templates/trellis/index.d.ts +2 -0
  10. package/dist/templates/trellis/index.d.ts.map +1 -1
  11. package/dist/templates/trellis/index.js +4 -0
  12. package/dist/templates/trellis/index.js.map +1 -1
  13. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +78 -29
  14. package/dist/templates/trellis/scripts/common/cursor_retrieval_env.py +92 -0
  15. package/dist/templates/trellis/scripts/common/retrieval_adapter_metadata.py +100 -9
  16. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +76 -31
  17. package/dist/templates/trellis/scripts/common/retrieval_tool_classification.py +18 -3
  18. package/dist/templates/trellis/scripts/common/semantic_plan_gate.py +19 -0
  19. package/dist/templates/trellis/scripts/common/smart_search_evidence.py +5 -2
  20. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +527 -0
  21. package/dist/templates/trellis/scripts/common/task_store.py +32 -0
  22. package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +396 -0
  23. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +300 -0
  24. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +126 -0
  25. package/dist/templates/trellis/scripts/task.py +26 -0
  26. package/dist/templates/trellis/workflow.md +5 -22
  27. package/dist/utils/codebase-retrieval-router.d.ts +5 -0
  28. package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
  29. package/dist/utils/codebase-retrieval-router.js +48 -28
  30. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  31. package/dist/utils/cursor-retrieval-env.d.ts +28 -0
  32. package/dist/utils/cursor-retrieval-env.d.ts.map +1 -0
  33. package/dist/utils/cursor-retrieval-env.js +89 -0
  34. package/dist/utils/cursor-retrieval-env.js.map +1 -0
  35. package/dist/utils/project-capabilities.d.ts.map +1 -1
  36. package/dist/utils/project-capabilities.js +22 -15
  37. package/dist/utils/project-capabilities.js.map +1 -1
  38. package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -1
  39. package/dist/utils/retrieval-agent-instructions.js +37 -21
  40. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  41. package/dist/utils/retrieval-tool-classification.d.ts +2 -0
  42. package/dist/utils/retrieval-tool-classification.d.ts.map +1 -1
  43. package/dist/utils/retrieval-tool-classification.js +10 -2
  44. package/dist/utils/retrieval-tool-classification.js.map +1 -1
  45. package/dist/utils/semantic-plan-gate.d.ts +8 -0
  46. package/dist/utils/semantic-plan-gate.d.ts.map +1 -0
  47. package/dist/utils/semantic-plan-gate.js +42 -0
  48. package/dist/utils/semantic-plan-gate.js.map +1 -0
  49. 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