@blxzer/cursor-trellis 0.3.4 → 0.3.6

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 (210) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +13 -13
  3. package/README.zh-CN.md +1 -1
  4. package/bin/cstl.js +1 -1
  5. package/bin/smart-search.js +1 -1
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +17 -2
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/campaign/canvas-render.d.ts +68 -0
  10. package/dist/commands/campaign/canvas-render.d.ts.map +1 -0
  11. package/dist/commands/campaign/canvas-render.js +414 -0
  12. package/dist/commands/campaign/canvas-render.js.map +1 -0
  13. package/dist/commands/campaign/compose.d.ts +15 -0
  14. package/dist/commands/campaign/compose.d.ts.map +1 -0
  15. package/dist/commands/campaign/compose.js +110 -0
  16. package/dist/commands/campaign/compose.js.map +1 -0
  17. package/dist/commands/campaign/index.d.ts +13 -0
  18. package/dist/commands/campaign/index.d.ts.map +1 -0
  19. package/dist/commands/campaign/index.js +134 -0
  20. package/dist/commands/campaign/index.js.map +1 -0
  21. package/dist/commands/campaign/kind-map.d.ts +6 -0
  22. package/dist/commands/campaign/kind-map.d.ts.map +1 -0
  23. package/dist/commands/campaign/kind-map.js +21 -0
  24. package/dist/commands/campaign/kind-map.js.map +1 -0
  25. package/dist/commands/campaign/mcp-server.d.ts +6 -0
  26. package/dist/commands/campaign/mcp-server.d.ts.map +1 -0
  27. package/dist/commands/campaign/mcp-server.js +167 -0
  28. package/dist/commands/campaign/mcp-server.js.map +1 -0
  29. package/dist/commands/campaign/render.d.ts +5 -0
  30. package/dist/commands/campaign/render.d.ts.map +1 -0
  31. package/dist/commands/campaign/render.js +103 -0
  32. package/dist/commands/campaign/render.js.map +1 -0
  33. package/dist/commands/campaign/trellis-load.d.ts +13 -0
  34. package/dist/commands/campaign/trellis-load.d.ts.map +1 -0
  35. package/dist/commands/campaign/trellis-load.js +122 -0
  36. package/dist/commands/campaign/trellis-load.js.map +1 -0
  37. package/dist/commands/campaign/types.d.ts +61 -0
  38. package/dist/commands/campaign/types.d.ts.map +1 -0
  39. package/dist/commands/campaign/types.js +3 -0
  40. package/dist/commands/campaign/types.js.map +1 -0
  41. package/dist/commands/goal/index.d.ts +3 -0
  42. package/dist/commands/goal/index.d.ts.map +1 -0
  43. package/dist/commands/goal/index.js +159 -0
  44. package/dist/commands/goal/index.js.map +1 -0
  45. package/dist/commands/goal/review.d.ts +6 -0
  46. package/dist/commands/goal/review.d.ts.map +1 -0
  47. package/dist/commands/goal/review.js +28 -0
  48. package/dist/commands/goal/review.js.map +1 -0
  49. package/dist/commands/init.d.ts.map +1 -1
  50. package/dist/commands/init.js +11 -1
  51. package/dist/commands/init.js.map +1 -1
  52. package/dist/commands/rpc/client.d.ts +51 -0
  53. package/dist/commands/rpc/client.d.ts.map +1 -0
  54. package/dist/commands/rpc/client.js +147 -0
  55. package/dist/commands/rpc/client.js.map +1 -0
  56. package/dist/commands/rpc/index.d.ts +10 -0
  57. package/dist/commands/rpc/index.d.ts.map +1 -0
  58. package/dist/commands/rpc/index.js +157 -0
  59. package/dist/commands/rpc/index.js.map +1 -0
  60. package/dist/commands/rpc/serve.d.ts +23 -0
  61. package/dist/commands/rpc/serve.d.ts.map +1 -0
  62. package/dist/commands/rpc/serve.js +60 -0
  63. package/dist/commands/rpc/serve.js.map +1 -0
  64. package/dist/commands/rpc/smoke.d.ts +16 -0
  65. package/dist/commands/rpc/smoke.d.ts.map +1 -0
  66. package/dist/commands/rpc/smoke.js +92 -0
  67. package/dist/commands/rpc/smoke.js.map +1 -0
  68. package/dist/commands/sdk/index.d.ts +9 -0
  69. package/dist/commands/sdk/index.d.ts.map +1 -0
  70. package/dist/commands/sdk/index.js +58 -0
  71. package/dist/commands/sdk/index.js.map +1 -0
  72. package/dist/commands/sdk/run.d.ts +42 -0
  73. package/dist/commands/sdk/run.d.ts.map +1 -0
  74. package/dist/commands/sdk/run.js +220 -0
  75. package/dist/commands/sdk/run.js.map +1 -0
  76. package/dist/commands/sdk/status.d.ts +14 -0
  77. package/dist/commands/sdk/status.d.ts.map +1 -0
  78. package/dist/commands/sdk/status.js +69 -0
  79. package/dist/commands/sdk/status.js.map +1 -0
  80. package/dist/configurators/workflow.d.ts.map +1 -1
  81. package/dist/configurators/workflow.js +13 -1
  82. package/dist/configurators/workflow.js.map +1 -1
  83. package/dist/goal/action-packet.d.ts +89 -0
  84. package/dist/goal/action-packet.d.ts.map +1 -0
  85. package/dist/goal/action-packet.js +113 -0
  86. package/dist/goal/action-packet.js.map +1 -0
  87. package/dist/goal/audit.d.ts +14 -0
  88. package/dist/goal/audit.d.ts.map +1 -0
  89. package/dist/goal/audit.js +12 -0
  90. package/dist/goal/audit.js.map +1 -0
  91. package/dist/goal/constants.d.ts +26 -0
  92. package/dist/goal/constants.d.ts.map +1 -0
  93. package/dist/goal/constants.js +24 -0
  94. package/dist/goal/constants.js.map +1 -0
  95. package/dist/goal/paths.d.ts +8 -0
  96. package/dist/goal/paths.d.ts.map +1 -0
  97. package/dist/goal/paths.js +36 -0
  98. package/dist/goal/paths.js.map +1 -0
  99. package/dist/goal/preflight.d.ts +21 -0
  100. package/dist/goal/preflight.d.ts.map +1 -0
  101. package/dist/goal/preflight.js +97 -0
  102. package/dist/goal/preflight.js.map +1 -0
  103. package/dist/goal/review-seam.d.ts +44 -0
  104. package/dist/goal/review-seam.d.ts.map +1 -0
  105. package/dist/goal/review-seam.js +153 -0
  106. package/dist/goal/review-seam.js.map +1 -0
  107. package/dist/goal/reviewer-rules.d.ts +13 -0
  108. package/dist/goal/reviewer-rules.d.ts.map +1 -0
  109. package/dist/goal/reviewer-rules.js +153 -0
  110. package/dist/goal/reviewer-rules.js.map +1 -0
  111. package/dist/goal/reviewer.d.ts +9 -0
  112. package/dist/goal/reviewer.d.ts.map +1 -0
  113. package/dist/goal/reviewer.js +54 -0
  114. package/dist/goal/reviewer.js.map +1 -0
  115. package/dist/goal/runtime.d.ts +32 -0
  116. package/dist/goal/runtime.d.ts.map +1 -0
  117. package/dist/goal/runtime.js +242 -0
  118. package/dist/goal/runtime.js.map +1 -0
  119. package/dist/goal/sdk-client.d.ts +10 -0
  120. package/dist/goal/sdk-client.d.ts.map +1 -0
  121. package/dist/goal/sdk-client.js +34 -0
  122. package/dist/goal/sdk-client.js.map +1 -0
  123. package/dist/goal/state.d.ts +40 -0
  124. package/dist/goal/state.d.ts.map +1 -0
  125. package/dist/goal/state.js +37 -0
  126. package/dist/goal/state.js.map +1 -0
  127. package/dist/goal/task-triggers.d.ts +15 -0
  128. package/dist/goal/task-triggers.d.ts.map +1 -0
  129. package/dist/goal/task-triggers.js +23 -0
  130. package/dist/goal/task-triggers.js.map +1 -0
  131. package/dist/goal/walls.d.ts +12 -0
  132. package/dist/goal/walls.d.ts.map +1 -0
  133. package/dist/goal/walls.js +65 -0
  134. package/dist/goal/walls.js.map +1 -0
  135. package/dist/goal/worker-prompt.d.ts +13 -0
  136. package/dist/goal/worker-prompt.d.ts.map +1 -0
  137. package/dist/goal/worker-prompt.js +41 -0
  138. package/dist/goal/worker-prompt.js.map +1 -0
  139. package/dist/goal/worker.d.ts +42 -0
  140. package/dist/goal/worker.d.ts.map +1 -0
  141. package/dist/goal/worker.js +180 -0
  142. package/dist/goal/worker.js.map +1 -0
  143. package/dist/migrations/manifests/0.3.5.json +9 -0
  144. package/dist/migrations/manifests/0.3.6.json +9 -0
  145. package/dist/templates/common/bundled-skills/smart-search-cli/SKILL.md +35 -29
  146. package/dist/templates/common/bundled-skills/smart-search-cli/references/cli-contract.md +11 -25
  147. package/dist/templates/common/commands/continue.md +15 -3
  148. package/dist/templates/common/skills/check.md +7 -4
  149. package/dist/templates/cursor/agents/cstl-check.md +1 -0
  150. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  151. package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
  152. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  153. package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
  154. package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
  155. package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
  156. package/dist/templates/markdown/agents.md +2 -0
  157. package/dist/templates/markdown/index.d.ts +3 -0
  158. package/dist/templates/markdown/index.d.ts.map +1 -1
  159. package/dist/templates/markdown/index.js +3 -0
  160. package/dist/templates/markdown/index.js.map +1 -1
  161. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
  162. package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
  163. package/dist/templates/markdown/spec/guides/index.md.txt +107 -104
  164. package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
  165. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +38 -6
  166. package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +184 -0
  167. package/dist/templates/shared-hooks/session-start.py +10 -0
  168. package/dist/templates/trellis/config.yaml +18 -0
  169. package/dist/templates/trellis/index.d.ts +5 -0
  170. package/dist/templates/trellis/index.d.ts.map +1 -1
  171. package/dist/templates/trellis/index.js +14 -0
  172. package/dist/templates/trellis/index.js.map +1 -1
  173. package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
  174. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +51 -2
  175. package/dist/templates/trellis/scripts/common/config.py +39 -0
  176. package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
  177. package/dist/templates/trellis/scripts/common/parent_orchestration.py +500 -5
  178. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
  179. package/dist/templates/trellis/scripts/common/session_context.py +32 -4
  180. package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
  181. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
  182. package/dist/templates/trellis/scripts/common/task_context.py +18 -0
  183. package/dist/templates/trellis/scripts/common/task_dashboard.py +16 -1
  184. package/dist/templates/trellis/scripts/common/task_map.py +670 -614
  185. package/dist/templates/trellis/scripts/common/task_store.py +191 -40
  186. package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
  187. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
  188. package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
  189. package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
  190. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
  191. package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
  192. package/dist/templates/trellis/scripts/task.py +51 -0
  193. package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
  194. package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
  195. package/dist/templates/trellis/workflow.md +23 -3
  196. package/dist/utils/codebase-retrieval-router.d.ts.map +1 -1
  197. package/dist/utils/codebase-retrieval-router.js +14 -0
  198. package/dist/utils/codebase-retrieval-router.js.map +1 -1
  199. package/dist/utils/cursor-sdk-gate.d.ts +10 -0
  200. package/dist/utils/cursor-sdk-gate.d.ts.map +1 -0
  201. package/dist/utils/cursor-sdk-gate.js +32 -0
  202. package/dist/utils/cursor-sdk-gate.js.map +1 -0
  203. package/dist/utils/project-capabilities.d.ts +30 -3
  204. package/dist/utils/project-capabilities.d.ts.map +1 -1
  205. package/dist/utils/project-capabilities.js +182 -11
  206. package/dist/utils/project-capabilities.js.map +1 -1
  207. package/dist/utils/readiness.d.ts.map +1 -1
  208. package/dist/utils/readiness.js +26 -0
  209. package/dist/utils/readiness.js.map +1 -1
  210. package/package.json +4 -3
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python
2
2
  """
3
3
  Task CRUD operations.
4
4
 
@@ -25,6 +25,11 @@ import sys
25
25
  from datetime import datetime
26
26
  from pathlib import Path
27
27
 
28
+ from .artifact_locale import (
29
+ default_prd_content,
30
+ default_verify_content,
31
+ resolve_artifact_locale,
32
+ )
28
33
  from .config import (
29
34
  get_packages,
30
35
  get_session_auto_commit,
@@ -67,6 +72,7 @@ from .task_map import (
67
72
  get_child_state,
68
73
  record_child_worktree,
69
74
  remove_child_from_task_map,
75
+ resolve_child_map_id,
70
76
  set_child_state,
71
77
  set_parent_child_integration_state,
72
78
  validate_parent_child_integration,
@@ -249,11 +255,10 @@ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
249
255
  ".pi", # Pi Agent
250
256
  )
251
257
 
252
- _SEED_EXAMPLE = (
253
- "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. "
254
- "Put spec/research files only — no code paths. "
255
- "Run `python .cstl/scripts/get_context.py --mode packages` to list available specs. "
256
- "Delete this line once real entries are added."
258
+ _STABLE_SPEC_SEED_PATHS: tuple[str, ...] = (
259
+ ".cstl/spec/guides/index.md",
260
+ ".cstl/spec/guides/verification-strength-guide.md",
261
+ ".cstl/spec/guides/injection-budget-guide.md",
257
262
  )
258
263
 
259
264
 
@@ -270,41 +275,70 @@ def _has_subagent_platform(repo_root: Path) -> bool:
270
275
  return False
271
276
 
272
277
 
273
- def _write_seed_jsonl(path: Path) -> None:
274
- """Write a one-line seed JSONL file with a self-describing ``_example``.
275
-
276
- The seed row has no ``file`` field, so downstream consumers (hooks +
277
- preludes) that iterate entries via ``item.get("file")`` naturally skip
278
- it. The row exists purely as an in-file prompt for the AI curator.
279
- """
280
- seed = {"_example": _SEED_EXAMPLE}
281
- path.write_text(json.dumps(seed, ensure_ascii=False) + "\n", encoding="utf-8")
282
-
283
-
284
- def _default_prd_content(title: str, description: str | None = None) -> str:
285
- """Return the default PRD skeleton created with every task."""
286
- goal = (description or "").strip() or "TBD."
287
- heading = title.strip() or "Untitled task"
288
- return f"""# {heading}
289
-
290
- ## Goal
291
-
292
- {goal}
293
-
294
- ## Requirements
278
+ def _resolve_seed_spec_paths(
279
+ repo_root: Path,
280
+ task_dir: Path,
281
+ task_data: dict,
282
+ ) -> list[str]:
283
+ """Return 2–3 existing spec file paths for jsonl create seeds."""
284
+ from .task_gates import suggest_spec_targets
285
+
286
+ candidates: list[str] = []
287
+ for path in suggest_spec_targets(repo_root, task_dir, task_data):
288
+ if path not in candidates:
289
+ candidates.append(path)
290
+ for rel in _STABLE_SPEC_SEED_PATHS:
291
+ if rel not in candidates:
292
+ candidates.append(rel)
293
+
294
+ existing = [rel for rel in candidates if (repo_root / rel).is_file()]
295
+ if len(existing) >= 2:
296
+ return existing[:3]
297
+
298
+ fallback = [
299
+ rel for rel in _STABLE_SPEC_SEED_PATHS if (repo_root / rel).is_file()
300
+ ]
301
+ return fallback[:3]
295
302
 
296
- - TBD
297
303
 
298
- ## Acceptance Criteria
304
+ def _write_seed_jsonl(
305
+ path: Path,
306
+ repo_root: Path,
307
+ task_dir: Path,
308
+ task_data: dict,
309
+ ) -> None:
310
+ """Write 2–3 JSONL rows with real spec ``file`` paths for sub-agent context."""
311
+ spec_paths = _resolve_seed_spec_paths(repo_root, task_dir, task_data)
312
+ if len(spec_paths) < 2:
313
+ raise ValueError(
314
+ "task create requires at least two existing spec guide paths for jsonl seed"
315
+ )
299
316
 
300
- - [ ] TBD
317
+ lines: list[str] = []
318
+ for spec_path in spec_paths:
319
+ lines.append(
320
+ json.dumps(
321
+ {
322
+ "file": spec_path,
323
+ "reason": "default spec seed — curate entries for this task",
324
+ },
325
+ ensure_ascii=False,
326
+ )
327
+ )
328
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
301
329
 
302
- ## Notes
303
330
 
304
- - Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
305
- - Lightweight tasks can remain PRD-only.
306
- - For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start-execution --check`.
307
- """
331
+ def _default_prd_content(
332
+ title: str,
333
+ description: str | None = None,
334
+ *,
335
+ repo_root: Path | None = None,
336
+ task_dir: Path | None = None,
337
+ ) -> str:
338
+ """Return the default PRD skeleton created with every task."""
339
+ root = repo_root or get_repo_root()
340
+ locale = resolve_artifact_locale(task_dir, root)
341
+ return default_prd_content(title, description, locale, root)
308
342
 
309
343
 
310
344
  # =============================================================================
@@ -412,7 +446,14 @@ def cmd_create(args: argparse.Namespace) -> int:
412
446
  prd_path = task_dir / "prd.md"
413
447
  if not prd_path.exists():
414
448
  prd_path.write_text(
415
- _default_prd_content(args.title, args.description),
449
+ _default_prd_content(args.title, args.description, repo_root=repo_root, task_dir=task_dir),
450
+ encoding="utf-8",
451
+ )
452
+
453
+ verify_path = task_dir / "verify.md"
454
+ if not verify_path.exists():
455
+ verify_path.write_text(
456
+ default_verify_content(repo_root=repo_root, task_dir=task_dir),
416
457
  encoding="utf-8",
417
458
  )
418
459
 
@@ -425,7 +466,7 @@ def cmd_create(args: argparse.Namespace) -> int:
425
466
  for jsonl_name in ("implement.jsonl", "check.jsonl"):
426
467
  jsonl_path = task_dir / jsonl_name
427
468
  if not jsonl_path.exists():
428
- _write_seed_jsonl(jsonl_path)
469
+ _write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
429
470
  seeded_jsonl = True
430
471
 
431
472
  # Handle --parent: establish bidirectional link
@@ -1136,10 +1177,11 @@ def cmd_set_child_state(args: argparse.Namespace) -> int:
1136
1177
  )
1137
1178
  return 1
1138
1179
 
1180
+ child_map_id = resolve_child_map_id(parent_data, child_dir, child_data)
1139
1181
  ok, errors = set_child_state(
1140
1182
  parent_dir,
1141
1183
  parent_data,
1142
- child_dir.name,
1184
+ child_map_id,
1143
1185
  state,
1144
1186
  args.evidence,
1145
1187
  getattr(args, "reason", None),
@@ -1265,6 +1307,29 @@ def cmd_integrate_child(args: argparse.Namespace) -> int:
1265
1307
  if merge_ref:
1266
1308
  print(f"Merge executed: git merge --no-ff --no-commit {merge_ref}")
1267
1309
  print(f"Parent map: {_repo_relative_path(parent_dir / 'task-map.md', repo_root)}")
1310
+
1311
+ if state == "integrated" and not getattr(args, "no_publish_pack", False):
1312
+ from .parent_orchestration import build_publish_pack
1313
+
1314
+ summary, pack_errors = build_publish_pack(parent_dir)
1315
+ if pack_errors or summary is None:
1316
+ for item in pack_errors:
1317
+ print(f" - {item}", file=sys.stderr)
1318
+ print(
1319
+ colored(
1320
+ "Warning: integrate succeeded but publish-pack failed; "
1321
+ "PACK may be stale (see parent-status stalePack).",
1322
+ Colors.YELLOW,
1323
+ ),
1324
+ file=sys.stderr,
1325
+ )
1326
+ return 2
1327
+ print(colored("✓ PACK refreshed after integrate", Colors.GREEN))
1328
+ for line in summary.splitlines():
1329
+ if line.startswith("- Generated:"):
1330
+ print(line)
1331
+ break
1332
+
1268
1333
  return 0
1269
1334
 
1270
1335
 
@@ -1374,14 +1439,60 @@ def cmd_generate_child_prompt(args: argparse.Namespace) -> int:
1374
1439
 
1375
1440
  def cmd_parent_status(args: argparse.Namespace) -> int:
1376
1441
  """Show parent task-map orchestration status."""
1377
- from .parent_orchestration import build_parent_status
1442
+ import json
1443
+
1444
+ from .parent_orchestration import build_parent_status, build_parent_status_dict
1378
1445
 
1379
1446
  repo_root = get_repo_root()
1380
1447
  parent_dir = resolve_task_dir(args.parent_dir, repo_root)
1448
+ if getattr(args, "json", False):
1449
+ payload = build_parent_status_dict(parent_dir)
1450
+ if payload is None:
1451
+ print(
1452
+ json.dumps(
1453
+ {
1454
+ "error": "task-map.md missing or invalid",
1455
+ "path": str(parent_dir),
1456
+ }
1457
+ )
1458
+ )
1459
+ return 1
1460
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
1461
+ return 0
1381
1462
  print(build_parent_status(parent_dir))
1382
1463
  return 0
1383
1464
 
1384
1465
 
1466
+
1467
+ def cmd_publish_pack(args: argparse.Namespace) -> int:
1468
+ """Publish HYBRID campaign PACK (ready list + child prompts)."""
1469
+ from .parent_orchestration import build_publish_pack
1470
+
1471
+ repo_root = get_repo_root()
1472
+ parent_dir = resolve_task_dir(args.parent_dir, repo_root)
1473
+ mode = getattr(args, "mode", "inline") or "inline"
1474
+ if mode not in ("inline", "subagent"):
1475
+ print(colored("Error: --mode must be inline or subagent", Colors.RED), file=sys.stderr)
1476
+ return 1
1477
+
1478
+ summary, errors = build_publish_pack(
1479
+ parent_dir,
1480
+ stage_id=getattr(args, "stage", None),
1481
+ mode=mode,
1482
+ dry_run=bool(getattr(args, "dry_run", False)),
1483
+ )
1484
+ if errors:
1485
+ for item in errors:
1486
+ print(f" - {item}", file=sys.stderr)
1487
+ print(colored("Error: publish-pack failed", Colors.RED), file=sys.stderr)
1488
+ return 1
1489
+ if summary is None:
1490
+ print(colored("Error: publish-pack produced no summary", Colors.RED), file=sys.stderr)
1491
+ return 1
1492
+ print(summary)
1493
+ return 0
1494
+
1495
+
1385
1496
  def cmd_review_child(args: argparse.Namespace) -> int:
1386
1497
  """Review child handoff and optionally advance parent integration states."""
1387
1498
  from .parent_orchestration import (
@@ -1592,3 +1703,43 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
1592
1703
 
1593
1704
  print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
1594
1705
  return 0
1706
+
1707
+
1708
+ # =============================================================================
1709
+ # Command: artifact-locale
1710
+ # =============================================================================
1711
+
1712
+ def cmd_artifact_locale(args: argparse.Namespace) -> int:
1713
+ """Get or set human-reviewed artifact locale (zh | en)."""
1714
+ from .artifact_locale import (
1715
+ resolve_artifact_locale,
1716
+ set_task_artifact_locale,
1717
+ set_workspace_artifact_locale,
1718
+ )
1719
+
1720
+ repo_root = get_repo_root()
1721
+ subcommand = getattr(args, "artifact_locale_command", None)
1722
+
1723
+ if subcommand == "get":
1724
+ task_dir = resolve_task_dir(args.task, repo_root) if args.task else None
1725
+ print(resolve_artifact_locale(task_dir, repo_root))
1726
+ return 0
1727
+
1728
+ if subcommand == "set":
1729
+ try:
1730
+ if args.task:
1731
+ task_dir = resolve_task_dir(args.task, repo_root)
1732
+ set_task_artifact_locale(task_dir, args.locale, repo_root)
1733
+ scope = f"task {task_dir.name}"
1734
+ else:
1735
+ set_workspace_artifact_locale(args.locale, repo_root)
1736
+ scope = "workspace"
1737
+ except (ValueError, FileNotFoundError) as exc:
1738
+ print(colored(f"Error: {exc}", Colors.RED), file=sys.stderr)
1739
+ return 1
1740
+
1741
+ print(colored(f"✓ artifact_locale set to {args.locale} ({scope})", Colors.GREEN))
1742
+ return 0
1743
+
1744
+ print(colored("Error: use artifact-locale get|set", Colors.RED), file=sys.stderr)
1745
+ return 1
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for P1 observable defaults (verify seed, jsonl seed, dashboard summary)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
13
+
14
+ from common.artifact_locale import default_verify_content
15
+ from common.task_dashboard import format_verify_summary, render_task_dashboard
16
+ from common.task_store import _resolve_seed_spec_paths, _write_seed_jsonl
17
+
18
+
19
+ def test_default_verify_content_has_standard_sections() -> None:
20
+ content = default_verify_content(locale="en")
21
+ for heading in (
22
+ "## Planning check",
23
+ "## Execution evidence",
24
+ "### Validation commands",
25
+ "### Acceptance",
26
+ "### Durable learning",
27
+ ):
28
+ assert heading in content
29
+
30
+
31
+ def test_write_seed_jsonl_uses_real_spec_paths(tmp_path: Path) -> None:
32
+ repo_root = tmp_path / "repo"
33
+ spec_root = repo_root / ".cstl" / "spec" / "guides"
34
+ spec_root.mkdir(parents=True)
35
+ for name in ("index.md", "verification-strength-guide.md", "injection-budget-guide.md"):
36
+ (spec_root / name).write_text(f"# {name}\n", encoding="utf-8")
37
+
38
+ task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-seed"
39
+ task_dir.mkdir(parents=True)
40
+ task_data = {"id": "test-seed", "package": None, "scope": None, "children": []}
41
+
42
+ jsonl_path = task_dir / "implement.jsonl"
43
+ _write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
44
+
45
+ rows = [json.loads(line) for line in jsonl_path.read_text(encoding="utf-8").splitlines() if line.strip()]
46
+ assert len(rows) >= 2
47
+ assert all("file" in row and row["file"].startswith(".cstl/spec/") for row in rows)
48
+ assert all("_example" not in row for row in rows)
49
+ for row in rows:
50
+ assert (repo_root / row["file"]).is_file()
51
+
52
+
53
+ def test_resolve_seed_spec_paths_prefers_existing_guides(tmp_path: Path) -> None:
54
+ repo_root = tmp_path / "repo"
55
+ guides = repo_root / ".cstl" / "spec" / "guides"
56
+ guides.mkdir(parents=True)
57
+ (guides / "index.md").write_text("# index\n", encoding="utf-8")
58
+ (guides / "verification-strength-guide.md").write_text("# verify\n", encoding="utf-8")
59
+
60
+ task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-paths"
61
+ task_dir.mkdir(parents=True)
62
+ paths = _resolve_seed_spec_paths(repo_root, task_dir, {"children": []})
63
+ assert len(paths) >= 2
64
+ assert all((repo_root / rel).is_file() for rel in paths)
65
+
66
+
67
+ def test_format_verify_summary_missing_without_file(tmp_path: Path) -> None:
68
+ task_dir = tmp_path / "task"
69
+ task_dir.mkdir()
70
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: missing]"
71
+
72
+
73
+ def test_format_verify_summary_partial_from_signals(tmp_path: Path) -> None:
74
+ task_dir = tmp_path / "task"
75
+ task_dir.mkdir()
76
+ (task_dir / "verify.md").write_text(
77
+ "Validation commands: pytest -q — pass\n",
78
+ encoding="utf-8",
79
+ )
80
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: partial]"
81
+
82
+
83
+ def test_format_verify_summary_ok_when_all_signals(tmp_path: Path) -> None:
84
+ task_dir = tmp_path / "task"
85
+ task_dir.mkdir()
86
+ (task_dir / "verify.md").write_text(
87
+ "\n".join(
88
+ [
89
+ "Validation commands: pytest -q — pass",
90
+ "Check evidence: cstl-check — pass",
91
+ "Final acceptance evidence: all AC met",
92
+ "Durable learning decision: no durable learning",
93
+ "Reviewed change-set: git diff main..HEAD",
94
+ ]
95
+ ),
96
+ encoding="utf-8",
97
+ )
98
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: ok]"
99
+
100
+
101
+ def test_render_task_dashboard_includes_verify_summary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
102
+ repo_root = tmp_path / "repo"
103
+ tasks_dir = repo_root / ".cstl" / "tasks"
104
+ task_dir = tasks_dir / "08-07-dashboard-task"
105
+ task_dir.mkdir(parents=True)
106
+ (task_dir / "task.json").write_text(
107
+ json.dumps(
108
+ {
109
+ "id": "dashboard-task",
110
+ "name": "dashboard-task",
111
+ "title": "dashboard-task",
112
+ "status": "planning",
113
+ "assignee": "dev",
114
+ "children": [],
115
+ }
116
+ ),
117
+ encoding="utf-8",
118
+ )
119
+ (task_dir / "verify.md").write_text(
120
+ "Validation commands: echo ok — pass\n",
121
+ encoding="utf-8",
122
+ )
123
+
124
+ monkeypatch.chdir(repo_root)
125
+ monkeypatch.setattr("common.task_dashboard.get_repo_root", lambda: repo_root)
126
+ monkeypatch.setattr("common.task_dashboard.get_tasks_dir", lambda _root=None: tasks_dir)
127
+ monkeypatch.setattr("common.task_dashboard.get_developer", lambda _root=None: "dev")
128
+ monkeypatch.setattr(
129
+ "common.active_task.resolve_selected_task",
130
+ lambda *_args, **_kwargs: type("Sel", (), {"task_path": None, "source": None})(),
131
+ )
132
+
133
+ dashboard = render_task_dashboard(repo_root)
134
+ assert "[verify: partial]" in dashboard
135
+ assert "08-07-dashboard-task" in dashboard
@@ -251,36 +251,38 @@ verdict rules:
251
251
 
252
252
  ---
253
253
 
254
- --- PROBE P-10: Definition / reference (codegraph Agent LSP not exposed)
254
+ --- PROBE P-10: Definition / reference (Prefer Grep+Read; codegraph when ambiguous)
255
255
 
256
256
  Cursor Agent does **not** expose GO_TO_DEFINITION in the tool table (Native/BYOK).
257
- Trellis routes definition/reference to **codegraph**. This probe measures the
258
- **product path**, not raw LSP.
257
+ Trellis **Prefers** native **Grep → Read** for named-symbol definition; **codegraph**
258
+ is the **Own** fallback when Grep is ambiguous or structural context is required.
259
259
 
260
- You are a retrieval capability probe. Use **codegraph_node** or **codegraph_search**
261
- as the **primary** path. Do NOT use Grep as the first step. Use **Read** only to
262
- confirm line numbers after codegraph returns a definition.
260
+ You are a retrieval capability probe.
263
261
 
264
262
  Task: Locate the **definition** of `route_codebase_retrieval` (Python function).
265
263
 
264
+ **Step 1 (Prefer):** Use **Grep** for `def route_codebase_retrieval` — do NOT use codegraph first.
265
+ **Step 2:** Use **Read** to confirm the definition line and body start.
266
+ **Step 3 (only if Grep ambiguous):** codegraph_explore / codegraph_search.
267
+
266
268
  Expected: definition in `.cstl/scripts/common/codebase_retrieval_router.py`
267
- (`def route_codebase_retrieval(`).
269
+ (`def route_codebase_retrieval(` at line ~805).
268
270
 
269
271
  Reply in this exact format:
270
272
 
271
273
  ```
272
274
  P-10 RESULT
273
- tool_invoked: <exact tool name codegraph_node, codegraph_search, or "none">
275
+ tool_invoked: <primary tool — Grep, Read, codegraph_explore, or "none">
274
276
  definition_file: <path>
275
277
  definition_line: <line number or "unknown">
276
278
  verdict: pass|fail|environment_limitation
277
- notes: <if you tried GO_TO_DEFINITION and it was unavailable, say so; pass when codegraph + Read confirm def>
279
+ notes: <GO_TO_DEFINITION availability; did Grep+Read succeed before any codegraph?>
278
280
  ```
279
281
 
280
282
  verdict rules:
281
- - **pass**: codegraph (or equivalent MCP) returned the correct `def route_codebase_retrieval` file.
282
- - **environment_limitation**: only when codegraph MCP/index is missing; not when LSP is missing.
283
- - **fail**: wrong file or no structural tool executed.
283
+ - **pass**: Grep (+ Read) returned the correct `def route_codebase_retrieval` file; codegraph optional.
284
+ - **environment_limitation**: only when Grep/Read are unavailable in the host (not when LSP is missing).
285
+ - **fail**: wrong file, or codegraph used as first step without Grep attempt.
284
286
 
285
287
  ---
286
288
 
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI Layer 2 — PRIMARY (guaranteed) context path for cstl-* Task dispatch.
4
+
5
+ Usage:
6
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent research
7
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent implement --task .cstl/tasks/06-23-subagent-smoke
8
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent research --repo-root d:/MyHarness
9
+
10
+ Outputs the complete dispatch prompt to stdout. The main session MUST capture
11
+ this output and pass it as the `prompt` parameter to
12
+ `Task(subagent_type=cstl-*, prompt=<generated>)`. This is the ONLY
13
+ guaranteed context channel for custom Task subagents on Cursor.
14
+
15
+ Hook injection (`preToolUse` / `additional_context`) is best-effort only
16
+ (Cursor #158452; hook does not fire for Task tool in Cursor 3.8.22). Do not
17
+ treat hook-only injection as a substitute for this CLI path.
18
+
19
+ The generated prompt carries:
20
+ - Role identity ("You are the Trellis Research/Implement/Check Agent")
21
+ - Write scope constraints (Write ALLOWED / Write FORBIDDEN)
22
+ - Recursion guard
23
+ - Dynamic context (prd.md, design.md, implement.md, spec tree, jsonl files)
24
+
25
+ Task path resolution:
26
+ - `--task <path>` is the **primary fallback** when no selected-task pointer exists
27
+ (e.g. `task.py select` failed due to missing session identity).
28
+ - When `--task` is omitted, auto-detect via selected pointer is **best-effort** only —
29
+ dispatch scripts do not hard-depend on a persisted selection.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import sys
35
+ from pathlib import Path
36
+
37
+
38
+ def main() -> int:
39
+ parser = argparse.ArgumentParser(
40
+ description="Generate a complete Trellis subagent dispatch prompt."
41
+ )
42
+ parser.add_argument(
43
+ "--agent",
44
+ required=True,
45
+ choices=["research", "implement", "check"],
46
+ help="Subagent role to generate prompt for.",
47
+ )
48
+ parser.add_argument(
49
+ "--task",
50
+ default=None,
51
+ help="Task directory path (absolute or relative to repo root). "
52
+ "Primary fallback when no selected-task pointer exists. "
53
+ "If omitted, auto-detect via selected pointer is best-effort only.",
54
+ )
55
+ parser.add_argument(
56
+ "--repo-root",
57
+ default=None,
58
+ help="Repository root path. If omitted, auto-detect via .cstl or .git.",
59
+ )
60
+ parser.add_argument(
61
+ "--finish",
62
+ action="store_true",
63
+ help="Use finish context (check agent only, final pre-PR verification).",
64
+ )
65
+ parser.add_argument(
66
+ "--max-chars",
67
+ type=int,
68
+ default=None,
69
+ help="Truncate injected context at this character count. "
70
+ "When omitted, uses role default from injection-budget-guide.",
71
+ )
72
+ args = parser.parse_args()
73
+
74
+ # Resolve repo root
75
+ if args.repo_root:
76
+ repo_root = Path(args.repo_root).resolve()
77
+ else:
78
+ repo_root = _find_repo_root(Path.cwd())
79
+ if not repo_root:
80
+ print("ERROR: could not find repo root (no .cstl or .git upward)", file=sys.stderr)
81
+ return 1
82
+
83
+ from common.paths import DIR_WORKFLOW # type: ignore[import-not-found]
84
+
85
+ # Make scripts importable — workspace root first (subagent_dispatch.py lives there),
86
+ # then repo_root's own scripts (may be a subproject with different common/)
87
+ workspace_root = _find_workspace_root(repo_root, DIR_WORKFLOW)
88
+ if workspace_root and workspace_root != repo_root:
89
+ ws_scripts = workspace_root / DIR_WORKFLOW / "scripts"
90
+ if str(ws_scripts) not in sys.path:
91
+ sys.path.insert(0, str(ws_scripts))
92
+
93
+ scripts_dir = repo_root / DIR_WORKFLOW / "scripts"
94
+ if str(scripts_dir) not in sys.path:
95
+ sys.path.insert(1, str(scripts_dir))
96
+
97
+ from common.subagent_dispatch import build_dispatch_prompt # type: ignore[import-not-found]
98
+ from common.injection_budget import DISPATCH_DEFAULT_MAX_CHARS # type: ignore[import-not-found]
99
+
100
+ # Resolve task directory
101
+ if args.task:
102
+ task_dir = Path(args.task)
103
+ if not task_dir.is_absolute():
104
+ task_dir = repo_root / task_dir
105
+ task_dir = task_dir.resolve()
106
+ else:
107
+ task_dir = _resolve_selected_task(repo_root)
108
+ if not task_dir:
109
+ print(
110
+ "ERROR: no --task specified and no selected task found. "
111
+ "Run `python ./.cstl/scripts/task.py select <task>` first.",
112
+ file=sys.stderr,
113
+ )
114
+ return 1
115
+
116
+ if not task_dir.is_dir():
117
+ print(f"ERROR: task directory not found: {task_dir}", file=sys.stderr)
118
+ return 1
119
+
120
+ max_chars = args.max_chars
121
+ if max_chars is None:
122
+ max_chars = DISPATCH_DEFAULT_MAX_CHARS.get(args.agent)
123
+
124
+ prompt, warnings, errors = build_dispatch_prompt(
125
+ repo_root,
126
+ task_dir,
127
+ args.agent,
128
+ finish=args.finish,
129
+ max_chars=max_chars,
130
+ require_in_progress=False,
131
+ )
132
+
133
+ for w in warnings:
134
+ print(f"WARN: {w}", file=sys.stderr)
135
+ for e in errors:
136
+ print(f"ERROR: {e}", file=sys.stderr)
137
+
138
+ if not prompt:
139
+ print("ERROR: failed to generate prompt (see errors above)", file=sys.stderr)
140
+ return 1
141
+
142
+ # Output the prompt to stdout — the caller captures this
143
+ print(prompt)
144
+ return 0
145
+
146
+
147
+ def _find_repo_root(start: Path) -> Path | None:
148
+ current = start.resolve()
149
+ while current != current.parent:
150
+ if (current / ".cstl").is_dir() or (current / ".git").exists():
151
+ return current
152
+ current = current.parent
153
+ return None
154
+
155
+
156
+ def _find_workspace_root(start: Path, dir_workflow: str) -> Path | None:
157
+ """Find the outermost directory with workflow dir (workspace root, not subproject)."""
158
+ found = None
159
+ current = start.resolve()
160
+ while current != current.parent:
161
+ if (current / dir_workflow).is_dir():
162
+ found = current
163
+ current = current.parent
164
+ return found
165
+
166
+
167
+ def _resolve_selected_task(repo_root: Path) -> Path | None:
168
+ try:
169
+ from common.active_task import resolve_selected_task # type: ignore[import-not-found]
170
+ result = resolve_selected_task(repo_root, {}, platform="cursor")
171
+ if result.task_path:
172
+ task_path = Path(result.task_path)
173
+ if not task_path.is_absolute():
174
+ task_path = repo_root / task_path
175
+ return task_path
176
+ except Exception:
177
+ pass
178
+ return None
179
+
180
+
181
+ if __name__ == "__main__":
182
+ sys.exit(main())