@blxzer/cursor-trellis 0.3.5 → 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 (130) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +13 -0
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +3 -0
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/goal/index.d.ts +3 -0
  7. package/dist/commands/goal/index.d.ts.map +1 -0
  8. package/dist/commands/goal/index.js +159 -0
  9. package/dist/commands/goal/index.js.map +1 -0
  10. package/dist/commands/goal/review.d.ts +6 -0
  11. package/dist/commands/goal/review.d.ts.map +1 -0
  12. package/dist/commands/goal/review.js +28 -0
  13. package/dist/commands/goal/review.js.map +1 -0
  14. package/dist/commands/sdk/run.d.ts.map +1 -1
  15. package/dist/commands/sdk/run.js +2 -27
  16. package/dist/commands/sdk/run.js.map +1 -1
  17. package/dist/configurators/workflow.d.ts.map +1 -1
  18. package/dist/configurators/workflow.js +13 -1
  19. package/dist/configurators/workflow.js.map +1 -1
  20. package/dist/goal/action-packet.d.ts +89 -0
  21. package/dist/goal/action-packet.d.ts.map +1 -0
  22. package/dist/goal/action-packet.js +113 -0
  23. package/dist/goal/action-packet.js.map +1 -0
  24. package/dist/goal/audit.d.ts +14 -0
  25. package/dist/goal/audit.d.ts.map +1 -0
  26. package/dist/goal/audit.js +12 -0
  27. package/dist/goal/audit.js.map +1 -0
  28. package/dist/goal/constants.d.ts +26 -0
  29. package/dist/goal/constants.d.ts.map +1 -0
  30. package/dist/goal/constants.js +24 -0
  31. package/dist/goal/constants.js.map +1 -0
  32. package/dist/goal/paths.d.ts +8 -0
  33. package/dist/goal/paths.d.ts.map +1 -0
  34. package/dist/goal/paths.js +36 -0
  35. package/dist/goal/paths.js.map +1 -0
  36. package/dist/goal/preflight.d.ts +21 -0
  37. package/dist/goal/preflight.d.ts.map +1 -0
  38. package/dist/goal/preflight.js +97 -0
  39. package/dist/goal/preflight.js.map +1 -0
  40. package/dist/goal/review-seam.d.ts +44 -0
  41. package/dist/goal/review-seam.d.ts.map +1 -0
  42. package/dist/goal/review-seam.js +153 -0
  43. package/dist/goal/review-seam.js.map +1 -0
  44. package/dist/goal/reviewer-rules.d.ts +13 -0
  45. package/dist/goal/reviewer-rules.d.ts.map +1 -0
  46. package/dist/goal/reviewer-rules.js +153 -0
  47. package/dist/goal/reviewer-rules.js.map +1 -0
  48. package/dist/goal/reviewer.d.ts +9 -0
  49. package/dist/goal/reviewer.d.ts.map +1 -0
  50. package/dist/goal/reviewer.js +54 -0
  51. package/dist/goal/reviewer.js.map +1 -0
  52. package/dist/goal/runtime.d.ts +32 -0
  53. package/dist/goal/runtime.d.ts.map +1 -0
  54. package/dist/goal/runtime.js +242 -0
  55. package/dist/goal/runtime.js.map +1 -0
  56. package/dist/goal/sdk-client.d.ts +10 -0
  57. package/dist/goal/sdk-client.d.ts.map +1 -0
  58. package/dist/goal/sdk-client.js +34 -0
  59. package/dist/goal/sdk-client.js.map +1 -0
  60. package/dist/goal/state.d.ts +40 -0
  61. package/dist/goal/state.d.ts.map +1 -0
  62. package/dist/goal/state.js +37 -0
  63. package/dist/goal/state.js.map +1 -0
  64. package/dist/goal/task-triggers.d.ts +15 -0
  65. package/dist/goal/task-triggers.d.ts.map +1 -0
  66. package/dist/goal/task-triggers.js +23 -0
  67. package/dist/goal/task-triggers.js.map +1 -0
  68. package/dist/goal/walls.d.ts +12 -0
  69. package/dist/goal/walls.d.ts.map +1 -0
  70. package/dist/goal/walls.js +65 -0
  71. package/dist/goal/walls.js.map +1 -0
  72. package/dist/goal/worker-prompt.d.ts +13 -0
  73. package/dist/goal/worker-prompt.d.ts.map +1 -0
  74. package/dist/goal/worker-prompt.js +41 -0
  75. package/dist/goal/worker-prompt.js.map +1 -0
  76. package/dist/goal/worker.d.ts +42 -0
  77. package/dist/goal/worker.d.ts.map +1 -0
  78. package/dist/goal/worker.js +180 -0
  79. package/dist/goal/worker.js.map +1 -0
  80. package/dist/migrations/manifests/0.3.5.json +9 -0
  81. package/dist/migrations/manifests/0.3.6.json +9 -0
  82. package/dist/templates/common/commands/continue.md +15 -3
  83. package/dist/templates/common/skills/check.md +7 -4
  84. package/dist/templates/cursor/agents/cstl-check.md +1 -0
  85. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  86. package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
  87. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  88. package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
  89. package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
  90. package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
  91. package/dist/templates/markdown/index.d.ts +3 -0
  92. package/dist/templates/markdown/index.d.ts.map +1 -1
  93. package/dist/templates/markdown/index.js +3 -0
  94. package/dist/templates/markdown/index.js.map +1 -1
  95. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
  96. package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
  97. package/dist/templates/markdown/spec/guides/index.md.txt +107 -104
  98. package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
  99. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +24 -6
  100. package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +184 -0
  101. package/dist/templates/shared-hooks/session-start.py +10 -0
  102. package/dist/templates/trellis/config.yaml +18 -0
  103. package/dist/templates/trellis/index.d.ts +5 -0
  104. package/dist/templates/trellis/index.d.ts.map +1 -1
  105. package/dist/templates/trellis/index.js +14 -0
  106. package/dist/templates/trellis/index.js.map +1 -1
  107. package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
  108. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +37 -2
  109. package/dist/templates/trellis/scripts/common/config.py +39 -0
  110. package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
  111. package/dist/templates/trellis/scripts/common/parent_orchestration.py +161 -5
  112. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
  113. package/dist/templates/trellis/scripts/common/session_context.py +32 -4
  114. package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
  115. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
  116. package/dist/templates/trellis/scripts/common/task_context.py +18 -0
  117. package/dist/templates/trellis/scripts/common/task_dashboard.py +16 -1
  118. package/dist/templates/trellis/scripts/common/task_map.py +40 -8
  119. package/dist/templates/trellis/scripts/common/task_store.py +144 -39
  120. package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
  121. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
  122. package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
  123. package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
  124. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
  125. package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
  126. package/dist/templates/trellis/scripts/task.py +19 -0
  127. package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
  128. package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
  129. package/dist/templates/trellis/workflow.md +23 -3
  130. package/package.json +2 -2
@@ -20,6 +20,7 @@ from .paths import DIR_WORKFLOW, DIR_WORKSPACE, FILE_JOURNAL_PREFIX, get_develop
20
20
 
21
21
 
22
22
  SECTION_NAMES = {"summary", "main changes", "git commits", "testing", "status", "next steps"}
23
+ JOURNAL_SNIPPET_MAX_TOKENS = 150
23
24
  FIELD_WEIGHTS = {
24
25
  "title": 8,
25
26
  "task": 8,
@@ -337,6 +338,31 @@ def normalize_space(value: str) -> str:
337
338
  return re.sub(r"\s+", " ", value).strip()
338
339
 
339
340
 
341
+ def truncate_to_token_budget(text: str, max_tokens: int = JOURNAL_SNIPPET_MAX_TOKENS) -> str:
342
+ """Collapse whitespace and hard-cap by whitespace-split token count."""
343
+ normalized = normalize_space(text)
344
+ if not normalized:
345
+ return ""
346
+ tokens = normalized.split()
347
+ if len(tokens) <= max_tokens:
348
+ return normalized
349
+ return " ".join(tokens[:max_tokens])
350
+
351
+
352
+ def get_latest_journal_summary(
353
+ journal_path: Path,
354
+ repo_root: Path,
355
+ developer: str,
356
+ max_tokens: int = JOURNAL_SNIPPET_MAX_TOKENS,
357
+ ) -> str:
358
+ """Return the latest session summary from one journal file (≤ max_tokens)."""
359
+ entries = parse_journal(journal_path, repo_root, developer)
360
+ if not entries:
361
+ return ""
362
+ summary = entries[-1].sections.get("summary", "")
363
+ return truncate_to_token_budget(summary, max_tokens)
364
+
365
+
340
366
  def to_repo_path(path: Path, repo_root: Path) -> str:
341
367
  try:
342
368
  return path.resolve().relative_to(repo_root.resolve()).as_posix()
@@ -12,6 +12,11 @@ import sys
12
12
  from pathlib import Path
13
13
  from typing import Literal
14
14
 
15
+ from .injection_budget import (
16
+ JSONL_MAX_ENTRIES,
17
+ JSONL_MAX_FILE_CHARS,
18
+ JSONL_MAX_TOTAL_CHARS,
19
+ )
15
20
  from .io import read_json
16
21
  from .paths import FILE_TASK_JSON
17
22
 
@@ -87,7 +92,8 @@ def read_jsonl_entries(
87
92
  jsonl_path: str,
88
93
  *,
89
94
  warn_prefix: str = "subagent-dispatch",
90
- ) -> list[tuple[str, str]]:
95
+ apply_budget: bool = True,
96
+ ) -> tuple[list[tuple[str, str]], list[str]]:
91
97
  full_path = os.path.join(base_path, jsonl_path)
92
98
  if not os.path.exists(full_path):
93
99
  print(
@@ -95,10 +101,13 @@ def read_jsonl_entries(
95
101
  "sub-agent will receive only task artifacts",
96
102
  file=sys.stderr,
97
103
  )
98
- return []
104
+ return [], []
99
105
 
100
106
  results: list[tuple[str, str]] = []
107
+ skipped: list[str] = []
101
108
  saw_real_entry = False
109
+ loaded_entries = 0
110
+ total_chars = 0
102
111
  try:
103
112
  with open(full_path, "r", encoding="utf-8") as f:
104
113
  for line in f:
@@ -112,12 +121,38 @@ def read_jsonl_entries(
112
121
  if not file_path:
113
122
  continue
114
123
  saw_real_entry = True
124
+ if apply_budget and loaded_entries >= JSONL_MAX_ENTRIES:
125
+ skipped.append(f"{file_path}: entry cap ({JSONL_MAX_ENTRIES})")
126
+ continue
127
+
128
+ expanded: list[tuple[str, str]] = []
115
129
  if entry_type == "directory":
116
- results.extend(read_directory_contents(base_path, file_path))
130
+ expanded = read_directory_contents(base_path, file_path)
117
131
  else:
118
132
  content = read_file_content(base_path, file_path)
119
133
  if content:
120
- results.append((file_path, content))
134
+ expanded = [(file_path, content)]
135
+
136
+ if not expanded:
137
+ skipped.append(f"{file_path}: missing or empty")
138
+ continue
139
+
140
+ block_chars = sum(len(content) for _, content in expanded)
141
+ if apply_budget and block_chars > JSONL_MAX_FILE_CHARS:
142
+ skipped.append(
143
+ f"{file_path}: file cap ({block_chars}>{JSONL_MAX_FILE_CHARS} chars)"
144
+ )
145
+ continue
146
+ if apply_budget and total_chars + block_chars > JSONL_MAX_TOTAL_CHARS:
147
+ skipped.append(
148
+ f"{file_path}: total cap "
149
+ f"({total_chars + block_chars}>{JSONL_MAX_TOTAL_CHARS})"
150
+ )
151
+ continue
152
+
153
+ results.extend(expanded)
154
+ total_chars += block_chars
155
+ loaded_entries += 1
121
156
  except json.JSONDecodeError:
122
157
  continue
123
158
  except OSError:
@@ -129,13 +164,23 @@ def read_jsonl_entries(
129
164
  "(only seed / empty) — sub-agent will receive only task artifacts.",
130
165
  file=sys.stderr,
131
166
  )
132
- return results
167
+ elif apply_budget and (results or skipped):
168
+ loaded_paths = ", ".join(path for path, _ in results) or "(none)"
169
+ print(
170
+ f"[{warn_prefix}] injection-budget: {jsonl_path} "
171
+ f"loaded {loaded_entries} entries ({total_chars} chars): {loaded_paths}",
172
+ file=sys.stderr,
173
+ )
174
+ for reason in skipped:
175
+ print(f"[{warn_prefix}] injection-budget: skipped {reason}", file=sys.stderr)
176
+ return results, skipped
133
177
 
134
178
 
135
179
  def get_agent_context(repo_root: str, task_dir: str, agent_type: str) -> str:
136
180
  context_parts: list[str] = []
137
181
  agent_jsonl = f"{task_dir}/{agent_type}.jsonl"
138
- for file_path, content in read_jsonl_entries(repo_root, agent_jsonl):
182
+ entries, _ = read_jsonl_entries(repo_root, agent_jsonl)
183
+ for file_path, content in entries:
139
184
  context_parts.append(f"=== {file_path} ===\n{content}")
140
185
  return "\n\n".join(context_parts)
141
186
 
@@ -166,7 +211,8 @@ def get_implement_context(repo_root: str, task_dir: str) -> str:
166
211
 
167
212
  def get_check_context(repo_root: str, task_dir: str) -> str:
168
213
  context_parts: list[str] = []
169
- for file_path, content in read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl"):
214
+ entries, _ = read_jsonl_entries(repo_root, f"{task_dir}/check.jsonl")
215
+ for file_path, content in entries:
170
216
  context_parts.append(f"=== {file_path} ===\n{content}")
171
217
 
172
218
  prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
@@ -192,7 +238,25 @@ def get_finish_context(repo_root: str, task_dir: str) -> str:
192
238
 
193
239
 
194
240
  def get_research_context(repo_root: str, task_dir: str | None) -> str:
195
- _ = task_dir
241
+ context_parts: list[str] = []
242
+
243
+ if task_dir:
244
+ entries, _ = read_jsonl_entries(repo_root, f"{task_dir}/research.jsonl")
245
+ for file_path, content in entries:
246
+ context_parts.append(f"=== {file_path} ===\n{content}")
247
+
248
+ prd_content = read_file_content(repo_root, f"{task_dir}/prd.md")
249
+ if prd_content:
250
+ context_parts.append(
251
+ f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}"
252
+ )
253
+
254
+ design_content = read_file_content(repo_root, f"{task_dir}/design.md")
255
+ if design_content:
256
+ context_parts.append(
257
+ f"=== {task_dir}/design.md (Technical Design) ===\n{design_content}"
258
+ )
259
+
196
260
  spec_path = f"{DIR_WORKFLOW}/{DIR_SPEC}"
197
261
  spec_root = Path(repo_root) / DIR_WORKFLOW / DIR_SPEC
198
262
 
@@ -219,7 +283,8 @@ To get structured package info, run: `python ./{DIR_WORKFLOW}/scripts/get_contex
219
283
  - Spec files: `{spec_path}/**/*.md`
220
284
  - Code search: Use Glob and Grep tools
221
285
  - External facts / docs: load `smart-search-cli` skill and use Bash (`smart-search` CLI), not Cursor WebSearch/WebFetch by default"""
222
- return project_structure
286
+ context_parts.append(project_structure)
287
+ return "\n\n".join(context_parts)
223
288
 
224
289
 
225
290
  def build_implement_prompt(original_prompt: str, context: str) -> str:
@@ -24,6 +24,11 @@ from pathlib import Path
24
24
  from .log import Colors, colored
25
25
  from .paths import get_repo_root
26
26
  from .task_utils import resolve_task_dir
27
+ from .injection_budget import (
28
+ JSONL_MAX_ENTRIES,
29
+ JSONL_MAX_TOTAL_CHARS,
30
+ measure_jsonl_manifest,
31
+ )
27
32
 
28
33
 
29
34
  # =============================================================================
@@ -159,6 +164,19 @@ def _validate_jsonl(jsonl_file: Path, repo_root: Path) -> int:
159
164
 
160
165
  if errors == 0:
161
166
  print(f" {colored(f'{file_name}: ✓ ({real_entries} entries)', Colors.GREEN)}")
167
+ budget = measure_jsonl_manifest(repo_root, jsonl_file.parent, file_name)
168
+ if real_entries > JSONL_MAX_ENTRIES:
169
+ print(
170
+ f" {colored(f'{file_name}: budget WARN — {real_entries} entries (max {JSONL_MAX_ENTRIES})', Colors.YELLOW)}"
171
+ )
172
+ if budget.expanded_chars > JSONL_MAX_TOTAL_CHARS:
173
+ print(
174
+ f" {colored(f'{file_name}: budget WARN — expanded {budget.expanded_chars} chars (max {JSONL_MAX_TOTAL_CHARS})', Colors.YELLOW)}"
175
+ )
176
+ elif budget.skipped:
177
+ print(
178
+ f" {colored(f'{file_name}: budget WARN — {len(budget.skipped)} entries would be skipped at dispatch', Colors.YELLOW)}"
179
+ )
162
180
  else:
163
181
  print(f" {colored(f'{file_name}: ✗ ({errors} errors)', Colors.RED)}")
164
182
 
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
 
8
8
  from .active_task import resolve_selected_task
9
9
  from .paths import DIR_TASKS, DIR_WORKFLOW, get_developer, get_repo_root, get_tasks_dir
10
+ from .task_gates import verify_evidence_status
10
11
  from .task_map import get_child_state
11
12
  from .tasks import (
12
13
  children_progress,
@@ -23,6 +24,19 @@ def _task_path(dir_name: str) -> str:
23
24
  return f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}"
24
25
 
25
26
 
27
+ def format_verify_summary(task_dir: Path, task_data: dict) -> str:
28
+ """Return a compact verify evidence label for dashboard task lines."""
29
+ verify_path = task_dir / "verify.md"
30
+ if not verify_path.is_file():
31
+ return "[verify: missing]"
32
+ status = verify_evidence_status(task_dir, task_data)
33
+ if all(status.values()):
34
+ return "[verify: ok]"
35
+ if any(status.values()):
36
+ return "[verify: partial]"
37
+ return "[verify: missing]"
38
+
39
+
26
40
  def _selected_line(
27
41
  repo_root: Path,
28
42
  platform_input: dict | None = None,
@@ -123,9 +137,10 @@ def _append_task(
123
137
  integration_state = get_child_state(parent_dir, name)
124
138
  status_display = format_child_task_display(task.status, integration_state)
125
139
  assignee = task.assignee or "-"
140
+ verify_summary = format_verify_summary(task.directory, task.raw or {})
126
141
  prefix = " " * indent + " - "
127
142
  lines.append(
128
- f"{prefix}{_task_path(name)} ({status_display}){progress} [{assignee}]"
143
+ f"{prefix}{_task_path(name)} ({status_display}){progress} [{assignee}] {verify_summary}"
129
144
  )
130
145
  for child_name in task.children:
131
146
  if child_name in all_tasks:
@@ -127,6 +127,37 @@ def remove_child_from_task_map(
127
127
  write_task_map(parent_dir, data, body, f"Unlinked Child `{child_name}`.")
128
128
 
129
129
 
130
+ def resolve_child_map_id(
131
+ parent_data: dict,
132
+ child_dir: Path,
133
+ child_data: dict | None = None,
134
+ ) -> str:
135
+ """Resolve task-map child id for a child directory (nested slash slugs)."""
136
+ structural = parent_data.get("children", [])
137
+ if not isinstance(structural, list):
138
+ structural = []
139
+
140
+ leaf = child_dir.name
141
+ if leaf in structural:
142
+ return leaf
143
+
144
+ if child_data:
145
+ for key in ("name", "id"):
146
+ value = child_data.get(key)
147
+ if isinstance(value, str) and value in structural:
148
+ return value
149
+
150
+ suffix_matches = [
151
+ cid
152
+ for cid in structural
153
+ if isinstance(cid, str) and (cid == leaf or cid.endswith(f"/{leaf}"))
154
+ ]
155
+ if len(suffix_matches) == 1:
156
+ return suffix_matches[0]
157
+
158
+ return leaf
159
+
160
+
130
161
  def set_child_state(
131
162
  parent_dir: Path,
132
163
  parent_data: dict,
@@ -183,7 +214,7 @@ def validate_parent_child_integration(
183
214
  ) -> list[str]:
184
215
  """Validate a Parent-controlled Child integration state transition."""
185
216
  errors: list[str] = []
186
- child_name = child_dir.name
217
+ child_name = resolve_child_map_id(parent_data, child_dir, child_data)
187
218
  if state not in PARENT_CONTROLLED_STATES:
188
219
  errors.append(f"state is not Parent-controlled: {state}")
189
220
  if not _short_text(evidence):
@@ -295,10 +326,11 @@ def set_parent_child_integration_state(
295
326
  if errors:
296
327
  return False, errors
297
328
 
329
+ child_name = resolve_child_map_id(parent_data, child_dir, child_data)
298
330
  data = ensure_task_map(parent_dir, parent_data, list(parent_data.get("children", [])))
299
- child = get_child_entry(data, child_dir.name)
331
+ child = get_child_entry(data, child_name)
300
332
  if child is None:
301
- return False, [f"child missing from task-map.md: {child_dir.name}"]
333
+ return False, [f"child missing from task-map.md: {child_name}"]
302
334
 
303
335
  child["state"] = state
304
336
  child["evidence"] = evidence
@@ -316,12 +348,12 @@ def set_parent_child_integration_state(
316
348
  queue = []
317
349
  data["integration_queue"] = queue
318
350
  if state == "integrating":
319
- if child_dir.name not in queue:
320
- queue.append(child_dir.name)
321
- elif child_dir.name in queue:
322
- data["integration_queue"] = [item for item in queue if item != child_dir.name]
351
+ if child_name not in queue:
352
+ queue.append(child_name)
353
+ elif child_name in queue:
354
+ data["integration_queue"] = [item for item in queue if item != child_name]
323
355
 
324
- event = f"Parent set Child `{child_dir.name}` integration state to `{state}`. Evidence: {evidence}."
356
+ event = f"Parent set Child `{child_name}` integration state to `{state}`. Evidence: {evidence}."
325
357
  if ref:
326
358
  event = f"{event} Ref: {ref}."
327
359
  if merge_ref:
@@ -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
 
@@ -1638,3 +1703,43 @@ def cmd_set_scope(args: argparse.Namespace) -> int:
1638
1703
 
1639
1704
  print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
1640
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