claude-dev-env 8.29.1 → 8.30.1

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.
@@ -36,6 +36,21 @@ Each level file reports through the ReportFindings tool. When the host has no Re
36
36
 
37
37
  ## The process
38
38
 
39
+ ### Native GitHub evidence pilot
40
+
41
+ Keep one coordinator responsible for native review requests. Record the request comment and full candidate head. Read raw GitHub review records to retain commit_id; normalized connector output can omit it. Completed reviews can contain findings. The quota_notice_ids field records historical usage-limit comments. Driver replies and resolved threads provide no clean completion proof.
42
+
43
+ Use process-local GITHUB_TOKEN or GH_TOKEN authentication without printing credentials. A code-review usage-limit response stops further triggers until capacity is verified. Preserve the hold; review permission changes and credit purchases require separate authorization. Capture API bytes as UTF-8 before decoding them so Windows shell output encoding preserves review headings.
44
+
45
+ Inspect a candidate from the shared source checkout:
46
+
47
+ ```powershell
48
+ python packages/claude-dev-env/scripts/codex_review_observer.py <owner/repository> <pull-request> <full-head>
49
+ python -m pytest packages/claude-dev-env/scripts/tests/test_codex_review_observer.py packages/claude-dev-env/scripts/tests/test_codex_review_guidance.py -q --override-ini="addopts=" -p no:cacheprovider
50
+ ```
51
+
52
+ The current observer holds admission for every supported diagnostic. Require captured successful native completion with full-head and attempt coverage before adding a clean verdict. After a repair push, review the new head and preserve all existing checks. Keep current repository contracts authoritative over copied hosted policy.
53
+
39
54
  1. Read the level and the optional `--fix` flag. Load the level file.
40
55
  2. Run it end to end, ending in its ReportFindings call.
41
56
  3. With `--fix`, load `reference/fix.md` and run it on those findings.
@@ -39,6 +39,7 @@ prove the task by the artifact it changes.
39
39
  - `LastTaskResult: 0` from a run that took the idle branch says nothing about the branch that acts.
40
40
  - A `#Requires -RunAsAdministrator` line pasted at a prompt checks nothing. It applies only to a script file.
41
41
  - An elevated window opens in `C:\WINDOWS\system32`. A prompt in the user's home folder is usually unelevated.
42
+ - An Interactive task that starts `pwsh.exe -WindowStyle Hidden` still flashes a console window on the signed-in desktop. Windows draws the console before PowerShell reads the flag. `conhost.exe --headless` in front of the executable starts it with no window.
42
43
 
43
44
  ## When this applies
44
45
 
@@ -49,7 +50,7 @@ module. Register the task in an elevated shell when the task must run while the
49
50
 
50
51
  1. Resolve every executable in the action with `Get-Command <name>` and read `.Source`.
51
52
  2. Read `files[]` in `~/.claude/.claude-dev-env-manifest.json` and pick a directory that list omits.
52
- 3. Build the action with the absolute executable path and `-WindowStyle Hidden` inside the argument string.
53
+ 3. Build the action with `conhost.exe --headless` as the executable, followed by the absolute path of the program it starts.
53
54
  4. Build a `-Once` trigger with a repetition interval and no duration, then clear the duration on the trigger object.
54
55
  5. Build the settings set.
55
56
  6. Register with an S4U principal, catch the registration failure, and register with an Interactive principal.
@@ -83,18 +84,21 @@ try {
83
84
  ```
84
85
 
85
86
  S4U runs the task with no window whether or not the user is signed in, and its registration needs an
86
- elevated shell. The Interactive principal with `-WindowStyle Hidden` in the action runs windowless
87
- while the user is signed in. Report `$PrincipalKind` so the operator knows which one the task holds.
87
+ elevated shell. The Interactive principal runs the task on the signed-in desktop, so its action goes
88
+ through `conhost.exe --headless` to stay windowless. Report `$PrincipalKind` so the operator knows
89
+ which one the task holds.
88
90
 
89
91
  ## Absolute paths in the action
90
92
 
91
93
  ```powershell
94
+ $ConhostPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\conhost.exe'
92
95
  $PwshPath = (Get-Command pwsh).Source
93
- $Action = New-ScheduledTaskAction -Execute $PwshPath -Argument "-NoProfile -WindowStyle Hidden -File ""$ScriptPath"""
96
+ $Action = New-ScheduledTaskAction -Execute $ConhostPath -Argument "--headless ""$PwshPath"" -NoProfile -File ""$ScriptPath"""
94
97
  ```
95
98
 
96
99
  Resolve each executable at registration time and store the absolute path in the action. Give the
97
- script an absolute path too.
100
+ script an absolute path too. The same `--headless` front works for any console program, such as
101
+ `python.exe` or `cmd.exe`.
98
102
 
99
103
  ## Settings
100
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.29.1",
3
+ "version": "8.30.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -99,7 +99,8 @@ if (-not $?) {
99
99
  $Target = (Resolve-Path $Target).Path
100
100
  $Target = [System.IO.Path]::TrimEndingDirectorySeparator($Target)
101
101
 
102
- $Action = New-ScheduledTaskAction -Execute $PythonPath -Argument """$ScriptPath"" --once --age $AgeSeconds ""$Target"""
102
+ $ConhostPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\conhost.exe'
103
+ $Action = New-ScheduledTaskAction -Execute $ConhostPath -Argument "--headless ""$PythonPath"" ""$ScriptPath"" --once --age $AgeSeconds ""$Target"""
103
104
  $Trigger = New-ScheduledTaskTrigger -Once -At $StartAt -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) -RepetitionDuration (New-TimeSpan -Days 31)
104
105
  $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
105
106
 
@@ -0,0 +1,285 @@
1
+ """Observe candidate reviews without granting admission to a merge queue.
2
+
3
+ Native completion representation and reviewer identity remain unverified.
4
+ Every observation holds admission, including plausible success records.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import re
12
+ import sys
13
+ from collections.abc import Callable, Mapping, Sequence
14
+ from dataclasses import dataclass
15
+ from datetime import datetime
16
+ from enum import Enum
17
+ from urllib.error import URLError
18
+
19
+ from dev_env_scripts_constants.codex_review_observer_constants import (
20
+ CODEX_FINDINGS_HEADING,
21
+ CODEX_FINDINGS_PREFIX,
22
+ CODEX_QUOTA_PREFIX,
23
+ CODEX_REVIEWER_LOGIN,
24
+ COMMENTS_URL,
25
+ COMMIT_PATTERN,
26
+ HOLD_EXIT_CODE,
27
+ MAX_PAGES,
28
+ PAGE_SIZE,
29
+ PULL_URL,
30
+ REPOSITORY_PATTERN,
31
+ REVIEWS_SUFFIX,
32
+ UNAVAILABLE_EXIT_CODE,
33
+ )
34
+ from pr_verification.github_parsing import GitHubError
35
+ from review_closure_github import github_token, request_json
36
+
37
+ GitHubRead = Callable[[str], object]
38
+
39
+
40
+ class HoldReason(str, Enum):
41
+ MISSING_REVIEW = "missing_review"
42
+ STALE_REVIEW = "stale_review"
43
+ HEAD_CHANGED = "head_changed"
44
+ NATIVE_FORMAT_UNVERIFIED = "native_format_unverified"
45
+ READ_UNAVAILABLE = "read_unavailable"
46
+ READ_TIMEOUT = "read_timeout"
47
+ INCOMPLETE_READ = "incomplete_read"
48
+ CONFLICTING_EVIDENCE = "conflicting_evidence"
49
+ REVIEW_FINDINGS = "review_findings"
50
+ QUOTA_NOTICE = "quota_notice"
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class Candidate:
55
+ repository: str
56
+ pull_request: int
57
+ head_sha: str
58
+
59
+ def __post_init__(self) -> None:
60
+ if (
61
+ re.fullmatch(REPOSITORY_PATTERN, self.repository) is None
62
+ or type(self.pull_request) is not int
63
+ or self.pull_request <= 0
64
+ or re.fullmatch(COMMIT_PATTERN, self.head_sha) is None
65
+ ):
66
+ raise ValueError("A repository, pull request, and full head are required.")
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ReviewObservation:
71
+ candidate: Candidate
72
+ reason: HoldReason
73
+ evidence_ids: tuple[int, ...] = ()
74
+ quota_notice_ids: tuple[int, ...] = ()
75
+
76
+
77
+ def _matches_candidate(document: object, candidate: Candidate) -> bool:
78
+ if not isinstance(document, Mapping):
79
+ return False
80
+ head = document.get("head")
81
+ return (
82
+ document.get("number") == candidate.pull_request
83
+ and isinstance(head, Mapping)
84
+ and head.get("sha") == candidate.head_sha
85
+ )
86
+
87
+
88
+ def _is_submitted_codex_findings(all_review: Mapping[str, object]) -> bool:
89
+ author = all_review.get("user")
90
+ body = all_review.get("body")
91
+ submitted_at = all_review.get("submitted_at")
92
+ if not (
93
+ isinstance(author, Mapping)
94
+ and author.get("login") == CODEX_REVIEWER_LOGIN
95
+ and all_review.get("state") == "COMMENTED"
96
+ and isinstance(body, str)
97
+ and body.lstrip().startswith((CODEX_FINDINGS_PREFIX, CODEX_FINDINGS_HEADING))
98
+ and isinstance(submitted_at, str)
99
+ ):
100
+ return False
101
+ try:
102
+ return datetime.fromisoformat(submitted_at).tzinfo is not None
103
+ except ValueError:
104
+ return False
105
+
106
+
107
+ class _EvidenceReadRunFatal(Exception):
108
+ def __init__(self, reason: HoldReason) -> None:
109
+ super().__init__(reason.value)
110
+ self.reason = reason
111
+
112
+
113
+ def _merge_page(all_records: dict[int, Mapping[str, object]], all_page: object) -> int:
114
+ if not isinstance(all_page, list):
115
+ raise _EvidenceReadRunFatal(HoldReason.INCOMPLETE_READ)
116
+ for each_record in all_page:
117
+ if not isinstance(each_record, Mapping):
118
+ raise _EvidenceReadRunFatal(HoldReason.INCOMPLETE_READ)
119
+ record_id = each_record.get("id")
120
+ if type(record_id) is not int or record_id <= 0:
121
+ raise _EvidenceReadRunFatal(HoldReason.INCOMPLETE_READ)
122
+ if record_id in all_records and all_records[record_id] != each_record:
123
+ raise _EvidenceReadRunFatal(HoldReason.CONFLICTING_EVIDENCE)
124
+ all_records[record_id] = each_record
125
+ return len(all_page)
126
+
127
+
128
+ def _read_collection(url: str, read: GitHubRead) -> dict[int, Mapping[str, object]]:
129
+ all_records: dict[int, Mapping[str, object]] = {}
130
+ for each_page in range(1, MAX_PAGES + 1):
131
+ count = _merge_page(
132
+ all_records, read(f"{url}?per_page={PAGE_SIZE}&page={each_page}")
133
+ )
134
+ if count < PAGE_SIZE:
135
+ return all_records
136
+ raise _EvidenceReadRunFatal(HoldReason.INCOMPLETE_READ)
137
+
138
+
139
+ def _quota_notice_ids(
140
+ all_comments: Mapping[int, Mapping[str, object]],
141
+ ) -> tuple[int, ...]:
142
+ all_ids: list[int] = []
143
+ for each_id, each_comment in all_comments.items():
144
+ author = each_comment.get("user")
145
+ body = each_comment.get("body")
146
+ if (
147
+ isinstance(author, Mapping)
148
+ and author.get("login") == CODEX_REVIEWER_LOGIN
149
+ and isinstance(body, str)
150
+ and body.startswith(CODEX_QUOTA_PREFIX)
151
+ ):
152
+ all_ids.append(each_id)
153
+ return tuple(sorted(all_ids))
154
+
155
+
156
+ def _review_observation(
157
+ candidate: Candidate, all_reviews: Mapping[int, Mapping[str, object]]
158
+ ) -> ReviewObservation:
159
+ all_current = {
160
+ each_id: each_review
161
+ for each_id, each_review in all_reviews.items()
162
+ if each_review.get("commit_id") == candidate.head_sha
163
+ }
164
+ reason = HoldReason.MISSING_REVIEW
165
+ if all_reviews:
166
+ reason = HoldReason.STALE_REVIEW
167
+ if all_current:
168
+ reason = HoldReason.NATIVE_FORMAT_UNVERIFIED
169
+ if any(
170
+ _is_submitted_codex_findings(each_review)
171
+ for each_review in all_current.values()
172
+ ):
173
+ reason = HoldReason.REVIEW_FINDINGS
174
+ return ReviewObservation(candidate, reason, tuple(sorted(all_current)))
175
+
176
+
177
+ def _stable_observation(
178
+ candidate: Candidate, read: GitHubRead, pull_url: str
179
+ ) -> ReviewObservation:
180
+ all_reviews = _read_collection(pull_url + REVIEWS_SUFFIX, read)
181
+ observation = _review_observation(candidate, all_reviews)
182
+ comments_url = COMMENTS_URL.format(
183
+ repository=candidate.repository, number=candidate.pull_request
184
+ )
185
+ all_quota_ids = _quota_notice_ids(_read_collection(comments_url, read))
186
+ reason = observation.reason
187
+ if all_quota_ids and reason in (
188
+ HoldReason.MISSING_REVIEW,
189
+ HoldReason.STALE_REVIEW,
190
+ ):
191
+ reason = HoldReason.QUOTA_NOTICE
192
+ return ReviewObservation(candidate, reason, observation.evidence_ids, all_quota_ids)
193
+
194
+
195
+ def _transport_failure_reason(failure: BaseException) -> HoldReason:
196
+ cause = failure.__cause__
197
+ if isinstance(failure, TimeoutError) or isinstance(cause, TimeoutError):
198
+ return HoldReason.READ_TIMEOUT
199
+ if isinstance(cause, URLError) and isinstance(cause.reason, TimeoutError):
200
+ return HoldReason.READ_TIMEOUT
201
+ return HoldReason.READ_UNAVAILABLE
202
+
203
+
204
+ def observe_codex_review(candidate: Candidate, read: GitHubRead) -> ReviewObservation:
205
+ """Read evidence for a stable full head and return its admission hold.
206
+
207
+ Args:
208
+ candidate: The repository, pull request, and requested full head.
209
+ read: Authenticated read-only JSON transport.
210
+
211
+ Returns:
212
+ The observed hold and evidence identifiers.
213
+ """
214
+ pull_url = PULL_URL.format(
215
+ repository=candidate.repository, number=candidate.pull_request
216
+ )
217
+ observation = ReviewObservation(candidate, HoldReason.MISSING_REVIEW)
218
+ try:
219
+ if not _matches_candidate(read(pull_url), candidate):
220
+ return ReviewObservation(candidate, HoldReason.HEAD_CHANGED)
221
+ try:
222
+ observation = _stable_observation(candidate, read, pull_url)
223
+ except _EvidenceReadRunFatal as failure:
224
+ observation = ReviewObservation(candidate, failure.reason)
225
+ if not _matches_candidate(read(pull_url), candidate):
226
+ return ReviewObservation(candidate, HoldReason.HEAD_CHANGED)
227
+ except TimeoutError:
228
+ return ReviewObservation(candidate, HoldReason.READ_TIMEOUT)
229
+ except (GitHubError, OSError) as failure:
230
+ return ReviewObservation(candidate, _transport_failure_reason(failure))
231
+ return observation
232
+
233
+
234
+ def _observation_document(observation: ReviewObservation) -> dict[str, object]:
235
+ candidate = observation.candidate
236
+ return {
237
+ "repository": candidate.repository,
238
+ "pull_request": candidate.pull_request,
239
+ "head_sha": candidate.head_sha,
240
+ "admission": "hold",
241
+ "reason": observation.reason.value,
242
+ "evidence_ids": observation.evidence_ids,
243
+ "quota_notice_ids": observation.quota_notice_ids,
244
+ }
245
+
246
+
247
+ def _parse_candidate(all_arguments: Sequence[str]) -> Candidate:
248
+ parser = argparse.ArgumentParser(description=__doc__)
249
+ parser.add_argument("repository")
250
+ parser.add_argument("pull_request", type=int)
251
+ parser.add_argument("head_sha")
252
+ parsed = parser.parse_args(all_arguments)
253
+ return Candidate(parsed.repository, parsed.pull_request, parsed.head_sha)
254
+
255
+
256
+ def main(all_arguments: Sequence[str]) -> int:
257
+ """Print a read-only observation and return nonzero for every hold.
258
+
259
+ Args:
260
+ all_arguments: Command arguments without the executable name.
261
+
262
+ Returns:
263
+ One for a hold, or two when the read is unavailable.
264
+ """
265
+ try:
266
+ candidate = _parse_candidate(all_arguments)
267
+ token = github_token()
268
+ except (ValueError, GitHubError) as failure:
269
+ print(str(failure), file=sys.stderr)
270
+ return UNAVAILABLE_EXIT_CODE
271
+
272
+ def read(url: str) -> object:
273
+ return request_json("GET", url, token, None)
274
+
275
+ observation = observe_codex_review(candidate, read)
276
+ print(json.dumps(_observation_document(observation)))
277
+ return (
278
+ UNAVAILABLE_EXIT_CODE
279
+ if observation.reason in (HoldReason.READ_TIMEOUT, HoldReason.READ_UNAVAILABLE)
280
+ else HOLD_EXIT_CODE
281
+ )
282
+
283
+
284
+ if __name__ == "__main__":
285
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,17 @@
1
+ """Bounds for read-only Codex review observations."""
2
+
3
+ GITHUB_API_ROOT = "https://api.github.com"
4
+ PAGE_SIZE = 100
5
+ MAX_PAGES = 20
6
+ COMMIT_PATTERN = r"[0-9a-f]{40}"
7
+ REPOSITORY_PATTERN = r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+"
8
+ HOLD_EXIT_CODE = 1
9
+ UNAVAILABLE_EXIT_CODE = 2
10
+ CODEX_REVIEWER_LOGIN = "chatgpt-codex-connector[bot]"
11
+ CODEX_FINDINGS_PREFIX = "Here are some automated review suggestions"
12
+ CODEX_FINDINGS_HEADING = "### 💡 Codex Review\n\n" + CODEX_FINDINGS_PREFIX
13
+
14
+ PULL_URL = GITHUB_API_ROOT + "/repos/{repository}/pulls/{number}"
15
+ COMMENTS_URL = GITHUB_API_ROOT + "/repos/{repository}/issues/{number}/comments"
16
+ REVIEWS_SUFFIX = "/reviews"
17
+ CODEX_QUOTA_PREFIX = "You have reached your Codex usage limits for code reviews."
@@ -0,0 +1,14 @@
1
+ {
2
+ "repository": "jl-cmd/claude-dev-env",
3
+ "pull_request": 1576,
4
+ "review": {
5
+ "id": 5360553725,
6
+ "state": "COMMENTED",
7
+ "commit_id": "d66450a145554778c9938c310cae61cca5927884",
8
+ "submitted_at": "2026-09-30T01:50:30Z",
9
+ "body": "\n### \ud83d\udca1 Codex Review\n\nHere are some automated review suggestions for this pull request.\n\n**Reviewed commit:** `d66450a145`\n \n\n<details> <summary>\u2139\ufe0f About Codex in GitHub</summary>\n<br/>\n\n[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you\n- Open a pull request for review\n- Mark a draft as ready\n- Comment \"@codex review\".\n\nIf Codex has suggestions, it will comment; otherwise it will react with \ud83d\udc4d.\n\n\n\n\nCodex can also answer questions or update the PR. Try commenting \"@codex address that feedback\".\n \n</details>",
10
+ "user": {
11
+ "login": "chatgpt-codex-connector[bot]"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "body": "You have reached your Codex usage limits for code reviews. You can see your limits in the [Codex usage dashboard](https://chatgpt.com/codex/cloud/settings/usage).",
3
+ "created_at": "2026-09-30T00:12:01Z",
4
+ "id": 5901476928,
5
+ "user": {
6
+ "login": "chatgpt-codex-connector[bot]"
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "body": "You have reached your Codex usage limits for code reviews. You can see your limits in the [Codex usage dashboard](https://chatgpt.com/codex/cloud/settings/usage).\nTo continue using code reviews, add credits to your account and enable them for code reviews in your [settings](https://chatgpt.com/codex/cloud/settings/code-review).",
3
+ "created_at": "2026-09-30T01:50:30Z",
4
+ "id": 5902483341,
5
+ "user": {
6
+ "login": "chatgpt-codex-connector[bot]"
7
+ }
8
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "provenance": "Fields verified by the parent coordinator from the raw authenticated API. Body contains only the verified prefix. Repository identity is anonymized for the shared package; review fields are unchanged.",
3
+ "repository": "owner/repository",
4
+ "pull_request": 4743,
5
+ "review": {
6
+ "id": 5322799208,
7
+ "user": {
8
+ "login": "chatgpt-codex-connector[bot]"
9
+ },
10
+ "state": "COMMENTED",
11
+ "commit_id": "bed23717ab2ea352113c8f2058fbc959464bdf43",
12
+ "submitted_at": "2026-09-25T21:15:07Z",
13
+ "body": "Here are some automated review suggestions"
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ """Native guidance has a repository entry point and stays repository-independent."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def test_native_guidance_is_discoverable() -> None:
7
+ repository = Path(__file__).resolve().parents[4]
8
+ instructions = (repository / "AGENTS.md").read_text(encoding="utf-8")
9
+ assert "# Code Review Rules" in instructions
10
+ assert "docs/review/review.md" in instructions
11
+ guidance = (repository / "docs/review/review.md").read_text(encoding="utf-8")
12
+ assert "Current repository instructions override stale hosted copies." in guidance
13
+ assert "Completed review can contain findings." in guidance
14
+ assert "theme_db_id" not in guidance
15
+ assert "saved-grid" not in guidance
@@ -0,0 +1,357 @@
1
+ """Refusal behavior for native review evidence whose success format is unknown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parents[1]
12
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
13
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
14
+
15
+ import codex_review_observer as observer
16
+ import review_closure_github
17
+ from pr_verification.github_parsing import GitHubError
18
+
19
+ HEAD = "a" * 40
20
+ NEXT_HEAD = "b" * 40
21
+ CANDIDATE = observer.Candidate("owner/repository", 7, HEAD)
22
+
23
+
24
+ def observe(
25
+ all_pages: list[object], after_head: str = HEAD
26
+ ) -> observer.ReviewObservation:
27
+ all_reads: list[object] = [
28
+ {"number": 7, "head": {"sha": HEAD}},
29
+ *all_pages,
30
+ {"number": 7, "head": {"sha": after_head}},
31
+ ]
32
+
33
+ def read(url: str) -> object:
34
+ if "/issues/7/comments?" in url:
35
+ return []
36
+ assert url.startswith("https://api.github.com/repos/owner/repository/pulls/7")
37
+ return all_reads.pop(0)
38
+
39
+ return observer.observe_codex_review(CANDIDATE, read)
40
+
41
+
42
+ @pytest.mark.parametrize(
43
+ "body",
44
+ [
45
+ "No findings. Review completed successfully.",
46
+ "Fixed in the next push.",
47
+ "Review resolved.",
48
+ "Codex cloud task completed.",
49
+ "You've reached your Codex code review usage limits.",
50
+ ],
51
+ )
52
+ def test_body_text_never_authorizes_merge(body: str) -> None:
53
+ review = {"id": 1, "commit_id": HEAD, "state": "APPROVED", "body": body}
54
+ observed = observe([[review]])
55
+ assert observed.reason is observer.HoldReason.NATIVE_FORMAT_UNVERIFIED
56
+ assert observed.evidence_ids == (1,)
57
+
58
+
59
+ def test_missing_review_holds() -> None:
60
+ assert observe([[]]).reason is observer.HoldReason.MISSING_REVIEW
61
+
62
+
63
+ def test_stale_completion_holds() -> None:
64
+ review = {"id": 1, "commit_id": NEXT_HEAD, "state": "APPROVED"}
65
+ assert observe([[review]]).reason is observer.HoldReason.STALE_REVIEW
66
+
67
+
68
+ def test_head_movement_invalidates_observation() -> None:
69
+ review = {"id": 1, "commit_id": HEAD, "state": "APPROVED"}
70
+ assert observe([[review]], NEXT_HEAD).reason is observer.HoldReason.HEAD_CHANGED
71
+
72
+
73
+ @pytest.mark.parametrize(
74
+ "state", ["PENDING", "DISMISSED", "FAILED", "CANCELLED", "SKIPPED"]
75
+ )
76
+ def test_unsuccessful_state_holds(state: str) -> None:
77
+ review = {"id": 1, "commit_id": HEAD, "state": state}
78
+ assert observe([[review]]).reason is observer.HoldReason.NATIVE_FORMAT_UNVERIFIED
79
+
80
+
81
+ def test_identical_duplicates_are_idempotent() -> None:
82
+ review = {"id": 1, "commit_id": HEAD, "body": "No findings"}
83
+ assert observe([[review, review]]).evidence_ids == (1,)
84
+
85
+
86
+ def test_conflicting_duplicate_cannot_pass() -> None:
87
+ first = {"id": 1, "commit_id": HEAD, "body": "No findings"}
88
+ later = {**first, "body": "A finding remains"}
89
+ assert observe([[first, later]]).reason is observer.HoldReason.CONFLICTING_EVIDENCE
90
+
91
+
92
+ def test_delayed_success_cannot_override_newer_failure() -> None:
93
+ failed = {"id": 2, "commit_id": HEAD, "state": "FAILED"}
94
+ delayed = {"id": 1, "commit_id": HEAD, "state": "APPROVED"}
95
+ assert (
96
+ observe([[failed, delayed]]).reason
97
+ is observer.HoldReason.NATIVE_FORMAT_UNVERIFIED
98
+ )
99
+
100
+
101
+ @pytest.mark.parametrize(
102
+ "failure", [TimeoutError(), GitHubError("quota unavailable"), OSError()]
103
+ )
104
+ def test_transport_failure_holds(failure: Exception) -> None:
105
+ def read(url: str) -> object:
106
+ raise failure
107
+
108
+ observed = observer.observe_codex_review(CANDIDATE, read)
109
+ assert observed.reason in (
110
+ observer.HoldReason.READ_TIMEOUT,
111
+ observer.HoldReason.READ_UNAVAILABLE,
112
+ )
113
+
114
+
115
+ @pytest.mark.parametrize("page", [None, {}, [None], [{"id": "1"}]])
116
+ def test_malformed_review_collection_holds(page: object) -> None:
117
+ assert observe([page]).reason is observer.HoldReason.INCOMPLETE_READ
118
+
119
+
120
+ def test_incomplete_pagination_holds(monkeypatch: pytest.MonkeyPatch) -> None:
121
+ monkeypatch.setattr(observer, "MAX_PAGES", 1)
122
+ all_reviews = [{"id": each_id + 1, "commit_id": HEAD} for each_id in range(100)]
123
+ assert observe([all_reviews]).reason is observer.HoldReason.INCOMPLETE_READ
124
+
125
+
126
+ @pytest.mark.parametrize("head", ["abc123", "A" * 40, "a" * 39])
127
+ def test_candidate_requires_full_head(head: str) -> None:
128
+ with pytest.raises(ValueError):
129
+ observer.Candidate("owner/repository", 7, head)
130
+
131
+
132
+ def test_wrong_pull_request_is_a_hold() -> None:
133
+ def read(url: str) -> object:
134
+ return {"number": 8, "head": {"sha": HEAD}}
135
+
136
+ assert (
137
+ observer.observe_codex_review(CANDIDATE, read).reason
138
+ is observer.HoldReason.HEAD_CHANGED
139
+ )
140
+
141
+
142
+ def test_cli_returns_nonzero_with_plausible_success(
143
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
144
+ ) -> None:
145
+ monkeypatch.setattr(observer, "github_token", lambda: "test-token")
146
+ all_reads = iter(
147
+ [
148
+ {"number": 7, "head": {"sha": HEAD}},
149
+ [{"id": 1, "commit_id": HEAD, "state": "APPROVED"}],
150
+ [],
151
+ {"number": 7, "head": {"sha": HEAD}},
152
+ ]
153
+ )
154
+ monkeypatch.setattr(observer, "request_json", lambda *arguments: next(all_reads))
155
+ assert observer.main(["owner/repository", "7", HEAD]) == 1
156
+ assert '"admission": "hold"' in capsys.readouterr().out
157
+
158
+
159
+ def test_parent_verified_submitted_findings_hold() -> None:
160
+ fixture_path = Path(__file__).parent / "fixtures/codex-submitted-findings.json"
161
+ captured = json.loads(fixture_path.read_text(encoding="utf-8"))
162
+ review = captured["review"]
163
+ candidate = observer.Candidate(
164
+ captured["repository"], captured["pull_request"], review["commit_id"]
165
+ )
166
+ all_reads = iter(
167
+ [
168
+ {"number": candidate.pull_request, "head": {"sha": candidate.head_sha}},
169
+ [review],
170
+ [],
171
+ {"number": candidate.pull_request, "head": {"sha": candidate.head_sha}},
172
+ ]
173
+ )
174
+ observed = observer.observe_codex_review(candidate, lambda url: next(all_reads))
175
+ assert observed.reason is observer.HoldReason.REVIEW_FINDINGS
176
+ assert observed.evidence_ids == (5322799208,)
177
+
178
+
179
+ def test_native_pilot_heading_preserves_findings_hold() -> None:
180
+ captured = json.loads(
181
+ (Path(__file__).parent / "fixtures/codex-pilot-findings.json").read_text(
182
+ encoding="utf-8-sig"
183
+ )
184
+ )
185
+ review = captured["review"]
186
+ candidate = observer.Candidate(
187
+ captured["repository"], captured["pull_request"], review["commit_id"]
188
+ )
189
+ all_reads = iter(
190
+ [
191
+ {"number": candidate.pull_request, "head": {"sha": candidate.head_sha}},
192
+ [review],
193
+ [],
194
+ {"number": candidate.pull_request, "head": {"sha": candidate.head_sha}},
195
+ ]
196
+ )
197
+ observed = observer.observe_codex_review(candidate, lambda url: next(all_reads))
198
+ assert observed.reason is observer.HoldReason.REVIEW_FINDINGS
199
+ assert observed.evidence_ids == (5360553725,)
200
+
201
+
202
+ def test_cli_production_transport_preserves_timeout(
203
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
204
+ ) -> None:
205
+ def timed_out(*args: object, **kwargs: object) -> object:
206
+ raise TimeoutError("socket read timed out")
207
+
208
+ monkeypatch.setattr(observer, "github_token", lambda: "test-token")
209
+ monkeypatch.setattr(review_closure_github.urllib.request, "urlopen", timed_out)
210
+ assert observer.main(["owner/repository", "7", HEAD]) == 2
211
+ assert json.loads(capsys.readouterr().out)["reason"] == "read_timeout"
212
+
213
+
214
+ def test_findings_text_from_wrong_actor_stays_unsupported() -> None:
215
+ review = {
216
+ "id": 1,
217
+ "commit_id": HEAD,
218
+ "state": "COMMENTED",
219
+ "submitted_at": "2026-09-25T21:15:07Z",
220
+ "user": {"login": "unrelated-reviewer"},
221
+ "body": observer.CODEX_FINDINGS_PREFIX,
222
+ }
223
+ assert observe([[review]]).reason is observer.HoldReason.NATIVE_FORMAT_UNVERIFIED
224
+
225
+
226
+ @pytest.mark.parametrize("actor", [observer.CODEX_REVIEWER_LOGIN, "other-user"])
227
+ @pytest.mark.parametrize(
228
+ "fixture_name", ["codex-quota-comment.json", "codex-quota-credits-comment.json"]
229
+ )
230
+ def test_authentic_quota_comment_is_diagnostic_only(
231
+ actor: str, fixture_name: str
232
+ ) -> None:
233
+ comment = json.loads(
234
+ (Path(__file__).parent / "fixtures" / fixture_name).read_text(
235
+ encoding="utf-8-sig"
236
+ )
237
+ )
238
+ comment["user"]["login"] = actor
239
+
240
+ def read(url: str) -> object:
241
+ if "/reviews?" in url:
242
+ return []
243
+ if "/comments?" in url:
244
+ return [comment]
245
+ return {"number": 7, "head": {"sha": HEAD}}
246
+
247
+ observed = observer.observe_codex_review(CANDIDATE, read)
248
+ assert observed.reason is (
249
+ observer.HoldReason.QUOTA_NOTICE
250
+ if actor == observer.CODEX_REVIEWER_LOGIN
251
+ else observer.HoldReason.MISSING_REVIEW
252
+ )
253
+ assert observed.evidence_ids == ()
254
+ assert observed.quota_notice_ids == (
255
+ (comment["id"],) if actor == observer.CODEX_REVIEWER_LOGIN else ()
256
+ )
257
+
258
+
259
+ @pytest.mark.parametrize(
260
+ "response,reason",
261
+ [
262
+ ({}, observer.HoldReason.INCOMPLETE_READ),
263
+ ([None], observer.HoldReason.INCOMPLETE_READ),
264
+ (TimeoutError(), observer.HoldReason.READ_TIMEOUT),
265
+ (GitHubError("comments unavailable"), observer.HoldReason.READ_UNAVAILABLE),
266
+ (OSError(), observer.HoldReason.READ_UNAVAILABLE),
267
+ ],
268
+ )
269
+ @pytest.mark.parametrize("has_current_review", [False, True])
270
+ def test_quota_comment_read_failure_holds(
271
+ response: object, reason: observer.HoldReason, has_current_review: bool
272
+ ) -> None:
273
+ def read(url: str) -> object:
274
+ if "/reviews?" in url:
275
+ return [{"id": 1, "commit_id": HEAD}] if has_current_review else []
276
+ if "/comments?" in url:
277
+ if isinstance(response, Exception):
278
+ raise response
279
+ return response
280
+ return {"number": 7, "head": {"sha": HEAD}}
281
+
282
+ observed = observer.observe_codex_review(CANDIDATE, read)
283
+ assert observed.reason is reason
284
+ assert observed.evidence_ids == ()
285
+ assert observed.quota_notice_ids == ()
286
+
287
+
288
+ @pytest.mark.parametrize("has_findings", [False, True])
289
+ @pytest.mark.parametrize("head_changes", [False, True])
290
+ def test_current_review_preserves_historical_quota_notices(
291
+ has_findings: bool, head_changes: bool
292
+ ) -> None:
293
+ captured = json.loads(
294
+ (Path(__file__).parent / "fixtures/codex-pilot-findings.json").read_text(
295
+ encoding="utf-8-sig"
296
+ )
297
+ )
298
+ review = captured["review"]
299
+ if not has_findings:
300
+ review = {
301
+ "id": 42,
302
+ "commit_id": review["commit_id"],
303
+ "state": "APPROVED",
304
+ "user": {"login": "human-reviewer"},
305
+ }
306
+ candidate = observer.Candidate(
307
+ captured["repository"], captured["pull_request"], review["commit_id"]
308
+ )
309
+ comment = json.loads(
310
+ (Path(__file__).parent / "fixtures/codex-quota-credits-comment.json").read_text(
311
+ encoding="utf-8-sig"
312
+ )
313
+ )
314
+ all_heads = iter(
315
+ [candidate.head_sha, NEXT_HEAD if head_changes else candidate.head_sha]
316
+ )
317
+
318
+ def read(url: str) -> object:
319
+ if "/reviews?" in url:
320
+ return [review]
321
+ if "/comments?" in url:
322
+ return [comment]
323
+ return {"number": candidate.pull_request, "head": {"sha": next(all_heads)}}
324
+
325
+ observed = observer.observe_codex_review(candidate, read)
326
+ if head_changes:
327
+ assert observed.reason is observer.HoldReason.HEAD_CHANGED
328
+ assert observed.evidence_ids == ()
329
+ assert observed.quota_notice_ids == ()
330
+ else:
331
+ assert observed.reason is (
332
+ observer.HoldReason.REVIEW_FINDINGS
333
+ if has_findings
334
+ else observer.HoldReason.NATIVE_FORMAT_UNVERIFIED
335
+ )
336
+ assert observed.evidence_ids == (review["id"],)
337
+ assert observed.quota_notice_ids == (5902483341,)
338
+
339
+
340
+ def test_quota_after_stale_review_remains_diagnostic_hold() -> None:
341
+ comment = json.loads(
342
+ (Path(__file__).parent / "fixtures/codex-quota-credits-comment.json").read_text(
343
+ encoding="utf-8-sig"
344
+ )
345
+ )
346
+
347
+ def read(url: str) -> object:
348
+ if "/reviews?" in url:
349
+ return [{"id": 1, "commit_id": NEXT_HEAD}]
350
+ if "/comments?" in url:
351
+ return [comment]
352
+ return {"number": 7, "head": {"sha": HEAD}}
353
+
354
+ observed = observer.observe_codex_review(CANDIDATE, read)
355
+ assert observed.reason is observer.HoldReason.QUOTA_NOTICE
356
+ assert observed.evidence_ids == ()
357
+ assert observed.quota_notice_ids == (5902483341,)
@@ -0,0 +1,42 @@
1
+ """Specifications for the scheduled task Install-SweepEmptyDirs.ps1 registers.
2
+
3
+ A task that starts a console program straight from Task Scheduler draws a
4
+ console window on the signed-in desktop at every run. The registered action
5
+ starts the program through conhost.exe --headless so no window appears.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+
13
+ _INSTALLER_PATH = Path(__file__).resolve().parents[1] / "Install-SweepEmptyDirs.ps1"
14
+ _UTF8_ENCODING = "utf-8"
15
+ _ACTION_PATTERN = re.compile(
16
+ r"^\$Action = New-ScheduledTaskAction -Execute (\$\w+) -Argument (.+)$",
17
+ re.MULTILINE,
18
+ )
19
+ _CONHOST_ASSIGNMENT = (
20
+ "$ConhostPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\\conhost.exe'"
21
+ )
22
+
23
+
24
+ def _registered_action() -> tuple[str, str]:
25
+ installer_text = _INSTALLER_PATH.read_text(encoding=_UTF8_ENCODING)
26
+ all_actions = _ACTION_PATTERN.findall(installer_text)
27
+ assert len(all_actions) == 1, all_actions
28
+ return all_actions[0]
29
+
30
+
31
+ def test_should_start_the_task_through_conhost() -> None:
32
+ installer_text = _INSTALLER_PATH.read_text(encoding=_UTF8_ENCODING)
33
+ executable_variable, _ = _registered_action()
34
+
35
+ assert executable_variable == "$ConhostPath"
36
+ assert _CONHOST_ASSIGNMENT in installer_text
37
+
38
+
39
+ def test_should_pass_headless_before_the_python_program() -> None:
40
+ _, argument_text = _registered_action()
41
+
42
+ assert argument_text.startswith('"--headless ""$PythonPath"" ""$ScriptPath""')