@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
@@ -12,6 +12,7 @@ import re
12
12
  from datetime import datetime, timezone
13
13
  from pathlib import Path
14
14
 
15
+ from .io import read_json
15
16
  from .paths import get_repo_root
16
17
  from .task_gates import (
17
18
  build_spec_update_scaffold,
@@ -22,6 +23,7 @@ from .task_map import (
22
23
  PARENT_TERMINAL_STATES,
23
24
  get_child_entry,
24
25
  load_task_map,
26
+ resolve_child_map_id,
25
27
  validate_parent_child_integration,
26
28
  )
27
29
 
@@ -100,7 +102,7 @@ def _child_prompt_section(
100
102
  *,
101
103
  include_artifacts: bool,
102
104
  ) -> str:
103
- child_name = child_dir.name
105
+ child_name = child_entry.get("id") or child_dir.name
104
106
  parent_rel = _repo_rel(parent_dir, repo_root)
105
107
  child_rel = _repo_rel(child_dir, repo_root)
106
108
  state = child_entry.get("state", "open")
@@ -164,12 +166,15 @@ def build_child_prompt(
164
166
  """Build an implementation prompt for a linked child task."""
165
167
  errors: list[str] = []
166
168
  repo_root = get_repo_root()
167
- child_name = child_dir.name
168
169
 
169
170
  parent_json = parent_dir / "task.json"
170
171
  if not parent_json.is_file():
171
172
  return None, [f"parent task.json missing: {parent_dir}"]
172
173
 
174
+ parent_data = read_json(parent_json) or {}
175
+ child_data = read_json(child_dir / "task.json") or {}
176
+ child_name = resolve_child_map_id(parent_data, child_dir, child_data)
177
+
173
178
  data, body = load_task_map(parent_dir)
174
179
  if data is None:
175
180
  return None, ["parent task-map.md missing or invalid"]
@@ -289,6 +294,457 @@ def build_child_prompt(
289
294
  return "\n".join(lines), errors
290
295
 
291
296
 
297
+
298
+ def _children_by_id(data: dict) -> dict[str, dict]:
299
+ return {
300
+ item.get("id"): item
301
+ for item in data.get("children", [])
302
+ if isinstance(item, dict) and isinstance(item.get("id"), str)
303
+ }
304
+
305
+
306
+ def _normalize_stages(data: dict) -> tuple[list[dict], list[str], bool]:
307
+ """Return (stages, errors, used_legacy_default)."""
308
+ errors: list[str] = []
309
+ children = _children_by_id(data)
310
+ raw_stages = data.get("stages") or []
311
+ if not isinstance(raw_stages, list):
312
+ return [], ["task-map stages must be a list"], False
313
+
314
+ if not raw_stages:
315
+ units = list(children.keys())
316
+ return (
317
+ [{"id": "default", "title": "All children (no stages declared)", "units": units}],
318
+ [],
319
+ True,
320
+ )
321
+
322
+ stages: list[dict] = []
323
+ seen_units: set[str] = set()
324
+ for idx, stage in enumerate(raw_stages):
325
+ if not isinstance(stage, dict):
326
+ errors.append(f"stages[{idx}] must be a mapping")
327
+ continue
328
+ stage_id = stage.get("id")
329
+ if not isinstance(stage_id, str) or not stage_id.strip():
330
+ errors.append(f"stages[{idx}] missing id")
331
+ continue
332
+ units = stage.get("units") or []
333
+ if not isinstance(units, list):
334
+ errors.append(f"stage `{stage_id}` units must be a list")
335
+ continue
336
+ norm_units: list[str] = []
337
+ for unit in units:
338
+ if not isinstance(unit, str) or not unit.strip():
339
+ errors.append(f"stage `{stage_id}` has invalid unit entry")
340
+ continue
341
+ if unit not in children:
342
+ errors.append(f"stage `{stage_id}` unit `{unit}` not in children[]")
343
+ continue
344
+ if unit in seen_units:
345
+ errors.append(f"unit `{unit}` appears in multiple stages")
346
+ continue
347
+ seen_units.add(unit)
348
+ norm_units.append(unit)
349
+ stages.append(
350
+ {
351
+ "id": stage_id,
352
+ "title": stage.get("title") if isinstance(stage.get("title"), str) else "",
353
+ "units": norm_units,
354
+ }
355
+ )
356
+ return stages, errors, False
357
+
358
+
359
+ def _unit_readiness(
360
+ unit_id: str,
361
+ children_by_id: dict[str, dict],
362
+ ) -> tuple[str, list[str]]:
363
+ entry = children_by_id.get(unit_id) or {}
364
+ blocked = _unmet_dependencies(entry, children_by_id)
365
+ return ("ready" if not blocked else "blocked", blocked)
366
+
367
+
368
+ _PACK_GENERATED_RE = re.compile(r"^- Generated:\s*(.+)$", re.MULTILINE)
369
+ _PACK_DASH = r"[\u2014\-]"
370
+ _PACK_UNIT_LINE_RE = re.compile(
371
+ rf"^- `([^`]+)` {_PACK_DASH} `([^`]+)` {_PACK_DASH} \*\*(ready|blocked)\*\*",
372
+ re.MULTILINE,
373
+ )
374
+
375
+
376
+ def _parse_iso_timestamp(raw: str) -> datetime | None:
377
+ text = raw.strip()
378
+ if not text:
379
+ return None
380
+ normalized = text[:-1] + "+00:00" if text.endswith("Z") else text
381
+ try:
382
+ parsed = datetime.fromisoformat(normalized)
383
+ except ValueError:
384
+ try:
385
+ parsed = datetime.strptime(text, "%Y-%m-%d")
386
+ except ValueError:
387
+ return None
388
+ if parsed.tzinfo is None:
389
+ return parsed.replace(tzinfo=timezone.utc)
390
+ return parsed
391
+
392
+
393
+ def _parse_pack_generated(pack_text: str) -> datetime | None:
394
+ match = _PACK_GENERATED_RE.search(pack_text)
395
+ if not match:
396
+ return None
397
+ return _parse_iso_timestamp(match.group(1))
398
+
399
+
400
+ def _last_integrate_event_time(body: str) -> datetime | None:
401
+ last: datetime | None = None
402
+ for line in body.splitlines():
403
+ stripped = line.strip()
404
+ if not stripped.startswith("- "):
405
+ continue
406
+ remainder = stripped[2:]
407
+ parts = remainder.split(" - ", 1)
408
+ if len(parts) != 2:
409
+ continue
410
+ ts_raw, message = parts[0].strip(), parts[1]
411
+ lowered = message.lower()
412
+ if "integration state" not in message and "integrate-through" not in lowered:
413
+ continue
414
+ parsed = _parse_iso_timestamp(ts_raw)
415
+ if parsed is not None and (last is None or parsed > last):
416
+ last = parsed
417
+ return last
418
+
419
+
420
+ def _pack_ready_units(pack_text: str) -> set[str]:
421
+ ready: set[str] = set()
422
+ for unit_id, _state, readiness in _PACK_UNIT_LINE_RE.findall(pack_text):
423
+ if readiness == "ready":
424
+ ready.add(unit_id)
425
+ return ready
426
+
427
+
428
+ def _expected_pack_ready_units(
429
+ data: dict,
430
+ children_by_id: dict[str, dict],
431
+ ) -> set[str]:
432
+ stages, _, _ = _normalize_stages(data)
433
+ ready: set[str] = set()
434
+ for stage in stages:
435
+ for unit in stage.get("units") or []:
436
+ if not isinstance(unit, str):
437
+ continue
438
+ readiness, _ = _unit_readiness(unit, children_by_id)
439
+ if readiness == "ready":
440
+ ready.add(unit)
441
+ return ready
442
+
443
+
444
+ def _pack_unit_states_mismatch(
445
+ pack_text: str,
446
+ children_by_id: dict[str, dict],
447
+ ) -> bool:
448
+ for unit_id, pack_state, _readiness in _PACK_UNIT_LINE_RE.findall(pack_text):
449
+ entry = children_by_id.get(unit_id) or {}
450
+ current_state = entry.get("state", "?")
451
+ if str(current_state) != pack_state:
452
+ return True
453
+ return False
454
+
455
+
456
+ def _compute_stale_pack(
457
+ parent_dir: Path,
458
+ data: dict,
459
+ body: str,
460
+ children_by_id: dict[str, dict],
461
+ ) -> bool | None:
462
+ pack_path = parent_dir / "child-prompts" / "PACK.md"
463
+ if not pack_path.is_file():
464
+ return None
465
+ try:
466
+ pack_text = pack_path.read_text(encoding="utf-8")
467
+ except OSError:
468
+ return None
469
+
470
+ generated = _parse_pack_generated(pack_text)
471
+ if generated is None:
472
+ return None
473
+
474
+ stale = False
475
+ last_integrate = _last_integrate_event_time(body)
476
+ if last_integrate is not None and generated < last_integrate:
477
+ stale = True
478
+ if _pack_unit_states_mismatch(pack_text, children_by_id):
479
+ stale = True
480
+ expected_ready = _expected_pack_ready_units(data, children_by_id)
481
+ if _pack_ready_units(pack_text) != expected_ready:
482
+ stale = True
483
+ return stale
484
+
485
+
486
+ def _compute_newly_ready(children_by_id: dict[str, dict]) -> list[str]:
487
+ ready_ids: list[str] = []
488
+ for unit_id in sorted(children_by_id):
489
+ entry = children_by_id[unit_id]
490
+ if entry.get("state") != "open":
491
+ continue
492
+ readiness, _ = _unit_readiness(unit_id, children_by_id)
493
+ if readiness == "ready":
494
+ ready_ids.append(unit_id)
495
+ return ready_ids
496
+
497
+
498
+ def _child_prompt_filename(unit_id: str) -> str:
499
+ """Flat filename for child-prompts (nested slash unit ids use `--`)."""
500
+ return f"{unit_id.replace('/', '--')}.md"
501
+
502
+
503
+ def build_publish_pack(
504
+ parent_dir: Path,
505
+ *,
506
+ stage_id: str | None = None,
507
+ mode: str = "inline",
508
+ dry_run: bool = False,
509
+ ) -> tuple[str | None, list[str]]:
510
+ """Build campaign PACK index + per-unit prompts under child-prompts/."""
511
+ errors: list[str] = []
512
+ repo_root = get_repo_root()
513
+ parent_rel = _repo_rel(parent_dir, repo_root)
514
+
515
+ if not (parent_dir / "task.json").is_file():
516
+ return None, [f"parent task.json missing: {parent_dir}"]
517
+
518
+ data, _ = load_task_map(parent_dir)
519
+ if data is None:
520
+ return None, ["parent task-map.md missing or invalid"]
521
+
522
+ stages, stage_errors, legacy = _normalize_stages(data)
523
+ errors.extend(stage_errors)
524
+ if errors:
525
+ return None, errors
526
+
527
+ if stage_id:
528
+ stages = [s for s in stages if s.get("id") == stage_id]
529
+ if not stages:
530
+ return None, [f"unknown stage id: {stage_id}"]
531
+
532
+ children_by_id = _children_by_id(data)
533
+ prompts_dir = parent_dir / "child-prompts"
534
+ ready_rows: list[tuple[str, str, str]] = []
535
+ blocked_rows: list[tuple[str, str, list[str]]] = []
536
+
537
+ pack_lines = [
538
+ f"# Campaign PACK — `{parent_dir.name}`",
539
+ "",
540
+ f"- Parent: `{parent_rel}`",
541
+ f"- Generated: {_utc_now()}",
542
+ f"- Mode: `{mode}`",
543
+ "- Path rule: every worker must `task.py select <explicit-child-path>`; "
544
+ "do not rely on selected_task inheritance across windows.",
545
+ "",
546
+ "## Manual window path (PACK)",
547
+ "",
548
+ "1. Run `publish-pack` (this file) on the Parent.",
549
+ "2. For each **ready** unit: IDE **New Chat** or Agents Window → paste `child-prompts/<child>.md`.",
550
+ "3. In that session run `python ./.cstl/scripts/task.py select <child-path>`.",
551
+ "4. Parent retains `review-child` / `integrate-child`. PACK is not RUN auto-dispatch.",
552
+ "",
553
+ "## Stages",
554
+ "",
555
+ ]
556
+ if legacy:
557
+ pack_lines.append(
558
+ "> Warning: no `stages:` in task-map; using implicit `default` stage "
559
+ "with all children. Declare `stages:` for HYBRID campaign packs."
560
+ )
561
+ pack_lines.append("")
562
+
563
+ written: list[str] = []
564
+ for stage in stages:
565
+ sid = stage["id"]
566
+ title = stage.get("title") or ""
567
+ heading = f"### `{sid}`" + (f" — {title}" if title else "")
568
+ pack_lines.append(heading)
569
+ pack_lines.append("")
570
+ for unit in stage.get("units") or []:
571
+ readiness, blocked = _unit_readiness(unit, children_by_id)
572
+ child_dir = parent_dir.parent / unit
573
+ child_rel = _repo_rel(child_dir, repo_root)
574
+ state = (children_by_id.get(unit) or {}).get("state", "?")
575
+ if readiness == "ready":
576
+ prompt, prompt_errors = build_child_prompt(
577
+ parent_dir,
578
+ child_dir,
579
+ include_artifacts=False,
580
+ mode=mode,
581
+ )
582
+ if prompt_errors or not prompt:
583
+ errors.extend(prompt_errors or [f"could not build prompt for `{unit}`"])
584
+ continue
585
+ out_name = _child_prompt_filename(unit)
586
+ out_rel = f"{parent_rel}/child-prompts/{out_name}"
587
+ if not dry_run:
588
+ prompts_dir.mkdir(parents=True, exist_ok=True)
589
+ (prompts_dir / out_name).write_text(prompt + "\n", encoding="utf-8")
590
+ written.append(out_rel)
591
+ ready_rows.append((sid, unit, out_rel))
592
+ pack_lines.append(
593
+ f"- `{unit}` — `{state}` — **ready** — prompt: `child-prompts/{out_name}` "
594
+ f"(select `{child_rel}`)"
595
+ )
596
+ else:
597
+ blocked_rows.append((sid, unit, blocked))
598
+ stub = "\n".join(
599
+ [
600
+ f"# BLOCKED — `{unit}`",
601
+ "",
602
+ f"- Parent: `{parent_rel}`",
603
+ f"- Child: `{child_rel}`",
604
+ f"- Stage: `{sid}`",
605
+ "- Do not start implementation until dependencies reach "
606
+ "`integrated` or `cancelled`.",
607
+ "",
608
+ "Unmet dependencies:",
609
+ *[f"- {item}" for item in blocked],
610
+ "",
611
+ ]
612
+ )
613
+ out_name = _child_prompt_filename(unit)
614
+ out_rel = f"{parent_rel}/child-prompts/{out_name}"
615
+ if not dry_run:
616
+ prompts_dir.mkdir(parents=True, exist_ok=True)
617
+ (prompts_dir / out_name).write_text(stub, encoding="utf-8")
618
+ written.append(out_rel)
619
+ pack_lines.append(
620
+ f"- `{unit}` — `{state}` — **blocked** — {', '.join(blocked)}"
621
+ )
622
+ pack_lines.append("")
623
+
624
+ pack_lines.extend(["## Ready", ""])
625
+ if ready_rows:
626
+ for sid, unit, path in ready_rows:
627
+ pack_lines.append(f"- `{sid}` / `{unit}` → `{path}`")
628
+ else:
629
+ pack_lines.append("(none)")
630
+ pack_lines.extend(["", "## Blocked", ""])
631
+ if blocked_rows:
632
+ for sid, unit, deps in blocked_rows:
633
+ pack_lines.append(f"- `{sid}` / `{unit}` — {', '.join(deps)}")
634
+ else:
635
+ pack_lines.append("(none)")
636
+ pack_lines.append("")
637
+
638
+ pack_body = "\n".join(pack_lines)
639
+ pack_rel = f"{parent_rel}/child-prompts/PACK.md"
640
+ if not dry_run:
641
+ prompts_dir.mkdir(parents=True, exist_ok=True)
642
+ (prompts_dir / "PACK.md").write_text(pack_body + "\n", encoding="utf-8")
643
+ written.append(pack_rel)
644
+
645
+ if errors:
646
+ return None, errors
647
+
648
+ summary = [
649
+ f"# publish-pack — `{parent_dir.name}`",
650
+ "",
651
+ f"- Parent: `{parent_rel}`",
652
+ f"- dry_run: {dry_run}",
653
+ f"- legacy_no_stages: {legacy}",
654
+ f"- ready: {len(ready_rows)}",
655
+ f"- blocked: {len(blocked_rows)}",
656
+ f"- pack_index: `{pack_rel}`",
657
+ "",
658
+ ]
659
+ if written:
660
+ summary.append("## Written")
661
+ summary.append("")
662
+ for path in written:
663
+ summary.append(f"- `{path}`")
664
+ summary.append("")
665
+ elif dry_run:
666
+ summary.append("(dry-run: no files written)")
667
+ summary.append("")
668
+ summary.append(pack_body)
669
+ return "\n".join(summary), []
670
+
671
+
672
+ def build_parent_status_dict(parent_dir: Path) -> dict | None:
673
+ """Machine-readable parent orchestration snapshot (campaign UI / MCP)."""
674
+ repo_root = get_repo_root()
675
+ parent_rel = _repo_rel(parent_dir, repo_root)
676
+ data, _ = load_task_map(parent_dir)
677
+ if data is None:
678
+ return None
679
+
680
+ children_by_id = _children_by_id(data)
681
+ stages, stage_errors, legacy = _normalize_stages(data)
682
+ _, body = load_task_map(parent_dir)
683
+ stale_pack = _compute_stale_pack(parent_dir, data, body, children_by_id)
684
+ newly_ready = _compute_newly_ready(children_by_id)
685
+
686
+ stage_rows: list[dict] = []
687
+ for stage in stages:
688
+ units: list[dict] = []
689
+ for unit in stage.get("units") or []:
690
+ entry = children_by_id.get(unit) or {}
691
+ readiness, blocked = _unit_readiness(unit, children_by_id)
692
+ units.append(
693
+ {
694
+ "id": unit,
695
+ "state": entry.get("state", "?"),
696
+ "readiness": readiness,
697
+ "blockedBy": blocked,
698
+ }
699
+ )
700
+ stage_rows.append(
701
+ {
702
+ "id": stage["id"],
703
+ "title": stage.get("title") or "",
704
+ "units": units,
705
+ }
706
+ )
707
+
708
+ children_rows: list[dict] = []
709
+ for child in data.get("children") or []:
710
+ if not isinstance(child, dict):
711
+ continue
712
+ cid = child.get("id")
713
+ if not isinstance(cid, str):
714
+ continue
715
+ child_dir = parent_dir.parent / cid
716
+ children_rows.append(
717
+ {
718
+ "id": cid,
719
+ "state": child.get("state", "?"),
720
+ "dependsOn": child.get("depends_on") or [],
721
+ "touches": child.get("touches") or [],
722
+ "isolation": child.get("isolation"),
723
+ "branch": child.get("branch"),
724
+ "worktreePath": child.get("worktree_path"),
725
+ "evidence": child.get("evidence"),
726
+ "ref": child.get("ref"),
727
+ "verifyMd": (child_dir / "verify.md").is_file(),
728
+ "handoffMd": (child_dir / "handoff.md").is_file(),
729
+ }
730
+ )
731
+
732
+ return {
733
+ "id": parent_dir.name,
734
+ "path": parent_rel,
735
+ "contractEpoch": data.get("contract_epoch"),
736
+ "executionTopology": data.get("execution_topology"),
737
+ "mergeLimit": data.get("merge_limit"),
738
+ "stages": stage_rows,
739
+ "children": children_rows,
740
+ "integrationQueue": data.get("integration_queue") or [],
741
+ "stageErrors": stage_errors,
742
+ "legacyStages": legacy,
743
+ "stalePack": stale_pack,
744
+ "newlyReady": newly_ready,
745
+ }
746
+
747
+
292
748
  def build_parent_status(parent_dir: Path) -> str:
293
749
  """Render a parent task-map status summary for the reviewer."""
294
750
  repo_root = get_repo_root()
@@ -305,10 +761,48 @@ def build_parent_status(parent_dir: Path) -> str:
305
761
  f"- execution_topology: {data.get('execution_topology', '?')}",
306
762
  f"- merge_limit: {data.get('merge_limit', '?')}",
307
763
  "",
308
- "## Children",
309
- "",
310
764
  ]
311
765
 
766
+ children_by_id = _children_by_id(data)
767
+ stages, stage_errors, legacy = _normalize_stages(data)
768
+ _, body = load_task_map(parent_dir)
769
+ stale_pack = _compute_stale_pack(parent_dir, data, body, children_by_id)
770
+ newly_ready = _compute_newly_ready(children_by_id)
771
+ lines.append("## Pack freshness")
772
+ lines.append("")
773
+ lines.append(f"- stalePack: {stale_pack}")
774
+ if newly_ready:
775
+ lines.append(f"- newlyReady: {', '.join(newly_ready)}")
776
+ else:
777
+ lines.append("- newlyReady: (none)")
778
+ lines.append("")
779
+ lines.append("## Stages")
780
+ lines.append("")
781
+ if stage_errors:
782
+ for err in stage_errors:
783
+ lines.append(f"- error: {err}")
784
+ lines.append("")
785
+ elif legacy:
786
+ lines.append("stages: (none declared — implicit `default` for publish-pack)")
787
+ lines.append("")
788
+ else:
789
+ for stage in stages:
790
+ sid = stage["id"]
791
+ title = stage.get("title") or ""
792
+ heading = f"### `{sid}`" + (f" — {title}" if title else "")
793
+ lines.append(heading)
794
+ for unit in stage.get("units") or []:
795
+ entry = children_by_id.get(unit) or {}
796
+ state = entry.get("state", "?")
797
+ readiness, blocked = _unit_readiness(unit, children_by_id)
798
+ if readiness == "ready":
799
+ lines.append(f"- `{unit}` — `{state}` — ready")
800
+ else:
801
+ lines.append(f"- `{unit}` — `{state}` — blocked ({', '.join(blocked)})")
802
+ lines.append("")
803
+
804
+ lines.extend(["## Children", ""])
805
+
312
806
  children = data.get("children") or []
313
807
  if not children:
314
808
  lines.append("(no children in task-map)")
@@ -354,6 +848,7 @@ def build_parent_status(parent_dir: Path) -> str:
354
848
  lines.append("")
355
849
  lines.append("```bash")
356
850
  lines.append(f"python ./.cstl/scripts/task.py parent-status {parent_rel}")
851
+ lines.append(f"python ./.cstl/scripts/task.py publish-pack {parent_rel}")
357
852
  lines.append(f"python ./.cstl/scripts/task.py generate-child-prompt {parent_rel} <child> --mode subagent")
358
853
  lines.append(f"python ./.cstl/scripts/task.py generate-child-prompt {parent_rel} <child> --mode inline")
359
854
  lines.append(f"python ./.cstl/scripts/task.py review-child {parent_rel} <child> --check")
@@ -451,7 +946,7 @@ def build_review_report(
451
946
  notes: str | None,
452
947
  ) -> tuple[str, list[str], dict]:
453
948
  """Build review summary and validate integration transitions for a decision."""
454
- child_name = child_dir.name
949
+ child_name = resolve_child_map_id(parent_data, child_dir, child_data)
455
950
  summary = _summarize_handoff(child_dir)
456
951
  errors: list[str] = []
457
952
  actions: dict = {"integrate": None, "gates": []}
@@ -138,10 +138,15 @@ def _cursor_step_for_route(
138
138
  return (
139
139
  f"使用 **codegraph_search**,路径侧重 `extensions/`,符号 `{symbol}`。"
140
140
  )
141
+ if route_id == "definition-jump-native":
142
+ return (
143
+ f"**Prefer 原生**:用 **Grep** 定位 `{symbol}` 的定义行,再用 **Read** 验证文件与行号 "
144
+ "(Agent 无 GO_TO_DEFINITION)。仅当跨包 trap、重载歧义或需 blast 时再上 codegraph。"
145
+ )
141
146
  if route_id == "lsp-navigation":
142
147
  return (
143
- f"使用 **codegraph_node**(`includeCode=true`)或 **codegraph_search** 定位 `{symbol}` "
144
- "的定义/引用(Agent GO_TO_DEFINITION);再用 **Read** 验证行号与正文。"
148
+ f"**Own/codegraph**:用 **codegraph_explore** / **codegraph_search** 定位 `{symbol}` "
149
+ "的定义/引用(Grep+Read 不足或计划含 trap/blast 时);再用 **Read** 验证行号与正文。"
145
150
  )
146
151
  if role == "ast" or route_id in ("ast-codegraph",):
147
152
  return (
@@ -294,7 +299,8 @@ def render_agent_instructions(
294
299
 
295
300
  lines.append("")
296
301
  lines.append(
297
- "**codegraph 独用场景**:调用链、跨包 trap、extension 符号、影响面、定义/引用定位;"
302
+ "**Prefer / Adapt / Own**:定义跳转 Prefer **Grep → Read**;诊断 **ReadLints**;"
303
+ "codegraph 独用调用链、跨包 trap、extension 符号、影响面,以及 Grep+Read 歧义时的定义/引用;"
298
304
  "纯字面搜索用 Grep(勿用 GO_TO_DEFINITION,Agent 未暴露)。"
299
305
  )
300
306
 
@@ -20,7 +20,9 @@ import subprocess
20
20
  from pathlib import Path
21
21
 
22
22
  from .active_task import resolve_context_key
23
- from .config import get_git_packages
23
+ from .artifact_locale import artifact_locale_summary, resolve_artifact_locale
24
+ from .config import get_git_packages, get_journal_snippet_enabled
25
+ from .session_memory import get_latest_journal_summary
24
26
  from .git import run_git
25
27
  from .packages_context import get_packages_section
26
28
  from .tasks import iter_active_tasks, load_task, get_all_statuses, children_progress
@@ -46,7 +48,7 @@ from .paths import (
46
48
  # =============================================================================
47
49
 
48
50
  _PACKAGE_NAME = "@blxzer/cursor-trellis"
49
- _PYTHON_CMD = "python3"
51
+ _PYTHON_CMD = "python"
50
52
  _UPDATE_CHECK_TIMEOUT_SECONDS = 1.0
51
53
  _VERSION_RE = re.compile(
52
54
  r"^\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?\s*$"
@@ -797,6 +799,10 @@ def get_context_json(repo_root: Path | None = None) -> dict:
797
799
 
798
800
  result = {
799
801
  "developer": developer or "",
802
+ "artifactLocale": resolve_artifact_locale(
803
+ repo_root / selected_task if selected_task else None,
804
+ repo_root,
805
+ ),
800
806
  "git": {
801
807
  "isRepo": root_git_info["isRepo"],
802
808
  "branch": root_git_info["branch"],
@@ -819,9 +825,26 @@ def get_context_json(repo_root: Path | None = None) -> dict:
819
825
  if pkg_git_info:
820
826
  result["packageGit"] = pkg_git_info
821
827
 
828
+ if get_journal_snippet_enabled(repo_root):
829
+ result["journal"]["snippet"] = _resolve_journal_snippet(
830
+ repo_root,
831
+ journal_file,
832
+ developer,
833
+ )
834
+
822
835
  return result
823
836
 
824
837
 
838
+ def _resolve_journal_snippet(
839
+ repo_root: Path,
840
+ journal_file: Path | None,
841
+ developer: str | None,
842
+ ) -> str:
843
+ if not journal_file or not developer:
844
+ return ""
845
+ return get_latest_journal_summary(journal_file, repo_root, developer)
846
+
847
+
825
848
  def output_json(repo_root: Path | None = None) -> None:
826
849
  """Output context in JSON format.
827
850
 
@@ -860,11 +883,12 @@ def get_context_text(repo_root: Path | None = None) -> str:
860
883
  lines.append("## DEVELOPER")
861
884
  if not developer:
862
885
  lines.append(
863
- f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
886
+ f"ERROR: Not initialized. Run: python ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
864
887
  )
865
888
  return "\n".join(lines)
866
889
 
867
890
  lines.append(f"Name: {developer}")
891
+ lines.append(artifact_locale_summary(repo_root))
868
892
  lines.append("")
869
893
 
870
894
  root_git_info = _collect_root_git_info(repo_root)
@@ -966,6 +990,10 @@ def get_context_text(repo_root: Path | None = None) -> str:
966
990
  lines.append(f"Line count: {journal_lines} / 2000")
967
991
  if journal_lines > 1800:
968
992
  lines.append("[!] WARNING: Approaching 2000 line limit!")
993
+ if get_journal_snippet_enabled(repo_root):
994
+ snippet = _resolve_journal_snippet(repo_root, journal_file, developer)
995
+ if snippet:
996
+ lines.append(f"Recent snippet: {snippet}")
969
997
  else:
970
998
  lines.append("No journal file found")
971
999
  lines.append("")
@@ -1085,7 +1113,7 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
1085
1113
  developer = get_developer(repo_root)
1086
1114
  if not developer:
1087
1115
  lines.append(
1088
- f"ERROR: Not initialized. Run: python3 ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
1116
+ f"ERROR: Not initialized. Run: python ./{DIR_WORKFLOW}/{DIR_SCRIPTS}/init_developer.py <name>"
1089
1117
  )
1090
1118
  return "\n".join(lines)
1091
1119