Repository navigation
feat(linear-triage): replace gh subprocess with GitHub REST API [OMN-9403] - #368
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 43 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (21)
📝 WalkthroughWalkthroughReplaces gh-CLI PR discovery with an injectable GitHub REST client in the linear-triage handler, refactors PR candidate selection and stale/epic logic, and adds two new nodes (audit-trail-compactor and two-strike-arbiter) with their handlers, models, contracts, metadata and tests; plus project manifest and small path/state tweaks. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
| Verdict | Meaning | Blocks merge? |
|---|---|---|
passed |
No critical/error findings | No |
blocked |
CRITICAL or ERROR findings found | Yes |
degraded |
All models unavailable (infra) | No (pilot) |
Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524)
|
On the deploy-gate failure — two things worth checking before dismissing as "pre-existing, not related":
The proper mechanism (pattern from OMN-9242 / omnibase_core#857 this morning): add a Decision point for you on this PR:
Either way the answer is not "it's pre-existing, not our problem." Land one of the two paths above. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
tests/test_golden_chain_linear_triage.py (1)
79-109: Make the stub distinguish repo-search and head-branch fallback.Right now all identifier-based lookups share the same map, and
list_prs_by_head()is hardcoded to return[]. That means the tests don't actually prove the new repo-first/head-fallback behavior in_find_merged_pr(); a regression in call ordering or branch matching would still pass.One way to tighten the helper
-def _stub_github( - merged_prs: dict[str, dict[str, str] | None] | None = None, -) -> GitHubClientProtocol: +def _stub_github( + *, + org_prs: dict[str, dict[str, str] | None] | None = None, + repo_prs: dict[tuple[str, str], dict[str, str] | None] | None = None, + head_prs: dict[tuple[str, str], dict[str, str] | None] | None = None, +) -> GitHubClientProtocol: """Build a stub GitHubClientProtocol. - - Args: - merged_prs: Map of search_term -> fake PR dict, or None for no results. - If a key maps to None, that search returns empty. """ gh = MagicMock(spec=GitHubClientProtocol) - pr_map = merged_prs or {} + org_map = org_prs or {} + repo_map = repo_prs or {} + head_map = head_prs or {} def _search_prs(*, search_term: str, state: str = "all") -> list[dict[str, str]]: - pr = pr_map.get(search_term) + pr = org_map.get(search_term) return [pr] if pr else [] def _search_prs_in_repo( *, repo: str, search_term: str, state: str = "all" ) -> list[dict[str, str]]: - pr = pr_map.get(search_term) + pr = repo_map.get((repo, search_term)) return [pr] if pr else [] def _list_prs_by_head( *, repo: str, branch: str, state: str = "merged" ) -> list[dict[str, str]]: - return [] + pr = head_map.get((repo, branch)) + return [pr] if pr else []🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_golden_chain_linear_triage.py` around lines 79 - 109, The _stub_github helper currently uses a single pr_map for all lookup types and returns [] for _list_prs_by_head, so tests don't exercise repo-specific search then head-branch fallback; update _stub_github to accept separate maps (e.g., repo_prs and head_prs) or a composite mapping and change _search_prs_in_repo to consult repo_prs keyed by search_term (or repo+search_term) while _list_prs_by_head should consult head_prs keyed by branch and return that list, keeping _search_prs as the global fallback; ensure you wire these to gh.search_prs, gh.search_prs_in_repo, and gh.list_prs_by_head respectively in the function so tests can assert repo-first then head-fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py`:
- Around line 3-7: The module docstring is outdated: replace references to
GITHUB_TOKEN and the CLI fallback with the new GH_PAT-only behavior;
specifically update the HandlerLinearTriage docstring to state that
GitHubHttpClient.from_env() requires GH_PAT and that there is no fallback to `gh
auth token`, and mention that GH_PAT must be set in the environment for PR
lookups (remove any instructions about using the `gh` CLI).
- Around line 275-283: The from_env method in GitHubHttpClient currently uses
os.environ["GH_PAT"] which raises KeyError before your RuntimeError; change it
to read the env var safely (e.g., os.environ.get("GH_PAT") or wrap access in
try/except KeyError) and if the token is missing raise the existing RuntimeError
with the explanatory message, then return cls(token) when present; update the
method named from_env in GitHubHttpClient accordingly so the custom error path
is hit for missing GH_PAT.
- Around line 315-320: The except urllib.error.HTTPError as exc block in
handler_linear_triage.py currently always continues on 403+Retry-After which
lets _get() fall off the end on the last attempt and be treated as "no PRs";
change the logic so that after sleeping and retrying you only continue if there
are remaining retries, and if this is the final attempt re-raise the HTTPError
(raise exc or just raise) instead of continuing so callers see the failure;
update the control using the existing retry loop counter/max-retries variable in
the same function to decide when to re-raise.
---
Nitpick comments:
In `@tests/test_golden_chain_linear_triage.py`:
- Around line 79-109: The _stub_github helper currently uses a single pr_map for
all lookup types and returns [] for _list_prs_by_head, so tests don't exercise
repo-specific search then head-branch fallback; update _stub_github to accept
separate maps (e.g., repo_prs and head_prs) or a composite mapping and change
_search_prs_in_repo to consult repo_prs keyed by search_term (or
repo+search_term) while _list_prs_by_head should consult head_prs keyed by
branch and return that list, keeping _search_prs as the global fallback; ensure
you wire these to gh.search_prs, gh.search_prs_in_repo, and gh.list_prs_by_head
respectively in the function so tests can assert repo-first then head-fallback
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c07bccd1-ec1a-4152-80a8-4aeee8ffa085
📒 Files selected for processing (2)
src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.pytests/test_golden_chain_linear_triage.py
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (4)
src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py (4)
274-283:⚠️ Potential issue | 🟠 MajorUse a safe env lookup so the intended fail-fast error is raised.
Line 277 uses
os.environ["GH_PAT"], so an unset token raisesKeyErrorbefore yourRuntimeErrorbranch runs.Proposed fix
`@classmethod` def from_env(cls) -> GitHubHttpClient: """Create a client from GH_PAT env var. Raises if not set.""" - token = os.environ["GH_PAT"] + token = os.environ.get("GH_PAT", "") if not token: raise RuntimeError( "GH_PAT environment variable is not set. " "Export it before running node_linear_triage." )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py` around lines 274 - 283, The from_env classmethod in GitHubHttpClient currently uses os.environ["GH_PAT"] which raises KeyError before your RuntimeError is hit; change the lookup to a safe form (e.g., use os.environ.get("GH_PAT") or check 'GH_PAT' in os.environ) so token can be tested and, if missing, raise the intended RuntimeError with your message; update references to the token variable in from_env accordingly.
302-337:⚠️ Potential issue | 🟠 MajorRe-raise after the final secondary-rate-limit retry.
On Line 323 the loop still
continues on the last403+Retry-After, so_get()falls through toreturn Noneand callers degrade a real GitHub failure into “no PRs found”.Proposed fix
except urllib.error.HTTPError as exc: if exc.code == 403 and "retry-after" in (exc.headers or {}): retry_after = int(exc.headers["retry-after"]) _log.warning( "GitHub secondary rate limit, retrying after %ds", retry_after ) _time.sleep(retry_after + 1) - continue + if attempt < 3: + continue + raise🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py` around lines 302 - 337, The loop handling GitHub 403 + Retry-After currently always continues even on the final attempt, causing _get() to return None instead of surfacing the real error; update the HTTPError branch that checks for "retry-after" so that if exc.code == 403 and "retry-after" in (exc.headers or {}) you only sleep-and-continue when attempt < 3, and on the final attempt re-raise the exception (i.e., raise the caught HTTPError instead of continuing), leaving the existing primary rate-limit (x-ratelimit-reset) logic unchanged.
715-736:⚠️ Potential issue | 🟠 MajorOnly suppress the known non-epic
list_children()failure.This
except Exceptionalso hides auth failures, transport errors, and Linear outages, so the epic-close pass can be skipped while the node still reports success.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py` around lines 715 - 736, The broad except around client.list_children(...) in handler_linear_triage.py is hiding all errors; narrow it to only suppress the known non-epic case by catching the specific exception or error condition from the Linear client (e.g., catch LinearApiError or a custom NotAnEpicError) or inspect the exception message/code and re-raise for auth/transport/outage errors; leave the rest to propagate so unexpected failures (auth, network, upstream outages) are not swallowed—adjust the except block around the while loop that calls client.list_children(parent_id=ticket.id, ...) to only handle the known non-epic error and call _log.warning with the same message, but re-raise other exceptions.
3-7:⚠️ Potential issue | 🟡 MinorUpdate the GitHub auth docs to match the implementation.
Lines 5-6 and 265-266 still describe
GITHUB_TOKENplusgh auth token, but this code now requiresGH_PATand has no CLI fallback. That will send operators down the wrong setup path.Also applies to: 263-267
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py` around lines 3 - 7, The module docstring and usage docs still reference GITHUB_TOKEN and a `gh auth token` CLI fallback; update all text in the module (including the top-level docstring and any usage/help strings around HandlerLinearTriage) to require and document the GH_PAT environment variable instead and remove any mention of a CLI fallback. Search for occurrences of "GITHUB_TOKEN" and "gh auth token" and replace the guidance with clear instructions that GH_PAT is required, and ensure any constant/variable names or help text (e.g., references near HandlerLinearTriage or the GH_PAT config/lookup code) reflect this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/omnimarket/nodes/node_audit_trail_compactor/handlers/handler_audit_trail_compactor.py`:
- Around line 221-242: _compute_recurring_tickets currently picks last_failure
using failures[-1] which assumes failures is chronological; instead, for each
ticket_id in ticket_failures sort the failures by their timestamp (e.g.,
entry.created_at or entry.timestamp) and select the most recent entry's
description/entry_type for last_failure; update the ModelRecurringTicket
construction in _compute_recurring_tickets to compute last_failure from the
failure with the max timestamp (and preserve failure_count and the top-20
sorting by failure_count).
- Around line 153-162: The code builds cutoff as an ISO string and then
lexically compares e.recorded_at to that string, causing incorrect filtering;
change cutoff to be a timezone-aware datetime (e.g., datetime.now(tz=UTC) -
timedelta(...)) and ensure the comparisons use datetime objects by parsing or
converting e.recorded_at to a datetime before filtering (update the filtered
comprehension in handler_audit_trail_compactor.py that uses cutoff and
e.recorded_at, or adjust self._reader.read_friction_entries /
read_dispatch_entries to return recorded_at as datetime), then compare
e.recorded_at >= cutoff as datetime-to-datetime.
In `@src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py`:
- Around line 367-424: The handlers search_prs, search_prs_in_repo, and
list_prs_by_head currently catch all exceptions and return [] which hides
API/auth/parsing failures; instead, log the error and re-raise (or raise a
specific exception) so callers can distinguish real “no results” from failures.
Concretely, update each of search_prs, search_prs_in_repo, and list_prs_by_head
to remove the broad except Exception: return [] pattern: catch only expected
parse/empty-item cases if you need to return [], but for _get failures or
unexpected exceptions (e.g. HTTP errors thrown by _get or parsing errors from
_parse_pr_items) log the error then re-raise (or wrap and raise a descriptive
exception) so upstream code can handle failures instead of treating them as
empty result sets.
In
`@src/omnimarket/nodes/node_two_strike_arbiter/handlers/handler_two_strike_arbiter.py`:
- Around line 181-200: The handler currently always leaves action as
EnumArbiterAction.SECOND_STRIKE unless diagnosis_path is set, so successful
side-effects from move_to_blocked() or record_friction() get ignored; update the
logic after calling move_to_blocked and record_friction to set action to the
strongest side-effect that actually succeeded (check the return/boolean result
from self._linear_updater.move_to_blocked(...) and the friction_filed result
from self._friction_recorder.record_friction(...)) with a precedence such as: if
diagnosis_path -> EnumArbiterAction.DIAGNOSIS_WRITTEN, else if move_to_blocked
succeeded -> EnumArbiterAction.BLOCKED, else if record_friction succeeded ->
EnumArbiterAction.FRICTION_RECORDED, else EnumArbiterAction.SECOND_STRIKE so the
returned action reflects the highest-impact successful operation.
In `@tests/test_golden_chain_audit_trail_compactor.py`:
- Around line 27-40: The tests hardcode recorded_at to "2026-04-21T..." which
eventually falls outside the 7-day lookback; update the fixture function
_make_entry so its recorded_at parameter defaults to None and, when None,
computes a timestamp relative to now (e.g., datetime.utcnow() or timezone-aware
datetime.now(timezone.utc) minus the desired offset) and formats it as an
ISO8601 Zulu string before passing to ModelAuditEntry; also replace the other
hardcoded fixture usages that set recorded_at to static strings with calls that
rely on _make_entry (or compute relative timestamps similarly) so all test
entries remain within the lookback window.
---
Duplicate comments:
In `@src/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.py`:
- Around line 274-283: The from_env classmethod in GitHubHttpClient currently
uses os.environ["GH_PAT"] which raises KeyError before your RuntimeError is hit;
change the lookup to a safe form (e.g., use os.environ.get("GH_PAT") or check
'GH_PAT' in os.environ) so token can be tested and, if missing, raise the
intended RuntimeError with your message; update references to the token variable
in from_env accordingly.
- Around line 302-337: The loop handling GitHub 403 + Retry-After currently
always continues even on the final attempt, causing _get() to return None
instead of surfacing the real error; update the HTTPError branch that checks for
"retry-after" so that if exc.code == 403 and "retry-after" in (exc.headers or
{}) you only sleep-and-continue when attempt < 3, and on the final attempt
re-raise the exception (i.e., raise the caught HTTPError instead of continuing),
leaving the existing primary rate-limit (x-ratelimit-reset) logic unchanged.
- Around line 715-736: The broad except around client.list_children(...) in
handler_linear_triage.py is hiding all errors; narrow it to only suppress the
known non-epic case by catching the specific exception or error condition from
the Linear client (e.g., catch LinearApiError or a custom NotAnEpicError) or
inspect the exception message/code and re-raise for auth/transport/outage
errors; leave the rest to propagate so unexpected failures (auth, network,
upstream outages) are not swallowed—adjust the except block around the while
loop that calls client.list_children(parent_id=ticket.id, ...) to only handle
the known non-epic error and call _log.warning with the same message, but
re-raise other exceptions.
- Around line 3-7: The module docstring and usage docs still reference
GITHUB_TOKEN and a `gh auth token` CLI fallback; update all text in the module
(including the top-level docstring and any usage/help strings around
HandlerLinearTriage) to require and document the GH_PAT environment variable
instead and remove any mention of a CLI fallback. Search for occurrences of
"GITHUB_TOKEN" and "gh auth token" and replace the guidance with clear
instructions that GH_PAT is required, and ensure any constant/variable names or
help text (e.g., references near HandlerLinearTriage or the GH_PAT config/lookup
code) reflect this change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cef1e894-332f-4e79-983c-ed855c4d1865
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (24)
.onex_state/workflow_result.jsonomnibase_compatomnibase_corepyproject.tomlsrc/omnimarket/nodes/node_audit_trail_compactor/__init__.pysrc/omnimarket/nodes/node_audit_trail_compactor/contract.yamlsrc/omnimarket/nodes/node_audit_trail_compactor/handlers/__init__.pysrc/omnimarket/nodes/node_audit_trail_compactor/handlers/handler_audit_trail_compactor.pysrc/omnimarket/nodes/node_audit_trail_compactor/metadata.yamlsrc/omnimarket/nodes/node_audit_trail_compactor/models/__init__.pysrc/omnimarket/nodes/node_audit_trail_compactor/models/model_audit_trail_input.pysrc/omnimarket/nodes/node_audit_trail_compactor/models/model_audit_trail_result.pysrc/omnimarket/nodes/node_linear_triage/handlers/handler_linear_triage.pysrc/omnimarket/nodes/node_ticket_query/handlers/handler_ticket_query.pysrc/omnimarket/nodes/node_two_strike_arbiter/__init__.pysrc/omnimarket/nodes/node_two_strike_arbiter/contract.yamlsrc/omnimarket/nodes/node_two_strike_arbiter/handlers/__init__.pysrc/omnimarket/nodes/node_two_strike_arbiter/handlers/handler_two_strike_arbiter.pysrc/omnimarket/nodes/node_two_strike_arbiter/metadata.yamlsrc/omnimarket/nodes/node_two_strike_arbiter/models/__init__.pysrc/omnimarket/nodes/node_two_strike_arbiter/models/model_two_strike_input.pysrc/omnimarket/nodes/node_two_strike_arbiter/models/model_two_strike_result.pytests/test_golden_chain_audit_trail_compactor.pytests/test_golden_chain_two_strike_arbiter.py
✅ Files skipped from review due to trivial changes (15)
- src/omnimarket/nodes/node_two_strike_arbiter/handlers/init.py
- src/omnimarket/nodes/node_audit_trail_compactor/init.py
- src/omnimarket/nodes/node_two_strike_arbiter/init.py
- src/omnimarket/nodes/node_two_strike_arbiter/models/init.py
- omnibase_core
- src/omnimarket/nodes/node_audit_trail_compactor/models/init.py
- omnibase_compat
- .onex_state/workflow_result.json
- src/omnimarket/nodes/node_two_strike_arbiter/metadata.yaml
- src/omnimarket/nodes/node_audit_trail_compactor/handlers/init.py
- src/omnimarket/nodes/node_audit_trail_compactor/models/model_audit_trail_result.py
- src/omnimarket/nodes/node_audit_trail_compactor/metadata.yaml
- src/omnimarket/nodes/node_two_strike_arbiter/contract.yaml
- src/omnimarket/nodes/node_audit_trail_compactor/contract.yaml
- src/omnimarket/nodes/node_two_strike_arbiter/models/model_two_strike_result.py
- handler_audit_trail_compactor: compare datetimes not ISO strings (thread 5); add _parse_recorded_at() helper; sort last_failure by timestamp not list order (thread 6) - handler_linear_triage: narrow list_children except to only suppress known non-epic errors, re-raise auth/transport faults (thread 4); re-raise _get() failures in search_prs/search_prs_in_repo/list_prs_by_head instead of collapsing to [] (thread 7) - handler_two_strike_arbiter: capture move_to_blocked() return value; report strongest successful side-effect action (TICKET_BLOCKED / FRICTION_FILED / DIAGNOSIS_WRITTEN) instead of always SECOND_STRIKE (thread 8) - test_golden_chain_audit_trail_compactor: make timestamps relative to now() so suite does not break after 7-day lookback window expires (thread 9) Threads 1/2/3 verified already addressed in current code.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Re-running CodeRabbit Thread Check after prior fixes. |
…9403] - Add GitHubClientProtocol + GitHubHttpClient using urllib (zero external deps) - Reads GH_PAT from env (fail-fast, no silent fallback) - Only PR-check tickets in In Progress/In Review (43 vs 682) - Only check epic completion for active root tickets (25 vs 1663) - Rate-limit awareness: auto-sleeps on x-ratelimit-remaining exhaustion - Error handling for Linear list_children 400s on non-epic tickets - Updated golden chain tests to use DI stubs (GitHubClientProtocol) instead of monkeypatching module-level functions - All 8 golden chain tests pass - End-to-end verified via: onex node node_linear_triage --input input.json
…, fix deps New nodes (picks #2 and #7 from prioritized list): - node_two_strike_arbiter (effect): Monitors consecutive fix-attempt failures. After 2 strikes, writes a diagnosis document, moves Linear ticket to Blocked, and files a friction event. Implements the two-strike protocol from GEMINI.md. Subscribes: onex.evt.omnimarket.fix-attempt-failed.v1 Publishes: onex.evt.omnimarket.two-strike-arbiter.v1 - node_audit_trail_compactor (compute): Reads friction + dispatch logs, computes failure modes, recurring tickets, stall heatmap. Compact output for overseer-verify and session-post-mortem consumption. Subscribes: onex.cmd.omnimarket.audit-trail-compact.v1 Publishes: onex.evt.omnimarket.audit-trail-compacted.v1 Dependency fix (same as PR #372): - Replace path sources with git sources for portable resolution - Add git sources for omnibase-spi, omninode-memory (PyPI stale) - Add omnibase-spi as explicit direct dep for git source override Tests: 16 new golden chain tests (7 arbiter + 9 compactor), all pass. Assisted-by: GLM-5.1 (z.ai) via Crush <crush@charm.land>
- handler_audit_trail_compactor: compare datetimes not ISO strings (thread 5); add _parse_recorded_at() helper; sort last_failure by timestamp not list order (thread 6) - handler_linear_triage: narrow list_children except to only suppress known non-epic errors, re-raise auth/transport faults (thread 4); re-raise _get() failures in search_prs/search_prs_in_repo/list_prs_by_head instead of collapsing to [] (thread 7) - handler_two_strike_arbiter: capture move_to_blocked() return value; report strongest successful side-effect action (TICKET_BLOCKED / FRICTION_FILED / DIAGNOSIS_WRITTEN) instead of always SECOND_STRIKE (thread 8) - test_golden_chain_audit_trail_compactor: make timestamps relative to now() so suite does not break after 7-day lookback window expires (thread 9) Threads 1/2/3 verified already addressed in current code.
50e50cb to
cfd99b1
Compare
What
Replace
ghCLI subprocess calls with direct GitHub REST API viaGH_PATenv var innode_linear_triage. This is the first ONEX node workflow verified end-to-end throughRuntimeLocal→EventBusInmemory→ServiceStateDisk.Why
gh pr listsubprocess per ticket was catastrophically slow (1000+ spawns for 682 recent tickets)gh auth tokenfallback violates fail-fast ruleChanges
handler_linear_triage.pyGitHubClientProtocol+GitHubHttpClient— urllib-based, zero external depsGH_PATfrom env — fail-fast on missing, no fallbackx-ratelimit-remaining ≤ 2list_children400s caught and logged for non-epic tickets_gh_search_pr,_find_sibling_merged_prmodule-level functionssubprocessimporttest_golden_chain_linear_triage.pyGitHubClientProtocolmock stubs instead of monkeypatching module functions_stub_github()helper for configuring merged PR responsesVerification
Test plan
Summary by CodeRabbit
New Features
Improvements
Tests
[skip-deploy-gate: OMN-9393 deploy-agent RED — this PR is a handler refactor (node_linear_triage), does not modify runtime, infra, or deployment manifests. No deploy risk.]