@deftai/directive-content 0.59.0 → 0.61.0

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