@deftai/directive-content 0.59.0 → 0.61.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/.githooks/pre-commit +10 -128
- package/.githooks/pre-push +8 -108
- package/Taskfile.yml +48 -58
- package/UPGRADING.md +19 -3
- package/docs/assets/directive-lifecycle-diagram.png +0 -0
- package/docs/directive-lifecycle.md +73 -0
- package/docs/getting-started.md +5 -1
- package/package.json +3 -3
- package/packs/skills/skills-pack-0.1.json +1 -1
- package/packs/strategies/strategies-pack-0.1.json +19 -19
- package/scm/github.md +37 -6
- package/skills/deft-directive-setup/SKILL.md +24 -15
- package/strategies/speckit.md +14 -14
- package/strategies/v0-20-contract.md +12 -1
- package/tasks/change.yml +16 -31
- package/tasks/ci.yml +8 -0
- package/tasks/commit.yml +12 -19
- package/tasks/core.yml +10 -0
- package/tasks/engine.yml +42 -0
- package/tasks/framework.yml +3 -0
- package/tasks/install.yml +20 -19
- package/tasks/migrate.yml +26 -15
- package/tasks/project.yml +26 -0
- package/tasks/toolchain.yml +15 -5
- package/tasks/vbrief.yml +4 -3
- package/tasks/verify.yml +12 -14
- package/templates/agents-entry.md +1 -1
- package/scripts/_agents_md.py +0 -494
- package/scripts/_cache_fetch.py +0 -635
- package/scripts/_cache_quota.py +0 -529
- package/scripts/_cache_refresh.py +0 -163
- package/scripts/_cache_validate.py +0 -209
- package/scripts/_content_root.py +0 -42
- package/scripts/_doctor_state.py +0 -277
- package/scripts/_event_detect.py +0 -305
- package/scripts/_events.py +0 -514
- package/scripts/_lifecycle_hygiene.py +0 -568
- package/scripts/_pathspec.py +0 -91
- package/scripts/_policy_show_cli.py +0 -266
- package/scripts/_precutover.py +0 -92
- package/scripts/_project_context.py +0 -224
- package/scripts/_project_definition_io.py +0 -164
- package/scripts/_relocate_snapshot.py +0 -209
- package/scripts/_relocate_states.py +0 -343
- package/scripts/_resolve_preflight_path.py +0 -152
- package/scripts/_safe_subprocess.py +0 -167
- package/scripts/_session_start_hook.py +0 -205
- package/scripts/_sor_gate_diff.py +0 -365
- package/scripts/_stdio_utf8.py +0 -59
- package/scripts/_triage_bootstrap_gitignore.py +0 -904
- package/scripts/_triage_classify_cli.py +0 -122
- package/scripts/_triage_queue_cli.py +0 -625
- package/scripts/_triage_scope_cli.py +0 -343
- package/scripts/_triage_scope_drift_cli.py +0 -121
- package/scripts/_triage_scope_ignores.py +0 -286
- package/scripts/_triage_scope_milestone.py +0 -432
- package/scripts/_triage_scope_mutations.py +0 -337
- package/scripts/_triage_scope_renderers.py +0 -207
- package/scripts/_triage_smoketest_stages.py +0 -674
- package/scripts/_triage_subscribe_cli.py +0 -140
- package/scripts/_triage_welcome_cli.py +0 -421
- package/scripts/_vbrief_build.py +0 -239
- package/scripts/_vbrief_fidelity.py +0 -479
- package/scripts/_vbrief_legacy.py +0 -589
- package/scripts/_vbrief_reconciliation.py +0 -883
- package/scripts/_vbrief_routing.py +0 -277
- package/scripts/_vbrief_safety.py +0 -778
- package/scripts/_vbrief_sources.py +0 -312
- package/scripts/_vbrief_speckit.py +0 -262
- package/scripts/_vbrief_story_quality.py +0 -353
- package/scripts/_vbrief_validation.py +0 -299
- package/scripts/build_dist.py +0 -412
- package/scripts/cache.py +0 -1078
- package/scripts/cache_scanner.py +0 -745
- package/scripts/candidates_log.py +0 -432
- package/scripts/capacity_backfill.py +0 -680
- package/scripts/capacity_show.py +0 -653
- package/scripts/ci_local.py +0 -689
- package/scripts/code_structure_validate.py +0 -765
- package/scripts/codebase_default_extractor.py +0 -495
- package/scripts/codebase_map.py +0 -304
- package/scripts/codebase_map_fresh.py +0 -104
- package/scripts/codebase_projection_registry.py +0 -94
- package/scripts/codebase_provider.py +0 -582
- package/scripts/doctor.py +0 -2552
- package/scripts/framework_commands.py +0 -505
- package/scripts/gh_rest.py +0 -882
- package/scripts/github_auth_modes.py +0 -437
- package/scripts/github_body.py +0 -292
- package/scripts/ip_risk.py +0 -531
- package/scripts/issue_emit.py +0 -670
- package/scripts/issue_ingest.py +0 -1064
- package/scripts/migrate_preflight.py +0 -418
- package/scripts/migrate_vbrief.py +0 -2677
- package/scripts/monitor_pr.py +0 -401
- package/scripts/pack_migrate_lessons.py +0 -336
- package/scripts/pack_migrate_patterns.py +0 -254
- package/scripts/pack_migrate_rules.py +0 -350
- package/scripts/pack_migrate_skills.py +0 -423
- package/scripts/pack_migrate_strategies.py +0 -311
- package/scripts/pack_migrate_swarm_spec.py +0 -250
- package/scripts/pack_render.py +0 -434
- package/scripts/packs_slice.py +0 -712
- package/scripts/platform_capabilities.py +0 -336
- package/scripts/policy.py +0 -2826
- package/scripts/policy_set.py +0 -324
- package/scripts/pr_check_closing_keywords.py +0 -524
- package/scripts/pr_check_protected_issues.py +0 -267
- package/scripts/pr_merge_readiness.py +0 -1004
- package/scripts/pr_wait_mergeable.py +0 -669
- package/scripts/prd_render.py +0 -159
- package/scripts/preflight_architecture_sor.py +0 -974
- package/scripts/preflight_branch.py +0 -289
- package/scripts/preflight_cache.py +0 -974
- package/scripts/preflight_gh.py +0 -721
- package/scripts/preflight_implementation.py +0 -272
- package/scripts/preflight_story_start.py +0 -838
- package/scripts/preflight_wip_cap.py +0 -149
- package/scripts/probe_session.py +0 -545
- package/scripts/project_render.py +0 -293
- package/scripts/quarantine_ext.py +0 -237
- package/scripts/reconcile_issues.py +0 -1442
- package/scripts/refresh-path.ps1 +0 -107
- package/scripts/release.py +0 -2030
- package/scripts/release_e2e.py +0 -1011
- package/scripts/release_publish.py +0 -486
- package/scripts/release_rollback.py +0 -980
- package/scripts/relocate.py +0 -1034
- package/scripts/resolve_changelog_unreleased.py +0 -667
- package/scripts/resolve_version.py +0 -490
- package/scripts/resume_conditions.py +0 -706
- package/scripts/ritual_sentinel.py +0 -609
- package/scripts/roadmap_render.py +0 -635
- package/scripts/rule_ownership_lint.py +0 -325
- package/scripts/scm.py +0 -591
- package/scripts/scope_audit_log.py +0 -387
- package/scripts/scope_decompose.py +0 -654
- package/scripts/scope_demote.py +0 -509
- package/scripts/scope_lifecycle.py +0 -1126
- package/scripts/scope_undo.py +0 -772
- package/scripts/session_start.py +0 -406
- package/scripts/setup_ghx.py +0 -339
- package/scripts/setup_windows.ps1 +0 -220
- package/scripts/slice_audit.py +0 -585
- package/scripts/slice_record.py +0 -530
- package/scripts/slice_record_existing.py +0 -692
- package/scripts/slug_normalize.py +0 -178
- package/scripts/spec_render.py +0 -477
- package/scripts/spec_validate.py +0 -238
- package/scripts/subagent_monitor.py +0 -658
- package/scripts/swarm_complete_cohort.py +0 -644
- package/scripts/swarm_launch.py +0 -1206
- package/scripts/swarm_readiness.py +0 -554
- package/scripts/swarm_verify_review_clean.py +0 -438
- package/scripts/swarm_worktrees.py +0 -497
- package/scripts/toolchain-check.py +0 -52
- package/scripts/triage_actions.py +0 -871
- package/scripts/triage_bootstrap.py +0 -1153
- package/scripts/triage_bulk.py +0 -630
- package/scripts/triage_classify.py +0 -932
- package/scripts/triage_help.py +0 -1685
- package/scripts/triage_queue.py +0 -1944
- package/scripts/triage_reconcile.py +0 -581
- package/scripts/triage_refresh.py +0 -643
- package/scripts/triage_scope.py +0 -999
- package/scripts/triage_scope_drift.py +0 -575
- package/scripts/triage_smoketest.py +0 -396
- package/scripts/triage_subscribe.py +0 -399
- package/scripts/triage_summary.py +0 -1011
- package/scripts/triage_welcome.py +0 -1178
- package/scripts/ts_check_lane.py +0 -86
- package/scripts/validate-links.py +0 -64
- package/scripts/validate_strategy_output.py +0 -212
- package/scripts/vbrief_activate.py +0 -228
- package/scripts/vbrief_migrate_conformance.py +0 -368
- package/scripts/vbrief_reconcile_graph.py +0 -306
- package/scripts/vbrief_reconcile_labels.py +0 -460
- package/scripts/vbrief_reconcile_umbrellas.py +0 -741
- package/scripts/vbrief_validate.py +0 -1144
- package/scripts/verify-stubs.py +0 -61
- package/scripts/verify_capacity.py +0 -160
- package/scripts/verify_encoding.py +0 -699
- package/scripts/verify_hooks_installed.py +0 -206
- package/scripts/verify_investigation.py +0 -360
- package/scripts/verify_judgment_gates.py +0 -827
- package/scripts/verify_no_task_runtime.py +0 -171
- package/scripts/verify_scm_boundary.py +0 -509
- package/scripts/verify_session_ritual.py +0 -389
- package/scripts/verify_tools.py +0 -426
- package/scripts/verify_vbrief_conformance.py +0 -478
package/scripts/packs_slice.py
DELETED
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""packs_slice.py -- named, structured slice access to a content pack (#1283, #1294).
|
|
3
|
-
|
|
4
|
-
Implements ``task packs:slice <pack> <name> [-- <filters>]``: the agent-facing
|
|
5
|
-
Layer-B slice surface from ADR-001 / the #1283 converged design.
|
|
6
|
-
|
|
7
|
-
Design contract (#1283):
|
|
8
|
-
- The agent-facing API is the slice NAME only (``recent``, ``by-tag``). The
|
|
9
|
-
dotted path + filter dialect are pack-author implementation detail declared
|
|
10
|
-
in the pack's JSON Schema ``x-sliceRegistry`` block -- NOT JSONPath, NOT a
|
|
11
|
-
query language exposed to consumers.
|
|
12
|
-
- Slices read the CANONICAL pack source (JSON) directly, NEVER the rendered
|
|
13
|
-
``.md`` projection. Reading source guarantees byte-stable, drift-free slices.
|
|
14
|
-
- Output is ``text`` by default (cheapest for the read-into-context path the
|
|
15
|
-
ADR optimises) with ``--json`` / ``--format json`` for harness consumers.
|
|
16
|
-
- Every result carries provenance: ``pack``, ``slice``, ``source`` (path),
|
|
17
|
-
``source_sha`` (sha256 of the source file).
|
|
18
|
-
- ``--list`` discovers slice names + one-liners; an unknown slice exits 2 with
|
|
19
|
-
a did-you-mean suggestion. Three-state exit: 0 ok / 2 usage error.
|
|
20
|
-
- ``--list-packs`` discovers the available packs (short-name + version +
|
|
21
|
-
one-liner) by scanning the on-disk pack registry (``packs/*/`` sources +
|
|
22
|
-
``vbrief/schemas/*-pack.schema.json``). It is registry-driven / self-
|
|
23
|
-
extending: a new pack appears with no code change here (#1637).
|
|
24
|
-
|
|
25
|
-
Exit codes:
|
|
26
|
-
0 -- ok
|
|
27
|
-
2 -- usage error (unknown pack/slice, bad filter, malformed --since, ...)
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
from __future__ import annotations
|
|
31
|
-
|
|
32
|
-
import argparse
|
|
33
|
-
import difflib
|
|
34
|
-
import hashlib
|
|
35
|
-
import json
|
|
36
|
-
import re
|
|
37
|
-
import sys
|
|
38
|
-
from pathlib import Path
|
|
39
|
-
from typing import Any
|
|
40
|
-
|
|
41
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
42
|
-
|
|
43
|
-
from _content_root import content_root # noqa: E402
|
|
44
|
-
|
|
45
|
-
# Repo root resolved from this file's location (scripts/ -> repo root) so pack
|
|
46
|
-
# source / schema paths are CWD-independent.
|
|
47
|
-
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
48
|
-
# Shippable content lives under content/ in the source repo and at the framework
|
|
49
|
-
# root in a flattened consumer deposit (#1875 C1); resolve both contexts.
|
|
50
|
-
CONTENT_ROOT = content_root(REPO_ROOT)
|
|
51
|
-
|
|
52
|
-
# Pack short-name -> on-disk source + schema. The slice surface resolves the
|
|
53
|
-
# canonical source (never the rendered .md) and the schema-declared registry.
|
|
54
|
-
PACK_REGISTRY: dict[str, dict[str, Path]] = {
|
|
55
|
-
"lessons": {
|
|
56
|
-
"source": CONTENT_ROOT / "packs" / "lessons" / "lessons-pack-0.1.json",
|
|
57
|
-
"schema": CONTENT_ROOT / "vbrief" / "schemas" / "lessons-pack.schema.json",
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# Pack-LEVEL discovery (``--list-packs``, #1637) scans the on-disk pack
|
|
62
|
-
# registry rather than the hardcoded ``PACK_REGISTRY`` above so a new pack
|
|
63
|
-
# (e.g. a future skills-pack / rules-pack) appears WITHOUT any code change
|
|
64
|
-
# here: drop ``packs/<name>/<name>-pack-*.json`` + its
|
|
65
|
-
# ``vbrief/schemas/<name>-pack.schema.json`` and ``--list-packs`` lists it.
|
|
66
|
-
PACKS_DIR = CONTENT_ROOT / "packs"
|
|
67
|
-
SCHEMAS_DIR = CONTENT_ROOT / "vbrief" / "schemas"
|
|
68
|
-
|
|
69
|
-
_SINCE_RE = re.compile(r"^\d{4}-\d{2}(-\d{2})?$")
|
|
70
|
-
|
|
71
|
-
# Fallback display spec used when a pack schema declares no ``x-display`` block
|
|
72
|
-
# (and when ``format_slice_text`` is called without one). Mirrors the lessons
|
|
73
|
-
# pack's render shape so the legacy single-arg call sites stay byte-stable.
|
|
74
|
-
_DEFAULT_DISPLAY: dict[str, Any] = {
|
|
75
|
-
"heading": "title",
|
|
76
|
-
"fields": [],
|
|
77
|
-
"body": "body",
|
|
78
|
-
"noun": "lessons",
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class UsageError(Exception):
|
|
83
|
-
"""A recoverable usage error -- mapped to exit code 2 in ``main``."""
|
|
84
|
-
|
|
85
|
-
def __init__(self, message: str, suggestion: str | None = None) -> None:
|
|
86
|
-
super().__init__(message)
|
|
87
|
-
self.message = message
|
|
88
|
-
self.suggestion = suggestion
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def sha256_file(path: Path) -> str:
|
|
92
|
-
"""Return the hex sha256 of a file's bytes."""
|
|
93
|
-
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _rel_to_repo(path: Path) -> str:
|
|
97
|
-
"""Return a repo-relative POSIX path string for provenance, or the name."""
|
|
98
|
-
try:
|
|
99
|
-
return path.resolve().relative_to(REPO_ROOT).as_posix()
|
|
100
|
-
except ValueError:
|
|
101
|
-
return path.name
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def resolve_pack(pack_name: str) -> tuple[Path, Path]:
|
|
105
|
-
"""Resolve a pack short-name to its (source, schema) paths.
|
|
106
|
-
|
|
107
|
-
Resolution is self-extending (#1295): the hardcoded ``PACK_REGISTRY`` is an
|
|
108
|
-
override / fast-path (it is also the monkeypatch seam the tests use), but any
|
|
109
|
-
pack that ships ``packs/<name>/<name>-pack-*.json`` plus a companion
|
|
110
|
-
``vbrief/schemas/<name>-pack.schema.json`` resolves with NO code change here
|
|
111
|
-
-- the same registry-driven contract ``--list-packs`` already honours. Raises
|
|
112
|
-
``UsageError`` (with a did-you-mean suggestion) for an unknown pack.
|
|
113
|
-
"""
|
|
114
|
-
if pack_name in PACK_REGISTRY:
|
|
115
|
-
entry = PACK_REGISTRY[pack_name]
|
|
116
|
-
return entry["source"], entry["schema"]
|
|
117
|
-
|
|
118
|
-
pack_dir = PACKS_DIR / pack_name
|
|
119
|
-
sources = sorted(pack_dir.glob("*.json")) if pack_dir.is_dir() else []
|
|
120
|
-
schema_path = SCHEMAS_DIR / f"{pack_name}-pack.schema.json"
|
|
121
|
-
if sources and schema_path.is_file():
|
|
122
|
-
return sources[0], schema_path
|
|
123
|
-
|
|
124
|
-
known = sorted({*PACK_REGISTRY, *(p["name"] for p in discover_packs())})
|
|
125
|
-
suggestions = difflib.get_close_matches(pack_name, known, n=1)
|
|
126
|
-
raise UsageError(
|
|
127
|
-
f"unknown pack '{pack_name}'",
|
|
128
|
-
suggestion=suggestions[0] if suggestions else None,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def load_display(schema_path: Path) -> dict[str, Any]:
|
|
133
|
-
"""Load the schema-declared ``x-display`` block (slice text-render hints).
|
|
134
|
-
|
|
135
|
-
Falls back to the lessons-shaped ``_DEFAULT_DISPLAY`` when a pack schema
|
|
136
|
-
omits the block, so the slice formatter is pack-agnostic without requiring
|
|
137
|
-
every pack to declare it.
|
|
138
|
-
"""
|
|
139
|
-
if not schema_path.is_file():
|
|
140
|
-
raise UsageError(f"pack schema not found: {schema_path}")
|
|
141
|
-
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
142
|
-
display = schema.get("x-display")
|
|
143
|
-
if not isinstance(display, dict):
|
|
144
|
-
return dict(_DEFAULT_DISPLAY)
|
|
145
|
-
return display
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def load_registry(schema_path: Path) -> dict[str, dict[str, Any]]:
|
|
149
|
-
"""Load the schema-declared ``x-sliceRegistry`` map from a pack schema."""
|
|
150
|
-
if not schema_path.is_file():
|
|
151
|
-
raise UsageError(f"pack schema not found: {schema_path}")
|
|
152
|
-
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
153
|
-
registry = schema.get("x-sliceRegistry")
|
|
154
|
-
if not isinstance(registry, dict):
|
|
155
|
-
raise UsageError(f"pack schema has no x-sliceRegistry: {schema_path}")
|
|
156
|
-
return registry
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def load_source(source_path: Path) -> dict[str, Any]:
|
|
160
|
-
"""Load the canonical pack source JSON (never the rendered .md)."""
|
|
161
|
-
if not source_path.is_file():
|
|
162
|
-
raise UsageError(f"pack source not found: {source_path}")
|
|
163
|
-
data: dict[str, Any] = json.loads(source_path.read_text(encoding="utf-8"))
|
|
164
|
-
return data
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def resolve_dotted_path(data: Any, dotted: str) -> Any:
|
|
168
|
-
"""Walk a constrained dotted path into ``data`` with ``.get()`` guards.
|
|
169
|
-
|
|
170
|
-
Each segment indexes a mapping; a missing / non-mapping step yields ``None``.
|
|
171
|
-
This is the constrained dotted-path dialect from #1283 -- NOT JSONPath.
|
|
172
|
-
"""
|
|
173
|
-
current = data
|
|
174
|
-
for segment in dotted.split("."):
|
|
175
|
-
if isinstance(current, dict):
|
|
176
|
-
current = current.get(segment)
|
|
177
|
-
else:
|
|
178
|
-
return None
|
|
179
|
-
return current
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def apply_since(entries: list[dict], since: str) -> list[dict]:
|
|
183
|
-
"""Filter entries to those dated on or after ``since`` (year-month grain).
|
|
184
|
-
|
|
185
|
-
``since`` may be ``YYYY-MM`` or ``YYYY-MM-DD``; comparison is at month
|
|
186
|
-
granularity (the entries' date grain). Null-dated entries are excluded.
|
|
187
|
-
"""
|
|
188
|
-
since_ym = since[:7]
|
|
189
|
-
return [e for e in entries if e.get("date") and e["date"] >= since_ym]
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def apply_tags(entries: list[dict], tags: list[str]) -> list[dict]:
|
|
193
|
-
"""Filter entries to those carrying any of the requested ``tags``."""
|
|
194
|
-
wanted = set(tags)
|
|
195
|
-
return [e for e in entries if wanted & set(e.get("tags", []))]
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def apply_triggers(entries: list[dict], triggers: list[str]) -> list[dict]:
|
|
199
|
-
"""Filter entries to those whose ``triggers`` include any requested value.
|
|
200
|
-
|
|
201
|
-
Matching is case-insensitive exact membership: the agent passes a routing
|
|
202
|
-
keyword from the AGENTS.md Skill Routing table and gets back the skill(s)
|
|
203
|
-
that keyword routes to.
|
|
204
|
-
"""
|
|
205
|
-
wanted = {t.lower() for t in triggers}
|
|
206
|
-
return [
|
|
207
|
-
e
|
|
208
|
-
for e in entries
|
|
209
|
-
if wanted & {str(t).lower() for t in e.get("triggers", [])}
|
|
210
|
-
]
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def apply_scalar(entries: list[dict], field: str, values: list[str]) -> list[dict]:
|
|
214
|
-
"""Filter entries whose scalar ``field`` matches any requested value.
|
|
215
|
-
|
|
216
|
-
Case-insensitive exact match on a single-valued field (e.g. the rules pack's
|
|
217
|
-
``tier`` and ``domain``), as opposed to the list-membership filters above.
|
|
218
|
-
"""
|
|
219
|
-
wanted = {v.lower() for v in values}
|
|
220
|
-
return [e for e in entries if str(e.get(field, "")).lower() in wanted]
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def _normalize_issue(value: str) -> str:
|
|
224
|
-
"""Normalise an issue reference for comparison (strip leading '#', lower)."""
|
|
225
|
-
return str(value).lstrip("#").strip().lower()
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def apply_issue_refs(entries: list[dict], issues: list[str]) -> list[dict]:
|
|
229
|
-
"""Filter entries whose ``issue_refs`` include any requested issue number.
|
|
230
|
-
|
|
231
|
-
The lessons pack stores issue refs as ``"#754"`` strings; the agent passes a
|
|
232
|
-
bare or hashed number (``754`` / ``#754``) and both sides are normalised so
|
|
233
|
-
``--issue 754`` matches ``"#754"``. List-membership semantics (#1637).
|
|
234
|
-
"""
|
|
235
|
-
wanted = {_normalize_issue(i) for i in issues}
|
|
236
|
-
return [
|
|
237
|
-
e
|
|
238
|
-
for e in entries
|
|
239
|
-
if wanted & {_normalize_issue(r) for r in e.get("issue_refs", [])}
|
|
240
|
-
]
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def apply_select(entries: list[dict], select: dict[str, Any]) -> list[dict]:
|
|
244
|
-
"""Apply a slice's fixed (argument-less) predicate from its registry spec.
|
|
245
|
-
|
|
246
|
-
Some deeper slices (#1637) subset WITHOUT an agent-supplied filter -- the
|
|
247
|
-
predicate is baked into the slice name. ``select`` declares it in the pack
|
|
248
|
-
schema's ``x-sliceRegistry`` entry. Supported keys:
|
|
249
|
-
|
|
250
|
-
- ``tier_in``: keep entries whose ``tier`` is in the listed values
|
|
251
|
-
(case-insensitive). Powers the rules pack ``must`` / ``prohibitions``.
|
|
252
|
-
- ``body_contains_any``: keep entries whose ``body`` (case-insensitive)
|
|
253
|
-
contains any listed substring. Powers the lessons pack ``anti-patterns``.
|
|
254
|
-
|
|
255
|
-
The agent-facing contract stays the slice NAME only -- ``select`` is a
|
|
256
|
-
pack-author authoring detail, never exposed as a query language (ADR-001).
|
|
257
|
-
"""
|
|
258
|
-
result = entries
|
|
259
|
-
tier_in = select.get("tier_in")
|
|
260
|
-
if isinstance(tier_in, list) and tier_in:
|
|
261
|
-
wanted = {str(t).lower() for t in tier_in}
|
|
262
|
-
result = [e for e in result if str(e.get("tier", "")).lower() in wanted]
|
|
263
|
-
needles = select.get("body_contains_any")
|
|
264
|
-
if isinstance(needles, list) and needles:
|
|
265
|
-
lowered = [str(n).lower() for n in needles]
|
|
266
|
-
result = [
|
|
267
|
-
e
|
|
268
|
-
for e in result
|
|
269
|
-
if any(n in str(e.get("body") or "").lower() for n in lowered)
|
|
270
|
-
]
|
|
271
|
-
return result
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def _validate_filters(
|
|
275
|
-
slice_name: str,
|
|
276
|
-
allowed: list[str],
|
|
277
|
-
*,
|
|
278
|
-
since: str | None,
|
|
279
|
-
tags: list[str] | None,
|
|
280
|
-
triggers: list[str] | None,
|
|
281
|
-
tiers: list[str] | None,
|
|
282
|
-
domains: list[str] | None,
|
|
283
|
-
issues: list[str] | None,
|
|
284
|
-
ids: list[str] | None,
|
|
285
|
-
) -> None:
|
|
286
|
-
"""Reject filters not declared for this slice in the registry."""
|
|
287
|
-
provided: list[str] = []
|
|
288
|
-
if since is not None:
|
|
289
|
-
provided.append("since")
|
|
290
|
-
if tags:
|
|
291
|
-
provided.append("tag")
|
|
292
|
-
if triggers:
|
|
293
|
-
provided.append("trigger")
|
|
294
|
-
if tiers:
|
|
295
|
-
provided.append("tier")
|
|
296
|
-
if domains:
|
|
297
|
-
provided.append("domain")
|
|
298
|
-
if issues:
|
|
299
|
-
provided.append("issue")
|
|
300
|
-
if ids:
|
|
301
|
-
provided.append("id")
|
|
302
|
-
for filt in provided:
|
|
303
|
-
if filt not in allowed:
|
|
304
|
-
raise UsageError(
|
|
305
|
-
f"slice '{slice_name}' does not support the --{filt} filter "
|
|
306
|
-
f"(allowed: {', '.join(allowed) or 'none'})"
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def slice_pack(
|
|
311
|
-
pack_id: str,
|
|
312
|
-
slice_name: str,
|
|
313
|
-
registry: dict[str, dict[str, Any]],
|
|
314
|
-
source_data: dict[str, Any],
|
|
315
|
-
source_path: Path,
|
|
316
|
-
*,
|
|
317
|
-
since: str | None = None,
|
|
318
|
-
tags: list[str] | None = None,
|
|
319
|
-
triggers: list[str] | None = None,
|
|
320
|
-
tiers: list[str] | None = None,
|
|
321
|
-
domains: list[str] | None = None,
|
|
322
|
-
issues: list[str] | None = None,
|
|
323
|
-
ids: list[str] | None = None,
|
|
324
|
-
) -> dict[str, Any]:
|
|
325
|
-
"""Resolve and execute a named slice, returning a provenance-tagged result.
|
|
326
|
-
|
|
327
|
-
Raises ``UsageError`` for an unknown slice (with did-you-mean), an
|
|
328
|
-
unsupported filter, or a malformed ``--since`` value.
|
|
329
|
-
"""
|
|
330
|
-
if slice_name not in registry:
|
|
331
|
-
suggestions = difflib.get_close_matches(slice_name, registry, n=1)
|
|
332
|
-
raise UsageError(
|
|
333
|
-
f"unknown slice '{slice_name}' for pack {pack_id}",
|
|
334
|
-
suggestion=suggestions[0] if suggestions else None,
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
spec = registry[slice_name]
|
|
338
|
-
allowed = spec.get("filters", [])
|
|
339
|
-
_validate_filters(
|
|
340
|
-
slice_name,
|
|
341
|
-
allowed,
|
|
342
|
-
since=since,
|
|
343
|
-
tags=tags,
|
|
344
|
-
triggers=triggers,
|
|
345
|
-
tiers=tiers,
|
|
346
|
-
domains=domains,
|
|
347
|
-
issues=issues,
|
|
348
|
-
ids=ids,
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
if since is not None and not _SINCE_RE.match(since):
|
|
352
|
-
raise UsageError(f"--since must be YYYY-MM or YYYY-MM-DD, got '{since}'")
|
|
353
|
-
|
|
354
|
-
resolved = resolve_dotted_path(source_data, spec["path"])
|
|
355
|
-
entries: list[dict] = list(resolved) if isinstance(resolved, list) else []
|
|
356
|
-
|
|
357
|
-
# Fixed (argument-less) predicate baked into the slice name (#1637): applied
|
|
358
|
-
# before the agent-supplied filters so a `must` / `anti-patterns` slice
|
|
359
|
-
# subsets with no flags.
|
|
360
|
-
select = spec.get("select")
|
|
361
|
-
if isinstance(select, dict):
|
|
362
|
-
entries = apply_select(entries, select)
|
|
363
|
-
|
|
364
|
-
if since is not None:
|
|
365
|
-
entries = apply_since(entries, since)
|
|
366
|
-
if tags:
|
|
367
|
-
entries = apply_tags(entries, tags)
|
|
368
|
-
if triggers:
|
|
369
|
-
entries = apply_triggers(entries, triggers)
|
|
370
|
-
if tiers:
|
|
371
|
-
entries = apply_scalar(entries, "tier", tiers)
|
|
372
|
-
if domains:
|
|
373
|
-
entries = apply_scalar(entries, "domain", domains)
|
|
374
|
-
if issues:
|
|
375
|
-
entries = apply_issue_refs(entries, issues)
|
|
376
|
-
if ids:
|
|
377
|
-
entries = apply_scalar(entries, "id", ids)
|
|
378
|
-
|
|
379
|
-
return {
|
|
380
|
-
"pack": pack_id,
|
|
381
|
-
"slice": slice_name,
|
|
382
|
-
"source": _rel_to_repo(source_path),
|
|
383
|
-
"source_sha": sha256_file(source_path),
|
|
384
|
-
"count": len(entries),
|
|
385
|
-
"results": entries,
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def list_slices(
|
|
390
|
-
pack_id: str,
|
|
391
|
-
registry: dict[str, dict[str, Any]],
|
|
392
|
-
source_path: Path,
|
|
393
|
-
) -> dict[str, Any]:
|
|
394
|
-
"""Build the ``--list`` discovery payload for a pack."""
|
|
395
|
-
slices = [
|
|
396
|
-
{
|
|
397
|
-
"name": name,
|
|
398
|
-
"description": spec.get("description", ""),
|
|
399
|
-
"filters": spec.get("filters", []),
|
|
400
|
-
}
|
|
401
|
-
for name, spec in sorted(registry.items())
|
|
402
|
-
]
|
|
403
|
-
return {
|
|
404
|
-
"pack": pack_id,
|
|
405
|
-
"source": _rel_to_repo(source_path),
|
|
406
|
-
"source_sha": sha256_file(source_path),
|
|
407
|
-
"slices": slices,
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def _one_line(text: str) -> str:
|
|
412
|
-
"""Collapse whitespace and return the first sentence of ``text``.
|
|
413
|
-
|
|
414
|
-
Pack descriptions in the schemas are multi-paragraph; ``--list-packs``
|
|
415
|
-
wants a single token-cheap one-liner, so take the leading sentence (up to
|
|
416
|
-
the first period-space) of the whitespace-folded text.
|
|
417
|
-
"""
|
|
418
|
-
folded = " ".join(text.split())
|
|
419
|
-
head = folded.split(". ", 1)[0]
|
|
420
|
-
return head.rstrip(".") if head else ""
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def discover_packs(
|
|
424
|
-
packs_dir: Path = PACKS_DIR,
|
|
425
|
-
schemas_dir: Path = SCHEMAS_DIR,
|
|
426
|
-
) -> list[dict[str, Any]]:
|
|
427
|
-
"""Scan the on-disk pack registry and return sorted pack descriptors.
|
|
428
|
-
|
|
429
|
-
Registry-driven / self-extending (#1637): each ``packs/<name>/`` directory
|
|
430
|
-
holding a canonical ``*.json`` source is a pack. The short-name is the
|
|
431
|
-
directory name, the version comes from the source's ``version`` field, and
|
|
432
|
-
the one-line description is read from the companion
|
|
433
|
-
``vbrief/schemas/<name>-pack.schema.json`` (its ``description`` / ``title``).
|
|
434
|
-
A pack added later appears here with NO code change.
|
|
435
|
-
"""
|
|
436
|
-
packs: list[dict[str, Any]] = []
|
|
437
|
-
if not packs_dir.is_dir():
|
|
438
|
-
return packs
|
|
439
|
-
for pack_dir in sorted(packs_dir.iterdir()):
|
|
440
|
-
if not pack_dir.is_dir():
|
|
441
|
-
continue
|
|
442
|
-
short_name = pack_dir.name
|
|
443
|
-
sources = sorted(pack_dir.glob("*.json"))
|
|
444
|
-
if not sources:
|
|
445
|
-
continue
|
|
446
|
-
source_path = sources[0]
|
|
447
|
-
try:
|
|
448
|
-
source_data = json.loads(source_path.read_text(encoding="utf-8"))
|
|
449
|
-
except (OSError, ValueError):
|
|
450
|
-
continue
|
|
451
|
-
pack_id = source_data.get("pack", short_name)
|
|
452
|
-
version = str(source_data.get("version", ""))
|
|
453
|
-
|
|
454
|
-
description = ""
|
|
455
|
-
schema_path = schemas_dir / f"{short_name}-pack.schema.json"
|
|
456
|
-
if schema_path.is_file():
|
|
457
|
-
try:
|
|
458
|
-
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
459
|
-
description = _one_line(
|
|
460
|
-
schema.get("description") or schema.get("title") or ""
|
|
461
|
-
)
|
|
462
|
-
except (OSError, ValueError):
|
|
463
|
-
description = ""
|
|
464
|
-
|
|
465
|
-
packs.append(
|
|
466
|
-
{
|
|
467
|
-
"name": short_name,
|
|
468
|
-
"pack": pack_id,
|
|
469
|
-
"version": version,
|
|
470
|
-
"description": description,
|
|
471
|
-
"source": _rel_to_repo(source_path),
|
|
472
|
-
}
|
|
473
|
-
)
|
|
474
|
-
return packs
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
def list_packs(
|
|
478
|
-
packs_dir: Path = PACKS_DIR,
|
|
479
|
-
schemas_dir: Path = SCHEMAS_DIR,
|
|
480
|
-
) -> dict[str, Any]:
|
|
481
|
-
"""Build the ``--list-packs`` discovery payload (registry-driven)."""
|
|
482
|
-
return {"packs": discover_packs(packs_dir, schemas_dir)}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def format_list_packs_text(payload: dict[str, Any]) -> str:
|
|
486
|
-
"""Render the ``--list-packs`` discovery payload as text."""
|
|
487
|
-
packs = payload["packs"]
|
|
488
|
-
if not packs:
|
|
489
|
-
return "No content packs found.\n"
|
|
490
|
-
lines = ["Available content packs:"]
|
|
491
|
-
name_w = max(len(p["name"]) for p in packs)
|
|
492
|
-
ver_w = max(len(p["version"]) for p in packs)
|
|
493
|
-
for p in packs:
|
|
494
|
-
lines.append(
|
|
495
|
-
f" {p['name']:<{name_w}} {p['version']:<{ver_w}} {p['description']}"
|
|
496
|
-
)
|
|
497
|
-
return "\n".join(lines) + "\n"
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
def format_slice_text(
|
|
501
|
-
result: dict[str, Any], display: dict[str, Any] | None = None
|
|
502
|
-
) -> str:
|
|
503
|
-
"""Render a slice result as token-efficient text with a provenance header.
|
|
504
|
-
|
|
505
|
-
The entry shape is driven by the pack schema's ``x-display`` block
|
|
506
|
-
(``heading`` field, optional labelled ``fields``, optional ``body`` field,
|
|
507
|
-
and the ``noun`` used in the empty-result line) so the formatter is
|
|
508
|
-
pack-agnostic. When ``display`` is omitted it falls back to the
|
|
509
|
-
lessons-shaped default, keeping legacy call sites byte-stable.
|
|
510
|
-
"""
|
|
511
|
-
display = display or _DEFAULT_DISPLAY
|
|
512
|
-
header = (
|
|
513
|
-
f"# pack: {result['pack']} | slice: {result['slice']} | "
|
|
514
|
-
f"source: {result['source']} | source_sha: {result['source_sha']} | "
|
|
515
|
-
f"{result['count']} result(s)"
|
|
516
|
-
)
|
|
517
|
-
noun = display.get("noun", "entries")
|
|
518
|
-
if not result["results"]:
|
|
519
|
-
return f"{header}\n\n(no matching {noun})"
|
|
520
|
-
|
|
521
|
-
heading_field = display.get("heading", "title")
|
|
522
|
-
field_specs: list[str] = display.get("fields", [])
|
|
523
|
-
body_field = display.get("body")
|
|
524
|
-
|
|
525
|
-
parts = [header]
|
|
526
|
-
for entry in result["results"]:
|
|
527
|
-
block = f"\n## {entry.get(heading_field)}\n"
|
|
528
|
-
field_lines: list[str] = []
|
|
529
|
-
for field in field_specs:
|
|
530
|
-
value = entry.get(field)
|
|
531
|
-
if value in (None, "", []):
|
|
532
|
-
continue
|
|
533
|
-
if isinstance(value, list):
|
|
534
|
-
value = ", ".join(str(v) for v in value)
|
|
535
|
-
field_lines.append(f"- {field}: {value}")
|
|
536
|
-
if field_lines:
|
|
537
|
-
block += "\n" + "\n".join(field_lines) + "\n"
|
|
538
|
-
if body_field:
|
|
539
|
-
body = entry.get(body_field)
|
|
540
|
-
if body:
|
|
541
|
-
block += f"\n{body}\n"
|
|
542
|
-
parts.append(block)
|
|
543
|
-
return "".join(parts)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
def format_list_text(payload: dict[str, Any]) -> str:
|
|
547
|
-
"""Render the ``--list`` discovery payload as text."""
|
|
548
|
-
lines = [f"Slices for pack {payload['pack']} (source: {payload['source']}):"]
|
|
549
|
-
width = max((len(s["name"]) for s in payload["slices"]), default=0)
|
|
550
|
-
for s in payload["slices"]:
|
|
551
|
-
filters = ", ".join(s["filters"]) or "none"
|
|
552
|
-
lines.append(f" {s['name']:<{width}} {s['description']} [filters: {filters}]")
|
|
553
|
-
return "\n".join(lines) + "\n"
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
def _build_parser() -> argparse.ArgumentParser:
|
|
557
|
-
parser = argparse.ArgumentParser(
|
|
558
|
-
prog="packs_slice.py",
|
|
559
|
-
description="Named, structured slice access to a content pack (#1283).",
|
|
560
|
-
)
|
|
561
|
-
parser.add_argument(
|
|
562
|
-
"pack",
|
|
563
|
-
nargs="?",
|
|
564
|
-
help="Pack short-name (e.g. 'lessons'). Omit with --list-packs.",
|
|
565
|
-
)
|
|
566
|
-
parser.add_argument(
|
|
567
|
-
"name",
|
|
568
|
-
nargs="?",
|
|
569
|
-
help="Slice name (e.g. 'recent', 'by-tag'). Omit with --list.",
|
|
570
|
-
)
|
|
571
|
-
parser.add_argument("--since", help="recent filter: YYYY-MM or YYYY-MM-DD.")
|
|
572
|
-
parser.add_argument(
|
|
573
|
-
"--tag",
|
|
574
|
-
action="append",
|
|
575
|
-
default=[],
|
|
576
|
-
help="by-tag filter: tag value (repeatable or comma-listed).",
|
|
577
|
-
)
|
|
578
|
-
parser.add_argument(
|
|
579
|
-
"--trigger",
|
|
580
|
-
action="append",
|
|
581
|
-
default=[],
|
|
582
|
-
help="by-trigger filter: routing keyword (repeatable or comma-listed).",
|
|
583
|
-
)
|
|
584
|
-
parser.add_argument(
|
|
585
|
-
"--tier",
|
|
586
|
-
action="append",
|
|
587
|
-
default=[],
|
|
588
|
-
help="by-tier filter: RFC2119 tier (e.g. MUST; repeatable or comma-listed).",
|
|
589
|
-
)
|
|
590
|
-
parser.add_argument(
|
|
591
|
-
"--domain",
|
|
592
|
-
action="append",
|
|
593
|
-
default=[],
|
|
594
|
-
help="by-domain filter: source doc stem (e.g. testing; repeatable or comma-listed).",
|
|
595
|
-
)
|
|
596
|
-
parser.add_argument(
|
|
597
|
-
"--issue",
|
|
598
|
-
action="append",
|
|
599
|
-
default=[],
|
|
600
|
-
help="by-issue filter: issue number, bare or hashed (e.g. 754; repeatable/comma).",
|
|
601
|
-
)
|
|
602
|
-
parser.add_argument(
|
|
603
|
-
"--id",
|
|
604
|
-
action="append",
|
|
605
|
-
default=[],
|
|
606
|
-
dest="ids",
|
|
607
|
-
help="by-id filter: entry id (e.g. deft-directive-cost; repeatable or comma-listed).",
|
|
608
|
-
)
|
|
609
|
-
parser.add_argument(
|
|
610
|
-
"--format",
|
|
611
|
-
choices=("text", "json"),
|
|
612
|
-
default="text",
|
|
613
|
-
help="Output format (default: text).",
|
|
614
|
-
)
|
|
615
|
-
parser.add_argument(
|
|
616
|
-
"--json",
|
|
617
|
-
action="store_true",
|
|
618
|
-
help="Alias for --format json.",
|
|
619
|
-
)
|
|
620
|
-
parser.add_argument(
|
|
621
|
-
"--list",
|
|
622
|
-
action="store_true",
|
|
623
|
-
dest="list_slices",
|
|
624
|
-
help="List the pack's slice names + descriptions, then exit.",
|
|
625
|
-
)
|
|
626
|
-
parser.add_argument(
|
|
627
|
-
"--list-packs",
|
|
628
|
-
action="store_true",
|
|
629
|
-
dest="list_packs",
|
|
630
|
-
help="List the available packs (name + version + one-liner), then exit.",
|
|
631
|
-
)
|
|
632
|
-
return parser
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
def _collect_tags(raw: list[str]) -> list[str]:
|
|
636
|
-
"""Flatten repeated / comma-listed --tag values into a normalised list."""
|
|
637
|
-
out: list[str] = []
|
|
638
|
-
for item in raw:
|
|
639
|
-
out.extend(t.strip().lower() for t in item.split(",") if t.strip())
|
|
640
|
-
return out
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
def main(argv: list[str] | None = None) -> int:
|
|
644
|
-
parser = _build_parser()
|
|
645
|
-
args = parser.parse_args(argv)
|
|
646
|
-
|
|
647
|
-
fmt = "json" if args.json else args.format
|
|
648
|
-
tags = _collect_tags(args.tag)
|
|
649
|
-
triggers = _collect_tags(args.trigger)
|
|
650
|
-
tiers = _collect_tags(args.tier)
|
|
651
|
-
domains = _collect_tags(args.domain)
|
|
652
|
-
issues = _collect_tags(args.issue)
|
|
653
|
-
ids = _collect_tags(args.ids)
|
|
654
|
-
|
|
655
|
-
try:
|
|
656
|
-
if args.list_packs:
|
|
657
|
-
payload = list_packs()
|
|
658
|
-
if fmt == "json":
|
|
659
|
-
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
660
|
-
else:
|
|
661
|
-
print(format_list_packs_text(payload), end="")
|
|
662
|
-
return 0
|
|
663
|
-
|
|
664
|
-
if not args.pack:
|
|
665
|
-
raise UsageError("a pack name is required (or pass --list-packs)")
|
|
666
|
-
|
|
667
|
-
source_path, schema_path = resolve_pack(args.pack)
|
|
668
|
-
registry = load_registry(schema_path)
|
|
669
|
-
display = load_display(schema_path)
|
|
670
|
-
source_data = load_source(source_path)
|
|
671
|
-
pack_id = source_data.get("pack", args.pack)
|
|
672
|
-
|
|
673
|
-
if args.list_slices:
|
|
674
|
-
payload = list_slices(pack_id, registry, source_path)
|
|
675
|
-
if fmt == "json":
|
|
676
|
-
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
677
|
-
else:
|
|
678
|
-
print(format_list_text(payload), end="")
|
|
679
|
-
return 0
|
|
680
|
-
|
|
681
|
-
if not args.name:
|
|
682
|
-
raise UsageError("a slice name is required (or pass --list)")
|
|
683
|
-
|
|
684
|
-
result = slice_pack(
|
|
685
|
-
pack_id,
|
|
686
|
-
args.name,
|
|
687
|
-
registry,
|
|
688
|
-
source_data,
|
|
689
|
-
source_path,
|
|
690
|
-
since=args.since,
|
|
691
|
-
tags=tags,
|
|
692
|
-
triggers=triggers,
|
|
693
|
-
tiers=tiers,
|
|
694
|
-
domains=domains,
|
|
695
|
-
issues=issues,
|
|
696
|
-
ids=ids,
|
|
697
|
-
)
|
|
698
|
-
if fmt == "json":
|
|
699
|
-
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
700
|
-
else:
|
|
701
|
-
print(format_slice_text(result, display))
|
|
702
|
-
return 0
|
|
703
|
-
except UsageError as exc:
|
|
704
|
-
msg = f"error: {exc.message}"
|
|
705
|
-
if exc.suggestion:
|
|
706
|
-
msg += f". Did you mean '{exc.suggestion}'?"
|
|
707
|
-
print(msg, file=sys.stderr)
|
|
708
|
-
return 2
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if __name__ == "__main__":
|
|
712
|
-
raise SystemExit(main())
|