@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.
- package/CHANGELOG.md +46 -0
- package/README.md +13 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/goal/index.d.ts +3 -0
- package/dist/commands/goal/index.d.ts.map +1 -0
- package/dist/commands/goal/index.js +159 -0
- package/dist/commands/goal/index.js.map +1 -0
- package/dist/commands/goal/review.d.ts +6 -0
- package/dist/commands/goal/review.d.ts.map +1 -0
- package/dist/commands/goal/review.js +28 -0
- package/dist/commands/goal/review.js.map +1 -0
- package/dist/commands/sdk/run.d.ts.map +1 -1
- package/dist/commands/sdk/run.js +2 -27
- package/dist/commands/sdk/run.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +13 -1
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/goal/action-packet.d.ts +89 -0
- package/dist/goal/action-packet.d.ts.map +1 -0
- package/dist/goal/action-packet.js +113 -0
- package/dist/goal/action-packet.js.map +1 -0
- package/dist/goal/audit.d.ts +14 -0
- package/dist/goal/audit.d.ts.map +1 -0
- package/dist/goal/audit.js +12 -0
- package/dist/goal/audit.js.map +1 -0
- package/dist/goal/constants.d.ts +26 -0
- package/dist/goal/constants.d.ts.map +1 -0
- package/dist/goal/constants.js +24 -0
- package/dist/goal/constants.js.map +1 -0
- package/dist/goal/paths.d.ts +8 -0
- package/dist/goal/paths.d.ts.map +1 -0
- package/dist/goal/paths.js +36 -0
- package/dist/goal/paths.js.map +1 -0
- package/dist/goal/preflight.d.ts +21 -0
- package/dist/goal/preflight.d.ts.map +1 -0
- package/dist/goal/preflight.js +97 -0
- package/dist/goal/preflight.js.map +1 -0
- package/dist/goal/review-seam.d.ts +44 -0
- package/dist/goal/review-seam.d.ts.map +1 -0
- package/dist/goal/review-seam.js +153 -0
- package/dist/goal/review-seam.js.map +1 -0
- package/dist/goal/reviewer-rules.d.ts +13 -0
- package/dist/goal/reviewer-rules.d.ts.map +1 -0
- package/dist/goal/reviewer-rules.js +153 -0
- package/dist/goal/reviewer-rules.js.map +1 -0
- package/dist/goal/reviewer.d.ts +9 -0
- package/dist/goal/reviewer.d.ts.map +1 -0
- package/dist/goal/reviewer.js +54 -0
- package/dist/goal/reviewer.js.map +1 -0
- package/dist/goal/runtime.d.ts +32 -0
- package/dist/goal/runtime.d.ts.map +1 -0
- package/dist/goal/runtime.js +242 -0
- package/dist/goal/runtime.js.map +1 -0
- package/dist/goal/sdk-client.d.ts +10 -0
- package/dist/goal/sdk-client.d.ts.map +1 -0
- package/dist/goal/sdk-client.js +34 -0
- package/dist/goal/sdk-client.js.map +1 -0
- package/dist/goal/state.d.ts +40 -0
- package/dist/goal/state.d.ts.map +1 -0
- package/dist/goal/state.js +37 -0
- package/dist/goal/state.js.map +1 -0
- package/dist/goal/task-triggers.d.ts +15 -0
- package/dist/goal/task-triggers.d.ts.map +1 -0
- package/dist/goal/task-triggers.js +23 -0
- package/dist/goal/task-triggers.js.map +1 -0
- package/dist/goal/walls.d.ts +12 -0
- package/dist/goal/walls.d.ts.map +1 -0
- package/dist/goal/walls.js +65 -0
- package/dist/goal/walls.js.map +1 -0
- package/dist/goal/worker-prompt.d.ts +13 -0
- package/dist/goal/worker-prompt.d.ts.map +1 -0
- package/dist/goal/worker-prompt.js +41 -0
- package/dist/goal/worker-prompt.js.map +1 -0
- package/dist/goal/worker.d.ts +42 -0
- package/dist/goal/worker.d.ts.map +1 -0
- package/dist/goal/worker.js +180 -0
- package/dist/goal/worker.js.map +1 -0
- package/dist/migrations/manifests/0.3.5.json +9 -0
- package/dist/migrations/manifests/0.3.6.json +9 -0
- package/dist/templates/common/commands/continue.md +15 -3
- package/dist/templates/common/skills/check.md +7 -4
- package/dist/templates/cursor/agents/cstl-check.md +1 -0
- package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
- package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
- package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
- package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
- package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
- package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
- package/dist/templates/markdown/index.d.ts +3 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +3 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
- package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +107 -104
- package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +24 -6
- package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +184 -0
- package/dist/templates/shared-hooks/session-start.py +10 -0
- package/dist/templates/trellis/config.yaml +18 -0
- package/dist/templates/trellis/index.d.ts +5 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +14 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +37 -2
- package/dist/templates/trellis/scripts/common/config.py +39 -0
- package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +161 -5
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
- package/dist/templates/trellis/scripts/common/session_context.py +32 -4
- package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
- package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
- package/dist/templates/trellis/scripts/common/task_context.py +18 -0
- package/dist/templates/trellis/scripts/common/task_dashboard.py +16 -1
- package/dist/templates/trellis/scripts/common/task_map.py +40 -8
- package/dist/templates/trellis/scripts/common/task_store.py +144 -39
- package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
- package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
- package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
- package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
- package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
- package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
- package/dist/templates/trellis/scripts/task.py +19 -0
- package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
- package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
- package/dist/templates/trellis/workflow.md +23 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
331
|
+
child = get_child_entry(data, child_name)
|
|
300
332
|
if child is None:
|
|
301
|
-
return False, [f"child missing from task-map.md: {
|
|
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
|
|
320
|
-
queue.append(
|
|
321
|
-
elif
|
|
322
|
-
data["integration_queue"] = [item for item in queue if item !=
|
|
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 `{
|
|
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
|
|
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
|
-
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|