@deftai/directive-content 0.59.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.
Files changed (184) hide show
  1. package/.githooks/pre-push +10 -9
  2. package/Taskfile.yml +48 -58
  3. package/UPGRADING.md +1 -1
  4. package/docs/assets/directive-lifecycle-diagram.png +0 -0
  5. package/docs/directive-lifecycle.md +73 -0
  6. package/docs/getting-started.md +5 -1
  7. package/package.json +3 -3
  8. package/packs/skills/skills-pack-0.1.json +22 -22
  9. package/scm/github.md +20 -2
  10. package/tasks/change.yml +16 -31
  11. package/tasks/ci.yml +8 -0
  12. package/tasks/commit.yml +12 -19
  13. package/tasks/core.yml +10 -0
  14. package/tasks/engine.yml +42 -0
  15. package/tasks/framework.yml +3 -0
  16. package/tasks/install.yml +20 -19
  17. package/tasks/migrate.yml +26 -15
  18. package/tasks/project.yml +16 -0
  19. package/tasks/toolchain.yml +15 -5
  20. package/tasks/vbrief.yml +4 -3
  21. package/tasks/verify.yml +12 -14
  22. package/scripts/_agents_md.py +0 -494
  23. package/scripts/_cache_fetch.py +0 -635
  24. package/scripts/_cache_quota.py +0 -529
  25. package/scripts/_cache_refresh.py +0 -163
  26. package/scripts/_cache_validate.py +0 -209
  27. package/scripts/_content_root.py +0 -42
  28. package/scripts/_doctor_state.py +0 -277
  29. package/scripts/_event_detect.py +0 -305
  30. package/scripts/_events.py +0 -514
  31. package/scripts/_lifecycle_hygiene.py +0 -568
  32. package/scripts/_pathspec.py +0 -91
  33. package/scripts/_policy_show_cli.py +0 -266
  34. package/scripts/_precutover.py +0 -92
  35. package/scripts/_project_context.py +0 -224
  36. package/scripts/_project_definition_io.py +0 -164
  37. package/scripts/_relocate_snapshot.py +0 -209
  38. package/scripts/_relocate_states.py +0 -343
  39. package/scripts/_resolve_preflight_path.py +0 -152
  40. package/scripts/_safe_subprocess.py +0 -167
  41. package/scripts/_session_start_hook.py +0 -205
  42. package/scripts/_sor_gate_diff.py +0 -365
  43. package/scripts/_stdio_utf8.py +0 -59
  44. package/scripts/_triage_bootstrap_gitignore.py +0 -904
  45. package/scripts/_triage_classify_cli.py +0 -122
  46. package/scripts/_triage_queue_cli.py +0 -625
  47. package/scripts/_triage_scope_cli.py +0 -343
  48. package/scripts/_triage_scope_drift_cli.py +0 -121
  49. package/scripts/_triage_scope_ignores.py +0 -286
  50. package/scripts/_triage_scope_milestone.py +0 -432
  51. package/scripts/_triage_scope_mutations.py +0 -337
  52. package/scripts/_triage_scope_renderers.py +0 -207
  53. package/scripts/_triage_smoketest_stages.py +0 -674
  54. package/scripts/_triage_subscribe_cli.py +0 -140
  55. package/scripts/_triage_welcome_cli.py +0 -421
  56. package/scripts/_vbrief_build.py +0 -239
  57. package/scripts/_vbrief_fidelity.py +0 -479
  58. package/scripts/_vbrief_legacy.py +0 -589
  59. package/scripts/_vbrief_reconciliation.py +0 -883
  60. package/scripts/_vbrief_routing.py +0 -277
  61. package/scripts/_vbrief_safety.py +0 -778
  62. package/scripts/_vbrief_sources.py +0 -312
  63. package/scripts/_vbrief_speckit.py +0 -262
  64. package/scripts/_vbrief_story_quality.py +0 -353
  65. package/scripts/_vbrief_validation.py +0 -299
  66. package/scripts/build_dist.py +0 -412
  67. package/scripts/cache.py +0 -1078
  68. package/scripts/cache_scanner.py +0 -745
  69. package/scripts/candidates_log.py +0 -432
  70. package/scripts/capacity_backfill.py +0 -680
  71. package/scripts/capacity_show.py +0 -653
  72. package/scripts/ci_local.py +0 -689
  73. package/scripts/code_structure_validate.py +0 -765
  74. package/scripts/codebase_default_extractor.py +0 -495
  75. package/scripts/codebase_map.py +0 -304
  76. package/scripts/codebase_map_fresh.py +0 -104
  77. package/scripts/codebase_projection_registry.py +0 -94
  78. package/scripts/codebase_provider.py +0 -582
  79. package/scripts/doctor.py +0 -2552
  80. package/scripts/framework_commands.py +0 -505
  81. package/scripts/gh_rest.py +0 -882
  82. package/scripts/github_auth_modes.py +0 -437
  83. package/scripts/github_body.py +0 -292
  84. package/scripts/ip_risk.py +0 -531
  85. package/scripts/issue_emit.py +0 -670
  86. package/scripts/issue_ingest.py +0 -1064
  87. package/scripts/migrate_preflight.py +0 -418
  88. package/scripts/migrate_vbrief.py +0 -2677
  89. package/scripts/monitor_pr.py +0 -401
  90. package/scripts/pack_migrate_lessons.py +0 -336
  91. package/scripts/pack_migrate_patterns.py +0 -254
  92. package/scripts/pack_migrate_rules.py +0 -350
  93. package/scripts/pack_migrate_skills.py +0 -423
  94. package/scripts/pack_migrate_strategies.py +0 -311
  95. package/scripts/pack_migrate_swarm_spec.py +0 -250
  96. package/scripts/pack_render.py +0 -434
  97. package/scripts/packs_slice.py +0 -712
  98. package/scripts/platform_capabilities.py +0 -336
  99. package/scripts/policy.py +0 -2826
  100. package/scripts/policy_set.py +0 -324
  101. package/scripts/pr_check_closing_keywords.py +0 -524
  102. package/scripts/pr_check_protected_issues.py +0 -267
  103. package/scripts/pr_merge_readiness.py +0 -1004
  104. package/scripts/pr_wait_mergeable.py +0 -669
  105. package/scripts/prd_render.py +0 -159
  106. package/scripts/preflight_architecture_sor.py +0 -974
  107. package/scripts/preflight_branch.py +0 -289
  108. package/scripts/preflight_cache.py +0 -974
  109. package/scripts/preflight_gh.py +0 -721
  110. package/scripts/preflight_implementation.py +0 -272
  111. package/scripts/preflight_story_start.py +0 -838
  112. package/scripts/preflight_wip_cap.py +0 -149
  113. package/scripts/probe_session.py +0 -545
  114. package/scripts/project_render.py +0 -293
  115. package/scripts/quarantine_ext.py +0 -237
  116. package/scripts/reconcile_issues.py +0 -1442
  117. package/scripts/refresh-path.ps1 +0 -107
  118. package/scripts/release.py +0 -2030
  119. package/scripts/release_e2e.py +0 -1011
  120. package/scripts/release_publish.py +0 -486
  121. package/scripts/release_rollback.py +0 -980
  122. package/scripts/relocate.py +0 -1034
  123. package/scripts/resolve_changelog_unreleased.py +0 -667
  124. package/scripts/resolve_version.py +0 -490
  125. package/scripts/resume_conditions.py +0 -706
  126. package/scripts/ritual_sentinel.py +0 -609
  127. package/scripts/roadmap_render.py +0 -635
  128. package/scripts/rule_ownership_lint.py +0 -325
  129. package/scripts/scm.py +0 -591
  130. package/scripts/scope_audit_log.py +0 -387
  131. package/scripts/scope_decompose.py +0 -654
  132. package/scripts/scope_demote.py +0 -509
  133. package/scripts/scope_lifecycle.py +0 -1126
  134. package/scripts/scope_undo.py +0 -772
  135. package/scripts/session_start.py +0 -406
  136. package/scripts/setup_ghx.py +0 -339
  137. package/scripts/setup_windows.ps1 +0 -220
  138. package/scripts/slice_audit.py +0 -585
  139. package/scripts/slice_record.py +0 -530
  140. package/scripts/slice_record_existing.py +0 -692
  141. package/scripts/slug_normalize.py +0 -178
  142. package/scripts/spec_render.py +0 -477
  143. package/scripts/spec_validate.py +0 -238
  144. package/scripts/subagent_monitor.py +0 -658
  145. package/scripts/swarm_complete_cohort.py +0 -644
  146. package/scripts/swarm_launch.py +0 -1206
  147. package/scripts/swarm_readiness.py +0 -554
  148. package/scripts/swarm_verify_review_clean.py +0 -438
  149. package/scripts/swarm_worktrees.py +0 -497
  150. package/scripts/toolchain-check.py +0 -52
  151. package/scripts/triage_actions.py +0 -871
  152. package/scripts/triage_bootstrap.py +0 -1153
  153. package/scripts/triage_bulk.py +0 -630
  154. package/scripts/triage_classify.py +0 -932
  155. package/scripts/triage_help.py +0 -1685
  156. package/scripts/triage_queue.py +0 -1944
  157. package/scripts/triage_reconcile.py +0 -581
  158. package/scripts/triage_refresh.py +0 -643
  159. package/scripts/triage_scope.py +0 -999
  160. package/scripts/triage_scope_drift.py +0 -575
  161. package/scripts/triage_smoketest.py +0 -396
  162. package/scripts/triage_subscribe.py +0 -399
  163. package/scripts/triage_summary.py +0 -1011
  164. package/scripts/triage_welcome.py +0 -1178
  165. package/scripts/ts_check_lane.py +0 -86
  166. package/scripts/validate-links.py +0 -64
  167. package/scripts/validate_strategy_output.py +0 -212
  168. package/scripts/vbrief_activate.py +0 -228
  169. package/scripts/vbrief_migrate_conformance.py +0 -368
  170. package/scripts/vbrief_reconcile_graph.py +0 -306
  171. package/scripts/vbrief_reconcile_labels.py +0 -460
  172. package/scripts/vbrief_reconcile_umbrellas.py +0 -741
  173. package/scripts/vbrief_validate.py +0 -1144
  174. package/scripts/verify-stubs.py +0 -61
  175. package/scripts/verify_capacity.py +0 -160
  176. package/scripts/verify_encoding.py +0 -699
  177. package/scripts/verify_hooks_installed.py +0 -206
  178. package/scripts/verify_investigation.py +0 -360
  179. package/scripts/verify_judgment_gates.py +0 -827
  180. package/scripts/verify_no_task_runtime.py +0 -171
  181. package/scripts/verify_scm_boundary.py +0 -509
  182. package/scripts/verify_session_ritual.py +0 -389
  183. package/scripts/verify_tools.py +0 -426
  184. 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())