Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@
## 2026-08-09 - [λŒ€μš©λŸ‰ 둜그 μŠ€μΊ” μ‹œ μ •κ·œν‘œν˜„μ‹ μ‹€ν–‰ μ „ O(N) μ„œλΈŒμŠ€νŠΈλ§ 검증 μ„ ν–‰]
**Learning:** `classify_testthat_failure`μ—μ„œ ν…ŒμŠ€νŠΈ μ‹€νŒ¨ 내역이 μ—†λŠ” 2MB 둜그 νŒŒμΌμ„ λŒ€μƒμœΌλ‘œ μ •κ·œν‘œν˜„μ‹μ„ μ‹€ν–‰ν•˜λ©΄ μ•½ 20msκ°€ μ†Œμš”λ˜μ§€λ§Œ, λ‹¨μˆœ λ¬Έμžμ—΄ 검색은 μ•½ 1ms만 μ†Œμš”λ©λ‹ˆλ‹€. λ¬Έμžμ—΄ 쑴재 μ—¬λΆ€κ°€ μ •κ·œν‘œν˜„μ‹ 맀칭의 μ „μ œ 쑰건일 λ•Œ, μ½œλ“œ 패슀(Cold Path)μ—μ„œ μˆœμ„œ μ΅œμ ν™”λŠ” 맀우 큰 μ„±λŠ₯ 차이λ₯Ό λ§Œλ“­λ‹ˆλ‹€.
**Action:** λŒ€μš©λŸ‰ ν…μŠ€νŠΈ μž…λ ₯(CI 둜그 λ“±)μ—μ„œ λ³΅μž‘ν•œ μ •κ·œν‘œν˜„μ‹μ„ νŒŒμ‹±ν•˜κΈ° 전에 항상 λΉ λ₯Έ O(N) λ¬Έμžμ—΄ 쑴재 μ—¬λΆ€ 확인을 λ¨Όμ € μˆ˜ν–‰ν•˜μ‹­μ‹œμ˜€.
## 2026-08-18 - Concurrently yield items from an executor
**Learning:** `ThreadPoolExecutor.shutdown(wait=False, cancel_futures=True)` cancels pending futures but cannot interrupt a running external request. Generator shutdown latency therefore depends on every transport call having a finite request timeout. Worker callbacks must also avoid mutating caller-owned state concurrently.
**Action:** Keep the executor's non-waiting shutdown in a `finally` block, enforce and translate a finite `gh api` timeout in `GitHubClient.request`, and report repository failures from the generator thread rather than worker threads.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Semantic Versioning where the repository publishes a release.

### Changed

- Bound concurrent agent-mention API calls with a finite GitHub CLI timeout, report worker failures from the generator thread, preserve independent mention and sweep queues, and enforce the repository-dispatch ten-key limit while keeping OpenCode review-only flags wrapper-owned.
- Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap.
- Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services.
- Run the bounded DiskSage repair heartbeat at minute 37 of every hour, dispatch no more than one exact-head repair, and wait two hours before redispatching an unchanged head so legitimate OpenCode or NVIDIA NIM latency does not create duplicate writers.
Expand Down
40 changes: 20 additions & 20 deletions scripts/ci/agent_mention_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ class MentionRequest:
class GitHubClient:
"""Small token-bound wrapper around ``gh api`` for JSON requests."""

def __init__(self, token: str) -> None:
"""Initialize a client with one non-empty GitHub credential."""
def __init__(self, token: str, *, timeout_seconds: int = 60) -> None:
"""Initialize a client with one credential and request timeout."""

if not token:
raise ValueError("GitHub token is required")
self._token = token
self._timeout_seconds = timeout_seconds

def request(
self,
Expand All @@ -73,14 +74,21 @@ def request(
command.extend(["--input", "-"])
environment = os.environ.copy()
environment["GH_TOKEN"] = self._token
completed = subprocess.run(
command,
input=None if input_payload is None else json.dumps(input_payload),
text=True,
capture_output=True,
check=False,
env=environment,
)
try:
completed = subprocess.run(
command,
input=None if input_payload is None else json.dumps(input_payload),
text=True,
capture_output=True,
shell=False,
check=False,
env=environment,
timeout=self._timeout_seconds,
)
except subprocess.TimeoutExpired as exc:
raise RuntimeError(
f"gh api timed out after {self._timeout_seconds} seconds"
) from exc
return_code = int(getattr(completed, "returncode", 0))
if return_code:
diagnostic = " ".join(
Expand Down Expand Up @@ -374,23 +382,15 @@ def repository_dispatch_body(
event_type: str,
client_payload: dict[str, Any],
) -> dict[str, Any]:
"""Return a repository_dispatch body within GitHub's 10-key payload limit.

GitHub's create-repository-dispatch endpoint accepts at most 10 top-level
``client_payload`` properties. A larger object is rejected with HTTP 422,
so mention routing cannot enqueue a review.
"""
"""Return a repository-dispatch body within GitHub's 10-key payload limit."""

if len(client_payload) > REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS:
raise ValueError(
"repository_dispatch client_payload has "
f"{len(client_payload)} keys; GitHub allows at most "
f"{REPOSITORY_DISPATCH_CLIENT_PAYLOAD_MAX_KEYS}"
)
return {
"event_type": event_type,
"client_payload": client_payload,
}
return {"event_type": event_type, "client_payload": client_payload}


def noema_payload(request: MentionRequest) -> dict[str, Any]:
Expand Down
145 changes: 89 additions & 56 deletions scripts/ci/agent_mention_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import argparse
import concurrent.futures
import os
import re
from dataclasses import dataclass
Expand Down Expand Up @@ -139,6 +140,65 @@ def list_accessible_repositories(
return sorted(set(names))


def _fetch_repo_recent_pull_requests(
client: GitHubClient,
repository: str,
cutoff: datetime,
) -> list[dict[str, Any]]:
"""Fetch recent open pull requests for a single repository."""
results: list[dict[str, Any]] = []
page = 1
while True:
response = client.request(
[
f"repos/{repository}/pulls",
"-X",
"GET",
"-f",
"state=open",
"-f",
"sort=updated",
"-f",
"direction=desc",
"-f",
"per_page=100",
"-f",
f"page={page}",
]
)
pull_requests = flatten_pages(response)
if not pull_requests:
break
reached_cutoff = False
for pull_request in pull_requests:
if (
parse_timestamp(
str(pull_request.get("updated_at") or "")
)
< cutoff
):
reached_cutoff = True
break
number = pull_request.get("number")
if not isinstance(number, int) or number < 1:
raise ValueError(
"GitHub returned an invalid pull request number"
)
results.append({
"number": number,
"repository": repository,
"pull_request": {
"url": (
"https://api.github.com/repos/"
f"{repository}/pulls/{number}"
)
},
})
if reached_cutoff or len(pull_requests) < 100:
break
page += 1
return results

def list_recent_pull_requests(
client: GitHubClient,
*,
Expand All @@ -155,62 +215,35 @@ def list_recent_pull_requests(
organization=organization,
repository_source=repository_source,
)
for repository in repositories:
try:
page = 1
while True:
response = client.request(
[
f"repos/{repository}/pulls",
"-X",
"GET",
"-f",
"state=open",
"-f",
"sort=updated",
"-f",
"direction=desc",
"-f",
"per_page=100",
"-f",
f"page={page}",
]
)
pull_requests = flatten_pages(response)
if not pull_requests:
break
reached_cutoff = False
for pull_request in pull_requests:
if (
parse_timestamp(
str(pull_request.get("updated_at") or "")
)
< cutoff
):
reached_cutoff = True
break
number = pull_request.get("number")
if not isinstance(number, int) or number < 1:
raise ValueError(
"GitHub returned an invalid pull request number"
)
yield {
"number": number,
"repository": repository,
"pull_request": {
"url": (
"https://api.github.com/repos/"
f"{repository}/pulls/{number}"
)
},
}
if reached_cutoff or len(pull_requests) < 100:
break
page += 1
except Exception as exc: # noqa: BLE001 - repository isolation boundary
if on_error is None:
raise
on_error(repository, exc)

if not repositories: # pragma: no cover
return

executor = concurrent.futures.ThreadPoolExecutor(
max_workers=min(10, len(repositories))
)
try:
future_repositories = {
executor.submit(
_fetch_repo_recent_pull_requests,
client,
repository,
cutoff,
): repository
for repository in repositories
}
for future in concurrent.futures.as_completed(future_repositories):
repository = future_repositories[future]
try:
pull_requests = future.result()
except Exception as exc: # noqa: BLE001 - repository isolation boundary
if on_error is None:
raise
on_error(repository, exc)
continue
yield from pull_requests
finally:
executor.shutdown(wait=False, cancel_futures=True)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def list_recent_comments(
Expand Down
Loading
Loading