@deftai/directive-content 0.55.1 → 0.56.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 +143 -0
- package/.githooks/pre-push +121 -0
- package/QUICK-START.md +13 -3
- package/Taskfile.yml +934 -0
- package/UPGRADING.md +82 -11
- package/events/README.md +3 -3
- package/package.json +5 -4
- package/packs/skills/skills-pack-0.1.json +22 -22
- package/scripts/_agents_md.py +494 -0
- package/scripts/_cache_fetch.py +635 -0
- package/scripts/_cache_quota.py +529 -0
- package/scripts/_cache_refresh.py +163 -0
- package/scripts/_cache_validate.py +209 -0
- package/scripts/_content_root.py +42 -0
- package/scripts/_doctor_state.py +277 -0
- package/scripts/_event_detect.py +305 -0
- package/scripts/_events.py +514 -0
- package/scripts/_lifecycle_hygiene.py +568 -0
- package/scripts/_pathspec.py +91 -0
- package/scripts/_policy_show_cli.py +266 -0
- package/scripts/_precutover.py +92 -0
- package/scripts/_project_context.py +224 -0
- package/scripts/_project_definition_io.py +164 -0
- package/scripts/_relocate_snapshot.py +209 -0
- package/scripts/_relocate_states.py +343 -0
- package/scripts/_resolve_preflight_path.py +152 -0
- package/scripts/_safe_subprocess.py +167 -0
- package/scripts/_session_start_hook.py +205 -0
- package/scripts/_sor_gate_diff.py +365 -0
- package/scripts/_stdio_utf8.py +59 -0
- package/scripts/_triage_bootstrap_gitignore.py +904 -0
- package/scripts/_triage_classify_cli.py +122 -0
- package/scripts/_triage_queue_cli.py +625 -0
- package/scripts/_triage_scope_cli.py +343 -0
- package/scripts/_triage_scope_drift_cli.py +121 -0
- package/scripts/_triage_scope_ignores.py +286 -0
- package/scripts/_triage_scope_milestone.py +432 -0
- package/scripts/_triage_scope_mutations.py +337 -0
- package/scripts/_triage_scope_renderers.py +207 -0
- package/scripts/_triage_smoketest_stages.py +674 -0
- package/scripts/_triage_subscribe_cli.py +140 -0
- package/scripts/_triage_welcome_cli.py +421 -0
- package/scripts/_vbrief_build.py +239 -0
- package/scripts/_vbrief_fidelity.py +479 -0
- package/scripts/_vbrief_legacy.py +589 -0
- package/scripts/_vbrief_reconciliation.py +883 -0
- package/scripts/_vbrief_routing.py +277 -0
- package/scripts/_vbrief_safety.py +778 -0
- package/scripts/_vbrief_sources.py +312 -0
- package/scripts/_vbrief_speckit.py +262 -0
- package/scripts/_vbrief_story_quality.py +353 -0
- package/scripts/_vbrief_validation.py +299 -0
- package/scripts/build_dist.py +412 -0
- package/scripts/cache.py +1078 -0
- package/scripts/cache_scanner.py +745 -0
- package/scripts/candidates_log.py +432 -0
- package/scripts/capacity_backfill.py +680 -0
- package/scripts/capacity_show.py +653 -0
- package/scripts/ci_local.py +689 -0
- package/scripts/code_structure_validate.py +765 -0
- package/scripts/codebase_default_extractor.py +495 -0
- package/scripts/codebase_map.py +304 -0
- package/scripts/codebase_map_fresh.py +104 -0
- package/scripts/codebase_projection_registry.py +94 -0
- package/scripts/codebase_provider.py +582 -0
- package/scripts/doctor.py +2257 -0
- package/scripts/framework_commands.py +505 -0
- package/scripts/gh_rest.py +882 -0
- package/scripts/github_auth_modes.py +437 -0
- package/scripts/github_body.py +292 -0
- package/scripts/ip_risk.py +531 -0
- package/scripts/issue_emit.py +670 -0
- package/scripts/issue_ingest.py +1064 -0
- package/scripts/migrate_preflight.py +418 -0
- package/scripts/migrate_vbrief.py +2677 -0
- package/scripts/monitor_pr.py +401 -0
- package/scripts/pack_migrate_lessons.py +336 -0
- package/scripts/pack_migrate_patterns.py +254 -0
- package/scripts/pack_migrate_rules.py +350 -0
- package/scripts/pack_migrate_skills.py +423 -0
- package/scripts/pack_migrate_strategies.py +311 -0
- package/scripts/pack_migrate_swarm_spec.py +250 -0
- package/scripts/pack_render.py +434 -0
- package/scripts/packs_slice.py +712 -0
- package/scripts/platform_capabilities.py +336 -0
- package/scripts/policy.py +2826 -0
- package/scripts/policy_set.py +324 -0
- package/scripts/pr_check_closing_keywords.py +524 -0
- package/scripts/pr_check_protected_issues.py +267 -0
- package/scripts/pr_merge_readiness.py +1004 -0
- package/scripts/pr_wait_mergeable.py +669 -0
- package/scripts/prd_render.py +159 -0
- package/scripts/preflight_architecture_sor.py +974 -0
- package/scripts/preflight_branch.py +289 -0
- package/scripts/preflight_cache.py +974 -0
- package/scripts/preflight_gh.py +721 -0
- package/scripts/preflight_implementation.py +272 -0
- package/scripts/preflight_story_start.py +838 -0
- package/scripts/preflight_wip_cap.py +149 -0
- package/scripts/probe_session.py +545 -0
- package/scripts/project_render.py +293 -0
- package/scripts/quarantine_ext.py +237 -0
- package/scripts/reconcile_issues.py +1442 -0
- package/scripts/refresh-path.ps1 +107 -0
- package/scripts/release.py +2030 -0
- package/scripts/release_e2e.py +1011 -0
- package/scripts/release_publish.py +486 -0
- package/scripts/release_rollback.py +980 -0
- package/scripts/relocate.py +1034 -0
- package/scripts/resolve_changelog_unreleased.py +667 -0
- package/scripts/resolve_version.py +490 -0
- package/scripts/resume_conditions.py +706 -0
- package/scripts/ritual_sentinel.py +609 -0
- package/scripts/roadmap_render.py +635 -0
- package/scripts/rule_ownership_lint.py +325 -0
- package/scripts/scm.py +591 -0
- package/scripts/scope_audit_log.py +387 -0
- package/scripts/scope_decompose.py +654 -0
- package/scripts/scope_demote.py +509 -0
- package/scripts/scope_lifecycle.py +1126 -0
- package/scripts/scope_undo.py +772 -0
- package/scripts/session_start.py +406 -0
- package/scripts/setup_ghx.py +339 -0
- package/scripts/setup_windows.ps1 +220 -0
- package/scripts/slice_audit.py +585 -0
- package/scripts/slice_record.py +530 -0
- package/scripts/slice_record_existing.py +692 -0
- package/scripts/slug_normalize.py +178 -0
- package/scripts/spec_render.py +477 -0
- package/scripts/spec_validate.py +238 -0
- package/scripts/subagent_monitor.py +658 -0
- package/scripts/swarm_complete_cohort.py +644 -0
- package/scripts/swarm_launch.py +1206 -0
- package/scripts/swarm_readiness.py +554 -0
- package/scripts/swarm_verify_review_clean.py +438 -0
- package/scripts/swarm_worktrees.py +497 -0
- package/scripts/toolchain-check.py +52 -0
- package/scripts/triage_actions.py +871 -0
- package/scripts/triage_bootstrap.py +1153 -0
- package/scripts/triage_bulk.py +630 -0
- package/scripts/triage_classify.py +932 -0
- package/scripts/triage_help.py +1685 -0
- package/scripts/triage_queue.py +1944 -0
- package/scripts/triage_reconcile.py +581 -0
- package/scripts/triage_refresh.py +643 -0
- package/scripts/triage_scope.py +999 -0
- package/scripts/triage_scope_drift.py +575 -0
- package/scripts/triage_smoketest.py +396 -0
- package/scripts/triage_subscribe.py +399 -0
- package/scripts/triage_summary.py +1011 -0
- package/scripts/triage_welcome.py +1178 -0
- package/scripts/ts_check_lane.py +86 -0
- package/scripts/validate-links.py +64 -0
- package/scripts/validate_strategy_output.py +212 -0
- package/scripts/vbrief_activate.py +228 -0
- package/scripts/vbrief_migrate_conformance.py +368 -0
- package/scripts/vbrief_reconcile_graph.py +306 -0
- package/scripts/vbrief_reconcile_labels.py +460 -0
- package/scripts/vbrief_reconcile_umbrellas.py +741 -0
- package/scripts/vbrief_validate.py +1195 -0
- package/scripts/verify-stubs.py +61 -0
- package/scripts/verify_capacity.py +160 -0
- package/scripts/verify_encoding.py +699 -0
- package/scripts/verify_hooks_installed.py +206 -0
- package/scripts/verify_investigation.py +360 -0
- package/scripts/verify_judgment_gates.py +827 -0
- package/scripts/verify_no_task_runtime.py +171 -0
- package/scripts/verify_scm_boundary.py +509 -0
- package/scripts/verify_session_ritual.py +389 -0
- package/scripts/verify_tools.py +426 -0
- package/scripts/verify_vbrief_conformance.py +478 -0
- package/skills/deft-directive-swarm/SKILL.md +7 -26
- package/skills/deft-directive-sync/SKILL.md +1 -1
- package/tasks/architecture.yml +13 -0
- package/tasks/cache.yml +69 -0
- package/tasks/capacity.yml +38 -0
- package/tasks/change.yml +46 -0
- package/tasks/changelog.yml +24 -0
- package/tasks/ci.yml +49 -0
- package/tasks/codebase.yml +47 -0
- package/tasks/commit.yml +30 -0
- package/tasks/core.yml +126 -0
- package/tasks/deployments.yml +54 -0
- package/tasks/framework.yml +74 -0
- package/tasks/install.yml +60 -0
- package/tasks/issue.yml +50 -0
- package/tasks/migrate.yml +73 -0
- package/tasks/packs.yml +92 -0
- package/tasks/policy.yml +75 -0
- package/tasks/pr.yml +89 -0
- package/tasks/prd.yml +39 -0
- package/tasks/project.yml +27 -0
- package/tasks/reconcile.yml +32 -0
- package/tasks/relocate.yml +56 -0
- package/tasks/roadmap.yml +28 -0
- package/tasks/scm.yml +126 -0
- package/tasks/scope-undo.yml +36 -0
- package/tasks/scope.yml +141 -0
- package/tasks/session.yml +19 -0
- package/tasks/setup.yml +37 -0
- package/tasks/slice.yml +69 -0
- package/tasks/spec.yml +41 -0
- package/tasks/swarm.yml +85 -0
- package/tasks/toolchain.yml +13 -0
- package/tasks/triage-actions.yml +94 -0
- package/tasks/triage-bootstrap.yml +43 -0
- package/tasks/triage-bulk.yml +75 -0
- package/tasks/triage-classify.yml +30 -0
- package/tasks/triage-queue.yml +50 -0
- package/tasks/triage-reconcile.yml +29 -0
- package/tasks/triage-scope-drift.yml +29 -0
- package/tasks/triage-scope.yml +31 -0
- package/tasks/triage-smoketest.yml +33 -0
- package/tasks/triage-subscribe.yml +36 -0
- package/tasks/triage-summary.yml +29 -0
- package/tasks/triage-welcome.yml +32 -0
- package/tasks/ts.yml +328 -0
- package/tasks/vbrief.yml +206 -0
- package/tasks/verify.yml +292 -0
- package/templates/agents-entry.md +2 -2
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""slice_record_existing.py -- ``task slice:record-existing`` driver (#1147 / N7 of #1119).
|
|
3
|
+
|
|
4
|
+
Retrofit a ``vbrief/.eval/slices.jsonl`` entry for a cohort that the
|
|
5
|
+
framework did NOT produce (hand-filed umbrella + manually-created
|
|
6
|
+
children -- the dominant historical pattern in deftai/directive,
|
|
7
|
+
including #1119 itself). D13 (#1132) writes ``slices.jsonl`` only when
|
|
8
|
+
slicing skills fire (``deft-directive-gh-slice``,
|
|
9
|
+
``deft-directive-gh-arch``, refinement's slice phase); this verb is
|
|
10
|
+
the canonical backfill path for everything else.
|
|
11
|
+
|
|
12
|
+
Two operating modes
|
|
13
|
+
-------------------
|
|
14
|
+
|
|
15
|
+
1. ``slice:record-existing`` (default sub-command):
|
|
16
|
+
|
|
17
|
+
slice_record_existing.py record-existing \
|
|
18
|
+
--umbrella=<N> --children=<N>,<M>,... \
|
|
19
|
+
[--wave-1=<N>,...] [--wave-N=...] \
|
|
20
|
+
[--actor=manual:operator] \
|
|
21
|
+
[--expected-close-signal=all-children-merged] \
|
|
22
|
+
[--sliced-at=<iso>] \
|
|
23
|
+
[--notes=<text>] \
|
|
24
|
+
[--dry-run] [--force] \
|
|
25
|
+
[--repo OWNER/NAME] [--project-root PATH]
|
|
26
|
+
|
|
27
|
+
Default ``actor`` is ``manual:operator`` (vs the skill-emitted
|
|
28
|
+
``skill:gh-slice``). Wave assignment: a child appearing in
|
|
29
|
+
``--wave-N`` is assigned to that wave; otherwise wave 1.
|
|
30
|
+
|
|
31
|
+
2. ``slice:list`` companion sub-command:
|
|
32
|
+
|
|
33
|
+
slice_record_existing.py list [--repo OWNER/NAME] [--project-root PATH]
|
|
34
|
+
|
|
35
|
+
Prints every recorded slice with umbrella + child count + actor +
|
|
36
|
+
sliced_at timestamp. Useful for verifying the backfill landed
|
|
37
|
+
alongside skill-produced entries.
|
|
38
|
+
|
|
39
|
+
Validation
|
|
40
|
+
----------
|
|
41
|
+
|
|
42
|
+
* Umbrella + each child issue number must exist (probed via
|
|
43
|
+
``scm.call("github-issue", "issue", ["view", str(N), ...])`` per N5
|
|
44
|
+
/ #1145). The probe is skipped only when ``--skip-validation`` is
|
|
45
|
+
passed (an escape hatch for cohorts whose issues live in a private
|
|
46
|
+
mirror -- documented but not advertised). ``--dry-run`` alone does
|
|
47
|
+
NOT bypass the probe; validation still fires so the preview reflects
|
|
48
|
+
the actual reachability of each issue (#1230 -- Greptile P2).
|
|
49
|
+
* Idempotency: a record with the same ``umbrella`` AND the same
|
|
50
|
+
``children`` set (compared by ``{n}`` set, order-insensitive) is
|
|
51
|
+
treated as already-present -- the verb is a no-op with
|
|
52
|
+
informational stderr and exits 0. ``--force`` bypasses this check
|
|
53
|
+
so an umbrella can carry multiple slice records (legitimate when
|
|
54
|
+
slicing happens in multiple sessions).
|
|
55
|
+
|
|
56
|
+
Exit codes
|
|
57
|
+
----------
|
|
58
|
+
|
|
59
|
+
* 0 -- record written, dry-run preview, or idempotent no-op.
|
|
60
|
+
* 1 -- validation failure (missing umbrella / child, scm error,
|
|
61
|
+
invalid record schema, malformed flags).
|
|
62
|
+
* 2 -- usage error (missing required flag, unknown sub-command,
|
|
63
|
+
undetectable project root / repo).
|
|
64
|
+
|
|
65
|
+
Refs: #1119 (umbrella), #1132 (D13 writer + schema this consumes),
|
|
66
|
+
#1144 (N4 ``vbrief/.eval/`` governance -- ``slices.jsonl`` is
|
|
67
|
+
committed, not gitignored), #1145 (N5 ``scm.call`` shim).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
from __future__ import annotations
|
|
71
|
+
|
|
72
|
+
import argparse
|
|
73
|
+
import json
|
|
74
|
+
import re
|
|
75
|
+
import subprocess
|
|
76
|
+
import sys
|
|
77
|
+
from collections.abc import Sequence
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
|
|
80
|
+
# Make sibling helpers importable when run as __main__.
|
|
81
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
82
|
+
|
|
83
|
+
import scm # noqa: E402
|
|
84
|
+
import slice_record # noqa: E402
|
|
85
|
+
from _project_context import ( # noqa: E402
|
|
86
|
+
resolve_project_repo,
|
|
87
|
+
resolve_project_root,
|
|
88
|
+
)
|
|
89
|
+
from _stdio_utf8 import reconfigure_stdio # noqa: E402
|
|
90
|
+
|
|
91
|
+
reconfigure_stdio()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Constants
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
DEFAULT_ACTOR = "manual:operator"
|
|
99
|
+
DEFAULT_EXPECTED_CLOSE_SIGNAL = "all-children-merged"
|
|
100
|
+
DEFAULT_ROLE = "manual"
|
|
101
|
+
|
|
102
|
+
_WAVE_FLAG_RE = re.compile(r"^--wave-(\d+)(?:=(.*))?$")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Helpers
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _parse_children_csv(value: str) -> list[int]:
|
|
111
|
+
"""Parse a comma-separated list of issue numbers; raise on malformed input."""
|
|
112
|
+
if not value:
|
|
113
|
+
raise ValueError("expected at least one child issue number")
|
|
114
|
+
out: list[int] = []
|
|
115
|
+
seen: set[int] = set()
|
|
116
|
+
for part in value.split(","):
|
|
117
|
+
token = part.strip()
|
|
118
|
+
if not token:
|
|
119
|
+
continue
|
|
120
|
+
try:
|
|
121
|
+
n = int(token)
|
|
122
|
+
except ValueError as exc:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"invalid child issue number {token!r} (must be a positive int)"
|
|
125
|
+
) from exc
|
|
126
|
+
if n < 1:
|
|
127
|
+
raise ValueError(f"invalid child issue number {n} (must be a positive int)")
|
|
128
|
+
if n in seen:
|
|
129
|
+
raise ValueError(f"duplicate child issue number {n}")
|
|
130
|
+
seen.add(n)
|
|
131
|
+
out.append(n)
|
|
132
|
+
if not out:
|
|
133
|
+
raise ValueError("expected at least one child issue number")
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _consume_wave_flags(raw_args: list[str]) -> tuple[dict[int, list[int]], list[str]]:
|
|
138
|
+
"""Extract every ``--wave-N=<csv>`` / ``--wave-N <csv>`` occurrence.
|
|
139
|
+
|
|
140
|
+
Returns ``(wave_map, remaining_args)`` where ``wave_map`` is keyed by
|
|
141
|
+
the wave number (1, 2, ...) and carries the list of child numbers
|
|
142
|
+
assigned to that wave. ``remaining_args`` carries every token argparse
|
|
143
|
+
will then parse with the static flag list. ``argparse`` cannot model
|
|
144
|
+
a dynamic flag prefix on its own, so this small pre-pass owns the
|
|
145
|
+
``--wave-N`` shape (mirrors the pattern in scripts/scm.py's
|
|
146
|
+
``_extract_value_flag``).
|
|
147
|
+
"""
|
|
148
|
+
wave_map: dict[int, list[int]] = {}
|
|
149
|
+
remaining: list[str] = []
|
|
150
|
+
i = 0
|
|
151
|
+
while i < len(raw_args):
|
|
152
|
+
token = raw_args[i]
|
|
153
|
+
match = _WAVE_FLAG_RE.match(token)
|
|
154
|
+
if not match:
|
|
155
|
+
remaining.append(token)
|
|
156
|
+
i += 1
|
|
157
|
+
continue
|
|
158
|
+
wave_n = int(match.group(1))
|
|
159
|
+
if wave_n < 1:
|
|
160
|
+
raise ValueError(f"invalid wave number in {token!r} (must be >= 1)")
|
|
161
|
+
value: str | None
|
|
162
|
+
if match.group(2) is not None:
|
|
163
|
+
value = match.group(2)
|
|
164
|
+
i += 1
|
|
165
|
+
elif i + 1 < len(raw_args):
|
|
166
|
+
value = raw_args[i + 1]
|
|
167
|
+
i += 2
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(f"missing value for {token!r}")
|
|
170
|
+
children = _parse_children_csv(value)
|
|
171
|
+
bucket = wave_map.setdefault(wave_n, [])
|
|
172
|
+
for n in children:
|
|
173
|
+
if n in bucket:
|
|
174
|
+
# Tolerate intra-wave duplicates (cheap), surface
|
|
175
|
+
# cross-wave duplicates below.
|
|
176
|
+
continue
|
|
177
|
+
bucket.append(n)
|
|
178
|
+
# Cross-wave duplicates: a child cannot be in two waves.
|
|
179
|
+
placement: dict[int, int] = {}
|
|
180
|
+
for wave_n, members in wave_map.items():
|
|
181
|
+
for n in members:
|
|
182
|
+
if n in placement and placement[n] != wave_n:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"child {n} appears in both --wave-{placement[n]} "
|
|
185
|
+
f"and --wave-{wave_n}; each child belongs to one wave"
|
|
186
|
+
)
|
|
187
|
+
placement[n] = wave_n
|
|
188
|
+
return wave_map, remaining
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _repo_slug_to_url(repo: str, n: int) -> str:
|
|
192
|
+
return f"https://github.com/{repo}/issues/{n}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _issues_jsonl_path(project_root: Path) -> Path:
|
|
196
|
+
return project_root / "vbrief" / ".eval" / "slices.jsonl"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Issue existence validation (N5 shim)
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class IssueValidationError(RuntimeError):
|
|
205
|
+
"""Raised when an issue number cannot be validated via the SCM shim."""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _validate_issue_exists(
|
|
209
|
+
n: int,
|
|
210
|
+
*,
|
|
211
|
+
repo: str,
|
|
212
|
+
scm_module=scm,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Probe ``gh issue view <N> --repo <repo>`` via the N5 shim.
|
|
215
|
+
|
|
216
|
+
Raises :class:`IssueValidationError` on a non-zero exit. The shim
|
|
217
|
+
itself raises :class:`NotImplementedError` for non-``github-issue``
|
|
218
|
+
sources -- that bubbles up so a consumer on GitLab / Gitea sees
|
|
219
|
+
the deferred abstraction (#445 / #935 Workstream 6) immediately.
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
proc = scm_module.call(
|
|
223
|
+
"github-issue",
|
|
224
|
+
"issue",
|
|
225
|
+
["view", str(n), "--repo", repo, "--json", "number,url"],
|
|
226
|
+
check=False,
|
|
227
|
+
capture_output=True,
|
|
228
|
+
text=True,
|
|
229
|
+
timeout=30,
|
|
230
|
+
)
|
|
231
|
+
except subprocess.TimeoutExpired as exc:
|
|
232
|
+
raise IssueValidationError(f"timed out validating issue #{n} in {repo}") from exc
|
|
233
|
+
if proc.returncode != 0:
|
|
234
|
+
stderr = (proc.stderr or "").strip() or "(no stderr)"
|
|
235
|
+
raise IssueValidationError(f"issue #{n} in {repo} not found / inaccessible: {stderr}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# Build + write
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _build_children(
|
|
244
|
+
children: list[int],
|
|
245
|
+
wave_map: dict[int, list[int]],
|
|
246
|
+
repo: str,
|
|
247
|
+
) -> list[dict[str, object]]:
|
|
248
|
+
"""Construct the per-child dicts in the slices.jsonl schema shape."""
|
|
249
|
+
wave_for: dict[int, int] = {}
|
|
250
|
+
for wave_n, members in wave_map.items():
|
|
251
|
+
for n in members:
|
|
252
|
+
wave_for[n] = wave_n
|
|
253
|
+
out: list[dict[str, object]] = []
|
|
254
|
+
for n in children:
|
|
255
|
+
out.append(
|
|
256
|
+
{
|
|
257
|
+
"n": n,
|
|
258
|
+
"url": _repo_slug_to_url(repo, n),
|
|
259
|
+
"wave": wave_for.get(n, 1),
|
|
260
|
+
"role": DEFAULT_ROLE,
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _children_set(record: dict) -> frozenset[int]:
|
|
267
|
+
children = record.get("children")
|
|
268
|
+
if not isinstance(children, list):
|
|
269
|
+
return frozenset()
|
|
270
|
+
out: set[int] = set()
|
|
271
|
+
for child in children:
|
|
272
|
+
if isinstance(child, dict):
|
|
273
|
+
n = child.get("n")
|
|
274
|
+
if isinstance(n, int):
|
|
275
|
+
out.add(n)
|
|
276
|
+
return frozenset(out)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _find_duplicate(
|
|
280
|
+
umbrella: int,
|
|
281
|
+
children_numbers: list[int],
|
|
282
|
+
*,
|
|
283
|
+
slices_path: Path,
|
|
284
|
+
record_module=slice_record,
|
|
285
|
+
) -> dict | None:
|
|
286
|
+
"""Return the first existing slice record that matches umbrella + child-set, or None."""
|
|
287
|
+
target = frozenset(children_numbers)
|
|
288
|
+
for record in record_module.read_all(path=slices_path):
|
|
289
|
+
if record.get("umbrella") != umbrella:
|
|
290
|
+
continue
|
|
291
|
+
if _children_set(record) == target:
|
|
292
|
+
return record
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
# CLI
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
302
|
+
parser = argparse.ArgumentParser(
|
|
303
|
+
prog="slice_record_existing.py",
|
|
304
|
+
description=(
|
|
305
|
+
"Retrofit a slices.jsonl entry for a hand-filed cohort "
|
|
306
|
+
"(#1147 / N7 of #1119). Default sub-command is 'record-existing'."
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
sub = parser.add_subparsers(dest="command")
|
|
310
|
+
|
|
311
|
+
record = sub.add_parser(
|
|
312
|
+
"record-existing",
|
|
313
|
+
help="Write a backfill slice record (default sub-command).",
|
|
314
|
+
)
|
|
315
|
+
record.add_argument(
|
|
316
|
+
"--umbrella",
|
|
317
|
+
type=int,
|
|
318
|
+
required=True,
|
|
319
|
+
help="Umbrella issue number.",
|
|
320
|
+
)
|
|
321
|
+
record.add_argument(
|
|
322
|
+
"--children",
|
|
323
|
+
required=True,
|
|
324
|
+
help="Comma-separated child issue numbers (e.g. 1121,1122,1123).",
|
|
325
|
+
)
|
|
326
|
+
record.add_argument(
|
|
327
|
+
"--actor",
|
|
328
|
+
default=DEFAULT_ACTOR,
|
|
329
|
+
help=(
|
|
330
|
+
f"Slicing actor identity (default: {DEFAULT_ACTOR}). "
|
|
331
|
+
"Distinguishes backfill records from skill-emitted records."
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
record.add_argument(
|
|
335
|
+
"--expected-close-signal",
|
|
336
|
+
default=DEFAULT_EXPECTED_CLOSE_SIGNAL,
|
|
337
|
+
help=(
|
|
338
|
+
f"One of all-children-merged|wave-1-merged|manual "
|
|
339
|
+
f"(default: {DEFAULT_EXPECTED_CLOSE_SIGNAL})."
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
record.add_argument(
|
|
343
|
+
"--sliced-at",
|
|
344
|
+
default=None,
|
|
345
|
+
help="ISO-8601 UTC timestamp (e.g. 2026-05-14T17:00:00Z). Defaults to now.",
|
|
346
|
+
)
|
|
347
|
+
record.add_argument(
|
|
348
|
+
"--notes",
|
|
349
|
+
default=None,
|
|
350
|
+
help="Free-text rationale recorded on the slice entry.",
|
|
351
|
+
)
|
|
352
|
+
record.add_argument(
|
|
353
|
+
"--dry-run",
|
|
354
|
+
action="store_true",
|
|
355
|
+
help="Print the proposed entry to stdout without writing.",
|
|
356
|
+
)
|
|
357
|
+
record.add_argument(
|
|
358
|
+
"--force",
|
|
359
|
+
action="store_true",
|
|
360
|
+
help=(
|
|
361
|
+
"Bypass idempotency: write a new record even when an entry "
|
|
362
|
+
"with the same umbrella + child set already exists. "
|
|
363
|
+
"Legitimate when slicing happens in multiple sessions."
|
|
364
|
+
),
|
|
365
|
+
)
|
|
366
|
+
record.add_argument(
|
|
367
|
+
"--skip-validation",
|
|
368
|
+
action="store_true",
|
|
369
|
+
help=(
|
|
370
|
+
"Skip the scm.call issue-existence probes. Documented escape "
|
|
371
|
+
"hatch for cohorts whose issues live in a private mirror -- "
|
|
372
|
+
"use sparingly."
|
|
373
|
+
),
|
|
374
|
+
)
|
|
375
|
+
record.add_argument(
|
|
376
|
+
"--repo",
|
|
377
|
+
default=None,
|
|
378
|
+
help=(
|
|
379
|
+
"Consumer GitHub repo (OWNER/NAME). Defaults to "
|
|
380
|
+
"$DEFT_PROJECT_REPO or `git remote get-url origin`."
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
record.add_argument(
|
|
384
|
+
"--project-root",
|
|
385
|
+
default=None,
|
|
386
|
+
help="Consumer project root. Overrides $DEFT_PROJECT_ROOT.",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
list_cmd = sub.add_parser(
|
|
390
|
+
"list",
|
|
391
|
+
help="List recorded slices with umbrella + child counts + actor.",
|
|
392
|
+
)
|
|
393
|
+
list_cmd.add_argument(
|
|
394
|
+
"--project-root",
|
|
395
|
+
default=None,
|
|
396
|
+
help="Consumer project root. Overrides $DEFT_PROJECT_ROOT.",
|
|
397
|
+
)
|
|
398
|
+
list_cmd.add_argument(
|
|
399
|
+
"--json",
|
|
400
|
+
dest="as_json",
|
|
401
|
+
action="store_true",
|
|
402
|
+
help="Emit the slice records as a JSON array (one object per slice).",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return parser
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _resolve_root_and_repo(
|
|
409
|
+
cli_project_root: str | None,
|
|
410
|
+
cli_repo: str | None,
|
|
411
|
+
*,
|
|
412
|
+
require_repo: bool,
|
|
413
|
+
) -> tuple[Path, str | None, int]:
|
|
414
|
+
"""Resolve ``(project_root, repo, error_exit_code)``.
|
|
415
|
+
|
|
416
|
+
On success returns ``(root, repo_or_None, 0)``. On failure prints
|
|
417
|
+
a loud error to stderr and returns ``(Path('.'), None, exit_code)``.
|
|
418
|
+
"""
|
|
419
|
+
project_root = resolve_project_root(cli_project_root)
|
|
420
|
+
if project_root is None:
|
|
421
|
+
print(
|
|
422
|
+
"error: cannot determine project root. Pass --project-root PATH, "
|
|
423
|
+
"set $DEFT_PROJECT_ROOT, or run from inside a directory tree that "
|
|
424
|
+
"contains vbrief/ or .git/ (#535).",
|
|
425
|
+
file=sys.stderr,
|
|
426
|
+
)
|
|
427
|
+
return Path("."), None, 2
|
|
428
|
+
if not require_repo:
|
|
429
|
+
return project_root, None, 0
|
|
430
|
+
repo = resolve_project_repo(cli_repo, project_root=project_root)
|
|
431
|
+
if not repo:
|
|
432
|
+
print(
|
|
433
|
+
"error: cannot determine repo slug. Pass --repo OWNER/NAME, "
|
|
434
|
+
"set $DEFT_PROJECT_REPO, or run inside a git checkout with an "
|
|
435
|
+
"origin remote.",
|
|
436
|
+
file=sys.stderr,
|
|
437
|
+
)
|
|
438
|
+
return project_root, None, 2
|
|
439
|
+
return project_root, repo, 0
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _run_record_existing(args: argparse.Namespace, wave_map: dict[int, list[int]]) -> int:
|
|
443
|
+
project_root, repo, exit_code = _resolve_root_and_repo(
|
|
444
|
+
args.project_root, args.repo, require_repo=True
|
|
445
|
+
)
|
|
446
|
+
if exit_code != 0:
|
|
447
|
+
return exit_code
|
|
448
|
+
if repo is None: # pragma: no cover -- guaranteed non-None when require_repo=True
|
|
449
|
+
# Explicit guard so this safety check survives `python -O` (where
|
|
450
|
+
# bare ``assert`` is stripped). See #1230 -- Greptile P2.
|
|
451
|
+
raise RuntimeError(
|
|
452
|
+
"repo is None despite require_repo=True; this is a bug in "
|
|
453
|
+
"_resolve_root_and_repo"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
children = _parse_children_csv(args.children)
|
|
458
|
+
except ValueError as exc:
|
|
459
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
460
|
+
return 2
|
|
461
|
+
|
|
462
|
+
# Validate wave members are a subset of the declared children.
|
|
463
|
+
declared = set(children)
|
|
464
|
+
for wave_n, members in wave_map.items():
|
|
465
|
+
for n in members:
|
|
466
|
+
if n not in declared:
|
|
467
|
+
print(
|
|
468
|
+
f"error: --wave-{wave_n} references child #{n} not present in --children",
|
|
469
|
+
file=sys.stderr,
|
|
470
|
+
)
|
|
471
|
+
return 2
|
|
472
|
+
|
|
473
|
+
if args.umbrella in declared:
|
|
474
|
+
print(
|
|
475
|
+
f"error: umbrella #{args.umbrella} cannot also appear in --children",
|
|
476
|
+
file=sys.stderr,
|
|
477
|
+
)
|
|
478
|
+
return 2
|
|
479
|
+
|
|
480
|
+
# Issue-existence validation via scm.call (N5 shim). Skipped under
|
|
481
|
+
# --skip-validation (documented escape hatch) so an operator can
|
|
482
|
+
# backfill cohorts whose issues live in a private mirror or have
|
|
483
|
+
# been deleted post-slice.
|
|
484
|
+
if not args.skip_validation:
|
|
485
|
+
try:
|
|
486
|
+
_validate_issue_exists(args.umbrella, repo=repo)
|
|
487
|
+
for n in children:
|
|
488
|
+
_validate_issue_exists(n, repo=repo)
|
|
489
|
+
except IssueValidationError as exc:
|
|
490
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
491
|
+
return 1
|
|
492
|
+
except NotImplementedError as exc:
|
|
493
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
494
|
+
return 1
|
|
495
|
+
|
|
496
|
+
# Idempotency: refuse to duplicate a record with the same umbrella +
|
|
497
|
+
# child set unless --force. Pre-lock peek is a fast-path optimisation
|
|
498
|
+
# for the common no-concurrency case (no file IO under the lock when
|
|
499
|
+
# an obvious duplicate exists); the authoritative re-check fires
|
|
500
|
+
# under the file lock below so two concurrent invocations cannot
|
|
501
|
+
# both observe "no duplicate" and both append (P1 TOCTOU per #1231).
|
|
502
|
+
slices_path = _issues_jsonl_path(project_root)
|
|
503
|
+
duplicate = _find_duplicate(args.umbrella, children, slices_path=slices_path)
|
|
504
|
+
if duplicate is not None and not args.force:
|
|
505
|
+
print(
|
|
506
|
+
f"slice:record-existing: umbrella #{args.umbrella} already has a "
|
|
507
|
+
f"matching record (slice_id={duplicate.get('slice_id')}, "
|
|
508
|
+
f"actor={duplicate.get('actor')}). Re-run with --force to write "
|
|
509
|
+
"a second record.",
|
|
510
|
+
file=sys.stderr,
|
|
511
|
+
)
|
|
512
|
+
return 0
|
|
513
|
+
|
|
514
|
+
child_dicts = _build_children(children, wave_map, repo)
|
|
515
|
+
|
|
516
|
+
# Dry-run path: build the proposed record without writing. No lock
|
|
517
|
+
# needed -- dry-run is read-only and does not race against itself.
|
|
518
|
+
if args.dry_run:
|
|
519
|
+
proposed = {
|
|
520
|
+
"slice_id": "<dry-run>",
|
|
521
|
+
"umbrella": args.umbrella,
|
|
522
|
+
"umbrella_url": _repo_slug_to_url(repo, args.umbrella),
|
|
523
|
+
"sliced_at": args.sliced_at or slice_record.now_iso(),
|
|
524
|
+
"actor": args.actor,
|
|
525
|
+
"children": child_dicts,
|
|
526
|
+
"expected_close_signal": args.expected_close_signal,
|
|
527
|
+
}
|
|
528
|
+
if args.notes is not None:
|
|
529
|
+
proposed["notes"] = args.notes
|
|
530
|
+
print(json.dumps(proposed, sort_keys=True, ensure_ascii=False, indent=2))
|
|
531
|
+
wave_summary = _summarise_waves(wave_map, len(children))
|
|
532
|
+
print(
|
|
533
|
+
f"DRY-RUN: would write slices.jsonl entry for umbrella "
|
|
534
|
+
f"#{args.umbrella} ({len(children)} children, {wave_summary}).",
|
|
535
|
+
file=sys.stderr,
|
|
536
|
+
)
|
|
537
|
+
return 0
|
|
538
|
+
|
|
539
|
+
# Atomic idempotency (#1231 / P1 TOCTOU fix): the duplicate check
|
|
540
|
+
# AND the append must run under one critical section so two
|
|
541
|
+
# concurrent invocations of `task slice:record-existing` (neither
|
|
542
|
+
# passing --force) cannot both observe "no duplicate" between the
|
|
543
|
+
# check and the append. Acquire the sidecar lock that already
|
|
544
|
+
# serialises every slice_record.write_slice call, run a second
|
|
545
|
+
# _find_duplicate inside the lock (this is the authoritative pass
|
|
546
|
+
# -- the pre-lock peek above is only a fast path for the common
|
|
547
|
+
# uncontended case), and then call write_slice_unlocked so we do
|
|
548
|
+
# not deadlock on re-entry into the same lock.
|
|
549
|
+
record: dict = {
|
|
550
|
+
"slice_id": slice_record.new_slice_id(),
|
|
551
|
+
"umbrella": args.umbrella,
|
|
552
|
+
"umbrella_url": _repo_slug_to_url(repo, args.umbrella),
|
|
553
|
+
"sliced_at": args.sliced_at or slice_record.now_iso(),
|
|
554
|
+
"actor": args.actor,
|
|
555
|
+
"children": child_dicts,
|
|
556
|
+
"expected_close_signal": args.expected_close_signal,
|
|
557
|
+
}
|
|
558
|
+
if args.notes is not None:
|
|
559
|
+
record["notes"] = args.notes
|
|
560
|
+
|
|
561
|
+
slices_path.parent.mkdir(parents=True, exist_ok=True)
|
|
562
|
+
try:
|
|
563
|
+
with slice_record.append_lock(slices_path):
|
|
564
|
+
authoritative_dup = _find_duplicate(
|
|
565
|
+
args.umbrella, children, slices_path=slices_path
|
|
566
|
+
)
|
|
567
|
+
if authoritative_dup is not None and not args.force:
|
|
568
|
+
print(
|
|
569
|
+
f"slice:record-existing: umbrella #{args.umbrella} "
|
|
570
|
+
f"already has a matching record (slice_id="
|
|
571
|
+
f"{authoritative_dup.get('slice_id')}, actor="
|
|
572
|
+
f"{authoritative_dup.get('actor')}). Re-run with "
|
|
573
|
+
"--force to write a second record.",
|
|
574
|
+
file=sys.stderr,
|
|
575
|
+
)
|
|
576
|
+
return 0
|
|
577
|
+
slice_id = slice_record.write_slice_unlocked(
|
|
578
|
+
record=record, path=slices_path
|
|
579
|
+
)
|
|
580
|
+
except slice_record.SliceRecordError as exc:
|
|
581
|
+
print(f"error: invalid record -- {exc}", file=sys.stderr)
|
|
582
|
+
return 1
|
|
583
|
+
|
|
584
|
+
wave_summary = _summarise_waves(wave_map, len(children))
|
|
585
|
+
print(
|
|
586
|
+
f"Wrote vbrief/.eval/slices.jsonl entry for umbrella "
|
|
587
|
+
f"#{args.umbrella} ({len(children)} children, {wave_summary}). "
|
|
588
|
+
f"slice_id={slice_id}"
|
|
589
|
+
)
|
|
590
|
+
return 0
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _summarise_waves(wave_map: dict[int, list[int]], total_children: int) -> str:
|
|
594
|
+
"""Render the operator-facing wave-distribution summary.
|
|
595
|
+
|
|
596
|
+
Children declared in ``--children`` but absent from every ``--wave-N``
|
|
597
|
+
flag fall through to wave 1 (the default). Per #1230 -- Greptile P2,
|
|
598
|
+
the unassigned-default count is MERGED into the wave-1 entry rather
|
|
599
|
+
than rendered as a second ``wave-1=N (default)`` segment, so a caller
|
|
600
|
+
passing ``--wave-1=2 --wave-2=3`` with one unassigned child sees
|
|
601
|
+
``"2 wave(s): wave-1=2, wave-2=1"`` rather than the pre-fix
|
|
602
|
+
``"3 wave(s): wave-1=1, wave-2=1, wave-1=1 (default)"``.
|
|
603
|
+
"""
|
|
604
|
+
if not wave_map:
|
|
605
|
+
return f"{total_children} in wave 1 (default)"
|
|
606
|
+
placed_by_wave: dict[int, int] = {
|
|
607
|
+
wave_n: len(members) for wave_n, members in wave_map.items()
|
|
608
|
+
}
|
|
609
|
+
placed_total = sum(placed_by_wave.values())
|
|
610
|
+
unassigned = total_children - placed_total
|
|
611
|
+
if unassigned > 0:
|
|
612
|
+
placed_by_wave[1] = placed_by_wave.get(1, 0) + unassigned
|
|
613
|
+
parts = [f"wave-{wave_n}={placed_by_wave[wave_n]}" for wave_n in sorted(placed_by_wave)]
|
|
614
|
+
return f"{len(parts)} wave(s): " + ", ".join(parts)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _run_list(args: argparse.Namespace) -> int:
|
|
618
|
+
project_root, _repo, exit_code = _resolve_root_and_repo(
|
|
619
|
+
args.project_root, None, require_repo=False
|
|
620
|
+
)
|
|
621
|
+
if exit_code != 0:
|
|
622
|
+
return exit_code
|
|
623
|
+
|
|
624
|
+
slices_path = _issues_jsonl_path(project_root)
|
|
625
|
+
records = slice_record.read_all(path=slices_path)
|
|
626
|
+
|
|
627
|
+
if args.as_json:
|
|
628
|
+
print(json.dumps(records, ensure_ascii=False, indent=2, sort_keys=True))
|
|
629
|
+
return 0
|
|
630
|
+
|
|
631
|
+
if not records:
|
|
632
|
+
print("slice:list: no records found in vbrief/.eval/slices.jsonl (file absent or empty).")
|
|
633
|
+
return 0
|
|
634
|
+
|
|
635
|
+
print(f"slice:list: {len(records)} record(s) in vbrief/.eval/slices.jsonl")
|
|
636
|
+
for record in records:
|
|
637
|
+
umbrella = record.get("umbrella", "?")
|
|
638
|
+
actor = record.get("actor", "?")
|
|
639
|
+
sliced_at = record.get("sliced_at", "?")
|
|
640
|
+
slice_id = record.get("slice_id", "?")
|
|
641
|
+
children = record.get("children")
|
|
642
|
+
child_count = len(children) if isinstance(children, list) else 0
|
|
643
|
+
signal = record.get("expected_close_signal", "?")
|
|
644
|
+
notes = record.get("notes")
|
|
645
|
+
line = (
|
|
646
|
+
f" - umbrella=#{umbrella} children={child_count} "
|
|
647
|
+
f"actor={actor} sliced_at={sliced_at} "
|
|
648
|
+
f"signal={signal} slice_id={slice_id}"
|
|
649
|
+
)
|
|
650
|
+
if notes:
|
|
651
|
+
line += f" notes={notes!r}"
|
|
652
|
+
print(line)
|
|
653
|
+
return 0
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
657
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
658
|
+
# No sub-command and at least one non-flag arg is unusual; default
|
|
659
|
+
# to `record-existing` to match the documented user-facing surface
|
|
660
|
+
# (``task slice:record-existing --umbrella=N ...`` forwards the
|
|
661
|
+
# remaining flags to this script with no positional sub-command).
|
|
662
|
+
if raw and raw[0] not in {"record-existing", "list", "-h", "--help"}:
|
|
663
|
+
raw = ["record-existing", *raw]
|
|
664
|
+
elif not raw:
|
|
665
|
+
raw = ["record-existing"]
|
|
666
|
+
|
|
667
|
+
# Pre-pass: strip out --wave-N flags before argparse sees them
|
|
668
|
+
# (argparse cannot model a dynamic flag prefix). Only relevant for
|
|
669
|
+
# the `record-existing` sub-command; the `list` sub-command has no
|
|
670
|
+
# --wave-N surface so the pre-pass is a no-op there.
|
|
671
|
+
if raw and raw[0] == "record-existing":
|
|
672
|
+
try:
|
|
673
|
+
wave_map, raw = _consume_wave_flags(raw)
|
|
674
|
+
except ValueError as exc:
|
|
675
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
676
|
+
return 2
|
|
677
|
+
else:
|
|
678
|
+
wave_map = {}
|
|
679
|
+
|
|
680
|
+
parser = _build_parser()
|
|
681
|
+
try:
|
|
682
|
+
args = parser.parse_args(raw)
|
|
683
|
+
except SystemExit as exc:
|
|
684
|
+
return int(exc.code) if isinstance(exc.code, int) else 2
|
|
685
|
+
|
|
686
|
+
if args.command == "list":
|
|
687
|
+
return _run_list(args)
|
|
688
|
+
return _run_record_existing(args, wave_map)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
if __name__ == "__main__":
|
|
692
|
+
raise SystemExit(main())
|