@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
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for P1 observable defaults (verify seed, jsonl seed, dashboard summary)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
13
+
14
+ from common.artifact_locale import default_verify_content
15
+ from common.task_dashboard import format_verify_summary, render_task_dashboard
16
+ from common.task_store import _resolve_seed_spec_paths, _write_seed_jsonl
17
+
18
+
19
+ def test_default_verify_content_has_standard_sections() -> None:
20
+ content = default_verify_content(locale="en")
21
+ for heading in (
22
+ "## Planning check",
23
+ "## Execution evidence",
24
+ "### Validation commands",
25
+ "### Acceptance",
26
+ "### Durable learning",
27
+ ):
28
+ assert heading in content
29
+
30
+
31
+ def test_write_seed_jsonl_uses_real_spec_paths(tmp_path: Path) -> None:
32
+ repo_root = tmp_path / "repo"
33
+ spec_root = repo_root / ".cstl" / "spec" / "guides"
34
+ spec_root.mkdir(parents=True)
35
+ for name in ("index.md", "verification-strength-guide.md", "injection-budget-guide.md"):
36
+ (spec_root / name).write_text(f"# {name}\n", encoding="utf-8")
37
+
38
+ task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-seed"
39
+ task_dir.mkdir(parents=True)
40
+ task_data = {"id": "test-seed", "package": None, "scope": None, "children": []}
41
+
42
+ jsonl_path = task_dir / "implement.jsonl"
43
+ _write_seed_jsonl(jsonl_path, repo_root, task_dir, task_data)
44
+
45
+ rows = [json.loads(line) for line in jsonl_path.read_text(encoding="utf-8").splitlines() if line.strip()]
46
+ assert len(rows) >= 2
47
+ assert all("file" in row and row["file"].startswith(".cstl/spec/") for row in rows)
48
+ assert all("_example" not in row for row in rows)
49
+ for row in rows:
50
+ assert (repo_root / row["file"]).is_file()
51
+
52
+
53
+ def test_resolve_seed_spec_paths_prefers_existing_guides(tmp_path: Path) -> None:
54
+ repo_root = tmp_path / "repo"
55
+ guides = repo_root / ".cstl" / "spec" / "guides"
56
+ guides.mkdir(parents=True)
57
+ (guides / "index.md").write_text("# index\n", encoding="utf-8")
58
+ (guides / "verification-strength-guide.md").write_text("# verify\n", encoding="utf-8")
59
+
60
+ task_dir = repo_root / ".cstl" / "tasks" / "08-07-test-paths"
61
+ task_dir.mkdir(parents=True)
62
+ paths = _resolve_seed_spec_paths(repo_root, task_dir, {"children": []})
63
+ assert len(paths) >= 2
64
+ assert all((repo_root / rel).is_file() for rel in paths)
65
+
66
+
67
+ def test_format_verify_summary_missing_without_file(tmp_path: Path) -> None:
68
+ task_dir = tmp_path / "task"
69
+ task_dir.mkdir()
70
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: missing]"
71
+
72
+
73
+ def test_format_verify_summary_partial_from_signals(tmp_path: Path) -> None:
74
+ task_dir = tmp_path / "task"
75
+ task_dir.mkdir()
76
+ (task_dir / "verify.md").write_text(
77
+ "Validation commands: pytest -q — pass\n",
78
+ encoding="utf-8",
79
+ )
80
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: partial]"
81
+
82
+
83
+ def test_format_verify_summary_ok_when_all_signals(tmp_path: Path) -> None:
84
+ task_dir = tmp_path / "task"
85
+ task_dir.mkdir()
86
+ (task_dir / "verify.md").write_text(
87
+ "\n".join(
88
+ [
89
+ "Validation commands: pytest -q — pass",
90
+ "Check evidence: cstl-check — pass",
91
+ "Final acceptance evidence: all AC met",
92
+ "Durable learning decision: no durable learning",
93
+ "Reviewed change-set: git diff main..HEAD",
94
+ ]
95
+ ),
96
+ encoding="utf-8",
97
+ )
98
+ assert format_verify_summary(task_dir, {"children": []}) == "[verify: ok]"
99
+
100
+
101
+ def test_render_task_dashboard_includes_verify_summary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
102
+ repo_root = tmp_path / "repo"
103
+ tasks_dir = repo_root / ".cstl" / "tasks"
104
+ task_dir = tasks_dir / "08-07-dashboard-task"
105
+ task_dir.mkdir(parents=True)
106
+ (task_dir / "task.json").write_text(
107
+ json.dumps(
108
+ {
109
+ "id": "dashboard-task",
110
+ "name": "dashboard-task",
111
+ "title": "dashboard-task",
112
+ "status": "planning",
113
+ "assignee": "dev",
114
+ "children": [],
115
+ }
116
+ ),
117
+ encoding="utf-8",
118
+ )
119
+ (task_dir / "verify.md").write_text(
120
+ "Validation commands: echo ok — pass\n",
121
+ encoding="utf-8",
122
+ )
123
+
124
+ monkeypatch.chdir(repo_root)
125
+ monkeypatch.setattr("common.task_dashboard.get_repo_root", lambda: repo_root)
126
+ monkeypatch.setattr("common.task_dashboard.get_tasks_dir", lambda _root=None: tasks_dir)
127
+ monkeypatch.setattr("common.task_dashboard.get_developer", lambda _root=None: "dev")
128
+ monkeypatch.setattr(
129
+ "common.active_task.resolve_selected_task",
130
+ lambda *_args, **_kwargs: type("Sel", (), {"task_path": None, "source": None})(),
131
+ )
132
+
133
+ dashboard = render_task_dashboard(repo_root)
134
+ assert "[verify: partial]" in dashboard
135
+ assert "08-07-dashboard-task" in dashboard
@@ -251,36 +251,38 @@ verdict rules:
251
251
 
252
252
  ---
253
253
 
254
- --- PROBE P-10: Definition / reference (codegraph Agent LSP not exposed)
254
+ --- PROBE P-10: Definition / reference (Prefer Grep+Read; codegraph when ambiguous)
255
255
 
256
256
  Cursor Agent does **not** expose GO_TO_DEFINITION in the tool table (Native/BYOK).
257
- Trellis routes definition/reference to **codegraph**. This probe measures the
258
- **product path**, not raw LSP.
257
+ Trellis **Prefers** native **Grep → Read** for named-symbol definition; **codegraph**
258
+ is the **Own** fallback when Grep is ambiguous or structural context is required.
259
259
 
260
- You are a retrieval capability probe. Use **codegraph_node** or **codegraph_search**
261
- as the **primary** path. Do NOT use Grep as the first step. Use **Read** only to
262
- confirm line numbers after codegraph returns a definition.
260
+ You are a retrieval capability probe.
263
261
 
264
262
  Task: Locate the **definition** of `route_codebase_retrieval` (Python function).
265
263
 
264
+ **Step 1 (Prefer):** Use **Grep** for `def route_codebase_retrieval` — do NOT use codegraph first.
265
+ **Step 2:** Use **Read** to confirm the definition line and body start.
266
+ **Step 3 (only if Grep ambiguous):** codegraph_explore / codegraph_search.
267
+
266
268
  Expected: definition in `.cstl/scripts/common/codebase_retrieval_router.py`
267
- (`def route_codebase_retrieval(`).
269
+ (`def route_codebase_retrieval(` at line ~805).
268
270
 
269
271
  Reply in this exact format:
270
272
 
271
273
  ```
272
274
  P-10 RESULT
273
- tool_invoked: <exact tool name codegraph_node, codegraph_search, or "none">
275
+ tool_invoked: <primary tool — Grep, Read, codegraph_explore, or "none">
274
276
  definition_file: <path>
275
277
  definition_line: <line number or "unknown">
276
278
  verdict: pass|fail|environment_limitation
277
- notes: <if you tried GO_TO_DEFINITION and it was unavailable, say so; pass when codegraph + Read confirm def>
279
+ notes: <GO_TO_DEFINITION availability; did Grep+Read succeed before any codegraph?>
278
280
  ```
279
281
 
280
282
  verdict rules:
281
- - **pass**: codegraph (or equivalent MCP) returned the correct `def route_codebase_retrieval` file.
282
- - **environment_limitation**: only when codegraph MCP/index is missing; not when LSP is missing.
283
- - **fail**: wrong file or no structural tool executed.
283
+ - **pass**: Grep (+ Read) returned the correct `def route_codebase_retrieval` file; codegraph optional.
284
+ - **environment_limitation**: only when Grep/Read are unavailable in the host (not when LSP is missing).
285
+ - **fail**: wrong file, or codegraph used as first step without Grep attempt.
284
286
 
285
287
  ---
286
288
 
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI Layer 2 — PRIMARY (guaranteed) context path for cstl-* Task dispatch.
4
+
5
+ Usage:
6
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent research
7
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent implement --task .cstl/tasks/06-23-subagent-smoke
8
+ python ./.cstl/scripts/generate_dispatch_prompt.py --agent research --repo-root d:/MyHarness
9
+
10
+ Outputs the complete dispatch prompt to stdout. The main session MUST capture
11
+ this output and pass it as the `prompt` parameter to
12
+ `Task(subagent_type=cstl-*, prompt=<generated>)`. This is the ONLY
13
+ guaranteed context channel for custom Task subagents on Cursor.
14
+
15
+ Hook injection (`preToolUse` / `additional_context`) is best-effort only
16
+ (Cursor #158452; hook does not fire for Task tool in Cursor 3.8.22). Do not
17
+ treat hook-only injection as a substitute for this CLI path.
18
+
19
+ The generated prompt carries:
20
+ - Role identity ("You are the Trellis Research/Implement/Check Agent")
21
+ - Write scope constraints (Write ALLOWED / Write FORBIDDEN)
22
+ - Recursion guard
23
+ - Dynamic context (prd.md, design.md, implement.md, spec tree, jsonl files)
24
+
25
+ Task path resolution:
26
+ - `--task <path>` is the **primary fallback** when no selected-task pointer exists
27
+ (e.g. `task.py select` failed due to missing session identity).
28
+ - When `--task` is omitted, auto-detect via selected pointer is **best-effort** only —
29
+ dispatch scripts do not hard-depend on a persisted selection.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import sys
35
+ from pathlib import Path
36
+
37
+
38
+ def main() -> int:
39
+ parser = argparse.ArgumentParser(
40
+ description="Generate a complete Trellis subagent dispatch prompt."
41
+ )
42
+ parser.add_argument(
43
+ "--agent",
44
+ required=True,
45
+ choices=["research", "implement", "check"],
46
+ help="Subagent role to generate prompt for.",
47
+ )
48
+ parser.add_argument(
49
+ "--task",
50
+ default=None,
51
+ help="Task directory path (absolute or relative to repo root). "
52
+ "Primary fallback when no selected-task pointer exists. "
53
+ "If omitted, auto-detect via selected pointer is best-effort only.",
54
+ )
55
+ parser.add_argument(
56
+ "--repo-root",
57
+ default=None,
58
+ help="Repository root path. If omitted, auto-detect via .cstl or .git.",
59
+ )
60
+ parser.add_argument(
61
+ "--finish",
62
+ action="store_true",
63
+ help="Use finish context (check agent only, final pre-PR verification).",
64
+ )
65
+ parser.add_argument(
66
+ "--max-chars",
67
+ type=int,
68
+ default=None,
69
+ help="Truncate injected context at this character count. "
70
+ "When omitted, uses role default from injection-budget-guide.",
71
+ )
72
+ args = parser.parse_args()
73
+
74
+ # Resolve repo root
75
+ if args.repo_root:
76
+ repo_root = Path(args.repo_root).resolve()
77
+ else:
78
+ repo_root = _find_repo_root(Path.cwd())
79
+ if not repo_root:
80
+ print("ERROR: could not find repo root (no .cstl or .git upward)", file=sys.stderr)
81
+ return 1
82
+
83
+ from common.paths import DIR_WORKFLOW # type: ignore[import-not-found]
84
+
85
+ # Make scripts importable — workspace root first (subagent_dispatch.py lives there),
86
+ # then repo_root's own scripts (may be a subproject with different common/)
87
+ workspace_root = _find_workspace_root(repo_root, DIR_WORKFLOW)
88
+ if workspace_root and workspace_root != repo_root:
89
+ ws_scripts = workspace_root / DIR_WORKFLOW / "scripts"
90
+ if str(ws_scripts) not in sys.path:
91
+ sys.path.insert(0, str(ws_scripts))
92
+
93
+ scripts_dir = repo_root / DIR_WORKFLOW / "scripts"
94
+ if str(scripts_dir) not in sys.path:
95
+ sys.path.insert(1, str(scripts_dir))
96
+
97
+ from common.subagent_dispatch import build_dispatch_prompt # type: ignore[import-not-found]
98
+ from common.injection_budget import DISPATCH_DEFAULT_MAX_CHARS # type: ignore[import-not-found]
99
+
100
+ # Resolve task directory
101
+ if args.task:
102
+ task_dir = Path(args.task)
103
+ if not task_dir.is_absolute():
104
+ task_dir = repo_root / task_dir
105
+ task_dir = task_dir.resolve()
106
+ else:
107
+ task_dir = _resolve_selected_task(repo_root)
108
+ if not task_dir:
109
+ print(
110
+ "ERROR: no --task specified and no selected task found. "
111
+ "Run `python ./.cstl/scripts/task.py select <task>` first.",
112
+ file=sys.stderr,
113
+ )
114
+ return 1
115
+
116
+ if not task_dir.is_dir():
117
+ print(f"ERROR: task directory not found: {task_dir}", file=sys.stderr)
118
+ return 1
119
+
120
+ max_chars = args.max_chars
121
+ if max_chars is None:
122
+ max_chars = DISPATCH_DEFAULT_MAX_CHARS.get(args.agent)
123
+
124
+ prompt, warnings, errors = build_dispatch_prompt(
125
+ repo_root,
126
+ task_dir,
127
+ args.agent,
128
+ finish=args.finish,
129
+ max_chars=max_chars,
130
+ require_in_progress=False,
131
+ )
132
+
133
+ for w in warnings:
134
+ print(f"WARN: {w}", file=sys.stderr)
135
+ for e in errors:
136
+ print(f"ERROR: {e}", file=sys.stderr)
137
+
138
+ if not prompt:
139
+ print("ERROR: failed to generate prompt (see errors above)", file=sys.stderr)
140
+ return 1
141
+
142
+ # Output the prompt to stdout — the caller captures this
143
+ print(prompt)
144
+ return 0
145
+
146
+
147
+ def _find_repo_root(start: Path) -> Path | None:
148
+ current = start.resolve()
149
+ while current != current.parent:
150
+ if (current / ".cstl").is_dir() or (current / ".git").exists():
151
+ return current
152
+ current = current.parent
153
+ return None
154
+
155
+
156
+ def _find_workspace_root(start: Path, dir_workflow: str) -> Path | None:
157
+ """Find the outermost directory with workflow dir (workspace root, not subproject)."""
158
+ found = None
159
+ current = start.resolve()
160
+ while current != current.parent:
161
+ if (current / dir_workflow).is_dir():
162
+ found = current
163
+ current = current.parent
164
+ return found
165
+
166
+
167
+ def _resolve_selected_task(repo_root: Path) -> Path | None:
168
+ try:
169
+ from common.active_task import resolve_selected_task # type: ignore[import-not-found]
170
+ result = resolve_selected_task(repo_root, {}, platform="cursor")
171
+ if result.task_path:
172
+ task_path = Path(result.task_path)
173
+ if not task_path.is_absolute():
174
+ task_path = repo_root / task_path
175
+ return task_path
176
+ except Exception:
177
+ pass
178
+ return None
179
+
180
+
181
+ if __name__ == "__main__":
182
+ sys.exit(main())
@@ -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 via codegraph (LSP not in Agent tool table)",
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": "codegraph_search + node; def route_codebase_retrieval L791; GO_TO_DEFINITION none", "actual_tool": "codegraph_search", "probed_at": "2026-06-23" },
92
- "byok": { "status": "pass", "evidence": "codegraph_node at def route_codebase_retrieval; GO_TO_DEFINITION none", "actual_tool": "codegraph_node", "probed_at": "2026-06-23" }
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`.