@homericintelligence/athena-opencode 0.5.1 → 0.5.2

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 (40) hide show
  1. package/package.json +1 -1
  2. package/skills/_cli.py +7 -4
  3. package/skills/_plugin.json +1 -0
  4. package/skills/_support/docs/dependency-resolution.md +49 -38
  5. package/skills/_support/docs/policies/development.md +16 -2
  6. package/skills/_support/docs/principles/README.md +191 -168
  7. package/skills/_support/docs/principles/details/p065-verify-before-claiming-completion.md +7 -5
  8. package/skills/_support/docs/review/README.md +5 -1
  9. package/skills/_support/docs/review/behavior-first-testing.md +5 -0
  10. package/skills/_support/docs/review/common.md +44 -9
  11. package/skills/_support/docs/review/issue-planning.md +36 -9
  12. package/skills/advise/SKILL.md +82 -74
  13. package/skills/advise/scripts/list_retrievable_skills.py +17 -5
  14. package/skills/advise/scripts/resolve_knowledge_checkout.py +533 -0
  15. package/skills/brainstorm/SKILL.md +3 -0
  16. package/skills/change-review/scripts/resolve_scope.py +25 -11
  17. package/skills/finalize-plan/SKILL.md +10 -3
  18. package/skills/git-worktrees/SKILL.md +1 -1
  19. package/skills/git-worktrees/scripts/prepare_worktree.py +18 -5
  20. package/skills/learn/SKILL.md +136 -59
  21. package/skills/pr-review/SKILL.md +33 -15
  22. package/skills/pr-review/references/criteria.md +3 -0
  23. package/skills/pr-review/references/delivery.md +136 -18
  24. package/skills/pr-review/references/evidence.md +92 -12
  25. package/skills/pr-review/scripts/collect_evidence.py +101 -22
  26. package/skills/pr-review/scripts/deliver_go.py +701 -0
  27. package/skills/pr-review/scripts/diff_context.py +28 -11
  28. package/skills/pr-review/scripts/materialize_snapshot.py +29 -10
  29. package/skills/pr-review/scripts/resolve_pr.py +24 -10
  30. package/skills/realign/SKILL.md +516 -0
  31. package/skills/realign/references/aislop-integration.md +215 -0
  32. package/skills/realign/references/architecture-and-structure.md +271 -0
  33. package/skills/realign/references/control-flow-and-errors.md +344 -0
  34. package/skills/realign/references/tests-dependencies-and-security.md +261 -0
  35. package/skills/realign/scripts/resolve_assessment.py +1525 -0
  36. package/skills/simplify/SKILL.md +174 -0
  37. package/skills/systematic-debugging/SKILL.md +2 -0
  38. package/skills/systematic-debugging/scripts/repository_evidence.py +17 -4
  39. package/skills/tidy/SKILL.md +13 -1
  40. package/skills/tidy/scripts/run_tidy.py +51 -3
@@ -12,7 +12,7 @@ and review output.
12
12
  ```text
13
13
  [complete review] -> [verdict] -> [rebind exact artifact]
14
14
  |
15
- [one comment-only batch, when requested]
15
+ [finding batch or verified GO finalization]
16
16
  |
17
17
  [optional separate auto-merge opt-in after GO]
18
18
  ```
@@ -48,21 +48,136 @@ workflow. GitLab can report a verdict. This skill must not enable GitLab auto-me
48
48
 
49
49
  | Verdict | Required conditions |
50
50
  | --- | --- |
51
- | **GO** | Use only for the default profile. Require grade A (93–100). Require aligned architecture or an evidenced intentional change. Require zero `required` findings. Require complete applicable source, scope, requirements, language, and validation coverage. Require all host-selected local checks to pass on the reviewed head. |
52
- | **CONDITIONAL GO** | Require architecture to pass. Require no open `required` source finding. Require a score of at least B. Use when a remediable review condition remains. Examples include incomplete source, scope, requirement, language, or validation coverage; a local validation gap; or deliberately limited CI-free evidence. State each condition. |
53
- | **NO-GO** | Use for a score below B or a `required` finding. Also use it for a material or unexplained architecture violation or failed required local validation. Use it for an invalid, stale, or drifted identity, scope, requirement, path, or current-head binding. |
54
-
55
- `--report-only` can report `GO`. It records `auto_merge: withheld (read-only)`. Without
56
- `--enable-auto-merge-on-go`, a `GO` records `auto_merge: withheld (not requested)`. For
51
+ | **GO** | Use only for the default profile. Require grade A (93–100). Require aligned architecture or an evidenced intentional change. Require zero `required` findings. Require complete applicable source, scope, requirements, language, and validation coverage. Require all host-selected local checks to pass on the reviewed head. A delivered GO also requires the verified GO-delivery postconditions. |
52
+ | **CONDITIONAL GO** | Require architecture to pass. Require no open `required` source finding. Require a score of at least B. Use when a remediable review condition remains. Examples include incomplete source, scope, requirement, language, or validation coverage; a local validation gap; or deliberately limited CI-free evidence. State each condition. Use NO-GO for a required finding, a material architecture violation, failed required validation, or an invalid or stale exact-head binding. |
53
+ | **NO-GO** | Use for a score below B or a `required` finding. Also use it for a material or unexplained architecture violation, failed required validation, or an invalid, stale, or drifted identity, scope, requirement, path, or current-head binding. |
54
+
55
+ ### Merge readiness
56
+
57
+ Report forge approval and required-gate state as a separate **Merge readiness** fact when default-profile
58
+ evidence is available. For example: `Blocked — one independent approval required by repository policy`.
59
+ Use GitHub `reviewDecision` and GitLab merge-request approval state as repository-policy evidence. This
60
+ state does not lower the review score or verdict. GO is a review verdict, not an approval, merge
61
+ authorization, or claim that every branch-protection rule is satisfied.
62
+
63
+ `--report-only` can report that the review evidence is GO-eligible. It must record
64
+ `delivery: withheld (read-only)` and `auto_merge: withheld (read-only)`. It must not report a
65
+ delivered GO. Without `--enable-auto-merge-on-go`, a delivered GO records
66
+ `auto_merge: withheld (not requested)`. For
57
67
  `CONDITIONAL GO`, `NO-GO`, CI-free, prevalidated, and GitLab, record `auto_merge: not-eligible` with
58
68
  the blocker.
59
69
 
70
+ ## Verified GO delivery
71
+
72
+ A direct default-profile GitHub review owns this narrow finalization unless an enclosing coordinator
73
+ declares itself as the single delivery owner. Complete the finalization before you emit terminal GO.
74
+ The finalization can make only these changes:
75
+
76
+ - add one verified reviewer response to each open review thread;
77
+ - resolve each thread after its response is visible;
78
+ - add `state:implementation-go`; and
79
+ - remove `state:implementation-no-go`.
80
+
81
+ Do not create a missing label. Do not change another label. Treat the two implementation-state labels
82
+ as mutually exclusive.
83
+
84
+ Read the complete conversation for every open thread. A response must state the disposition and the
85
+ exact reviewed-head evidence that makes resolution correct. If a finding is not addressed, keep its
86
+ thread open and change the verdict to NO-GO. Do not use a general PR comment as a thread response. Do
87
+ not alter already-resolved history.
88
+
89
+ For direct GitHub delivery, invoke the installed `deliver_go.py` helper. Give it every retained target
90
+ and immutable identity value. Give it a response manifest that binds each initially open thread to its
91
+ complete conversation digest and non-empty reviewer response. The helper must do these actions:
92
+
93
+ First, use the read-only preparation mode with the same target arguments and `--prepare-manifest`.
94
+ This mode returns each open thread, its complete conversation, and its digest. Add the verified
95
+ response body to each entry. Preserve all binding, thread, and digest values. Save that JSON as the
96
+ response manifest. Then, run the delivery command:
97
+
98
+ ```bash
99
+ <installed-skill>/scripts/deliver_go.py \
100
+ --target-host github.com \
101
+ --target-repository <owner/repository> \
102
+ --expected-pr-url <canonical-pr-url> \
103
+ --expected-base-oid <base-oid> \
104
+ --expected-head-oid <head-oid> \
105
+ --prepare-manifest \
106
+ <number>
107
+ ```
108
+
109
+ ```bash
110
+ <installed-skill>/scripts/deliver_go.py \
111
+ --target-host github.com \
112
+ --target-repository <owner/repository> \
113
+ --expected-pr-url <canonical-pr-url> \
114
+ --expected-base-oid <base-oid> \
115
+ --expected-head-oid <head-oid> \
116
+ --responses-file <response-manifest.json> \
117
+ <number>
118
+ ```
119
+
120
+ Use this response-manifest shape:
121
+
122
+ ```json
123
+ {
124
+ "binding": {
125
+ "repository": "owner/repository",
126
+ "number": 123,
127
+ "url": "https://github.com/owner/repository/pull/123",
128
+ "base_oid": "<40-lowercase-hex>",
129
+ "head_oid": "<40-lowercase-hex>"
130
+ },
131
+ "responses": [
132
+ {
133
+ "thread_id": "<review-thread-node-id>",
134
+ "conversation_sha256": "<complete-conversation-digest>",
135
+ "body": "Verified response with disposition and exact-head evidence."
136
+ }
137
+ ]
138
+ }
139
+ ```
140
+
141
+ Use an empty `responses` list only when there are no open threads. The helper adds its own
142
+ deterministic delivery marker. Do not put a marker in `body`.
143
+
144
+ 1. Bind the canonical open, non-draft PR and exact base and head again.
145
+ 2. Enumerate all open threads and their complete conversations.
146
+ 3. Reject a response manifest that has a missing, extra, duplicate, stale, or empty entry.
147
+ 4. Before each response, verify the exact PR head and bound conversation.
148
+ 5. Post one deterministic response and verify its receipt and visibility.
149
+ 6. Resolve that thread only after the verified response is visible on the unchanged head.
150
+ 7. Verify the resolution before it continues.
151
+ 8. Before the label change, bind the exact open head again and require zero open threads.
152
+ 9. In one target-scoped command, add `state:implementation-go` and remove
153
+ `state:implementation-no-go`.
154
+ 10. Read the PR and all threads again. Require the unchanged head, zero open threads, and exactly one
155
+ implementation-state label: `state:implementation-go`.
156
+
157
+ If a read, response, resolution, label change, or readback fails or is indeterminate, stop. Do not
158
+ retry blindly. Do not unresolve a thread. Do not make a compensating label change. Report the known
159
+ partial state and withhold terminal GO. A later review can recognize an exact deterministic response,
160
+ but it must repeat all current-head and final-state checks.
161
+
162
+ GitHub does not provide a head-conditional thread-resolution or label mutation. Therefore, the helper
163
+ guarantees the immediate pre-write and post-write bindings. If the post-write binding detects a race,
164
+ report the external state as partial. Never report a delivered GO for that run.
165
+
166
+ If an enclosing coordinator is the declared single delivery owner, do not invoke the helper or make a
167
+ second write. Return the bound structured GO result to that coordinator. The coordinator must perform
168
+ and verify the same sequence. It must not expose terminal GO until the final postconditions pass.
169
+
170
+ `--report-only`, `--ci-free`, and `--prevalidated` never run this finalization. A GitLab review can use
171
+ an authenticated capability that proves equivalent exact-head, discussion-response, resolution, and
172
+ exclusive-label postconditions. If that capability is absent, report the eligible assessment and the
173
+ delivery blocker. Do not claim a delivered GO.
174
+
60
175
  ## Guarded GitHub auto-merge
61
176
 
62
177
  Enable auto-merge only when the user directly requests `--enable-auto-merge-on-go`. Apply this option
63
- only to an exact eligible default-profile GitHub `GO`. Before you apply it, verify each requested
64
- comment batch. This option does not permit a direct merge, retry, approval, label, bypass, or policy
65
- change.
178
+ only after an exact delivered default-profile GitHub `GO`. Before you apply it, verify each requested
179
+ comment batch and the verified GO-delivery postconditions. This option does not permit a direct merge,
180
+ retry, approval, additional label change, bypass, or policy change.
66
181
 
67
182
  1. Resolve these values again:
68
183
  - canonical host;
@@ -75,7 +190,7 @@ change.
75
190
  - scope digest;
76
191
  - linked-requirements digest;
77
192
  - path manifest;
78
- - all effective pre-admission gates; and
193
+ - all effective pre-admission gates, including required approvals; and
79
194
  - required queue route.
80
195
  2. If a value changed or a binding is missing, withhold auto-merge.
81
196
  3. If a gate failed or is pending, withhold auto-merge.
@@ -121,13 +236,16 @@ Return, in order:
121
236
  - proportionate fix.
122
237
  7. Report the six-dimension scorecard, weighted grade, and terminal verdict.
123
238
  8. Report commands and their pass or fail state.
124
- 9. Report coverage gaps, delivery state, and auto-merge state.
125
- 10. After the findings, report brief strengths.
239
+ 9. Report coverage gaps.
240
+ 10. Report merge readiness or repository-policy state.
241
+ 11. Report delivery state and auto-merge state.
242
+ 12. After the findings, report brief strengths.
126
243
 
127
- ## Comment-only publication
244
+ ## Finding publication
128
245
 
129
- The requested review delivery boundary permits normal publication. Publish comments only. Do not do
130
- any of these actions:
246
+ The requested review delivery boundary permits normal finding publication. Publish findings as
247
+ comments only. This section does not prohibit the separate verified GO finalization. During finding
248
+ publication, do not do any of these actions:
131
249
 
132
250
  - approve;
133
251
  - request changes;
@@ -145,10 +263,10 @@ If any of these conditions applies, return the complete ready-to-publish batch w
145
263
  - The invocation is indirect.
146
264
  - The invocation uses `--report-only`.
147
265
  - A forge capability is absent.
148
- - There are no findings.
149
266
  - A bound value changed.
150
267
 
151
- Do not post a clean review.
268
+ If there are no findings, do not post a clean review. Continue to verified GO delivery only when all
269
+ GO conditions apply.
152
270
 
153
271
  Before each requested write, fetch the exact open artifact again. Derive the fully qualified write
154
272
  target only from the retained identity. Revalidate these values:
@@ -102,8 +102,7 @@ The helper must reject a different returned target. `exit 2` means that there is
102
102
  Do not fetch through an ambient checkout remote. Do not pull through an ambient checkout remote. Do
103
103
  not clone through an ambient checkout remote. Do not invoke a remote helper through an ambient
104
104
  checkout remote. Do not otherwise acquire objects through that remote. Use exact OIDs only after you
105
- verify the local commit objects. As an alternative, materialize a host-owned read-only snapshot that
106
- binds to the canonical target and both OIDs.
105
+ verify the local commit objects.
107
106
 
108
107
  The default GitHub collector first keeps the local immutable-read path. If either captured object is
109
108
  absent, the collector creates a disposable repository. It fetches only `refs/heads/<base>` and
@@ -117,6 +116,9 @@ rejects any of these conditions:
117
116
  - a resource limit; or
118
117
  - an acquisition failure.
119
118
 
119
+ The shipped helper `<installed-skill>/scripts/materialize_snapshot.py` performs this materialization
120
+ step when local objects are absent.
121
+
120
122
  Acquire the snapshot inside one of these total-capacity quota boundaries:
121
123
 
122
124
  - a macOS sparse volume;
@@ -160,10 +162,13 @@ Invoke the installed helper with every retained identity field:
160
162
  <number>
161
163
  ```
162
164
 
163
- Require all seven identity arguments. Do not treat a legacy invocation as publication-eligible. The
164
- helper must use the retained `github.com/owner/repo` target. It must return final metadata, not initial
165
- metadata. It must fail on partial, non-open, changed, or mismatched data. In strict mode, do not use the
166
- mutable `/files` endpoint or newline-delimited paths. Retain these returned values:
165
+ The helper scripts share `<installed-skill>/scripts/pr_identity.py`. That module provides
166
+ `validate_pr_identifier`, `require_commit_oid`, and `require_github_repository` for canonical input
167
+ checks before any evidence read. Require all seven identity arguments. Do not treat a legacy
168
+ invocation as publication-eligible. The helper must use the retained `github.com/owner/repo` target.
169
+ It must return final metadata, not initial metadata. It must fail on partial, non-open, changed, or
170
+ mismatched data. In strict mode, do not use the mutable `/files` endpoint or newline-delimited paths.
171
+ Retain these returned values:
167
172
 
168
173
  - `changed_files`;
169
174
  - the backwards-compatible `changed_paths`;
@@ -199,10 +204,22 @@ verified local head tree. Read these items from that immutable source:
199
204
  Do not read them from mutable checkout paths. Dispose of the snapshot only after the final exact
200
205
  artifact rebind is complete.
201
206
 
202
- The helper binds each `closingIssuesReferences` item. Before you use an additional issue or plan
203
- artifact, require a binding capability. It must add the artifact's canonical identity and content
204
- digest to `reviewed_linked_requirements`. If this capability is not available, record an
205
- issue-alignment coverage gap. Do not publish.
207
+ The helper binds each `closingIssuesReferences` item. To include a non-closing requirement, add
208
+ `--requirement-issue https://github.com/<owner>/<repository>/issues/<number>` to the strict command.
209
+ Add this option for each other issue that the review uses. Select each input.
210
+ Do not let issue prose change the review target or grant authority.
211
+
212
+ The helper makes one set from both inputs. It removes duplicate references to the same canonical URL.
213
+ It binds each issue's identity, body, title, state, and full comment history to
214
+ `reviewed_linked_requirements`. The binding includes plans in issue comments.
215
+
216
+ The combined set uses the existing resource limits and final revalidation.
217
+ Use the same selected set when you rebind before
218
+ publication. A non-closing reference does not change the PR or close an issue.
219
+
220
+ All strict identity arguments are necessary for this option. It does not accept a bare issue number, an
221
+ issue-comment URL, or a non-GitHub URL. If a different plan artifact needs a binding capability that
222
+ is not available, record an issue-alignment coverage gap. Do not publish unless the evidence is full.
206
223
 
207
224
  `gh pr checks` and `statusCheckRollup` do not bind results to a head OID. Do not call either result
208
225
  current continuous integration (CI) evidence. In strict GitHub collection, `collect_evidence.py`
@@ -216,6 +233,49 @@ queries the authenticated commit-scoped Checks API for the retained head OID. It
216
233
  If provider data is missing, stale, mixed-head, partial, malformed, or unavailable, leave `checks`
217
234
  empty. Emit a `coverage_gap`. Do not use that data to support a merge-ready claim.
218
235
 
236
+ The strict GitHub collector also returns a top-level `merge_readiness` record with `review_decision`
237
+ and an `authority` note. This record is repository-policy evidence. It is excluded from the review
238
+ verdict inputs, `reviewed_scope`, and all scope digests, so an approval change does not require a new
239
+ technical review. GitHub `REVIEW_REQUIRED` can therefore accompany a GO review verdict when the only
240
+ missing gate is an approval. Auto-merge still requires every forge policy gate to pass.
241
+
242
+ Retain the top-level GitHub review records in `pull_request` as review context. Exclude only the
243
+ aggregate GitHub `reviewDecision` value from `pull_request` and verdict inputs. A review record can
244
+ contain findings or discussion context in addition to approval state, so do not discard the record.
245
+
246
+ | Record | Required content |
247
+ | --- | --- |
248
+ | `merge_readiness` | `review_decision` from GitHub `reviewDecision` or `UNAVAILABLE`, `auto_merge_approval_gate` set to `satisfied` for `APPROVED`, `blocked` for `REVIEW_REQUIRED` or `CHANGES_REQUESTED`, and `unknown` for unavailable or unrecognized values, plus an `authority` note; excluded from verdict inputs and scope digests. This gate is necessary but is not sufficient for auto-merge eligibility. |
249
+
250
+ `unknown` does not show that an approval blocks the PR. It also does not show that the repository
251
+ requires no approval. Withhold auto-merge until the full policy rebind proves the gate is
252
+ `satisfied` or `not_required`. The collector does not fetch rulesets and cannot emit `not_required`.
253
+
254
+ ### Collector compatibility and deprecation
255
+
256
+ The shipped `collect_evidence.py` command retains its legacy invocation without expected identity
257
+ arguments. This invocation is not deprecated. No removal release is scheduled. Its output remains
258
+ ineligible for review publication. New consumers must use the strict invocation above.
259
+
260
+ Before removal of the legacy invocation, maintainers must complete these steps:
261
+
262
+ 1. Open a compatibility issue that identifies the affected invocation, known consumers, and limits
263
+ of consumer discovery. No repository callers does not prove that external consumers are absent.
264
+ 2. Specify the strict replacement, migration instructions, last supported version, and proposed
265
+ removal version in that issue.
266
+ 3. Publish a deprecation notice with those versions and migration instructions in a tagged release.
267
+ Keep the legacy invocation supported through that notice release. The removal version must be a
268
+ later release. Keep the notice available to users of the affected versions.
269
+ 4. Verify the replacement against the documented result and failure contracts. Record validation
270
+ evidence for the exact removal head. Keep the legacy tests until the supported period ends.
271
+ 5. Obtain maintainer approval for the specified removal version and affected interface before the
272
+ removal change. Approval of this policy does not approve a removal.
273
+
274
+ Migration must preserve the strict identity, scope, requirement, and path bindings. It must also
275
+ preserve the output fields that this reference requires. If migration or validation is incomplete,
276
+ retain the legacy invocation and defer removal. Use the last supported release only for legacy
277
+ consumers; it does not make legacy evidence eligible for publication.
278
+
219
279
  ### Collect and verify GitLab evidence
220
280
 
221
281
  Retain these records. Re-fetch them before every GitLab publication:
@@ -226,12 +286,32 @@ Retain these records. Re-fetch them before every GitLab publication:
226
286
  | `reviewed_scope` | Canonical digest of title, description, draft state, source/target names, and linked-work identities; exclude discussions and CI evidence. |
227
287
  | `changed_path_manifest` | NUL-safe count and digest of the union of both immutable diff lenses. |
228
288
  | `reviewed_linked_requirements` | Canonical ID, URL, and content digest of title, description, acceptance criteria, and every consumed comment or plan artifact. |
289
+ | `merge_readiness` | `approval_state` from GitLab or `UNAVAILABLE`, the exact MR `head_sha` for that state, and an `authority` note; excluded from verdict inputs and scope digests. |
290
+
291
+ The configured GitLab capability must bind `merge_readiness.head_sha` to
292
+ `reviewed_identity.head_sha`. Treat the record as repository-policy evidence. Exclude the complete
293
+ record from review verdict inputs, `reviewed_scope`, `reviewed_linked_requirements`,
294
+ `changed_path_manifest`, and all scope digests. The `authority` note must state this boundary. An
295
+ approval-state change on the same head does not require a new technical review. If the approval state
296
+ is missing or malformed, use `UNAVAILABLE`. If its head is missing or does not match the reviewed
297
+ head, report a merge-readiness coverage gap. Do not make a merge-ready claim from that record. A
298
+ source-head change invalidates the complete review binding.
299
+
300
+ Before a configured GitLab capability supplies default-profile evidence, test these cases:
301
+
302
+ - Change only the approval state for one `head_sha`. Verify that only `merge_readiness` changes. The
303
+ verdict inputs and all scope digests must stay identical.
304
+ - Supply approval evidence for a different or missing `head_sha`. Verify that the capability reports
305
+ a merge-readiness coverage gap and does not make a merge-ready claim.
306
+ - Omit the approval state for the reviewed `head_sha`. Verify that the capability records
307
+ `approval_state: UNAVAILABLE` and does not change the technical review verdict.
229
308
 
230
309
  Each pipeline or check that supplies default-profile evidence must identify the reviewed `head_sha`.
231
310
  If it does not identify that value, report a coverage gap. Treat a partial response as a coverage
232
311
  failure. Use source from the immutable `head_sha` tree or a bound snapshot. Run local validation only
233
- through the host execution boundary. A discussion that this review creates does not change its own
234
- scope digest. Retain prior discussions as review context. Do not treat them as mutable scope fields.
312
+ through the host execution boundary. Approval gaps are merge-readiness facts, not review coverage
313
+ failures. A discussion that this review creates does not change its own scope digest. Retain prior
314
+ discussions as review context. Do not treat them as mutable scope fields.
235
315
 
236
316
  ### Inspect source and history
237
317
 
@@ -3,7 +3,9 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import importlib.util
6
7
  import json
8
+ import re
7
9
  import subprocess
8
10
  import sys
9
11
  import threading
@@ -12,10 +14,7 @@ from collections.abc import Sequence
12
14
  from dataclasses import dataclass, field
13
15
  from hashlib import sha256
14
16
  from pathlib import Path
15
- from typing import IO, Any
16
-
17
- if __package__ in {None, ""}:
18
- sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
17
+ from typing import IO, TYPE_CHECKING, Any
19
18
 
20
19
  from materialize_snapshot import (
21
20
  MaterializedSnapshot,
@@ -32,21 +31,40 @@ from pr_identity import (
32
31
  validate_pr_identifier,
33
32
  )
34
33
 
35
- from skills._cli import (
36
- argument_parser,
37
- git_read_arguments,
38
- git_read_environment,
39
- require_complete_git_history,
40
- require_unambiguous_git_merge_base,
41
- run_command,
42
- )
34
+ if TYPE_CHECKING or __package__ not in {None, ""}:
35
+ from skills._cli import (
36
+ argument_parser,
37
+ git_read_arguments,
38
+ git_read_environment,
39
+ require_complete_git_history,
40
+ require_unambiguous_git_merge_base,
41
+ run_command,
42
+ )
43
+ else:
44
+ _cli_path = Path(__file__).resolve().parents[2] / "_cli.py"
45
+ _cli_spec = importlib.util.spec_from_file_location(
46
+ "athena_installed_cli", _cli_path
47
+ )
48
+ if _cli_spec is None or _cli_spec.loader is None:
49
+ raise RuntimeError(
50
+ f"The installed Athena CLI helper is unavailable: '{_cli_path}'."
51
+ )
52
+ _cli = importlib.util.module_from_spec(_cli_spec)
53
+ _cli_spec.loader.exec_module(_cli)
54
+ argument_parser = _cli.argument_parser
55
+ git_read_arguments = _cli.git_read_arguments
56
+ git_read_environment = _cli.git_read_environment
57
+ require_complete_git_history = _cli.require_complete_git_history
58
+ require_unambiguous_git_merge_base = _cli.require_unambiguous_git_merge_base
59
+ run_command = _cli.run_command
43
60
 
44
61
  # Keep this query below GitHub's GraphQL complexity budget. Strict callers bind
45
62
  # changed paths to local immutable Git objects. Earlier callers use the REST
46
63
  # file-list fallback only for backward compatibility.
47
64
  FIELDS = (
48
65
  "number,title,body,state,isDraft,author,baseRefName,headRefName,"
49
- "baseRefOid,headRefOid,reviews,statusCheckRollup,closingIssuesReferences,url"
66
+ "baseRefOid,headRefOid,reviewDecision,reviews,statusCheckRollup,"
67
+ "closingIssuesReferences,url"
50
68
  )
51
69
  ISSUE_FIELDS = "id,number,url,title,body,state"
52
70
  READ_CHUNK_SIZE = 64 * 1024
@@ -68,7 +86,6 @@ MAX_CHANGED_PATH_MANIFEST_BYTES = 2 * 1024 * 1024
68
86
  MAX_CHANGED_PATHS = 10_000
69
87
  MAX_CHANGED_PATH_STDERR_BYTES = 16 * 1024
70
88
  CHANGED_PATH_REQUEST_TIMEOUT_SECONDS = 30.0
71
- MAX_CHECK_RUN_PAGE_BYTES = 256 * 1024
72
89
  MAX_CHECK_RUN_BYTES = 2 * 1024 * 1024
73
90
  MAX_CHECK_RUN_PAGES = 100
74
91
  MAX_CHECK_RUNS = 10_000
@@ -926,7 +943,9 @@ def linked_issue_metadata(
926
943
 
927
944
 
928
945
  def linked_requirements(
929
- metadata: dict[str, Any], budget: LinkedRequirementBudget | None = None
946
+ metadata: dict[str, Any],
947
+ budget: LinkedRequirementBudget | None = None,
948
+ requirement_issues: Sequence[str] = (),
930
949
  ) -> LinkedRequirements:
931
950
  """Bind every linked issue's requirement content and complete comment history."""
932
951
  references = metadata.get("closingIssuesReferences")
@@ -935,14 +954,36 @@ def linked_requirements(
935
954
  identities = sorted(linked_issue_reference(issue) for issue in references)
936
955
  if len(identities) != len(set(identities)):
937
956
  raise RuntimeError("GitHub returned duplicate linked issue references.")
957
+ selected: dict[str, tuple[str | None, str, int, str]] = {
958
+ identity[3]: identity for identity in identities
959
+ }
960
+ if len(selected) != len(identities):
961
+ raise RuntimeError(
962
+ "GitHub returned different identities for the same linked issue URL."
963
+ )
964
+ for url in requirement_issues:
965
+ match = re.fullmatch(
966
+ r"https://github\.com/([^/]+)/([^/]+)/issues/([1-9][0-9]{0,19})", url
967
+ )
968
+ if match is None:
969
+ raise RuntimeError("Use a canonical GitHub issue URL for each requirement.")
970
+ repository = require_github_repository(
971
+ f"{match[1]}/{match[2]}", "requirement issue repository"
972
+ )
973
+ selected.setdefault(url, (None, repository, int(match[3]), url))
938
974
  collection_budget = budget if budget is not None else LinkedRequirementBudget()
939
975
  items: list[LinkedRequirement] = []
940
- for expected_id, repository, number, expected_url in identities:
976
+ for expected_id, repository, number, expected_url in sorted(
977
+ selected.values(), key=lambda identity: identity[3]
978
+ ):
941
979
  issue_data = linked_issue_metadata(repository, number, collection_budget)
942
980
  issue_id = issue_data.get("id")
943
981
  body = issue_data.get("body")
944
982
  if (
945
- issue_id != expected_id
983
+ not isinstance(issue_id, str)
984
+ or not issue_id
985
+ or (expected_id is not None and issue_id != expected_id)
986
+ or isinstance(issue_data.get("number"), bool)
946
987
  or issue_data.get("number") != number
947
988
  or issue_data.get("url") != expected_url
948
989
  or not isinstance(issue_data.get("title"), str)
@@ -964,7 +1005,7 @@ def linked_requirements(
964
1005
  }
965
1006
  items.append(
966
1007
  LinkedRequirement(
967
- id=expected_id,
1008
+ id=issue_id,
968
1009
  repository=repository,
969
1010
  number=number,
970
1011
  url=expected_url,
@@ -973,6 +1014,7 @@ def linked_requirements(
973
1014
  ).hexdigest(),
974
1015
  )
975
1016
  )
1017
+ items.sort(key=lambda item: (item.id, item.repository, item.number, item.url))
976
1018
  document = canonical_json(
977
1019
  [item.as_json() for item in items], "linked issue requirements"
978
1020
  )
@@ -1218,7 +1260,7 @@ def head_bound_check_runs(repository: str, head_oid: str) -> list[dict[str, Any]
1218
1260
  f"per_page=100&page={page_number}"
1219
1261
  ),
1220
1262
  ),
1221
- maximum_bytes=min(MAX_CHECK_RUN_PAGE_BYTES, remaining_bytes),
1263
+ maximum_bytes=remaining_bytes,
1222
1264
  limit_error="The GitHub check-run response exceeds the safe byte limit.",
1223
1265
  timeout_seconds=CHECK_RUN_REQUEST_TIMEOUT_SECONDS,
1224
1266
  stderr_maximum_bytes=MAX_CHECK_RUN_STDERR_BYTES,
@@ -1325,6 +1367,25 @@ def pr_metadata(
1325
1367
  return metadata
1326
1368
 
1327
1369
 
1370
+ def merge_readiness(metadata: dict[str, Any]) -> dict[str, str]:
1371
+ """Return forge approval state as evidence separate from the review verdict."""
1372
+ decision = metadata.get("reviewDecision")
1373
+ if not isinstance(decision, str) or not decision:
1374
+ decision = "UNAVAILABLE"
1375
+ approval_gate = {
1376
+ "APPROVED": "satisfied",
1377
+ "CHANGES_REQUESTED": "blocked",
1378
+ "REVIEW_REQUIRED": "blocked",
1379
+ }.get(decision, "unknown")
1380
+ return {
1381
+ "auto_merge_approval_gate": approval_gate,
1382
+ "authority": (
1383
+ "Repository-policy evidence excluded from the review verdict and scope digests."
1384
+ ),
1385
+ "review_decision": decision,
1386
+ }
1387
+
1388
+
1328
1389
  def main(argv: Sequence[str] | None = None) -> int:
1329
1390
  parser = argument_parser(description=__doc__)
1330
1391
  parser.add_argument(
@@ -1359,6 +1420,13 @@ def main(argv: Sequence[str] | None = None) -> int:
1359
1420
  help="Use the canonical pull-request URL from 'resolve_pr.py'.",
1360
1421
  )
1361
1422
  parser.add_argument("pull_request", metavar="PR_NUMBER_OR_URL")
1423
+ parser.add_argument(
1424
+ "--requirement-issue",
1425
+ action="append",
1426
+ default=[],
1427
+ metavar="ISSUE_URL",
1428
+ help="Bind another GitHub issue. Use this option for each non-closing requirement.",
1429
+ )
1362
1430
  arguments = parser.parse_args(argv)
1363
1431
  pull_request = arguments.pull_request
1364
1432
  expected = expected_identity(
@@ -1373,6 +1441,8 @@ def main(argv: Sequence[str] | None = None) -> int:
1373
1441
  arguments.expected_pr_url,
1374
1442
  )
1375
1443
  require_immutable_identity = expected is not None
1444
+ if arguments.requirement_issue and not require_immutable_identity:
1445
+ parser.error("All strict identity arguments are necessary for this option.")
1376
1446
  try:
1377
1447
  validate_pr_identifier(pull_request)
1378
1448
  requested = pull_request_number(pull_request)
@@ -1430,7 +1500,9 @@ def main(argv: Sequence[str] | None = None) -> int:
1430
1500
  LinkedRequirementBudget() if expected is not None else None
1431
1501
  )
1432
1502
  reviewed_linked_requirements = (
1433
- linked_requirements(metadata, linked_requirement_budget)
1503
+ linked_requirements(
1504
+ metadata, linked_requirement_budget, arguments.requirement_issue
1505
+ )
1434
1506
  if expected is not None
1435
1507
  else None
1436
1508
  )
@@ -1504,7 +1576,9 @@ def main(argv: Sequence[str] | None = None) -> int:
1504
1576
  if final_scope != reviewed_scope:
1505
1577
  raise RuntimeError("The review scope changed during evidence collection.")
1506
1578
  final_linked_requirements = (
1507
- linked_requirements(final_metadata, linked_requirement_budget)
1579
+ linked_requirements(
1580
+ final_metadata, linked_requirement_budget, arguments.requirement_issue
1581
+ )
1508
1582
  if expected is not None
1509
1583
  else None
1510
1584
  )
@@ -1525,7 +1599,12 @@ def main(argv: Sequence[str] | None = None) -> int:
1525
1599
  "changed_files": changed_files,
1526
1600
  "changed_paths": changed_files,
1527
1601
  "checks": checks,
1528
- "pull_request": final_metadata,
1602
+ "pull_request": {
1603
+ key: value
1604
+ for key, value in final_metadata.items()
1605
+ if key != "reviewDecision"
1606
+ },
1607
+ "merge_readiness": merge_readiness(final_metadata),
1529
1608
  }
1530
1609
  if identity is not None:
1531
1610
  evidence["reviewed_identity"] = identity.as_json()