@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.
- package/CHANGELOG.md +69 -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/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +5 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +51 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/constants/paths.d.ts +4 -0
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -0
- package/dist/constants/paths.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/migrations/manifests/0.4.0.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 +9 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +9 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/spec/guides/artifact-locale-guide.md.txt +93 -0
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +7 -7
- 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/cursor-subagent-policy.md.txt +10 -8
- package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
- package/dist/templates/markdown/spec/guides/goal-release-regression-runbook.md.txt +132 -0
- package/dist/templates/markdown/spec/guides/index.md.txt +144 -104
- package/dist/templates/markdown/spec/guides/injection-budget-guide.md.txt +111 -0
- package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +28 -6
- package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +138 -0
- package/dist/templates/markdown/spec/guides/verification-strength-guide.md.txt +185 -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 +17 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +40 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/pool/README.md +103 -0
- package/dist/templates/trellis/pool/items/.gitkeep +0 -0
- package/dist/templates/trellis/pool/plan.md +26 -0
- 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/pool_store.py +702 -0
- 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 +24 -1
- package/dist/templates/trellis/scripts/common/task_dependencies.py +673 -0
- package/dist/templates/trellis/scripts/common/task_gates.py +58 -5
- package/dist/templates/trellis/scripts/common/task_map.py +40 -8
- package/dist/templates/trellis/scripts/common/task_store.py +400 -39
- package/dist/templates/trellis/scripts/common/test_depends_mode_block.py +489 -0
- package/dist/templates/trellis/scripts/common/test_observable_defaults.py +135 -0
- package/dist/templates/trellis/scripts/common/test_pool_store.py +428 -0
- package/dist/templates/trellis/scripts/common/test_task_dependencies.py +345 -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/pool.py +192 -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 +85 -1
- package/dist/templates/trellis/scripts/verify_evidence_probe.py +138 -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 +55 -5
- 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
|
|
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())
|
|
@@ -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
|
-
|
|
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())
|