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