@blxzer/cursor-trellis 0.3.5 → 0.4.0

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 (156) hide show
  1. package/CHANGELOG.md +69 -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/commands/update.d.ts.map +1 -1
  18. package/dist/commands/update.js +5 -1
  19. package/dist/commands/update.js.map +1 -1
  20. package/dist/configurators/workflow.d.ts.map +1 -1
  21. package/dist/configurators/workflow.js +51 -2
  22. package/dist/configurators/workflow.js.map +1 -1
  23. package/dist/constants/paths.d.ts +4 -0
  24. package/dist/constants/paths.d.ts.map +1 -1
  25. package/dist/constants/paths.js +4 -0
  26. package/dist/constants/paths.js.map +1 -1
  27. package/dist/goal/action-packet.d.ts +89 -0
  28. package/dist/goal/action-packet.d.ts.map +1 -0
  29. package/dist/goal/action-packet.js +113 -0
  30. package/dist/goal/action-packet.js.map +1 -0
  31. package/dist/goal/audit.d.ts +14 -0
  32. package/dist/goal/audit.d.ts.map +1 -0
  33. package/dist/goal/audit.js +12 -0
  34. package/dist/goal/audit.js.map +1 -0
  35. package/dist/goal/constants.d.ts +26 -0
  36. package/dist/goal/constants.d.ts.map +1 -0
  37. package/dist/goal/constants.js +24 -0
  38. package/dist/goal/constants.js.map +1 -0
  39. package/dist/goal/paths.d.ts +8 -0
  40. package/dist/goal/paths.d.ts.map +1 -0
  41. package/dist/goal/paths.js +36 -0
  42. package/dist/goal/paths.js.map +1 -0
  43. package/dist/goal/preflight.d.ts +21 -0
  44. package/dist/goal/preflight.d.ts.map +1 -0
  45. package/dist/goal/preflight.js +97 -0
  46. package/dist/goal/preflight.js.map +1 -0
  47. package/dist/goal/review-seam.d.ts +44 -0
  48. package/dist/goal/review-seam.d.ts.map +1 -0
  49. package/dist/goal/review-seam.js +153 -0
  50. package/dist/goal/review-seam.js.map +1 -0
  51. package/dist/goal/reviewer-rules.d.ts +13 -0
  52. package/dist/goal/reviewer-rules.d.ts.map +1 -0
  53. package/dist/goal/reviewer-rules.js +153 -0
  54. package/dist/goal/reviewer-rules.js.map +1 -0
  55. package/dist/goal/reviewer.d.ts +9 -0
  56. package/dist/goal/reviewer.d.ts.map +1 -0
  57. package/dist/goal/reviewer.js +54 -0
  58. package/dist/goal/reviewer.js.map +1 -0
  59. package/dist/goal/runtime.d.ts +32 -0
  60. package/dist/goal/runtime.d.ts.map +1 -0
  61. package/dist/goal/runtime.js +242 -0
  62. package/dist/goal/runtime.js.map +1 -0
  63. package/dist/goal/sdk-client.d.ts +10 -0
  64. package/dist/goal/sdk-client.d.ts.map +1 -0
  65. package/dist/goal/sdk-client.js +34 -0
  66. package/dist/goal/sdk-client.js.map +1 -0
  67. package/dist/goal/state.d.ts +40 -0
  68. package/dist/goal/state.d.ts.map +1 -0
  69. package/dist/goal/state.js +37 -0
  70. package/dist/goal/state.js.map +1 -0
  71. package/dist/goal/task-triggers.d.ts +15 -0
  72. package/dist/goal/task-triggers.d.ts.map +1 -0
  73. package/dist/goal/task-triggers.js +23 -0
  74. package/dist/goal/task-triggers.js.map +1 -0
  75. package/dist/goal/walls.d.ts +12 -0
  76. package/dist/goal/walls.d.ts.map +1 -0
  77. package/dist/goal/walls.js +65 -0
  78. package/dist/goal/walls.js.map +1 -0
  79. package/dist/goal/worker-prompt.d.ts +13 -0
  80. package/dist/goal/worker-prompt.d.ts.map +1 -0
  81. package/dist/goal/worker-prompt.js +41 -0
  82. package/dist/goal/worker-prompt.js.map +1 -0
  83. package/dist/goal/worker.d.ts +42 -0
  84. package/dist/goal/worker.d.ts.map +1 -0
  85. package/dist/goal/worker.js +180 -0
  86. package/dist/goal/worker.js.map +1 -0
  87. package/dist/migrations/manifests/0.3.5.json +9 -0
  88. package/dist/migrations/manifests/0.3.6.json +9 -0
  89. package/dist/migrations/manifests/0.4.0.json +9 -0
  90. package/dist/templates/common/commands/continue.md +15 -3
  91. package/dist/templates/common/skills/check.md +7 -4
  92. package/dist/templates/cursor/agents/cstl-check.md +1 -0
  93. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -1
  94. package/dist/templates/cursor/fixtures/expected-rules.js +6 -1
  95. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -1
  96. package/dist/templates/cursor/rules/cstl-cursor-modes.mdc +32 -0
  97. package/dist/templates/cursor/rules/cstl-triage.mdc +2 -4
  98. package/dist/templates/cursor/rules/retrieval-routing.mdc +26 -7
  99. package/dist/templates/markdown/index.d.ts +9 -0
  100. package/dist/templates/markdown/index.d.ts.map +1 -1
  101. package/dist/templates/markdown/index.js +9 -0
  102. package/dist/templates/markdown/index.js.map +1 -1
  103. package/dist/templates/markdown/spec/guides/artifact-locale-guide.md.txt +93 -0
  104. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +7 -7
  105. package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +38 -21
  106. package/dist/templates/markdown/spec/guides/cursor-native-modes-guide.md.txt +125 -0
  107. package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +10 -8
  108. package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
  109. package/dist/templates/markdown/spec/guides/goal-release-regression-runbook.md.txt +132 -0
  110. package/dist/templates/markdown/spec/guides/index.md.txt +144 -104
  111. package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
  112. package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
  113. package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +28 -6
  114. package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +138 -0
  115. package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +185 -0
  116. package/dist/templates/shared-hooks/session-start.py +10 -0
  117. package/dist/templates/trellis/config.yaml +18 -0
  118. package/dist/templates/trellis/index.d.ts +17 -0
  119. package/dist/templates/trellis/index.d.ts.map +1 -1
  120. package/dist/templates/trellis/index.js +40 -0
  121. package/dist/templates/trellis/index.js.map +1 -1
  122. package/dist/templates/trellis/pool/README.md +103 -0
  123. package/dist/templates/trellis/pool/items/.gitkeep +0 -0
  124. package/dist/templates/trellis/pool/plan.md +26 -0
  125. package/dist/templates/trellis/scripts/common/artifact_locale.py +278 -0
  126. package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +37 -2
  127. package/dist/templates/trellis/scripts/common/config.py +39 -0
  128. package/dist/templates/trellis/scripts/common/injection_budget.py +317 -0
  129. package/dist/templates/trellis/scripts/common/parent_orchestration.py +161 -5
  130. package/dist/templates/trellis/scripts/common/pool_store.py +702 -0
  131. package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +9 -3
  132. package/dist/templates/trellis/scripts/common/session_context.py +32 -4
  133. package/dist/templates/trellis/scripts/common/session_memory.py +26 -0
  134. package/dist/templates/trellis/scripts/common/subagent_dispatch.py +74 -9
  135. package/dist/templates/trellis/scripts/common/task_context.py +18 -0
  136. package/dist/templates/trellis/scripts/common/task_dashboard.py +24 -1
  137. package/dist/templates/trellis/scripts/common/task_dependencies.py +673 -0
  138. package/dist/templates/trellis/scripts/common/task_gates.py +58 -5
  139. package/dist/templates/trellis/scripts/common/task_map.py +40 -8
  140. package/dist/templates/trellis/scripts/common/task_store.py +400 -39
  141. package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +489 -0
  142. package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
  143. package/dist/templates/trellis/scripts/common/test_pool_store.py +428 -0
  144. package/dist/templates/trellis/scripts/common/test_task_dependencies.py +345 -0
  145. package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +14 -12
  146. package/dist/templates/trellis/scripts/generate_dispatch_prompt.py +182 -0
  147. package/dist/templates/trellis/scripts/injection_budget_probe.py +69 -0
  148. package/dist/templates/trellis/scripts/pool.py +192 -0
  149. package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +3 -3
  150. package/dist/templates/trellis/scripts/spec_health_outcomes.py +160 -0
  151. package/dist/templates/trellis/scripts/task.py +85 -1
  152. package/dist/templates/trellis/scripts/verify_evidence_probe.py +138 -0
  153. package/dist/templates/trellis/tasks/locale/en/default-prd.md +19 -0
  154. package/dist/templates/trellis/tasks/locale/zh/default-prd.md +19 -0
  155. package/dist/templates/trellis/workflow.md +55 -5
  156. package/package.json +2 -2
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Review-pool CLI: validate / plan-check / link / unlink / show.
5
+
6
+ Usage:
7
+ python pool.py validate [--root <repo>]
8
+ python pool.py plan-check [--root <repo>]
9
+ python pool.py link <item-id> <task-ref> [--root <repo>]
10
+ python pool.py unlink <item-id> <task-ref> [--root <repo>]
11
+ python pool.py show <item-id> [--root <repo>]
12
+
13
+ Exit codes: any error-level finding (or a failed link/unlink/show) -> 1;
14
+ warnings only -> 0.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ from common.io import read_json
24
+ from common.log import Colors, colored
25
+ from common.paths import get_repo_root
26
+ from common import pool_store
27
+
28
+
29
+ def _print_issue(issue: pool_store.Issue) -> None:
30
+ color = Colors.RED if issue.is_error else Colors.YELLOW
31
+ level = "error" if issue.is_error else "warning"
32
+ location = f" ({issue.path})" if issue.path is not None else ""
33
+ print(colored(f"[{level}] {issue.code}: {issue.message}{location}", color))
34
+
35
+
36
+ def _exit_for_issues(issues: list[pool_store.Issue]) -> int:
37
+ errors = [issue for issue in issues if issue.is_error]
38
+ warnings = [issue for issue in issues if not issue.is_error]
39
+ for issue in issues:
40
+ _print_issue(issue)
41
+ if not issues:
42
+ print(colored("OK", Colors.GREEN))
43
+ elif not errors:
44
+ print(colored(f"OK with {len(warnings)} warning(s)", Colors.GREEN))
45
+ else:
46
+ print(colored(f"{len(errors)} error(s), {len(warnings)} warning(s)", Colors.RED))
47
+ return 1 if errors else 0
48
+
49
+
50
+ def cmd_validate(args: argparse.Namespace) -> int:
51
+ issues = pool_store.validate_pool(args.root)
52
+ return _exit_for_issues(issues)
53
+
54
+
55
+ def cmd_plan_check(args: argparse.Namespace) -> int:
56
+ issues = pool_store.check_plan(args.root)
57
+ return _exit_for_issues(issues)
58
+
59
+
60
+ def cmd_link(args: argparse.Namespace) -> int:
61
+ result = pool_store.link_item_task(args.root, args.item_id, args.task_ref)
62
+ for issue in result.errors:
63
+ _print_issue(issue)
64
+ if not result.ok:
65
+ return 1
66
+ print(colored(f"linked {result.item_id} <-> {result.task_ref}", Colors.GREEN))
67
+ item = pool_store.load_item(args.root, result.item_id)
68
+ if item is not None:
69
+ print(f" item.linked_tasks: {item.linked_tasks or '[]'}")
70
+ data = read_json(result.task_path / "task.json") if result.task_path else None
71
+ if data is not None:
72
+ print(f" task.meta.pool_items: {pool_store.get_pool_items(data) or '[]'}")
73
+ return 0
74
+
75
+
76
+ def cmd_unlink(args: argparse.Namespace) -> int:
77
+ result = pool_store.unlink_item_task(args.root, args.item_id, args.task_ref)
78
+ for issue in result.errors:
79
+ _print_issue(issue)
80
+ if not result.ok:
81
+ return 1
82
+ for issue in result.warnings:
83
+ _print_issue(issue)
84
+ print(colored(f"unlinked {result.item_id} <-> {result.task_ref}", Colors.GREEN))
85
+ return 0
86
+
87
+
88
+ def cmd_show(args: argparse.Namespace) -> int:
89
+ item = pool_store.load_item(args.root, args.item_id)
90
+ if item is None:
91
+ print(
92
+ colored(
93
+ f"[error] item-not-found: pool item {args.item_id!r} not found",
94
+ Colors.RED,
95
+ ),
96
+ file=sys.stderr,
97
+ )
98
+ return 1
99
+ tasks_dir = args.root / ".cstl" / "tasks"
100
+ print(f"id: {item.id or '<missing>'}")
101
+ print(f"title: {item.title or '<missing>'}")
102
+ print(f"status: {item.status or '<missing>'}")
103
+ print(f"type: {item.type or '<missing>'}")
104
+ print(f"path: {item.path}")
105
+ if not item.linked_tasks:
106
+ print("linked_tasks: []")
107
+ for ref in item.linked_tasks:
108
+ task_dir = pool_store.find_task_dir(ref, tasks_dir)
109
+ if task_dir is None:
110
+ print(f"linked_tasks: {ref} (missing)")
111
+ continue
112
+ data = read_json(task_dir / "task.json") or {}
113
+ status = data.get("status", "?")
114
+ archived = "archive/" in task_dir.as_posix()
115
+ suffix = " (archived)" if archived else ""
116
+ print(f"linked_tasks: {ref} -> {task_dir.name} ({status}){suffix}")
117
+ return 0
118
+
119
+
120
+ def build_parser() -> argparse.ArgumentParser:
121
+ parser = argparse.ArgumentParser(
122
+ prog="pool.py",
123
+ description="Review-pool maintenance CLI: validate, plan-check, link, unlink, show.",
124
+ )
125
+ parser.add_argument(
126
+ "--root",
127
+ type=Path,
128
+ default=None,
129
+ help="Repository root. Defaults to nearest parent containing .cstl/.",
130
+ )
131
+ subparsers = parser.add_subparsers(dest="command", required=True)
132
+
133
+ def add_root(entry_parser: argparse.ArgumentParser) -> None:
134
+ # SUPPRESS so a missing subcommand --root never clobbers the global
135
+ # --root with None (argparse subparser defaults overwrite parent values).
136
+ entry_parser.add_argument(
137
+ "--root",
138
+ type=Path,
139
+ default=argparse.SUPPRESS,
140
+ help="Repository root (overrides the global --root).",
141
+ )
142
+
143
+ validate_parser = subparsers.add_parser(
144
+ "validate",
145
+ help="Validate pool entries (sections, frontmatter, links, consistency).",
146
+ )
147
+ add_root(validate_parser)
148
+ plan_parser = subparsers.add_parser(
149
+ "plan-check",
150
+ help="Check plan.md references against known pool item ids.",
151
+ )
152
+ add_root(plan_parser)
153
+ link_parser = subparsers.add_parser(
154
+ "link",
155
+ help="Link a pool item to a task on both sides (idempotent).",
156
+ )
157
+ add_root(link_parser)
158
+ link_parser.add_argument("item_id", help="Pool item id, e.g. P01")
159
+ link_parser.add_argument("task_ref", help="Task dir name or resolvable ref")
160
+ unlink_parser = subparsers.add_parser(
161
+ "unlink",
162
+ help="Remove the item<->task link on both sides (best-effort).",
163
+ )
164
+ add_root(unlink_parser)
165
+ unlink_parser.add_argument("item_id", help="Pool item id, e.g. P01")
166
+ unlink_parser.add_argument("task_ref", help="Task dir name or resolvable ref")
167
+ show_parser = subparsers.add_parser(
168
+ "show",
169
+ help="Print one pool item with linked task statuses.",
170
+ )
171
+ add_root(show_parser)
172
+ show_parser.add_argument("item_id", help="Pool item id, e.g. P01")
173
+ return parser
174
+
175
+
176
+ def main(argv: list[str] | None = None) -> int:
177
+ args = build_parser().parse_args(argv)
178
+ root = getattr(args, "root", None)
179
+ args.root = root.resolve() if root is not None else get_repo_root()
180
+
181
+ handlers = {
182
+ "validate": cmd_validate,
183
+ "plan-check": cmd_plan_check,
184
+ "link": cmd_link,
185
+ "unlink": cmd_unlink,
186
+ "show": cmd_show,
187
+ }
188
+ return handlers[args.command](args)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ 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())
@@ -17,6 +17,8 @@ Usage:
17
17
  python task.py set-branch <dir> <branch> # Set git branch
18
18
  python task.py set-base-branch <dir> <branch> # Set PR target branch
19
19
  python task.py set-scope <dir> <scope> # Set scope for PR title
20
+ python task.py set-deps <dir> <id...> # Set task-level depends_on (soft checks)
21
+ python task.py set-depends-mode <dir> <warn|block|off> # Dependency policy mode
20
22
  python task.py archive <task-dir> [--check] [--archive-integrated-children] # Check or archive completed task
21
23
  python task.py prepare-archive-evidence <task-dir> [--dry-run] # Draft missing verify.md archive evidence
22
24
  python task.py prepare-learning-scaffold <task-dir> [--trigger <text>] # Print spec-capture checklist (stdout only)
@@ -69,6 +71,7 @@ from common.task_gates import (
69
71
  write_gate_record,
70
72
  )
71
73
  from common.task_utils import resolve_task_dir, run_task_hooks
74
+ from common.task_dependencies import read_depends_mode
72
75
  from common.tasks import (
73
76
  children_progress,
74
77
  format_child_task_display,
@@ -86,6 +89,9 @@ from common.task_store import (
86
89
  cmd_set_branch,
87
90
  cmd_set_base_branch,
88
91
  cmd_set_scope,
92
+ cmd_set_deps,
93
+ cmd_set_depends_mode,
94
+ append_depends_ignore_event,
89
95
  cmd_add_subtask,
90
96
  cmd_remove_subtask,
91
97
  cmd_prepare_child_worktree,
@@ -97,6 +103,7 @@ from common.task_store import (
97
103
  cmd_parent_status,
98
104
  cmd_publish_pack,
99
105
  cmd_review_child,
106
+ cmd_artifact_locale,
100
107
  )
101
108
  from common.task_context import (
102
109
  cmd_add_context,
@@ -240,6 +247,15 @@ def _print_guard_errors(items: list[str], stream=None) -> None:
240
247
  print(f" - {item}", file=stream)
241
248
 
242
249
 
250
+ def _print_dependency_warnings(guard) -> None:
251
+ """Print dependency warnings (Plan A soft checks; never affect PASS/FAIL)."""
252
+ for warning in guard.warnings:
253
+ print(
254
+ colored(f"[dependencies] WARN: {warning}", Colors.YELLOW),
255
+ file=sys.stderr,
256
+ )
257
+
258
+
243
259
  def cmd_start_execution(args: argparse.Namespace) -> int:
244
260
  """Start approved task execution after a non-mutating readiness check."""
245
261
  repo_root = get_repo_root()
@@ -258,6 +274,7 @@ def cmd_start_execution(args: argparse.Namespace) -> int:
258
274
  for hint in start_execution_repair_hints(guard.errors, task_dir):
259
275
  print(f" Hint: {hint}")
260
276
  return 1
277
+ _print_dependency_warnings(guard)
261
278
  print(colored("Start-execution check: PASS", Colors.GREEN))
262
279
  print(f"Contract fingerprint: {guard.contract_fingerprint}")
263
280
  baseline_fingerprint = guard.artifact_fingerprints.get(BASELINE_GATE)
@@ -293,13 +310,29 @@ def cmd_start_execution(args: argparse.Namespace) -> int:
293
310
  print("Run with --check for non-mutating preflight or --approved after explicit user approval.", file=sys.stderr)
294
311
  return 1
295
312
 
296
- guard = validate_start_execution(task_dir, task_data, approved=True)
313
+ ignore_deps = bool(getattr(args, "ignore_deps", False))
314
+ guard = validate_start_execution(
315
+ task_dir,
316
+ task_data,
317
+ approved=True,
318
+ ignore_deps=ignore_deps,
319
+ enforce_deps_block=True,
320
+ )
297
321
  if not guard.ok:
298
322
  print(colored("Error: cannot start execution; readiness check failed.", Colors.RED), file=sys.stderr)
299
323
  _print_guard_errors(guard.errors, stream=sys.stderr)
300
324
  return 1
301
325
 
326
+ _print_dependency_warnings(guard)
327
+
302
328
  assert task_data is not None
329
+ if ignore_deps and guard.deps_blocking_summary:
330
+ append_depends_ignore_event(
331
+ task_data,
332
+ command="start-execution",
333
+ mode=read_depends_mode(task_data),
334
+ blocking_summary=guard.deps_blocking_summary,
335
+ )
303
336
  if guard.baseline_record:
304
337
  write_gate_record(task_data, "start-execution", BASELINE_GATE, guard.baseline_record)
305
338
  for gate, record in guard.auto_gate_records.items():
@@ -536,6 +569,7 @@ Usage:
536
569
  python task.py set-branch <dir> <branch> Set git branch
537
570
  python task.py set-base-branch <dir> <branch> Set PR target branch
538
571
  python task.py set-scope <dir> <scope> Set scope for PR title
572
+ python task.py set-deps <dir> [dep ...] Set depends_on (no deps clears to [])
539
573
  python task.py archive <task-dir> [--check] Check or archive completed task
540
574
  python task.py add-subtask <parent> <child> Link child task to parent
541
575
  python task.py remove-subtask <parent> <child> Unlink child from parent
@@ -671,6 +705,11 @@ def main() -> int:
671
705
  help="Run non-mutating execution readiness check")
672
706
  p_start_execution.add_argument("--approved", action="store_true",
673
707
  help="Record explicit approval and start execution")
708
+ p_start_execution.add_argument(
709
+ "--ignore-deps",
710
+ action="store_true",
711
+ help="Override blocking dependencies (depends_mode=block) and record an audit event",
712
+ )
674
713
 
675
714
  # record-gate
676
715
  p_record_gate = subparsers.add_parser("record-gate", help="Record reviewer quality gate")
@@ -707,6 +746,43 @@ def main() -> int:
707
746
  p_scope.add_argument("dir", help="Task directory")
708
747
  p_scope.add_argument("scope", help="Scope name")
709
748
 
749
+ # set-deps
750
+ p_deps = subparsers.add_parser(
751
+ "set-deps",
752
+ help="Set task-level depends_on (declare only; soft checks in Plan A)",
753
+ )
754
+ p_deps.add_argument("dir", help="Task directory or name")
755
+ p_deps.add_argument(
756
+ "dep",
757
+ nargs="*",
758
+ help="Dependency task ids (bare id or pool:Pxx). No deps clears to [].",
759
+ )
760
+
761
+ # set-depends-mode
762
+ p_depends_mode = subparsers.add_parser(
763
+ "set-depends-mode",
764
+ help="Set meta.depends_mode (warn | block | off) for dependency policy",
765
+ )
766
+ p_depends_mode.add_argument("dir", help="Task directory or name")
767
+ p_depends_mode.add_argument(
768
+ "mode",
769
+ choices=["warn", "block", "off"],
770
+ help="warn = Plan A soft checks (default); block = hard gates at "
771
+ "start-execution --approved and set-child-state working; off = silent",
772
+ )
773
+
774
+ # artifact-locale
775
+ p_artifact_locale = subparsers.add_parser(
776
+ "artifact-locale",
777
+ help="Get or set human-reviewed artifact locale (zh | en)",
778
+ )
779
+ locale_sub = p_artifact_locale.add_subparsers(dest="artifact_locale_command")
780
+ p_locale_get = locale_sub.add_parser("get", help="Print resolved artifact locale")
781
+ p_locale_get.add_argument("--task", help="Task directory (includes task override)")
782
+ p_locale_set = locale_sub.add_parser("set", help="Set artifact locale")
783
+ p_locale_set.add_argument("locale", choices=["zh", "en"], help="Locale code")
784
+ p_locale_set.add_argument("--task", help="Task directory (task-level override)")
785
+
710
786
  # archive
711
787
  p_archive = subparsers.add_parser("archive", help="Archive task")
712
788
  p_archive.add_argument("name", help="Task directory or name")
@@ -782,6 +858,11 @@ def main() -> int:
782
858
  p_integrate_child.add_argument("--ref", help="Child git ref or reviewed diff reference")
783
859
  p_integrate_child.add_argument("--reason", help="Optional short reason")
784
860
  p_integrate_child.add_argument("--execute-merge", action="store_true", help="Execute git merge --no-ff --no-commit for an integrated Child")
861
+ p_integrate_child.add_argument(
862
+ "--no-publish-pack",
863
+ action="store_true",
864
+ help="Skip auto publish-pack after integrated (default: refresh child-prompts/ including evidence-only integrates)",
865
+ )
785
866
  p_integrate_child.add_argument("--check", action="store_true", help="Run non-mutating integration readiness check")
786
867
 
787
868
  # suggest-execution-strategy
@@ -921,6 +1002,8 @@ def main() -> int:
921
1002
  "set-branch": cmd_set_branch,
922
1003
  "set-base-branch": cmd_set_base_branch,
923
1004
  "set-scope": cmd_set_scope,
1005
+ "set-deps": cmd_set_deps,
1006
+ "set-depends-mode": cmd_set_depends_mode,
924
1007
  "archive": cmd_archive,
925
1008
  "prepare-archive-evidence": cmd_prepare_archive_evidence,
926
1009
  "prepare-learning-scaffold": cmd_prepare_learning_scaffold,
@@ -935,6 +1018,7 @@ def main() -> int:
935
1018
  "parent-status": cmd_parent_status,
936
1019
  "publish-pack": cmd_publish_pack,
937
1020
  "review-child": cmd_review_child,
1021
+ "artifact-locale": cmd_artifact_locale,
938
1022
  "list": cmd_list,
939
1023
  "list-archive": cmd_list_archive,
940
1024
  }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """Verification evidence probe — repeatable false-green detection.
3
+
4
+ Evaluates a fixture verify.md against closeout-profile evidence requirements
5
+ by reusing task_gates.verify_evidence_status() (no duplicate rule set).
6
+
7
+ Exit 0 when actual outcome matches --expect; exit 1 on mismatch or error.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import io
14
+ import shutil
15
+ import sys
16
+ import tempfile
17
+ from pathlib import Path
18
+
19
+ _SCRIPTS = Path(__file__).resolve().parent
20
+ if str(_SCRIPTS) not in sys.path:
21
+ sys.path.insert(0, str(_SCRIPTS))
22
+
23
+ from common.task_gates import verify_evidence_status # noqa: E402
24
+
25
+ if sys.platform.startswith("win"):
26
+ for _stream_name in ("stdin", "stdout", "stderr"):
27
+ _stream = getattr(sys, _stream_name, None)
28
+ if _stream is None and _stream_name != "stdin":
29
+ continue
30
+ if _stream is None:
31
+ continue
32
+ if hasattr(_stream, "reconfigure"):
33
+ try:
34
+ _stream.reconfigure(encoding="utf-8", errors="replace")
35
+ except Exception:
36
+ pass
37
+ elif hasattr(_stream, "detach"):
38
+ try:
39
+ setattr(
40
+ sys,
41
+ _stream_name,
42
+ io.TextIOWrapper(_stream.detach(), encoding="utf-8", errors="replace"),
43
+ )
44
+ except Exception:
45
+ pass
46
+
47
+ PROFILE_REQUIRED_SIGNALS: dict[str, list[str]] = {
48
+ "lite": ["validation", "acceptance", "durable_learning"],
49
+ "full": [
50
+ "validation",
51
+ "acceptance",
52
+ "durable_learning",
53
+ "check_evidence",
54
+ "reviewed_change_set",
55
+ ],
56
+ "parent": [
57
+ "validation",
58
+ "acceptance",
59
+ "durable_learning",
60
+ "integration",
61
+ ],
62
+ }
63
+
64
+
65
+ def evaluate_fixture(fixture_path: Path, profile: str) -> tuple[bool, dict[str, bool], list[str]]:
66
+ """Return (passes, status_map, missing_signals) for a fixture verify.md."""
67
+ profile = profile.lower().strip()
68
+ if profile not in PROFILE_REQUIRED_SIGNALS:
69
+ raise ValueError(f"unknown profile: {profile}")
70
+
71
+ required = PROFILE_REQUIRED_SIGNALS[profile]
72
+ task_data: dict = {"children": ["probe-child"] if profile == "parent" else []}
73
+
74
+ with tempfile.TemporaryDirectory(prefix="cstl-verify-probe-") as tmp:
75
+ task_dir = Path(tmp)
76
+ shutil.copy2(fixture_path, task_dir / "verify.md")
77
+ status = verify_evidence_status(task_dir, task_data)
78
+
79
+ missing = [signal for signal in required if not status.get(signal)]
80
+ return len(missing) == 0, status, missing
81
+
82
+
83
+ def main(argv: list[str] | None = None) -> int:
84
+ parser = argparse.ArgumentParser(
85
+ description="Probe verify.md fixture against closeout evidence requirements.",
86
+ )
87
+ parser.add_argument(
88
+ "--fixture",
89
+ required=True,
90
+ type=Path,
91
+ help="Path to fixture verify.md",
92
+ )
93
+ parser.add_argument(
94
+ "--profile",
95
+ required=True,
96
+ choices=sorted(PROFILE_REQUIRED_SIGNALS),
97
+ help="Closeout profile to evaluate against",
98
+ )
99
+ parser.add_argument(
100
+ "--expect",
101
+ required=True,
102
+ choices=("pass", "fail"),
103
+ help="Expected probe outcome",
104
+ )
105
+ args = parser.parse_args(argv)
106
+
107
+ fixture = args.fixture.resolve()
108
+ if not fixture.is_file():
109
+ print(f"verify-evidence-probe: fixture not found: {fixture}", file=sys.stderr)
110
+ return 2
111
+
112
+ try:
113
+ passes, status, missing = evaluate_fixture(fixture, args.profile)
114
+ except ValueError as exc:
115
+ print(f"verify-evidence-probe: {exc}", file=sys.stderr)
116
+ return 2
117
+
118
+ actual = "pass" if passes else "fail"
119
+ expected = args.expect.lower()
120
+
121
+ print(f"fixture: {fixture}")
122
+ print(f"profile: {args.profile}")
123
+ print(f"status: {status}")
124
+ if missing:
125
+ print(f"missing: {', '.join(missing)}")
126
+ print(f"actual: {actual}")
127
+ print(f"expect: {expected}")
128
+
129
+ if actual == expected:
130
+ print("result: OK")
131
+ return 0
132
+
133
+ print("result: MISMATCH", file=sys.stderr)
134
+ return 1
135
+
136
+
137
+ if __name__ == "__main__":
138
+ raise SystemExit(main())