@deftai/directive-content 0.58.0 → 0.60.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-push +10 -9
- package/Taskfile.yml +57 -67
- package/UPGRADING.md +1 -1
- 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/rules/rules-pack-0.1.json +3 -3
- package/packs/skills/skills-pack-0.1.json +22 -22
- package/scm/github.md +20 -2
- 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 +16 -0
- package/tasks/relocate.yml +18 -48
- package/tasks/toolchain.yml +15 -5
- package/tasks/vbrief.yml +4 -3
- package/tasks/verify.yml +12 -14
- package/templates/agents-entry.md +1 -2
- 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 -2551
- 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
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""swarm_verify_review_clean.py -- Cohort-level CLEAN verification gate (#1364).
|
|
3
|
-
|
|
4
|
-
Verifies that EVERY PR in a swarm cohort satisfies the
|
|
5
|
-
``skills/deft-directive-review-cycle/SKILL.md`` Phase 2 Step 6 exit condition
|
|
6
|
-
AND the ``skills/deft-directive-swarm/SKILL.md`` Phase 5 Exit Condition on the
|
|
7
|
-
**current HEAD** before the monitor is allowed to discuss the Phase 5 -> 6
|
|
8
|
-
merge cascade.
|
|
9
|
-
|
|
10
|
-
Background (#1364)
|
|
11
|
-
------------------
|
|
12
|
-
The swarm skill's Phase 5 Exit Condition correctly documents the strong
|
|
13
|
-
per-PR CLEAN bar (confidence > 3, no P0/P1, no errored sentinel, CI clean,
|
|
14
|
-
HEAD-SHA freshness), and the per-PR programmatic gate
|
|
15
|
-
(``scripts/pr_merge_readiness.py`` / ``task pr:merge-ready``) closes the
|
|
16
|
-
per-merge SUCCESS-with-findings blind spot. But there is no mandatory
|
|
17
|
-
deterministic gate the monitor must pass at the COHORT level after the
|
|
18
|
-
Phase 6 pollers terminate but before the merge discussion begins. The
|
|
19
|
-
result: during the #1166 strategy-consistency swarm, multiple pollers
|
|
20
|
-
exited with ``clean_gate_holdout=confidence`` (i.e. confidence == 3) and
|
|
21
|
-
the monitor still surfaced the Phase 5 -> 6 merge gate because the
|
|
22
|
-
trigger keyed on "all pollers have reported back" rather than "every PR
|
|
23
|
-
in the cohort is objectively CLEAN".
|
|
24
|
-
|
|
25
|
-
This script is that structural gap-closer. It re-uses the Greptile
|
|
26
|
-
rolling-summary parser from ``scripts/pr_merge_readiness.py`` so the two
|
|
27
|
-
surfaces stay in lockstep -- a future fix to the parser (e.g. a new
|
|
28
|
-
Greptile rendering surface, a new severity badge) lands in both surfaces
|
|
29
|
-
at once. Do NOT duplicate the parsing logic here.
|
|
30
|
-
|
|
31
|
-
What it checks (per PR)
|
|
32
|
-
-----------------------
|
|
33
|
-
For every PR in the cohort, all of the following MUST hold on the current
|
|
34
|
-
PR HEAD:
|
|
35
|
-
|
|
36
|
-
1. ``Last reviewed commit:`` SHA in the Greptile rolling-summary comment
|
|
37
|
-
body matches the live PR HEAD ref OID.
|
|
38
|
-
2. The body is NOT the errored sentinel (#526) ``Greptile encountered an
|
|
39
|
-
error while reviewing this PR``.
|
|
40
|
-
3. ``Confidence Score: X / 5`` is greater than 3 (i.e. 4 or 5).
|
|
41
|
-
4. P0 and P1 finding counts are both zero. P2 findings are non-blocking
|
|
42
|
-
style suggestions per the review-cycle skill and do NOT gate the
|
|
43
|
-
cohort.
|
|
44
|
-
|
|
45
|
-
CI lane verification is intentionally out of scope: lane names vary per
|
|
46
|
-
repository, the Greptile body verdict already encodes review readiness,
|
|
47
|
-
and the per-merge ``task pr:merge-ready`` gate stays the freshness-window-
|
|
48
|
-
atomic merge-time check that pins HEAD-SHA equality. This cohort gate
|
|
49
|
-
fires once after the pollers terminate; the per-merge gate fires inside
|
|
50
|
-
the shell-`&&` chain that follows.
|
|
51
|
-
|
|
52
|
-
Usage
|
|
53
|
-
-----
|
|
54
|
-
# Explicit PR list
|
|
55
|
-
task swarm:verify-review-clean -- 1370 1371 1372 --repo deftai/directive
|
|
56
|
-
|
|
57
|
-
# Or cohort discovered from active vBRIEFs (resolves each vBRIEF's
|
|
58
|
-
# x-vbrief/github-pr reference, if any)
|
|
59
|
-
task swarm:verify-review-clean -- --cohort vbrief/active/*.vbrief.json --repo deftai/directive
|
|
60
|
-
|
|
61
|
-
# JSON output for programmatic consumers (a parent monitor agent)
|
|
62
|
-
task swarm:verify-review-clean -- 1370 1371 --repo deftai/directive --json
|
|
63
|
-
|
|
64
|
-
Exit codes
|
|
65
|
-
----------
|
|
66
|
-
0 -- every PR in the cohort is CLEAN on current HEAD (merge discussion may proceed)
|
|
67
|
-
1 -- one or more PRs is unclean; per-PR diagnostics printed
|
|
68
|
-
2 -- external / config error (gh missing, empty cohort, malformed vBRIEF,
|
|
69
|
-
no x-vbrief/github-pr references resolved, ...)
|
|
70
|
-
|
|
71
|
-
Pure stdlib + ``gh`` CLI; no third-party deps. The parser is imported
|
|
72
|
-
from ``scripts/pr_merge_readiness.py`` (so both surfaces share one source
|
|
73
|
-
of truth).
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
from __future__ import annotations
|
|
77
|
-
|
|
78
|
-
import argparse
|
|
79
|
-
import glob
|
|
80
|
-
import json
|
|
81
|
-
import re
|
|
82
|
-
import sys
|
|
83
|
-
from dataclasses import asdict, dataclass, field
|
|
84
|
-
from pathlib import Path
|
|
85
|
-
|
|
86
|
-
# Make sibling scripts importable both when run as __main__ and when imported
|
|
87
|
-
# by tests.
|
|
88
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
from _stdio_utf8 import reconfigure_stdio # noqa: E402
|
|
92
|
-
reconfigure_stdio()
|
|
93
|
-
except ImportError:
|
|
94
|
-
# _stdio_utf8 is optional; some test contexts load this module directly.
|
|
95
|
-
pass
|
|
96
|
-
|
|
97
|
-
# Re-use the proven Greptile body parser + per-PR gate from
|
|
98
|
-
# pr_merge_readiness.py. Duplicating the parsing logic here would let the
|
|
99
|
-
# two surfaces drift -- a fix in one would not land in the other. The
|
|
100
|
-
# parser is module-private to pr_merge_readiness but exported by name and
|
|
101
|
-
# is the right load-bearing reuse point. See #1364.
|
|
102
|
-
import pr_merge_readiness as _mr # noqa: E402
|
|
103
|
-
|
|
104
|
-
EXIT_OK = 0
|
|
105
|
-
EXIT_UNCLEAN = 1
|
|
106
|
-
EXIT_EXTERNAL_ERROR = 2
|
|
107
|
-
|
|
108
|
-
# ---------------------------------------------------------------------------
|
|
109
|
-
# Cohort discovery
|
|
110
|
-
# ---------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
# Regex for an x-vbrief/github-pr URI of the form
|
|
113
|
-
# `https://github.com/<owner>/<repo>/pull/<N>`. The cohort discovery path
|
|
114
|
-
# resolves the PR number from any reference that matches.
|
|
115
|
-
_PR_URI_RE = re.compile(
|
|
116
|
-
r"https?://github\.com/[^/]+/[^/]+/pull/(?P<pr>\d+)",
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@dataclass
|
|
121
|
-
class CohortResolutionError:
|
|
122
|
-
"""Structured failure from cohort discovery."""
|
|
123
|
-
vbrief_path: str
|
|
124
|
-
reason: str
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def resolve_cohort_from_vbriefs(
|
|
128
|
-
vbrief_globs: list[str],
|
|
129
|
-
) -> tuple[list[int], list[CohortResolutionError]]:
|
|
130
|
-
"""Resolve a list of PR numbers from one or more glob patterns over vBRIEF
|
|
131
|
-
paths.
|
|
132
|
-
|
|
133
|
-
For each matched ``*.vbrief.json`` file, read ``plan.references[]`` and
|
|
134
|
-
extract every URI matching ``https://github.com/.../pull/<N>``. Returns
|
|
135
|
-
a flat de-duplicated list of PR numbers preserving first-seen order
|
|
136
|
-
AND a per-vBRIEF list of resolution failures so the caller can surface
|
|
137
|
-
them with EXIT_EXTERNAL_ERROR.
|
|
138
|
-
|
|
139
|
-
Acceptable failure modes (each surfaced as a structured
|
|
140
|
-
``CohortResolutionError`` but NOT raised) so a partial cohort can
|
|
141
|
-
still be diagnosed:
|
|
142
|
-
- vBRIEF JSON is malformed
|
|
143
|
-
- vBRIEF carries no PR references at all
|
|
144
|
-
- vBRIEF references a PR URL on a different host (we record but skip)
|
|
145
|
-
"""
|
|
146
|
-
seen_prs: list[int] = []
|
|
147
|
-
seen_set: set[int] = set()
|
|
148
|
-
failures: list[CohortResolutionError] = []
|
|
149
|
-
paths: list[Path] = []
|
|
150
|
-
for pattern in vbrief_globs:
|
|
151
|
-
# Each glob can match zero or more files. We treat a glob that
|
|
152
|
-
# matches nothing as a soft failure (e.g. a typo): the caller
|
|
153
|
-
# gets a structured error so they can fix the glob.
|
|
154
|
-
matched = sorted(Path(p) for p in glob.glob(pattern))
|
|
155
|
-
if not matched:
|
|
156
|
-
failures.append(
|
|
157
|
-
CohortResolutionError(
|
|
158
|
-
vbrief_path=pattern,
|
|
159
|
-
reason=f"glob matched no files: {pattern!r}",
|
|
160
|
-
)
|
|
161
|
-
)
|
|
162
|
-
continue
|
|
163
|
-
paths.extend(matched)
|
|
164
|
-
for path in paths:
|
|
165
|
-
try:
|
|
166
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
167
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
168
|
-
failures.append(
|
|
169
|
-
CohortResolutionError(vbrief_path=str(path), reason=f"unreadable: {exc}")
|
|
170
|
-
)
|
|
171
|
-
continue
|
|
172
|
-
references = payload.get("plan", {}).get("references", []) or []
|
|
173
|
-
pr_numbers_in_file: list[int] = []
|
|
174
|
-
for ref in references:
|
|
175
|
-
uri = (ref or {}).get("uri", "") if isinstance(ref, dict) else ""
|
|
176
|
-
if not uri:
|
|
177
|
-
continue
|
|
178
|
-
m = _PR_URI_RE.search(uri)
|
|
179
|
-
if not m:
|
|
180
|
-
continue
|
|
181
|
-
pr_numbers_in_file.append(int(m.group("pr")))
|
|
182
|
-
if not pr_numbers_in_file:
|
|
183
|
-
failures.append(
|
|
184
|
-
CohortResolutionError(
|
|
185
|
-
vbrief_path=str(path),
|
|
186
|
-
reason="no x-vbrief/github-pr-style references found",
|
|
187
|
-
)
|
|
188
|
-
)
|
|
189
|
-
continue
|
|
190
|
-
for pr_num in pr_numbers_in_file:
|
|
191
|
-
if pr_num in seen_set:
|
|
192
|
-
continue
|
|
193
|
-
seen_set.add(pr_num)
|
|
194
|
-
seen_prs.append(pr_num)
|
|
195
|
-
return seen_prs, failures
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
# ---------------------------------------------------------------------------
|
|
199
|
-
# Per-PR + cohort evaluation
|
|
200
|
-
# ---------------------------------------------------------------------------
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
@dataclass
|
|
204
|
-
class CohortPRResult:
|
|
205
|
-
"""Per-PR slice of the cohort verdict."""
|
|
206
|
-
pr_number: int
|
|
207
|
-
head_sha: str | None
|
|
208
|
-
verdict: dict # asdict(GreptileVerdict)
|
|
209
|
-
failures: list[str] = field(default_factory=list)
|
|
210
|
-
|
|
211
|
-
@property
|
|
212
|
-
def clean(self) -> bool:
|
|
213
|
-
return not self.failures
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
@dataclass
|
|
217
|
-
class CohortResult:
|
|
218
|
-
"""Aggregate cohort verdict."""
|
|
219
|
-
repo: str | None
|
|
220
|
-
pr_results: list[CohortPRResult] = field(default_factory=list)
|
|
221
|
-
resolution_errors: list[CohortResolutionError] = field(default_factory=list)
|
|
222
|
-
|
|
223
|
-
@property
|
|
224
|
-
def all_clean(self) -> bool:
|
|
225
|
-
return (
|
|
226
|
-
bool(self.pr_results)
|
|
227
|
-
and not self.resolution_errors
|
|
228
|
-
and all(r.clean for r in self.pr_results)
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
def to_dict(self) -> dict:
|
|
232
|
-
return {
|
|
233
|
-
"repo": self.repo,
|
|
234
|
-
"all_clean": self.all_clean,
|
|
235
|
-
"pr_count": len(self.pr_results),
|
|
236
|
-
"pr_results": [
|
|
237
|
-
{
|
|
238
|
-
"pr_number": r.pr_number,
|
|
239
|
-
"head_sha": r.head_sha,
|
|
240
|
-
"clean": r.clean,
|
|
241
|
-
"verdict": r.verdict,
|
|
242
|
-
"failures": list(r.failures),
|
|
243
|
-
}
|
|
244
|
-
for r in self.pr_results
|
|
245
|
-
],
|
|
246
|
-
"resolution_errors": [asdict(e) for e in self.resolution_errors],
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def evaluate_pr(
|
|
251
|
-
pr_number: int,
|
|
252
|
-
repo: str | None,
|
|
253
|
-
) -> CohortPRResult | None:
|
|
254
|
-
"""Evaluate one PR. Returns None on external error (caller maps to EXIT 2).
|
|
255
|
-
|
|
256
|
-
Resolves every fetch / parse / gate call through the ``_mr`` module
|
|
257
|
-
binding so monkey-patching ``_mr.fetch_pr_head_sha`` /
|
|
258
|
-
``_mr.fetch_greptile_comment_body`` (the canonical seam for tests of
|
|
259
|
-
this script AND of pr_merge_readiness itself) propagates here at call
|
|
260
|
-
time. A previous draft captured the fetchers as default keyword
|
|
261
|
-
arguments, which froze the binding at function-definition time and
|
|
262
|
-
silently bypassed monkeypatch; resolving via the module attribute is
|
|
263
|
-
the right late-binding shape.
|
|
264
|
-
"""
|
|
265
|
-
head_sha = _mr.fetch_pr_head_sha(pr_number, repo)
|
|
266
|
-
if head_sha is None:
|
|
267
|
-
return None
|
|
268
|
-
body = _mr.fetch_greptile_comment_body(pr_number, repo)
|
|
269
|
-
if body is None:
|
|
270
|
-
return None
|
|
271
|
-
verdict = _mr.parse_greptile_body(body)
|
|
272
|
-
failures = _mr.evaluate_gates(pr_number, head_sha, verdict)
|
|
273
|
-
return CohortPRResult(
|
|
274
|
-
pr_number=pr_number,
|
|
275
|
-
head_sha=head_sha,
|
|
276
|
-
verdict=asdict(verdict),
|
|
277
|
-
failures=failures,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
# ---------------------------------------------------------------------------
|
|
282
|
-
# CLI
|
|
283
|
-
# ---------------------------------------------------------------------------
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _build_parser() -> argparse.ArgumentParser:
|
|
287
|
-
parser = argparse.ArgumentParser(
|
|
288
|
-
prog="swarm_verify_review_clean",
|
|
289
|
-
description=(
|
|
290
|
-
"Cohort-level CLEAN verification gate (#1364). Exits 0 only when "
|
|
291
|
-
"EVERY PR in the cohort has SHA match, confidence > 3, zero P0/P1, "
|
|
292
|
-
"not errored on the current HEAD. Re-uses the Greptile body parser "
|
|
293
|
-
"from scripts/pr_merge_readiness.py so the per-PR merge gate and "
|
|
294
|
-
"the cohort gate stay in lockstep."
|
|
295
|
-
),
|
|
296
|
-
)
|
|
297
|
-
parser.add_argument(
|
|
298
|
-
"pr_numbers",
|
|
299
|
-
nargs="*",
|
|
300
|
-
type=int,
|
|
301
|
-
help="Explicit PR numbers to verify.",
|
|
302
|
-
)
|
|
303
|
-
parser.add_argument(
|
|
304
|
-
"--cohort",
|
|
305
|
-
dest="cohort_globs",
|
|
306
|
-
action="append",
|
|
307
|
-
default=[],
|
|
308
|
-
metavar="GLOB",
|
|
309
|
-
help=(
|
|
310
|
-
"Glob pattern over vBRIEF JSON files. Each matched vBRIEF's "
|
|
311
|
-
"plan.references[].uri is scanned for github.com/.../pull/<N> "
|
|
312
|
-
"URIs; matching PRs join the cohort. May be passed multiple "
|
|
313
|
-
"times."
|
|
314
|
-
),
|
|
315
|
-
)
|
|
316
|
-
parser.add_argument(
|
|
317
|
-
"--repo",
|
|
318
|
-
default=None,
|
|
319
|
-
metavar="OWNER/REPO",
|
|
320
|
-
help="Repository in OWNER/REPO form. Defaults to the current checkout's remote.",
|
|
321
|
-
)
|
|
322
|
-
parser.add_argument(
|
|
323
|
-
"--json",
|
|
324
|
-
dest="emit_json",
|
|
325
|
-
action="store_true",
|
|
326
|
-
help="Emit the cohort result as a single JSON object on stdout.",
|
|
327
|
-
)
|
|
328
|
-
return parser
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def main(argv: list[str] | None = None) -> int:
|
|
332
|
-
args = _build_parser().parse_args(argv)
|
|
333
|
-
|
|
334
|
-
# Step 1: build the cohort (union of explicit PR numbers + --cohort globs).
|
|
335
|
-
pr_numbers: list[int] = list(dict.fromkeys(args.pr_numbers)) # de-dupe, preserve order
|
|
336
|
-
resolution_errors: list[CohortResolutionError] = []
|
|
337
|
-
if args.cohort_globs:
|
|
338
|
-
discovered, errs = resolve_cohort_from_vbriefs(args.cohort_globs)
|
|
339
|
-
for pr_num in discovered:
|
|
340
|
-
if pr_num not in pr_numbers:
|
|
341
|
-
pr_numbers.append(pr_num)
|
|
342
|
-
resolution_errors.extend(errs)
|
|
343
|
-
|
|
344
|
-
# Empty cohort is a config error -- the gate cannot affirm CLEAN over
|
|
345
|
-
# zero PRs (it would silently exit 0 and let the merge discussion
|
|
346
|
-
# proceed). Surface as EXIT_EXTERNAL_ERROR.
|
|
347
|
-
if not pr_numbers:
|
|
348
|
-
msg = (
|
|
349
|
-
"Error: empty cohort. Pass one or more PR numbers as positional "
|
|
350
|
-
"arguments and/or --cohort <glob> to discover PRs from vBRIEF "
|
|
351
|
-
"references."
|
|
352
|
-
)
|
|
353
|
-
if args.emit_json:
|
|
354
|
-
result = CohortResult(
|
|
355
|
-
repo=args.repo, pr_results=[], resolution_errors=resolution_errors
|
|
356
|
-
)
|
|
357
|
-
print(json.dumps(result.to_dict(), indent=2))
|
|
358
|
-
else:
|
|
359
|
-
print(msg, file=sys.stderr)
|
|
360
|
-
if resolution_errors:
|
|
361
|
-
for err in resolution_errors:
|
|
362
|
-
print(f" [{err.vbrief_path}] {err.reason}", file=sys.stderr)
|
|
363
|
-
return EXIT_EXTERNAL_ERROR
|
|
364
|
-
|
|
365
|
-
# If the --cohort globs surfaced resolution errors AND no PRs at all
|
|
366
|
-
# were resolved from them (but explicit PR numbers are present), keep
|
|
367
|
-
# going -- the explicit args satisfy the intent. If both surfaces
|
|
368
|
-
# contribute nothing, the empty-cohort branch above already handled
|
|
369
|
-
# it. We keep the resolution_errors in the result regardless so a
|
|
370
|
-
# JSON consumer / human reader can see partial failures.
|
|
371
|
-
|
|
372
|
-
# Step 2: per-PR evaluation.
|
|
373
|
-
pr_results: list[CohortPRResult] = []
|
|
374
|
-
for pr_num in pr_numbers:
|
|
375
|
-
per_pr = evaluate_pr(pr_num, args.repo)
|
|
376
|
-
if per_pr is None:
|
|
377
|
-
# External error already printed by the fetchers; abort the
|
|
378
|
-
# cohort with EXIT_EXTERNAL_ERROR so the operator sees the
|
|
379
|
-
# failed PR rather than a misleading "MERGE-BLOCKED" verdict
|
|
380
|
-
# on stale state.
|
|
381
|
-
return EXIT_EXTERNAL_ERROR
|
|
382
|
-
pr_results.append(per_pr)
|
|
383
|
-
|
|
384
|
-
cohort = CohortResult(
|
|
385
|
-
repo=args.repo,
|
|
386
|
-
pr_results=pr_results,
|
|
387
|
-
resolution_errors=resolution_errors,
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
if args.emit_json:
|
|
391
|
-
print(json.dumps(cohort.to_dict(), indent=2))
|
|
392
|
-
else:
|
|
393
|
-
_render_text(cohort)
|
|
394
|
-
|
|
395
|
-
return EXIT_OK if cohort.all_clean else EXIT_UNCLEAN
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def _render_text(cohort: CohortResult) -> None:
|
|
399
|
-
"""Pretty-print the cohort verdict for human consumers."""
|
|
400
|
-
n = len(cohort.pr_results)
|
|
401
|
-
print(f"Swarm cohort CLEAN verification ({n} PR{'s' if n != 1 else ''})")
|
|
402
|
-
if cohort.repo:
|
|
403
|
-
print(f" Repo: {cohort.repo}")
|
|
404
|
-
if cohort.resolution_errors:
|
|
405
|
-
print(" Resolution errors:")
|
|
406
|
-
for err in cohort.resolution_errors:
|
|
407
|
-
print(f" [{err.vbrief_path}] {err.reason}")
|
|
408
|
-
for r in cohort.pr_results:
|
|
409
|
-
status = "CLEAN" if r.clean else "UNCLEAN"
|
|
410
|
-
v = r.verdict
|
|
411
|
-
print()
|
|
412
|
-
print(f" PR #{r.pr_number} -- {status}")
|
|
413
|
-
print(f" HEAD SHA: {r.head_sha or '<unknown>'}")
|
|
414
|
-
print(f" Greptile reviewed: {v.get('last_reviewed_sha') or '<not parsed>'}")
|
|
415
|
-
conf = v.get("confidence")
|
|
416
|
-
conf_str = str(conf) if conf is not None else "<not parsed>"
|
|
417
|
-
print(f" Confidence: {conf_str}/5")
|
|
418
|
-
print(
|
|
419
|
-
f" Findings: P0={v.get('p0_count', 0)} "
|
|
420
|
-
f"P1={v.get('p1_count', 0)} P2={v.get('p2_count', 0)}"
|
|
421
|
-
)
|
|
422
|
-
print(f" Errored sentinel: {v.get('errored', False)}")
|
|
423
|
-
for i, fail in enumerate(r.failures, 1):
|
|
424
|
-
print(f" [{i}] {fail}")
|
|
425
|
-
print()
|
|
426
|
-
if cohort.all_clean:
|
|
427
|
-
print("Result: COHORT CLEAN -- Phase 5 -> 6 merge discussion may proceed")
|
|
428
|
-
else:
|
|
429
|
-
n_unclean = sum(1 for r in cohort.pr_results if not r.clean)
|
|
430
|
-
print(
|
|
431
|
-
f"Result: COHORT BLOCKED -- {n_unclean}/{n} PR(s) unclean. "
|
|
432
|
-
"Do NOT raise the Phase 5 -> 6 gate; re-dispatch pollers or "
|
|
433
|
-
"address findings, then re-run task swarm:verify-review-clean."
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if __name__ == "__main__":
|
|
438
|
-
sys.exit(main())
|