@blxzer/cursor-trellis 0.2.9 → 0.3.2

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 (234) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +85 -19
  3. package/README.zh-CN.md +60 -21
  4. package/bin/smart-search.js +1 -1
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +46 -18
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/commands/capability-smoke.d.ts +24 -0
  9. package/dist/commands/capability-smoke.d.ts.map +1 -0
  10. package/dist/commands/capability-smoke.js +83 -0
  11. package/dist/commands/capability-smoke.js.map +1 -0
  12. package/dist/commands/channel/agent-loader.d.ts +1 -1
  13. package/dist/commands/channel/agent-loader.js +4 -4
  14. package/dist/commands/channel/agent-loader.js.map +1 -1
  15. package/dist/commands/channel/guard.d.ts +2 -2
  16. package/dist/commands/channel/guard.js +3 -3
  17. package/dist/commands/channel/guard.js.map +1 -1
  18. package/dist/commands/channel/index.js +3 -3
  19. package/dist/commands/channel/index.js.map +1 -1
  20. package/dist/commands/channel/list.d.ts +1 -1
  21. package/dist/commands/channel/list.js +1 -1
  22. package/dist/commands/channel/rm.d.ts +1 -1
  23. package/dist/commands/channel/rm.js +1 -1
  24. package/dist/commands/channel/store/lock.d.ts +1 -1
  25. package/dist/commands/channel/store/lock.js +1 -1
  26. package/dist/commands/channel/store/paths.js +1 -1
  27. package/dist/commands/channel/store/paths.js.map +1 -1
  28. package/dist/commands/init.d.ts.map +1 -1
  29. package/dist/commands/init.js +197 -91
  30. package/dist/commands/init.js.map +1 -1
  31. package/dist/commands/uninstall.d.ts +4 -4
  32. package/dist/commands/uninstall.js +12 -12
  33. package/dist/commands/uninstall.js.map +1 -1
  34. package/dist/commands/update.d.ts +7 -2
  35. package/dist/commands/update.d.ts.map +1 -1
  36. package/dist/commands/update.js +161 -61
  37. package/dist/commands/update.js.map +1 -1
  38. package/dist/commands/upgrade.js +2 -2
  39. package/dist/commands/upgrade.js.map +1 -1
  40. package/dist/commands/workflow.d.ts +4 -4
  41. package/dist/commands/workflow.d.ts.map +1 -1
  42. package/dist/commands/workflow.js +10 -9
  43. package/dist/commands/workflow.js.map +1 -1
  44. package/dist/configurators/cursor.d.ts +2 -2
  45. package/dist/configurators/cursor.js +4 -4
  46. package/dist/configurators/cursor.js.map +1 -1
  47. package/dist/configurators/index.d.ts.map +1 -1
  48. package/dist/configurators/index.js +4 -4
  49. package/dist/configurators/index.js.map +1 -1
  50. package/dist/configurators/shared.d.ts +10 -10
  51. package/dist/configurators/shared.d.ts.map +1 -1
  52. package/dist/configurators/shared.js +32 -32
  53. package/dist/configurators/shared.js.map +1 -1
  54. package/dist/configurators/workflow.d.ts +6 -6
  55. package/dist/configurators/workflow.js +1 -1
  56. package/dist/constants/paths.d.ts +24 -24
  57. package/dist/constants/paths.d.ts.map +1 -1
  58. package/dist/constants/paths.js +16 -16
  59. package/dist/constants/paths.js.map +1 -1
  60. package/dist/migrations/manifests/0.2.10.json +374 -0
  61. package/dist/migrations/manifests/0.3.0.json +89 -0
  62. package/dist/migrations/manifests/0.3.1.json +32 -0
  63. package/dist/templates/common/bundled-skills/{trellis-cursor2plus-setup → cstl-cursor2plus-setup}/SKILL.md +15 -14
  64. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/SKILL.md +75 -75
  65. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/add-project-local-conventions.md +19 -19
  66. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-agents.md +8 -8
  67. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-context-loading.md +26 -26
  68. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-hooks.md +9 -9
  69. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-skills-or-commands.md +8 -8
  70. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-spec-structure.md +17 -17
  71. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-task-lifecycle.md +46 -46
  72. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/change-workflow.md +18 -18
  73. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/customize-local/overview.md +55 -55
  74. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/context-injection.md +20 -20
  75. package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/generated-files.md +83 -0
  76. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/overview.md +15 -15
  77. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/spec-system.md +12 -12
  78. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/task-system.md +34 -34
  79. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workflow.md +8 -8
  80. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/local-architecture/workspace-memory.md +19 -19
  81. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/agents.md +12 -12
  82. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/hooks-and-settings.md +7 -7
  83. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/overview.md +9 -9
  84. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/platform-map.md +7 -7
  85. package/dist/templates/common/bundled-skills/{trellis-meta → cstl-meta}/references/platform-files/skills-and-commands.md +19 -19
  86. package/dist/templates/common/bundled-skills/{trellis-micro-grill → cstl-micro-grill}/SKILL.md +62 -47
  87. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/SKILL.md +44 -44
  88. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/authoring-rules.md +97 -97
  89. package/dist/templates/common/bundled-skills/{trellis-skill-creator/references/trellis-skill-locations.md → cstl-skill-creator/references/cstl-skill-locations.md} +52 -52
  90. package/dist/templates/common/bundled-skills/{trellis-skill-creator → cstl-skill-creator}/references/review-checklist.md +47 -47
  91. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/SKILL.md +6 -6
  92. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/repository-analysis.md +1 -1
  93. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-task-planning.md +3 -3
  94. package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/spec-writing.md +1 -1
  95. package/dist/templates/common/commands/continue.md +24 -24
  96. package/dist/templates/common/commands/finish-work.md +5 -5
  97. package/dist/templates/common/commands/start.md +20 -20
  98. package/dist/templates/common/index.d.ts +1 -1
  99. package/dist/templates/common/index.d.ts.map +1 -1
  100. package/dist/templates/common/skills/before-dev.md +4 -4
  101. package/dist/templates/common/skills/brainstorm.md +188 -164
  102. package/dist/templates/common/skills/break-loop.md +3 -3
  103. package/dist/templates/common/skills/check.md +5 -5
  104. package/dist/templates/common/skills/update-spec.md +4 -4
  105. package/dist/templates/cursor/agents/{trellis-check.md → cstl-check.md} +14 -14
  106. package/dist/templates/cursor/agents/{trellis-implement.md → cstl-implement.md} +14 -14
  107. package/dist/templates/cursor/agents/{trellis-research.md → cstl-research.md} +11 -11
  108. package/dist/templates/cursor/commands/cursor2plus-setup.md +15 -15
  109. package/dist/templates/cursor/fixtures/expected-rules.d.ts +1 -1
  110. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  111. package/dist/templates/cursor/fixtures/expected-rules.js +2 -2
  112. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  113. package/dist/templates/cursor/rules/{trellis-subagent-dispatch.mdc → cstl-subagent-dispatch.mdc} +4 -4
  114. package/dist/templates/cursor/rules/{trellis-triage.mdc → cstl-triage.mdc} +3 -3
  115. package/dist/templates/cursor/rules/retrieval-routing.mdc +4 -4
  116. package/dist/templates/extract.d.ts +1 -1
  117. package/dist/templates/extract.js +1 -1
  118. package/dist/templates/markdown/agents.md +13 -13
  119. package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +1 -1
  120. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +634 -634
  121. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +5 -5
  122. package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -1
  123. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +393 -391
  124. package/dist/templates/markdown/spec/guides/durable-learning-decision-guide.md.txt +14 -14
  125. package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +5 -5
  126. package/dist/templates/markdown/spec/guides/index.md.txt +3 -2
  127. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +10 -10
  128. package/dist/templates/markdown/workspace-index.md +3 -3
  129. package/dist/templates/markdown/worktree.yaml.txt +1 -1
  130. package/dist/templates/shared-hooks/index.d.ts +2 -2
  131. package/dist/templates/shared-hooks/index.js +2 -2
  132. package/dist/templates/shared-hooks/inject-retrieval-plan.py +3 -3
  133. package/dist/templates/shared-hooks/inject-shell-session-context.py +7 -7
  134. package/dist/templates/shared-hooks/inject-subagent-context.py +4 -4
  135. package/dist/templates/shared-hooks/inject-workflow-state.py +37 -37
  136. package/dist/templates/shared-hooks/research-end-retrieval-pack.py +2 -2
  137. package/dist/templates/shared-hooks/session-start.py +16 -16
  138. package/dist/templates/trellis/config/execution-strategy-rules.json +1 -1
  139. package/dist/templates/trellis/config.yaml +5 -25
  140. package/dist/templates/trellis/index.d.ts +2 -2
  141. package/dist/templates/trellis/index.js +2 -2
  142. package/dist/templates/trellis/local/README.md +15 -13
  143. package/dist/templates/trellis/local/patch_wpelc8.py +7 -7
  144. package/dist/templates/trellis/local/subagent-models.json.example +1 -1
  145. package/dist/templates/trellis/local/trellis-task-models.json.example +3 -3
  146. package/dist/templates/trellis/local/trellis-task-models.json5.example +5 -5
  147. package/dist/templates/trellis/local/trellis_task_models_config.py +6 -6
  148. package/dist/templates/trellis/scripts/add_session.py +20 -20
  149. package/dist/templates/trellis/scripts/common/active_task.py +2 -2
  150. package/dist/templates/trellis/scripts/common/artifact_search.py +587 -587
  151. package/dist/templates/trellis/scripts/common/cli_adapter.py +3 -3
  152. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +6 -6
  153. package/dist/templates/trellis/scripts/common/config.py +3 -3
  154. package/dist/templates/trellis/scripts/common/developer.py +2 -2
  155. package/dist/templates/trellis/scripts/common/execution_strategy.py +1 -1
  156. package/dist/templates/trellis/scripts/common/packages_context.py +23 -23
  157. package/dist/templates/trellis/scripts/common/parent_orchestration.py +702 -702
  158. package/dist/templates/trellis/scripts/common/paths.py +107 -107
  159. package/dist/templates/trellis/scripts/common/project_file_stats.py +1 -1
  160. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +1 -1
  161. package/dist/templates/trellis/scripts/common/safe_commit.py +31 -31
  162. package/dist/templates/trellis/scripts/common/session_context.py +1159 -1159
  163. package/dist/templates/trellis/scripts/common/session_memory.py +360 -360
  164. package/dist/templates/trellis/scripts/common/smart_search_evidence.py +514 -514
  165. package/dist/templates/trellis/scripts/common/smart_search_resolve.py +1 -1
  166. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +7 -7
  167. package/dist/templates/trellis/scripts/common/task_context.py +1 -1
  168. package/dist/templates/trellis/scripts/common/task_dashboard.py +140 -140
  169. package/dist/templates/trellis/scripts/common/task_gates.py +2056 -2034
  170. package/dist/templates/trellis/scripts/common/task_store.py +1594 -1594
  171. package/dist/templates/trellis/scripts/common/task_utils.py +3 -3
  172. package/dist/templates/trellis/scripts/common/tasks.py +1 -1
  173. package/dist/templates/trellis/scripts/common/trellis_config.py +3 -3
  174. package/dist/templates/trellis/scripts/common/workflow_phase.py +1 -1
  175. package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +3 -3
  176. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +11 -11
  177. package/dist/templates/trellis/scripts/hooks/linear_sync.py +8 -8
  178. package/dist/templates/trellis/scripts/init_developer.py +2 -2
  179. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +4 -4
  180. package/dist/templates/trellis/scripts/route_codebase_retrieval.py +1 -1
  181. package/dist/templates/trellis/scripts/task.py +918 -918
  182. package/dist/templates/trellis/tasks/templates/release-readiness/implement.md +2 -2
  183. package/dist/templates/trellis/workflow.md +805 -805
  184. package/dist/types/ai-tools.d.ts +1 -1
  185. package/dist/types/ai-tools.d.ts.map +1 -1
  186. package/dist/types/ai-tools.js +1 -1
  187. package/dist/types/ai-tools.js.map +1 -1
  188. package/dist/types/migration.d.ts +1 -1
  189. package/dist/types/migration.d.ts.map +1 -1
  190. package/dist/utils/agents-md.d.ts +37 -0
  191. package/dist/utils/agents-md.d.ts.map +1 -0
  192. package/dist/utils/agents-md.js +82 -0
  193. package/dist/utils/agents-md.js.map +1 -0
  194. package/dist/utils/codebase-retrieval-router.js +5 -5
  195. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  196. package/dist/utils/manifest-prune.d.ts +2 -2
  197. package/dist/utils/manifest-prune.js +4 -4
  198. package/dist/utils/manifest-prune.js.map +1 -1
  199. package/dist/utils/mirror-check.d.ts +3 -1
  200. package/dist/utils/mirror-check.d.ts.map +1 -1
  201. package/dist/utils/mirror-check.js +22 -11
  202. package/dist/utils/mirror-check.js.map +1 -1
  203. package/dist/utils/project-capabilities.d.ts +14 -3
  204. package/dist/utils/project-capabilities.d.ts.map +1 -1
  205. package/dist/utils/project-capabilities.js +127 -17
  206. package/dist/utils/project-capabilities.js.map +1 -1
  207. package/dist/utils/readiness.d.ts +10 -0
  208. package/dist/utils/readiness.d.ts.map +1 -1
  209. package/dist/utils/readiness.js +6 -3
  210. package/dist/utils/readiness.js.map +1 -1
  211. package/dist/utils/retrieval-agent-instructions.js +1 -1
  212. package/dist/utils/retrieval-agent-instructions.js.map +1 -1
  213. package/dist/utils/retrieval-execution-telemetry.js +1 -1
  214. package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
  215. package/dist/utils/template-fetcher.js +1 -1
  216. package/dist/utils/template-fetcher.js.map +1 -1
  217. package/dist/utils/template-hash.d.ts +2 -2
  218. package/dist/utils/template-hash.d.ts.map +1 -1
  219. package/dist/utils/template-hash.js +12 -10
  220. package/dist/utils/template-hash.js.map +1 -1
  221. package/dist/utils/workflow-dir.d.ts +13 -0
  222. package/dist/utils/workflow-dir.d.ts.map +1 -0
  223. package/dist/utils/workflow-dir.js +35 -0
  224. package/dist/utils/workflow-dir.js.map +1 -0
  225. package/dist/utils/workflow-ownership.d.ts +36 -0
  226. package/dist/utils/workflow-ownership.d.ts.map +1 -0
  227. package/dist/utils/workflow-ownership.js +137 -0
  228. package/dist/utils/workflow-ownership.js.map +1 -0
  229. package/dist/utils/workflow-resolver.d.ts +1 -1
  230. package/dist/utils/workflow-resolver.js +1 -1
  231. package/package.json +3 -4
  232. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +0 -83
  233. /package/bin/{trellis.js → cstl.js} +0 -0
  234. /package/dist/templates/common/bundled-skills/{trellis-spec-bootstrap → cstl-spec-bootstrap}/references/mcp-setup.md +0 -0
@@ -1,703 +1,703 @@
1
- #!/usr/bin/env python3
2
- """
3
- Parent reviewer orchestration: child prompt generation and review workflows.
4
-
5
- Inline mode prints prompts and review summaries for manual handoff to external agents.
6
- Subagent-capable platforms may use the same output with platform-specific dispatch.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import re
12
- from datetime import datetime, timezone
13
- from pathlib import Path
14
-
15
- from .paths import get_repo_root
16
- from .task_gates import (
17
- build_spec_update_scaffold,
18
- durable_learning_decision_status,
19
- task_closeout_profile,
20
- )
21
- from .task_map import (
22
- PARENT_TERMINAL_STATES,
23
- get_child_entry,
24
- load_task_map,
25
- validate_parent_child_integration,
26
- )
27
-
28
- _ARTIFACT_NAMES = ("prd.md", "design.md", "implement.md", "verify.md", "handoff.md")
29
- _GLOBAL_CHILD_CONSTRAINTS = """General constraints:
30
- - Read the parent task artifacts first: prd.md, design.md, implement.md, task-map.md, and child-prompts.md when present.
31
- - Read your own child task artifacts: prd.md, design.md, implement.md.
32
- - Stay inside your child scope and declared touch areas.
33
- - Do not publish packages, push remotes, create tags, edit credentials, or change global/MCP configuration.
34
- - Preserve explicit user approval for destructive, remote, credential-bearing, publish, push, tag, and global configuration actions.
35
- - Write `verify.md` and `handoff.md` in your child task directory.
36
- - Handoff must include changed files, validation commands/results, residual risks, and parent integration notes.
37
- - If you find a needed contract change, stop and report it to the parent instead of silently redefining shared state/gate semantics.
38
-
39
- Validation baseline:
40
- - Run focused tests for touched behavior.
41
- - Run typecheck when touching TypeScript.
42
- - Run ESLint for changed TypeScript test/source files when applicable.
43
- - Run Python compile checks when touching Python scripts/templates.
44
- - Run `python ./.trellis/scripts/task.py validate <your-child-task>` before handoff."""
45
-
46
- _VALIDATION_BASELINE = """Validation baseline:
47
- - Run focused tests for touched behavior.
48
- - Run typecheck when touching TypeScript.
49
- - Run ESLint for changed TypeScript test/source files when applicable.
50
- - Run Python compile checks when touching Python scripts/templates.
51
- - Run `python ./.trellis/scripts/task.py validate <child-dir>` before handoff."""
52
-
53
-
54
- def _utc_now() -> str:
55
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
56
-
57
-
58
- def _read_optional(path: Path, max_chars: int = 12000) -> str | None:
59
- if not path.is_file():
60
- return None
61
- try:
62
- text = path.read_text(encoding="utf-8")
63
- except OSError:
64
- return None
65
- if len(text) > max_chars:
66
- return text[:max_chars] + "\n\n...[truncated for prompt]...\n"
67
- return text
68
-
69
-
70
- def _repo_rel(path: Path, repo_root: Path) -> str:
71
- try:
72
- return path.relative_to(repo_root).as_posix()
73
- except ValueError:
74
- return str(path)
75
-
76
-
77
- def _unmet_dependencies(child_entry: dict, children_by_id: dict[str, dict]) -> list[str]:
78
- deps = child_entry.get("depends_on") or []
79
- if not isinstance(deps, list):
80
- return []
81
- blocked: list[str] = []
82
- for dep_id in deps:
83
- if not isinstance(dep_id, str):
84
- continue
85
- dep = children_by_id.get(dep_id)
86
- if dep is None:
87
- blocked.append(f"{dep_id} (missing from task-map)")
88
- continue
89
- state = dep.get("state")
90
- if state not in PARENT_TERMINAL_STATES:
91
- blocked.append(f"{dep_id} (state={state!r}, need integrated or cancelled)")
92
- return blocked
93
-
94
-
95
- def _child_prompt_section(
96
- parent_dir: Path,
97
- child_dir: Path,
98
- child_entry: dict,
99
- repo_root: Path,
100
- *,
101
- include_artifacts: bool,
102
- ) -> str:
103
- child_name = child_dir.name
104
- parent_rel = _repo_rel(parent_dir, repo_root)
105
- child_rel = _repo_rel(child_dir, repo_root)
106
- state = child_entry.get("state", "open")
107
- depends_on = child_entry.get("depends_on") or []
108
- touches = child_entry.get("touches") or []
109
- isolation = child_entry.get("isolation") or "git-worktree"
110
-
111
- lines = [
112
- f"## Child: `{child_name}`",
113
- "",
114
- f"- Parent task: `{parent_rel}`",
115
- f"- Child task: `{child_rel}`",
116
- f"- Integration state: `{state}`",
117
- f"- Isolation: `{isolation}`",
118
- ]
119
- if depends_on:
120
- lines.append(f"- Depends on: {', '.join(f'`{d}`' for d in depends_on)}")
121
- else:
122
- lines.append("- Depends on: (none)")
123
- if touches:
124
- lines.append("- Touch scope:")
125
- for touch in touches:
126
- lines.append(f" - `{touch}`")
127
- else:
128
- lines.append("- Touch scope: (not declared in task-map; stay within child prd/design/implement)")
129
-
130
- if include_artifacts:
131
- lines.append("")
132
- lines.append("### Child artifacts (context)")
133
- for name in _ARTIFACT_NAMES:
134
- content = _read_optional(child_dir / name, max_chars=8000)
135
- if content:
136
- lines.append(f"#### {name}")
137
- lines.append("")
138
- lines.append(content.strip())
139
- lines.append("")
140
-
141
- lines.append("### Suggested child worker commands")
142
- lines.append("")
143
- lines.append("```bash")
144
- lines.append(f"python ./.trellis/scripts/task.py select {child_rel}")
145
- lines.append(
146
- f"python ./.trellis/scripts/task.py set-child-state {parent_rel} {child_rel} working --evidence implement.md"
147
- )
148
- lines.append("# ... implement ...")
149
- lines.append(
150
- f"python ./.trellis/scripts/task.py set-child-state {parent_rel} {child_rel} review --evidence verify.md"
151
- )
152
- lines.append("```")
153
- lines.append("")
154
- return "\n".join(lines)
155
-
156
-
157
- def build_child_prompt(
158
- parent_dir: Path,
159
- child_dir: Path,
160
- *,
161
- include_artifacts: bool = False,
162
- mode: str = "inline",
163
- ) -> tuple[str | None, list[str]]:
164
- """Build an implementation prompt for a linked child task."""
165
- errors: list[str] = []
166
- repo_root = get_repo_root()
167
- child_name = child_dir.name
168
-
169
- parent_json = parent_dir / "task.json"
170
- if not parent_json.is_file():
171
- return None, [f"parent task.json missing: {parent_dir}"]
172
-
173
- data, body = load_task_map(parent_dir)
174
- if data is None:
175
- return None, ["parent task-map.md missing or invalid"]
176
-
177
- child_entry = get_child_entry(data, child_name)
178
- if child_entry is None:
179
- return None, [f"child not in parent task-map: {child_name}"]
180
-
181
- children_by_id = {
182
- item.get("id"): item
183
- for item in data.get("children", [])
184
- if isinstance(item, dict) and item.get("id")
185
- }
186
- blocked = _unmet_dependencies(child_entry, children_by_id)
187
-
188
- parent_rel = _repo_rel(parent_dir, repo_root)
189
- child_rel = _repo_rel(child_dir, repo_root)
190
- repo_rel = _repo_rel(repo_root, repo_root)
191
-
192
- child_prd = _read_optional(child_dir / "prd.md", 4000)
193
- child_design = _read_optional(child_dir / "design.md", 4000)
194
- child_implement = _read_optional(child_dir / "implement.md", 4000)
195
- parent_child_prompts = _read_optional(parent_dir / "child-prompts.md", 16000)
196
-
197
- goal = ""
198
- if child_prd:
199
- goal_match = re.search(r"(?ms)^##\s*Goal\s*\n+(.*?)(?:\n##|\Z)", child_prd)
200
- if goal_match:
201
- goal = goal_match.group(1).strip()
202
-
203
- lines = [
204
- f"You are implementing child task `{child_name}`.",
205
- "",
206
- "Workspace:",
207
- f"- Root: {repo_rel}",
208
- f"- Parent task: {parent_rel}",
209
- f"- Child task: {child_rel}",
210
- "",
211
- ]
212
-
213
- if blocked:
214
- lines.extend(
215
- [
216
- "Start condition:",
217
- "- **BLOCKED** — unmet dependencies:",
218
- ]
219
- )
220
- for item in blocked:
221
- lines.append(f" - {item}")
222
- lines.append("- Do not start implementation until dependencies reach `integrated` or `cancelled`.")
223
- lines.append("")
224
- elif depends_on := child_entry.get("depends_on"):
225
- lines.extend(
226
- [
227
- "Start condition:",
228
- f"- Dependencies satisfied: {', '.join(f'`{d}`' for d in depends_on)}.",
229
- "",
230
- ]
231
- )
232
-
233
- lines.extend(
234
- [
235
- "Read first:",
236
- f"- Parent prd.md/design.md/implement.md/task-map.md",
237
- f"- Child prd.md/design.md/implement.md under `{child_rel}`",
238
- ]
239
- )
240
- if parent_child_prompts and child_name in parent_child_prompts:
241
- lines.append(f"- Parent child-prompts.md section for `{child_name}` when present")
242
-
243
- if goal:
244
- lines.extend(["", "Goal:", goal, ""])
245
-
246
- lines.append(_GLOBAL_CHILD_CONSTRAINTS)
247
- lines.append("")
248
-
249
- if child_implement:
250
- lines.extend(["Implementation plan (from child implement.md):", child_implement.strip()[:6000], ""])
251
- elif child_design:
252
- lines.extend(["Design notes (from child design.md):", child_design.strip()[:4000], ""])
253
-
254
- touches = child_entry.get("touches") or []
255
- if touches:
256
- lines.append("Declared touch scope (parent task-map):")
257
- for touch in touches:
258
- lines.append(f"- `{touch}`")
259
- lines.append("")
260
-
261
- if mode == "subagent":
262
- topo = data.get("execution_topology", "serial")
263
- merge_lim = data.get("merge_limit", 1)
264
- lines.extend(
265
- [
266
- f"Selected task: {child_rel}",
267
- "",
268
- "Delivery mode: subagent (Cursor)",
269
- "- **Default:** Parent session dispatches **Task** with `subagent_type=trellis-implement` and this prompt as the task description (writable sub-agent). Model under Cursor++ BYOK comes from `.trellis/local/cursor2plus/` routing (see `cursor-subagent-policy.md` Method 2.5/2.6).",
270
- "- **Exception:** If parent `child-prompts.md` or the user names this child for a **separate writable Agent chat**, open a new Agent session, pick the model manually, paste this prompt — do not use Task from Parent.",
271
- "- Parent retains `review-child` / `integrate-child`. Child must not nest further `trellis-research` / `trellis-implement` / `trellis-check` Task dispatches.",
272
- f"- Parent orchestration: `execution_topology={topo}`, `merge_limit={merge_lim}`.",
273
- "- When `isolation: git-worktree`, prepare worktree from a **git repo root** (e.g. `Trellis/`) before large edits.",
274
- "",
275
- ]
276
- )
277
- else:
278
- lines.extend(
279
- [
280
- "Delivery mode: inline",
281
- "- Hand this prompt to the child worker agent or session manually.",
282
- "- Parent session retains integration authority (`integrate-child`, `review-child`).",
283
- "",
284
- ]
285
- )
286
-
287
- lines.append(_child_prompt_section(parent_dir, child_dir, child_entry, repo_root, include_artifacts=include_artifacts))
288
-
289
- return "\n".join(lines), errors
290
-
291
-
292
- def build_parent_status(parent_dir: Path) -> str:
293
- """Render a parent task-map status summary for the reviewer."""
294
- repo_root = get_repo_root()
295
- parent_rel = _repo_rel(parent_dir, repo_root)
296
- data, _ = load_task_map(parent_dir)
297
- if data is None:
298
- return f"Parent `{parent_rel}`: task-map.md missing or invalid."
299
-
300
- lines = [
301
- f"# Parent orchestration status — `{parent_dir.name}`",
302
- "",
303
- f"- Path: `{parent_rel}`",
304
- f"- contract_epoch: {data.get('contract_epoch', '?')}",
305
- f"- execution_topology: {data.get('execution_topology', '?')}",
306
- f"- merge_limit: {data.get('merge_limit', '?')}",
307
- "",
308
- "## Children",
309
- "",
310
- ]
311
-
312
- children = data.get("children") or []
313
- if not children:
314
- lines.append("(no children in task-map)")
315
- return "\n".join(lines)
316
-
317
- for child in children:
318
- if not isinstance(child, dict):
319
- continue
320
- cid = child.get("id", "?")
321
- state = child.get("state", "?")
322
- evidence = child.get("evidence")
323
- ref = child.get("ref")
324
- deps = child.get("depends_on") or []
325
- touches = child.get("touches") or []
326
- isolation = child.get("isolation")
327
- branch = child.get("branch")
328
- worktree_path = child.get("worktree_path")
329
- lines.append(f"### `{cid}` — `{state}`")
330
- if deps:
331
- lines.append(f"- depends_on: {', '.join(deps)}")
332
- if touches:
333
- lines.append(f"- touches: {', '.join(touches)}")
334
- if isolation:
335
- lines.append(f"- isolation: {isolation}")
336
- if branch:
337
- lines.append(f"- branch: {branch}")
338
- if worktree_path:
339
- lines.append(f"- worktree_path: {worktree_path}")
340
- if evidence:
341
- lines.append(f"- evidence: {evidence}")
342
- if ref:
343
- lines.append(f"- ref: {ref}")
344
- child_dir = parent_dir.parent / cid
345
- for artifact in ("verify.md", "handoff.md"):
346
- flag = "yes" if (child_dir / artifact).is_file() else "missing"
347
- lines.append(f"- {artifact}: {flag}")
348
- lines.append("")
349
-
350
- queue = data.get("integration_queue") or []
351
- lines.append(f"integration_queue: {queue}")
352
- lines.append("")
353
- lines.append("## Suggested parent commands")
354
- lines.append("")
355
- lines.append("```bash")
356
- lines.append(f"python ./.trellis/scripts/task.py parent-status {parent_rel}")
357
- lines.append(f"python ./.trellis/scripts/task.py generate-child-prompt {parent_rel} <child> --mode subagent")
358
- lines.append(f"python ./.trellis/scripts/task.py generate-child-prompt {parent_rel} <child> --mode inline")
359
- lines.append(f"python ./.trellis/scripts/task.py review-child {parent_rel} <child> --check")
360
- lines.append(f"python ./.trellis/scripts/task.py review-child {parent_rel} <child> --decision accept --ref <ref>")
361
- lines.append("```")
362
- return "\n".join(lines)
363
-
364
-
365
- def _summarize_handoff(child_dir: Path) -> dict[str, str | list[str] | bool]:
366
- verify = _read_optional(child_dir / "verify.md", 6000) or ""
367
- handoff = _read_optional(child_dir / "handoff.md", 12000) or ""
368
- has_verify = (child_dir / "verify.md").is_file()
369
- has_handoff = (child_dir / "handoff.md").is_file()
370
-
371
- validation_ok = bool(
372
- re.search(r"(?i)validation\s+(evidence|commands|results)", verify)
373
- or re.search(r"(?i)validation\s*:", verify)
374
- )
375
- acceptance_ok = bool(re.search(r"(?i)acceptance\s+evidence|accepted\s+by\s+user", verify))
376
- learning = durable_learning_decision_status(verify) if verify else {
377
- "no_durable_learning": False,
378
- "spec_update": False,
379
- "learning_artifact": False,
380
- "any": False,
381
- }
382
-
383
- changed_files: list[str] = []
384
- for line in handoff.splitlines():
385
- if "|" in line and not line.strip().startswith("| ---"):
386
- cells = [c.strip() for c in line.split("|") if c.strip()]
387
- if len(cells) >= 2 and cells[0].lower() not in ("path", "file"):
388
- changed_files.append(cells[0])
389
-
390
- return {
391
- "has_verify": has_verify,
392
- "has_handoff": has_handoff,
393
- "validation_section": validation_ok,
394
- "acceptance_section": acceptance_ok,
395
- "learning_decision": learning,
396
- "verify_excerpt": verify.strip()[:2000],
397
- "handoff_excerpt": handoff.strip()[:4000],
398
- "changed_files": changed_files[:20],
399
- }
400
-
401
-
402
- def _learning_decision_review_lines(
403
- parent_dir: Path,
404
- child_dir: Path,
405
- child_data: dict,
406
- summary: dict,
407
- *,
408
- decision: str | None,
409
- reason: str | None,
410
- ) -> list[str]:
411
- """Prompt parent/child to record durable learning before child archive."""
412
- learning = summary.get("learning_decision") or {}
413
- if learning.get("any"):
414
- return []
415
-
416
- repo_root = get_repo_root()
417
- child_rel = _repo_rel(child_dir, repo_root)
418
- lines = [
419
- "## Durable learning decision (required before child archive)",
420
- "",
421
- "Child verify.md has no grep-friendly durable-learning line yet. Pick one outcome:",
422
- "",
423
- "- `Durable learning decision: no durable learning` — routine scope, no reusable contract.",
424
- "- `Spec update evidence: .trellis/spec/<path>` — after `/trellis:update-spec` with reviewer confirmation.",
425
- f"- `Learning artifact: {child_rel}/handoff.md` — handoff already captures the insight.",
426
- "",
427
- f"Helper: `python ./.trellis/scripts/task.py prepare-archive-evidence {child_rel}`",
428
- "",
429
- ]
430
-
431
- trigger: str | None = None
432
- if decision == "changes":
433
- trigger = f"Parent review requested changes ({reason or 'see review notes'})"
434
- elif decision in ("accept", "integrate-through"):
435
- trigger = "Parent accepted child handoff — confirm whether workflow/contracts should be captured in spec"
436
-
437
- if trigger or decision == "changes":
438
- lines.append(build_spec_update_scaffold(repo_root, child_dir, child_data, trigger=trigger))
439
- return lines
440
-
441
-
442
- def build_review_report(
443
- parent_dir: Path,
444
- child_dir: Path,
445
- parent_data: dict,
446
- child_data: dict,
447
- *,
448
- decision: str | None,
449
- ref: str | None,
450
- reason: str | None,
451
- notes: str | None,
452
- ) -> tuple[str, list[str], dict]:
453
- """Build review summary and validate integration transitions for a decision."""
454
- child_name = child_dir.name
455
- summary = _summarize_handoff(child_dir)
456
- errors: list[str] = []
457
- actions: dict = {"integrate": None, "gates": []}
458
-
459
- data, _ = load_task_map(parent_dir)
460
- child_entry = get_child_entry(data, child_name) if data else None
461
- current_state = child_entry.get("state") if child_entry else None
462
-
463
- lines = [
464
- f"# Parent review — `{child_name}`",
465
- "",
466
- f"- Timestamp: {_utc_now()}",
467
- f"- Current integration state: `{current_state}`",
468
- f"- Decision: `{decision or 'check-only'}`",
469
- "",
470
- "## Handoff artifacts",
471
- "",
472
- f"- verify.md: {'present' if summary['has_verify'] else '**missing**'}",
473
- f"- handoff.md: {'present' if summary['has_handoff'] else '**missing**'}",
474
- f"- verify validation section signal: {summary['validation_section']}",
475
- f"- verify acceptance section signal: {summary['acceptance_section']}",
476
- f"- durable learning decision signal: {summary['learning_decision'].get('any')}",
477
- "",
478
- ]
479
- learning_lines = _learning_decision_review_lines(
480
- parent_dir,
481
- child_dir,
482
- child_data,
483
- summary,
484
- decision=decision,
485
- reason=reason,
486
- )
487
- if learning_lines:
488
- lines.extend(learning_lines)
489
-
490
- if summary["changed_files"]:
491
- lines.append("Changed files (from handoff table):")
492
- for path in summary["changed_files"]:
493
- lines.append(f"- `{path}`")
494
- lines.append("")
495
-
496
- if notes:
497
- lines.extend(["## Parent notes", "", notes.strip(), ""])
498
-
499
- if decision in ("accept", "changes", "cancel"):
500
- if decision == "accept":
501
- target = "accepted"
502
- if current_state != "review":
503
- errors.append(f"accept requires child state 'review', got {current_state!r}")
504
- errors.extend(
505
- validate_parent_child_integration(
506
- parent_dir,
507
- parent_data,
508
- child_dir,
509
- child_data,
510
- target,
511
- "handoff.md",
512
- ref,
513
- reason,
514
- )
515
- )
516
- actions["integrate"] = {
517
- "state": target,
518
- "evidence": "handoff.md",
519
- "ref": ref,
520
- }
521
- actions["gates"].append(
522
- {
523
- "task": "parent",
524
- "transition": "parent-accepted",
525
- "optional": True,
526
- "hint": (
527
- f"Optional audit: python ./.trellis/scripts/task.py record-gate {parent_dir.name} "
528
- f"--transition parent-accepted --gate code-review --result PASS "
529
- f"--reviewer parent --evidence review-{child_name}.md"
530
- ),
531
- }
532
- )
533
- child_profile = task_closeout_profile(child_dir, child_data)
534
- if child_profile == "full":
535
- actions["gates"].append(
536
- {
537
- "task": "child",
538
- "transition": "child-review",
539
- "optional": False,
540
- "hint": (
541
- f"Required before accept: python ./.trellis/scripts/task.py record-gate {child_name} "
542
- f"--transition child-review --gate code-review --result PASS "
543
- f"--reviewer parent --evidence verify.md"
544
- ),
545
- }
546
- )
547
- elif child_profile == "lite":
548
- actions["gates"].append(
549
- {
550
- "task": "child",
551
- "transition": "child-review",
552
- "optional": True,
553
- "hint": "Lite child: no child-review gate chain required.",
554
- }
555
- )
556
- elif decision == "changes":
557
- target = "changes"
558
- errors.extend(
559
- validate_parent_child_integration(
560
- parent_dir,
561
- parent_data,
562
- child_dir,
563
- child_data,
564
- target,
565
- "handoff.md",
566
- ref,
567
- reason,
568
- )
569
- )
570
- if not reason:
571
- errors.append("changes requires --reason")
572
- actions["integrate"] = {
573
- "state": target,
574
- "evidence": "handoff.md",
575
- "ref": ref,
576
- "reason": reason,
577
- }
578
- elif decision == "cancel":
579
- target = "cancelled"
580
- errors.extend(
581
- validate_parent_child_integration(
582
- parent_dir,
583
- parent_data,
584
- child_dir,
585
- child_data,
586
- target,
587
- "handoff.md",
588
- ref,
589
- reason,
590
- )
591
- )
592
- if not reason:
593
- errors.append("cancel requires --reason")
594
- actions["integrate"] = {
595
- "state": target,
596
- "evidence": "handoff.md",
597
- "reason": reason,
598
- }
599
- elif decision == "integrate-through":
600
- sequence = [
601
- ("accepted", "handoff.md"),
602
- ("integrating", "task-map.md"),
603
- ("integrated", "task-map.md"),
604
- ]
605
- sim_state = current_state
606
- for target, evidence in sequence:
607
- if target == "accepted" and sim_state != "review":
608
- errors.append(f"integrate-through requires child state 'review', got {sim_state!r}")
609
- break
610
- if target == "integrating" and sim_state != "accepted":
611
- errors.append(f"integrating requires 'accepted', simulated from {sim_state!r}")
612
- break
613
- if target == "integrated" and sim_state != "integrating":
614
- errors.append(f"integrated requires 'integrating', simulated from {sim_state!r}")
615
- break
616
- step_errors = validate_parent_child_integration(
617
- parent_dir,
618
- parent_data,
619
- child_dir,
620
- child_data,
621
- target,
622
- evidence,
623
- ref,
624
- reason,
625
- current_state_override=sim_state,
626
- )
627
- errors.extend(step_errors)
628
- if step_errors:
629
- break
630
- sim_state = target
631
- if not errors:
632
- actions["integrate_sequence"] = [
633
- {"state": "accepted", "evidence": "handoff.md", "ref": ref},
634
- {"state": "integrating", "evidence": "task-map.md", "ref": ref},
635
- {"state": "integrated", "evidence": "task-map.md", "ref": ref},
636
- ]
637
- actions["gates"].append(
638
- {
639
- "task": "parent",
640
- "transition": "parent-integrated",
641
- "optional": False,
642
- "hint": (
643
- f"Required before parent archive: python ./.trellis/scripts/task.py record-gate {parent_dir.name} "
644
- f"--transition parent-integrated --gate integration-review --result PASS "
645
- f"--reviewer parent --evidence task-map.md"
646
- ),
647
- }
648
- )
649
-
650
- lines.extend(
651
- [
652
- "## verify.md excerpt",
653
- "",
654
- (summary["verify_excerpt"] or "(empty)"),
655
- "",
656
- "## handoff.md excerpt",
657
- "",
658
- (summary["handoff_excerpt"] or "(empty)"),
659
- "",
660
- ]
661
- )
662
-
663
- if decision and not errors:
664
- lines.append("## Integration plan")
665
- lines.append("")
666
- if actions.get("integrate_sequence"):
667
- for step in actions["integrate_sequence"]:
668
- lines.append(
669
- f"- integrate-child → `{step['state']}` (evidence={step['evidence']}, ref={step.get('ref')})"
670
- )
671
- elif actions.get("integrate"):
672
- step = actions["integrate"]
673
- lines.append(
674
- f"- integrate-child → `{step['state']}` (evidence={step['evidence']}, ref={step.get('ref')})"
675
- )
676
- for gate in actions.get("gates", []):
677
- if gate.get("optional"):
678
- lines.append(f"- Optional gate ({gate['transition']}): {gate['hint']}")
679
- lines.append("")
680
-
681
- report = "\n".join(lines)
682
- return report, errors, actions
683
-
684
-
685
- def append_parent_review_notes(parent_dir: Path, child_name: str, report: str) -> None:
686
- """Append a parent review section to parent verify.md."""
687
- verify_path = parent_dir / "verify.md"
688
- header = f"\n\n## Parent review — `{child_name}` ({_utc_now()})\n\n"
689
- if verify_path.is_file():
690
- existing = verify_path.read_text(encoding="utf-8")
691
- verify_path.write_text(existing.rstrip() + header + report.strip() + "\n", encoding="utf-8")
692
- else:
693
- verify_path.write_text(
694
- "# Verification Evidence\n\n" + header + report.strip() + "\n",
695
- encoding="utf-8",
696
- )
697
-
698
-
699
- def write_review_artifact(parent_dir: Path, child_name: str, report: str) -> Path:
700
- """Write a standalone review artifact under the parent task directory."""
701
- path = parent_dir / f"review-{child_name}.md"
702
- path.write_text(report.strip() + "\n", encoding="utf-8")
1
+ #!/usr/bin/env python3
2
+ """
3
+ Parent reviewer orchestration: child prompt generation and review workflows.
4
+
5
+ Inline mode prints prompts and review summaries for manual handoff to external agents.
6
+ Subagent-capable platforms may use the same output with platform-specific dispatch.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ from .paths import get_repo_root
16
+ from .task_gates import (
17
+ build_spec_update_scaffold,
18
+ durable_learning_decision_status,
19
+ task_closeout_profile,
20
+ )
21
+ from .task_map import (
22
+ PARENT_TERMINAL_STATES,
23
+ get_child_entry,
24
+ load_task_map,
25
+ validate_parent_child_integration,
26
+ )
27
+
28
+ _ARTIFACT_NAMES = ("prd.md", "design.md", "implement.md", "verify.md", "handoff.md")
29
+ _GLOBAL_CHILD_CONSTRAINTS = """General constraints:
30
+ - Read the parent task artifacts first: prd.md, design.md, implement.md, task-map.md, and child-prompts.md when present.
31
+ - Read your own child task artifacts: prd.md, design.md, implement.md.
32
+ - Stay inside your child scope and declared touch areas.
33
+ - Do not publish packages, push remotes, create tags, edit credentials, or change global/MCP configuration.
34
+ - Preserve explicit user approval for destructive, remote, credential-bearing, publish, push, tag, and global configuration actions.
35
+ - Write `verify.md` and `handoff.md` in your child task directory.
36
+ - Handoff must include changed files, validation commands/results, residual risks, and parent integration notes.
37
+ - If you find a needed contract change, stop and report it to the parent instead of silently redefining shared state/gate semantics.
38
+
39
+ Validation baseline:
40
+ - Run focused tests for touched behavior.
41
+ - Run typecheck when touching TypeScript.
42
+ - Run ESLint for changed TypeScript test/source files when applicable.
43
+ - Run Python compile checks when touching Python scripts/templates.
44
+ - Run `python ./.cstl/scripts/task.py validate <your-child-task>` before handoff."""
45
+
46
+ _VALIDATION_BASELINE = """Validation baseline:
47
+ - Run focused tests for touched behavior.
48
+ - Run typecheck when touching TypeScript.
49
+ - Run ESLint for changed TypeScript test/source files when applicable.
50
+ - Run Python compile checks when touching Python scripts/templates.
51
+ - Run `python ./.cstl/scripts/task.py validate <child-dir>` before handoff."""
52
+
53
+
54
+ def _utc_now() -> str:
55
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
56
+
57
+
58
+ def _read_optional(path: Path, max_chars: int = 12000) -> str | None:
59
+ if not path.is_file():
60
+ return None
61
+ try:
62
+ text = path.read_text(encoding="utf-8")
63
+ except OSError:
64
+ return None
65
+ if len(text) > max_chars:
66
+ return text[:max_chars] + "\n\n...[truncated for prompt]...\n"
67
+ return text
68
+
69
+
70
+ def _repo_rel(path: Path, repo_root: Path) -> str:
71
+ try:
72
+ return path.relative_to(repo_root).as_posix()
73
+ except ValueError:
74
+ return str(path)
75
+
76
+
77
+ def _unmet_dependencies(child_entry: dict, children_by_id: dict[str, dict]) -> list[str]:
78
+ deps = child_entry.get("depends_on") or []
79
+ if not isinstance(deps, list):
80
+ return []
81
+ blocked: list[str] = []
82
+ for dep_id in deps:
83
+ if not isinstance(dep_id, str):
84
+ continue
85
+ dep = children_by_id.get(dep_id)
86
+ if dep is None:
87
+ blocked.append(f"{dep_id} (missing from task-map)")
88
+ continue
89
+ state = dep.get("state")
90
+ if state not in PARENT_TERMINAL_STATES:
91
+ blocked.append(f"{dep_id} (state={state!r}, need integrated or cancelled)")
92
+ return blocked
93
+
94
+
95
+ def _child_prompt_section(
96
+ parent_dir: Path,
97
+ child_dir: Path,
98
+ child_entry: dict,
99
+ repo_root: Path,
100
+ *,
101
+ include_artifacts: bool,
102
+ ) -> str:
103
+ child_name = child_dir.name
104
+ parent_rel = _repo_rel(parent_dir, repo_root)
105
+ child_rel = _repo_rel(child_dir, repo_root)
106
+ state = child_entry.get("state", "open")
107
+ depends_on = child_entry.get("depends_on") or []
108
+ touches = child_entry.get("touches") or []
109
+ isolation = child_entry.get("isolation") or "git-worktree"
110
+
111
+ lines = [
112
+ f"## Child: `{child_name}`",
113
+ "",
114
+ f"- Parent task: `{parent_rel}`",
115
+ f"- Child task: `{child_rel}`",
116
+ f"- Integration state: `{state}`",
117
+ f"- Isolation: `{isolation}`",
118
+ ]
119
+ if depends_on:
120
+ lines.append(f"- Depends on: {', '.join(f'`{d}`' for d in depends_on)}")
121
+ else:
122
+ lines.append("- Depends on: (none)")
123
+ if touches:
124
+ lines.append("- Touch scope:")
125
+ for touch in touches:
126
+ lines.append(f" - `{touch}`")
127
+ else:
128
+ lines.append("- Touch scope: (not declared in task-map; stay within child prd/design/implement)")
129
+
130
+ if include_artifacts:
131
+ lines.append("")
132
+ lines.append("### Child artifacts (context)")
133
+ for name in _ARTIFACT_NAMES:
134
+ content = _read_optional(child_dir / name, max_chars=8000)
135
+ if content:
136
+ lines.append(f"#### {name}")
137
+ lines.append("")
138
+ lines.append(content.strip())
139
+ lines.append("")
140
+
141
+ lines.append("### Suggested child worker commands")
142
+ lines.append("")
143
+ lines.append("```bash")
144
+ lines.append(f"python ./.cstl/scripts/task.py select {child_rel}")
145
+ lines.append(
146
+ f"python ./.cstl/scripts/task.py set-child-state {parent_rel} {child_rel} working --evidence implement.md"
147
+ )
148
+ lines.append("# ... implement ...")
149
+ lines.append(
150
+ f"python ./.cstl/scripts/task.py set-child-state {parent_rel} {child_rel} review --evidence verify.md"
151
+ )
152
+ lines.append("```")
153
+ lines.append("")
154
+ return "\n".join(lines)
155
+
156
+
157
+ def build_child_prompt(
158
+ parent_dir: Path,
159
+ child_dir: Path,
160
+ *,
161
+ include_artifacts: bool = False,
162
+ mode: str = "inline",
163
+ ) -> tuple[str | None, list[str]]:
164
+ """Build an implementation prompt for a linked child task."""
165
+ errors: list[str] = []
166
+ repo_root = get_repo_root()
167
+ child_name = child_dir.name
168
+
169
+ parent_json = parent_dir / "task.json"
170
+ if not parent_json.is_file():
171
+ return None, [f"parent task.json missing: {parent_dir}"]
172
+
173
+ data, body = load_task_map(parent_dir)
174
+ if data is None:
175
+ return None, ["parent task-map.md missing or invalid"]
176
+
177
+ child_entry = get_child_entry(data, child_name)
178
+ if child_entry is None:
179
+ return None, [f"child not in parent task-map: {child_name}"]
180
+
181
+ children_by_id = {
182
+ item.get("id"): item
183
+ for item in data.get("children", [])
184
+ if isinstance(item, dict) and item.get("id")
185
+ }
186
+ blocked = _unmet_dependencies(child_entry, children_by_id)
187
+
188
+ parent_rel = _repo_rel(parent_dir, repo_root)
189
+ child_rel = _repo_rel(child_dir, repo_root)
190
+ repo_rel = _repo_rel(repo_root, repo_root)
191
+
192
+ child_prd = _read_optional(child_dir / "prd.md", 4000)
193
+ child_design = _read_optional(child_dir / "design.md", 4000)
194
+ child_implement = _read_optional(child_dir / "implement.md", 4000)
195
+ parent_child_prompts = _read_optional(parent_dir / "child-prompts.md", 16000)
196
+
197
+ goal = ""
198
+ if child_prd:
199
+ goal_match = re.search(r"(?ms)^##\s*Goal\s*\n+(.*?)(?:\n##|\Z)", child_prd)
200
+ if goal_match:
201
+ goal = goal_match.group(1).strip()
202
+
203
+ lines = [
204
+ f"You are implementing child task `{child_name}`.",
205
+ "",
206
+ "Workspace:",
207
+ f"- Root: {repo_rel}",
208
+ f"- Parent task: {parent_rel}",
209
+ f"- Child task: {child_rel}",
210
+ "",
211
+ ]
212
+
213
+ if blocked:
214
+ lines.extend(
215
+ [
216
+ "Start condition:",
217
+ "- **BLOCKED** — unmet dependencies:",
218
+ ]
219
+ )
220
+ for item in blocked:
221
+ lines.append(f" - {item}")
222
+ lines.append("- Do not start implementation until dependencies reach `integrated` or `cancelled`.")
223
+ lines.append("")
224
+ elif depends_on := child_entry.get("depends_on"):
225
+ lines.extend(
226
+ [
227
+ "Start condition:",
228
+ f"- Dependencies satisfied: {', '.join(f'`{d}`' for d in depends_on)}.",
229
+ "",
230
+ ]
231
+ )
232
+
233
+ lines.extend(
234
+ [
235
+ "Read first:",
236
+ f"- Parent prd.md/design.md/implement.md/task-map.md",
237
+ f"- Child prd.md/design.md/implement.md under `{child_rel}`",
238
+ ]
239
+ )
240
+ if parent_child_prompts and child_name in parent_child_prompts:
241
+ lines.append(f"- Parent child-prompts.md section for `{child_name}` when present")
242
+
243
+ if goal:
244
+ lines.extend(["", "Goal:", goal, ""])
245
+
246
+ lines.append(_GLOBAL_CHILD_CONSTRAINTS)
247
+ lines.append("")
248
+
249
+ if child_implement:
250
+ lines.extend(["Implementation plan (from child implement.md):", child_implement.strip()[:6000], ""])
251
+ elif child_design:
252
+ lines.extend(["Design notes (from child design.md):", child_design.strip()[:4000], ""])
253
+
254
+ touches = child_entry.get("touches") or []
255
+ if touches:
256
+ lines.append("Declared touch scope (parent task-map):")
257
+ for touch in touches:
258
+ lines.append(f"- `{touch}`")
259
+ lines.append("")
260
+
261
+ if mode == "subagent":
262
+ topo = data.get("execution_topology", "serial")
263
+ merge_lim = data.get("merge_limit", 1)
264
+ lines.extend(
265
+ [
266
+ f"Selected task: {child_rel}",
267
+ "",
268
+ "Delivery mode: subagent (Cursor)",
269
+ "- **Default:** Parent session dispatches **Task** with `subagent_type=cstl-implement` and this prompt as the task description (writable sub-agent). Model under Cursor++ BYOK comes from `.cstl/local/cursor2plus/` routing (see `cursor-subagent-policy.md` Method 2.5/2.6).",
270
+ "- **Exception:** If parent `child-prompts.md` or the user names this child for a **separate writable Agent chat**, open a new Agent session, pick the model manually, paste this prompt — do not use Task from Parent.",
271
+ "- Parent retains `review-child` / `integrate-child`. Child must not nest further `cstl-research` / `cstl-implement` / `cstl-check` Task dispatches.",
272
+ f"- Parent orchestration: `execution_topology={topo}`, `merge_limit={merge_lim}`.",
273
+ "- When `isolation: git-worktree`, prepare worktree from a **git repo root** (e.g. `Trellis/`) before large edits.",
274
+ "",
275
+ ]
276
+ )
277
+ else:
278
+ lines.extend(
279
+ [
280
+ "Delivery mode: inline",
281
+ "- Hand this prompt to the child worker agent or session manually.",
282
+ "- Parent session retains integration authority (`integrate-child`, `review-child`).",
283
+ "",
284
+ ]
285
+ )
286
+
287
+ lines.append(_child_prompt_section(parent_dir, child_dir, child_entry, repo_root, include_artifacts=include_artifacts))
288
+
289
+ return "\n".join(lines), errors
290
+
291
+
292
+ def build_parent_status(parent_dir: Path) -> str:
293
+ """Render a parent task-map status summary for the reviewer."""
294
+ repo_root = get_repo_root()
295
+ parent_rel = _repo_rel(parent_dir, repo_root)
296
+ data, _ = load_task_map(parent_dir)
297
+ if data is None:
298
+ return f"Parent `{parent_rel}`: task-map.md missing or invalid."
299
+
300
+ lines = [
301
+ f"# Parent orchestration status — `{parent_dir.name}`",
302
+ "",
303
+ f"- Path: `{parent_rel}`",
304
+ f"- contract_epoch: {data.get('contract_epoch', '?')}",
305
+ f"- execution_topology: {data.get('execution_topology', '?')}",
306
+ f"- merge_limit: {data.get('merge_limit', '?')}",
307
+ "",
308
+ "## Children",
309
+ "",
310
+ ]
311
+
312
+ children = data.get("children") or []
313
+ if not children:
314
+ lines.append("(no children in task-map)")
315
+ return "\n".join(lines)
316
+
317
+ for child in children:
318
+ if not isinstance(child, dict):
319
+ continue
320
+ cid = child.get("id", "?")
321
+ state = child.get("state", "?")
322
+ evidence = child.get("evidence")
323
+ ref = child.get("ref")
324
+ deps = child.get("depends_on") or []
325
+ touches = child.get("touches") or []
326
+ isolation = child.get("isolation")
327
+ branch = child.get("branch")
328
+ worktree_path = child.get("worktree_path")
329
+ lines.append(f"### `{cid}` — `{state}`")
330
+ if deps:
331
+ lines.append(f"- depends_on: {', '.join(deps)}")
332
+ if touches:
333
+ lines.append(f"- touches: {', '.join(touches)}")
334
+ if isolation:
335
+ lines.append(f"- isolation: {isolation}")
336
+ if branch:
337
+ lines.append(f"- branch: {branch}")
338
+ if worktree_path:
339
+ lines.append(f"- worktree_path: {worktree_path}")
340
+ if evidence:
341
+ lines.append(f"- evidence: {evidence}")
342
+ if ref:
343
+ lines.append(f"- ref: {ref}")
344
+ child_dir = parent_dir.parent / cid
345
+ for artifact in ("verify.md", "handoff.md"):
346
+ flag = "yes" if (child_dir / artifact).is_file() else "missing"
347
+ lines.append(f"- {artifact}: {flag}")
348
+ lines.append("")
349
+
350
+ queue = data.get("integration_queue") or []
351
+ lines.append(f"integration_queue: {queue}")
352
+ lines.append("")
353
+ lines.append("## Suggested parent commands")
354
+ lines.append("")
355
+ lines.append("```bash")
356
+ lines.append(f"python ./.cstl/scripts/task.py parent-status {parent_rel}")
357
+ lines.append(f"python ./.cstl/scripts/task.py generate-child-prompt {parent_rel} <child> --mode subagent")
358
+ lines.append(f"python ./.cstl/scripts/task.py generate-child-prompt {parent_rel} <child> --mode inline")
359
+ lines.append(f"python ./.cstl/scripts/task.py review-child {parent_rel} <child> --check")
360
+ lines.append(f"python ./.cstl/scripts/task.py review-child {parent_rel} <child> --decision accept --ref <ref>")
361
+ lines.append("```")
362
+ return "\n".join(lines)
363
+
364
+
365
+ def _summarize_handoff(child_dir: Path) -> dict[str, str | list[str] | bool]:
366
+ verify = _read_optional(child_dir / "verify.md", 6000) or ""
367
+ handoff = _read_optional(child_dir / "handoff.md", 12000) or ""
368
+ has_verify = (child_dir / "verify.md").is_file()
369
+ has_handoff = (child_dir / "handoff.md").is_file()
370
+
371
+ validation_ok = bool(
372
+ re.search(r"(?i)validation\s+(evidence|commands|results)", verify)
373
+ or re.search(r"(?i)validation\s*:", verify)
374
+ )
375
+ acceptance_ok = bool(re.search(r"(?i)acceptance\s+evidence|accepted\s+by\s+user", verify))
376
+ learning = durable_learning_decision_status(verify) if verify else {
377
+ "no_durable_learning": False,
378
+ "spec_update": False,
379
+ "learning_artifact": False,
380
+ "any": False,
381
+ }
382
+
383
+ changed_files: list[str] = []
384
+ for line in handoff.splitlines():
385
+ if "|" in line and not line.strip().startswith("| ---"):
386
+ cells = [c.strip() for c in line.split("|") if c.strip()]
387
+ if len(cells) >= 2 and cells[0].lower() not in ("path", "file"):
388
+ changed_files.append(cells[0])
389
+
390
+ return {
391
+ "has_verify": has_verify,
392
+ "has_handoff": has_handoff,
393
+ "validation_section": validation_ok,
394
+ "acceptance_section": acceptance_ok,
395
+ "learning_decision": learning,
396
+ "verify_excerpt": verify.strip()[:2000],
397
+ "handoff_excerpt": handoff.strip()[:4000],
398
+ "changed_files": changed_files[:20],
399
+ }
400
+
401
+
402
+ def _learning_decision_review_lines(
403
+ parent_dir: Path,
404
+ child_dir: Path,
405
+ child_data: dict,
406
+ summary: dict,
407
+ *,
408
+ decision: str | None,
409
+ reason: str | None,
410
+ ) -> list[str]:
411
+ """Prompt parent/child to record durable learning before child archive."""
412
+ learning = summary.get("learning_decision") or {}
413
+ if learning.get("any"):
414
+ return []
415
+
416
+ repo_root = get_repo_root()
417
+ child_rel = _repo_rel(child_dir, repo_root)
418
+ lines = [
419
+ "## Durable learning decision (required before child archive)",
420
+ "",
421
+ "Child verify.md has no grep-friendly durable-learning line yet. Pick one outcome:",
422
+ "",
423
+ "- `Durable learning decision: no durable learning` — routine scope, no reusable contract.",
424
+ "- `Spec update evidence: .cstl/spec/<path>` — after `/cstl:update-spec` with reviewer confirmation.",
425
+ f"- `Learning artifact: {child_rel}/handoff.md` — handoff already captures the insight.",
426
+ "",
427
+ f"Helper: `python ./.cstl/scripts/task.py prepare-archive-evidence {child_rel}`",
428
+ "",
429
+ ]
430
+
431
+ trigger: str | None = None
432
+ if decision == "changes":
433
+ trigger = f"Parent review requested changes ({reason or 'see review notes'})"
434
+ elif decision in ("accept", "integrate-through"):
435
+ trigger = "Parent accepted child handoff — confirm whether workflow/contracts should be captured in spec"
436
+
437
+ if trigger or decision == "changes":
438
+ lines.append(build_spec_update_scaffold(repo_root, child_dir, child_data, trigger=trigger))
439
+ return lines
440
+
441
+
442
+ def build_review_report(
443
+ parent_dir: Path,
444
+ child_dir: Path,
445
+ parent_data: dict,
446
+ child_data: dict,
447
+ *,
448
+ decision: str | None,
449
+ ref: str | None,
450
+ reason: str | None,
451
+ notes: str | None,
452
+ ) -> tuple[str, list[str], dict]:
453
+ """Build review summary and validate integration transitions for a decision."""
454
+ child_name = child_dir.name
455
+ summary = _summarize_handoff(child_dir)
456
+ errors: list[str] = []
457
+ actions: dict = {"integrate": None, "gates": []}
458
+
459
+ data, _ = load_task_map(parent_dir)
460
+ child_entry = get_child_entry(data, child_name) if data else None
461
+ current_state = child_entry.get("state") if child_entry else None
462
+
463
+ lines = [
464
+ f"# Parent review — `{child_name}`",
465
+ "",
466
+ f"- Timestamp: {_utc_now()}",
467
+ f"- Current integration state: `{current_state}`",
468
+ f"- Decision: `{decision or 'check-only'}`",
469
+ "",
470
+ "## Handoff artifacts",
471
+ "",
472
+ f"- verify.md: {'present' if summary['has_verify'] else '**missing**'}",
473
+ f"- handoff.md: {'present' if summary['has_handoff'] else '**missing**'}",
474
+ f"- verify validation section signal: {summary['validation_section']}",
475
+ f"- verify acceptance section signal: {summary['acceptance_section']}",
476
+ f"- durable learning decision signal: {summary['learning_decision'].get('any')}",
477
+ "",
478
+ ]
479
+ learning_lines = _learning_decision_review_lines(
480
+ parent_dir,
481
+ child_dir,
482
+ child_data,
483
+ summary,
484
+ decision=decision,
485
+ reason=reason,
486
+ )
487
+ if learning_lines:
488
+ lines.extend(learning_lines)
489
+
490
+ if summary["changed_files"]:
491
+ lines.append("Changed files (from handoff table):")
492
+ for path in summary["changed_files"]:
493
+ lines.append(f"- `{path}`")
494
+ lines.append("")
495
+
496
+ if notes:
497
+ lines.extend(["## Parent notes", "", notes.strip(), ""])
498
+
499
+ if decision in ("accept", "changes", "cancel"):
500
+ if decision == "accept":
501
+ target = "accepted"
502
+ if current_state != "review":
503
+ errors.append(f"accept requires child state 'review', got {current_state!r}")
504
+ errors.extend(
505
+ validate_parent_child_integration(
506
+ parent_dir,
507
+ parent_data,
508
+ child_dir,
509
+ child_data,
510
+ target,
511
+ "handoff.md",
512
+ ref,
513
+ reason,
514
+ )
515
+ )
516
+ actions["integrate"] = {
517
+ "state": target,
518
+ "evidence": "handoff.md",
519
+ "ref": ref,
520
+ }
521
+ actions["gates"].append(
522
+ {
523
+ "task": "parent",
524
+ "transition": "parent-accepted",
525
+ "optional": True,
526
+ "hint": (
527
+ f"Optional audit: python ./.cstl/scripts/task.py record-gate {parent_dir.name} "
528
+ f"--transition parent-accepted --gate code-review --result PASS "
529
+ f"--reviewer parent --evidence review-{child_name}.md"
530
+ ),
531
+ }
532
+ )
533
+ child_profile = task_closeout_profile(child_dir, child_data)
534
+ if child_profile == "full":
535
+ actions["gates"].append(
536
+ {
537
+ "task": "child",
538
+ "transition": "child-review",
539
+ "optional": False,
540
+ "hint": (
541
+ f"Required before accept: python ./.cstl/scripts/task.py record-gate {child_name} "
542
+ f"--transition child-review --gate code-review --result PASS "
543
+ f"--reviewer parent --evidence verify.md"
544
+ ),
545
+ }
546
+ )
547
+ elif child_profile == "lite":
548
+ actions["gates"].append(
549
+ {
550
+ "task": "child",
551
+ "transition": "child-review",
552
+ "optional": True,
553
+ "hint": "Lite child: no child-review gate chain required.",
554
+ }
555
+ )
556
+ elif decision == "changes":
557
+ target = "changes"
558
+ errors.extend(
559
+ validate_parent_child_integration(
560
+ parent_dir,
561
+ parent_data,
562
+ child_dir,
563
+ child_data,
564
+ target,
565
+ "handoff.md",
566
+ ref,
567
+ reason,
568
+ )
569
+ )
570
+ if not reason:
571
+ errors.append("changes requires --reason")
572
+ actions["integrate"] = {
573
+ "state": target,
574
+ "evidence": "handoff.md",
575
+ "ref": ref,
576
+ "reason": reason,
577
+ }
578
+ elif decision == "cancel":
579
+ target = "cancelled"
580
+ errors.extend(
581
+ validate_parent_child_integration(
582
+ parent_dir,
583
+ parent_data,
584
+ child_dir,
585
+ child_data,
586
+ target,
587
+ "handoff.md",
588
+ ref,
589
+ reason,
590
+ )
591
+ )
592
+ if not reason:
593
+ errors.append("cancel requires --reason")
594
+ actions["integrate"] = {
595
+ "state": target,
596
+ "evidence": "handoff.md",
597
+ "reason": reason,
598
+ }
599
+ elif decision == "integrate-through":
600
+ sequence = [
601
+ ("accepted", "handoff.md"),
602
+ ("integrating", "task-map.md"),
603
+ ("integrated", "task-map.md"),
604
+ ]
605
+ sim_state = current_state
606
+ for target, evidence in sequence:
607
+ if target == "accepted" and sim_state != "review":
608
+ errors.append(f"integrate-through requires child state 'review', got {sim_state!r}")
609
+ break
610
+ if target == "integrating" and sim_state != "accepted":
611
+ errors.append(f"integrating requires 'accepted', simulated from {sim_state!r}")
612
+ break
613
+ if target == "integrated" and sim_state != "integrating":
614
+ errors.append(f"integrated requires 'integrating', simulated from {sim_state!r}")
615
+ break
616
+ step_errors = validate_parent_child_integration(
617
+ parent_dir,
618
+ parent_data,
619
+ child_dir,
620
+ child_data,
621
+ target,
622
+ evidence,
623
+ ref,
624
+ reason,
625
+ current_state_override=sim_state,
626
+ )
627
+ errors.extend(step_errors)
628
+ if step_errors:
629
+ break
630
+ sim_state = target
631
+ if not errors:
632
+ actions["integrate_sequence"] = [
633
+ {"state": "accepted", "evidence": "handoff.md", "ref": ref},
634
+ {"state": "integrating", "evidence": "task-map.md", "ref": ref},
635
+ {"state": "integrated", "evidence": "task-map.md", "ref": ref},
636
+ ]
637
+ actions["gates"].append(
638
+ {
639
+ "task": "parent",
640
+ "transition": "parent-integrated",
641
+ "optional": False,
642
+ "hint": (
643
+ f"Required before parent archive: python ./.cstl/scripts/task.py record-gate {parent_dir.name} "
644
+ f"--transition parent-integrated --gate integration-review --result PASS "
645
+ f"--reviewer parent --evidence task-map.md"
646
+ ),
647
+ }
648
+ )
649
+
650
+ lines.extend(
651
+ [
652
+ "## verify.md excerpt",
653
+ "",
654
+ (summary["verify_excerpt"] or "(empty)"),
655
+ "",
656
+ "## handoff.md excerpt",
657
+ "",
658
+ (summary["handoff_excerpt"] or "(empty)"),
659
+ "",
660
+ ]
661
+ )
662
+
663
+ if decision and not errors:
664
+ lines.append("## Integration plan")
665
+ lines.append("")
666
+ if actions.get("integrate_sequence"):
667
+ for step in actions["integrate_sequence"]:
668
+ lines.append(
669
+ f"- integrate-child → `{step['state']}` (evidence={step['evidence']}, ref={step.get('ref')})"
670
+ )
671
+ elif actions.get("integrate"):
672
+ step = actions["integrate"]
673
+ lines.append(
674
+ f"- integrate-child → `{step['state']}` (evidence={step['evidence']}, ref={step.get('ref')})"
675
+ )
676
+ for gate in actions.get("gates", []):
677
+ if gate.get("optional"):
678
+ lines.append(f"- Optional gate ({gate['transition']}): {gate['hint']}")
679
+ lines.append("")
680
+
681
+ report = "\n".join(lines)
682
+ return report, errors, actions
683
+
684
+
685
+ def append_parent_review_notes(parent_dir: Path, child_name: str, report: str) -> None:
686
+ """Append a parent review section to parent verify.md."""
687
+ verify_path = parent_dir / "verify.md"
688
+ header = f"\n\n## Parent review — `{child_name}` ({_utc_now()})\n\n"
689
+ if verify_path.is_file():
690
+ existing = verify_path.read_text(encoding="utf-8")
691
+ verify_path.write_text(existing.rstrip() + header + report.strip() + "\n", encoding="utf-8")
692
+ else:
693
+ verify_path.write_text(
694
+ "# Verification Evidence\n\n" + header + report.strip() + "\n",
695
+ encoding="utf-8",
696
+ )
697
+
698
+
699
+ def write_review_artifact(parent_dir: Path, child_name: str, report: str) -> Path:
700
+ """Write a standalone review artifact under the parent task directory."""
701
+ path = parent_dir / f"review-{child_name}.md"
702
+ path.write_text(report.strip() + "\n", encoding="utf-8")
703
703
  return path