@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for P1 observable defaults (verify seed, jsonl seed, dashboard summary)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
13
|
+
|
|
14
|
+
from common.artifact_locale import default_verify_content
|
|
15
|
+
from common.task_dashboard import format_verify_summary, render_task_dashboard
|
|
16
|
+
from common.task_store import _resolve_seed_spec_paths, _write_seed_jsonl
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_default_verify_content_has_standard_sections() -> None:
|
|
20
|
+
content = default_verify_content(locale="en")
|
|
21
|
+
for heading in (
|
|
22
|
+
"## Planning check",
|
|
23
|
+
"## Execution evidence",
|
|
24
|
+
"### Validation commands",
|
|
25
|
+
"### Acceptance",
|
|
26
|
+
"### Durable learning",
|
|
27
|
+
):
|
|
28
|
+
assert heading in content
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_write_seed_jsonl_uses_real_spec_paths(tmp_path: Path) -> None:
|
|
32
|
+
repo_root = tmp_path / "repo"
|
|
33
|
+
spec_root = repo_root / ".cstl" / "spec" / "guides"
|
|
34
|
+
spec_root.mkdir(parents=True)
|
|
35
|
+
for name in ("index.md", "verification-strength-guide.md", "injection-budget-guide.md"):
|
|
36
|
+
(spec_root / name).write_text(f"# {name}\n", encoding="utf-8")
|
|
37
|
+
|
|
38
|
+
task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-seed"
|
|
39
|
+
task_dir.mkdir(parents=True)
|
|
40
|
+
task_data = {"id": "test-seed", "package": None, "scope": None, "children": []}
|
|
41
|
+
|
|
42
|
+
jsonl_path = task_dir / "implement.jsonl"
|
|
43
|
+
_write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
|
|
44
|
+
|
|
45
|
+
rows = [json.loads(line) for line in jsonl_path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
46
|
+
assert len(rows) >= 2
|
|
47
|
+
assert all("file" in row and row["file"].startswith(".cstl/spec/") for row in rows)
|
|
48
|
+
assert all("_example" not in row for row in rows)
|
|
49
|
+
for row in rows:
|
|
50
|
+
assert (repo_root / row["file"]).is_file()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_resolve_seed_spec_paths_prefers_existing_guides(tmp_path: Path) -> None:
|
|
54
|
+
repo_root = tmp_path / "repo"
|
|
55
|
+
guides = repo_root / ".cstl" / "spec" / "guides"
|
|
56
|
+
guides.mkdir(parents=True)
|
|
57
|
+
(guides / "index.md").write_text("# index\n", encoding="utf-8")
|
|
58
|
+
(guides / "verification-strength-guide.md").write_text("# verify\n", encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-paths"
|
|
61
|
+
task_dir.mkdir(parents=True)
|
|
62
|
+
paths = _resolve_seed_spec_paths(repo_root, task_dir, {"children": []})
|
|
63
|
+
assert len(paths) >= 2
|
|
64
|
+
assert all((repo_root / rel).is_file() for rel in paths)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_format_verify_summary_missing_without_file(tmp_path: Path) -> None:
|
|
68
|
+
task_dir = tmp_path / "task"
|
|
69
|
+
task_dir.mkdir()
|
|
70
|
+
assert format_verify_summary(task_dir, {"children": []}) == "[verify: missing]"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_format_verify_summary_partial_from_signals(tmp_path: Path) -> None:
|
|
74
|
+
task_dir = tmp_path / "task"
|
|
75
|
+
task_dir.mkdir()
|
|
76
|
+
(task_dir / "verify.md").write_text(
|
|
77
|
+
"Validation commands: pytest -q — pass\n",
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
)
|
|
80
|
+
assert format_verify_summary(task_dir, {"children": []}) == "[verify: partial]"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_format_verify_summary_ok_when_all_signals(tmp_path: Path) -> None:
|
|
84
|
+
task_dir = tmp_path / "task"
|
|
85
|
+
task_dir.mkdir()
|
|
86
|
+
(task_dir / "verify.md").write_text(
|
|
87
|
+
"\n".join(
|
|
88
|
+
[
|
|
89
|
+
"Validation commands: pytest -q — pass",
|
|
90
|
+
"Check evidence: cstl-check — pass",
|
|
91
|
+
"Final acceptance evidence: all AC met",
|
|
92
|
+
"Durable learning decision: no durable learning",
|
|
93
|
+
"Reviewed change-set: git diff main..HEAD",
|
|
94
|
+
]
|
|
95
|
+
),
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
)
|
|
98
|
+
assert format_verify_summary(task_dir, {"children": []}) == "[verify: ok]"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_render_task_dashboard_includes_verify_summary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
102
|
+
repo_root = tmp_path / "repo"
|
|
103
|
+
tasks_dir = repo_root / ".cstl" / "tasks"
|
|
104
|
+
task_dir = tasks_dir / "08-07-dashboard-task"
|
|
105
|
+
task_dir.mkdir(parents=True)
|
|
106
|
+
(task_dir / "task.json").write_text(
|
|
107
|
+
json.dumps(
|
|
108
|
+
{
|
|
109
|
+
"id": "dashboard-task",
|
|
110
|
+
"name": "dashboard-task",
|
|
111
|
+
"title": "dashboard-task",
|
|
112
|
+
"status": "planning",
|
|
113
|
+
"assignee": "dev",
|
|
114
|
+
"children": [],
|
|
115
|
+
}
|
|
116
|
+
),
|
|
117
|
+
encoding="utf-8",
|
|
118
|
+
)
|
|
119
|
+
(task_dir / "verify.md").write_text(
|
|
120
|
+
"Validation commands: echo ok — pass\n",
|
|
121
|
+
encoding="utf-8",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
monkeypatch.chdir(repo_root)
|
|
125
|
+
monkeypatch.setattr("common.task_dashboard.get_repo_root", lambda: repo_root)
|
|
126
|
+
monkeypatch.setattr("common.task_dashboard.get_tasks_dir", lambda _root=None: tasks_dir)
|
|
127
|
+
monkeypatch.setattr("common.task_dashboard.get_developer", lambda _root=None: "dev")
|
|
128
|
+
monkeypatch.setattr(
|
|
129
|
+
"common.active_task.resolve_selected_task",
|
|
130
|
+
lambda *_args, **_kwargs: type("Sel", (), {"task_path": None, "source": None})(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
dashboard = render_task_dashboard(repo_root)
|
|
134
|
+
assert "[verify: partial]" in dashboard
|
|
135
|
+
assert "08-07-dashboard-task" in dashboard
|
|
@@ -251,36 +251,38 @@ verdict rules:
|
|
|
251
251
|
|
|
252
252
|
---
|
|
253
253
|
|
|
254
|
-
--- PROBE P-10: Definition / reference (
|
|
254
|
+
--- PROBE P-10: Definition / reference (Prefer Grep+Read; codegraph when ambiguous)
|
|
255
255
|
|
|
256
256
|
Cursor Agent does **not** expose GO_TO_DEFINITION in the tool table (Native/BYOK).
|
|
257
|
-
Trellis
|
|
258
|
-
**
|
|
257
|
+
Trellis **Prefers** native **Grep → Read** for named-symbol definition; **codegraph**
|
|
258
|
+
is the **Own** fallback when Grep is ambiguous or structural context is required.
|
|
259
259
|
|
|
260
|
-
You are a retrieval capability probe.
|
|
261
|
-
as the **primary** path. Do NOT use Grep as the first step. Use **Read** only to
|
|
262
|
-
confirm line numbers after codegraph returns a definition.
|
|
260
|
+
You are a retrieval capability probe.
|
|
263
261
|
|
|
264
262
|
Task: Locate the **definition** of `route_codebase_retrieval` (Python function).
|
|
265
263
|
|
|
264
|
+
**Step 1 (Prefer):** Use **Grep** for `def route_codebase_retrieval` — do NOT use codegraph first.
|
|
265
|
+
**Step 2:** Use **Read** to confirm the definition line and body start.
|
|
266
|
+
**Step 3 (only if Grep ambiguous):** codegraph_explore / codegraph_search.
|
|
267
|
+
|
|
266
268
|
Expected: definition in `.cstl/scripts/common/codebase_retrieval_router.py`
|
|
267
|
-
(`def route_codebase_retrieval(`).
|
|
269
|
+
(`def route_codebase_retrieval(` at line ~805).
|
|
268
270
|
|
|
269
271
|
Reply in this exact format:
|
|
270
272
|
|
|
271
273
|
```
|
|
272
274
|
P-10 RESULT
|
|
273
|
-
tool_invoked: <
|
|
275
|
+
tool_invoked: <primary tool — Grep, Read, codegraph_explore, or "none">
|
|
274
276
|
definition_file: <path>
|
|
275
277
|
definition_line: <line number or "unknown">
|
|
276
278
|
verdict: pass|fail|environment_limitation
|
|
277
|
-
notes: <
|
|
279
|
+
notes: <GO_TO_DEFINITION availability; did Grep+Read succeed before any codegraph?>
|
|
278
280
|
```
|
|
279
281
|
|
|
280
282
|
verdict rules:
|
|
281
|
-
- **pass**:
|
|
282
|
-
- **environment_limitation**: only when
|
|
283
|
-
- **fail**: wrong file or
|
|
283
|
+
- **pass**: Grep (+ Read) returned the correct `def route_codebase_retrieval` file; codegraph optional.
|
|
284
|
+
- **environment_limitation**: only when Grep/Read are unavailable in the host (not when LSP is missing).
|
|
285
|
+
- **fail**: wrong file, or codegraph used as first step without Grep attempt.
|
|
284
286
|
|
|
285
287
|
---
|
|
286
288
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CLI Layer 2 — PRIMARY (guaranteed) context path for cstl-* Task dispatch.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python ./.cstl/scripts/generate_dispatch_prompt.py --agent research
|
|
7
|
+
python ./.cstl/scripts/generate_dispatch_prompt.py --agent implement --task .cstl/tasks/06-23-subagent-smoke
|
|
8
|
+
python ./.cstl/scripts/generate_dispatch_prompt.py --agent research --repo-root d:/MyHarness
|
|
9
|
+
|
|
10
|
+
Outputs the complete dispatch prompt to stdout. The main session MUST capture
|
|
11
|
+
this output and pass it as the `prompt` parameter to
|
|
12
|
+
`Task(subagent_type=cstl-*, prompt=<generated>)`. This is the ONLY
|
|
13
|
+
guaranteed context channel for custom Task subagents on Cursor.
|
|
14
|
+
|
|
15
|
+
Hook injection (`preToolUse` / `additional_context`) is best-effort only
|
|
16
|
+
(Cursor #158452; hook does not fire for Task tool in Cursor 3.8.22). Do not
|
|
17
|
+
treat hook-only injection as a substitute for this CLI path.
|
|
18
|
+
|
|
19
|
+
The generated prompt carries:
|
|
20
|
+
- Role identity ("You are the Trellis Research/Implement/Check Agent")
|
|
21
|
+
- Write scope constraints (Write ALLOWED / Write FORBIDDEN)
|
|
22
|
+
- Recursion guard
|
|
23
|
+
- Dynamic context (prd.md, design.md, implement.md, spec tree, jsonl files)
|
|
24
|
+
|
|
25
|
+
Task path resolution:
|
|
26
|
+
- `--task <path>` is the **primary fallback** when no selected-task pointer exists
|
|
27
|
+
(e.g. `task.py select` failed due to missing session identity).
|
|
28
|
+
- When `--task` is omitted, auto-detect via selected pointer is **best-effort** only —
|
|
29
|
+
dispatch scripts do not hard-depend on a persisted selection.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> int:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
description="Generate a complete Trellis subagent dispatch prompt."
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--agent",
|
|
44
|
+
required=True,
|
|
45
|
+
choices=["research", "implement", "check"],
|
|
46
|
+
help="Subagent role to generate prompt for.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--task",
|
|
50
|
+
default=None,
|
|
51
|
+
help="Task directory path (absolute or relative to repo root). "
|
|
52
|
+
"Primary fallback when no selected-task pointer exists. "
|
|
53
|
+
"If omitted, auto-detect via selected pointer is best-effort only.",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--repo-root",
|
|
57
|
+
default=None,
|
|
58
|
+
help="Repository root path. If omitted, auto-detect via .cstl or .git.",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--finish",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Use finish context (check agent only, final pre-PR verification).",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--max-chars",
|
|
67
|
+
type=int,
|
|
68
|
+
default=None,
|
|
69
|
+
help="Truncate injected context at this character count. "
|
|
70
|
+
"When omitted, uses role default from injection-budget-guide.",
|
|
71
|
+
)
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
# Resolve repo root
|
|
75
|
+
if args.repo_root:
|
|
76
|
+
repo_root = Path(args.repo_root).resolve()
|
|
77
|
+
else:
|
|
78
|
+
repo_root = _find_repo_root(Path.cwd())
|
|
79
|
+
if not repo_root:
|
|
80
|
+
print("ERROR: could not find repo root (no .cstl or .git upward)", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
from common.paths import DIR_WORKFLOW # type: ignore[import-not-found]
|
|
84
|
+
|
|
85
|
+
# Make scripts importable — workspace root first (subagent_dispatch.py lives there),
|
|
86
|
+
# then repo_root's own scripts (may be a subproject with different common/)
|
|
87
|
+
workspace_root = _find_workspace_root(repo_root, DIR_WORKFLOW)
|
|
88
|
+
if workspace_root and workspace_root != repo_root:
|
|
89
|
+
ws_scripts = workspace_root / DIR_WORKFLOW / "scripts"
|
|
90
|
+
if str(ws_scripts) not in sys.path:
|
|
91
|
+
sys.path.insert(0, str(ws_scripts))
|
|
92
|
+
|
|
93
|
+
scripts_dir = repo_root / DIR_WORKFLOW / "scripts"
|
|
94
|
+
if str(scripts_dir) not in sys.path:
|
|
95
|
+
sys.path.insert(1, str(scripts_dir))
|
|
96
|
+
|
|
97
|
+
from common.subagent_dispatch import build_dispatch_prompt # type: ignore[import-not-found]
|
|
98
|
+
from common.injection_budget import DISPATCH_DEFAULT_MAX_CHARS # type: ignore[import-not-found]
|
|
99
|
+
|
|
100
|
+
# Resolve task directory
|
|
101
|
+
if args.task:
|
|
102
|
+
task_dir = Path(args.task)
|
|
103
|
+
if not task_dir.is_absolute():
|
|
104
|
+
task_dir = repo_root / task_dir
|
|
105
|
+
task_dir = task_dir.resolve()
|
|
106
|
+
else:
|
|
107
|
+
task_dir = _resolve_selected_task(repo_root)
|
|
108
|
+
if not task_dir:
|
|
109
|
+
print(
|
|
110
|
+
"ERROR: no --task specified and no selected task found. "
|
|
111
|
+
"Run `python ./.cstl/scripts/task.py select <task>` first.",
|
|
112
|
+
file=sys.stderr,
|
|
113
|
+
)
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
if not task_dir.is_dir():
|
|
117
|
+
print(f"ERROR: task directory not found: {task_dir}", file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
max_chars = args.max_chars
|
|
121
|
+
if max_chars is None:
|
|
122
|
+
max_chars = DISPATCH_DEFAULT_MAX_CHARS.get(args.agent)
|
|
123
|
+
|
|
124
|
+
prompt, warnings, errors = build_dispatch_prompt(
|
|
125
|
+
repo_root,
|
|
126
|
+
task_dir,
|
|
127
|
+
args.agent,
|
|
128
|
+
finish=args.finish,
|
|
129
|
+
max_chars=max_chars,
|
|
130
|
+
require_in_progress=False,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
for w in warnings:
|
|
134
|
+
print(f"WARN: {w}", file=sys.stderr)
|
|
135
|
+
for e in errors:
|
|
136
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
137
|
+
|
|
138
|
+
if not prompt:
|
|
139
|
+
print("ERROR: failed to generate prompt (see errors above)", file=sys.stderr)
|
|
140
|
+
return 1
|
|
141
|
+
|
|
142
|
+
# Output the prompt to stdout — the caller captures this
|
|
143
|
+
print(prompt)
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _find_repo_root(start: Path) -> Path | None:
|
|
148
|
+
current = start.resolve()
|
|
149
|
+
while current != current.parent:
|
|
150
|
+
if (current / ".cstl").is_dir() or (current / ".git").exists():
|
|
151
|
+
return current
|
|
152
|
+
current = current.parent
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _find_workspace_root(start: Path, dir_workflow: str) -> Path | None:
|
|
157
|
+
"""Find the outermost directory with workflow dir (workspace root, not subproject)."""
|
|
158
|
+
found = None
|
|
159
|
+
current = start.resolve()
|
|
160
|
+
while current != current.parent:
|
|
161
|
+
if (current / dir_workflow).is_dir():
|
|
162
|
+
found = current
|
|
163
|
+
current = current.parent
|
|
164
|
+
return found
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _resolve_selected_task(repo_root: Path) -> Path | None:
|
|
168
|
+
try:
|
|
169
|
+
from common.active_task import resolve_selected_task # type: ignore[import-not-found]
|
|
170
|
+
result = resolve_selected_task(repo_root, {}, platform="cursor")
|
|
171
|
+
if result.task_path:
|
|
172
|
+
task_path = Path(result.task_path)
|
|
173
|
+
if not task_path.is_absolute():
|
|
174
|
+
task_path = repo_root / task_path
|
|
175
|
+
return task_path
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Injection budget probe — measure alwaysApply, AGENTS, jsonl, and Layer 2 dispatch."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
_SCRIPTS = Path(__file__).resolve().parent
|
|
11
|
+
if str(_SCRIPTS) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(_SCRIPTS))
|
|
13
|
+
|
|
14
|
+
from common.injection_budget import ( # noqa: E402
|
|
15
|
+
budget_violations,
|
|
16
|
+
collect_budget_report,
|
|
17
|
+
format_budget_report,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> int:
|
|
22
|
+
parser = argparse.ArgumentParser(description="Measure Trellis injection budget surfaces.")
|
|
23
|
+
parser.add_argument("--repo-root", default=".", help="Repository root (default: .)")
|
|
24
|
+
parser.add_argument("--task", default=None, help="Task directory for jsonl/dispatch probes")
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--dispatch-role",
|
|
27
|
+
choices=["implement", "check", "research"],
|
|
28
|
+
default="implement",
|
|
29
|
+
help="Role when measuring dispatch prompt (default: implement)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--strict",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="Exit 1 on FAIL thresholds (rules >32KiB) or any WARN",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("--json", action="store_true", help="Reserved for future machine output")
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
if args.json:
|
|
40
|
+
print("ERROR: --json not implemented yet", file=sys.stderr)
|
|
41
|
+
return 2
|
|
42
|
+
|
|
43
|
+
repo_root = Path(args.repo_root).resolve()
|
|
44
|
+
task_dir = None
|
|
45
|
+
if args.task:
|
|
46
|
+
task_dir = Path(args.task)
|
|
47
|
+
if not task_dir.is_absolute():
|
|
48
|
+
task_dir = repo_root / task_dir
|
|
49
|
+
task_dir = task_dir.resolve()
|
|
50
|
+
|
|
51
|
+
report = collect_budget_report(
|
|
52
|
+
repo_root,
|
|
53
|
+
task_dir=task_dir,
|
|
54
|
+
dispatch_role=args.dispatch_role if task_dir else None,
|
|
55
|
+
)
|
|
56
|
+
print(format_budget_report(report))
|
|
57
|
+
|
|
58
|
+
violations = budget_violations(report, strict=args.strict)
|
|
59
|
+
if violations:
|
|
60
|
+
print("\n## Violations", file=sys.stderr)
|
|
61
|
+
for item in violations:
|
|
62
|
+
print(item, file=sys.stderr)
|
|
63
|
+
if args.strict or any(item.startswith("FAIL:") for item in violations):
|
|
64
|
+
return 1
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
sys.exit(main())
|
|
@@ -85,11 +85,11 @@
|
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"id": "P-10",
|
|
88
|
-
"label": "Definition / reference
|
|
88
|
+
"label": "Definition / reference — Prefer Grep+Read; codegraph when ambiguous",
|
|
89
89
|
"nature": "local-mcp",
|
|
90
90
|
"expected_byok": "pass",
|
|
91
|
-
"native": { "status": "pass", "evidence": "
|
|
92
|
-
"byok": { "status": "pass", "evidence": "
|
|
91
|
+
"native": { "status": "pass", "evidence": "Grep def route_codebase_retrieval L805 codebase_retrieval_router.py; Read confirm; GO_TO_DEFINITION none; codegraph optional", "actual_tool": "Grep", "probed_at": "2026-08-05" },
|
|
92
|
+
"byok": { "status": "pass", "evidence": "Grep+Read same as native; GO_TO_DEFINITION none", "actual_tool": "Grep", "probed_at": "2026-08-05" }
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"id": "P-08-SA",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""S1 spec-health outcomes — read-only durable-learning distribution over archive verify.md.
|
|
3
|
+
|
|
4
|
+
Maintainer observability only — NOT a blocking gate or KPI.
|
|
5
|
+
See .cstl/spec/guides/durable-learning-decision-guide.md for outcome definitions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
19
|
+
if str(_SCRIPT_DIR) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(_SCRIPT_DIR))
|
|
21
|
+
|
|
22
|
+
from common.task_gates import NO_DURABLE_LEARNING_RE # noqa: E402
|
|
23
|
+
|
|
24
|
+
BUCKET_NAMES = ("no_only", "spec", "artifact", "ambiguous_multi", "legacy_none")
|
|
25
|
+
|
|
26
|
+
MONTH_RE = re.compile(r"^(\d{4}-\d{2})$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def detect_signals(content: str) -> dict[str, bool]:
|
|
30
|
+
"""Return which durable-learning signal families appear in verify.md text.
|
|
31
|
+
|
|
32
|
+
Uses the same substring probes as spec-health-signals.md S1 baseline (2026-08-06).
|
|
33
|
+
"""
|
|
34
|
+
lower = content.lower()
|
|
35
|
+
return {
|
|
36
|
+
"no": bool(NO_DURABLE_LEARNING_RE.search(content)),
|
|
37
|
+
"spec": "spec update evidence" in lower,
|
|
38
|
+
"artifact": "learning artifact" in lower,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def classify_bucket(content: str) -> str:
|
|
43
|
+
"""Assign exactly one mutually-exclusive S1 outcome bucket."""
|
|
44
|
+
active = [name for name, present in detect_signals(content).items() if present]
|
|
45
|
+
if not active:
|
|
46
|
+
return "legacy_none"
|
|
47
|
+
if len(active) > 1:
|
|
48
|
+
return "ambiguous_multi"
|
|
49
|
+
return {"no": "no_only", "spec": "spec", "artifact": "artifact"}[active[0]]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_month(verify_path: Path, archive_root: Path) -> str | None:
|
|
53
|
+
"""Derive YYYY-MM from archive folder layout (e.g. archive/2026-08/<task>/verify.md)."""
|
|
54
|
+
try:
|
|
55
|
+
rel = verify_path.relative_to(archive_root)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return None
|
|
58
|
+
if rel.parts and MONTH_RE.match(rel.parts[0]):
|
|
59
|
+
return rel.parts[0]
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def empty_counts() -> dict[str, int]:
|
|
64
|
+
return {name: 0 for name in BUCKET_NAMES}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def scan_archive(root: Path) -> tuple[dict[str, int], dict[str, dict[str, int]], int]:
|
|
68
|
+
totals = empty_counts()
|
|
69
|
+
by_month: dict[str, dict[str, int]] = defaultdict(empty_counts)
|
|
70
|
+
total_files = 0
|
|
71
|
+
|
|
72
|
+
for verify_path in sorted(root.rglob("verify.md")):
|
|
73
|
+
if not verify_path.is_file():
|
|
74
|
+
continue
|
|
75
|
+
total_files += 1
|
|
76
|
+
try:
|
|
77
|
+
content = verify_path.read_text(encoding="utf-8")
|
|
78
|
+
except OSError:
|
|
79
|
+
content = ""
|
|
80
|
+
bucket = classify_bucket(content)
|
|
81
|
+
totals[bucket] += 1
|
|
82
|
+
month = extract_month(verify_path, root)
|
|
83
|
+
if month:
|
|
84
|
+
by_month[month][bucket] += 1
|
|
85
|
+
|
|
86
|
+
return totals, dict(sorted(by_month.items())), total_files
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_report(root: Path, *, by_month: bool) -> dict[str, Any]:
|
|
90
|
+
totals, monthly, total_files = scan_archive(root)
|
|
91
|
+
report: dict[str, Any] = {
|
|
92
|
+
"root": str(root),
|
|
93
|
+
"total": total_files,
|
|
94
|
+
"buckets": totals,
|
|
95
|
+
"note": "Maintainer observability only — not a blocking gate or KPI.",
|
|
96
|
+
}
|
|
97
|
+
if by_month:
|
|
98
|
+
report["by_month"] = monthly
|
|
99
|
+
return report
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def format_text_report(report: dict[str, Any]) -> str:
|
|
103
|
+
lines = [
|
|
104
|
+
f"Spec health S1 outcomes — {report['total']} verify.md files",
|
|
105
|
+
f"Root: {report['root']}",
|
|
106
|
+
"",
|
|
107
|
+
]
|
|
108
|
+
total = int(report["total"])
|
|
109
|
+
for name in BUCKET_NAMES:
|
|
110
|
+
count = int(report["buckets"][name])
|
|
111
|
+
pct = (count / total * 100) if total else 0.0
|
|
112
|
+
lines.append(f" {name}: {count} ({pct:.1f}%)")
|
|
113
|
+
lines.extend(["", str(report["note"])])
|
|
114
|
+
if report.get("by_month"):
|
|
115
|
+
lines.append("")
|
|
116
|
+
lines.append("By month:")
|
|
117
|
+
for month, counts in report["by_month"].items():
|
|
118
|
+
month_total = sum(int(v) for v in counts.values())
|
|
119
|
+
lines.append(f" {month}: {month_total} files — {counts}")
|
|
120
|
+
return "\n".join(lines)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv: list[str] | None = None) -> int:
|
|
124
|
+
parser = argparse.ArgumentParser(
|
|
125
|
+
description=(
|
|
126
|
+
"S1 spec-health: durable-learning outcome distribution in archive verify.md "
|
|
127
|
+
"(read-only; not a gate)."
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--root",
|
|
132
|
+
type=Path,
|
|
133
|
+
default=Path(".cstl/tasks/archive"),
|
|
134
|
+
help="Archive root to scan (default: .cstl/tasks/archive)",
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument("--json", action="store_true", help="Emit JSON report on stdout")
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--by-month",
|
|
139
|
+
action="store_true",
|
|
140
|
+
help="Include per YYYY-MM bucket counts (from archive folder names)",
|
|
141
|
+
)
|
|
142
|
+
args = parser.parse_args(argv)
|
|
143
|
+
|
|
144
|
+
root = args.root.resolve()
|
|
145
|
+
if not root.is_dir():
|
|
146
|
+
print(f"error: archive root not found: {root}", file=sys.stderr)
|
|
147
|
+
return 1
|
|
148
|
+
|
|
149
|
+
report = build_report(root, by_month=args.by_month)
|
|
150
|
+
|
|
151
|
+
if args.json:
|
|
152
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
153
|
+
else:
|
|
154
|
+
print(format_text_report(report))
|
|
155
|
+
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
raise SystemExit(main())
|
|
@@ -97,6 +97,7 @@ from common.task_store import (
|
|
|
97
97
|
cmd_parent_status,
|
|
98
98
|
cmd_publish_pack,
|
|
99
99
|
cmd_review_child,
|
|
100
|
+
cmd_artifact_locale,
|
|
100
101
|
)
|
|
101
102
|
from common.task_context import (
|
|
102
103
|
cmd_add_context,
|
|
@@ -707,6 +708,18 @@ def main() -> int:
|
|
|
707
708
|
p_scope.add_argument("dir", help="Task directory")
|
|
708
709
|
p_scope.add_argument("scope", help="Scope name")
|
|
709
710
|
|
|
711
|
+
# artifact-locale
|
|
712
|
+
p_artifact_locale = subparsers.add_parser(
|
|
713
|
+
"artifact-locale",
|
|
714
|
+
help="Get or set human-reviewed artifact locale (zh | en)",
|
|
715
|
+
)
|
|
716
|
+
locale_sub = p_artifact_locale.add_subparsers(dest="artifact_locale_command")
|
|
717
|
+
p_locale_get = locale_sub.add_parser("get", help="Print resolved artifact locale")
|
|
718
|
+
p_locale_get.add_argument("--task", help="Task directory (includes task override)")
|
|
719
|
+
p_locale_set = locale_sub.add_parser("set", help="Set artifact locale")
|
|
720
|
+
p_locale_set.add_argument("locale", choices=["zh", "en"], help="Locale code")
|
|
721
|
+
p_locale_set.add_argument("--task", help="Task directory (task-level override)")
|
|
722
|
+
|
|
710
723
|
# archive
|
|
711
724
|
p_archive = subparsers.add_parser("archive", help="Archive task")
|
|
712
725
|
p_archive.add_argument("name", help="Task directory or name")
|
|
@@ -782,6 +795,11 @@ def main() -> int:
|
|
|
782
795
|
p_integrate_child.add_argument("--ref", help="Child git ref or reviewed diff reference")
|
|
783
796
|
p_integrate_child.add_argument("--reason", help="Optional short reason")
|
|
784
797
|
p_integrate_child.add_argument("--execute-merge", action="store_true", help="Execute git merge --no-ff --no-commit for an integrated Child")
|
|
798
|
+
p_integrate_child.add_argument(
|
|
799
|
+
"--no-publish-pack",
|
|
800
|
+
action="store_true",
|
|
801
|
+
help="Skip auto publish-pack after integrated (default: refresh child-prompts/ including evidence-only integrates)",
|
|
802
|
+
)
|
|
785
803
|
p_integrate_child.add_argument("--check", action="store_true", help="Run non-mutating integration readiness check")
|
|
786
804
|
|
|
787
805
|
# suggest-execution-strategy
|
|
@@ -935,6 +953,7 @@ def main() -> int:
|
|
|
935
953
|
"parent-status": cmd_parent_status,
|
|
936
954
|
"publish-pack": cmd_publish_pack,
|
|
937
955
|
"review-child": cmd_review_child,
|
|
956
|
+
"artifact-locale": cmd_artifact_locale,
|
|
938
957
|
"list": cmd_list,
|
|
939
958
|
"list-archive": cmd_list_archive,
|
|
940
959
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# {title}
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
{goal}
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- TBD
|
|
10
|
+
|
|
11
|
+
## Acceptance Criteria
|
|
12
|
+
|
|
13
|
+
- [ ] TBD
|
|
14
|
+
|
|
15
|
+
## Notes
|
|
16
|
+
|
|
17
|
+
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
|
|
18
|
+
- Lightweight tasks can remain PRD-only.
|
|
19
|
+
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start-execution --check`.
|