From 31b867e050440ae5bab9eac587b45489d8c5476d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 10:14:21 +0000 Subject: [PATCH 01/58] =?UTF-8?q?docs(gaps):=20refresh=20stale=20=C2=A75.1?= =?UTF-8?q?=20next-increment=20list,=20record=20naruon=20Noema=20increment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1297 was already merged and #1345/#1326 were closed unmerged, but all three were still listed as pending candidates for this loop's next pass. Replace with current state and record this pass's actual increment: ContextualWisdomLab/naruon#1486 adds a check_calendar_conflict tool to naruon's noema-general-agent, reusing the existing deterministic conflict policy instead of a second one, and clarifies that naruon's Noema and this repo's central review-bot Noema are separate agents sharing only a name. --- CHANGELOG.md | 10 ++++ docs/product-technical-gap-baseline.md | 68 ++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d02f6ee..d0ac92a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Refresh `docs/product-technical-gap-baseline.md`'s §5.1 next-increment list, + which had gone stale: #1297 was already merged, and #1345/#1326 were closed + unmerged, yet all three were still listed as pending candidates. Replaced + with the current state (#1347 still open/dirty; `ContextualWisdomLab/naruon#1486` + added as this pass's naruon increment) and recorded the naruon-side Noema + role clarification: naruon's `noema-general-agent` is a separate, correctly + BYO-LLM-scoped agent from this repo's central review-bot Noema — they + intentionally share only a name — and it now has a `check_calendar_conflict` + tool (PRD-02) that reuses naruon's existing deterministic conflict policy + instead of inventing a second one. - Bump the vendored `contextual-orchestrator` review-sidecar pin from `5f2753a` (the #1422 pin) to current `main` `30c6d716`, picking up `ContextualWisdomLab/contextual-orchestrator#919`: generalizes the diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0933f55e8..8a28ffb40 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -792,6 +792,61 @@ recurrence" section below out of the file entirely; both are restored here.) Following up on that hosted-run confirmation is the concrete next check for this entry, not a new code change. +## 2026-08-30 hourly pass: §5.1 next-increment list was stale; naruon Noema role clarified + +- Re-verified the three §5.1 items from the previous pass against their current state rather than + assuming they still apply (per this document's own "병합 판단에는 재사용하지 않는다" rule): + - **#1297 is already merged** (`merged_at` 2026-08-26T21:47:16Z, into `main` at + `31e5f5337d8a8d844c456fe03f123c51b62416c9`+). No action remains; the item should not have still + been listed as pending. + - **#1345 is closed, unmerged** (`mergeable_state: dirty`, closed 2026-08-28T17:03:18Z without + merging). Its normalizer-linear-scan fix duplicates what #1417 already landed on `main` per the + 2026-08-30 entry above ("Bolt: label_section 탐색 로직 최적화"); treat as superseded, not a live + candidate. + - **#1326 is closed, unmerged** (`mergeable_state: behind`, closed 2026-08-27T11:59:18Z). The + appguardrail/macos_utility_packs hourly-caller onboarding it proposed was not carried forward by + this pass; if still wanted, it needs a fresh PR rebased on current `main`, not a reopen of #1326. + - **#1347 remains open** (SSRF/isolation hardening for `sandboxed_web_e2e.py`), `mergeable_state: + dirty` against current `main`. Its only review signal is CodeRabbit/Devin bot commentary (one + nitpick, several addressed rounds) — no human or required-check-independent `APPROVED` verdict + yet. Not touched this pass (time budget went to the naruon increment below instead); next pass + should merge current `main` into its head as an ordinary merge commit (never rebase) and re-check. +- **naruon-side Noema role clarified and widened (this pass's concrete increment, not just an + audit).** The user's directive for this loop specifically flagged that Noema is the central + `.github` review/CI agent, but naruon needs its own suited role rather than a copy of that one. + Investigation found naruon already has a *separate*, correctly-scoped agent identity — + `noema-general-agent` in `ContextualWisdomLab/naruon` `backend/services/noema_agent.py` — that + reasons over mail/content-graph/tasks on the **tenant's own configured LLM provider** (never routed + through the org's shared `contextual-orchestrator` review gateway; doing so would mix customer + prompts into shared org infrastructure and defeat the ZDR/cost-isolation boundary + `docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md` establishes for CI review). This is the + correct design, not a gap — the note in `ContextualWisdomLab/noema`'s `CLAUDE.md` that "every LLM + path ... naruon judgments — calls contextual-orchestrator" is imprecise about this and should be + read as covering a distinct, not-yet-built internal-governance use of Noema, not the customer-facing + workspace assistant. + - The actual, concrete gap: naruon's Noema was scoped to mail/tasks/calendar-*writeback-only* with + no scheduling-conflict judgment, even though naruon already has a stateless, deterministic, + fully-tested conflict policy (`services/calendar_conflict_policy.py::evaluate_calendar_conflicts`, + status-weighted confirmed > tentative > desired, RFC 5545 `STATUS:CANCELLED`-aware) behind + `POST /api/calendar/conflicts/evaluate` — directly serving PRD-02 ("일정 이동과 RSVP/commitment + 충돌을 놓치지 않는다") but not reachable from the agent. + - Fix: `ContextualWisdomLab/naruon#1486` adds a `check_calendar_conflict` tool to + `noema_agent.py` that calls the *same* `evaluate_calendar_conflicts` function the REST endpoint + uses (no second conflict policy invented), so the agent's judgment and the customer-facing API can + never diverge. Naruon does not persist provider calendar events server-side, so the tool evaluates + only commitments the caller already supplies (e.g. ones the LLM read from mail/tasks earlier in + the same run) rather than fetching a provider calendar itself; malformed `existing` rows are + skipped rather than raised. `registered_agents.json`/`task_agent_mapping.json` updated to list the + new `calendar.conflict_check` capability and to state explicitly that naruon's Noema and the + central `.github` review-bot Noema are two separate agents that intentionally share only a name. + - Validation (naruon repo): `PYTHONPATH=. python -m pytest backend/tests/test_noema_agent.py -q` → + 21 passed (6 new, including the pre-existing full-agent-run `TestModel` test that now also + exercises this tool end-to-end); full backend suite `python -m pytest -q` → 1808 passed, 32 + skipped; `ruff check` clean on both changed files. + - Acceptance open until `#1486` clears naruon's own required Checks (OpenCode/Strix/merge-scheduler, + central-workflow-sourced same as every other consumer repo) and merges; this snapshot is + implementation + local-evidence only, not merge authorization. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. @@ -810,10 +865,15 @@ recurrence" section below out of the file entirely; both are restored here.) ### 5.1 이번 루프의 다음 개발 increment -1. ContextualWisdomLab/.github#1297 — current-head Strix serialization과 scoped close cleanup의 hosted Checks·독립 승인을 재확인한 뒤 보호된 auto-merge를 기다린다. -2. ContextualWisdomLab/.github#1345/#1347 — 각각 normalizer 선형 스캔과 web-E2E isolation/SSRF 수정의 terminal Checks·Strix·Noema 증거를 같은 HEAD에서 재확인한다. -3. ContextualWisdomLab/.github#1326 — Appguardrail/macOS hourly caller를 current CodeRabbit finding 및 APA citation evidence와 함께 재검토한다. -4. G-01/G-02는 중앙 control-plane merge evidence의 current-head 품질 문제, G-05/G-06는 naruon ecosystem 소비 증거, G-15는 대용량·미지원 첨부파일 parser registry의 소유 저장소 PR로 연결한다. +1. ContextualWisdomLab/.github#1347 — web-E2E isolation/SSRF 수정을 current `main`으로 merge-conflict + 해소(ordinary merge commit, no rebase) 후 terminal Checks·독립 승인을 재확인한다. (#1297은 이미 + 병합됨; #1345/#1326은 closed·unmerged로 확인되어 더 이상 후보가 아니다 — 위 2026-08-30 항목 참고.) +2. ContextualWisdomLab/naruon#1486 — 새로 추가된 Noema `check_calendar_conflict` 도구의 naruon 자체 + required Checks(OpenCode/Strix/merge-scheduler)를 current head에서 재확인하고, 조건 충족 시 + merge한다. +3. G-01/G-02는 중앙 control-plane merge evidence의 current-head 품질 문제, G-05/G-06는 naruon + ecosystem 소비 증거(부분적으로 #1486이 G-06/PRD-02에 기여), G-15는 대용량·미지원 첨부파일 parser + registry의 소유 저장소 PR로 연결한다. ## 6. Compliance and data boundary From d01e66293719e6d15da4c813253797678884e5aa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 10:18:43 +0000 Subject: [PATCH 02/58] docs(gaps): record post-#919-pin-bump noema-review failure signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit naruon#1486 confirms the current sidecar pin (30c6d716...) actually reaches a hosted PR-target run, but still fails closed with a new signature: a non-fatal 413 on ZDR-catalog prefetch (falls back to a live feed) followed by bytez discovery returning HTTP 500, which empties the orchestrator/free pool the same way the already-tracked structural gap describes. Also confirms opencode-review's gate correctly fails closed awaiting the async dispatch verdict on fresh heads (naruon#1486, .github#1438) — expected, not a defect. --- docs/product-technical-gap-baseline.md | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8a28ffb40..c1edf40b6 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -847,6 +847,43 @@ recurrence" section below out of the file entirely; both are restored here.) central-workflow-sourced same as every other consumer repo) and merges; this snapshot is implementation + local-evidence only, not merge authorization. +## 2026-08-30 post-#1486/#1438 wake: pin `30c6d716…` confirmed live, still fails closed (new signature) + +- `ContextualWisdomLab/naruon#1486`'s `noema-review` run (`run 33305922007`, PR-target on trusted base + `dc2ed58f…`) confirms the vendored sidecar now provisions the current pin + (`30c6d71680e659f25a0a433d4726ad0d437f9757`, the #919 User-Agent/403 fix bumped in earlier this same + pass) — this is the first hosted confirmation that pin actually reaches a PR-target run, not just + static contract tests. It still fails closed, but with a **new** failure shape distinct from the + prior HTTP 403/413-on-Models.dev signature: + 1. A ZDR-catalog prefetch got `request_failed status=413 code=request_too_large` and fell back to + "using live OpenRouter ZDR endpoint feed" — handled non-fatally, logged only, sidecar continued. + 2. During sidecar startup, `bytez` model discovery returned `provider_discovery_failed + provider=bytez code=http_status_500` — this line was fully visible in the CI log (not folded + into `omitted_unstructured_lines`), confirming the `_log_discovery_errors()` visibility fix + recorded earlier this pass is working as intended. 4 other stderr lines were still folded into + `omitted_unstructured_lines=4` (not inspected further this pass — could be additional detail, not + assumed secret-bearing given the sanitizer's existing allowlist-based design). + 3. `[contextual-orchestrator-sidecar] error: sidecar exited before healthz (status 1)` — the process + exits non-zero, matching the already-tracked "review sidecar discovered no eligible models; + orchestrator/free would fail closed" `SystemExit` path once `bytez` (a candidate free-pool + contributor per the five-secret design) also fails to enumerate. +- This is consistent with, not contradictory to, the already-open "orchestrator/free pool exhausted" + gap above: OpenRouter's genuinely-free models remain intentionally `evidence_only` (correct ZDR + hardening, must not be reverted), and nvidia_nim/nvidia_nim_sub/openai were already found to + contribute ~0 routable free models even after the #919 Models.dev fix. `bytez` returning HTTP 500 in + this run removes what may have been the last remaining candidate source for that run, though whether + this specific 500 is a transient Bytez-side fault or a persistent one was not re-tested this pass + (would need a second hosted run to distinguish; not attempted given time budget). Either way, the + underlying decision this gap needs — accept real spend via `orchestrator/auto`, or wire a genuine + zero-cost source (`opencode_zen`, per the 2026-08-30 entry above) — remains open and still requires a + budget-owner call, not a unilaterally-applied code change. +- Also confirmed this pass: `opencode-review`'s required-check gate (a separate mechanism from + `noema-review`) correctly fails closed on a fresh head with no `opencode-agent` review yet, on both + `naruon#1486` and `.github#1438` — this is the documented "asynchronous model dispatch... had not + completed for any of the refreshed PRs by the time this pass ended" pattern from the entry above, not + a new defect. Commented on both PRs distinguishing the two failure classes; no code change made in + either PR for either failure, since neither is caused by their own diffs. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. From b6aa078e9a554239f314818fae87d67ad8966741 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:14:09 +0000 Subject: [PATCH 03/58] feat(sidecar): surface preflight-rejection detail; widen failure-log tail The review sidecar's live warm-up preflight already records a bounded error_type/http_status per rejected route, but only into a JSON artifact -- not the CI job's visible console log. That made a real fail-closed incident impossible to diagnose as transient or not without downloading the artifact separately, and led directly to a misdiagnosis this pass corrects in docs/product-technical-gap-baseline.md. Add _log_preflight_rejections (mirrors the existing _log_discovery_errors visibility fix), a matching sanitizer allowlist entry, and widen the failure-path stderr tail from a fixed 20 lines to a named SIDECAR_STDERR_TAIL_LINES=60 so discovery errors plus preflight rejections plus summary lines can no longer silently truncate. Companion to ContextualWisdomLab/contextual-orchestrator#923, which fixes the analogous single-shot discovery fetch. This repo's own completion-warm-up-probe call site is deliberately left single-shot; see that PR's description for the latency/amplification risk that ruled out retrying it blind. --- CHANGELOG.md | 13 ++ ...contextual_orchestrator_review_launcher.py | 44 +++++++ .../contextual_orchestrator_review_sidecar.sh | 12 +- ..._contextual_orchestrator_sidecar_stream.py | 14 +++ ...l_orchestrator_review_runtime_preflight.py | 119 ++++++++++++++++++ ...al_orchestrator_review_sidecar_contract.py | 16 ++- 6 files changed, 214 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ac92a2f..0e51b979c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Surface the review sidecar's preflight-rejection detail (`error_type`, + `http_status` per rejected route) to the CI job's visible console log + (`_log_preflight_rejections` in `contextual_orchestrator_review_launcher.py`, + new sanitizer allowlist entry), mirroring the existing discovery-error + visibility fix. Previously this detail only reached a JSON artifact, + making it impossible to tell from CI logs alone whether a fail-closed + incident was transient or not. Bumped the sidecar's failure-path stderr + tail from a fixed 20 lines to a named `SIDECAR_STDERR_TAIL_LINES=60` so + the new diagnostics aren't truncated. Companion to + `ContextualWisdomLab/contextual-orchestrator#923`, which fixes the + matching discovery-side single-shot-fetch gap; this repo's own + completion-warm-up-probe path is deliberately left single-shot (see that + PR's description for why). - Refresh `docs/product-technical-gap-baseline.md`'s §5.1 next-increment list, which had gone stale: #1297 was already merged, and #1345/#1326 were closed unmerged, yet all three were still listed as pending candidates. Replaced diff --git a/scripts/ci/contextual_orchestrator_review_launcher.py b/scripts/ci/contextual_orchestrator_review_launcher.py index 606e69458..56bcc8650 100644 --- a/scripts/ci/contextual_orchestrator_review_launcher.py +++ b/scripts/ci/contextual_orchestrator_review_launcher.py @@ -99,6 +99,49 @@ def _log_discovery_errors(errors: list[object]) -> None: print(_DISCOVERY_DIAGNOSTICS_COMPLETE_SENTINEL, file=sys.stderr, flush=True) +def _rejected_preflight_routes(report: dict[str, object]) -> list[dict[str, object]]: + """Collect rejected route rows from a preflight report, primary attempt included. + + A ``pool=="auto"`` fallback report nests the primary attempt's own report + under ``primary_attempt`` (see :func:`_preflight_with_fallback`); this + walks into it so a fallback-then-fail run still surfaces the primary + attempt's rejections, not just the fallback's. + """ + routes: list[dict[str, object]] = [] + primary_attempt = report.get("primary_attempt") + if isinstance(primary_attempt, dict): + routes.extend(_rejected_preflight_routes(primary_attempt)) + for route in report.get("routes") or []: + if isinstance(route, dict) and route.get("status") == "rejected": + routes.append(route) + return routes + + +def _log_preflight_rejections(report: dict[str, object]) -> None: + """Print one bounded, secret-free diagnostic per rejected preflight route. + + ``_preflight_review_agents`` already records a stable ``error_type`` and + optional ``http_status`` per rejected route in its JSON evidence artifact + (``args.preflight_out``), but that artifact is not part of the CI job's + visible console log -- exactly the gap that made a real fail-closed + incident impossible to diagnose as transient (5xx/timeout) versus not + (bad credential, empty completion) from CI logs alone. Safe to print + because ``error_type``/``http_status`` are the same bounded + classifications ``_log_discovery_errors`` already trusts, never raw + provider response text, exception messages, prompts, or credentials. + """ + for route in _rejected_preflight_routes(report): + http_status = route.get("http_status") + print( + f"preflight_rejected agent={route.get('agent_id', 'unknown')} " + f"provider={route.get('provider', 'unknown')} " + f"error_type={route.get('error_type', 'unknown')} " + f"http_status={http_status if isinstance(http_status, int) else 'none'}", + file=sys.stderr, + flush=True, + ) + + def _routable_discovered_models(discovered: list[object] | None) -> list[object]: """Drop evidence-only discovery rows before any live-serving selection. @@ -544,6 +587,7 @@ def main(argv: list[str] | None = None) -> int: ) except ReviewPreflightError as exc: _write_json(args.preflight_out, exc.report) + _log_preflight_rejections(exc.report) raise SystemExit(f"review sidecar preflight failed: {exc}") from None if fallback_used and fallback_result is not None: Path(args.catalog_out).write_text( diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index b044cab82..8e97043c0 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -35,6 +35,12 @@ SIDECAR_LOG_SANITIZER="$ORG_REPO_ROOT/scripts/ci/sanitize_contextual_orchestrato # finishes, letting the shell script wait for a deterministic marker instead # of guessing whether the async sanitizer has caught up. SIDECAR_DISCOVERY_DIAGNOSTICS_SENTINEL="discovery_diagnostics_complete" +# Bounded tail shown in job-log failure messages: discovery errors (up to one +# per credentialed provider) plus preflight-rejection diagnostics (up to +# REVIEW_PREFLIGHT_MAX_TOTAL_ROUTES routes) plus a couple of summary lines can +# together exceed the old 20-line cap, silently truncating exactly the +# evidence a fail-closed incident needs. +SIDECAR_STDERR_TAIL_LINES=60 CATALOG_LIMIT="${ORCHESTRATOR_CATALOG_LIMIT:-12}" CATALOG_FAMILY_CAP="${ORCHESTRATOR_CATALOG_FAMILY_CAP:-4}" ORCHESTRATOR_GITHUB_ENV="${GITHUB_ENV:-}" @@ -335,11 +341,11 @@ until curl -fsSL --max-time 2 "http://${ORCHESTRATOR_HOST}:${ORCHESTRATOR_PORT}/ # guarantees $sidecar_stderr holds everything the sidecar wrote before we # read it for the failure message below. wait_for_sidecar_sanitizers - fail "sidecar exited before healthz (status ${sidecar_status}); stderr: $(sed -n '1,20p' "$sidecar_stderr")" + fail "sidecar exited before healthz (status ${sidecar_status}); stderr: $(sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" "$sidecar_stderr")" fi i=$((i + 1)) if [ "$i" -ge 180 ]; then - fail "sidecar did not become healthy; stderr: $(sed -n '1,20p' "$sidecar_stderr")" + fail "sidecar did not become healthy; stderr: $(sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" "$sidecar_stderr")" fi sleep 1 done @@ -370,7 +376,7 @@ until grep -qx "$SIDECAR_DISCOVERY_DIAGNOSTICS_SENTINEL" "$sidecar_stderr" 2>/de fi sleep 0.2 done -sidecar_startup_warnings="$(grep -vx "$SIDECAR_DISCOVERY_DIAGNOSTICS_SENTINEL" "$sidecar_stderr" 2>/dev/null | sed -n '1,20p' || true)" +sidecar_startup_warnings="$(grep -vx "$SIDECAR_DISCOVERY_DIAGNOSTICS_SENTINEL" "$sidecar_stderr" 2>/dev/null | sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" || true)" if [ -n "$sidecar_startup_warnings" ]; then log "sidecar startup warnings (non-fatal): $sidecar_startup_warnings" fi diff --git a/scripts/ci/sanitize_contextual_orchestrator_sidecar_stream.py b/scripts/ci/sanitize_contextual_orchestrator_sidecar_stream.py index 0d1db2dab..c2f9b58a8 100644 --- a/scripts/ci/sanitize_contextual_orchestrator_sidecar_stream.py +++ b/scripts/ci/sanitize_contextual_orchestrator_sidecar_stream.py @@ -15,6 +15,12 @@ r"provider_discovery_failed provider=(?P[a-z][a-z0-9_]{0,63}) " r"code=(?P[A-Za-z0-9_.-]{1,64})" ) +_PREFLIGHT_REJECTED = re.compile( + r"preflight_rejected agent=(?P[A-Za-z0-9_./:-]{1,128}) " + r"provider=(?P[a-z][a-z0-9_]{0,63}) " + r"error_type=(?P[A-Za-z0-9_]{1,64}) " + r"http_status=(?P[0-9]{1,3}|none)" +) _PREFIX_SUMMARIES = ( ("review sidecar preflight failed:", "review sidecar preflight failed"), ("review sidecar discovery failed:", "review sidecar discovery failed"), @@ -53,6 +59,14 @@ def sanitize_line(line: str) -> str | None: f"provider_discovery_failed provider={provider_discovery_failed.group('provider')} " f"code={provider_discovery_failed.group('code')}" ) + preflight_rejected = _PREFLIGHT_REJECTED.search(stripped) + if preflight_rejected is not None: + return ( + f"preflight_rejected agent={preflight_rejected.group('agent')} " + f"provider={preflight_rejected.group('provider')} " + f"error_type={preflight_rejected.group('error_type')} " + f"http_status={preflight_rejected.group('http_status')}" + ) if stripped in ("client_disconnected", "discovery_diagnostics_complete"): return stripped for prefix, summary in _PREFIX_SUMMARIES: diff --git a/tests/test_contextual_orchestrator_review_runtime_preflight.py b/tests/test_contextual_orchestrator_review_runtime_preflight.py index d3ec36237..daff31408 100644 --- a/tests/test_contextual_orchestrator_review_runtime_preflight.py +++ b/tests/test_contextual_orchestrator_review_runtime_preflight.py @@ -130,6 +130,117 @@ def test_log_discovery_errors_sentinel_matches_the_sidecar_scripts_constant() -> assert f'SIDECAR_DISCOVERY_DIAGNOSTICS_SENTINEL="{sentinel}"' in sidecar_text +def test_log_preflight_rejections_prints_one_bounded_line_per_rejected_route( + capsys: pytest.CaptureFixture[str], +) -> None: + """A rejected-route report must become a visible, sanitizer-safe diagnostic. + + This is the same visibility gap ``_log_discovery_errors`` closed for + discovery, applied to preflight: the per-route ``error_type``/ + ``http_status`` already recorded in the JSON evidence artifact must also + reach the CI job's visible console log, so a future incident can be + diagnosed as transient or not without downloading the artifact. + """ + namespace = _load_launcher() + log_preflight_rejections = namespace.get("_log_preflight_rejections") + assert callable(log_preflight_rejections), "launcher must expose a preflight-rejection logger" + + report = { + "routes": [ + { + "agent_id": "nvidia_nim/meta-llama-3.1", + "provider": "nvidia_nim", + "error_type": "HTTPError", + "http_status": 500, + "status": "rejected", + }, + { + "agent_id": "bytez/llama-guard", + "provider": "bytez", + "error_type": "InvalidChatResponse", + "status": "rejected", + }, + { + "agent_id": "openai/gpt-5", + "provider": "openai", + "status": "ready", + }, + ] + } + + log_preflight_rejections(report) + + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err.splitlines() == [ + ( + "preflight_rejected agent=nvidia_nim/meta-llama-3.1 provider=nvidia_nim " + "error_type=HTTPError http_status=500" + ), + ( + "preflight_rejected agent=bytez/llama-guard provider=bytez " + "error_type=InvalidChatResponse http_status=none" + ), + ] + + +def test_log_preflight_rejections_walks_the_nested_primary_attempt_report( + capsys: pytest.CaptureFixture[str], +) -> None: + """A ``pool=="auto"`` fallback-then-fail report also surfaces primary rejections.""" + namespace = _load_launcher() + log_preflight_rejections = namespace["_log_preflight_rejections"] + + report = { + "routes": [ + { + "agent_id": "openrouter/priced", + "provider": "openrouter", + "error_type": "TimeoutError", + "status": "rejected", + }, + ], + "primary_attempt": { + "routes": [ + { + "agent_id": "openrouter/free", + "provider": "openrouter", + "error_type": "TimeoutError", + "status": "rejected", + }, + ], + }, + } + + log_preflight_rejections(report) + + captured = capsys.readouterr() + assert captured.err.splitlines() == [ + ( + "preflight_rejected agent=openrouter/free provider=openrouter " + "error_type=TimeoutError http_status=none" + ), + ( + "preflight_rejected agent=openrouter/priced provider=openrouter " + "error_type=TimeoutError http_status=none" + ), + ] + + +def test_log_preflight_rejections_prints_nothing_when_every_route_is_ready( + capsys: pytest.CaptureFixture[str], +) -> None: + """No rejections -> no diagnostic lines at all (nothing to explain).""" + namespace = _load_launcher() + log_preflight_rejections = namespace["_log_preflight_rejections"] + + log_preflight_rejections({"routes": [{"agent_id": "openai/gpt-5", "status": "ready"}]}) + + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err == "" + + def test_preflight_mirrors_runtime_request_and_keeps_only_compatible_routes() -> None: """Reject provider errors/malformed replies before the sidecar becomes ready.""" namespace = _load_launcher() @@ -387,6 +498,14 @@ def test_sidecar_stream_sanitizer_allowlists_only_bounded_diagnostics() -> None: assert sanitize_line( "provider_discovery_failed provider=bytez code=http_status_401" ) == "provider_discovery_failed provider=bytez code=http_status_401" + assert sanitize_line( + "preflight_rejected agent=nvidia_nim/meta-llama-3.1 provider=nvidia_nim " + "error_type=HTTPError http_status=500 upstream sk-secret" + ) == "preflight_rejected agent=nvidia_nim/meta-llama-3.1 provider=nvidia_nim error_type=HTTPError http_status=500" + assert sanitize_line( + "preflight_rejected agent=bytez/llama-guard provider=bytez " + "error_type=InvalidChatResponse http_status=none" + ) == "preflight_rejected agent=bytez/llama-guard provider=bytez error_type=InvalidChatResponse http_status=none" assert sanitize_line("provider response sk-secret") is None diff --git a/tests/test_contextual_orchestrator_review_sidecar_contract.py b/tests/test_contextual_orchestrator_review_sidecar_contract.py index 60b3ade48..f39079b59 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -433,7 +433,7 @@ def test_sidecar_surfaces_nonfatal_discovery_warnings_on_a_successful_startup() # `grep -v` exits 1 when every line was filtered out (the common, healthy # case with zero warnings); under `set -o pipefail` that would abort the # whole script unless explicitly tolerated. - assert "sed -n '1,20p' || true)\"" in text + assert 'sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" || true)"' in text assert 'log "sidecar startup warnings (non-fatal): $sidecar_startup_warnings"' in text # Must not `wait_for_sidecar_sanitizers` here: the sidecar keeps serving # after a successful healthz, so its sanitizer never sees EOF and doing @@ -444,6 +444,20 @@ def test_sidecar_surfaces_nonfatal_discovery_warnings_on_a_successful_startup() assert "wait_for_sidecar_sanitizers" not in text[healthz_confirmed:] +def test_sidecar_stderr_tail_covers_discovery_and_preflight_diagnostics() -> None: + """The failure-path log tail must be wide enough for the new diagnostics. + + Discovery errors (one per credentialed provider) plus preflight-rejection + diagnostics (up to ``REVIEW_PREFLIGHT_MAX_TOTAL_ROUTES`` routes) plus a + couple of summary lines can together exceed the old fixed 20-line cap, + silently truncating exactly the evidence a fail-closed incident needs. + """ + text = _read(SIDECAR) + assert "SIDECAR_STDERR_TAIL_LINES=60" in text + assert text.count('sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p"') == 3 + assert "sed -n '1,20p'" not in text + + def test_noema_review_workflow_provisions_sidecar_with_all_five_secrets() -> None: """Required Noema review uses the gateway; the public NIM hardcode is gone.""" workflow = _read(NOEMA_WORKFLOW) From f80bf02ce16c7a6950106338ae66ec15706ce3e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:15:11 +0000 Subject: [PATCH 04/58] docs(gaps): correct bytez/413 misattribution, record the real fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial re-investigation (triggered by direct feedback that a single provider erroring should never fail-close org-wide review CI) found the "2026-08-30 post-#1486/#1438 wake" entry misdiagnosed the incident on two counts: Bytez can never populate orchestrator/free regardless of HTTP status (_parse_bytez never sets is_free), and the 413 line is the sidecar's own unconditional self-test, not a live ZDR-prefetch fallback -- also present in two earlier entries, flagged here rather than hand-edited there. The actual terminating message was "review sidecar preflight failed" (a live warm-up-probe rejection), not the "no eligible models" path those entries claimed. Records the real root cause (two single-shot HTTP call sites with no retry) and the fix that follows: contextual-orchestrator#923 (discovery retry) and this repo's own #1438 (preflight-rejection visibility + wider stderr tail). Updates §5.1 to track both to merge. --- CHANGELOG.md | 7 +++ docs/product-technical-gap-baseline.md | 82 ++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e51b979c..231177dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Correct `docs/product-technical-gap-baseline.md`'s "2026-08-30 post-#1486/#1438 wake" entry (and flag + two earlier ones with the same error): Bytez can never populate `orchestrator/free` regardless of its + HTTP status (`_parse_bytez` never sets `is_free`), the `request_failed status=413` line is the + sidecar's own unconditional self-test rather than a live ZDR-prefetch fallback, and the incident's + actual terminating message was `"review sidecar preflight failed"` — a live warm-up-probe rejection, + not the `"no eligible models"` path those entries claimed. See `ContextualWisdomLab/contextual-orchestrator#923` + and this repo's #1438 for the fix that follows from the corrected diagnosis. - Surface the review sidecar's preflight-rejection detail (`error_type`, `http_status` per rejected route) to the CI job's visible console log (`_log_preflight_rejections` in `contextual_orchestrator_review_launcher.py`, diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c1edf40b6..855db081f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -884,6 +884,74 @@ recurrence" section below out of the file entirely; both are restored here.) a new defect. Commented on both PRs distinguishing the two failure classes; no code change made in either PR for either failure, since neither is caused by their own diffs. +## 2026-08-30 correction: Bytez was never free-eligible; the fix is elsewhere + +Triggered directly by explicit user feedback that a single external provider erroring should never be +able to fail-close org-wide PR review CI. A 7-agent investigation (3 independent code readers, one +synthesis, 3 adversarial skeptics who each independently re-read the cited source rather than trusting +the synthesis) found the entry immediately above, and two earlier ones, misdiagnosed the mechanism. + +**What was wrong:** + +1. **Bytez can never populate `orchestrator/free`, regardless of its HTTP status.** + `contextual_orchestrator/model_discovery.py`'s `_parse_bytez` never sets `is_free` on any row it + builds, and `DiscoveredModel.is_free` defaults to `False`; `free_discovered_models()` is a flat + `is_free` filter. So the entry immediately above's claim that Bytez's HTTP 500 "removes what may + have been the last remaining candidate free-pool contributor" is false — Bytez was never a + candidate in the first place, whether its discovery call succeeds or fails. This is not new + information: `docs/planning/adrs/0041-generalize-models-dev-cost-classification.md` in + `contextual-orchestrator` already documents that Bytez has zero Models.dev coverage; the two + documents had simply never been cross-checked against each other on this point. +2. **The `request_failed status=413 code=request_too_large` line is the sidecar's own offline + self-test, not a live ZDR-catalog prefetch that "fell back" to anything.** + `scripts/ci/contextual_orchestrator_review_sidecar.sh`'s embedded self-test heredoc deliberately + spins up a throwaway local server and asserts `response.status == 413` on every single sidecar + boot, unconditionally, before any discovery or ZDR fetch starts. The immediately-following "using + live OpenRouter ZDR endpoint feed" line is printed only on that *unrelated* step's curl success, never + as a fallback from the 413. This misattribution — reading two adjacent, unrelated, always-emitted + log lines as a causal pair — appears not just in the entry immediately above but also in "2026-08-30 + sidecar pin staleness recurrence" (`gateway preflight returned HTTP 502 (and, on a differently-shaped + request, request_failed status=413...) before the model pool can run`) and in "2026-08-30 + post-#1413/#1422 backlog refresh cycle"'s #1420 bullet (`failed with request_failed status=413 ... + during model discovery, fell back to the OpenRouter ZDR feed`). Those two entries are left as + historical record rather than hand-edited (the first is explicitly marked superseded/kept-unedited + already; both predate this correction) — this note is the authoritative correction for all three. +3. **The naruon#1486/.github#1438 incident's actual terminating message was `"review sidecar preflight + failed"`, a distinct, separately-sanitized string from `"review sidecar discovered no eligible + models"`** (confirmed in `contextual_orchestrator_review_launcher.py`'s two separate `SystemExit` + sites and `sanitize_contextual_orchestrator_sidecar_stream.py`'s two separate allowlist entries). + That proves discovery *did* find at least one genuinely free-eligible candidate (necessarily from + `nvidia_nim`/`nvidia_nim_sub`/`openai` — the only sources with real Models.dev-sourced zero pricing) + and every one of those candidates then failed the live chat-completion warm-up probe in + `_preflight_review_agents`. The entry immediately above's claim that this "match[es]" the + no-eligible-models path is wrong. + +**The actual, confirmed root cause:** two call sites make exactly one HTTP attempt with zero retry — +`discover_provider_models`'s primary model-list fetch, and `_preflight_review_agents`'s live +`client.proxy_send_once(...)` warm-up probe (which hardcodes `allow_transient_retries=False`, +independent of `ModelClient.max_retries`). Either one hitting a single transient failure (5xx, timeout, +connection reset) is architecturally sufficient to fail the whole sidecar closed, unrelated to whether +any specific provider is structurally free-eligible. + +**What shipped this pass**, after adversarial review rejected the first draft fix (retrying both call +sites) as unsafe — it would stack added latency past the sidecar's own ~180s healthz-wait budget and +reintroduce exactly the request amplification `proxy_send_once`'s single-shot design exists to +prevent, especially risky during a genuine partial provider outage: + +- `ContextualWisdomLab/contextual-orchestrator#923` — one bounded retry (short delay, shortened + timeout, gated on the existing `is_transient_error` classifier reused as-is) on the discovery-side + fetch only. Full suite: 2765 passed, 1 skipped. +- `ContextualWisdomLab/.github#1438` — surfaces `_preflight_review_agents`'s already-recorded + per-route `error_type`/`http_status` to the CI job's visible console log (previously JSON-artifact + only), and widens the failure-path stderr tail (`SIDECAR_STDERR_TAIL_LINES`, 20→60 lines) so the new + diagnostics and existing discovery errors can no longer be silently truncated together. The + completion-warm-up-probe call site itself is deliberately left single-shot — retrying it needs + confirmed transience evidence from a real incident first, which this visibility fix now makes + possible to obtain, rather than a blind retry now. Full suite: 1884 passed, 1 skipped, 25 subtests; + coverage 100% (pre-existing `pingora_edge_policy.py:274` gap, owned by #1398, unaffected); + interrogate 100%. +- §5.1 below is updated to track these two PRs to merge. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. @@ -902,15 +970,21 @@ recurrence" section below out of the file entirely; both are restored here.) ### 5.1 이번 루프의 다음 개발 increment -1. ContextualWisdomLab/.github#1347 — web-E2E isolation/SSRF 수정을 current `main`으로 merge-conflict +1. ContextualWisdomLab/contextual-orchestrator#923 — discovery-side transient-retry 수정의 required + Checks·독립 승인을 재확인하고, 조건 충족 시 merge한다. 병합 후 `.github`의 `ORCHESTRATOR_PIN_SHA`를 + 해당 커밋으로 갱신하는 후속 PR이 필요하다(#1422/#1426이 이미 확립한 패턴과 동일). +2. ContextualWisdomLab/.github#1438 — preflight-rejection 가시성 개선 + stderr tail 확장 + 이번 + correction 문서 갱신의 required Checks·독립 승인을 재확인하고, 조건 충족 시 merge한다. +3. ContextualWisdomLab/.github#1347 — web-E2E isolation/SSRF 수정을 current `main`으로 merge-conflict 해소(ordinary merge commit, no rebase) 후 terminal Checks·독립 승인을 재확인한다. (#1297은 이미 병합됨; #1345/#1326은 closed·unmerged로 확인되어 더 이상 후보가 아니다 — 위 2026-08-30 항목 참고.) -2. ContextualWisdomLab/naruon#1486 — 새로 추가된 Noema `check_calendar_conflict` 도구의 naruon 자체 +4. ContextualWisdomLab/naruon#1486 — 새로 추가된 Noema `check_calendar_conflict` 도구의 naruon 자체 required Checks(OpenCode/Strix/merge-scheduler)를 current head에서 재확인하고, 조건 충족 시 merge한다. -3. G-01/G-02는 중앙 control-plane merge evidence의 current-head 품질 문제, G-05/G-06는 naruon +5. G-01/G-02는 중앙 control-plane merge evidence의 current-head 품질 문제, G-05/G-06는 naruon ecosystem 소비 증거(부분적으로 #1486이 G-06/PRD-02에 기여), G-15는 대용량·미지원 첨부파일 parser - registry의 소유 저장소 PR로 연결한다. + registry의 소유 저장소 PR로 연결한다. 완료된 discovery-side retry 이후에도 preflight warm-up probe + 재시도 여부는 실제 transience 증거(이번 pass의 가시성 개선으로 확보 가능)가 나오기 전까지 보류한다. ## 6. Compliance and data boundary From abaab114fe0ab2924d10871453a04b24262662ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:30:07 +0000 Subject: [PATCH 05/58] docs(gaps): record this pass's reconciliation with concurrent owner fixes Summarizes the wakeup that found main had advanced with the owner's own deeper investigation into the same bytez/preflight incident, reconciled this branch's three open PRs against it (merged current main/develop into each, dropped the now-redundant preflight-visibility fix, marked all three ready for review), and explicitly defers .github#1347's merge conflict to a dedicated next pass given it overlaps a file main has independently hardened for SSRF. --- docs/product-technical-gap-baseline.md | 66 +++++++++++++++++++------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c0cf4fc01..8b46db35f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1474,6 +1474,34 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t complementary to, not overlapping with, the consolidated-evidence fix's own new log line). - §5.1 below is updated to reflect this. +## 2026-08-30 wakeup: reconciled with concurrent owner fixes; three PRs merged-current and marked ready + +- Confirmed all three open PRs from the previous pass (`contextual-orchestrator#923`, `.github#1438`, + `naruon#1486`) had fallen behind their base branches while this session was investigating the + bytez/preflight incident — `.github`'s `main` in particular had advanced by 6 commits (owner + bypass-merges) covering the exact same incident with much deeper, hosted-run-artifact-backed + evidence than this session's own investigation had. Rather than merge blind, read every one of + those commits' diffs and the doc's own new "sidecar-preflight outage: consolidated evidence" entry + before touching anything — see that entry and the "correction" entry directly below it for the full + reconciliation. Net effect: this session's own `_log_preflight_rejections` visibility fix was + dropped as redundant with what had already shipped; this session's discovery-retry fix + (`contextual-orchestrator#923`) and the doc corrections were kept as independently valid. +- Merged current `main`/`develop` into all three branches as ordinary merge commits (never rebase); + `contextual-orchestrator#923` needed no merge (its `main` had not moved). Full suites re-verified + clean after each merge: `.github` 1897 passed/1 skipped/21 subtests (100% coverage on touched files, + 100% interrogate), `naruon` 1812 passed/32 skipped, `contextual-orchestrator` 2765 passed/1 skipped + (unchanged, no merge needed). +- All three marked ready for review (undrafted) — implementation and local validation are complete; + keeping them in draft only paused the central review pipeline (CodeRabbit skips drafts entirely; + merge automation is explicitly gated off for drafts) with nothing left to gain from staying in that + state. +- `.github#1347` (SSRF/isolation for `sandboxed_web_e2e.py`) deliberately **not** touched this pass: + it is 8+ days stale against a `main` that has independently grown substantial SSRF hardening in the + exact same file (already flagged in an earlier entry above as a same-file, overlapping-logic case + that needs actual semantic reconciliation, not a mechanical merge) — attempting that at the tail end + of an already long pass risked a rushed, wrong resolution more than it risked leaving it one more + cycle. Left for a dedicated next pass. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. @@ -1492,25 +1520,29 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t ### 5.1 이번 루프의 다음 개발 increment -1. ContextualWisdomLab/contextual-orchestrator#923 — discovery-side transient-retry 수정(진짜 root - cause였던 family_cap/gateway-timeout 문제와는 별개의, 독립적인 resilience 개선)의 required - Checks·독립 승인을 재확인하고, 조건 충족 시 merge한다. 병합 후 `.github`의 `ORCHESTRATOR_PIN_SHA`를 - 해당 커밋으로 갱신하는 후속 PR이 필요하다(#1422/#1426이 이미 확립한 패턴과 동일). -2. ContextualWisdomLab/.github#1438 — stderr tail 확장(`SIDECAR_STDERR_TAIL_LINES`) + gap-baseline - correction(Bytez/413 오귀속 정정, family_cap 수정이 진짜 root cause임을 반영)의 required Checks·독립 - 승인을 재확인하고, 조건 충족 시 merge한다. (애초 이 PR에 포함했던 preflight-rejection 가시성 fix는 - `main`에 이미 동등한 기능(`log "sidecar preflight route evidence: ..."`)이 병렬로 병합되어 중복이므로 - 되돌렸다.) -3. ContextualWisdomLab/.github#1347 — web-E2E isolation/SSRF 수정을 current `main`으로 merge-conflict - 해소(ordinary merge commit, no rebase) 후 terminal Checks·독립 승인을 재확인한다. (#1297은 이미 - 병합됨; #1345/#1326은 closed·unmerged로 확인되어 더 이상 후보가 아니다 — 위 2026-08-30 항목 참고.) -4. ContextualWisdomLab/naruon#1486 — 새로 추가된 Noema `check_calendar_conflict` 도구의 naruon 자체 - required Checks(OpenCode/Strix/merge-scheduler)를 current head에서 재확인하고, 조건 충족 시 - merge한다. +1. ContextualWisdomLab/contextual-orchestrator#923 — main과 이미 동기화됨, ready-for-review로 전환 + 완료. discovery-side transient-retry 수정(진짜 root cause였던 family_cap/gateway-timeout 문제와는 + 별개의, 독립적인 resilience 개선)의 required Checks·독립 승인을 재확인하고, 조건 충족 시 merge한다. + 병합 후 `.github`의 `ORCHESTRATOR_PIN_SHA`를 해당 커밋으로 갱신하는 후속 PR이 필요하다(#1422/#1426이 + 이미 확립한 패턴과 동일). +2. ContextualWisdomLab/.github#1438 — main과 이미 동기화됨(6개 owner bypass-merge 반영), ready-for-review로 + 전환 완료. stderr tail 확장(`SIDECAR_STDERR_TAIL_LINES`) + gap-baseline correction(Bytez/413 오귀속 + 정정, family_cap 수정이 진짜 root cause임을 반영)의 required Checks·독립 승인을 재확인하고, 조건 충족 + 시 merge한다. +3. ContextualWisdomLab/naruon#1486 — develop과 이미 동기화됨, ready-for-review로 전환 완료. 새로 추가된 + Noema `check_calendar_conflict` 도구의 naruon 자체 required Checks(OpenCode/Strix/merge-scheduler)를 + current head에서 재확인하고, 조건 충족 시 merge한다. +4. ContextualWisdomLab/.github#1347 — **아직 손대지 않음, 다음 pass 전용 작업.** web-E2E isolation/SSRF + 수정이 8일 이상 stale한 상태이고, 그 사이 `main`이 정확히 같은 파일(`sandboxed_web_e2e.py`)에 독립적인 + SSRF 강화를 상당량 추가했다 — 기계적 merge가 아니라 실제 로직을 읽고 대조하는 작업이 필요하다(위 + 2026-08-30 항목에서도 "same-file, overlapping-logic" 사례로 이미 플래그됨). ordinary merge commit(no + rebase)으로 conflict를 해소하되, 서두르지 말고 전용 pass에서 진행한다. 5. G-01/G-02는 중앙 control-plane merge evidence의 current-head 품질 문제, G-05/G-06는 naruon ecosystem 소비 증거(부분적으로 #1486이 G-06/PRD-02에 기여), G-15는 대용량·미지원 첨부파일 parser - registry의 소유 저장소 PR로 연결한다. 완료된 discovery-side retry 이후에도 preflight warm-up probe - 재시도 여부는 실제 transience 증거(이번 pass의 가시성 개선으로 확보 가능)가 나오기 전까지 보류한다. + registry의 소유 저장소 PR로 연결한다. completion warm-up probe(`proxy_send_once`) 자체의 재시도 + 여부는 이미 merge된 family_cap/gateway-timeout 수정의 실제 hosted-run 결과와, `main`에 이미 병합된 + `log "sidecar preflight route evidence: ..."` 가시성 라인이 향후 축적할 실제 transience 증거가 + 나오기 전까지 보류한다 — 지금 다시 시도하는 것은 추측에 기반한 재작업일 뿐이다. ## 6. Compliance and data boundary From 91064ffb65bbeff0e05e6a80bb6ecf3a0e2d4229 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:52:18 +0000 Subject: [PATCH 06/58] docs: record CodeRabbit 10-star auto-review gate as root cause, not a finding naruon#1486 and contextual-orchestrator#923 both hit a PR-governance metadata-gate block that looked like a blocking CodeRabbit finding but was actually CodeRabbit's own "approval pending, not reviewed yet" state -- every ContextualWisdomLab repo is below CodeRabbit's 10-GitHub-star automatic-review threshold, so it never reviews a new commit without an explicit @coderabbitai review trigger. Triggered both PRs manually; documents the structural gap and a deferred central-automation candidate fix. --- CHANGELOG.md | 9 +++++++++ docs/product-technical-gap-baseline.md | 27 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c95faef8f..00c48ca20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Document a structural CodeRabbit gap in `docs/product-technical-gap-baseline.md` + (2026-08-30 entry): every ContextualWisdomLab repo is below CodeRabbit's + 10-GitHub-star automatic-review threshold, so CodeRabbit never reviews a new + commit without an explicit `@coderabbitai review` trigger comment — this was + surfacing as a `naruon` PR-governance metadata-gate block that looked like a + blocking finding but was actually CodeRabbit's own "not reviewed yet" state. + No code change; a central auto-trigger workflow is a candidate follow-up, + deliberately deferred pending a review of its required-workflow-ruleset + blast radius. - Raise `contextual_orchestrator_review_sidecar.sh`'s `ORCHESTRATOR_CATALOG_FAMILY_CAP` default from 4 to 8: root-caused the live "no provider route passed the Strix plain-chat preflight" outage diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 8b46db35f..fd1458e5c 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1502,6 +1502,33 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t of an already long pass risked a rushed, wrong resolution more than it risked leaving it one more cycle. Left for a dedicated next pass. +## 2026-08-30 CodeRabbit이 조용히 자동 리뷰를 하지 않는 근본 원인: repo star 임계값 + +- `naruon#1486`에서 `github-actions[bot]`의 `pr-governance:metadata-gate` 코멘트가 새 head + (`6a5365ee`)에서 "Current-head CodeRabbit issue comment has blocking warning/failure evidence"로 + 다시 막혔다. 처음엔 CodeRabbit이 실제 finding을 낸 것으로 의심했으나, 코멘트 원문을 다시 읽으니 + CodeRabbit 자신의 "Approval pending — has not reviewed the latest commit yet, check the box to + trigger review" 상태였다 — `docs/development/merge-gate-policy.md`(naruon) 정책상 "current-head + CodeRabbit issue comment has blocking warning/failure evidence"는 정확히 이 미해결 상태(clean + approval도 아니고 rebuttal도 없음)를 fail-closed로 잡아낸 것이었다. 즉 gate는 올바르게 동작했다. +- `contextual-orchestrator#923`의 동일 유형 코멘트(재발행 이벤트로 이 세션에 도착)가 근본 원인을 + 명시했다: "This repository does not receive automatic reviews because it has fewer than 10 stars." + CodeRabbit의 OSS 무료 자동 리뷰 기능은 public repo의 GitHub star 수가 10 미만이면 자동으로 + 트리거되지 않고, PR 코멘트의 체크박스(`🔍 Trigger review`) 또는 `@coderabbitai review` 커맨드로 + 수동 트리거해야만 그 커밋에 대한 리뷰가 실행된다. ContextualWisdomLab 산하 4개 저장소 모두 이 + 임계값 아래이므로, 이 조직의 모든 PR에서 새 커밋마다 동일하게 재발한다 — 특정 PR의 결함이 아니라 + 구조적 gap이다. +- 즉시 조치: `naruon#1486`과 `contextual-orchestrator#923` 양쪽에 `@coderabbitai review` 코멘트를 + 게시해 현재 head의 리뷰를 명시적으로 트리거했다. `.github#1438`은 이 세션에서 아직 CodeRabbit + 코멘트 자체가 관측되지 않아 우선 지켜본다. +- 근본 해결책 후보(아직 미착수, 다음 pass에서 검토): `.github`의 central 필수 workflow에 PR + `opened`/`synchronize`/`ready_for_review` 이벤트마다 `@coderabbitai review` 코멘트를 자동 + 게시하는 얇은 단계를 추가하면 이 수동 트리거가 사라진다. 다만 이는 org-wide required-workflow + ruleset(`CWL Central required workflows`, id `18156473`)에 새 workflow를 등록하는 작업이라 + blast radius가 크다 — 이번 pass에서는 구현하지 않고, 매 PR마다 수동으로 트리거하는 현재 관행을 + 유지하며 후속 pass의 별도 증분으로 남긴다. 대안으로 CodeRabbit 자체의 organization 설정에서 이 + 10-star 게이트를 우회하는 옵션이 있는지 확인하는 것도 병행 검토 대상이다. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. From 0d8be88d81572059d9ac62f6ebf3e1a43c4d8a5f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:53:48 +0000 Subject: [PATCH 07/58] docs: note CodeRabbit rate-limit as a second, independent constraint --- docs/product-technical-gap-baseline.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index fd1458e5c..c446a87bf 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1528,6 +1528,14 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t blast radius가 크다 — 이번 pass에서는 구현하지 않고, 매 PR마다 수동으로 트리거하는 현재 관행을 유지하며 후속 pass의 별도 증분으로 남긴다. 대안으로 CodeRabbit 자체의 organization 설정에서 이 10-star 게이트를 우회하는 옵션이 있는지 확인하는 것도 병행 검토 대상이다. +- **추가 발견 (트리거 직후)**: 두 트리거 코멘트 모두 CodeRabbit이 커맨드 자체는 수락했으나 + ("I will review pull request..."), 곧이어 별도의 "Review limit reached — next included review + available in ~7–31 minutes" 코멘트로 rate-limit에 걸렸다. 즉 이 조직에는 두 개의 독립적인 + CodeRabbit 제약이 겹쳐 있다: (1) 10-star 미만 repo는 애초에 자동 리뷰가 트리거되지 않는 gate, + (2) OSS 무료 티어의 리뷰 횟수 자체가 org 전체(또는 계정 전체)에서 공유되는 rate limit. 10-star + 게이트를 자동화로 우회해도 (2)가 여전히 남아 즉시 리뷰가 실행되지 않을 수 있으므로, 위 "근본 + 해결책 후보"는 재시도/backoff까지 함께 고려해야 완전하다. 이번 pass에서는 재트리거하지 않고 + rate-limit 창(가장 늦은 것 기준 naruon#1486 쪽 31분)이 지나기를 기다린다. ## 5. 실행 루프와 고객의 다음 행동 From ef311a0335896cf917c42300202f520b683ac08a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:54:37 +0000 Subject: [PATCH 08/58] docs: fix cross-repo reference links and scope the CodeRabbit-gap claim Devin review on #1438 flagged two real issues in the CodeRabbit gap-baseline entry: bare naruon#1486/contextual-orchestrator#923 references don't create durable cross-repo links (missing the org prefix), and the claim that "every org PR" is affected outran the evidence (only 4 repos were actually checked). Both fixed. --- docs/product-technical-gap-baseline.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c446a87bf..d6fb890c5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1504,23 +1504,25 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t ## 2026-08-30 CodeRabbit이 조용히 자동 리뷰를 하지 않는 근본 원인: repo star 임계값 -- `naruon#1486`에서 `github-actions[bot]`의 `pr-governance:metadata-gate` 코멘트가 새 head +- `ContextualWisdomLab/naruon#1486`에서 `github-actions[bot]`의 `pr-governance:metadata-gate` 코멘트가 새 head (`6a5365ee`)에서 "Current-head CodeRabbit issue comment has blocking warning/failure evidence"로 다시 막혔다. 처음엔 CodeRabbit이 실제 finding을 낸 것으로 의심했으나, 코멘트 원문을 다시 읽으니 CodeRabbit 자신의 "Approval pending — has not reviewed the latest commit yet, check the box to trigger review" 상태였다 — `docs/development/merge-gate-policy.md`(naruon) 정책상 "current-head CodeRabbit issue comment has blocking warning/failure evidence"는 정확히 이 미해결 상태(clean approval도 아니고 rebuttal도 없음)를 fail-closed로 잡아낸 것이었다. 즉 gate는 올바르게 동작했다. -- `contextual-orchestrator#923`의 동일 유형 코멘트(재발행 이벤트로 이 세션에 도착)가 근본 원인을 - 명시했다: "This repository does not receive automatic reviews because it has fewer than 10 stars." - CodeRabbit의 OSS 무료 자동 리뷰 기능은 public repo의 GitHub star 수가 10 미만이면 자동으로 - 트리거되지 않고, PR 코멘트의 체크박스(`🔍 Trigger review`) 또는 `@coderabbitai review` 커맨드로 - 수동 트리거해야만 그 커밋에 대한 리뷰가 실행된다. ContextualWisdomLab 산하 4개 저장소 모두 이 - 임계값 아래이므로, 이 조직의 모든 PR에서 새 커밋마다 동일하게 재발한다 — 특정 PR의 결함이 아니라 - 구조적 gap이다. -- 즉시 조치: `naruon#1486`과 `contextual-orchestrator#923` 양쪽에 `@coderabbitai review` 코멘트를 - 게시해 현재 head의 리뷰를 명시적으로 트리거했다. `.github#1438`은 이 세션에서 아직 CodeRabbit - 코멘트 자체가 관측되지 않아 우선 지켜본다. +- `ContextualWisdomLab/contextual-orchestrator#923`의 동일 유형 코멘트(재발행 이벤트로 이 세션에 + 도착)가 근본 원인을 명시했다: "This repository does not receive automatic reviews because it has + fewer than 10 stars." CodeRabbit의 OSS 무료 자동 리뷰 기능은 public repo의 GitHub star 수가 10 + 미만이면 자동으로 트리거되지 않고, PR 코멘트의 체크박스(`🔍 Trigger review`) 또는 + `@coderabbitai review` 커맨드로 수동 트리거해야만 그 커밋에 대한 리뷰가 실행된다. 이 세션이 직접 + 확인한 4개 저장소(`ContextualWisdomLab/.github`, `naruon`, `contextual-orchestrator`, `noema`) + 모두 이 임계값 아래다 — 다른 org 저장소까지 전수 확인한 것은 아니므로, 결론은 이 4개 저장소로 + 한정한다. 확인된 4개 저장소 안에서는 새 커밋마다 동일하게 재발하는, 특정 PR의 결함이 아닌 구조적 + gap이다. +- 즉시 조치: `ContextualWisdomLab/naruon#1486`과 `ContextualWisdomLab/contextual-orchestrator#923` + 양쪽에 `@coderabbitai review` 코멘트를 게시해 현재 head의 리뷰를 명시적으로 트리거했다. + `ContextualWisdomLab/.github#1438`도 이어서 같은 코멘트로 트리거했다. - 근본 해결책 후보(아직 미착수, 다음 pass에서 검토): `.github`의 central 필수 workflow에 PR `opened`/`synchronize`/`ready_for_review` 이벤트마다 `@coderabbitai review` 코멘트를 자동 게시하는 얇은 단계를 추가하면 이 수동 트리거가 사라진다. 다만 이는 org-wide required-workflow From c11b68c2e3cb4d14099c0925f4239c1ae13dc674 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 12:01:46 +0000 Subject: [PATCH 09/58] docs: describe ORCHESTRATOR_CATALOG_FAMILY_CAP=8 as a mitigation, not a fix CodeRabbit correctly flagged that "already fixed"/"resolves" overstates the 4->8 family-cap raise: it reduces the odds of the same retired/timed-out candidates being selected every run, but doesn't guarantee against it, and hosted-run confirmation of the fix is still pending. Softened both the changelog bullet and the gap-baseline correction entry to "mitigates ... hosted confirmation remains pending". --- CHANGELOG.md | 5 +++-- docs/product-technical-gap-baseline.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c48ca20..b15d6c9ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,8 +53,9 @@ Semantic Versioning where the repository publishes a release. `is_free`), and the `request_failed status=413` line is the sidecar's own unconditional self-test rather than a live ZDR-prefetch fallback. The incident's actual terminating message (`"review sidecar preflight failed"`, a live warm-up-probe rejection) is the same one the - `ORCHESTRATOR_CATALOG_FAMILY_CAP` fix above resolves — the two corrections converge on the same - real root cause rather than describing two different bugs. Also adds a bounded, one-retry + `ORCHESTRATOR_CATALOG_FAMILY_CAP` fix above mitigates (hosted confirmation remains pending) — the + two corrections converge on the same real root cause rather than describing two different bugs. + Also adds a bounded, one-retry resilience improvement to `contextual-orchestrator`'s provider *discovery* fetch (a different, non-overlapping call site from the family-cap/preflight fix above) in `ContextualWisdomLab/contextual-orchestrator#923`. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index d6fb890c5..b3fc3c8d8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1445,8 +1445,9 @@ candidate selection (`contextual_orchestrator_review_policy.py`'s `family_cap`) reliability signal, and since `nvidia_nim`/`nvidia_nim_sub` was the *only* family populating the free pool, the same 4 alphabetically-first candidates were selected on every single run — 2 of which are NVIDIA-retired model ids returning HTTP 404 **forever**, not transiently, plus 2 that timed out. That is -a deterministic selection defect, not a generic "one random transient failure" story, and it is already -fixed (`ORCHESTRATOR_CATALOG_FAMILY_CAP` raised 4→8) alongside two further, genuinely independent bugs +a deterministic selection defect, not a generic "one random transient failure" story, and it is +mitigated (`ORCHESTRATOR_CATALOG_FAMILY_CAP` raised 4→8; hosted confirmation remains pending) +alongside two further, genuinely independent bugs in the same call path (the gateway smoke-test's own `curl --max-time` was too tight for a real reasoning-model completion, and its `max_tokens` was desynchronized from the launcher's own probe budget) — see that entry for the full evidence trail and reasoning. None of this needed, and this From aef024d43a042795835f35fd34ae216ee337804e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 12:22:27 +0000 Subject: [PATCH 10/58] fix(coverage): mark unreachable pagination-exhaustion raise as dead code _load_changed_files's post-loop PolicyError at the end of the pagination loop can never execute: 31 full 100-item pages would push the file count past 3,000 during page 31's own iteration (30 full pages = exactly 3,000), tripping the len(files) > 3_000 raise inside the loop before the outer range(1, 32) can ever exhaust without an early return or that inner raise. This dead line has been silently failing this repo's org-wide coverage-evidence gate (fail_under=100 on scripts/ci) for every PR reviewed through the central OpenCode/Noema/Strix dispatcher -- confirmed via live Actions logs on multiple unrelated PRs (.github#1161, #1438) all showing "Coverage failure: total of 99 is less than fail-under=100" at this exact line, which in turn blocks opencode-agent from ever posting an APPROVED verdict anywhere in the org. Marked pragma: no cover with a justification, matching this repo's existing convention for provably-unreachable defensive code (see other pragma: no cover sites in scripts/ci/*.py). Verified: full suite 1897 passed, 1 skipped, 21 subtests; coverage TOTAL 100% (9966/9966 statements, 3926/3926 branches); interrogate 100%. --- scripts/ci/pingora_edge_policy.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 648bfd064..1d9fa9f0b 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -342,7 +342,13 @@ def _load_changed_files(api_url: str, repository: str, pull_request: int, token: raise PolicyError("GitHub changed-file pagination exceeded 3,000 files") if len(payload) < 100: return tuple(files) - raise PolicyError("GitHub changed-file pagination exceeded 3,000 files") + raise PolicyError( # pragma: no cover - unreachable: 31 full pages of 100 always + # trip the len(files) > 3_000 raise above during page 31's own iteration + # (30 full pages = exactly 3,000, so page 31's first item always pushes + # past 3,000) before this loop can exhaust its range; kept as a defensive + # invariant in case the page count or per-page size above ever changes. + "GitHub changed-file pagination exceeded 3,000 files" + ) def _load_raw_file_bytes(api_url: str, repository: str, path: str, head_sha: str, token: str, opener: OpenJson) -> bytes: From d167338e63ac4acee23aaaa7e2ab9a1a182a1466 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 12:23:33 +0000 Subject: [PATCH 11/58] docs: record the three-cause OpenCode-outage diagnosis and its fix Consolidates a 5-agent investigation into why opencode-agent had not posted a verdict on any of the three tracked PRs: naruon#1486 is stuck on a stale scheduler thread-count snapshot, contextual-orchestrator#923 is missing a cross-repo dispatch credential in its scheduler run, and .github#1438's dispatch ran but was blocked by the pingora_edge_policy.py coverage bug fixed in the preceding commit. Also records hosted-run confirmation that the earlier family_cap sidecar mitigation is now working (3/3 post-fix runs clean), while flagging a separate, still-open "healthz passes then completion request hangs" signature on the same commit. --- CHANGELOG.md | 11 +++++ docs/product-technical-gap-baseline.md | 59 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b15d6c9ca..f6f303550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Fix `scripts/ci/pingora_edge_policy.py`'s `_load_changed_files`: the + post-loop pagination-exhaustion `PolicyError` was unreachable dead code + (proven: 31 full 100-item pages always trip the in-loop `len(files) > + 3_000` raise during page 31's own iteration before the loop can exhaust + its range), and it was silently failing this repo's org-wide + `coverage-evidence` gate (`fail_under=100`) for every PR reviewed through + the central OpenCode/Noema/Strix dispatcher, blocking opencode-agent from + ever posting an APPROVED verdict anywhere in the org. Marked + `# pragma: no cover` with justification, matching this repo's existing + convention. See the 2026-08-30 "OpenCode Agent 자체 문제" gap-baseline + entry for the full three-PR diagnosis this came out of. - Document a structural CodeRabbit gap in `docs/product-technical-gap-baseline.md` (2026-08-30 entry): every ContextualWisdomLab repo is below CodeRabbit's 10-GitHub-star automatic-review threshold, so CodeRabbit never reviews a new diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b3fc3c8d8..acdd1f4f7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1540,6 +1540,65 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t 해결책 후보"는 재시도/backoff까지 함께 고려해야 완전하다. 이번 pass에서는 재트리거하지 않고 rate-limit 창(가장 늦은 것 기준 naruon#1486 쪽 31분)이 지나기를 기다린다. +## 2026-08-30 "OpenCode Agent 자체 문제" 진단: 세 PR이 서로 다른 3가지 원인으로 막혀 있었다 + +- 운영자 직접 질의("OpenCode Agent 자체에 문제가 있는 듯")에 대응해 4갈래 병렬 조사(디스패치 + 메커니즘 코드 분석, GitHub Actions 실행 이력, 조직 전체 리뷰 증거, 공유 게이트웨이 상태) + + 종합진단 5-agent Workflow를 실행했다. 결론: **단일 공통 장애가 아니라, 세 PR이 각기 다른 + 이유로 opencode-agent의 dispatch 단계에 도달하지 못하거나(2건) 도달은 했지만 근본 원인이 + 다른 버그로 막혀 있었다(1건)**. "async dispatch를 기다리는 중"이라는 이전 프레이밍은 두 PR에 + 대해서는 틀렸다 — dispatch 자체가 시도된 적이 없었다. + - `ContextualWisdomLab/naruon#1486`: 스케줄러(`scan-pr-queue`, 11:41Z 실행)가 + `{"action":"block","reason":"2 unresolved review thread(s)"}`로 dispatch를 보류했다. + 실제로는 이 세션이 이미 그 시점 이전에 모든 review thread를 resolve했으므로 stale한 + 스냅샷이었을 가능성이 높다 — 다음 스케줄러 tick(이벤트 기반 `scan-pr-queue` 또는 15분 + 주기 `org-queue-sweep`)에서 자동 해소되어야 한다. + - `ContextualWisdomLab/contextual-orchestrator#923`: `pr_review_merge_scheduler.py`의 + `inspect_pr()`가 OpenCode dispatch를 Strix evidence 뒤에 순서화하는데(Strix가 + `"completed"`가 아니면 OpenCode를 아예 호출하지 않음), 이 PR의 스케줄러 실행(11:49Z)이 + 동시에 `"this scheduler run has no cross-repository repository-dispatch credential"`을 + 로그에 남겼다 — 다른 저장소(`contextual-orchestrator`)에서 트리거된 스케줄러 컨텍스트가 + `.github`로의 cross-repo dispatch에 필요한 자격 증명을 갖지 못한 것으로 보인다. 이것이 + 비밀 값 만료/누락(사람이 로테이션해야 함)인지, 아니면 workflow 배선 누락(코드로 고칠 수 + 있음)인지는 아직 미확인 — 다음 pass에서 `.github/workflows/pr-review-merge-scheduler.yml`의 + cross-repo 토큰 배선을 직접 확인해야 한다. + - `ContextualWisdomLab/.github#1438`: dispatch는 실제로 실행되었다(run `33310753001`, + 12:09:57Z 트리거). 하지만 이 저장소 자신의 `coverage-evidence` job이 + `scripts/ci/pingora_edge_policy.py`의 `_load_changed_files` 함수 끝의 방어적 post-loop + `raise`(당시 345번째 줄)에서 커버리지 미달로 실패했다 — `Coverage failure: total of 99 is + less than fail-under=100`. 이 job의 실패는 `.github`를 통해 리뷰되는 **모든** 대상 저장소의 + approval을 막는다(`opencode-review-dispatch.yml`이 "Coverage evidence did not pass; + approval is blocked"라고 명시). 동일한 정확한 실패가 완전히 무관한 다른 PR(`.github#1161`, + 11:31Z run)에서도 재현되어, PR별 결함이 아니라 `main`에 이미 존재하는 구조적 결함임을 + 확인했다. +- **근본 원인 분석과 수정**: `_load_changed_files`는 `for page in range(1, 32)`(최대 31페이지, + page당 최대 100개)로 변경 파일을 페이지네이션하며, 루프 안에서 `len(files) > 3_000`이면 즉시 + raise한다. 30페이지 모두 정확히 100개씩 반환되면 정확히 3,000개(초과 아님, raise 없음)가 + 누적되고, 이어지는 31번째(마지막) 페이지가 100개 미만이면 조기 `return`, 정확히 100개이면 그 + 첫 항목을 추가하는 순간 3,001 > 3,000이 되어 루프 **안**의 raise가 먼저 발동한다. 즉 루프가 + break/return/raise 없이 정상적으로 31회를 모두 소진해 루프 **밖**의 post-loop raise에 도달할 + 수 있는 입력은 수학적으로 존재하지 않는다 — 순수한 방어적 죽은 코드였다. 이 저장소는 + `# pragma: no cover`를 이미 다른 곳에서(`scripts/ci/*.py`의 `if __name__ == "__main__":`, + `contextual_orchestrator_review_launcher.py`의 provider 예외 등) 근거 주석과 함께 쓰는 + 확립된 관례가 있으므로, 동일한 관례로 이 줄에 이유를 설명하는 pragma를 추가했다(코드 삭제가 + 아니라 유지 — 향후 page 개수·per_page 값이 바뀔 경우를 대비한 방어적 불변식이므로). + 검증: `coverage run -m pytest tests` → 1897 passed, 1 skipped, 21 subtests; `coverage report` + → TOTAL 9966/9966 statements, 3926/3926 branches, **100%**; `interrogate` → 100.0%. +- **hosted 확인**: 별도로, 이전 "family_cap 4→8 완화, hosted confirmation 보류" 항목의 hosted + 확인도 이번 조사에서 확보되었다 — family_cap 수정(`e36a1f71`, 2026-08-30T10:46:01Z 병합) 이후 + 베이스를 사용한 3건의 실제 hosted run(`.github#1161`/`#1438`/`#1448`) 모두 sidecar가 healthz+ + provider-route preflight를 통과했고, 그 이전 베이스를 쓴 1건은 정확히 문서화된 pre-fix 서명 + 그대로 실패했다. **family_cap 결정론적 결함은 해결된 것으로 확인**(표본 3건, load-sensitive + provider timeout/429/502 가설은 아직 미검증). 단, 이 확인과는 별개로, 같은 커밋(`c11b68c2`)에서 + noema-review와 strix가 "healthz 통과 후 실제 completion 요청이 120초 타임아웃으로 0바이트 + 응답"이라는 다른 실패 시그니처를 보였다 — family_cap과는 다른, 아직 미해결인 별도 문제로 다음 + pass에서 추적한다(위 "완화, hosted confirmation 보류" 항목의 새 하위 이슈로 취급). +- **다음 행동**: (1) 이 커밋 병합 후 `.github`를 통해 리뷰되는 모든 PR의 `coverage-evidence`가 + 회복되는지 재확인, (2) naruon#1486은 스케줄러의 다음 tick을 기다리거나 필요시 + `repository_dispatch`로 수동 재트리거, (3) contextual-orchestrator#923의 cross-repo + dispatch 자격 증명 배선을 직접 확인, (4) org-wide 15분 주기 cron이 07:03Z 이후 ~5시간 + 공백이 있었다는 조사 결과(별도의 신뢰성 회귀)도 다음 pass에서 조사한다. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. From 3c89f431c8c4fb5c345dce98156902f129d73a7c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 12:25:43 +0000 Subject: [PATCH 12/58] docs: pin the exact contextual-orchestrator cross-repo credential root cause --- docs/product-technical-gap-baseline.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index acdd1f4f7..4036ae920 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1557,11 +1557,20 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t `inspect_pr()`가 OpenCode dispatch를 Strix evidence 뒤에 순서화하는데(Strix가 `"completed"`가 아니면 OpenCode를 아예 호출하지 않음), 이 PR의 스케줄러 실행(11:49Z)이 동시에 `"this scheduler run has no cross-repository repository-dispatch credential"`을 - 로그에 남겼다 — 다른 저장소(`contextual-orchestrator`)에서 트리거된 스케줄러 컨텍스트가 - `.github`로의 cross-repo dispatch에 필요한 자격 증명을 갖지 못한 것으로 보인다. 이것이 - 비밀 값 만료/누락(사람이 로테이션해야 함)인지, 아니면 workflow 배선 누락(코드로 고칠 수 - 있음)인지는 아직 미확인 — 다음 pass에서 `.github/workflows/pr-review-merge-scheduler.yml`의 - cross-repo 토큰 배선을 직접 확인해야 한다. + 로그에 남겼다 — **원인을 정확히 특정했다**: `pr-review-merge-scheduler.yml`(501/798행)은 + `SCHEDULER_ALLOW_CROSS_REPO_REPOSITORY_DISPATCH`를 + `(secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '')`로만 `true`로 + 설정한다. 요구된 워크플로우 ruleset이 이 스케줄러를 "각 대상 저장소의 컨텍스트에서" 실행하므로 + (`GITHUB_REPOSITORY`가 대상 저장소가 됨), 이 두 secret 중 하나가 **`contextual-orchestrator` + 저장소(또는 조직) 레벨에 설정되어 있지 않으면** 이 플래그는 항상 `false`가 되고 + `repository_dispatch_wait_reason()`(`pr_review_merge_scheduler.py:680-701`)은 + `execution_repo == dispatch_repo`(스케줄러가 `.github` 자신의 컨텍스트에서 실행될 때만 + 참)도 만족하지 못하므로 항상 wait를 반환한다 — 즉 이 두 secret 중 하나가 + `contextual-orchestrator`에 없는 한 이 저장소의 어떤 PR도 영원히 이 gate를 통과하지 못한다. + 이것은 코드로 고칠 수 없다 — **사람이 organization 또는 `contextual-orchestrator` repository + 설정에서 `PR_REVIEW_MERGE_TOKEN` 또는 `OPENCODE_APPROVE_TOKEN` secret이 실제로 존재하고 + 유효한지 확인해야 한다.** (PR #939는 제목만 비슷할 뿐 실제로는 Strix/Inkspan scanner + 오탐·uv materialization에 관한 무관한 작업이므로, 겹치는 범위가 아님을 확인했다.) - `ContextualWisdomLab/.github#1438`: dispatch는 실제로 실행되었다(run `33310753001`, 12:09:57Z 트리거). 하지만 이 저장소 자신의 `coverage-evidence` job이 `scripts/ci/pingora_edge_policy.py`의 `_load_changed_files` 함수 끝의 방어적 post-loop From 7074ab25cd37cea9061de68bc663db73671f6136 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 12:37:54 +0000 Subject: [PATCH 13/58] docs: correct the "permanently blocked" overclaim; record the second contextual-orchestrator fix Devin review correctly flagged that the contextual-orchestrator#923 cross-repo credential analysis ignored org-queue-sweep, which runs in .github's own execution context on a 15-minute cron and has its own independent path past the same wait-reason check regardless of contextual-orchestrator's own secrets. Softened the claim accordingly. Also notes the self-referential circularity where .github#1438's own coverage fix can't help anything (including itself) until merged to main, and records the newly-found and fixed contextual-orchestrator stream_options/tools passthrough bug. --- docs/product-technical-gap-baseline.md | 40 ++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 4036ae920..ad9910234 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1565,12 +1565,22 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t 저장소(또는 조직) 레벨에 설정되어 있지 않으면** 이 플래그는 항상 `false`가 되고 `repository_dispatch_wait_reason()`(`pr_review_merge_scheduler.py:680-701`)은 `execution_repo == dispatch_repo`(스케줄러가 `.github` 자신의 컨텍스트에서 실행될 때만 - 참)도 만족하지 못하므로 항상 wait를 반환한다 — 즉 이 두 secret 중 하나가 - `contextual-orchestrator`에 없는 한 이 저장소의 어떤 PR도 영원히 이 gate를 통과하지 못한다. - 이것은 코드로 고칠 수 없다 — **사람이 organization 또는 `contextual-orchestrator` repository - 설정에서 `PR_REVIEW_MERGE_TOKEN` 또는 `OPENCODE_APPROVE_TOKEN` secret이 실제로 존재하고 - 유효한지 확인해야 한다.** (PR #939는 제목만 비슷할 뿐 실제로는 Strix/Inkspan scanner - 오탐·uv materialization에 관한 무관한 작업이므로, 겹치는 범위가 아님을 확인했다.) + 참)도 만족하지 못하므로, **이벤트 기반 `scan-pr-queue`(대상 저장소 컨텍스트에서 실행)만 놓고 + 보면** 이 두 secret 중 하나가 `contextual-orchestrator`에 없는 한 항상 wait를 반환한다. + **정정(Devin review 지적)**: "어떤 PR도 영원히 통과하지 못한다"는 표현은 과장이었다 — + 같은 파일의 `org-queue-sweep` job(574행)은 `if: github.repository == + 'ContextualWisdomLab/.github'`로 게이트되어 15분 주기 cron으로 **`.github` 자신의 + 컨텍스트에서** 조직 전체 저장소를 훑는 fallback이다. 이 경로에서는 + `execution_repo == dispatch_repo`가 항상 참(둘 다 `.github`)이므로 + `repository_dispatch_wait_reason()`의 두 번째 escape hatch를 만족해 `contextual-orchestrator` + 자체의 secret 유무와 무관하게 dispatch를 시도할 수 있다. 즉 이벤트 기반 경로는 확실히 + 막혀 있지만, 15분 주기 org-wide sweep은 별도의, 아마도 정상 동작하는 경로다 — + `contextual-orchestrator#923`이 "영원히" 막힌 것이 아니라 event-driven 경로만 막히고 + scheduled fallback의 다음 tick을 기다리는 상태일 가능성이 높다. 그럼에도 + `PR_REVIEW_MERGE_TOKEN`/`OPENCODE_APPROVE_TOKEN` secret 확인 자체는 여전히 유효한 + 후속 조치다 — 사람이 organization 또는 `contextual-orchestrator` repository 설정에서 + 확인해야 한다. (PR #939는 제목만 비슷할 뿐 실제로는 Strix/Inkspan scanner 오탐·uv + materialization에 관한 무관한 작업이므로, 겹치는 범위가 아님을 확인했다.) - `ContextualWisdomLab/.github#1438`: dispatch는 실제로 실행되었다(run `33310753001`, 12:09:57Z 트리거). 하지만 이 저장소 자신의 `coverage-evidence` job이 `scripts/ci/pingora_edge_policy.py`의 `_load_changed_files` 함수 끝의 방어적 post-loop @@ -1607,6 +1617,24 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t `repository_dispatch`로 수동 재트리거, (3) contextual-orchestrator#923의 cross-repo dispatch 자격 증명 배선을 직접 확인, (4) org-wide 15분 주기 cron이 07:03Z 이후 ~5시간 공백이 있었다는 조사 결과(별도의 신뢰성 회귀)도 다음 pass에서 조사한다. +- **순환 의존 주의**: `.github#1438` 자신도 이 pingora_edge_policy.py 수정 없이는 (다른 모든 PR과 + 마찬가지로) OpenCode approval을 받을 수 없다 — 그런데 그 수정 자체가 아직 `#1438`의 **병합되지 + 않은 PR 브랜치**에만 있고 `main`에는 없으므로, `#1438`을 리뷰하는 `coverage-evidence` job도 + 여전히 `main` 기준 코드로 실행되어 같은 이유로 실패한다(즉 이 수정은 자기 자신을 아직 + 구제하지 못한다 — `main`에 병합된 뒤에야 조직 전체에 효과가 발생한다). `org-queue-sweep`이 + `.github` 자신의 컨텍스트에서 도는 것과는 별개로, 이 특정 순환은 사람의 개입(관리자 병합 또는 + 동등한 경로)이 필요할 수 있다 — 코드만으로는 스스로를 풀 수 없는 경우다. +- **추가 발견 (사용자 직접 지적)**: `contextual-orchestrator`의 Strix 실행에서도 별도의, 진짜 + 내부 로직 버그를 발견해 수정했다 — `ContextualWisdomLab/contextual-orchestrator`의 + `server.py`가 `/v1/chat/completions`에서 `tools`가 있을 때 `stream_options.include_usage=true` + 조합을 무조건 400으로 거부하고 있었는데, 실제로는 하위의 `_chat_response_sse_chunks`가 이미 + tool_calls delta와 정직하게 라벨링된(reported/estimated) usage chunk를 완전히 지원하는 + 코드였다 — 즉 존재하지 않는 제약을 이유로 이미 동작하는 조합을 막고 있던, 순수한 자체 + 버그였다. Strix의 `openai-agents` SDK가 tools와 함께 이 조합을 항상 보내므로, 이 저장소를 + 경유하는 모든 Strix 실행이 (sidecar preflight 통과 여부와 무관하게) 이 지점에서 결정론적으로 + 실패하고 있었다. `response_format`만 있는 multi-agent "conduct" 경로는 (aggregate usage + 추적이 아직 구현되지 않아) 여전히 fail-closed 상태로 남겨두었다. 수정·테스트 갱신·전체 스위트 + 검증 후 `contextual-orchestrator#923`에 병합했다. ## 5. 실행 루프와 고객의 다음 행동 From 11d5a079e97522ed903f03a1ffccfd1b68e36241 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 12:43:29 +0000 Subject: [PATCH 14/58] docs: correct two more overclaims Devin caught in the diagnosis entry 1. The "circular dependency" claim was wrong: opencode-review-dispatch.yml's coverage-evidence job merges PR_BASE_SHA with the PR's own current PR_HEAD_SHA before measuring coverage, so #1438's own pragma fix (already on its head) should clear its own coverage-evidence on the next dispatch without needing to merge to main first. Only other PRs' coverage-evidence still needs the main merge. 2. The org-queue-sweep "likely-working" fallback claim was overstated: pr-review-merge-scheduler.yml:820-825 shows it has its own hard credential guard (exit 1 if neither PR_REVIEW_MERGE_TOKEN/OPENCODE_APPROVE_TOKEN nor an OpenCode app token exchange is available) -- it moves the credential requirement to the .github/org level rather than eliminating it. Whether contextual-orchestrator#923 actually gets rescued by it is unverified, not "likely". --- docs/product-technical-gap-baseline.md | 39 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ad9910234..2db8190dc 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1574,13 +1574,20 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t `execution_repo == dispatch_repo`가 항상 참(둘 다 `.github`)이므로 `repository_dispatch_wait_reason()`의 두 번째 escape hatch를 만족해 `contextual-orchestrator` 자체의 secret 유무와 무관하게 dispatch를 시도할 수 있다. 즉 이벤트 기반 경로는 확실히 - 막혀 있지만, 15분 주기 org-wide sweep은 별도의, 아마도 정상 동작하는 경로다 — - `contextual-orchestrator#923`이 "영원히" 막힌 것이 아니라 event-driven 경로만 막히고 - scheduled fallback의 다음 tick을 기다리는 상태일 가능성이 높다. 그럼에도 - `PR_REVIEW_MERGE_TOKEN`/`OPENCODE_APPROVE_TOKEN` secret 확인 자체는 여전히 유효한 - 후속 조치다 — 사람이 organization 또는 `contextual-orchestrator` repository 설정에서 - 확인해야 한다. (PR #939는 제목만 비슷할 뿐 실제로는 Strix/Inkspan scanner 오탐·uv - materialization에 관한 무관한 작업이므로, 겹치는 범위가 아님을 확인했다.) + 막혀 있지만, 15분 주기 org-wide sweep은 별도 경로다. **추가 정정(Devin review 재지적)**: + "아마도 정상 동작"이라는 표현도 과도했다 — `pr-review-merge-scheduler.yml:820-825`를 직접 + 읽으면 `org-queue-sweep` 자신도 `SCHEDULER_MUTATION_TOKEN_SOURCE == "github-token"`(즉 + `PR_REVIEW_MERGE_TOKEN`/`OPENCODE_APPROVE_TOKEN` secret도, OpenCode app 토큰 교환도 전혀 + 없을 때)이면 `exit 1`로 즉시 전체 실패한다 — "credential availability와 무관한 독립 경로"가 + 아니라, **같은 종류의 secret을 `contextual-orchestrator` 저장소가 아니라 `.github` + 저장소(또는 조직) 레벨에서 요구하는 것으로 요구 위치만 옮겨진 것**이다. 따라서 + `contextual-orchestrator#923`이 이 fallback으로 실제 구제되는지는 `.github`/조직 레벨에 + `PR_REVIEW_MERGE_TOKEN`/`OPENCODE_APPROVE_TOKEN`(또는 유효한 OpenCode app 토큰 교환)이 + 설정되어 있는지에 전적으로 달려 있다 — 이 세션은 secret 값을 읽을 권한이 없어 이를 검증할 + 수 없다. "likely-working"이 아니라 "미검증, 조건부"로 정정한다. 사람이 organization 또는 + `.github`/`contextual-orchestrator` repository 설정에서 확인해야 한다. (PR #939는 제목만 + 비슷할 뿐 실제로는 Strix/Inkspan scanner 오탐·uv materialization에 관한 무관한 작업이므로, + 겹치는 범위가 아님을 확인했다.) - `ContextualWisdomLab/.github#1438`: dispatch는 실제로 실행되었다(run `33310753001`, 12:09:57Z 트리거). 하지만 이 저장소 자신의 `coverage-evidence` job이 `scripts/ci/pingora_edge_policy.py`의 `_load_changed_files` 함수 끝의 방어적 post-loop @@ -1617,13 +1624,17 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t `repository_dispatch`로 수동 재트리거, (3) contextual-orchestrator#923의 cross-repo dispatch 자격 증명 배선을 직접 확인, (4) org-wide 15분 주기 cron이 07:03Z 이후 ~5시간 공백이 있었다는 조사 결과(별도의 신뢰성 회귀)도 다음 pass에서 조사한다. -- **순환 의존 주의**: `.github#1438` 자신도 이 pingora_edge_policy.py 수정 없이는 (다른 모든 PR과 - 마찬가지로) OpenCode approval을 받을 수 없다 — 그런데 그 수정 자체가 아직 `#1438`의 **병합되지 - 않은 PR 브랜치**에만 있고 `main`에는 없으므로, `#1438`을 리뷰하는 `coverage-evidence` job도 - 여전히 `main` 기준 코드로 실행되어 같은 이유로 실패한다(즉 이 수정은 자기 자신을 아직 - 구제하지 못한다 — `main`에 병합된 뒤에야 조직 전체에 효과가 발생한다). `org-queue-sweep`이 - `.github` 자신의 컨텍스트에서 도는 것과는 별개로, 이 특정 순환은 사람의 개입(관리자 병합 또는 - 동등한 경로)이 필요할 수 있다 — 코드만으로는 스스로를 풀 수 없는 경우다. +- **정정 (Devin review 지적, 순환 의존 주장은 틀렸음)**: 처음에는 "이 수정이 `main`에 병합되기 + 전까지는 `#1438` 자신도 구제받지 못하는 순환 의존"이라고 썼으나, 틀렸다. + `opencode-review-dispatch.yml:303-355`("Materialize pull request merge tree for coverage + measurement")를 직접 읽으면 `coverage-evidence`는 `PR_BASE_SHA`(=`main`)를 checkout한 뒤 + **PR의 현재 `PR_HEAD_SHA`를 그 위에 merge**해서 커버리지를 측정한다 — `PR_HEAD_SHA`는 + dispatch 시점의 PR 실제 head이므로, 이 pragma 수정이 이미 `#1438`의 head에 포함되어 있는 한 + 다음 dispatch부터 `#1438` 자신의 `coverage-evidence`는 (아직 `main`에 병합되기 전이라도) + 회복되어야 한다. 순환 의존은 없다 — `main`에 병합해야만 효과가 생기는 것은 **다른** PR들 + (naruon#1486, contextual-orchestrator#923 등, 이들 자신의 diff는 pingora_edge_policy.py를 + 건드리지 않으므로)의 coverage-evidence뿐이다. 사람의 개입(관리자 병합)이 필요하다는 주장도 + 철회한다 — `#1438`은 다음 dispatch에서 스스로 통과할 가능성이 높다. - **추가 발견 (사용자 직접 지적)**: `contextual-orchestrator`의 Strix 실행에서도 별도의, 진짜 내부 로직 버그를 발견해 수정했다 — `ContextualWisdomLab/contextual-orchestrator`의 `server.py`가 `/v1/chat/completions`에서 `tools`가 있을 때 `stream_options.include_usage=true` From 73459977cf4653c7e6654681d9be759173c8b770 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 12:50:51 +0000 Subject: [PATCH 15/58] docs: record merge-conflict resolution outcome and observed Strix-fix effect --- docs/product-technical-gap-baseline.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 65e5097f6..0748bae1e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1690,7 +1690,21 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t 설명했을 수 있다. naruon과 contextual-orchestrator는 자기 브랜치가 아니라 `.github`의 `main`에서 중앙 워크플로우를 매 dispatch 시점에 새로 가져오므로(trusted source ref), 이 두 PR은 **자기 브랜치를 건드리지 않고도** 다음 dispatch부터 이 fix의 혜택을 받을 수 있다. -- 병합 후 전체 스위트 재검증 결과는 아래에 기록한다(진행 중). +- 병합 커밋(`c55620fc`) 검증: 전체 스위트 1898 passed/1 skipped/21 subtests, coverage TOTAL + 9966/9966 statements·3926/3926 branches **100%**, interrogate **100%**. 푸시 완료 — + `.github#1438`의 `mergeable_state`가 `dirty`(conflict)에서 `blocked`(required Checks/리뷰 + 대기, 정상)로 돌아왔다. +- **관찰**: 이 병합 직후 `contextual-orchestrator#923`의 `noema-review`가 `success`로 전환되었고 + (이전에 봤던 "healthz 통과 후 completion이 120초간 행" 시그니처가 이번에는 재현되지 않음), + `strix`도 (이전처럼 즉시 provider-unavailable로 실패하는 대신) 실제로 스캔을 진행 중이다 — + `702392a2`(Strix SDK streaming 비활성화 workaround)가 실제로 유효하게 작동하고 있다는 + 직접 증거다. `opencode-review`는 여전히 실패 상태이지만 이는 Strix가 아직 완료 전이라 + scheduler가 dispatch를 순서화하며 기다리는, 이미 알려진 정상 대기 상태다. +- `.github#1347`(SSRF/isolation)은 이번 pass에서도 손대지 않았다 — 별도 브랜치 + (`fix/sandboxed-web-e2e-isolation-clean`, `main` 대비 8월 26일 이후로 stale, `mergeable_state: + dirty`)이며 실제 로직 대조가 필요한 전용 pass 대상이므로, 이미 상당한 시간을 투입한 이번 + pass에 무리해서 끼워넣지 않고 명시적으로 다음 pass로 넘긴다. naruon G-06/G-15도 동일한 + 이유로 이번 pass에서는 착수하지 못했다 — 다음 pass의 최우선 항목으로 남긴다. ## 5. 실행 루프와 고객의 다음 행동 From d19c8cae9572173a0c0b1bb2b563ccc1898f913a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:01:57 +0000 Subject: [PATCH 16/58] docs: record this pass's recheck + G-06/G-15/#1347 research kickoff Reconfirmed the three tracked PRs' opencode-review failures are the known async-verdict wait state, not a defect. Re-ran .github#1438's noema-review once after the sidecar's gateway-preflight hit the known 120s/0-byte hang signature again (shared review infra, not this PR's diff). Kicked off parallel research for naruon G-06's next increment, G-15's next increment, and .github#1347's SSRF merge conflict; results land in a follow-up entry. --- docs/product-technical-gap-baseline.md | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0748bae1e..0ab44b4c7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1706,6 +1706,37 @@ entry's fix operates one layer earlier, on *which* candidates are ever offered t pass에 무리해서 끼워넣지 않고 명시적으로 다음 pass로 넘긴다. naruon G-06/G-15도 동일한 이유로 이번 pass에서는 착수하지 못했다 — 다음 pass의 최우선 항목으로 남긴다. +## 2026-08-30 시간별 재개: 세 PR 재확인 + G-06/G-15/#1347 병행 조사 착수 + +세 PR(`naruon#1486`, `.github#1438`, `contextual-orchestrator#923`)의 required Checks를 +재확인했다. 공통 결론: 세 PR 모두 `opencode-review`가 실패 중이지만, 이는 코드 결함이 아니라 +현재 head에서 opencode-agent의 APPROVED/CHANGES_REQUESTED verdict가 아직 게시되지 않은, +이미 알려진 정상 비동기 대기 상태다(`.github#1438`은 구 head `c11b68c2`에서 받은 +`COMMENTED`(coverage gate가 그 시점에 실패해 opencode-agent 스스로 승인을 보류한 상태)만 +있고, 새 head `73459977`에 대한 verdict는 아직 없다; 나머지 두 PR은 아직 어떤 verdict도 없다). +`naruon#1486`의 `metadata-only gate evaluation` 실패도 동일하게 `opencode-review` 실패의 +하위 파생 결과일 뿐이다. + +`.github#1438`의 `noema-review`에서 이전에 문서화된 "healthz는 통과하지만 실제 completion +요청이 120초간 0바이트로 행"하는 시그니처가 재현됐다(`request_failed +status=413`/`provider_discovery_failed provider=bytez` 이후 healthz+preflight는 31초에 +확인됐으나, 이어진 `orchestrator/free` 전체 gateway preflight 요청이 `curl --max-time 120` +한계에서 0바이트로 타임아웃). 이 PR의 diff와 무관한 공유 리뷰 인프라(무료 티어 NVIDIA NIM +provider의 지연/부하 변동)로 판단해, 근거 없는 재작업 대신 governance 규칙에 따라 실패한 +job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재실행 결과는 다음 tick에서 +확인한다. + +병행해서 다음 3개 조사를 백그라운드 에이전트로 착수했다(결과는 다음 항목에서 반영): +1. naruon G-06 다음 증분 — thread/sender ontology 및 human-correction 슬라이스 중 어느 쪽이 + naruon의 기존 코드 관례(opaque `*_uid`, 구조화 Alembic, deny-first RBAC/ABAC) 위에서 + 가장 작고 실질적인 다음 조각인지 정찰. +2. naruon G-15 다음 증분 — 현재 첨부파일 1MB 상한의 실제 위치, 기존 parser/registry 유무, + streaming upload 여부, quarantine/zip-bomb 방어 유무를 정찰해 가장 작은 실질적 슬라이스를 + 특정. +3. `.github#1347`(SSRF/isolation) — PR 브랜치와 `main`이 독립적으로 각각 추가한 + `scripts/ci/sandboxed_web_e2e.py`의 SSRF 방어 로직을 정확히 대조하고, ordinary merge + commit(no rebase)으로 결합할 정확한 hunk별 해소안을 정찰. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. From 380ee20cecbf07e404b3e4eb9fba9fb8319b4331 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:21:09 +0000 Subject: [PATCH 17/58] docs: record G-06 shipment, #1347 conflict resolution, and G-15 scoping naruon G-06's human-correction slice shipped (naruon#1486). .github#1347's SSRF/isolation merge conflict resolved via ordinary merge commit, including reconciling with a concurrent session that had already pushed an equivalent resolution to the same branch (adopted their CHANGELOG wording and dropped a harmless duplicate test assertion; both sessions independently found and fixed the same empty-readiness-URL regression). G-15 scoped but not yet implemented. --- docs/product-technical-gap-baseline.md | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0ab44b4c7..518ddc1d1 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1737,6 +1737,45 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 `scripts/ci/sandboxed_web_e2e.py`의 SSRF 방어 로직을 정확히 대조하고, ordinary merge commit(no rebase)으로 결합할 정확한 hunk별 해소안을 정찰. +## 2026-08-30 시간별 재개: G-06 증분 배포 + `.github#1347` conflict 해소(동시 작업 병합 포함) + +세 배경 조사(위 §5.1)가 모두 완료되어 다음을 실행했다. + +- **naruon G-06 증분 배포**: 조사 결론(사람 정정 슬라이스가 sender ontology보다 작고 실질적인 + 다음 조각)에 따라, `evaluate_calendar_conflicts`의 결정을 `calendar_conflict_judgments` + 테이블에 판단(judgment)으로 영속화하고 `project_graph_corrections`와 동일한 before/after + 감사 흔적 패턴으로 사람이 그 판단을 정정할 수 있는 API 3개 + (`POST /judgments`, `GET /judgments`, `POST /judgments/{uid}/corrections`)를 + `naruon`에 추가했다(Alembic `0018_calendar_conflict_judgments`, 구조화 op). `/evaluate` + 자체의 무상태 계약은 바꾸지 않았다. 검증: 신규 테스트 8 passed, 전체 백엔드 스위트 1821 + passed/32 skipped(신규 skip 없음), ruff clean, `alembic heads`가 단일 head로 수렴. `naruon#1486`에 + 같은 브랜치로 push했다(#1486은 이미 이 세션이 연 PR이라 새 커밋이 자동으로 같은 PR에 반영됨). +- **`.github#1347`(SSRF/isolation) conflict 해소**: PR 브랜치를 로컬에 체크아웃해 `origin/main`을 + merge하니 사전 조사대로 정확히 3개 파일(`CHANGELOG.md`, `scripts/ci/sandboxed_web_e2e.py`, + `tests/test_sandboxed_web_e2e.py`)에서 충돌했다. `main`의 `require_loopback_readiness_url` + 계열(DNS-rebind 방지, userinfo 거부, IPv4-mapped IPv6 unwrap)을 정본으로 채택하고 PR + 브랜치의 bubblewrap isolation 코드와 "서비스 시작 전에 조기 실패"하는 `main()` 흐름은 그대로 + 유지했다. 이 과정에서 실제 회귀를 하나 발견해 직접 고쳤다: `main()`의 조기 검증 호출부는 + `wait_for_url`과 달리 빈 문자열 URL을 건너뛰는 가드가 없어, `--backend-ready-url`/ + `--frontend-ready-url`(기본값 `""`, "readiness 체크 없음"을 의미하는 흔한 경우)을 그대로 + 넘기면 `require_loopback_readiness_url("")`이 항상 실패하는 회귀가 생길 뻔했다 — 호출부에 + `if args.backend_ready_url:` 가드를 추가해 막았다. + **동시 작업 충돌**: 이 merge를 push하려는 순간, 같은 PR 브랜치에 이미 다른 세션이 정확히 + 동일한 `origin/main` merge를 독립적으로 수행해 먼저 push했음을 발견했다(동일한 3개 파일 + conflict, 동일 시각대). 지시문의 "동시 remote-agent 커밋을 경쟁으로 취급해 force-push하지 + 않는다"에 따라, 그 원격 커밋을 로컬에 merge해 재조정했다: `scripts/ci/sandboxed_web_e2e.py`는 + 두 세션의 해소가 완전히 동일해 자동 merge됐고(빈 문자열 가드 fix 포함, 서로 다른 세션이 같은 + 회귀를 각자 발견해 같은 방식으로 고쳤음을 확인) — `CHANGELOG.md`는 상대 세션이 쓴 더 완결된 + 단일 문단을 채택했으며, `tests/test_sandboxed_web_e2e.py`의 사소한 중복 assertion 2줄은 + 상대 세션 쪽(중복 없는 버전)을 채택했다. 재검증: PR 자체 명시 테스트 113 passed, 전체 스위트 + 1912 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%. Push 완료 — + `mergeable_state`가 `dirty`에서 `blocked`(required Checks/리뷰 대기, 정상)로 전환됨을 확인했다. +- **naruon G-15**: 이번 pass에서는 정찰만 완료(현재 1MB/20MB/64MB로 흩어진 상한 위치, 이미 + 존재하는 MIME 키 parser registry(`_PARSER_MANIFEST`), zip-bomb 방어가 첨부 경로에는 전혀 + 없음을 확인). 가장 작은 실질적 슬라이스로 "MIME sniffing + 불일치 시 명시적 + quarantine 상태 + `attachment_uid` 부여 + reparse-intent API"를 특정했으나, 아직 구현하지 + 않았다 — 다음 pass 최우선. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. From 4336e5088867c062a2eea6c21b6c819a3720e860 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:29:23 +0000 Subject: [PATCH 18/58] docs: record Devin/code-quality review fixes on naruon#1486's judgment API All 6 review threads addressed (5 fixed, 1 explained as a pre-existing repo-wide Postgres-test-coverage gap) and resolved. --- docs/product-technical-gap-baseline.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 518ddc1d1..a01952245 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1775,6 +1775,19 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 없음을 확인). 가장 작은 실질적 슬라이스로 "MIME sniffing + 불일치 시 명시적 quarantine 상태 + `attachment_uid` 부여 + reparse-intent API"를 특정했으나, 아직 구현하지 않았다 — 다음 pass 최우선. +- **추가**: `naruon#1486`에 push한 직후 Devin Review가 새 `calendar_conflict_judgment_service.py`에 + 대해 5건, github-code-quality가 1건을 지적했다(6개 unresolved review thread, PR governance + metadata gate 차단). 모두 검증 후 실제로 고쳤다: (1) `apply_correction`이 대상 judgment 행을 + `SELECT ... FOR UPDATE`로 잠가 동시 정정 경쟁을 막음, (2) `decision_code`를 바꾸는 정정은 + `reason_code`/`recommended_action`도 함께 교체해(`corrected_by_human_review` + rationale) + 서로 다른 결정의 필드가 섞인 응답을 방지(원본은 `before_json`에 보존), (3) `list_judgments`에 + 200건 상한 추가, (4) `MAX_EXISTING_COMMITMENTS`를 `services/calendar_conflict_policy.py`의 + 공유 상수로 통합해 `api/calendar_conflicts.py`/`noema_agent.py`가 서로 어긋날 수 없게 함, (5) + 테스트 파일의 이중 import 스타일 정리. 유일하게 고치지 않은 지적("PostgreSQL persistence + remains unverified")은 이 세션에 Postgres 접근이 없어 `test_project_graph_api.py`의 기존 + Postgres-스킵 스모크 테스트와 동일한 한계임을 코멘트로 남기고 resolve했다. 6개 thread 모두 + 코멘트+resolve 완료. 검증: 신규 테스트 4개 추가, 전체 백엔드 스위트 1825 passed/32 skipped, + ruff clean. ## 5. 실행 루프와 고객의 다음 행동 From 0dcf77906cfa978564d16100785ef2487cd25bb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:31:23 +0000 Subject: [PATCH 19/58] =?UTF-8?q?docs(gaps):=20fix=20stale=20=C2=A75.1=20n?= =?UTF-8?q?ext-increment=20ledger=20(Devin=20review=20on=20#1438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devin correctly flagged that §5.1 still labeled .github#1347 as "not yet touched" after it had actually been resolved, and didn't reflect that naruon#1486 now ships G-06's full judgment/correction increment (not just a partial contribution). Updated items 3-5 to match current reality. --- docs/product-technical-gap-baseline.md | 36 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a01952245..239cedba4 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1816,20 +1816,28 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 전환 완료. stderr tail 확장(`SIDECAR_STDERR_TAIL_LINES`) + gap-baseline correction(Bytez/413 오귀속 정정, family_cap 수정이 진짜 root cause임을 반영)의 required Checks·독립 승인을 재확인하고, 조건 충족 시 merge한다. -3. ContextualWisdomLab/naruon#1486 — develop과 이미 동기화됨, ready-for-review로 전환 완료. 새로 추가된 - Noema `check_calendar_conflict` 도구의 naruon 자체 required Checks(OpenCode/Strix/merge-scheduler)를 - current head에서 재확인하고, 조건 충족 시 merge한다. -4. ContextualWisdomLab/.github#1347 — **아직 손대지 않음, 다음 pass 전용 작업.** web-E2E isolation/SSRF - 수정이 8일 이상 stale한 상태이고, 그 사이 `main`이 정확히 같은 파일(`sandboxed_web_e2e.py`)에 독립적인 - SSRF 강화를 상당량 추가했다 — 기계적 merge가 아니라 실제 로직을 읽고 대조하는 작업이 필요하다(위 - 2026-08-30 항목에서도 "same-file, overlapping-logic" 사례로 이미 플래그됨). ordinary merge commit(no - rebase)으로 conflict를 해소하되, 서두르지 말고 전용 pass에서 진행한다. -5. G-01/G-02는 중앙 control-plane merge evidence의 current-head 품질 문제, G-05/G-06는 naruon - ecosystem 소비 증거(부분적으로 #1486이 G-06/PRD-02에 기여), G-15는 대용량·미지원 첨부파일 parser - registry의 소유 저장소 PR로 연결한다. completion warm-up probe(`proxy_send_once`) 자체의 재시도 - 여부는 이미 merge된 family_cap/gateway-timeout 수정의 실제 hosted-run 결과와, `main`에 이미 병합된 - `log "sidecar preflight route evidence: ..."` 가시성 라인이 향후 축적할 실제 transience 증거가 - 나오기 전까지 보류한다 — 지금 다시 시도하는 것은 추측에 기반한 재작업일 뿐이다. +3. ContextualWisdomLab/naruon#1486 — develop과 이미 동기화됨, ready-for-review로 전환 완료. `check_calendar_conflict` + 도구 위에 G-06의 human-correction 슬라이스(judgment 영속화 + 정정 API 3개, Alembic `0018`)를 추가 + 배포했고, 그 위에 도착한 Devin Review 5건 + github-code-quality 1건을 모두 실제로 고치고 6개 thread를 + 전부 코멘트+resolve했다(row lock, decision/reason/action 일관성, list 상한, 공유 상수 통합, import 정리). + naruon 자체 required Checks(OpenCode/Strix/merge-scheduler)를 current head(`7c20155f`)에서 재확인하고, + 조건 충족 시 merge한다. +4. ContextualWisdomLab/.github#1347 — **conflict 해소 완료** (더 이상 "아직 손대지 않음"이 아니다). web-E2E + isolation/SSRF 수정을 `main`과 merge해 정확히 예상된 3개 파일 충돌을 해소했고(main의 DNS-rebind 방지 + validator 채택 + PR의 bubblewrap isolation 유지), 그 과정에서 실제 회귀(빈 readiness URL 처리 누락)도 + 고쳤다. push 직전 다른 세션이 이미 동일한 merge를 독립적으로 push한 것을 발견해 force-push 없이 + 재조정했다(위 2026-08-30 "G-06 증분 배포 + `.github#1347` conflict 해소" 항목 참조). 현재 + `mergeable_state`는 `dirty`가 아니라 `blocked`(required Checks/리뷰 대기, 나머지 세 PR과 동일한 정상 + 상태)다. required Checks·독립 승인을 재확인하고 조건 충족 시 merge한다. +5. G-01/G-02는 중앙 control-plane merge evidence의 current-head 품질 문제다. G-06은 `#1486`이 이제 + `check_calendar_conflict`(temporal commitment/conflict) + judgment/correction API(human correction) + 두 다리를 모두 갖춘 실질적 증분을 배포했다 — 남은 것은 thread/sender ontology 다리뿐이다. G-15(대용량· + 미지원 첨부파일 parser registry)는 정찰만 끝났고(MIME sniffing + quarantine status + `attachment_uid` + + reparse-intent API로 슬라이스 특정) 아직 구현하지 않았다 — 다음 pass 최우선 구현 대상이다. + completion warm-up probe(`proxy_send_once`) 자체의 재시도 여부는 이미 merge된 family_cap/gateway-timeout + 수정의 실제 hosted-run 결과와, `main`에 이미 병합된 `log "sidecar preflight route evidence: ..."` + 가시성 라인이 향후 축적할 실제 transience 증거가 나오기 전까지 보류한다 — 지금 다시 시도하는 것은 + 추측에 기반한 재작업일 뿐이다. ## 6. Compliance and data boundary From a1c2ba50820eddcc7208c1d881e1dc12903fb386 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:43:15 +0000 Subject: [PATCH 20/58] docs: record naruon#1486's second Devin review round (workspace_id security fix) Documents the workspace_id authorization-boundary fix and the other 5 findings from Devin's second pass on the judgment/correction API. --- docs/product-technical-gap-baseline.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 239cedba4..a5ad06ee9 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1788,6 +1788,26 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 Postgres-스킵 스모크 테스트와 동일한 한계임을 코멘트로 남기고 resolve했다. 6개 thread 모두 코멘트+resolve 완료. 검증: 신규 테스트 4개 추가, 전체 백엔드 스위트 1825 passed/32 skipped, ruff clean. +- **추가(2차 Devin Review, 보안 finding 포함)**: 위 fix가 push되자 Devin이 같은 head에 6건을 + 더 지적했다. 가장 중요한 것은 **[보안, 최우선]** "workspace 경계를 넘어 판단을 열람·정정할 + 수 있다"는 finding이었다 — `calendar_conflict_judgments`/`corrections`가 `user_id`+ + `organization_id`만으로 범위를 제한하고 `workspace_id`를 빠뜨렸는데, `AuthContext.workspace_id`는 + 세션 토큰의 독립 claim(`api/auth.py`의 `_required_string_claim(payload, "workspace")`)이라 + 테스트 스텁만 편의상 user_id/org에서 파생할 뿐, 실제로는 동일 user_id+organization_id가 + 서로 다른 workspace를 오갈 수 있어 실제 인가 우회였다. 검증 후 `naruon`의 기존 + `project_graph` 모듈이 이미 확립한 workspace_id 스코핑 관례를 그대로 따라 두 테이블·모든 + scoped 쿼리·API 4개 경로에 `workspace_id`를 추가했다(Alembic `0018`은 아직 어떤 DB에도 + 적용되지 않은 이번 PR 자체 마이그레이션이라 새 마이그레이션 대신 직접 수정). 나머지 5건도 + 모두 고쳤다: `list_judgments`의 200건 상한 이후 접근 불가 문제는 전체 페이지네이션 대신 + `GET /judgments/{judgment_uid}` 단건 조회로, correction rationale이 recommended_action으로 + 둔갑하는 문제는 `calendar_conflict_policy.py`에 새로 추가한 + `default_recommended_action()`(정책 자체의 단일 소스, `evaluate_calendar_conflicts`도 재사용)로, + status_code/decision_code 모순 조합은 API 모델 validator + 서비스 계층 이중 검증으로, + ICS 파서의 별도 500건 하드코딩은 공유 상수로, Noema 도구의 스킵된 행 개수 미공개는 + `skipped_existing_count` 필드 추가로 해소했다. 6개 thread 모두 코멘트+resolve 완료. 검증: + 전체 백엔드 스위트 1835 passed/32 skipped(무관한 process-group 타이밍 테스트 1건이 전체 + 스위트 동시 실행에서만 간헐적으로 실패, 단독 실행 시 통과 확인 — 이번 변경과 무관), ruff + clean, `alembic heads` 단일 head 유지. push 완료(86f4bd9b). ## 5. 실행 루프와 고객의 다음 행동 From c2013d023050020dbd30ce7b28aafdb9e5409013 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:53:52 +0000 Subject: [PATCH 21/58] =?UTF-8?q?docs:=20refresh=20naruon#1486=20=C2=A75.1?= =?UTF-8?q?=20pointer=20to=20current=20head=20after=20full=20review=20cycl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devin flagged (.github#1438) that §5.1 item 3 still pointed verification at naruon#1486's stale 7c20155f head after three more review rounds (workspace_id security fix, no-op-override rationale fix, doctoring doc self-contradiction fix) landed on top of it. Updated to the current a5cebe53 head and the full 17/17 resolved-thread count. --- docs/product-technical-gap-baseline.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a5ad06ee9..6e3990ebc 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1837,11 +1837,14 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 정정, family_cap 수정이 진짜 root cause임을 반영)의 required Checks·독립 승인을 재확인하고, 조건 충족 시 merge한다. 3. ContextualWisdomLab/naruon#1486 — develop과 이미 동기화됨, ready-for-review로 전환 완료. `check_calendar_conflict` - 도구 위에 G-06의 human-correction 슬라이스(judgment 영속화 + 정정 API 3개, Alembic `0018`)를 추가 - 배포했고, 그 위에 도착한 Devin Review 5건 + github-code-quality 1건을 모두 실제로 고치고 6개 thread를 - 전부 코멘트+resolve했다(row lock, decision/reason/action 일관성, list 상한, 공유 상수 통합, import 정리). - naruon 자체 required Checks(OpenCode/Strix/merge-scheduler)를 current head(`7c20155f`)에서 재확인하고, - 조건 충족 시 merge한다. + 도구 위에 G-06의 human-correction 슬라이스(judgment 영속화 + 정정 API 4개 — 단건 조회 `GET + /judgments/{judgment_uid}` 포함, Alembic `0018`)를 추가 배포했고, 이후 여러 라운드에 걸쳐 도착한 Devin + Review 전건(1차 6건 + 2차 6건 — workspace_id 인가 우회 보안 수정 포함 + 3차 no-op override rationale + 보존 수정 + 4차 doctoring 문서 최신화/자기모순 정정) + github-code-quality 1건을 모두 실제로 고치고 + review thread 17/17을 전부 코멘트+resolve했다(row lock, decision/reason/action 일관성, workspace_id + 스코핑, list 상한 + 단건 조회 우회, 공유 상수 통합, import 정리, doctoring 문서 동기화). naruon 자체 + required Checks(OpenCode/Strix/merge-scheduler)를 current head(`a5cebe53`)에서 재확인하고, 조건 충족 + 시 merge한다. 4. ContextualWisdomLab/.github#1347 — **conflict 해소 완료** (더 이상 "아직 손대지 않음"이 아니다). web-E2E isolation/SSRF 수정을 `main`과 merge해 정확히 예상된 3개 파일 충돌을 해소했고(main의 DNS-rebind 방지 validator 채택 + PR의 bubblewrap isolation 유지), 그 과정에서 실제 회귀(빈 readiness URL 처리 누락)도 From 8469fb9518061fa147d4ffb65cb4902e3fecf985 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 13:59:26 +0000 Subject: [PATCH 22/58] docs: log this pass's 4-PR recheck and .github#1347/G-15 work launch --- docs/product-technical-gap-baseline.md | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6e3990ebc..ace24b31f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1862,6 +1862,40 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 가시성 라인이 향후 축적할 실제 transience 증거가 나오기 전까지 보류한다 — 지금 다시 시도하는 것은 추측에 기반한 재작업일 뿐이다. +## 2026-08-30 시간별 재개: 4개 PR 재확인 + `.github#1347` 실제 수정 착수 + G-15 착수 + +네 PR(`naruon#1486`, `.github#1438`, `contextual-orchestrator#923`, `.github#1347`)의 현재 head에서 +`get_check_runs`/`get_reviews`/`get_review_comments`를 전부 다시 읽었다. 결과: + +- `naruon#1486`(head `a5cebe53`): review thread 17/17 resolved. `opencode-review`와 + `metadata-only gate evaluation` 둘 다 `failure`이지만, 이 head에 대한 opencode-agent verdict가 + 아직 게시되지 않은 것뿐(같은 head에 대한 Devin/CodeRabbit/OpenCode 코멘트가 전무) — 이미 문서화된 + 비동기 대기 패턴이지 코드 결함이 아니다. Merge 조건 미충족, 다음 pass에서 재확인. +- `.github#1438`(head `a1c2ba50`): review thread 1건만 unresolved — Devin이 `86f4bd9b` push 이후 + §5.1 item 3이 여전히 `naruon#1486`의 stale head `7c20155f`를 가리킨다고 지적(정확한 지적). 그 사이 + `naruon#1486`은 두 라운드(no-op override 수정, doctoring 문서 자기모순 정정)를 더 거쳐 `a5cebe53`까지 + 진행한 상태였다. §5.1 item 3을 현재 head(`a5cebe53`)와 전체 review 이력(1차 6건 + 2차 6건[workspace_id + 보안 수정 포함] + 3차 no-op override 수정 + 4차 문서 정정, thread 17/17 resolved)으로 재작성해 커밋 + `c2013d02`로 push. `PYTHONPATH=. pytest tests/test_product_technical_gap_baseline.py` 5 passed로 + contract 유지 확인. +- `contextual-orchestrator#923`(head `eb453448`): review thread 5/5 resolved, 남은 건 없음. + `opencode-review`만 동일한 비동기 대기 패턴으로 `failure`. +- `.github#1347`(head `6ed44666`): review thread 25개 중 **6개가 unresolved** — Devin의 최신 라운드 + (commit `7ac8298b`)가 남긴 findings로, 그중 하나는 🟥 최고 심각도(**workspace 내 symlink가 파일시스템 + isolation을 우회할 수 있음** — 호스트 경로를 가리키는 repo 내 symlink가 sandbox로 복사되는 워킹 카피에 + 살아있는 채로 남아, 샌드박스 명령이 그 symlink를 따라가 sandbox 밖 호스트 파일을 읽거나 쓸 수 있다는 + 주장), 나머지는 🟡🟡🟨(malformed readiness port가 검증을 우회, bwrap이 PATH에는 있지만 실제 namespace + 생성 권한이 없는 host를 오분류, `isolated_command`가 `shutil.which`로 못 찾은 실행 파일을 검증 없이 + 통과시킴) + 📝 info 2건. 이전 pass에서 "conflict 해소 완료"로 기록했던 것은 main과의 3파일 merge + conflict였을 뿐, 이번 6건은 그 이후 새 Devin 라운드가 실제 코드에 대해 제기한 별개의 주장들이다 — 아직 + 검증도 수정도 하지 않은 상태였다. 이번 pass에서 이 6건을 현재 코드 기준으로 직접 검증하고 실제 결함만 + 최소 범위로 고치는 백그라운드 에이전트를 별도로 기동했다(worktree 격리, `fix/sandboxed-web-e2e-isolation-clean` + 브랜치, 6개 thread 각각에 회신+resolve, 기존 SSRF/isolation 테스트 재실행 후 push). 결과는 다음 pass에서 + 커밋/thread 상태로 확인한다. +- naruon G-15(첨부파일 parser registry) 실제 구현에 착수 — 기존 `Attachment` 모델, `_PARSER_MANIFEST`, + 산재한 1MB/20MB/64MB 상한 위치, ADR 번호 체계를 먼저 정찰하는 탐색 에이전트를 병행 기동했다. 결과는 + 다음 pass에서 실제 코드 증분으로 이어간다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From f3c1b29e8f5d70f71ee826a4d21553570a3ed06b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 14:00:53 +0000 Subject: [PATCH 23/58] chore: gitignore local .claude/ tool state (agent worktrees) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b98cb1f1d..c65dfa11e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ .coverage .pytest_cache/ .codegraph/ +.claude/ From c1c70af4f1ac552a7ebedfb769272372cb87263d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 14:13:59 +0000 Subject: [PATCH 24/58] docs: log G-15 first-slice shipment in naruon#1486 --- docs/product-technical-gap-baseline.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ace24b31f..3b35d1c4e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1892,9 +1892,23 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 최소 범위로 고치는 백그라운드 에이전트를 별도로 기동했다(worktree 격리, `fix/sandboxed-web-e2e-isolation-clean` 브랜치, 6개 thread 각각에 회신+resolve, 기존 SSRF/isolation 테스트 재실행 후 push). 결과는 다음 pass에서 커밋/thread 상태로 확인한다. -- naruon G-15(첨부파일 parser registry) 실제 구현에 착수 — 기존 `Attachment` 모델, `_PARSER_MANIFEST`, - 산재한 1MB/20MB/64MB 상한 위치, ADR 번호 체계를 먼저 정찰하는 탐색 에이전트를 병행 기동했다. 결과는 - 다음 pass에서 실제 코드 증분으로 이어간다. +- **naruon G-15 첫 슬라이스를 실제로 배포했다** (`naruon#1486`의 같은 브랜치에 push, 커밋 `ee83effd`). + 정찰 에이전트가 확인한 사실(`Attachment`에 opaque id 부재, `_PARSER_MANIFEST`가 튜플 기반 정적 + 디스크립터, 상한이 1MB/20MB/64MB 세 곳에 흩어져 있으나 각각 다른 게이트, quarantine 개념 전무, + Alembic 최신 head `0018`, `docs/adr/`가 0001-0004까지 존재)를 바탕으로 구현: (1) + `services/attachment_parser.py`가 첨부파일의 실제 바이트를 알려진 매직 바이트(PDF/PNG/JPEG/GIF/ZIP)로 + 스니핑해 선언된/추론된 content_type과 다르면 파싱·보류·unsupported 분류 대신 + `parse_status=parse_error_code="content_type_mismatch_quarantined"`으로 격리(원본 바이트는 기존 + 20MB 상한 재사용해 base64 보존, 새 컬럼 없이 기존 `content_type`/`parse_content_type` 두 컬럼 + 비교만으로 declared-vs-actual을 드러냄), (2) `Attachment.attachment_uid` 오파크 id 추가(Alembic + `0019_attachment_uid`, 기존 행 백필하는 구조적 마이그레이션), (3) `POST + /api/data/attachments/{attachment_uid}/reparse-intent`가 quarantine된 첨부파일을 + `reparse_pending`으로 전환하는 intent만 기록(기존 hwp-conversion-intent/pdf-dom-recognition-intent와 + 동일 패턴 — 실제 재파싱 워커는 별도 후속 슬라이스로 명시적으로 미룸), (4) + `docs/adr/0005-attachment-content-type-quarantine.md` 신설 + README 색인 갱신. 검증: 신규 테스트 + 8개(파서 5 + API 3) 추가, 전체 백엔드 스위트 1842 passed/33 skipped(회귀 없음), ruff clean, + `alembic heads`가 `0019_attachment_uid` 단일 head로 수렴. 다음 슬라이스 후보: `reparse_pending`을 + 실제로 소비하는 워커, HWP/HWPX 지원, 단일 첨부파일 upload-accept 상한(현재 부재). ## 6. Compliance and data boundary From b64f825bed127bd892011195093eff122956043a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 14:37:30 +0000 Subject: [PATCH 25/58] docs: record .github#1347 completion (28/28 threads resolved) --- docs/product-technical-gap-baseline.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 3b35d1c4e..a3765d094 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1890,8 +1890,21 @@ job을 1회만 재실행했다(`rerun_failed_jobs`, run `33312587048`) — 재 conflict였을 뿐, 이번 6건은 그 이후 새 Devin 라운드가 실제 코드에 대해 제기한 별개의 주장들이다 — 아직 검증도 수정도 하지 않은 상태였다. 이번 pass에서 이 6건을 현재 코드 기준으로 직접 검증하고 실제 결함만 최소 범위로 고치는 백그라운드 에이전트를 별도로 기동했다(worktree 격리, `fix/sandboxed-web-e2e-isolation-clean` - 브랜치, 6개 thread 각각에 회신+resolve, 기존 SSRF/isolation 테스트 재실행 후 push). 결과는 다음 pass에서 - 커밋/thread 상태로 확인한다. + 브랜치, 6개 thread 각각에 회신+resolve, 기존 SSRF/isolation 테스트 재실행 후 push). + **완료 및 검증 결과**: 6건 중 4건 실재(malformed port, capability probe 부재, `isolated_command`의 + unresolved-executable 우회, **workspace symlink escape — 단 `--isolation required` 경로가 아니라 + `sandboxed_verify.py`/`--isolation disabled` 경로에서 실재. bwrap 필수 경로에 대한 최초 "재현 불가" + 판단은 정확했지만 그 경로 하나만 봤다는 게 놓친 부분이었다**), 2건은 확인 후 변경 불필요. 동시에 진행 중이던 + 다른 세션의 겹치는 수정(`c01c1aa2`)을 발견해 강제 push 없이 `git merge`로 재조정(`4088430a`). 그 직후 + 같은 파일에 대한 새 Devin 라운드가 3건을 추가로 남겼다(probe가 실제 `isolated_command`보다 적은 연산만 + 검증, 🟥 "sandbox가 로그·자격증명을 노출" — 후자는 로그/scrubbed home의 쓰기 가능 mount 자체는 의도된 + 설계이지만 repo checkout이 우연히 갖고 있을 수 있는 자격증명 파일이 그대로 복사되는 것은 실재 결함이었음). + 이 3건도 검증 후 실제로 고쳤다(probe가 `--new-session`/`/tmp`/실제 mount point로의 bind+chdir까지 진짜 + 임시 디렉터리로 재현하도록 확장, `copy_workspace` 기본 제외 목록에 `.env*`/`.netrc`/`.npmrc`/`.ssh`/`.aws` + 등 자격증명 경로 추가, 커밋 `bde444d4`). Push 직전 또 다른 동시 세션의 겹치는 수정(symlink 순환 탐지를 + `resolve(strict=True)`로 강화)을 발견해 다시 `git merge`로 재조정(`cb25974c`). review thread 28/28 + 전부 코멘트+resolve 완료. 검증: 전체 스위트 1930 passed/1 skipped/21 subtests, coverage 100% + (`sandboxed_verify.py` 120/120, `sandboxed_web_e2e.py` 282/282), interrogate 100%, ruff clean. - **naruon G-15 첫 슬라이스를 실제로 배포했다** (`naruon#1486`의 같은 브랜치에 push, 커밋 `ee83effd`). 정찰 에이전트가 확인한 사실(`Attachment`에 opaque id 부재, `_PARSER_MANIFEST`가 튜플 기반 정적 디스크립터, 상한이 1MB/20MB/64MB 세 곳에 흩어져 있으나 각각 다른 게이트, quarantine 개념 전무, From 5ee913ce42ba9806bc9bf8c4f1b356620d991749 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 14:45:21 +0000 Subject: [PATCH 26/58] docs: log the two main-sync merge conflicts resolved this pass --- docs/product-technical-gap-baseline.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1cb13fd87..e784301b1 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1985,6 +1985,14 @@ accidental redundancy (`docs/adr/0002-product-technical-gap-baseline.md`: this d operational snapshot" and "live PR metadata inventory," a distinct role from the ADR's settled design record and the CHANGELOG's terse pointer entries, not a duplicate of either). +**추가 (2026-08-30, 같은 시간별 재개 안):** `main`이 위 sidecar-preflight ADR-0005(#1449)를 병합해 +`6ffd8f8a`로 전진하면서 `.github#1347`(`fix/sandboxed-web-e2e-isolation-clean`)과 +`.github#1438`(공유 task 브랜치) 둘 다 `mergeable_state`가 `dirty`로 전환됐다. 두 곳 모두 충돌은 +`CHANGELOG.md`/`docs/product-technical-gap-baseline.md`에 각자 독립적으로 추가한 인접 항목뿐이었다 +(같은 줄을 편집한 실제 충돌 아님) — 양쪽 다 유지하는 통상적 merge commit으로 해소했다 +(`.github#1347`: 커밋 `583af50b`, 전체 스위트 1930 passed 재확인; task 브랜치/`.github#1438`: 커밋 +`c76e5a24`, 전체 스위트 1898 passed 재확인). Force-push 없음. + ## 5. 실행 루프와 고객의 다음 행동 각 hourly pass는 아래 순서를 유지한다. From 19080a56c271f624c024d241765d3fb1937e6a09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:01:26 +0000 Subject: [PATCH 27/58] docs: log the second main-sync round and naruon#1486's follow-up Devin/CodeRabbit fixes --- docs/product-technical-gap-baseline.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 82867e49e..a178fd615 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1993,6 +1993,25 @@ record and the CHANGELOG's terse pointer entries, not a duplicate of either). (`.github#1347`: 커밋 `583af50b`, 전체 스위트 1930 passed 재확인; task 브랜치/`.github#1438`: 커밋 `c76e5a24`, 전체 스위트 1898 passed 재확인). Force-push 없음. +**추가 2**: 다른 세션이 `.github#1347`에 symlink-escape 가드를 두 라운드 더 강화했다(Devin이 +`resolve(strict=True)`가 `DEFAULT_IGNORE`로 제외된 멀쩡한 dangling symlink까지 오탐한다고 지적 → +`os.readlink`+`os.path.normpath` 기반 hop-by-hop lexical walk로 교체한 `be77d299`, 그 walk의 +off-by-one을 고친 `fe237c4f` — 정확히 N-hop인 정상 체인이 잘못 거부되던 문제, 1934 passed로 검증). +그 직후 `main`이 다시 전진해(`1ff82682`, ADR-0005의 실제 diagnostic/bounded-retry preflight 구현) +`.github#1347`과 task 브랜치/`.github#1438` 둘 다 재차 dirty가 됐다 — 이번에도 CHANGELOG/gap-baseline의 +인접 항목 추가일 뿐이라 양쪽 유지하는 통상 merge로 해소(`.github#1347`: `46fdc2d7`, 1967 passed; +task 브랜치/`.github#1438`: `6a74c672`, 1931 passed). Force-push 없음. 같은 패스에서 naruon#1486에 +새로 도착한 Devin 6건 + CodeRabbit 2건도 검증했다: 실재 결함 3건을 고쳤다 — (1) DOCX/XLSX/PPTX 등 +ZIP 기반 컨테이너 형식이 ZIP 매직 바이트와 일치한다는 이유만으로 quarantine되던 오탐(ZIP 컨테이너 +계열 MIME 부분 문자열 판정으로 제외), (2) 상한 초과로 바이트를 보존 못한 mismatch가 여전히 +reparse-intent가 수락하는 quarantine 상태를 받던 문제(다른 초과-크기 첨부와 동일하게 +parse_size_limit_exceeded로 전환), (3) `apply_correction`의 status_code/decision_code 검증이 +텍스트 전용 ValueError였던 것을 `error_code` 속성을 가진 타입으로 교체(현재 REST 경로는 Literal +타입으로 이미 막혀 있어 방어적 일관성 확보 목적). 🟥 보안 지적(`_get_scoped_attachment`가 +workspace_id를 검증하지 않음)은 실재하지만 `Email` 모델 자체가 애초에 workspace_id가 없다는 +저장소 전반의 기존 gap임을 확인해 조용히 임시방편을 넣는 대신 ADR에 후속 작업으로 명시했다. +review thread 25/25 코멘트+resolve 완료(커밋 `dcc9fcd0`, 전체 백엔드 스위트 1845 passed). + - **Implemented** (`scripts/ci/contextual_orchestrator_review_launcher.py`, `scripts/ci/contextual_orchestrator_review_sidecar.sh`): Layer 1's `_preflight_review_agents` now probes each candidate at a new `REVIEW_PREFLIGHT_BASE_TOKENS = 16`, escalating that same candidate From c9b54080433da64a584ffa414270afcfe1f80e56 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:15:03 +0000 Subject: [PATCH 28/58] docs(gaps): fold in naruon#1486's dcc9fcd0 round + confirm async-wait diagnosis on all 4 PRs naruon#1486 advanced to dcc9fcd0 (25/25 threads, 3 real fixes: OOXML quarantine false-positive, oversized-mismatch reparse, calendar error_code convention) since this section was last updated at a5cebe53 (17/17). CodeRabbit's stale CHANGES_REQUESTED (against ee83effd) is explained: one of its two findings is already fixed, the other (reparse_pending worker) was already a documented ADR-0005 follow-up. Re-confirmed via job logs that all four open PRs (naruon#1486, .github#1438, contextual-orchestrator#923, .github#1347) are blocked only on the same known non-actionable pattern: opencode-review exits 1 because no authenticated dispatch verdict has posted for that exact head yet, not a merge conflict or code defect. .github#1438 had 3 more Devin threads this pass (a test-coverage-shape info note, two purely informational confirmations) -- verified and resolved. .github#1347 has a new Devin round under active triage in a background agent; its outcome will fold into this same entry once complete, per this session's practice of extending entries rather than proliferating new sections. --- docs/product-technical-gap-baseline.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a178fd615..06b5f2caf 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2311,6 +2311,26 @@ signature as the original round-4 bug) before passing after the fix. 1930 tests `alembic heads`가 `0019_attachment_uid` 단일 head로 수렴. 다음 슬라이스 후보: `reparse_pending`을 실제로 소비하는 워커, HWP/HWPX 지원, 단일 첨부파일 upload-accept 상한(현재 부재). +**추가**: 이후 `naruon#1486`에 Devin 6건 + CodeRabbit 2건이 더 도착해 실재 결함 3건을 고쳐 커밋 +`dcc9fcd0`로 push했다(전체 스위트 1845 passed/33 skipped, review thread 25/25 코멘트+resolve) — (1) +DOCX/XLSX/PPTX 등 ZIP 기반 컨테이너 형식이 ZIP 매직 바이트와 일치한다는 이유만으로 quarantine되던 +오탐(ZIP 컨테이너 계열 MIME 부분 문자열 판정으로 제외), (2) 상한 초과로 바이트를 보존 못한 mismatch가 +여전히 reparse-intent가 수락하는 quarantine 상태를 받던 문제(다른 초과-크기 첨부와 동일하게 +`parse_size_limit_exceeded`로 전환), (3) `apply_correction`의 status_code/decision_code 검증이 +텍스트 전용 `ValueError`였던 것을 `error_code` 속성을 가진 타입(`CalendarConflictUnsupportedValueError`) +으로 교체. CodeRabbit의 `CHANGES_REQUESTED`(이전 head `ee83effd` 대상)가 지적한 2건 중 후자는 이렇게 +고쳤고, 전자(quarantine된 첨부파일을 실제로 재처리하는 worker 부재)는 위 "다음 슬라이스 후보"에 이미 +있던 바로 그 gap — ADR-0005에 알려진 후속 작업으로 명시된 채로 유지, 임시방편 없이 그대로 둔다. +CodeRabbit이 rate limit 이후 `dcc9fcd0`에 새 pass를 돌리면 review decision이 자동 갱신될 것으로 예상. +네 PR 모두 이번 pass에서 `get_check_runs`/job 로그로 재확인: `mergeable_state`는 전부 `blocked`이고, +원인은 전부 동일한 이미 문서화된 패턴(`opencode-review` job이 "No APPROVED or CHANGES_REQUESTED from +opencode-agent on the current head"로 exit 1 — 그 head에 대한 authenticated dispatch verdict이 아직 +게시되지 않은 것뿐, merge conflict나 코드 결함이 아님). `.github#1438`에는 Devin이 새로 3건(테스트가 +텍스트만 검사한다는 지적 — 이 계약 테스트 파일 전체가 원래 텍스트 기반 검증이라 이 PR이 새로 도입한 +격차가 아님, 순수 정보성 확인 2건)을 남겨 전부 검증 후 코멘트+resolve했다. `.github#1347`에는 또 새 +Devin 라운드가 도착해 실제 코드 대조 검증을 전담 백그라운드 에이전트에 위임했다(진행 중, 다음 항목에서 +결과를 기록한다). + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 179743884c8dd703905db81301c0478f72cf1b6e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:20:07 +0000 Subject: [PATCH 29/58] fix(sidecar): reject leading-zero REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS CodeRabbit correctly flagged that the digit-only guard's zero check only matched the exact one-character string "0", so an all-digit override like "00" or "0000" passed validation. `[ -ge ]` parses those as decimal 0, so the retry loop would fail after exactly one attempt instead of respecting the configured retry count -- the opposite failure mode from the original malformed-value bug, but still bad config that must fail closed before any curl call. Added "00"/"0000" to the case pattern and to the existing parametrized regression test. --- .../ci/contextual_orchestrator_review_sidecar.sh | 8 ++++++-- ...extual_orchestrator_review_runtime_preflight.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 67f0dbf91..79e86cf76 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -511,9 +511,13 @@ REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS="${REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS:- # expected", identical to a non-numeric one) -- so the bound below also caps # digit COUNT, not just digit-ness. Four digits (up to 9999) is already far # beyond any realistic attempt count and stays safely representable on every -# platform this runs on. +# platform this runs on. An all-digit value can still be numerically zero +# with leading zeros ("00", "0000"): `[ -ge ]` parses those as decimal 0, so +# the loop would fail after exactly one attempt instead of respecting the +# configured retry count -- listed explicitly alongside the bare `0` case +# rather than folded into the digit-count cap below. case "$REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS" in - ''|*[!0-9]*|0) + ''|*[!0-9]*|0|00|000|0000) fail "REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS must be a positive integer" ;; ?????*) fail "REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS must be at most 9999" ;; diff --git a/tests/test_contextual_orchestrator_review_runtime_preflight.py b/tests/test_contextual_orchestrator_review_runtime_preflight.py index 942c2a005..af365684c 100644 --- a/tests/test_contextual_orchestrator_review_runtime_preflight.py +++ b/tests/test_contextual_orchestrator_review_runtime_preflight.py @@ -525,7 +525,9 @@ def _run_gateway_retry_loop( return result, report -@pytest.mark.parametrize("malformed_value", ["not-a-number", "0", "-1", "3.5"]) +@pytest.mark.parametrize( + "malformed_value", ["not-a-number", "0", "-1", "3.5", "00", "0000"] +) def test_gateway_retry_loop_rejects_a_malformed_attempt_limit_before_any_curl_call( tmp_path: Path, malformed_value: str ) -> None: @@ -539,6 +541,16 @@ def test_gateway_retry_loop_rejects_a_malformed_attempt_limit_before_any_curl_ca the guard's own ``''`` pattern is defense in depth for a future change to that assignment, not a reachable case today.) + ``"00"``/``"0000"`` are a follow-up CodeRabbit finding on top of the + original fix: the digit-only guard's ``0`` branch only matched the exact + one-character string, so an all-digit-but-zero-valued override (leading + zeros) passed the guard and then made ``gateway_attempt -ge + $REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS`` evaluate true on the very first + attempt (`test` parses a leading-zero numeral as decimal, so ``"00"`` is + ``0``) -- the opposite failure mode from the original bug (fails after + exactly one attempt instead of respecting the configured retry count), + but still config that must be rejected before any curl call. + The plan is deliberately empty: if the fix regresses and the loop reaches curl at all, the fake curl exits 2 with a distinct "no plan queued" message, which the assertions below would not match -- proving this From 78b81a37ceb0ef64bc40bc9ebe5fd7a80651d9c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:21:16 +0000 Subject: [PATCH 30/58] docs(gaps): record the REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS leading-zero fix --- docs/product-technical-gap-baseline.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 06b5f2caf..076b0fe59 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2327,9 +2327,14 @@ CodeRabbit이 rate limit 이후 `dcc9fcd0`에 새 pass를 돌리면 review decis opencode-agent on the current head"로 exit 1 — 그 head에 대한 authenticated dispatch verdict이 아직 게시되지 않은 것뿐, merge conflict나 코드 결함이 아님). `.github#1438`에는 Devin이 새로 3건(테스트가 텍스트만 검사한다는 지적 — 이 계약 테스트 파일 전체가 원래 텍스트 기반 검증이라 이 PR이 새로 도입한 -격차가 아님, 순수 정보성 확인 2건)을 남겨 전부 검증 후 코멘트+resolve했다. `.github#1347`에는 또 새 -Devin 라운드가 도착해 실제 코드 대조 검증을 전담 백그라운드 에이전트에 위임했다(진행 중, 다음 항목에서 -결과를 기록한다). +격차가 아님, 순수 정보성 확인 2건)을 남겨 전부 검증 후 코멘트+resolve했다. CodeRabbit도 diff 밖(outside +diff range) finding 1건을 남겼다: `REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS` 검증의 zero 거부가 정확히 +한 글자인 `"0"`만 매치해 `"00"`/`"0000"`처럼 전부 숫자이면서 0인 값이 통과했고, `[ -ge ]`는 이를 10진수 +0으로 파싱해 재시도 1회 만에 실패(설정한 재시도 횟수를 무시하는, 원래 malformed-value 버그와는 반대 +방향의 결함)하는 실재 버그였다 — case 패턴에 `00`/`0000`을 추가하고 기존 parametrized 회귀 테스트에도 +포함시켜 커밋 `17974388`으로 고쳤다(전체 스위트 1933 passed/1 skipped/21 subtests, coverage 100%, +interrogate 100%). `.github#1347`에는 또 새 Devin 라운드가 도착해 실제 코드 대조 검증을 전담 백그라운드 +에이전트에 위임했다(진행 중, 다음 항목에서 결과를 기록한다). ## 6. Compliance and data boundary From 98605275b78900c49e36203043ccbd34a4cd75b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:28:01 +0000 Subject: [PATCH 31/58] docs(gaps): record .github#1347's completed triage (4 concurrent, 3 no-action, 2 fixed) --- docs/product-technical-gap-baseline.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 076b0fe59..c1ce41cef 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2334,7 +2334,23 @@ diff range) finding 1건을 남겼다: `REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS` 방향의 결함)하는 실재 버그였다 — case 패턴에 `00`/`0000`을 추가하고 기존 parametrized 회귀 테스트에도 포함시켜 커밋 `17974388`으로 고쳤다(전체 스위트 1933 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%). `.github#1347`에는 또 새 Devin 라운드가 도착해 실제 코드 대조 검증을 전담 백그라운드 -에이전트에 위임했다(진행 중, 다음 항목에서 결과를 기록한다). +에이전트에 위임했다 — **완료**: 위임 중 다른 세션이 독립적으로 동일한 9건 중 4건을 이미 고쳐(`fe68c2fa` +— repo launcher 경로 해석, `.env.example` 등 env 템플릿이 `DEFAULT_IGNORE`에 잘못 걸리던 문제, 공백만 +있는 명령이 코드화된 실패 대신 처리되지 않은 예외로 새던 문제, `shutil.which("sh")`가 호출자 PATH를 써 +bwrap mount 안에 없는 셸을 잘못 통과시키던 문제) 별도 hardening(`bd0697aa`, symlink 순환 탐지를 재귀 +active-set 방식으로 교체)까지 push했음을 발견 — 에이전트는 자신의 중복 draft를 버리고 `git merge +--ff-only`로만 재조정했다(새 커밋 없음, force-push 없음). 나머지 5건: 2건은 정보성 확인(이미 정확, +조치 불필요), 1건(readiness probe가 bwrap의 `--unshare-net` 부재로 같은 runner의 다른 서비스를 오탐할 +수 있음)은 이미 다른 세션이 PR 코멘트로 3가지 옵션을 제시한 진짜 아키텍처 결정 사항이라 그대로 유지, +나머지 2건은 이번 세션이 직접 검증해 처리: (a) "rejected copy 유지" — `copy_workspace`의 symlink-escape +`ValueError`를 `sandboxed_verify.py`/`sandboxed_web_e2e.py` 양쪽 `main()` 모두 잡지 않아 코드화된 실패 +대신 raw traceback이 새던 실재 버그를 확인·수정(exit 125, 기존 다른 ValueError 거부와 동일 관례) — +`--keep-sandbox`가 rejected copy도 유지하는 것 자체는 "디버깅용 보존"이라는 플래그의 명시된 목적과 +일치해 정책 문제가 아니라고 판단, 변경하지 않음; (b) "`sandboxed`: true가 isolation 비활성 상태를 +가린다" — 별도 `isolation`/`isolation_backend` 필드가 이미 실제 구분을 담당하고, 어떤 스크립트도 +`sandboxed`를 프로그램적으로 파싱하지 않으며, sibling 모듈(`sandboxed_verify.py`)에서 이 필드는 애초에 +OS 격리 개념이 없어 의미가 다르므로 스키마 변경 없이 그대로 둠. 커밋 `528a1eae`로 push, 전체 스위트 +1980 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%, ruff clean. ## 6. Compliance and data boundary From 49df8c37752507038c279bd097598d143ee7d76f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 15:38:58 +0000 Subject: [PATCH 32/58] docs(gaps): correct .github#1347's final triage count (7 unresolved, not 5; 6/7 resolved) --- docs/product-technical-gap-baseline.md | 31 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c1ce41cef..54d915fd6 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2339,18 +2339,25 @@ interrogate 100%). `.github#1347`에는 또 새 Devin 라운드가 도착해 실 있는 명령이 코드화된 실패 대신 처리되지 않은 예외로 새던 문제, `shutil.which("sh")`가 호출자 PATH를 써 bwrap mount 안에 없는 셸을 잘못 통과시키던 문제) 별도 hardening(`bd0697aa`, symlink 순환 탐지를 재귀 active-set 방식으로 교체)까지 push했음을 발견 — 에이전트는 자신의 중복 draft를 버리고 `git merge ---ff-only`로만 재조정했다(새 커밋 없음, force-push 없음). 나머지 5건: 2건은 정보성 확인(이미 정확, -조치 불필요), 1건(readiness probe가 bwrap의 `--unshare-net` 부재로 같은 runner의 다른 서비스를 오탐할 -수 있음)은 이미 다른 세션이 PR 코멘트로 3가지 옵션을 제시한 진짜 아키텍처 결정 사항이라 그대로 유지, -나머지 2건은 이번 세션이 직접 검증해 처리: (a) "rejected copy 유지" — `copy_workspace`의 symlink-escape -`ValueError`를 `sandboxed_verify.py`/`sandboxed_web_e2e.py` 양쪽 `main()` 모두 잡지 않아 코드화된 실패 -대신 raw traceback이 새던 실재 버그를 확인·수정(exit 125, 기존 다른 ValueError 거부와 동일 관례) — -`--keep-sandbox`가 rejected copy도 유지하는 것 자체는 "디버깅용 보존"이라는 플래그의 명시된 목적과 -일치해 정책 문제가 아니라고 판단, 변경하지 않음; (b) "`sandboxed`: true가 isolation 비활성 상태를 -가린다" — 별도 `isolation`/`isolation_backend` 필드가 이미 실제 구분을 담당하고, 어떤 스크립트도 -`sandboxed`를 프로그램적으로 파싱하지 않으며, sibling 모듈(`sandboxed_verify.py`)에서 이 필드는 애초에 -OS 격리 개념이 없어 의미가 다르므로 스키마 변경 없이 그대로 둠. 커밋 `528a1eae`로 push, 전체 스위트 -1980 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%, ruff clean. +--ff-only`로만 재조정했다(새 커밋 없음, force-push 없음). 남은 것 중 (a) "rejected copy 유지" — +`copy_workspace`의 symlink-escape `ValueError`를 `sandboxed_verify.py`/`sandboxed_web_e2e.py` 양쪽 +`main()` 모두 잡지 않아 코드화된 실패 대신 raw traceback이 새던 실재 버그를 확인·수정(exit 125, 기존 +다른 ValueError 거부와 동일 관례; `--keep-sandbox`가 rejected copy도 유지하는 것은 "디버깅용 보존"이라는 +플래그의 명시된 목적과 일치해 정책 문제가 아니라고 판단, 변경하지 않음) — 커밋 `528a1eae`로 push. + +이후 Devin이 새 라운드에서 finding 2건을 더 남겨(총 unresolved 7건으로 증가) — "명시적 `--ignore +.env.example`가 env-template allowlist에 의해 무시당함"과 "PATH의 relative entry가 wrapper 자신의 +cwd 기준으로 해석돼 valid한 workspace-local 도구가 격리에서 거부됨" — 둘 다 직접 검증해 고치고 push +직전 재확인했더니, 이번엔 다른 세션이 **동일한 두 버그를 독립적으로, 동등하거나 더 방어적인 방식으로** +이미 고쳐 push해 있었다(`5b96f849`/`3c32d3ca` — PATH 수정은 relative entry가 sandbox_root 밖으로 +나가는 경우 파일시스템을 건드리기도 전에 걸러내는 추가 방어까지 포함). 내 uncommitted 중복 edit을 +버리고 `git merge --ff-only`로 재조정(새 커밋 없음). 나머지 2건은 정보성 확인(이미 정확, 조치 불필요), +1건(readiness probe가 bwrap의 `--unshare-net` 부재로 같은 runner의 다른 서비스를 오탐할 수 있음)은 +다른 세션이 PR 코멘트(`issuecomment-5469492897`)로 3가지 옵션(잔여 위험 수용 / `/proc` 기반 포트-소유권 +검증 / 전체 network-namespace 재설계)을 제시한 진짜 아키텍처 결정 사항이라 thread를 열어둔 채 유지. +review thread 7건 중 6건 코멘트+resolve, 1건(아키텍처 결정)은 의도적으로 open 유지. 최종 push된 tip +(`3c32d3ca`) 검증: 전체 스위트 1984 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%, +ruff clean. ## 6. Compliance and data boundary From 6f56dc1b31514cf6bb4d4d2696fbad0483e7258a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 16:47:32 +0000 Subject: [PATCH 33/58] docs(gaps): confirm naruon#1486's strix gateway-timeout diagnosis via successful re-run --- docs/product-technical-gap-baseline.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 54d915fd6..6e88f0cf1 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2359,6 +2359,14 @@ review thread 7건 중 6건 코멘트+resolve, 1건(아키텍처 결정)은 의 (`3c32d3ca`) 검증: 전체 스위트 1984 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%, ruff clean. +**추가**: `naruon#1486`(head `dcc9fcd0`)의 required `strix` check가 실패해 job 로그를 직접 확인 — +"Strix run failed for model 'orchestrator/free' after 5404s (exit code 124)", 즉 스캐너 자체가 +contextual-orchestrator 게이트웨이 backend를 기다리다 타임아웃한 것이지 이 PR의 diff에 대한 스캔 +결과가 아니었다(`.github#1438` PR 설명에 이미 별도 추적 중인 "healthz는 통과하지만 실제 completion +요청이 0바이트로 120초 행"과 동일 시그니처). PR에 근거와 함께 standing-down 코멘트를 남기고 +`rerun_failed_jobs`로 1회 재실행 — **재실행 성공**(`conclusion: success`), 진단이 맞았음을 확인. +naruon#1486에 남은 유일한 required-check 실패는 여전히 opencode-review의 비동기 verdict 대기뿐. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From dcdc3fc18750630e5750f879b895e734e9378aa0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 16:49:20 +0000 Subject: [PATCH 34/58] docs(gaps): fix Devin findings on the strix addendum (cross-repo links, overclaimed root cause) --- docs/product-technical-gap-baseline.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6e88f0cf1..29fc14fb1 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2359,13 +2359,16 @@ review thread 7건 중 6건 코멘트+resolve, 1건(아키텍처 결정)은 의 (`3c32d3ca`) 검증: 전체 스위트 1984 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%, ruff clean. -**추가**: `naruon#1486`(head `dcc9fcd0`)의 required `strix` check가 실패해 job 로그를 직접 확인 — -"Strix run failed for model 'orchestrator/free' after 5404s (exit code 124)", 즉 스캐너 자체가 -contextual-orchestrator 게이트웨이 backend를 기다리다 타임아웃한 것이지 이 PR의 diff에 대한 스캔 -결과가 아니었다(`.github#1438` PR 설명에 이미 별도 추적 중인 "healthz는 통과하지만 실제 completion -요청이 0바이트로 120초 행"과 동일 시그니처). PR에 근거와 함께 standing-down 코멘트를 남기고 -`rerun_failed_jobs`로 1회 재실행 — **재실행 성공**(`conclusion: success`), 진단이 맞았음을 확인. -naruon#1486에 남은 유일한 required-check 실패는 여전히 opencode-review의 비동기 verdict 대기뿐. +**추가**: `ContextualWisdomLab/naruon#1486`(head `dcc9fcd0`)의 required `strix` check가 실패해 job +로그를 직접 확인 — "Strix run failed for model 'orchestrator/free' after 5404s (exit code 124)", +즉 스캐너 자체가 ~90분 만에 timeout한 것이지 이 PR의 diff에 대한 스캔 결과가 아니었다. 이 job 로그 +자체는 일반적인 timeout(exit 124)만 보여줄 뿐 request-level 증거는 없어, `.github#1438` PR 설명에 +이미 추적 중인 "healthz는 통과하지만 실제 completion 요청이 0바이트로 120초 행"과 정확히 같은 +메커니즘이라고 확인한 것은 아니다 — 같은 범주(contextual-orchestrator 게이트웨이 backend 가용성 +문제)로 잠정 분류했을 뿐이다. PR에 이 근거와 함께 standing-down 코멘트를 남기고 `rerun_failed_jobs`로 +1회 재실행 — **재실행 성공**(`conclusion: success`), 일회성 timeout이었음을 확인. +`ContextualWisdomLab/naruon#1486`에 남은 유일한 required-check 실패는 여전히 opencode-review의 +비동기 verdict 대기뿐. ## 6. Compliance and data boundary From a6ff267541e674b3c69eb9162c16cb69defa3ab8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 17:16:03 +0000 Subject: [PATCH 35/58] docs(gaps): note a new main-branch sidecar-unreachable symptom (evidence only, not a required check) --- docs/product-technical-gap-baseline.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 29fc14fb1..aac7a8fcb 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2370,6 +2370,17 @@ ruff clean. `ContextualWisdomLab/naruon#1486`에 남은 유일한 required-check 실패는 여전히 opencode-review의 비동기 verdict 대기뿐. +**참고(main 자체의 별개 증거, 이 PR의 required check 아님)**: `.github#1438`에 붙은 "Default-branch +repository_dispatch Strix evidence" 상태(main 자신, head `1ff82682` — 정확히 ADR-0005 sidecar fix를 +구현한 그 커밋 — 를 스캔)가 실패. job 로그 확인: sidecar 자체는 정상 기동해 "healthz and +provider-route preflight confirmed after 36s"까지 성공했으나(Bytez 500은 기존에 알려진 non-fatal +경고), 그 이후 워크플로 자신의 "gateway preflight" 단계가 3회 재시도(각 2분 간격, 총 6분) 모두 +"did not reach the sidecar cleanly (status=unreachable)"로 실패 — 이전에 문서화된 "healthz는 통과하되 +completion 요청이 0바이트로 hang"과는 다른 새 증상(요청이 hang하는 게 아니라 sidecar 자체가 preflight +성공 이후 어느 시점에 완전히 응답 불가 상태가 된 것으로 보임, 프로세스 조기 종료 또는 포트 미수신 +가능성). main 자신에 대한 평가라 이 PR의 required check를 막지 않고(diff와 무관), 별도 PR로 원인 +조사가 필요한 새 데이터 포인트로만 기록한다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 7e47d84836862f2e5052c4cd9b9400d6444b1b31 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 17:25:07 +0000 Subject: [PATCH 36/58] docs(gaps): record naruon G-15's second slice (AttachmentReparseWorker) --- docs/product-technical-gap-baseline.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index aac7a8fcb..a2d7056b0 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2381,6 +2381,22 @@ completion 요청이 0바이트로 hang"과는 다른 새 증상(요청이 hang 가능성). main 자신에 대한 평가라 이 PR의 required check를 막지 않고(diff와 무관), 별도 PR로 원인 조사가 필요한 새 데이터 포인트로만 기록한다. +**naruon G-15 두 번째 슬라이스 배포**: 4개 PR이 전부 async 대기 상태로 머물러 있는 동안, PR 소진 +여부와 무관하게 다음 제품 Gap 증분을 진행한다는 원칙에 따라 `reparse_pending`을 실제로 소비하는 +`AttachmentReparseWorker`를 구현·배포했다(`naruon#1486`의 같은 브랜치에 push, 커밋 `ba9a01be`) — +ADR-0005가 명시적으로 미뤄뒀던 바로 그 후속 워커. `NewsdomRecognitionWorker`와 동일한 +jittered-loop + PostgreSQL advisory-lock lease + starvation-free cursor 구조를 그대로 따라 +`main.py` lifespan에 배선했고, 매 스윕마다 보존된 원본 바이트 + 원래 선언된 `content_type`으로 +`parse_email_attachment`를 재호출해 재평가한다(sniff된 타입을 신뢰하는 별도 로직 없이 동일 +분류 파이프라인에 같은 질문을 다시 던지는 방식 — 향후 그 파이프라인에 생기는 어떤 수정도 +자동으로 이 워커에 반영됨). 보존 payload가 base64로 유효하지 않은 경우만 새 terminal 상태 +`reparse_payload_invalid`로 분류. PDF 전용이 아닌 범용 base64 디코더 +`decode_quarantined_attachment_payload`도 `attachment_parser.py`에 추가했고, ADR-0005의 +"reparse_pending has no consumer worker yet" 서술과 그 README 색인 행을 갱신했다. 검증: 신규 +테스트 19개(worker 15 + parser 디코더 4), 전체 백엔드 스위트 1864 passed/33 skipped(기존 1845), +ruff clean. 남은 G-15 후보: HWP/HWPX 지원, 단일 첨부파일 upload-accept 상한. G-06(thread/sender +ontology 다리)이 다음 pass의 우선순위 후보로 남는다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From ccde3c223c18f47625b9be672b4c6be889e0d923 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 17:56:02 +0000 Subject: [PATCH 37/58] docs(gaps): record naruon#1486 CodeRabbit/Devin review round on AttachmentReparseWorker --- docs/product-technical-gap-baseline.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a2d7056b0..0db7db281 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2397,6 +2397,24 @@ jittered-loop + PostgreSQL advisory-lock lease + starvation-free cursor 구조 ruff clean. 남은 G-15 후보: HWP/HWPX 지원, 단일 첨부파일 upload-accept 상한. G-06(thread/sender ontology 다리)이 다음 pass의 우선순위 후보로 남는다. +**naruon#1486 `AttachmentReparseWorker` CodeRabbit/Devin review 라운드(같은 브랜치, 커밋 +`ef49fc96`/`da816566`)**: 두 실제 정합성 결함을 근본 수정했다 — (1) `_sweep_attachments`가 배치 +처리 *전에* 커서를 `rows[-1].id`로 미리 전진시켜, 처리 중 예외로 `reparse_pending` 상태 그대로 +남은 행이 커서 아래로 떨어져 전방 큐가 완전히 비워질 때까지(지속 트래픽 하에서는 무한정) 다시 +선택되지 못하던 starvation 버그 — 첫 실패 행 바로 앞까지만 커서를 전진시키도록 수정. (2) +PostgreSQL advisory lease를 매 항목 `commit()`이 커넥션을 풀로 반환하는 동일한 `AsyncSession`으로 +획득·해제해, 해제가 lock을 잡았던 것과 다른 물리 커넥션에서 실행되어 lease가 영구히 묶일 수 +있던 문제 — 스윕 전체 동안 여는 전용 `AsyncConnection` 하나로만 획득·해제하도록 재설계. 이 +두 번째 결함과 완전히 동일한 acquire/release-through-the-per-item-session 구조가 +`services/newsdom_worker.py`에도 그대로 있어 같은 잠재 위험을 가진 것으로 추정되나, 이 PR이 +건드리지 않은 기존 코드라 수정 범위 밖으로 남겨둔다 — **후속 후보**: newsdom_worker도 동일한 +전용-커넥션 lease 패턴으로 맞추는 별도 PR. 추가로 Alembic `0019_attachment_uid`의 downgrade가 +`Base.metadata.create_all()`로 부트스트랩된 DB(제약 형태의 동일 이름 인덱스)에서 `DROP INDEX`가 +거부되는 경로를 고쳤고, `list_judgments` 정렬에 `calendar_conflict_judgment_id` 2차 키를 +추가해 동일 타임스탬프 경합을 제거했다. 검증: 전체 백엔드 스위트 1875 passed/33 skipped, ruff +clean. ADR-0005 Revisions/Decision 두 문서 불일치와 `Email.workspace_id`(이미 추적된 gap의 +재발견) 스레드도 각각 문서 수정과 회신으로 정리했다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 50febfe7a9bd74c8c33d1eef6526a33f116f5c2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 19:35:11 +0000 Subject: [PATCH 38/58] docs(gaps): record naruon#1486 strix recurrence (runtime stall, not sidecar-startup hang) --- docs/product-technical-gap-baseline.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0db7db281..22d7bca60 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2415,6 +2415,21 @@ PostgreSQL advisory lease를 매 항목 `commit()`이 커넥션을 풀로 반환 clean. ADR-0005 Revisions/Decision 두 문서 불일치와 `Email.workspace_id`(이미 추적된 gap의 재발견) 스레드도 각각 문서 수정과 회신으로 정리했다. +**naruon#1486 `strix` 재발 (head `da816566`, run `33326526050`)**: 같은 PR의 앞선 `dcc9fcd0` +발생과 동일한 `STRIX_PROVIDER_UNAVAILABLE` 클래스가 새 head에서 다시 발생했으나 메커니즘은 +달랐다 — 이번에는 sidecar 기동과 preflight(healthz 25s, gateway chat/completions preflight +1차 시도 성공, `orchestrator/free` 8개 모델 선정)가 전부 정상 완료된 후, 실제 스캔 실행 자체가 +`orchestrator/free`를 대상으로 5400초(90분) 동안 응답 없이 멈췄다("Strix run timed out after +5400s" → exit 124 → `STRIX_PROVIDER_UNAVAILABLE: ...orchestrator/free exhausted`). 워크플로 +자체의 bounded-retry gate가 남은 job 예산(589초)이 재시도에 부족해 fail-closed됐다. 이는 앞서 +이 PR의 `noema-review` 코멘트(10:18)에서 이미 추적된 "`orchestrator/free` pool exhaustion" +클래스의 새 증거이지만, "healthz 이후 완전 무응답" 서브 증상과는 다른 "discovery/preflight는 +통과하지만 실제 스캔 완료 요청이 stall"하는 별도 서브 증상이다 — 두 서브 증상을 하나로 +단정하지 않고 구분해서 기록한다. `contextual-orchestrator#923`/`.github#1438`이 다루는 discovery +쪽 bounded retry가 이 런타임-스톨 서브 증상까지 커버하는지는 아직 확인되지 않았다. 진단 +코멘트를 남기고 실패한 job을 1회 재실행(`rerun_failed_jobs`, run `33326526050`)했다 — PR +자체의 diff와는 무관. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 8a843c405a7c917f90baf4000bd26608d767d418 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:15:00 +0000 Subject: [PATCH 39/58] docs(gaps): record .github#1438 strix recurrence of the same runtime-stall sub-symptom --- docs/product-technical-gap-baseline.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 22d7bca60..a864fa964 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2430,6 +2430,15 @@ clean. ADR-0005 Revisions/Decision 두 문서 불일치와 `Email.workspace_id`( 코멘트를 남기고 실패한 job을 1회 재실행(`rerun_failed_jobs`, run `33326526050`)했다 — PR 자체의 diff와는 무관. +**동일 서브 증상이 `.github#1438`(head `50febfe7`, run `33331290092`)에서도 재발**: 약 1시간 +뒤 다른 저장소의 다른 PR에서 완전히 동일한 로그 시그니처가 재현됐다 — sidecar healthz(48s)와 +gateway chat/completions preflight(1차 시도)는 정상 통과했지만, 실제 스캔이 5400초 동안 +멈췄다("Strix run timed out after 5400s" → exit 124 → `STRIX_PROVIDER_UNAVAILABLE: +...orchestrator/free exhausted`, 남은 job 예산 595초로 재시도 불가). 같은 시간대에 서로 다른 +두 저장소에서 동일 서브 증상이 나타난 것은 단발성 flake가 아니라 `orchestrator/free` pool이 +현재 실시간으로 저하된 상태임을 시사한다. 진단 코멘트를 남기고 실패한 job을 1회 재실행 +(`rerun_failed_jobs`, run `33331290092`)했다 — 이 PR의 diff와도 무관. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From ce10acb2ce0df5dc197e1215bc29c8453542b2ca Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:34:53 +0000 Subject: [PATCH 40/58] docs(gaps): record a third, deterministic strix sub-symptom (vision-only model in orchestrator/free) --- docs/product-technical-gap-baseline.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a864fa964..64aa4f2ac 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2439,6 +2439,23 @@ gateway chat/completions preflight(1차 시도)는 정상 통과했지만, 실 현재 실시간으로 저하된 상태임을 시사한다. 진단 코멘트를 남기고 실패한 job을 1회 재실행 (`rerun_failed_jobs`, run `33331290092`)했다 — 이 PR의 diff와도 무관. +**같은 PR의 바로 다음 head(`8a843c40`, run `33335906496`)에서 세 번째 서로 다른 서브 증상 발견 +— 이번엔 flake가 아니라 결정론적 버그**: 워크플로 자체의 내부 bounded-retry 루프가 3회 시도했고 +(135초/65초/73초 — 모두 빠름, hang 아님), 세 번 전부 완전히 동일한 모델/agent에서 실패했다 — +`agent_id: nvidia_nim_meta_llama_3_2_90b_vision_instruct`, `model: +meta/llama-3.2-90b-vision-instruct`, 매번 동일한 `Error code: 400 - invalid_request_error` +(`provider_status: 400, retryable: False`). 3/3 완전 동일 모델 실패는 flake를 배제하고 +결정론적 선택/호환성 버그를 가리킨다 — vision-only 모델이 Strix의 순수 텍스트 agentic +chat-completion 워크로드에 대해 `orchestrator/free` pool에 선정되고, provider가 매번 정확하게 +비-멀티모달 요청을 거부하는 것으로 보인다. `family_cap`을 4→8로 넓힌 것이 vision variant를 +pool에 끌어들인 것과 연관됐을 가능성이 있고, 라우터가 `retryable: false` 400을 받고도 다음 +후보로 failover하지 않는 것으로 보인다. 이 발견은 5400초 hang 클래스와는 별개의, 더 구체적이고 +실행 가능한 root-cause 후보다 — 재실행하지 않았다(결정론적이라 재실행해도 같은 모델에 다시 +걸릴 가능성이 높아 CI 시간만 낭비). 이 PR의 diff와도 무관(family_cap/model-discovery 로직은 +`contextual-orchestrator`에 있음) — free-pool 모델 카탈로그 소유자에게 vision-only 모델을 +텍스트 전용 소비자의 pool에서 제외하거나, 라우터가 `retryable: false` 400을 받으면 다음 후보로 +넘어가도록 하는 수정을 제안한다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 18987a7191f070fdcb134d5feb96a07442c39a98 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:39:42 +0000 Subject: [PATCH 41/58] docs(gaps): separate confirmed evidence from unverified root-cause hypothesis (Devin review) --- docs/product-technical-gap-baseline.md | 27 +++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 64aa4f2ac..72a15f412 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2444,17 +2444,22 @@ gateway chat/completions preflight(1차 시도)는 정상 통과했지만, 실 (135초/65초/73초 — 모두 빠름, hang 아님), 세 번 전부 완전히 동일한 모델/agent에서 실패했다 — `agent_id: nvidia_nim_meta_llama_3_2_90b_vision_instruct`, `model: meta/llama-3.2-90b-vision-instruct`, 매번 동일한 `Error code: 400 - invalid_request_error` -(`provider_status: 400, retryable: False`). 3/3 완전 동일 모델 실패는 flake를 배제하고 -결정론적 선택/호환성 버그를 가리킨다 — vision-only 모델이 Strix의 순수 텍스트 agentic -chat-completion 워크로드에 대해 `orchestrator/free` pool에 선정되고, provider가 매번 정확하게 -비-멀티모달 요청을 거부하는 것으로 보인다. `family_cap`을 4→8로 넓힌 것이 vision variant를 -pool에 끌어들인 것과 연관됐을 가능성이 있고, 라우터가 `retryable: false` 400을 받고도 다음 -후보로 failover하지 않는 것으로 보인다. 이 발견은 5400초 hang 클래스와는 별개의, 더 구체적이고 -실행 가능한 root-cause 후보다 — 재실행하지 않았다(결정론적이라 재실행해도 같은 모델에 다시 -걸릴 가능성이 높아 CI 시간만 낭비). 이 PR의 diff와도 무관(family_cap/model-discovery 로직은 -`contextual-orchestrator`에 있음) — free-pool 모델 카탈로그 소유자에게 vision-only 모델을 -텍스트 전용 소비자의 pool에서 제외하거나, 라우터가 `retryable: false` 400을 받으면 다음 후보로 -넘어가도록 하는 수정을 제안한다. +(`provider_status: 400, retryable: False`). **확인된 사실**: 3/3 완전 동일 모델/동일 에러 +반복은 flake가 아니라 이 모델에 대한 결정론적 실패를 증명한다 — 그 이상은 아니다. Devin +review가 정확히 지적했듯, 이 반복성 자체는 "왜" 400이 나는지(모달리티 불일치인지, 다른 요청 +파라미터 문제인지, 이 모델이 이 gateway 경로에서 아예 지원되지 않는지)를 증명하지 않으며, +`family_cap` 4→8 확대가 원인이라는 연결도 검증되지 않았다(생성기가 리턴한 일반적 wrapper +메시지("provider rejected the request with HTTP 400. Adjust the request parameters and +retry.")만 확인했을 뿐, NVIDIA NIM 쪽의 실제 원본 에러 바디는 읽지 못했고, family_cap=4일 때 +이 모델이 선택되지 않았을 것이라는 점도 직접 확인하지 못했다). **미검증 가설(다음 조사가 +필요, 단정하지 말 것)**: `meta/llama-3.2-90b-vision-instruct`는 이름으로 보아 공개적으로 +알려진 비전-멀티모달 모델이므로, 순수 텍스트 요청과의 모달리티 불일치가 그럴듯한 후보이긴 +하지만 확정된 근거는 아니다. 이 발견은 5400초 hang 클래스와는 별개의, 재현 가능한 증상이다 — +재실행하지 않았다(결정론적이라 재실행해도 같은 모델에 다시 걸릴 가능성이 높아 CI 시간만 +낭비). 이 PR의 diff와도 무관(family_cap/model-discovery 로직은 `contextual-orchestrator`에 +있음) — free-pool 모델 카탈로그 소유자가 이 모델의 실제 원본 provider 에러 바디를 확인해 +정확한 원인을 규명해야 하며, 그 전까지는 모달리티 불일치나 family_cap 연관을 확정된 root +cause로 취급하지 않는다. ## 6. Compliance and data boundary From d8ba170a78b707c9b18bb417abefb29b6fd773bb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 23:42:23 +0000 Subject: [PATCH 42/58] fix(sidecar): distinguish a dead sidecar process from a merely-unreachable one in gateway preflight When the gateway-preflight retry loop exhausted every attempt with no usable HTTP response, it always recorded generic "transport exhausted" evidence, even when the sidecar process itself had already exited after passing readiness -- discarding the single most useful diagnostic (the process's own exit status and stderr). Now checks kill -0 "$sidecar_pid" first (safe for a bash-owned background job: a failing kill -0 means bash's own job table has already reaped it, so the following wait retrieves the real cached exit status -- the same pattern the pre-existing healthz branch already used) and records distinct evidence plus a distinct fail message when the sidecar has exited. Reported via a cross-agent coordination comment on .github#1438, verified against issue #1399 and PR #1460 before acting on it. Exact-head evidence: Strix run/job 33341290448/99337282309 for ContextualWisdomLab/.github#1460 target 2cc819a9. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr --- CHANGELOG.md | 23 ++++++ docs/product-technical-gap-baseline.md | 45 +++++++++++ .../contextual_orchestrator_review_sidecar.sh | 56 +++++++++++-- ...l_orchestrator_review_runtime_preflight.py | 74 ++++++++++++++++++ ...al_orchestrator_review_sidecar_contract.py | 78 +++++++++++++++++-- 5 files changed, 265 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ca51d95..dd2aeddff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Fix `scripts/ci/contextual_orchestrator_review_sidecar.sh`'s gateway-preflight + retry loop: when every configured attempt exhausted with no usable HTTP + response, the script always recorded generic "transport exhausted" evidence + even when the sidecar process itself had already exited after passing + readiness -- discarding the single most useful diagnostic (the process's own + exit status and stderr). Now checks `kill -0 "$sidecar_pid"` first (safe for + a bash-owned background job: a failing `kill -0` means bash's own job table + has already reaped it, so the following `wait` retrieves the real cached + exit status, not a fresh `waitpid()` on an already-gone zombie -- the same + pattern the pre-existing healthz branch already used) and, when the sidecar + has exited, records distinct evidence (`error_type: + "sidecar_process_exited"`, exit status, attempts) and fails with a distinct + message including the exit status and stderr tail, instead of the generic + transport-exhausted path. Exact-head evidence: Strix run/job + `33341290448`/`99337282309` for `ContextualWisdomLab/.github#1460` target + `2cc819a9` (reported via a cross-agent coordination comment, verified + against issue #1399 and PR #1460 before acting on it). New tests: + `tests/test_contextual_orchestrator_review_sidecar_contract.py::test_gateway_preflight_distinguishes_a_dead_sidecar_from_an_unreachable_one` + and + `tests/test_contextual_orchestrator_review_runtime_preflight.py::test_gateway_retry_loop_diagnoses_a_sidecar_that_died_after_readiness`. + See the 2026-08-30 gap-baseline entry for the full diagnosis and the two + pairs of pre-existing-test regressions this surfaced and fixed along the + way. - Fix `scripts/ci/pingora_edge_policy.py`'s `_load_changed_files`: the post-loop pagination-exhaustion `PolicyError` was unreachable dead code (proven: 31 full 100-item pages always trip the in-loop `len(files) > diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 72a15f412..e9b849a66 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2461,6 +2461,51 @@ retry.")만 확인했을 뿐, NVIDIA NIM 쪽의 실제 원본 에러 바디는 정확한 원인을 규명해야 하며, 그 전까지는 모달리티 불일치나 family_cap 연관을 확정된 root cause로 취급하지 않는다. +**4번째 서로 다른 sidecar 실패 서브 증상 발견 및 근본 수정 — "readiness 통과 후 sidecar +프로세스 자체가 종료"**: 다른 동시 실행 AI 에이전트(issue #1399, PR #1460 — PR #1460 본문 +자체가 "PR created automatically by Jules for task ... started by @seonghobae"라고 밝혀 +Google Jules가 이 계정으로 동작 중임을 스스로 증명함)가 남긴 교차-에이전트 협업 코멘트를 +issue #1399/PR #1460 원문 대조로 먼저 진위를 확인한 뒤 착수했다. 근거(exact-head): Strix +run/job `33341290448`/`99337282309`, `ContextualWisdomLab/.github#1460` target `2cc819a9` +— healthz/provider-route readiness가 23초 만에 통과했으나, 6분 뒤 gateway-preflight 3회 +시도가 전부 sidecar에 도달하지 못했다. 기존 코드는 이 실패를 "gateway_transport_exhausted"로 +뭉뚱그려, sidecar가 여전히 살아있는 네트워크 문제인지 이미 완전히 종료했는지를 구분할 유일한 +단서인 프로세스 종료 상태(exit status)를 그냥 버리고 있었다. + +수정(`scripts/ci/contextual_orchestrator_review_sidecar.sh`): gateway-preflight 재시도 +루프의 `[ -z "$gateway_http_status" ]` 분기 맨 앞에, 기존 healthz 분기가 이미 쓰던 것과 +동일한 `kill -0 "$sidecar_pid"` 판정을 추가했다 — 이 프로세스는 이 bash 스크립트 자신이 +백그라운드로 띄운 job이므로, `kill -0`이 실패하면 bash 자신의 SIGCHLD 기반 job-table이 +이미 비동기로 reap을 완료했다는 뜻이고, 그 뒤에 부르는 `wait "$sidecar_pid"`는 새로 +`waitpid()`를 시도하는 게 아니라 job-table에 캐시된 진짜 종료 상태를 그대로 돌려주는 +것이 보장된다(healthz 분기가 이미 의존해온 것과 같은 안전한 패턴). 종료가 확인됐으면 +`wait_for_sidecar_sanitizers`로 stderr를 온전히 비우고, 기존 "transport exhausted" +증거 대신 별도의 `{"endpoint": "chat/completions", "error_type": +"sidecar_process_exited", "attempts": N, "sidecar_exit_status": , "status": +"rejected"}`를 기록하며, exit status와 stderr tail을 포함한 별도 fail 메시지로 +실패한다 — sidecar가 여전히 살아있는 경우에만 기존 "transport exhausted" 경로로 +진행한다. + +TDD: `tests/test_contextual_orchestrator_review_sidecar_contract.py`에 구조 계약 테스트 +`test_gateway_preflight_distinguishes_a_dead_sidecar_from_an_unreachable_one` 추가(첫 +시도에서 `kill -0` 탐색 경계를 잘못 잡아 무관한 healthz 분기의 기존 occurrence에 우연히 +매치해버리는 false positive를 스스로 발견·수정한 뒤에야 진짜 RED를 확인). 실제 스크립트 +슬라이스를 bash 서브프로세스로 실행하는 +`tests/test_contextual_orchestrator_review_runtime_preflight.py`에는 +`_run_gateway_retry_loop`에 `sidecar_alive` 파라미터를 추가한 새 테스트 +`test_gateway_retry_loop_diagnoses_a_sidecar_that_died_after_readiness`를 추가했다 +— `sidecar_alive=False`일 때 `(exit 7) &`로 진짜로 죽는 자식 프로세스를 만들고 +`wait`를 절대 먼저 부르지 않은 채 `kill -0`만으로 폴링해, 코드 아래의 `wait`가 진짜 +종료 상태(7)를 그대로 받아오도록 설계했다. 이 변경으로 두 파일에서 각각 회귀 2건씩 +발견돼 함께 고쳤다 — contract 파일 쪽은 두 기존 테스트의 검색 범위를 좁혀 실제 불변식은 +그대로 두면서 새 코드로 인해 넓어진 매치 범위만 바로잡았고, runtime-preflight 파일 쪽은 +이 스크립트 슬라이스의 최소 하네스가 애초에 `sidecar_pid`/`wait_for_sidecar_sanitizers`/ +`sidecar_stderr`/`SIDECAR_STDERR_TAIL_LINES`를 정의한 적이 없어(이전까지는 그 슬라이스 +안에서 아무도 참조하지 않았으므로) `set -u` 아래 "unbound variable"로 깨졌던 것을 +`sidecar_alive` 기본값(`True` → 회귀 없이 기존 동작 보존)으로 고쳤다. 검증: 전체 +스위트 1935 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%, `bash -n` +OK. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 79e86cf76..0c1f81432 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -544,11 +544,57 @@ while :; do fi if [ "$gateway_attempt" -ge "$REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS" ]; then if [ -z "$gateway_http_status" ]; then - # Every configured attempt exhausted with no usable HTTP response at - # all (Trigger A never resolved) -- record that before failing closed, - # using the same sanitize-then-atomic-replace pattern as the non-2xx - # and invalid-content paths below, so this exact failure case (the one - # telemetry matters most for) does not leave zero evidence trail. + # A transport failure on every attempt has two structurally different + # causes that "could not reach the sidecar" alone cannot distinguish: + # the sidecar process is still running (a real network/gateway issue), + # or it has already exited (its own bug/crash/OOM, unrelated to the + # network at all). Check which one this is before recording generic + # transport-exhausted evidence, so a died sidecar is never + # misclassified as merely unreachable -- exact-head evidence: Strix + # run/job 33341290448/99337282309 for ContextualWisdomLab/.github#1460 + # target 2cc819a9 passed healthz/provider-route readiness after 23s, + # then six minutes later all 3 gateway-preflight attempts failed to + # reach the sidecar at all, with no evidence of whether it had died. + if ! kill -0 "$sidecar_pid" 2>/dev/null; then + sidecar_exit_status=0 + wait "$sidecar_pid" 2>/dev/null || sidecar_exit_status=$? + # The sidecar has fully exited (confirmed above), so draining here + # cannot hang, and it guarantees $sidecar_stderr holds everything the + # sidecar wrote before we read it -- the same discipline the healthz + # branch above uses for the same reason. + wait_for_sidecar_sanitizers + "$sidecar_python" - "$preflight_report" "$gateway_attempt" "$sidecar_exit_status" <<'PY' +import json +from pathlib import Path +import sys + +report_path = Path(sys.argv[1]) +attempts = int(sys.argv[2]) if sys.argv[2].isdecimal() else 0 +exit_status_arg = sys.argv[3] +exit_status = int(exit_status_arg) if exit_status_arg.lstrip("-").isdecimal() else None +try: + report = json.loads(report_path.read_text(encoding="utf-8")) +except (OSError, json.JSONDecodeError): + report = {} +report["gateway"] = { + "endpoint": "chat/completions", + "error_type": "sidecar_process_exited", + "attempts": attempts, + "sidecar_exit_status": exit_status, + "status": "rejected", +} +temporary = report_path.with_suffix(".tmp") +temporary.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") +temporary.replace(report_path) +PY + fail "sidecar process exited after readiness, before gateway preflight completed (status ${sidecar_exit_status}); stderr: $(sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" "$sidecar_stderr")" + fi + # Sidecar still running -- every configured attempt exhausted with no + # usable HTTP response at all (Trigger A never resolved) -- record that + # before failing closed, using the same sanitize-then-atomic-replace + # pattern as the non-2xx and invalid-content paths below, so this exact + # failure case (the one telemetry matters most for) does not leave zero + # evidence trail. "$sidecar_python" - "$preflight_report" "$gateway_attempt" <<'PY' import json from pathlib import Path diff --git a/tests/test_contextual_orchestrator_review_runtime_preflight.py b/tests/test_contextual_orchestrator_review_runtime_preflight.py index af365684c..da89a42d7 100644 --- a/tests/test_contextual_orchestrator_review_runtime_preflight.py +++ b/tests/test_contextual_orchestrator_review_runtime_preflight.py @@ -442,6 +442,7 @@ def _run_gateway_retry_loop( *, max_attempts: int | str, plan: list[str], + sidecar_alive: bool = True, ) -> tuple[subprocess.CompletedProcess[str], dict[str, object]]: """Execute the sidecar's real gateway curl retry loop against a fake curl. @@ -458,6 +459,16 @@ def _run_gateway_retry_loop( regression test. plan: One entry per expected curl call, each either ``"FAIL"`` (a transport failure) or ``"\\n"``. + sidecar_alive: When True (default), ``$sidecar_pid`` names a process + that is genuinely running for the harness's whole lifetime (the + harness script itself, via ``$$``) -- the common case, and what + every pre-existing test in this module implicitly assumed before + the dead-sidecar branch existed. When False, a short-lived child + is spawned and the harness deterministically polls (via `kill + -0`, never `wait`, so the real exit status stays retrievable) + until it has actually exited before the retry loop starts, + simulating a sidecar that crashed between readiness and gateway + preflight. Returns: The completed harness process and the resulting preflight report @@ -486,12 +497,37 @@ def _run_gateway_retry_loop( gateway_preflight_response = work_dir / "gateway-preflight.json" preflight_report = work_dir / "preflight.json" preflight_report.write_text("{}", encoding="utf-8") + sidecar_stderr = work_dir / "sidecar.stderr.log" + sidecar_stderr.write_text("synthetic sidecar stderr tail\n", encoding="utf-8") + + if sidecar_alive: + sidecar_pid_setup = 'sidecar_pid="$$"\n' + else: + # Spawn a child that exits almost immediately, then poll `kill -0` + # (never `wait`) until it is confirmed gone -- this only asserts, + # never consumes, so the retry block's own later `wait "$sidecar_pid"` + # still retrieves this child's real exit status, exactly like it + # would for a genuinely crashed sidecar. + sidecar_pid_setup = ( + "(exit 7) &\n" + "sidecar_pid=$!\n" + "sidecar_dead_wait=0\n" + 'while kill -0 "$sidecar_pid" 2>/dev/null; do\n' + " sidecar_dead_wait=$((sidecar_dead_wait + 1))\n" + ' if [ "$sidecar_dead_wait" -ge 500 ]; then\n' + " echo 'test setup: child never exited' >&2\n" + " exit 99\n" + " fi\n" + " sleep 0.01\n" + "done\n" + ) harness = tmp_path / "harness.sh" harness.write_text( "set -euo pipefail\n" "log() { printf '[test-sidecar] %s\\n' \"$*\"; }\n" 'fail() { log "error: $*" >&2; exit 1; }\n' + "wait_for_sidecar_sanitizers() { :; }\n" 'orchestrator_pool="free"\n' 'ORCHESTRATOR_TOKEN="synthetic-test-bearer"\n' 'ORCHESTRATOR_HOST="127.0.0.1"\n' @@ -500,6 +536,9 @@ def _run_gateway_retry_loop( f'gateway_preflight_request="{gateway_preflight_request}"\n' f'gateway_preflight_response="{gateway_preflight_response}"\n' f'preflight_report="{preflight_report}"\n' + f'sidecar_stderr="{sidecar_stderr}"\n' + 'SIDECAR_STDERR_TAIL_LINES=60\n' + + sidecar_pid_setup + retry_block + "\n", encoding="utf-8", @@ -708,6 +747,41 @@ def test_gateway_retry_loop_records_transport_exhaustion_evidence_before_failing } +def test_gateway_retry_loop_diagnoses_a_sidecar_that_died_after_readiness( + tmp_path: Path, +) -> None: + """Exact-head evidence (Strix run/job 33341290448/99337282309 for + ContextualWisdomLab/.github#1460 target 2cc819a9): the sidecar passed + healthz/provider-route readiness after 23s, then six minutes later all 3 + gateway-preflight attempts failed to reach it at all -- with the + pre-existing code, that is indistinguishable from a sidecar that was + still running the whole time but merely unreachable over the network. + + When every attempt fails transport-wise AND the sidecar process itself + has already exited, the failure must be reported and recorded distinctly + from `gateway_transport_exhausted`, carrying the process's own exit + status -- the one piece of evidence that tells an operator the sidecar + crashed rather than the network being flaky. + """ + result, report = _run_gateway_retry_loop( + tmp_path, max_attempts=1, plan=["FAIL"], sidecar_alive=False + ) + + assert result.returncode == 1 + assert ( + "sidecar process exited after readiness, before gateway preflight " + "completed (status 7); stderr: synthetic sidecar stderr tail" + in result.stderr + ) + assert report["gateway"] == { + "endpoint": "chat/completions", + "error_type": "sidecar_process_exited", + "attempts": 1, + "sidecar_exit_status": 7, + "status": "rejected", + } + + def test_gateway_retry_loop_classifies_a_transport_then_http_exhaustion_by_the_final_attempt( tmp_path: Path, ) -> None: diff --git a/tests/test_contextual_orchestrator_review_sidecar_contract.py b/tests/test_contextual_orchestrator_review_sidecar_contract.py index 0c0078bcd..bbb01b6c9 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -461,13 +461,23 @@ def test_sidecar_surfaces_nonfatal_discovery_warnings_on_a_successful_startup() # whole script unless explicitly tolerated. assert 'sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" || true)"' in text assert 'log "sidecar startup warnings (non-fatal): $sidecar_startup_warnings"' in text - # Must not `wait_for_sidecar_sanitizers` here: the sidecar keeps serving - # after a successful healthz, so its sanitizer never sees EOF and doing - # so would hang the workflow forever. + # Must not `wait_for_sidecar_sanitizers` in this immediate post-healthz + # warning-surfacing block: the sidecar keeps serving after a successful + # healthz, so its sanitizer never sees EOF and doing so would hang the + # workflow forever. Scoped to end at the gateway-preflight section (not + # end of file): that later section has its own, differently-gated + # `wait_for_sidecar_sanitizers` call for a sidecar already confirmed dead + # (a legitimate, distinct case -- see + # test_gateway_preflight_distinguishes_a_dead_sidecar_from_an_unreachable_one), + # which this assertion must not flag. healthz_confirmed = text.index("healthz and provider-route preflight confirmed") warnings_line = text.index("sidecar startup warnings (non-fatal)") - assert healthz_confirmed < warnings_line - assert "wait_for_sidecar_sanitizers" not in text[healthz_confirmed:] + gateway_preflight_start = text.index("gateway_virtual_model=") + assert healthz_confirmed < warnings_line < gateway_preflight_start + assert ( + "wait_for_sidecar_sanitizers" + not in text[healthz_confirmed:gateway_preflight_start] + ) def test_sidecar_stderr_tail_covers_discovery_and_preflight_diagnostics() -> None: @@ -477,10 +487,15 @@ def test_sidecar_stderr_tail_covers_discovery_and_preflight_diagnostics() -> Non diagnostics (up to ``REVIEW_PREFLIGHT_MAX_TOTAL_ROUTES`` routes) plus a couple of summary lines can together exceed the old fixed 20-line cap, silently truncating exactly the evidence a fail-closed incident needs. + + Four uses total: the two healthz-branch failures, the post-healthz + non-fatal-warnings summary, and the dead-sidecar-during-gateway-preflight + failure (see + test_gateway_preflight_distinguishes_a_dead_sidecar_from_an_unreachable_one). """ text = _read(SIDECAR) assert "SIDECAR_STDERR_TAIL_LINES=60" in text - assert text.count('sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p"') == 3 + assert text.count('sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p"') == 4 assert "sed -n '1,20p'" not in text @@ -550,3 +565,54 @@ def test_required_strix_uses_the_gateway_and_zdr_visibility_contract() -> None: "Provision contextual-orchestrator Strix sidecar" ) assert "STRIX_FALLBACK_MODELS: \"\"" in workflow + + +def test_gateway_preflight_distinguishes_a_dead_sidecar_from_an_unreachable_one() -> None: + """A sidecar that dies between readiness and gateway preflight gets its own + diagnosis (exit status + stderr tail), not the generic transport-exhausted + message a merely-unreachable-but-still-running sidecar gets. + + Exact-head evidence (Strix run/job 33341290448/99337282309 for + ContextualWisdomLab/.github#1460 target 2cc819a9): the sidecar passed + healthz/provider-route readiness after 23s, then six minutes later all 3 + gateway-preflight attempts failed to reach it at all. The existing + ``gateway_transport_exhausted`` branch cannot tell that case apart from a + sidecar that is still running but merely unreachable over the network -- + it reports the same generic message either way, discarding the one piece + of evidence (the process's own exit status) that would tell an operator + whether the sidecar crashed. + """ + text = _read(SIDECAR) + exhausted_branch = text.index("gateway_transport_exhausted") + # Scope strictly to this branch's own opening condition so the search + # cannot accidentally match the unrelated, textually-earlier "sidecar + # exited before healthz" branch's own `kill -0 "$sidecar_pid"` check. + unreachable_branch_start = text.rindex( + 'if [ -z "$gateway_http_status" ]; then', 0, exhausted_branch + ) + dead_sidecar_check = text.index( + 'kill -0 "$sidecar_pid"', unreachable_branch_start, exhausted_branch + ) + # The dead-sidecar diagnosis must run BEFORE the generic + # transport-exhausted evidence is recorded, so a died sidecar is never + # misclassified as merely unreachable. + assert dead_sidecar_check < exhausted_branch + dead_sidecar_message = text.index( + "sidecar process exited after readiness, before gateway preflight completed" + ) + assert dead_sidecar_check < dead_sidecar_message < exhausted_branch + # Must reuse the same drain-then-read discipline as the healthz branch: + # wait() for the confirmed-dead child before reading its stderr tail, and + # drain the sanitizer first so the tail is not read mid-flight. + wait_call = text.index('wait "$sidecar_pid"', dead_sidecar_check) + assert wait_call < dead_sidecar_message + drain_call = text.index("wait_for_sidecar_sanitizers", wait_call) + assert wait_call < drain_call < dead_sidecar_message + assert f'sed -n "1,${{SIDECAR_STDERR_TAIL_LINES}}p" "$sidecar_stderr"' in text[ + dead_sidecar_message : dead_sidecar_message + 200 + ] + # The preflight evidence JSON must carry a distinct error_type so a reader + # of contextual-orchestrator-preflight.json can tell the two cases apart + # without parsing job-log prose. + assert '"error_type": "sidecar_process_exited"' in text + assert "sidecar_exit_status" in text From 0224535d195a6b7f7fe280b19894a64c25217edd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 23:54:15 +0000 Subject: [PATCH 43/58] docs(gap-baseline): record newsdom_worker.py lease/cursor follow-up as shipped Cross-references naruon#1486 commit 733f22cf, which applied AttachmentReparseWorker's already-fixed dedicated-lease-connection and starvation-free-cursor pattern to NewsdomRecognitionWorker's own attachment and document sweeps -- the deferred candidate this doc recorded when the reparse worker's version of these bugs was fixed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr --- docs/product-technical-gap-baseline.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index e9b849a66..c90ce914f 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2415,6 +2415,23 @@ PostgreSQL advisory lease를 매 항목 `commit()`이 커넥션을 풀로 반환 clean. ADR-0005 Revisions/Decision 두 문서 불일치와 `Email.workspace_id`(이미 추적된 gap의 재발견) 스레드도 각각 문서 수정과 회신으로 정리했다. +**위 "후속 후보"를 실제로 배포했다 — `services/newsdom_worker.py`도 동일한 두 결함을 그대로 +갖고 있어 근본 수정** (naruon#1486의 같은 브랜치에 push): `AttachmentReparseWorker`에 적용한 +것과 완전히 동일한 두 수정을 `NewsdomRecognitionWorker`의 첨부파일/문서 두 스윕 모두에 +적용했다 — (1) advisory lease를 스윕 전체 동안 여는 전용 `AsyncConnection` 하나로만 +획득·해제(`_engine_uses_postgresql()`/`_try_acquire_sweep_lease`/`_release_sweep_lease`가 +이제 세션이 아니라 커넥션을 받음), (2) 커서를 첫 실패 행 바로 앞까지만 전진. 문서 커서는 +`Document.document_id`가 문자열 기본키라 "실패 id - 1" 산술이 불가능해, 실패 이전에 실제로 +커밋된 마지막 행의 id를 추적하는 방식으로 일반화했다(연속 정수 키에서는 기존 방식과 동일한 +결과, 비연속/문자열 키에서도 올바름). 구현 중 재확인한 사실: `AsyncSessionLocal`이 +`expire_on_commit=False`로 구성돼 있어(`db/session.py`) 매 항목 `commit()`은 후속 행의 +속성 읽기를 깨지 않지만, `rollback()`은 여전히(설정과 무관하게) 세션에 이미 로드된 모든 +객체를 expire시킨다 — 이것이 실제로 재현되는지 aiosqlite 없는 이 환경에서 직접 실행 +검증하지는 못했지만(추측이 아니라 이미 `AttachmentReparseWorker`의 동일 코드베이스에서 +검증·적용된 전례를 따른 것), 두 스윕 모두 각 행을 처리 직전 id로 다시 가져오도록 맞춰 +일관성을 확보했다. 검증: 신규 테스트 6개, 전체 백엔드 스위트 1879 passed/33 skipped(기존 +1875), ruff clean. ADR-0005 Revisions에 기록. + **naruon#1486 `strix` 재발 (head `da816566`, run `33326526050`)**: 같은 PR의 앞선 `dcc9fcd0` 발생과 동일한 `STRIX_PROVIDER_UNAVAILABLE` 클래스가 새 head에서 다시 발생했으나 메커니즘은 달랐다 — 이번에는 sidecar 기동과 preflight(healthz 25s, gateway chat/completions preflight From 38846bbac0fb15c966810b55877bad3620c20c4f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 04:21:42 +0000 Subject: [PATCH 44/58] docs(gap-baseline): record .github#1438 main-merge conflict resolution Fold in this pass's resolution of .github#1438's dirty mergeable_state (three-file conflict against a since-advanced main: .gitignore, CHANGELOG.md, docs/product-technical-gap-baseline.md itself), the ordinary merge commit ca4c5ad4, and full-suite/coverage/interrogate re-verification, plus naruon#1486's current-head Check status. --- docs/product-technical-gap-baseline.md | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a34fcf2b7..2a7d170ed 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2591,6 +2591,42 @@ OK. cleanup을 pool-flip 논의와 분리한 것이다. `contextual_orchestrator_review_sidecar.sh`의 참조 주석은 git history를 가리키도록 갱신되었다. 이 branch에는 코드 변경이 필요 없다(이미 `main`을 merge해 반영됨). +## 2026-08-31 시간별 재개: `.github#1438`의 `main` merge conflict 해소 + +`naruon#1486`의 required Checks 진행 상황을 확인하던 중 `.github#1438`의 `mergeable_state`가 +`blocked`에서 **`dirty`**로 바뀐 것을 발견했다 — `main`이 이 세션의 마지막 동기화 이후 추가로 +전진해 있었다. `origin/main`을 이 branch에 ordinary merge commit으로 병합해(rebase/force-push +없음) 정확히 예상대로 세 파일에서 충돌했다: + +- `.gitignore`: 양쪽이 서로 다른 무관한 항목(`.claude/` vs `strix_runs/`)을 추가한 것뿐이라 둘 다 + 유지. +- `CHANGELOG.md`: `## [Unreleased]` 섹션에 양쪽이 각자 무관한 새 항목을 추가한 것뿐이라, 두 + 블록을 그대로 이어붙였다(HEAD 항목 다음에 `main` 항목). +- `docs/product-technical-gap-baseline.md`: 이 문서 자체에 두 지점에서 충돌 — (1) HEAD의 훨씬 + 최신·상세한 시간별 pass 서술(naruon#1486/.github#1438/contextual-orchestrator#923/.github#1347 + 네 PR 재확인 이력 전체)과 `main`이 독립적으로 추가한 "PR #1347 Devin Review 6건 검증" 항목이 + 같은 위치에서 충돌 — 두 서술 모두 유효한 시간순 기록이라 HEAD 다음에 `main`의 항목을 이어붙였다. + (2) `### 5.1` "다음 개발 increment" 목록 — HEAD의 목록(네 PR의 현재 상태를 정확히 반영, 이미 + §5.1 자체가 두 차례 갱신된 상태)이 `main`의 훨씬 오래되고 이미 stale한 목록(#1297/#1345/#1326 — + 이 문서 앞부분의 "§5.1 next-increment list was stale" 항목이 이미 지적한 바로 그 항목들)과 + 충돌 — HEAD 목록을 유지하고, `main` 쪽에만 있던 새 정보 하나(`select_nvidia_nim_model.py` 고아 + 스크립트 제거, `fix/remove-orphaned-nim-model-resolver` PR로 이미 `main`에 반영됨)만 별도 + 참고 항목으로 보존했다. + +병합 커밋(`ca4c5ad4`) 검증: `PYTHONPATH=. python -m coverage run -m pytest tests` → 2097 +passed, 1 skipped, 21 subtests; `coverage report` → TOTAL 100% (statements 10454/10454, branches +4164/4164); `interrogate` → 100.0%; `bash -n scripts/ci/contextual_orchestrator_review_sidecar.sh` +→ syntax OK; `tests/test_product_technical_gap_baseline.py` → 5 passed (문서 계약 유지 확인). 푸시 +직전 재-fetch로 원격이 그대로임을 확인(경합 없음), 푸시 완료. `mergeable_state`가 `dirty`에서 +`blocked`(required Checks/리뷰 대기, 정상)로 복귀함을 확인했다. + +`naruon#1486`(head `ffed35e5`)은 이 시점 기준 `opencode-review`만 이미 문서화된 비동기 대기 +패턴으로 실패 중이고(`get_job_logs`로 "No APPROVED or CHANGES_REQUESTED from opencode-agent on +the current head" 재확인), 나머지 대부분(backend/frontend/CodeQL/Semgrep/Trivy/osv-scan/ +scorecard/dependency-review/coverage-evidence/noema-review)은 이미 success — `strix`와 세 이미지 +validate job, `metadata-only gate evaluation`만 아직 in_progress. 다음 tick에서 이들이 완료되면 +bypass-merge 조건 충족 여부를 재확인한다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 489547a025e6892c3adcc22294a97b2a540bea2b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 21:29:03 +0000 Subject: [PATCH 45/58] fix(ci): scope required-workflow-bootstrap naming check to that job only The awk range in test_strix_quick_gate.sh's assert_opencode_review_uses_codegraph_and_contextual_orchestrator never terminated: no line in opencode-review.yml's jobs: block dedents to column 0 before EOF, so it scanned the whole rest of the file instead of just required-workflow-bootstrap. That flagged opencode-review-target's own legitimate if: github.event.action != 'closed' as a payload-dependence violation. Confirmed this predates this branch's merge: a fresh clone of main alone fails the identical assertion. Replaced the range with a flag-based scan that stops at the next job key, verified it still catches a genuine violation, and confirmed the full script now passes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y6UJHYbfbGdHfYPjgbVhAr --- CHANGELOG.md | 17 +++++++++ docs/product-technical-gap-baseline.md | 52 ++++++++++++++++++++++++++ scripts/ci/test_strix_quick_gate.sh | 12 +++++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3407c6cd..38cacf1a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Fix a false-positive in `scripts/ci/test_strix_quick_gate.sh`'s + `assert_opencode_review_uses_codegraph_and_contextual_orchestrator`: its + `awk '/^ required-workflow-bootstrap:$/,/^[^ ]/'` range never actually + terminated (no line in `opencode-review.yml`'s `jobs:` block dedents to + column 0 before EOF), so the assertion scanned the entire rest of the file + instead of just the `required-workflow-bootstrap` job -- flagging the + unrelated `opencode-review-target` job's own `if: + github.event.action != 'closed'` (a legitimate closed-PR skip) as if it + violated required-workflow-bootstrap's payload-independence rule. + Confirmed via two independent full runs of the script (this branch's + merged worktree, and a fresh clone of `main` alone) that both fail this + exact assertion identically -- a pre-existing defect on `main`, not + introduced by this branch's merge. Replaced the range with a flag-based + scan that stops at the next 2-space-indented job key. Verified the fix + still catches a genuine violation (a synthetic `if:` injected inside + `required-workflow-bootstrap` itself) and that the full script now passes + with zero failures. - Harden the review sidecar's per-account catalog cap against silent drift: `contextual_orchestrator_review_launcher.py`'s two `build_zdr_prioritized_catalog` call sites now source their diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 2a7d170ed..ba60a2367 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2627,6 +2627,58 @@ scorecard/dependency-review/coverage-evidence/noema-review)은 이미 success validate job, `metadata-only gate evaluation`만 아직 in_progress. 다음 tick에서 이들이 완료되면 bypass-merge 조건 충족 여부를 재확인한다. +## 2026-08-31 시간별 재개: naruon#1486 Devin 추가 배치 3건 수정 + `.github#1438` exact-head-path-policy false positive 근본원인 수정 + +이전 항목 이후 `naruon#1486`(head `399c1e5f`→`1b85703c`)에 대해 Devin이 head를 다시 분석하며 +새로 지적한 진짜 결함 3건을 각각 실제 RED 확인 후 고쳤다. + +1. **🔴 critical: POP3 동기화가 매 실행마다 조용히 메일을 0건 임포트.** `Pop3SyncWorker._import_messages`가 + `TenantConfig`에 존재하지 않는 `workspace_id`를 `getattr(config, "workspace_id", "")`로 읽어 + 항상 빈 문자열을 얻고, 바로 다음 가드에서 무조건 0건을 반환했다. `ImapSyncWorker`가 이미 쓰던 + `resolve_unambiguous_workspace_id()`(소유자의 기존 임포트 메일에서 workspace 역산, 0건/모호하면 + fail-closed)를 공용 헬퍼로 추출해 POP3에도 재사용(커밋 `c3e2856a`). +2. **🟡 `import_fixtures.py`가 커스텀 `NARUON_IMPORT_WORKSPACE_ID`를 무시.** 스레드 배정에는 반영하면서 + 실제 저장하는 `Email.workspace_id`는 그 자리에서 재계산해 env var를 무시했다(커밋 `399c1e5f`). +3. **🟡 owner당 이메일 임포트 할당량이 workspace마다 곱절로 증가.** `MAX_IMPORT_EMAILS_PER_OWNER`/advisory + lock은 `(user_id, organization_id)` 단위인데 카운트 쿼리가 `Email.owner_filters()`를 그대로 + 재사용해 `workspace_id`까지 필터링했다 — 같은 owner가 여러 workspace로 임포트하면 매 workspace가 + 독립적인 1000건 한도를 받았다(커밋 `c6085ef5`). +4. **🔍 analysis: calendar conflict judgment 영속화 경로에 실제 PostgreSQL 커버리지 부재.** + `apply_correction`의 `with_for_update()` row lock을 포함해 전부 mock 세션으로만 테스트되고 있었다 + — 새 real-Postgres smoke 테스트 추가, 로컬 PostgreSQL 16으로 실제 통과 확인(커밋 `c6085ef5`). +5. **🔍 analysis: `calendar_conflict_corrections.rationale`가 2단어 컬럼명 컨벤션 위반.** + `correction_rationale`로 리네임(모델/마이그레이션/서비스 계층만; API 응답 필드명은 유지). 동일 + 패턴의 기존 `project_graph_object_corrections.rationale`(이 PR 이전부터 존재)은 범위 밖으로 남김 + (커밋 `1b85703c`). + +각 수정 모두 전체 백엔드 스위트(최종 1897 passed / 36 skipped)와 ruff clean을 확인했고, 관련 +Devin 스레드 전부(POP3, fixture workspace 무시, fixture registry 우회, PostgreSQL 증거 부족, +legacy mail 도달 불가 재확인, IMAP 사전 workspace 증거 확인, owner quota, naming policy 확인, +rationale 리네임 등 총 9개)에 답글을 달고 resolve했다. + +**`.github#1438`: `exact-head-path-policy` 체크가 병합(`d753f38b`) 직후 FAIL로 나타난 원인을 +근본까지 추적.** `scripts/ci/test_strix_quick_gate.sh`의 +`assert_opencode_review_uses_codegraph_and_contextual_orchestrator`가 `required-workflow-bootstrap` +job 하나만 검사하려는 의도로 `awk '/^ required-workflow-bootstrap:$/,/^[^ ]/'` 범위를 썼는데, +GitHub Actions의 `jobs:` 아래 어떤 job 정의도 실제로는 column 0로 dedent되지 않는다(EOF까지 계속 +2-space indent) — 즉 `/^[^ ]/`는 사실상 파일 끝까지 절대 매치하지 않아, 이 assertion이 +`required-workflow-bootstrap` 하나가 아니라 **파일의 나머지 전체(다른 job들 포함)** 를 검사하고 +있었다. 그 결과 훨씬 뒤에 있는 무관한 `opencode-review-target` job 자신의 `if: +github.event.action != 'closed'`(closed-PR skip 목적, 정상)까지 "required-workflow-bootstrap이 +event payload에 의존한다"는 오탐으로 잡았다. + +**main 자체도 동일하게 실패함을 실증으로 확인**(이 PR이 만든 결함이 아님): (1) `.github#1438`이 +병합해 들어온 워크트리에서 `STRIX_TEST_PROCESS_TIMEOUT_SECONDS=3 STRIX_TEST_FAKE_SLEEP_SECONDS=5 +bash scripts/ci/test_strix_quick_gate.sh` 전체 실행 → 정확히 이 assertion 1건만 FAIL. (2) +`origin/main`(commit `1cbb6aa`) 단독 fresh clone에서 동일 스크립트를 독립적으로 실행 → **동일하게 +정확히 이 assertion 1건만 FAIL** — 병합이 가져온 게 아니라 `main` 자체의 선행 결함임을 확정했다. + +**수정**: `awk` 범위를 column-0 종료 조건 대신, 다음 2-space indent job 키(`^ [A-Za-z0-9_-]+:$`)에서 +멈추는 flag 기반 스크립트로 교체(`scripts/ci/test_strix_quick_gate.sh`). 검증: (a) 실제 +`required-workflow-bootstrap` job 안에 가짜 `if:`를 주입한 사본에서는 여전히 정확히 감지됨(진짜 +위반은 계속 잡음). (b) 수정된 스크립트를 `.github#1438` 워크트리 전체에 대해 재실행 → +`test_strix_quick_gate: PASS`로 회귀 없이 통과. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 4053f4fd5..4e14ed066 100644 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -522,7 +522,17 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_not_contains "$workflow_file" "Wait for trusted OpenCode approval review" "opencode pull_request bridge was removed to avoid duplicate required-check resource use" assert_file_not_contains "$workflow_file" "Trusted OpenCode requested changes for head" "opencode pull_request bridge no longer reconsumes stale trusted review state" assert_file_not_contains "$workflow_file" "github.event.pull_request.number == 240" "opencode review workflow must not hard-code repository-specific PR bypasses" - if awk '/^ required-workflow-bootstrap:$/,/^[^ ]/' "$bootstrap_file" | grep -q '^[[:space:]]*if:'; then + # The range must end at the *next job key* (2-space indent), not the next + # column-0 line: a GitHub Actions job list under `jobs:` never dedents to + # column 0 until EOF, so `/^[^ ]/` as an end pattern silently captured + # every line through the end of the file -- including unrelated later + # jobs' own `if:` conditions (e.g. opencode-review-target's own + # closed-PR skip) -- as if they belonged to required-workflow-bootstrap. + if awk ' + /^ required-workflow-bootstrap:$/ { in_job = 1; next } + in_job && /^ [A-Za-z0-9_-]+:$/ { in_job = 0 } + in_job + ' "$bootstrap_file" | grep -q '^[[:space:]]*if:'; then record_failure "opencode required workflow bootstrap must not depend on required-workflow event payload fields" fi assert_file_contains "$workflow_file" 'github.event.client_payload.target_repository || github.repository' "opencode review scopes concurrency by target repository" From 17f0ca2106128ff5559f4d2291358f3a610bf15a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 23:31:54 +0000 Subject: [PATCH 46/58] docs(gap-baseline): record naruon#1486 CodeRabbit batch-of-3 workspace-scoping fixes Documents this pass's three genuine bugs (project-graph-projection workspace mismatch, import_fixtures.py duplicate-check missing workspace filter, bootstrap_db.py missing second legacy constraint name) and the repeat/no-registry-exists finding, plus the false-negative test lesson from the duplicate-check assertion. --- docs/product-technical-gap-baseline.md | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ba60a2367..bb71d4ce4 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2679,6 +2679,47 @@ bash scripts/ci/test_strix_quick_gate.sh` 전체 실행 → 정확히 이 assert 위반은 계속 잡음). (b) 수정된 스크립트를 `.github#1438` 워크트리 전체에 대해 재실행 → `test_strix_quick_gate: PASS`로 회귀 없이 통과. +## 2026-08-31 시간별 재개: naruon#1486 CodeRabbit 배치 3건 수정(project-graph workspace mismatch, 중복검사 workspace 누락, bootstrap_db.py legacy constraint 2번째 이름) + +이전 항목(head `1b85703c`) 이후 CodeRabbit이 head `a1027af6`를 재분석하며 새로 지적한 결함 4건 +중 3건을 실제 RED 확인 후 고쳤다(head `786d1544`). 4번째는 이미 조사·해소된 gap의 반복. + +1. **🟠 major: `_persist_project_graph_projection`이 호출자의 이미 해석된 workspace를 무시하고 + 자체 재계산.** `email_import_service.py`의 이 함수가 여전히 + `workspace_id = f"workspace-{organization_id}" if organization_id else f"workspace-{user_id}"`를 + 내부에서 재계산하고 있어, non-default import workspace를 쓰면 이메일 본체와 그 이메일에서 + 파생된 project-graph 객체가 서로 다른 workspace에 저장될 수 있었다. `workspace_id`를 필수 + keyword-only 파라미터로 바꾸고 `_import_single_eml`의 `resolved_workspace_id`를 그대로 전달하도록 + 수정(커밋 `786d1544`). 새 테스트로 진짜 RED 확인(`TypeError: unexpected keyword argument + 'workspace_id'`) 후 GREEN. 이 수정으로 이 함수를 구 시그니처로 호출하며 구 fallback 동작 자체를 + 단언하던 `test_project_graph_import_wiring.py`의 기존 테스트 5개가 깨졌다 — 변경 대상 파일만이 + 아니라 전체 스위트를 돌려야 잡히는 회귀였고, 5개 전부 새 계약(명시적 `workspace_id` 인자)에 + 맞게 갱신했다. +2. **🟡 minor: `import_fixtures.py`의 중복 임포트 검사가 workspace로 스코프되지 않음.** 기존 + 메일 존재 확인 쿼리가 `message_id`/`user_id`/`organization_id`만 필터링해, 같은 owner가 서로 다른 + workspace로 같은 메일을 임포트하면 두 번째 워크스페이스에서 잘못 "이미 존재"로 스킵될 수 있었다. + `Email.workspace_id == IMPORT_WORKSPACE_ID` 조건 추가(커밋 `786d1544`). **주의**: 이 수정을 + 증명하는 첫 테스트 시도(`assert "email_records.workspace_id" in query_text`)는 수정 없이도 + PASS하는 false negative였다 — `select(Email)`은 WHERE 절과 무관하게 항상 모든 ORM 컬럼을 SELECT + 목록에 포함하기 때문. `query_text.partition("where ")[2]`로 WHERE 절만 분리해 정확한 + `email_records.workspace_id = :bind_name` 프래그먼트를 확인하도록 재작성한 뒤에야 진짜 증명이 + 됐다. +3. **🟡 minor: `bootstrap_db.py`가 legacy unique constraint 이름을 하나만 drop.** 기존 코드는 + 자신이 직접 만들었던 이름(`uq_email_records_owner_message_id`)만 drop했는데, workspace scoping + 이전 `Base.metadata.create_all()`로 부트스트랩된 DB는 Alembic ORM 메타데이터가 실제로 쓰던 또 + 다른 이름(`uq_emails_owner_message_id`, `0020_email_workspace_scope.py`의 + `_OLD_EMAIL_IDENTITY`)을 갖고 있어 그 경로만 영구히 예전 3-column identity에 갇혔다. 두 이름 + 모두(constraint + index 형태) drop하도록 추가(커밋 `786d1544`). +4. **🔍 repeat: `IMPORT_WORKSPACE_ID`를 registry-backed config로 옮기라는 지적** — 이 코드베이스에는 + 동작하는 KV/credential registry가 없다는, 앞선 Devin 지적과 동일한 gap. `import_fixtures.py`는 + 런타임 요청 경로가 아닌 dev-only fixture 스크립트이므로 새 아키텍처 의존성을 추가하지 않고 기존 + `IMPORT_USER_ID`/`IMPORT_ORGANIZATION_ID`와 같은 env-var-with-default 패턴을 유지하기로 결정. + +전체 백엔드 스위트 1900 passed / 36 skipped, ruff clean(head `786d1544`). CodeRabbit 스레드 2개 +(registry 지적, bootstrap_db.py 지적)에 답글·resolve했고, 별도 discussion thread가 없는 "outside +diff range" 지적 2건(project-graph mismatch, 중복검사 workspace 누락)은 일반 PR issue comment로 +답변했다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From c0afcb54d22378e61d630b843382ab78b5d4097f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 23:39:24 +0000 Subject: [PATCH 47/58] docs(gap-baseline): record naruon#1486 Devin re-analysis batch (search-quarantine fix, two reasoned deferrals) Documents the fix for base64 quarantine payloads leaking into hybrid search, and the reasoning behind deferring TicketTask workspace scoping (real gap, separate increment) and skipping a real-Postgres smoke test for the reparse path (no lock/constraint semantics a mock would hide). --- docs/product-technical-gap-baseline.md | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index bb71d4ce4..130adca7c 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2720,6 +2720,42 @@ bash scripts/ci/test_strix_quick_gate.sh` 전체 실행 → 정확히 이 assert diff range" 지적 2건(project-graph mismatch, 중복검사 workspace 누락)은 일반 PR issue comment로 답변했다. +## 2026-08-31 시간별 재개: naruon#1486 Devin 재분석 3건(quarantine 검색 노출 수정, TicketTask/reparse 두 건은 근거 있는 반려) + +이전 항목(head `786d1544`) 이후 Devin이 head를 재분석하며 새로 지적한 🔍 analysis 3건을 각각 +실제 코드로 검증했다. + +1. **✅ 수정: quarantine된 첨부파일의 base64 원본 payload가 hybrid 검색에 그대로 노출됨.** + `Attachment.parse_status`가 `"parsed"`가 아닌 모든 상태(이 PR이 새로 추가한 + `content_type_mismatch_quarantined` 포함, 기존 `pdf_dom_recognition_pending` 등)는 + `content`에 실제 파싱된 텍스트 대신 base64 인코딩된 원본 바이트나 빈 문자열을 저장하는데, + `build_lexical_attachment_statement`/`build_dense_attachment_statement`(둘 다 이 PR에서는 + 손대지 않은 기존 코드)가 이를 필터링 없이 그대로 검색 대상으로 삼고 있었다. 같은 파일의 + `project_graph_object` 채널이 이미 `_EXCLUDED_PROJECT_OBJECT_STATUS_CODES`로 유사한 상태 + 필터링을 하고 있어 그 패턴을 따라 두 statement 모두 + `.where(Attachment.parse_status == "parsed")`를 추가(커밋 `968b21fc`). 새 테스트 2개로 진짜 + RED(`assert "email_attachments.parse_status" in sql` 실패) 확인 후 GREEN. 전체 백엔드 스위트 + 1902 passed / 36 skipped, ruff clean. +2. **반려(범위 밖으로 결정, 후속 과제로 기록): `TicketTask`에 workspace 스코프가 없음.** + `TicketTask`는 이 PR 이전부터 `(user_id, organization_id)`로만 스코프되어 있었고 + `workspace_id` 컬럼 자체가 없다 — `_build_task_query`의 outer join은 소스 이메일을 + workspace로 필터링해 숨길 뿐, `TicketTask` 행 자신과 WHERE절에는 workspace 조건이 없어 + 같은 owner의 여러 workspace에 걸친 task가 서로 조회·수정 가능하다. 실재하는 아키텍처 격차이나, + 이미 63파일/6800줄 이상인 이 PR에 새 마이그레이션 + 모델/쿼리 변경 + 테스트가 필요한 별도 + 증분을 얹기보다 후속 과제로 분리하기로 결정. **다음 증분에서 필요**: `TicketTask`에 + `workspace_id` 컬럼 추가(마이그레이션 + 백필), `_build_task_query`/`update_ticket_task` 등 + 모든 task 엔드포인트에 workspace 필터 적용, 회귀 테스트. +3. **반려(근거 확인 후 불필요로 결론): reparse 경로에 실제 PostgreSQL 커버리지가 없다는 지적.** + `calendar_conflict_judgment_service.apply_correction`은 `with_for_update()` row lock을 쓰기 + 때문에 이 PR에서 실제 real-Postgres smoke 테스트를 추가했다(mock 세션은 실제 lock 동작을 + 재현할 수 없으므로). 반면 reparse 경로(`api/data.py`의 reparse 라우트, + `attachment_reparse_worker.py`)는 lock이나 제약조건 의존 동작이 전혀 없는 단순 단일 행 + 읽기→필드변경→commit이라, 기존 mock 세션 unit test가 이미 실제 로직(상태 전이, retained + payload 보존)을 충실히 검증하고 있음을 확인 — 추가할 real-Postgres 테스트가 검증할 새로운 + 내용이 없어 추가하지 않기로 결정. + +세 건 모두 스레드에 답글·resolve 완료. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 8e612b866020264182a784d5665301babedb5bbb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 00:03:18 +0000 Subject: [PATCH 48/58] docs(gap-baseline): record naruon CI's missing Postgres service and the 20 hidden real-Postgres test bugs it masked naruon#1486's Devin finding about 0001's fresh-install migration crash led to installing a local PostgreSQL to verify it -- which revealed naruon's CI backend job has no Postgres service at all, so every @pytest.mark.postgres test has always silently skipped in CI. Fixed the critical migration bug plus 19 hidden workspace_id/ORM-default test gaps (naruon commit b9b02dd0); records the still-open follow-up of actually wiring a Postgres service into naruon's CI. --- docs/product-technical-gap-baseline.md | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 130adca7c..33045da16 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2756,6 +2756,38 @@ diff range" 지적 2건(project-graph mismatch, 중복검사 workspace 누락) 세 건 모두 스레드에 답글·resolve 완료. +## 2026-09-01 시간별 재개: naruon CI에 Postgres 서비스가 전혀 없음을 발견 — real-Postgres 테스트 전량이 CI에서 한 번도 실행된 적 없었다 + +naruon#1486의 Devin 지적("Fresh migrations bypass legacy-table guard")을 검증하려 로컬 +PostgreSQL 16 + pgvector를 직접 설치·기동해 `@pytest.mark.postgres` 테스트를 처음으로 실제 +실행해 본 결과, `naruon/.github/workflows/app-ci.yml`의 backend job에 Postgres 서비스 컨테이너가 +**전혀 구성되어 있지 않음**을 확인했다. `tests/conftest.py`가 `DATABASE_URL`을 +`postgresql+asyncpg://test:test@localhost:5432/test_db`로 기본값 설정하지만 CI 러너에는 그 +주소로 연결 가능한 Postgres가 없어, 이 마커가 붙은 모든 테스트는 CI에서 매번 연결 실패로 +조용히 skip되어 왔다 — 이 저장소가 "real-PostgreSQL smoke test"라고 부르는 테스트 클래스 +전체가 사실상 CI에서 한 번도 실행된 적이 없었다는 뜻이다. + +이번 발견으로 실제 실행해 밝혀진 실재 결함(모두 naruon#1486에서 커밋 `b9b02dd0`으로 수정·검증 +완료): + +1. **🔴 critical: `0001_initial_control_plane.py::upgrade()`가 guard를 우회해, 신선한 DB에 + 대한 `alembic upgrade head`가 항상 실패.** `Base.metadata.create_all()`가 만들지 않는 legacy + `emails` 테이블에 인덱스를 만들려다 `relation "emails" does not exist`로 크래시. 실제 신선한 + DB에 대해 마이그레이션을 실행해 크래시를 직접 재현(진짜 RED)한 뒤 수정. +2. **이 PR이 `email_records.workspace_id`를 NOT NULL로 만든 뒤, 이를 반영하지 못한 이 PR과 + 무관한 기존 파일 4개의 real-Postgres 테스트 19건이 하드 실패.** `test_project_graph_api.py`, + `test_project_graph_projection.py`, `test_search_postgres.py`, `test_tasks_api.py`의 공유 + `Email(...)` 시딩 헬퍼가 `workspace_id`를 넘기지 않고 있었다. +3. **`test_data_api.py`의 raw SQL INSERT 3건이 `workspace_id`뿐 아니라 `is_read`/`attachment_uid`도 + 빠뜨림** — 둘 다 ORM 쪽 Python-side default(서버측 default 없음)라 raw SQL이 이를 우회했다. + +**남겨둔 후속 과제 (이번 커밋 범위 밖)**: naruon CI에 실제 Postgres(+ pgvector) 서비스 +컨테이너를 구성해, `@pytest.mark.postgres` 테스트가 매 PR마다 실제로 실행되도록 만드는 것. +현재 구조에서는 이 테스트 클래스 전체가 로컬에 우연히 Postgres를 설치해 둔 개발자가 수동으로 +실행하지 않는 한 영원히 검증되지 않는 죽은 코드나 다름없다 — 이번처럼 이 문서를 갱신하는 +세션이 우연히 로컬 Postgres를 기동하지 않았다면 이 20건의 결함(1번 critical 포함)은 계속 +발견되지 않았을 것이다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From e7fcd1b31a8d91c67dc989aa20703e7934069439 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 00:22:39 +0000 Subject: [PATCH 49/58] docs(gap-baseline): record naruon#1486's newsdom-starvation/reparse-race fixes and two CodeRabbit verifications Documents two more Devin findings fixed on naruon (RESULT_PENDING cursor starvation in NewsdomRecognitionWorker, unlocked reparse-intent TOCTOU race), plus two CodeRabbit findings verified: one correctly deferred (pre-existing raw-SQL migration pattern, out of scope), one confirmed a false positive (workspace mismatch that the query never actually reads). --- docs/product-technical-gap-baseline.md | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 33045da16..b9eaa7b70 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2788,6 +2788,45 @@ PostgreSQL 16 + pgvector를 직접 설치·기동해 `@pytest.mark.postgres` 테 세션이 우연히 로컬 Postgres를 기동하지 않았다면 이 20건의 결함(1번 critical 포함)은 계속 발견되지 않았을 것이다. +## 2026-09-01 시간별 재개: naruon#1486 Devin 재분석 2건 실재 결함(NewsDOM pending 커서 굶주림, reparse-intent 락 없는 경쟁) 수정 + CodeRabbit 2건 검증(1건 반려, 1건 오탐 확인) + +이전 항목(head b9b02dd0) 이후 Devin이 head `c1f02e24`를 재분석하며 지적한 2건과, 이후 +CodeRabbit이 지적한 2건을 모두 검증했다. + +1. **✅ 수정(Devin, 🟡): NewsDOM 재인식 sweep의 커서가 `RESULT_PENDING`(provider 미설정) 행도 + 해결된 것처럼 취급해 커서를 그 너머로 진행시킴.** `_sweep_attachments`/`_sweep_documents`는 + 이미 "예외 발생 행은 커서를 그 앞에서 멈춘다"는 불변식을 갖고 있었지만 + `RESULT_PENDING`(예외 없이 정상 반환되지만 상태는 그대로 pending)은 같은 취급을 받지 못해, + provider가 나중에 설정되어도 그 뒤로 새 업로드가 계속 들어오는 한 해당 행이 무기한 굶주릴 수 + 있었다. 두 sweep 모두 `RESULT_PENDING`을 예외와 동일하게 취급하도록 수정. 새 테스트 2개로 + 진짜 RED(커서가 배치의 마지막 행까지 진행) 확인 후 GREEN. 기존 + `test_document_sweep_advances_and_wraps_without_starvation`이 버그 이전 동작을 전제로 + 작성되어 있어 수정된 계약에 맞게 시나리오 재작성. +2. **✅ 수정(Devin, 🟨): 첨부파일 reparse-intent 엔드포인트가 락 없는 read-then-write로 상태를 + 전이해 TOCTOU 경쟁이 있었음.** `create_attachment_reparse_intent`가 quarantined 상태를 + 확인한 뒤 락 없이 reparse_pending으로 갱신·커밋 — 오래된 읽기를 든 지연된 중복 요청이 그 + 사이 워커가 이미 처리한 최신 상태를 되돌려 덮어쓸 수 있었다. + `calendar_conflict_judgment_service.apply_correction`이 이미 쓰는 `with_for_update()` 패턴을 + `_get_scoped_attachment`에 `lock` 키워드 인자로 추가해 이 엔드포인트에서만 적용. 새 테스트로 + 컴파일된 쿼리에 FOR UPDATE 포함 확인(같은 컨벤션의 기존 테스트와 동일한 검증 수준), 실제 + PostgreSQL로 JOIN + FOR UPDATE OF 조합이 유효한 SQL임을 별도 확인. + 두 수정 모두 커밋 `c1f02e24`. 전체 백엔드 스위트: Postgres 기동 시 1942 passed / 3 skipped, + 중지 시 1905 passed / 40 skipped, ruff clean. +3. **반려(사전 존재, 범위 밖, CodeRabbit 자신도 "Heavy lift"로 표시): `0001_initial_control_plane.py`가 + raw SQL(`execute_schema_backfill`) 대신 구조화된 Alembic 연산(`op.add_column` 등)을 써야 + 한다는 지적.** 확인 결과 `0018`/`0020` 등 다른 마이그레이션은 이미 구조화된 연산을 쓰고 있고, + `0001`만 이 PR 이전부터 raw SQL을 써온 유일한 예외(baseline 마이그레이션이라 성격이 다름) — + 이번 fresh-install 크래시 수정은 이 raw-SQL 특성 자체를 바꾸지 않았으므로 별도의 큰 리팩터로 + 남겨둠. +4. **오탐 확인(CodeRabbit, 🟡): `test_search_postgres.py`의 `_seed_segment_and_project_object`가 + `ProjectGraphObjectRecord.workspace_id="workspace-primary"`를 하드코딩해 `_make_email`의 + workspace(`workspace-org-acme`)와 불일치한다는 지적.** 실제 쿼리 로직을 추적한 결과, + `build_lexical_project_object_statement`의 owner_filters는 `Email.owner_filters(...)`(classmethod, + `cls`=Email)를 그대로 `.where()`에 넣고 `.join(Email, ...)`으로 조인하므로, workspace 필터는 + 전적으로 조인된 Email 행의 workspace_id에만 적용되고 `ProjectGraphObjectRecord.workspace_id`는 + 이 쿼리에서 전혀 참조되지 않음을 확인 — 즉 이 불일치는 검색 정확성에 영향을 주지 않는(하지만 + 지저분한) 테스트 픽스처 값일 뿐. 코드 수정 없이 근거와 함께 반려. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From 0c32943804cfce894a8982d78829d504e34fa197 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 01:40:55 +0000 Subject: [PATCH 50/58] fix(strix): raise Strix's timeout budget to the GitHub-hosted job ceiling The org's standing directive requires central Strix/OpenCode/Noema scans to get at least a 3-hour floor (observed real runs go well past that). Strix's own budget was 150-minute process / 155-minute total (obfuscated via budget_suffix, per contract, so the literal env var names never appear in workflow logs), bounded by a 170-minute step and 200-minute job -- 155 minutes falls short of the 3-hour floor. Raise process/total/step/job proportionally to the actual maximum job execution time GitHub-hosted runners allow (6 hours): job 200->360 (the platform ceiling itself), step 170->330, total budget 9300->18900s (315 min), process budget 9000->18600s (310 min) -- preserving the original buffer ratios between each layer. Update the matching scripts/ci/test_strix_quick_gate.sh contract assertions in lockstep. Also records this investigation, plus the contextual-orchestrator single-tool-call-limit failover fix (ContextualWisdomLab/contextual-orchestrator#986) and confirmation that Strix already scans the full codebase and the @opencode-agent mention convention is already correct, in docs/product-technical-gap-baseline.md. --- .github/workflows/strix.yml | 26 ++++++----- docs/product-technical-gap-baseline.md | 64 ++++++++++++++++++++++++++ scripts/ci/test_strix_quick_gate.sh | 8 ++-- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 505053287..4955ca6b0 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -167,15 +167,17 @@ jobs: strix: if: github.event_name != 'pull_request_target' || github.event.action != 'closed' # Large, actively-growing repositories (e.g. contextual-orchestrator) can - # legitimately require well over two hours to scan -- this org's own - # standing operating directive accepts that central OpenCode/Strix/Noema - # scans may take more than two hours per model (docs/product-goal-directive.md). - # The scanner gets a 150-minute process budget and a 155-minute total - # retry budget; the 170-minute step and 200-minute job leave deterministic - # time to preserve partial reports and publish a concrete failure reason. - # Hitting any cap is fail-closed and never turns an incomplete scan into - # an approval. - timeout-minutes: 200 + # legitimately require several hours to scan -- this org's own standing + # operating directive requires at least a 3-hour floor for central + # Strix/OpenCode/Noema scans, with real observed runs well past that + # (docs/product-goal-directive.md). The scanner gets a 310-minute process + # budget and a 315-minute total retry budget; the 330-minute step and + # 360-minute job leave deterministic time to preserve partial reports and + # publish a concrete failure reason. 360 minutes is also the maximum job + # execution time GitHub-hosted runners allow, so the job timeout itself + # cannot go any higher on ubuntu-latest. Hitting any cap is fail-closed + # and never turns an incomplete scan into an approval. + timeout-minutes: 360 runs-on: ubuntu-latest # Least-privilege token scoped to this job (Scorecard alert #43): the scan # exchanges an OIDC token (id-token) and publishes same-repo status evidence @@ -729,7 +731,7 @@ jobs: - name: Run Strix (quick) if: steps.gate.outputs.enabled == 'true' - timeout-minutes: 170 + timeout-minutes: 330 # Security invariant for pull_request_target: execute only from the # trusted base checkout. The gate copies PR-head blobs into an isolated # temporary scope with execute bits stripped, then scans that scope as @@ -771,11 +773,11 @@ jobs: IS_PR_EVIDENCE_RUN: ${{ (github.event_name == 'pull_request_target' || github.event.client_payload.pr_number != '') && 'true' || 'false' }} run: | budget_suffix="TIME""OUT" - process_budget_seconds="9000" + process_budget_seconds="18600" export "LLM_${budget_suffix}=900" export "STRIX_MEMORY_COMPRESSOR_${budget_suffix}=300" export "STRIX_PROCESS_${budget_suffix}_SECONDS=$process_budget_seconds" - export "STRIX_TOTAL_${budget_suffix}_SECONDS=9300" + export "STRIX_TOTAL_${budget_suffix}_SECONDS=18900" # Recognized signals that the LLM backend was unavailable / starved. # Defined before the gate loop so the bounded retry decision below diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b9eaa7b70..3f696908d 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2827,6 +2827,70 @@ CodeRabbit이 지적한 2건을 모두 검증했다. 이 쿼리에서 전혀 참조되지 않음을 확인 — 즉 이 불일치는 검색 정확성에 영향을 주지 않는(하지만 지저분한) 테스트 픽스처 값일 뿐. 코드 수정 없이 근거와 함께 반려. +## 2026-09-01 시간별 재개: owner 코멘트(`.github#1438`) 7개 항목 조사 — CO passthrough 수정 1건 + Strix 3시간대 상향 1건 + 확인-완료 2건 + 재확인 필요 2건 + +owner `seonghobae`가 `.github#1438`에 직접 남긴 코멘트(진짜 사람 입력, bot echo 아님)의 7개 +요청을 각각 코드 추적으로 조사했다. 추측이 아니라 실제 실행 경로를 읽고 확인한 결과만 기록한다. + +1. **✅ 수정(contextual-orchestrator, 새 PR [#986](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/986)): 일부 NVIDIA NIM 호스팅 모델(관측: vision 지원 Llama 계열, + naruon#1486 Strix 스캔에서 실제 발생)이 tool call을 두 번 이상 요청하는 턴을 거부** + — `invalid_request_error`라는 일반 코드로 감싸져 있고, 모델 고유의 "this model only + supports single tool-calls at once" 문장이 더 긴 agent-prefixed 메시지 안에 묻혀 있어 + 메시지 텍스트만이 유일한 신뢰 가능한 신호였다. `_is_passthrough_failover_error`가 이를 + 인식하지 못해 다음 capability-matched agent로 넘어가지 않고 스캔 전체가 중단됐다. + `_SINGLE_TOOL_CALL_LIMIT_MESSAGE` + `_is_single_tool_call_limit_error()`를 기존 + `_is_provider_tool_description_limit_error` 패턴과 동일한 모양으로 추가하고 failover + 판단에 연결. 새 회귀 테스트로 진짜 RED(수정 전 `ProviderUpstreamError`로 전체 실패) → GREEN + 확인, 전체 스위트 2824 passed / 1 skipped(무관한 사전 존재 `fast_mlsirm` optional-dependency + 갭 1건은 이 diff와 무관 — 별도 후속 필요). 이 수정은 owner의 2번 요청("NVIDIA NIM만 쓰는 건 + 허용 안 함, Contextual-Orchestrator를 쓰세요")의 근본 취지도 함께 강화한다: 중앙 리뷰 파이프라인은 + 이미 `pr-review-autofix.yml` → `contextual_orchestrator_review_sidecar.sh` 경로로 + contextual-orchestrator 게이트웨이를 거치도록 배선되어 있고(`docs/CWL-MASTER-CONTEXT.md`가 + 아니라 이 세션이 직접 코드로 확인), 이번 수정은 그 게이트웨이 내부에서 특정 NIM 모델 하나의 + 결함이 전체 요청을 죽이지 않고 다른 provider로 정상 failover하도록 만든다. +2. **✅ 수정(`.github`, 이번 diff): Strix의 실질 timeout이 "3시간 이상"이라는 지시에 못 미침을 + 코드로 확인.** `strix.yml`의 설명 주석은 "150분 process budget/155분 total budget"을 + 말했고 실제로 그 값(`process_budget_seconds=9000`, `STRIX_TOTAL_TIMEOUT_SECONDS=9300`, + 둘 다 GitHub 로그에 리터럴 이름이 노출되지 않도록 `budget_suffix="TIME""OUT"` 문자열 결합으로 + 난독화되어 있음을 `scripts/ci/test_strix_quick_gate.sh`의 계약으로 재확인)로 배선되어 + 있었지만, 155분(2h35m)은 owner가 요구한 "최소 3시간" 바닥에 못 미치고, 관측했다는 "6시간 이상"과는 + 더 멀었다. GitHub-hosted runner(`ubuntu-latest`)의 job 실행시간은 플랫폼 자체가 6시간(360분)으로 + 하드 캡을 걸어 두므로(어떤 `timeout-minutes` 값을 넣어도 그 이상은 실행되지 않음), 그 한도까지 + 기존 비율(job/step/total/process budget 사이 여유 시간 비율)을 그대로 유지하며 전부 상향했다: + job 200→**360분**(플랫폼 최대), step 170→**330분**, total budget 9300→**18900초(315분)**, + process budget 9000→**18600초(310분)**. 내부 실질 스캔 예산이 315분(5h15m)이 되어 "최소 + 3시간" 요구를 여유 있게 충족하고, 단일 GitHub-hosted job이 물리적으로 도달 가능한 최댓값에 + 근접한다. `scripts/ci/test_strix_quick_gate.sh`의 대응 계약 assertion(리터럴 숫자·설명 문자열)도 + 같은 커밋에서 갱신. owner가 관측했다는 "6시간 이상"이 이 정확한 워크플로/러너였는지는 확인할 + 길이 없다 — self-hosted runner이거나 별도 로컬 실행이었을 가능성이 있으므로, 정확히 어느 + 로그/컨텍스트였는지 owner 확인을 요청했다(코멘트에 남김). +3. **확인-완료(수정 불필요): Strix는 이미 전체 코드베이스를 스캔한다.** `STRIX_TARGET_PATH`의 + `__PR_SCOPE__` sentinel은 스캔 범위가 아니라 "PR 이벤트에서는 신뢰 가능한 base checkout이 아니라 + 격리된 PR-head 임시 스코프에서 읽어라"는 격리 경로 선택 플래그이며, + `scripts/ci/strix_quick_gate.sh`가 이를 `TARGET_PATH="$REPO_ROOT"`(레포 전체)로 해석한다. + `STRIX_DISABLE_PR_SCOPING`이 PR 이벤트에서 `'0'`이 되는 것도 "PR 파일만 본다"는 뜻이 아니라 + findings 필터링 단계의 이야기 — 스캔 자체는 diff-only가 아니다. +4. **확인-완료(수정 불필요): `@opencode-agent` 멘션 호출은 이미 정확히 작동한다.** + `scripts/ci/agent_mention_router.py`의 `AGENT_NAMES["opencode-agent"]`는 + `@opencode-agent`를 정확히 매칭하는 하드코딩된 정규식이다. `/oc`는 업스트림 OpenCode + 프로젝트 자체의 일반 문서에 나오는 무관한 명령어로, 이 조직의 커스텀 멘션 컨벤션과는 별개다. +5. **재확인 필요(부분 확인, 결론 유보): OpenCode/Noema 리뷰가 CodeRabbit/Devin 수준으로 + 실제 리뷰를 하는지.** `opencode.jsonc`/`ci-review-prompt.md`/`code-reviewer-prompt.md`의 + 현재 예산 체계(모델별 5400초 attempt + 11700초 aggregate + 12000초 pool step + job-level + 205~325분)는 이미 3h15m~5h25m 수준으로 결코 얕지 않지만, "실제로 CodeRabbit/Devin 수준의 + 깊이로 리뷰하는가"는 예산의 크기가 아니라 프롬프트·평가 기준·실제 산출물 품질의 문제라 이번 + 패스의 코드 추적만으로는 결론 내릴 수 없다. 별도의 품질 평가 트랙(실제 PR에서 나온 리뷰 + 산출물을 CodeRabbit/Devin의 산출물과 나란히 비교하는 벤치마크)이 필요 — 후속 작업으로 분리. +6. **재확인 필요(부분 확인, 결론 유보): Contextual-Orchestrator가 "빠르고 능력 좋은" 모델로 + 실시간 라우팅하는가.** `model_discovery.py`의 `select_cheapest_discovered_agent`/ + `select_top_n_cheapest_discovered_agents`는 발견된 모델을 **가격**으로만 랭크한다 — + 지연시간(latency)이나 처리 능력을 랭킹 기준에 넣지 않는다. 이번 패스의 single-tool-call-limit + failover 수정(위 1번)은 "느리거나 결함 있는 모델에 걸려 멈추지 않고 다음 모델로 넘어간다"는 + 점에서는 도움이 되지만, "가장 빠른 모델을 우선 선택한다"는 명시적 최적화는 아직 없다. 별도 + 증분(관측된 응답 지연을 랭킹에 반영하는 것)으로 분리 필요. + +owner 코멘트에 대한 전체 답변은 `.github#1438`에 코멘트로 남겼다(항목별 근거·PR 링크 포함). + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 4e14ed066..5fcbbcc8e 100644 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -289,11 +289,11 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "Provision contextual-orchestrator Strix sidecar" "strix workflow provisions the central contextual-orchestrator sidecar" assert_file_contains "$workflow_file" "CONTEXTUAL_ORCHESTRATOR_BASE_URL" "strix workflow uses the sidecar base URL" assert_file_contains "$workflow_file" "CONTEXTUAL_ORCHESTRATOR_TOKEN" "strix workflow uses the sidecar token" - assert_file_contains "$workflow_file" "timeout-minutes: 200" "strix workflow job budget preserves multi-hour scans and artifact publication margin" - assert_file_contains "$workflow_file" "timeout-minutes: 170" "strix workflow scan step permits legitimate 150-minute repository reviews" + assert_file_contains "$workflow_file" "timeout-minutes: 360" "strix workflow job budget preserves multi-hour scans up to the GitHub-hosted runner job execution ceiling" + assert_file_contains "$workflow_file" "timeout-minutes: 330" "strix workflow scan step permits legitimate 310-minute repository reviews" assert_file_contains "$workflow_file" 'budget_suffix="TIME""OUT"' "strix workflow builds budget env keys without visible timeout signal text" - assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=9300"' "strix workflow preserves a 155-minute bounded total Strix budget" - assert_file_contains "$workflow_file" 'process_budget_seconds="9000"' "strix workflow gives a legitimate scan up to 150 minutes" + assert_file_contains "$workflow_file" 'export "STRIX_TOTAL_${budget_suffix}_SECONDS=18900"' "strix workflow preserves a 315-minute bounded total Strix budget" + assert_file_contains "$workflow_file" 'process_budget_seconds="18600"' "strix workflow gives a legitimate scan up to 310 minutes" assert_file_contains "$workflow_file" 'Error code:[[:space:]]*500[^[:cntrl:]]*internal_error' "strix workflow retries contextual-orchestrator internal provider failures" assert_file_contains "$workflow_file" 'strix_gate_console.log" "$GITHUB_WORKSPACE/strix_runs/gate-console.log' "strix workflow preserves partial console output after failures and timeouts" assert_file_contains "$REPO_ROOT/scripts/ci/strix_quick_gate.sh" "gate-last-attempt.log" "strix gate preserves the last partial attempt before runtime cleanup" From 34bfd3a11981628d39a928b6dffb9369330d6eff Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 01:57:19 +0000 Subject: [PATCH 51/58] fix(noema): raise the review-completion request timeout from 120s to 3h scripts/ci/noema_review_gate.py's call_llm sent its actual review- completion request to the contextual-orchestrator gateway with a bare urllib timeout=120 and no retry of its own -- unlike the sidecar's own preflight self-check (ADR-0005, deliberately kept at 120s with its own bounded retry), this call had no fallback at all. Live reproduction today (naruon#1486, job 99690488248): the gateway's own routing pool rejected 11 of 12 candidates (mostly TimeoutError, two retired-model 404s, one 429) and picked its one remaining "ready" agent; that agent's real completion for a full PR diff ran past 120s with nothing to fall back to, failing the entire required review check with TimeoutError. Replace the bare literal with a named NOEMA_LLM_REQUEST_TIMEOUT_SECONDS = 10800 (3h, matching the org's standing at-least-3-hour floor for central Strix/OpenCode/Noema review latency), and update the tests that pinned the old 120s value. Also fixes 3 pre-existing test failures inherited from origin/main (commit a3f9f9b6, ContextualWisdomLab/.github#1531): that commit changed opencode-review-dispatch.yml's validate-pr-metadata step from exact-matching head_sha to warn-and-proceed on a head-only advance, but did not update the two tests that pin the workflow's exact git blob SHA (test_pr_review_autofix_nvidia_nim_contract.py, test_opencode_rust_coverage_toolchain_contract.py) or the assertion in test_opencode_agent_contract.py that still looked for the old exact-match line -- main itself was red on these three tests. --- docs/product-technical-gap-baseline.md | 25 +++++++++++++++++++ scripts/ci/noema_review_gate.py | 13 +++++++++- tests/test_noema_review_gate.py | 2 +- tests/test_opencode_agent_contract.py | 13 +++++++++- ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- ...itory_branch_coverage_review_schedulers.py | 2 +- 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ca14d626e..91fa0a531 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2918,6 +2918,31 @@ owner `seonghobae`가 `.github#1438`에 직접 남긴 코멘트(진짜 사람 failover 수정(위 1번)은 "느리거나 결함 있는 모델에 걸려 멈추지 않고 다음 모델로 넘어간다"는 점에서는 도움이 되지만, "가장 빠른 모델을 우선 선택한다"는 명시적 최적화는 아직 없다. 별도 증분(관측된 응답 지연을 랭킹에 반영하는 것)으로 분리 필요. +7. **✅ 수정("120초"의 실물을 실시간으로 포착): `naruon#1486`의 `noema-review` 필수 체크가 이 + 조사 도중 실제로 실패했고, 정확히 owner가 지적한 "120초"였다.** `scripts/ci/noema_review_gate.py`의 + `call_llm`이 contextual-orchestrator 게이트웨이에 보내는 실제 리뷰 완료 요청에 + `urllib.request timeout=120`을 하드코딩해 두었고 재시도가 전혀 없었다(sidecar 자체의 preflight + 점검용 120초는 ADR-0005로 의도적으로 고정되고 자체 재시도가 있는 별개 값 — 그대로 둠). 이번 + 실행에서는 게이트웨이 preflight가 12개 후보 중 11개를 거부(대부분 TimeoutError, 영구 은퇴 모델 + 404 2건, 429 1건)하고 남은 1개(`meta/llama-3.2-11b-vision-instruct` — 위 1번 항목에서 다룬 그 + 모델)만 "ready"로 판정했는데, 실제 PR diff 전체를 다루는 무거운 완료 요청이 120초 안에 끝나지 + 못해 `TimeoutError: timed out`으로 필수 체크 전체가 실패했다(job 99690488248). 하드코딩된 값을 + 이름 있는 상수 `NOEMA_LLM_REQUEST_TIMEOUT_SECONDS = 10800`(정확히 owner가 요청한 "최소 3시간")로 + 교체하고 `tests/test_noema_review_gate.py` 등 관련 테스트를 갱신. 다만 이 스크립트가 단일 호출· + 무재시도 구조라는 점 자체가 5번 항목("Noema를 CodeRabbit/Devin 수준으로")과 연결되는 구조적 + 격차로 보이며, 재시도 로직 추가는 범위를 넘어서 별도 후속으로 남긴다. +8. **✅ 수정(main에서 물려받은 사전 존재 계약-테스트 드리프트, 이번 diff와 무관하지만 merge 중 발견):** + origin/main의 커밋 `a3f9f9b6`("proceed on head-only advance in review dispatch validation", + owner 자신이 병합, `.github#1531` 참조)이 `opencode-review-dispatch.yml`의 + `validate-pr-metadata` 스텝에서 head_sha만 다를 때의 처리를 exact-match 거부에서 warn-and-proceed로 + 의도적으로 바꿨는데, 이 파일의 정확한 blob SHA를 고정하는 두 계약 테스트 + (`tests/test_pr_review_autofix_nvidia_nim_contract.py::test_independent_review_agent_workflow_matches_reviewed_blob`, + `tests/test_opencode_rust_coverage_toolchain_contract.py::test_review_dispatch_blob_sha_stays_paired_with_trusted_workflow`)와 + `tests/test_opencode_agent_contract.py`의 옛 exact-match 문자열을 찾는 assertion 1건이 갱신되지 + 않아 main 자체가 이 세 테스트에서 레드였다. `REVIEW_DISPATCH_BLOB_SHA`를 새 blob(`3762183e...`)로 + 갱신하고, `test_opencode_agent_contract.py`의 assertion을 새 warn-and-proceed 계약(head_sha + exact-match 부재 + 새 경고 로직 존재, base_ref/base_sha/head_ref exact-match는 그대로 유지)에 + 맞게 다시 작성. 전체 스위트 재검증 완료. owner 코멘트에 대한 전체 답변은 `.github#1438`에 코멘트로 남겼다(항목별 근거·PR 링크 포함). diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 90a69bed3..27aeafc5c 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -36,6 +36,17 @@ ORCHESTRATOR_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1"}) ORCHESTRATOR_BASE_ENV = "CONTEXTUAL_ORCHESTRATOR_BASE_URL" +# The org's standing operating directive requires at least a 3-hour floor for +# central Strix/OpenCode/Noema review latency (docs/product-goal-directive.md). +# Unlike the sidecar's own preflight self-check (ADR-0005, deliberately kept at +# 120s with its own bounded same-budget retry), this single call has no retry +# of its own -- a live reproduction (naruon#1486, job 99690488248, 2026-09-01) +# shows the gateway's own routing pool degraded (11/12 candidates rejected) +# picking its one remaining "ready" agent, whose real completion for a full PR +# diff then ran past the old 120s bound with nothing to fall back to, failing +# the entire required review. 10800s gives the gateway's own internal +# retry/failover machinery room to land on a working agent instead. +NOEMA_LLM_REQUEST_TIMEOUT_SECONDS = 10800 # ⚡ Bolt: Pre-compiled regex patterns to avoid recompilation on every scrub_sensitive_data call. # Impact: Improves string processing performance in error reporting. @@ -653,7 +664,7 @@ def call_llm( method="POST", ) opener = urllib.request.build_opener(NoRedirectHandler()) - with opener.open(request, timeout=120) as response: # nosec B310 + with opener.open(request, timeout=NOEMA_LLM_REQUEST_TIMEOUT_SECONDS) as response: # nosec B310 raw = response.read().decode("utf-8") data = json.loads(raw) content = (((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "").strip() diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 338d46ba8..09fc3080a 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -688,7 +688,7 @@ def read(self): class Opener: def open(self, request, timeout): - assert timeout == 120 + assert timeout == noema.NOEMA_LLM_REQUEST_TIMEOUT_SECONDS payloads.append(json.loads(request.data)) return Response(invalid if len(payloads) == 1 else valid) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 79fdba39a..e7d6740ac 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -2660,7 +2660,18 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' ) in metadata_step assert '[ "$live_head_repository" != "$TARGET_REPOSITORY" ]' not in metadata_step - assert '[ "$SUPPLIED_HEAD_SHA" = "$live_head_sha" ]' in metadata_step + assert '[ "$SUPPLIED_BASE_REF" = "$live_base_ref" ]' in metadata_step + assert '[ "$SUPPLIED_BASE_SHA" = "$live_base_sha" ]' in metadata_step + assert '[ "$SUPPLIED_HEAD_REF" = "$live_head_ref" ]' in metadata_step + # A head_sha-only advance between dispatch capture and this job is normal PR + # activity (Actions queue backlog widens the window); every downstream job + # re-checks the re-fetched live head_sha with its own STALE_HEAD guard, so + # this is a warn-and-proceed, not an exact-match fail-closed rejection. + assert '[ "$SUPPLIED_HEAD_SHA" = "$live_head_sha" ]' not in metadata_step + assert ( + 'if [ -n "$SUPPLIED_HEAD_SHA" ] && [ "$SUPPLIED_HEAD_SHA" != "$live_head_sha" ]; then' + ) in metadata_step + assert "repository_dispatch head advanced since dispatch" in metadata_step assert ( 'live_visibility="$(jq -r \'.base.repo.visibility // empty | ascii_downcase\'' ) in metadata_step diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 3dcfe2cdd..68a0614c0 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "2aa245e7f2a053a4c0b7a9cc8bac0d5d44d38092" +REVIEW_DISPATCH_BLOB_SHA = "3762183eb31c2805317362d2b2c2546e4fccdf09" def _workflow_text(path: Path) -> str: diff --git a/tests/test_repository_branch_coverage_review_schedulers.py b/tests/test_repository_branch_coverage_review_schedulers.py index 85cdc0b96..3c7636f5a 100644 --- a/tests/test_repository_branch_coverage_review_schedulers.py +++ b/tests/test_repository_branch_coverage_review_schedulers.py @@ -63,7 +63,7 @@ class Opener: """Open one deterministic provider response.""" def open(self, _request: Any, timeout: int) -> Response: - assert timeout == 120 + assert timeout == noema.NOEMA_LLM_REQUEST_TIMEOUT_SECONDS return Response() monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) From 37227f129e50820e57c57c8174be0fa666484ddb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 02:00:25 +0000 Subject: [PATCH 52/58] revert(tests): undo the head-only-advance test adaptation main reverted origin/main reverted a3f9f9b6 (commit 7b1a028e, #1540, no rationale given) right after this branch merged it and adapted test_pr_review_autofix_nvidia_nim_contract.py's REVIEW_DISPATCH_BLOB_SHA and test_opencode_agent_contract.py's metadata-step assertion to match its new warn-and-proceed behavior. Restore both to their original exact-match values now that opencode-review-dispatch.yml itself is back to its pre-a3f9f9b6 content (blob 2aa245e7f2a053a4c0b7a9cc8bac0d5d44d38092). --- docs/product-technical-gap-baseline.md | 25 ++++++++++--------- tests/test_opencode_agent_contract.py | 13 +--------- ...t_pr_review_autofix_nvidia_nim_contract.py | 2 +- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 91fa0a531..eaad76482 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -2931,18 +2931,19 @@ owner `seonghobae`가 `.github#1438`에 직접 남긴 코멘트(진짜 사람 교체하고 `tests/test_noema_review_gate.py` 등 관련 테스트를 갱신. 다만 이 스크립트가 단일 호출· 무재시도 구조라는 점 자체가 5번 항목("Noema를 CodeRabbit/Devin 수준으로")과 연결되는 구조적 격차로 보이며, 재시도 로직 추가는 범위를 넘어서 별도 후속으로 남긴다. -8. **✅ 수정(main에서 물려받은 사전 존재 계약-테스트 드리프트, 이번 diff와 무관하지만 merge 중 발견):** - origin/main의 커밋 `a3f9f9b6`("proceed on head-only advance in review dispatch validation", - owner 자신이 병합, `.github#1531` 참조)이 `opencode-review-dispatch.yml`의 - `validate-pr-metadata` 스텝에서 head_sha만 다를 때의 처리를 exact-match 거부에서 warn-and-proceed로 - 의도적으로 바꿨는데, 이 파일의 정확한 blob SHA를 고정하는 두 계약 테스트 - (`tests/test_pr_review_autofix_nvidia_nim_contract.py::test_independent_review_agent_workflow_matches_reviewed_blob`, - `tests/test_opencode_rust_coverage_toolchain_contract.py::test_review_dispatch_blob_sha_stays_paired_with_trusted_workflow`)와 - `tests/test_opencode_agent_contract.py`의 옛 exact-match 문자열을 찾는 assertion 1건이 갱신되지 - 않아 main 자체가 이 세 테스트에서 레드였다. `REVIEW_DISPATCH_BLOB_SHA`를 새 blob(`3762183e...`)로 - 갱신하고, `test_opencode_agent_contract.py`의 assertion을 새 warn-and-proceed 계약(head_sha - exact-match 부재 + 새 경고 로직 존재, base_ref/base_sha/head_ref exact-match는 그대로 유지)에 - 맞게 다시 작성. 전체 스위트 재검증 완료. +8. **✅ 수정 후 되돌림(main에서 물려받은 사전 존재 계약-테스트 드리프트, 이번 diff와 무관하지만 + merge 중 발견 — 그리고 다시 merge 중 원인 자체가 사라짐):** origin/main의 커밋 `a3f9f9b6` + ("proceed on head-only advance in review dispatch validation", owner 자신이 병합, + `.github#1531` 참조)이 `opencode-review-dispatch.yml`의 `validate-pr-metadata` 스텝에서 + head_sha만 다를 때의 처리를 exact-match 거부에서 warn-and-proceed로 바꿨는데, 이 파일의 정확한 + blob SHA를 고정하는 두 계약 테스트와 `test_opencode_agent_contract.py`의 옛 exact-match 문자열 + assertion 1건이 갱신되지 않아 main 자체가 이 세 테스트에서 레드였다 — `REVIEW_DISPATCH_BLOB_SHA`를 + 새 blob으로 갱신하고 assertion을 새 warn-and-proceed 계약에 맞게 다시 작성해 이 diff에 커밋했다. + 그런데 그 직후 origin/main을 다시 fetch하니 owner가 `a3f9f9b6` 자체를 커밋 `7b1a028e`(#1540, + 근거 문구 없음)로 **되돌려** 놓은 상태였다 — 즉 head_sha exact-match 거부가 원래대로 복원됐다. + 이 두 번째 merge로 `opencode-review-dispatch.yml`의 blob이 원래 SHA(`2aa245e7f...`)로 되돌아왔으므로, + 방금 고친 두 테스트 파일의 변경도 함께 원상 복구(옛 blob SHA·옛 exact-match assertion으로 되돌림)해 + 원인이 사라진 상태와 다시 정합시켰다. 전체 스위트 재검증 완료. owner 코멘트에 대한 전체 답변은 `.github#1438`에 코멘트로 남겼다(항목별 근거·PR 링크 포함). diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index e7d6740ac..79fdba39a 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -2660,18 +2660,7 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]' ) in metadata_step assert '[ "$live_head_repository" != "$TARGET_REPOSITORY" ]' not in metadata_step - assert '[ "$SUPPLIED_BASE_REF" = "$live_base_ref" ]' in metadata_step - assert '[ "$SUPPLIED_BASE_SHA" = "$live_base_sha" ]' in metadata_step - assert '[ "$SUPPLIED_HEAD_REF" = "$live_head_ref" ]' in metadata_step - # A head_sha-only advance between dispatch capture and this job is normal PR - # activity (Actions queue backlog widens the window); every downstream job - # re-checks the re-fetched live head_sha with its own STALE_HEAD guard, so - # this is a warn-and-proceed, not an exact-match fail-closed rejection. - assert '[ "$SUPPLIED_HEAD_SHA" = "$live_head_sha" ]' not in metadata_step - assert ( - 'if [ -n "$SUPPLIED_HEAD_SHA" ] && [ "$SUPPLIED_HEAD_SHA" != "$live_head_sha" ]; then' - ) in metadata_step - assert "repository_dispatch head advanced since dispatch" in metadata_step + assert '[ "$SUPPLIED_HEAD_SHA" = "$live_head_sha" ]' in metadata_step assert ( 'live_visibility="$(jq -r \'.base.repo.visibility // empty | ascii_downcase\'' ) in metadata_step diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 68a0614c0..3dcfe2cdd 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "3762183eb31c2805317362d2b2c2546e4fccdf09" +REVIEW_DISPATCH_BLOB_SHA = "2aa245e7f2a053a4c0b7a9cc8bac0d5d44d38092" def _workflow_text(path: Path) -> str: From 738741821f0b5d3d04cc5ff3be387aaf6ff460a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 02:09:47 +0000 Subject: [PATCH 53/58] fix(review): repair Devin's 3 findings on the timeout raises 1. strix.yml's retry-reserve check silently disabled every retry: raising process_budget_seconds to 18600 without raising the wrapper's own strix_gate_deadline (still 9600) made retry_reserve_seconds (process_budget_seconds + backoff) exceed the deadline unconditionally, so a transient provider outage that used to recover on attempt 2 would now fail closed on attempt 1 every time. Raise strix_gate_deadline to 19200 (same 600s buffer under the new 330-minute step, proportional to the original ratio) and add a regression test (test_retry_deadline_reserves_room_for_at_least_one_retry) asserting the live numbers -- not synthetic ones -- keep a retry reachable. 2. noema_review_gate.py's call_llm can recurse once for a validator- rejected repair; two independent NOEMA_LLM_REQUEST_TIMEOUT_SECONDS (10800s) calls would total 21600s -- exactly the 6h GitHub-hosted job execution ceiling, leaving no room for sidecar provisioning or cleanup and turning a fast failure into a reliable 6h one. Add a shared NOEMA_LLM_TOTAL_BUDGET_SECONDS (19800s) deadline threaded through the repair recursion; each call's own request timeout is now capped to whatever remains of it. New test (test_call_llm_repair_call_shares_the_total_budget_deadline) proves the repair call's timeout shrinks when the first call has already consumed most of the shared budget. 3. strix.yml's timeout comment misattributed the 3-hour floor to the standing operating directive; that directive only accepts scans over two hours per model. The 3-hour floor came from the repo owner's later comment on ContextualWisdomLab/.github#1438 -- corrected the citation. All three found by Devin Review on ContextualWisdomLab/.github#1438. --- .github/workflows/strix.yml | 12 ++- scripts/ci/noema_review_gate.py | 19 +++- tests/test_noema_review_gate.py | 101 ++++++++++++++++++ ...kend_unavailable_after_exempted_finding.py | 35 ++++++ 4 files changed, 161 insertions(+), 6 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 4955ca6b0..01dd31c43 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -167,10 +167,12 @@ jobs: strix: if: github.event_name != 'pull_request_target' || github.event.action != 'closed' # Large, actively-growing repositories (e.g. contextual-orchestrator) can - # legitimately require several hours to scan -- this org's own standing - # operating directive requires at least a 3-hour floor for central - # Strix/OpenCode/Noema scans, with real observed runs well past that - # (docs/product-goal-directive.md). The scanner gets a 310-minute process + # legitimately require several hours to scan -- the standing operating + # directive already accepts central Strix/OpenCode/Noema scans taking + # more than two hours per model (docs/product-goal-directive.md), and the + # repo owner separately raised the floor to at least three hours, with + # real observed runs well past that (owner comment, ContextualWisdomLab/.github#1438, + # 2026-09-01). The scanner gets a 310-minute process # budget and a 315-minute total retry budget; the 330-minute step and # 360-minute job leave deterministic time to preserve partial reports and # publish a concrete failure reason. 360 minutes is also the maximum job @@ -811,7 +813,7 @@ jobs: strix_terminal_log="$strix_run_log" strix_rc=0 strix_gate_attempt=1 - strix_gate_deadline=$(( SECONDS + 9600 )) + strix_gate_deadline=$(( SECONDS + 19200 )) # Reserve the scanner process budget, not the gate's total wrapper # budget. The latter includes setup/cleanup overhead already spent # by the current attempt and can make every retry impossible. diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 27aeafc5c..81ed9c33a 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -13,6 +13,7 @@ import socket import subprocess import sys +import time import urllib.error import urllib.parse import urllib.request @@ -47,6 +48,17 @@ # the entire required review. 10800s gives the gateway's own internal # retry/failover machinery room to land on a working agent instead. NOEMA_LLM_REQUEST_TIMEOUT_SECONDS = 10800 +# The noema-review job carries no explicit timeout-minutes, so it is bounded +# only by the 360-minute (6h) maximum job execution time GitHub-hosted +# runners allow. call_llm can recurse once for a single validator-rejected +# repair (see the repair_error branch below); two independent +# NOEMA_LLM_REQUEST_TIMEOUT_SECONDS calls would total 21600s -- exactly that +# 6h ceiling, leaving zero room for sidecar provisioning or job cleanup and +# turning a fast 120s failure into a reliable 6h one (Devin Review, +# ContextualWisdomLab/.github#1438). This is the shared deadline across the +# initial call AND its one possible repair call combined: each call's own +# request timeout is capped to whatever remains of it. +NOEMA_LLM_TOTAL_BUDGET_SECONDS = 19800 # ⚡ Bolt: Pre-compiled regex patterns to avoid recompilation on every scrub_sensitive_data call. # Impact: Improves string processing performance in error reporting. @@ -607,8 +619,12 @@ def call_llm( review_context: str = "", changed_paths: Sequence[str] = (), repair_error: str = "", + deadline: float | None = None, ) -> dict[str, Any]: """Call the configured OpenAI-compatible LLM endpoint for a review verdict.""" + if deadline is None: + deadline = time.monotonic() + NOEMA_LLM_TOTAL_BUDGET_SECONDS + request_timeout = max(1, min(NOEMA_LLM_REQUEST_TIMEOUT_SECONDS, deadline - time.monotonic())) api_url = os.environ.get("NOEMA_LLM_API_URL", "").strip() api_key = os.environ.get("NOEMA_LLM_API_KEY", "").strip() model = os.environ.get("NOEMA_LLM_MODEL", "").strip() or "noema-default" @@ -664,7 +680,7 @@ def call_llm( method="POST", ) opener = urllib.request.build_opener(NoRedirectHandler()) - with opener.open(request, timeout=NOEMA_LLM_REQUEST_TIMEOUT_SECONDS) as response: # nosec B310 + with opener.open(request, timeout=request_timeout) as response: # nosec B310 raw = response.read().decode("utf-8") data = json.loads(raw) content = (((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "").strip() @@ -706,6 +722,7 @@ def call_llm( review_context, changed_paths, str(exc), + deadline, ) return verdict diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 09fc3080a..d87951334 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -699,6 +699,107 @@ def open(self, request, timeout): assert "trusted validator" in payloads[1]["messages"][1]["content"] +def test_call_llm_repair_call_shares_the_total_budget_deadline(monkeypatch): + """A repair call must not get its own independent 3-hour budget. + + Devin Review (ContextualWisdomLab/.github#1438): two independent + NOEMA_LLM_REQUEST_TIMEOUT_SECONDS calls (the initial request plus one + validator-rejected repair) would total 21600s -- exactly the 6h + GitHub-hosted job execution ceiling, leaving no room for sidecar + provisioning or cleanup. The repair call must instead receive whatever + remains of the shared NOEMA_LLM_TOTAL_BUDGET_SECONDS deadline. + """ + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example/v1/chat/completions") + monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") + diff = """--- a/tool.py ++++ b/tool.py +@@ -1 +1 @@ +-old = True ++new = True +""" + invalid = { + "decision": "approve", + "summary": "Checked the replacement.", + "findings": [], + "reviewed_lines": [ + {"path": "tool.py", "line": 2, "side": "RIGHT", "analysis": "Checked."} + ], + "adversarial_validation": { + "status": "passed", + "residual_risk": "Callers were not executed.", + "probes": [], + }, + } + valid = { + **invalid, + "reviewed_lines": [ + {"path": "tool.py", "line": 1, "side": "RIGHT", "analysis": "Checked."} + ], + "adversarial_validation": { + "status": "passed", + "residual_risk": "Callers were not executed.", + "probes": [ + { + "path": "tool.py", + "line": 1, + "side": "RIGHT", + "hypothesis": "The assignment was removed.", + "attack_or_counterexample": "Inspect the added hunk line.", + "evidence": "The RIGHT-side assignment remains present.", + "outcome": "falsified", + }, + { + "path": "tool.py", + "line": 1, + "side": "RIGHT", + "hypothesis": "The value became false.", + "attack_or_counterexample": "Read the replacement literal.", + "evidence": "The literal is True.", + "outcome": "falsified", + }, + ], + }, + } + payloads = [] + timeouts = [] + fake_clock = [1000.0] + monkeypatch.setattr(noema.time, "monotonic", lambda: fake_clock[0]) + + class Response: + def __init__(self, verdict): + self.verdict = verdict + + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def read(self): + return json.dumps( + {"choices": [{"message": {"content": json.dumps(self.verdict)}}]} + ).encode() + + class Opener: + def open(self, request, timeout): + timeouts.append(timeout) + payloads.append(json.loads(request.data)) + if len(payloads) == 1: + # The first call itself consumes almost the entire shared + # budget, leaving only ~500s for a repair call. + fake_clock[0] += noema.NOEMA_LLM_TOTAL_BUDGET_SECONDS - 500 + return Response(invalid) + return Response(valid) + + monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) + + assert noema.call_llm("owner/repo", 7, make_pr(), diff, False)["decision"] == "approve" + assert len(timeouts) == 2 + assert timeouts[0] == noema.NOEMA_LLM_REQUEST_TIMEOUT_SECONDS + assert timeouts[1] < timeouts[0] + assert timeouts[1] == pytest.approx(500, abs=2) + + def test_substantive_approve_requires_exact_changed_lines_and_falsified_probes(): diff = """diff --git a/tool.py b/tool.py --- a/tool.py diff --git a/tests/test_strix_backend_unavailable_after_exempted_finding.py b/tests/test_strix_backend_unavailable_after_exempted_finding.py index 650db6d25..131f502c4 100644 --- a/tests/test_strix_backend_unavailable_after_exempted_finding.py +++ b/tests/test_strix_backend_unavailable_after_exempted_finding.py @@ -20,6 +20,7 @@ from __future__ import annotations +import re import shlex import subprocess import tempfile @@ -289,6 +290,40 @@ def test_retry_contract_preserves_logs_and_process_attempt_budget(self) -> None: self.assertNotIn("STRIX_TOTAL_TIMEOUT_SECONDS:", workflow) self.assertNotIn('remaining_seconds" -lt 600', workflow) + def test_retry_deadline_reserves_room_for_at_least_one_retry(self) -> None: + """The wrapper's own retry deadline must outlive a single process attempt. + + Devin Review (ContextualWisdomLab/.github#1438): raising + process_budget_seconds without raising strix_gate_deadline in lockstep + made retry_reserve_seconds (process_budget_seconds + backoff) exceed + the deadline's own remaining-time budget on the very first check, so + the reserve guard rejected every retry unconditionally -- a provider + outage that used to recover on attempt 2 would now fail closed on + attempt 1 every time. Assert the live numbers, not synthetic ones, + keep a retry structurally reachable: the deadline must exceed one + process-budget reservation plus its first backoff interval. + """ + + workflow = STRIX_WORKFLOW.read_text(encoding="utf-8") + process_budget_match = re.search( + r'process_budget_seconds="(\d+)"', workflow + ) + deadline_match = re.search( + r"strix_gate_deadline=\$\(\( SECONDS \+ (\d+) \)\)", workflow + ) + backoff_match = re.search( + r"STRIX_GATE_RETRY_BACKOFF_SECONDS:-(\d+)\}", workflow + ) + assert process_budget_match is not None + assert deadline_match is not None + assert backoff_match is not None + process_budget_seconds = int(process_budget_match.group(1)) + deadline_seconds = int(deadline_match.group(1)) + first_backoff_seconds = int(backoff_match.group(1)) * 1 + self.assertGreater( + deadline_seconds, process_budget_seconds + first_backoff_seconds + ) + if __name__ == "__main__": unittest.main() From 8555d4ef044059f9cf1d1611e8fc81fa8911eef8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 02:17:52 +0000 Subject: [PATCH 54/58] fix(review): repair Devin's second round of findings 1. noema_review_gate.py's timeout comment carried the same misattribution as strix.yml's -- fixed there but missed here -- crediting the 3-hour floor to the standing operating directive (which only accepts scans over two hours per model) instead of the owner's later comment on ContextualWisdomLab/.github#1438. Corrected the citation. 2. call_llm clamped an expired shared deadline to a 1-second timeout instead of stopping, so a late validator-rejected repair would still open a network request and burn through the job's already-exhausted reserved cleanup time. Raise TimeoutError immediately when the shared budget is gone, per Devin's suggested fix. New test (test_call_llm_raises_instead_of_sending_a_request_on_an_expired_budget) proves no request is sent once the deadline has passed -- verified genuine RED (the old clamp let a 1-second request through) before GREEN. 3. strix.yml's retry-loop comment still named the old 200-minute job budget after the job became 360 minutes, and did not make clear that the 3-attempt retry cap is conditional on an early failure rather than a guarantee of three full-budget attempts. Updated the comment with both corrections. All three found by Devin Review's second pass on ContextualWisdomLab/.github#1438. --- .github/workflows/strix.yml | 10 ++++++++-- scripts/ci/noema_review_gate.py | 13 +++++++++---- tests/test_noema_review_gate.py | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 01dd31c43..1a8846967 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -806,8 +806,14 @@ jobs: # provider failures do not fail the required check on the first # attempt. Genuine findings, configuration failures, and unexpected # exit codes never retry; the deadline keeps every path inside the - # deterministic 200-minute job budget, and all-terminal outcomes - # remain fail-closed. + # deterministic 330-minute step budget (within the 360-minute job), + # and all-terminal outcomes remain fail-closed. The 3-attempt cap + # below is a ceiling, not a guarantee: the remaining-time guard only + # allows a retry when enough of the deadline is left to reserve + # another full process_budget_seconds attempt, so only early + # failures actually get to retry -- an attempt that itself + # consumes most of its budget before failing has already spent the + # time a retry would need. strix_run_log="$RUNNER_TEMP/strix_gate_console.log" : > "$strix_run_log" strix_terminal_log="$strix_run_log" diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 81ed9c33a..dbb9c7d58 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -37,9 +37,11 @@ ORCHESTRATOR_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1"}) ORCHESTRATOR_BASE_ENV = "CONTEXTUAL_ORCHESTRATOR_BASE_URL" -# The org's standing operating directive requires at least a 3-hour floor for -# central Strix/OpenCode/Noema review latency (docs/product-goal-directive.md). -# Unlike the sidecar's own preflight self-check (ADR-0005, deliberately kept at +# The standing operating directive already accepts central Strix/OpenCode/Noema +# review latency exceeding two hours per model (docs/product-goal-directive.md), +# and the repo owner separately raised the floor to at least three hours +# (owner comment, ContextualWisdomLab/.github#1438, 2026-09-01). Unlike the +# sidecar's own preflight self-check (ADR-0005, deliberately kept at # 120s with its own bounded same-budget retry), this single call has no retry # of its own -- a live reproduction (naruon#1486, job 99690488248, 2026-09-01) # shows the gateway's own routing pool degraded (11/12 candidates rejected) @@ -624,7 +626,10 @@ def call_llm( """Call the configured OpenAI-compatible LLM endpoint for a review verdict.""" if deadline is None: deadline = time.monotonic() + NOEMA_LLM_TOTAL_BUDGET_SECONDS - request_timeout = max(1, min(NOEMA_LLM_REQUEST_TIMEOUT_SECONDS, deadline - time.monotonic())) + remaining_budget = deadline - time.monotonic() + if remaining_budget <= 0: + raise TimeoutError("Noema LLM review exhausted its total request budget") + request_timeout = min(NOEMA_LLM_REQUEST_TIMEOUT_SECONDS, remaining_budget) api_url = os.environ.get("NOEMA_LLM_API_URL", "").strip() api_key = os.environ.get("NOEMA_LLM_API_KEY", "").strip() model = os.environ.get("NOEMA_LLM_MODEL", "").strip() or "noema-default" diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index d87951334..74af6b81c 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -800,6 +800,30 @@ def open(self, request, timeout): assert timeouts[1] == pytest.approx(500, abs=2) +def test_call_llm_raises_instead_of_sending_a_request_on_an_expired_budget(monkeypatch): + """An exhausted shared deadline must fail fast, never clamp to 1 second. + + Devin Review (ContextualWisdomLab/.github#1438): the prior + ``max(1, ...)`` clamp let a repair call whose deadline had already + passed still open a (near-instant) network request instead of stopping, + wasting the job's already-exhausted reserved cleanup time. + """ + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example/v1/chat/completions") + monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") + + class Opener: + def open(self, request, timeout): # pragma: no cover - must never be reached + raise AssertionError("call_llm must not send a request on an expired budget") + + monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) + + expired_deadline = noema.time.monotonic() - 1 + with pytest.raises(TimeoutError, match="exhausted its total request budget"): + noema.call_llm( + "owner/repo", 7, make_pr(), "diff", False, deadline=expired_deadline + ) + + def test_substantive_approve_requires_exact_changed_lines_and_falsified_probes(): diff = """diff --git a/tool.py b/tool.py --- a/tool.py From 52b64f5da10b7220d86d4b90f56933cd24d409cf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 02:21:57 +0000 Subject: [PATCH 55/58] docs(noema): clarify the shared repair deadline covers processing time Devin Review (ContextualWisdomLab/.github#1438) noted call_llm's repair call reuses the same absolute deadline, so validation and response processing time between the initial call and its repair also count against the shared budget -- not just network time. That is intended: the deadline is a hard wall-clock ceiling regardless of where the time goes. Documented the intent inline so it reads as deliberate. --- scripts/ci/noema_review_gate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index dbb9c7d58..662d2a576 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -626,6 +626,11 @@ def call_llm( """Call the configured OpenAI-compatible LLM endpoint for a review verdict.""" if deadline is None: deadline = time.monotonic() + NOEMA_LLM_TOTAL_BUDGET_SECONDS + # deadline is an absolute monotonic timestamp shared across the initial + # call and its one possible repair call, by design: response parsing and + # validate_substantive_verdict's own processing time between them also + # count against it, not just network time, so the shared wall-clock + # ceiling holds regardless of where the time goes. remaining_budget = deadline - time.monotonic() if remaining_budget <= 0: raise TimeoutError("Noema LLM review exhausted its total request budget") From 97b3bcc52994e40cdf76a4b8cf30f7b9165f9b82 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 03:30:48 +0000 Subject: [PATCH 56/58] docs(gap-baseline): record naruon#1486 Devin CI-wiring finding + 2 regressions found while fixing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devin's claim (test_stacked_pr_workflow_contract.py never collected by app-ci.yml's backend-scoped pytest) verified real by reading the actual workflow files. Fixed in naruon db97962c. While fixing it, also found a4e01191's stacked-PR trigger change had broken 2 pre-existing contract tests in the same file (stale release/**-branch-list assertions) — also fixed in the same commit. --- docs/product-technical-gap-baseline.md | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 9d52003be..fb6b7cbc7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3546,6 +3546,37 @@ owner `seonghobae`가 `.github#1438`에 직접 남긴 코멘트(진짜 사람 owner 코멘트에 대한 전체 답변은 `.github#1438`에 코멘트로 남겼다(항목별 근거·PR 링크 포함). +## 2026-09-01 시간별 재개: naruon#1486 Devin CI-wiring 지적 검증 — 실재 확인, 수정 중 별개 회귀 2건 추가 발견 + +Devin이 `naruon#1486`(`tests/test_stacked_pr_workflow_contract.py:16`)에 남긴 새 지적: "Application CI가 +`backend`에서 pytest를 실행하므로 `test_governed_pull_request_workflows_accept_stacked_base_branches`가 +전혀 collect되지 않는다"를 추측이 아니라 실제 워크플로/스크립트를 직접 읽어 검증했다. + +1. **✅ 실재 확인 후 수정.** `app-ci.yml`의 backend job은 `cd backend && python -m pytest -q`만 + 실행하므로 repo-root `tests/`는 collection 경로 밖이다. `pr-governance.yml`(다른 유일한 + `scripts/ci`-연관 워크플로)도 `scripts/ci/pr_governance_gate.sh`를 직접 실행할 뿐, + `scripts/ci/test_pr_governance_gate.sh`(bash 자체-테스트)나 이 pytest 파일을 전혀 건드리지 + 않는다 — owner 코멘트의 "`test_pr_governance_gate: PASS`"는 그 bash 스크립트의 로컬 실행 + 증거일 뿐, 이 pytest 계약의 CI 배선 증거가 아니었다. `app-ci.yml`의 backend job에 + `python -m pytest -q tests` 스텝을 추가하고, 이를 잠그는 회귀 테스트 + (`backend/tests/test_release_governance.py::test_app_ci_collects_repository_root_governance_contract_tests`)를 + 추가해 진짜 RED(스텝 부재로 assert 실패) → GREEN 확인. +2. **🔴 별개 발견(수정 중 우연히 드러남, 이 지적과 무관하지만 같은 head의 실재 결함): 같은 + `a4e01191`이 stacked-PR 지원을 위해 4개 워크플로(`app-ci.yml`, `bandit.yml`, + `dependency-review.yml`, `docker-publish.yml`)의 `pull_request:` 트리거에서 `branches:` + 제한을 제거했는데, 그 리터럴 브랜치 목록(`release/**`, `develop`)을 그대로 assert하던 기존 + 계약 테스트 2개가 이미 깨져 있었다.** `backend/tests/test_release_governance.py`만 단독 + 실행하면 2 failed — owner 코멘트의 "workflow/Alembic contracts 29 passed"는 이 파일 전체를 + 포함한 실행이 아니었던 것으로 보인다. 두 테스트(`test_app_ci_runs_backend_and_frontend_checks_without_duplicate_release_pushes`, + `test_docker_publish_validates_pr_images_and_publishes_semver_images_only_on_tags`)를 새 의도 + (`branches:`가 `pull_request:` 아래 전혀 없어야 스택형 PR 베이스를 배제하지 않는다)에 맞게 + 갱신 — 워크플로 자체를 되돌리지 않음(`test_stacked_pr_workflow_contract.py`가 이미 그 방향을 + 명시적으로 요구). + +커밋 `db97962c`(naruon). 전체 백엔드 스위트 1906 passed / 40 skipped, repo-root `tests/` 1 passed, +ruff clean, `scripts/ci/test_pr_governance_gate.sh: PASS`. Devin 스레드 +(`PRRT_kwDOSNjZ2s6d9SmZ`)에 근거를 남기고 resolve 처리했다. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. From e18620355dcb10b921f393b2b80053bca3376939 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 03:54:22 +0000 Subject: [PATCH 57/58] fix(noema/sidecar): enforce an absolute wall-clock deadline, add explicit job timeout, detect a dead sidecar on every attempt Three Devin Review findings against the prior timeout/deadline fix, all verified real: 1. noema_review_gate.py::call_llm relied solely on urllib's timeout= argument, which bounds per-socket-operation inactivity, not the request's total wall-clock duration. A response trickling at least one byte before each such window elapses could keep the shared 5.5-hour budget unenforced indefinitely. The network call now runs on a daemon thread; call_llm enforces the real deadline via Thread.join(timeout=remaining_budget), so a trickling connection is preempted at the actual budget boundary regardless of how the far end paces its response. 2. The noema-review job carried no explicit timeout-minutes, leaving the relationship between its 5.5-hour LLM budget and GitHub's implicit 360-minute default unauditable. Made it explicit. 3. contextual_orchestrator_review_sidecar.sh's gateway-preflight retry loop only checked whether the sidecar process had died on the last configured attempt, so a sidecar that died on attempt 1 still burned the remaining attempts (each up to 120s) before detection -- the original incident this branch exists for shows all 3 attempts took roughly the full 120s each. Moved the dead-sidecar check to run immediately after any failed attempt. Each fix verified genuine RED against the pre-fix code/tests before being restored to GREEN. Full suite: 2214 passed, 1 skipped, 21 subtests, coverage 100%, interrogate 100%. --- .github/workflows/noema-review.yml | 8 ++ CHANGELOG.md | 24 ++++++ docs/product-technical-gap-baseline.md | 45 ++++++++++ .../contextual_orchestrator_review_sidecar.sh | 65 +++++++------- scripts/ci/noema_review_gate.py | 44 +++++++++- ...al_orchestrator_review_sidecar_contract.py | 47 +++++----- ...st_noema_orchestrator_workflow_contract.py | 20 +++++ tests/test_noema_review_gate.py | 85 +++++++++++++++++++ 8 files changed, 287 insertions(+), 51 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index df72f616c..f2b9fd1f0 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -189,6 +189,14 @@ jobs: noema-review: name: noema-review runs-on: ubuntu-latest + # Explicit, not just the platform's own 360-minute (6h) default for + # unset timeout-minutes: NOEMA_LLM_TOTAL_BUDGET_SECONDS (19800s = 5.5h, + # scripts/ci/noema_review_gate.py) already budgets this job against that + # ceiling and relies on ~30 minutes of margin for checkout, credential + # minting, and posting the review; stating it here makes the relationship + # auditable/testable instead of implicit (Devin Review, + # ContextualWisdomLab/.github#1438). + timeout-minutes: 360 if: >- github.event_name == 'repository_dispatch' || ( diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d08daabd..41137eb0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- **(Devin 리뷰 대응, 실재 결함 3건) 지난 timeout/deadline 수정 자체에 대한 Devin의 후속 지적 3건 + 반영.** (1) 🟡 `scripts/ci/noema_review_gate.py::call_llm`의 공유 데드라인이 + `urllib.request`의 `timeout=` 인자만으로 강제되고 있었는데, 그 인자는 소켓 단일 연산의 + inactivity(무응답) 타임아웃일 뿐 요청 전체의 wall-clock 상한이 아니다 -- 응답이 각 타임아웃 + 구간이 끝나기 전에 최소 1바이트씩 계속 흘러들어오면(trickle) 5.5시간 공유 예산을 무한정 + 초과할 수 있었다. 실제 네트워크 호출을 daemon 스레드에서 실행하고 `Thread.join(timeout= + remaining_budget)`으로 절대 wall-clock 데드라인을 강제하도록 재작성 -- daemon이라 데드라인 + 초과 후에도 스레드가 계속 트리클을 기다리고 있어도 프로세스 종료를 막지 않는다. 실제 + wall-clock을 사용하는(모킹하지 않는) 새 회귀 테스트로 진짜 RED(2초 트리클 응답이 0.05초 + 데드라인을 무시하고 정상 반환) → GREEN(0.24초 내 TimeoutError) 확인. 큐를 통한 예외 전달 + 경로 자체의 커버리지 공백(성공 케이스만 있고 전송 실패 케이스가 없었음)도 별도 테스트로 + 메움. (2) 🔍 `noema-review.yml`의 `noema-review` job에 명시적 `timeout-minutes`가 없어 + 5.5시간 예산과 GitHub의 암묵적 360분(6시간) 기본값 사이의 관계가 감사 불가능했다 -- + `timeout-minutes: 360`을 명시하고 계약 테스트 추가, 진짜 RED(부재) → GREEN 확인. (3) 🔍 + `scripts/ci/contextual_orchestrator_review_sidecar.sh`의 게이트웨이 preflight 재시도 + 루프에서, 사이드카가 죽었는지 확인하는 `kill -0` 체크가 마지막 시도에서만 실행되어, 1번째 + 시도에서 이미 죽은 사이드카도 나머지 시도(기본 3회, 시도당 최대 120초)를 모두 소진한 뒤에야 + 감지되었다 -- 실제 이 분기가 존재하게 된 사고(Strix job 99337282309, .github#1460)의 + 근거 자체가 "6분 동안 3번의 시도가 모두 실패"로, 정확히 3×120초에 해당해 매 시도가 거의 + 풀타임 소요됐음을 보여준다. 데드-사이드카 체크를 매 실패 시도마다(시도 예산 소진 여부와 + 무관하게) 즉시 실행하도록 이동. 계약 테스트를 새 구조(체크가 attempt-budget 검사보다 먼저 + 실행됨)에 맞게 재작성, 옛 스크립트에 대해 진짜 RED(`26104 < 25156` 실패) 확인 후 복원해 + GREEN. 세 항목 모두 `bash -n` 문법 확인, 전체 스위트 2214 passed/1 skipped/21 subtests, + coverage 100%, interrogate 100%. - Fix a false-positive in `scripts/ci/test_strix_quick_gate.sh`'s `assert_opencode_review_uses_codegraph_and_contextual_orchestrator`: its `awk '/^ required-workflow-bootstrap:$/,/^[^ ]/'` range never actually diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index fb6b7cbc7..7085ac1ea 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3577,6 +3577,51 @@ Devin이 `naruon#1486`(`tests/test_stacked_pr_workflow_contract.py:16`)에 남 ruff clean, `scripts/ci/test_pr_governance_gate.sh: PASS`. Devin 스레드 (`PRRT_kwDOSNjZ2s6d9SmZ`)에 근거를 남기고 resolve 처리했다. +## 2026-09-01 웨이크업: naruon#1486 CI-wiring 수정 자체에 대한 Devin 후속 지적 2건 + `.github#1438` +Devin 후속 지적 3건 — 모두 실재, 모두 수정 + +이번 웨이크업은 두 PR에서 각각 "직전 수정 자체를 겨냥한" Devin의 새 리뷰를 받았다. 둘 다 추측이 +아니라 코드/스크립트를 직접 추적해 실재를 확인한 뒤 수정했다. + +**naruon#1486 (2건, 둘 다 실재):** +1. **CI 배선 테스트가 주석도 통과시킴.** 직전 커밋의 회귀 테스트가 `"pytest -q tests" in + workflow` 원문 substring 검사여서, 실제 스텝이 삭제되고 같은 문자열이 주석으로만 남아도 + 통과했다. `yaml.safe_load`로 워크플로를 파싱해 `jobs.backend.steps[].run`에 실제로 존재하는 + 스텝만 인정하도록 재작성 — 손수 만든 fixture(주석만 남은 YAML)로 매치 0건임을 직접 확인. +2. **repo-root `tests/` 스텝에 출력 스크리닝 누락.** 기존 backend 테스트 스텝과 달리 새 + 스텝에는 Timeout/Fatal/Warn/Denied 그레핑이 없어, 이 스텝만 금지된 출력을 내고도 CI를 + 통과할 수 있었다. 동일한 `grep -qiE` 가드 추가, 회귀 테스트에 통합. 두 항목 모두 수정 전 + 상태로 되돌려 진짜 RED 확인 후 복원 → GREEN. 커밋 `ede7f4b1`. 전체 백엔드 스위트 1906 + passed/40 skipped, ruff clean. 두 Devin 스레드에 근거를 남기고 resolve 처리했다. + +**`.github#1438` (3건, 모두 실재):** +1. **🟡 공유 데드라인이 trickle 응답에 뚫린다.** `scripts/ci/noema_review_gate.py::call_llm`이 + `urllib.request`의 `timeout=` 인자 하나로만 5.5시간 공유 예산을 강제하고 있었는데, 그 + 인자는 소켓 단일 연산의 inactivity 타임아웃일 뿐 요청 전체의 wall-clock 상한이 아니다 — + 응답이 각 타임아웃 구간이 끝나기 전에 최소 1바이트씩 계속 흘러들어오면(trickle) 실제 + 데드라인을 무한정 초과할 수 있었다. 실제 네트워크 호출을 daemon 스레드에서 실행하고 + `Thread.join(timeout=remaining_budget)`으로 절대 wall-clock 데드라인을 강제하도록 재작성 — + daemon 스레드이므로 데드라인 초과 후에도 (여전히 trickle을 기다리며) 살아있어도 프로세스 + 종료를 막지 않는다. 실제 wall-clock을 쓰는(모킹하지 않는) 새 테스트로 진짜 RED(2초 trickle + 응답이 0.05초 데드라인을 무시하고 정상 반환) → GREEN(0.24초 내 TimeoutError) 확인. 큐를 + 통한 예외 전달 경로 자체의 커버리지 공백(성공 케이스만 있고 전송 실패 케이스가 없었음)도 + 별도 테스트로 메움. +2. **🔍 job의 6시간 상한이 암묵적이었다.** `noema-review` job에 명시적 `timeout-minutes`가 + 없어 5.5시간 예산과 GitHub의 암묵적 360분 기본값 사이의 관계가 감사 불가능했다 — + `timeout-minutes: 360`을 명시하고 계약 테스트 추가, 진짜 RED(부재) → GREEN 확인. +3. **🔍 죽은 사이드카 감지가 마지막 시도까지 지연됐다.** `scripts/ci/contextual_orchestrator_review_sidecar.sh`의 + 게이트웨이 preflight 재시도 루프에서 `kill -0` 사이드카 생존 체크가 마지막 시도에서만 + 실행되어, 1번째 시도에서 이미 죽은 사이드카도 나머지 시도(기본 3회, 시도당 최대 120초)를 + 모두 소진한 뒤에야 감지되었다 — 이 분기가 존재하게 된 실제 사고(Strix job 99337282309, + .github#1460)의 근거 자체가 "6분 동안 3번의 시도가 모두 실패"로, 정확히 3×120초에 해당해 + 매 시도가 거의 풀타임 소요됐음을 보여준다(즉 이론적 우려가 아니라 실측 낭비). 데드-사이드카 + 체크를 매 실패 시도마다(시도 예산 소진 여부와 무관하게) 즉시 실행하도록 이동. 계약 테스트를 + 새 순서(체크가 attempt-budget 검사보다 먼저 실행됨)에 맞게 재작성 — 옛 스크립트에 대해 진짜 + RED(`26104 < 25156` 실패) 확인 후 복원해 GREEN. + +세 항목 모두 `bash -n` 문법 확인, 전체 스위트 2214 passed/1 skipped/21 subtests, coverage 100%, +interrogate 100%. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 85a8bde36..bcc4a8622 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -507,28 +507,30 @@ while :; do if [ "$gateway_http_status" = "200" ]; then break fi - if [ "$gateway_attempt" -ge "$REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS" ]; then - if [ -z "$gateway_http_status" ]; then - # A transport failure on every attempt has two structurally different - # causes that "could not reach the sidecar" alone cannot distinguish: - # the sidecar process is still running (a real network/gateway issue), - # or it has already exited (its own bug/crash/OOM, unrelated to the - # network at all). Check which one this is before recording generic - # transport-exhausted evidence, so a died sidecar is never - # misclassified as merely unreachable -- exact-head evidence: Strix - # run/job 33341290448/99337282309 for ContextualWisdomLab/.github#1460 - # target 2cc819a9 passed healthz/provider-route readiness after 23s, - # then six minutes later all 3 gateway-preflight attempts failed to - # reach the sidecar at all, with no evidence of whether it had died. - if ! kill -0 "$sidecar_pid" 2>/dev/null; then - sidecar_exit_status=0 - wait "$sidecar_pid" 2>/dev/null || sidecar_exit_status=$? - # The sidecar has fully exited (confirmed above), so draining here - # cannot hang, and it guarantees $sidecar_stderr holds everything the - # sidecar wrote before we read it -- the same discipline the healthz - # branch above uses for the same reason. - wait_for_sidecar_sanitizers - "$sidecar_python" - "$preflight_report" "$gateway_attempt" "$sidecar_exit_status" <<'PY' + if [ -z "$gateway_http_status" ] && ! kill -0 "$sidecar_pid" 2>/dev/null; then + # A transport failure has two structurally different causes that "could + # not reach the sidecar" alone cannot distinguish: the sidecar process is + # still running (a real network/gateway issue), or it has already exited + # (its own bug/crash/OOM, unrelated to the network at all). Check which + # one this is on EVERY failed attempt, not only once the attempt budget + # is exhausted -- a died sidecar can never succeed on a later retry, so + # waiting for the remaining attempts only delays detection. Exact-head + # evidence -- Strix run/job 33341290448/99337282309 for + # ContextualWisdomLab/.github#1460 target 2cc819a9: the sidecar passed + # healthz/provider-route readiness after 23s, then six minutes later all + # 3 gateway-preflight attempts failed to reach it at all (roughly the + # full 3 x 120s max-time budget, since the last-attempt-only check that + # used to live here could not have caught it any earlier); Devin Review + # (ContextualWisdomLab/.github#1438) is what prompted checking on every + # attempt instead. + sidecar_exit_status=0 + wait "$sidecar_pid" 2>/dev/null || sidecar_exit_status=$? + # The sidecar has fully exited (confirmed above), so draining here + # cannot hang, and it guarantees $sidecar_stderr holds everything the + # sidecar wrote before we read it -- the same discipline the healthz + # branch above uses for the same reason. + wait_for_sidecar_sanitizers + "$sidecar_python" - "$preflight_report" "$gateway_attempt" "$sidecar_exit_status" <<'PY' import json from pathlib import Path import sys @@ -552,13 +554,18 @@ temporary = report_path.with_suffix(".tmp") temporary.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") temporary.replace(report_path) PY - fail "sidecar process exited after readiness, before gateway preflight completed (status ${sidecar_exit_status}); stderr: $(sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" "$sidecar_stderr")" - fi - # Sidecar still running -- every configured attempt exhausted with no - # usable HTTP response at all (Trigger A never resolved) -- record that - # before failing closed, using the same sanitize-then-atomic-replace - # pattern as the non-2xx and invalid-content paths below, so this exact - # failure case (the one telemetry matters most for) does not leave zero + fail "sidecar process exited after readiness, before gateway preflight completed (status ${sidecar_exit_status}); stderr: $(sed -n "1,${SIDECAR_STDERR_TAIL_LINES}p" "$sidecar_stderr")" + fi + if [ "$gateway_attempt" -ge "$REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS" ]; then + if [ -z "$gateway_http_status" ]; then + # The dead-sidecar case is already ruled out above (it fails closed + # immediately, before this point is ever reached), so every attempt + # counted here was against a sidecar confirmed still running -- every + # configured attempt exhausted with no usable HTTP response at all + # (Trigger A never resolved) -- record that before failing closed, + # using the same sanitize-then-atomic-replace pattern as the non-2xx + # and invalid-content paths below, so this exact failure case (the one + # telemetry matters most for) does not leave zero # evidence trail. "$sidecar_python" - "$preflight_report" "$gateway_attempt" <<'PY' import json diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index a7eae0a7b..89fe12cc8 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -10,10 +10,12 @@ import ipaddress import json import os +import queue import re import socket import subprocess import sys +import threading import time import urllib.error import urllib.parse @@ -909,6 +911,29 @@ class StaleHeadDuringRepairRetryError(RuntimeError): """Raised when the PR head moves before ``call_llm``'s repair-retry request fires.""" +def _open_llm_request( + opener: urllib.request.OpenerDirector, + request: urllib.request.Request, + timeout: float, + result_queue: "queue.Queue[tuple[str, Any]]", +) -> None: + """Run the blocking LLM call on a background thread. + + ``urllib``'s own ``timeout=`` argument bounds only per-socket-operation + inactivity, not the request's total wall-clock duration: a response that + keeps sending at least one byte before each such window elapses can keep + a synchronous caller blocked indefinitely (Devin Review, + ContextualWisdomLab/.github#1438). Running the call here lets ``call_llm`` + enforce its own absolute shared deadline with ``Thread.join(timeout=...)`` + regardless of how the far end paces its response. + """ + try: + with opener.open(request, timeout=timeout) as response: # nosec B310 + result_queue.put(("ok", response.read())) + except BaseException as exc: # noqa: BLE001 - forwarded to the caller thread, never swallowed + result_queue.put(("error", exc)) + + def call_llm( repo: str, number: int, @@ -1000,8 +1025,23 @@ def call_llm( method="POST", ) opener = urllib.request.build_opener(NoRedirectHandler()) - with opener.open(request, timeout=request_timeout) as response: # nosec B310 - raw_bytes = response.read() + result_queue: "queue.Queue[tuple[str, Any]]" = queue.Queue(maxsize=1) + request_thread = threading.Thread( + target=_open_llm_request, + args=(opener, request, request_timeout, result_queue), + daemon=True, + ) + request_thread.start() + request_thread.join(timeout=remaining_budget) + if request_thread.is_alive(): + raise TimeoutError( + "Noema LLM review exceeded its shared wall-clock budget " + "(the connection kept receiving data past the deadline)" + ) + status, payload = result_queue.get_nowait() + if status == "error": + raise payload + raw_bytes = payload try: raw = decode_llm_response_body(raw_bytes) content = extract_llm_message_content(raw) diff --git a/tests/test_contextual_orchestrator_review_sidecar_contract.py b/tests/test_contextual_orchestrator_review_sidecar_contract.py index 4ecdc92e7..0049f2eb6 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -570,37 +570,44 @@ def test_required_strix_uses_the_gateway_and_zdr_visibility_contract() -> None: def test_gateway_preflight_distinguishes_a_dead_sidecar_from_an_unreachable_one() -> None: """A sidecar that dies between readiness and gateway preflight gets its own diagnosis (exit status + stderr tail), not the generic transport-exhausted - message a merely-unreachable-but-still-running sidecar gets. + message a merely-unreachable-but-still-running sidecar gets -- and that + diagnosis must run on every failed attempt, not only once the attempt + budget is exhausted. Exact-head evidence (Strix run/job 33341290448/99337282309 for ContextualWisdomLab/.github#1460 target 2cc819a9): the sidecar passed healthz/provider-route readiness after 23s, then six minutes later all 3 - gateway-preflight attempts failed to reach it at all. The existing - ``gateway_transport_exhausted`` branch cannot tell that case apart from a - sidecar that is still running but merely unreachable over the network -- - it reports the same generic message either way, discarding the one piece - of evidence (the process's own exit status) that would tell an operator - whether the sidecar crashed. + gateway-preflight attempts failed to reach it at all -- roughly the full + 3 x 120s max-time budget, since a last-attempt-only dead-sidecar check + cannot detect a process that already died on attempt 1 until the + remaining attempts are burned against it first (Devin Review, + ContextualWisdomLab/.github#1438). The ``gateway_transport_exhausted`` + branch also cannot tell a died sidecar apart from one that is merely + unreachable over the network -- it reports the same generic message + either way, discarding the one piece of evidence (the process's own exit + status) that would tell an operator whether the sidecar crashed. """ text = _read(SIDECAR) + loop_start = text.index("gateway_attempt=1") exhausted_branch = text.index("gateway_transport_exhausted") - # Scope strictly to this branch's own opening condition so the search - # cannot accidentally match the unrelated, textually-earlier "sidecar - # exited before healthz" branch's own `kill -0 "$sidecar_pid"` check. - unreachable_branch_start = text.rindex( - 'if [ -z "$gateway_http_status" ]; then', 0, exhausted_branch + max_attempts_check = text.index( + 'if [ "$gateway_attempt" -ge "$REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS" ]; then', + loop_start, ) - dead_sidecar_check = text.index( - 'kill -0 "$sidecar_pid"', unreachable_branch_start, exhausted_branch - ) - # The dead-sidecar diagnosis must run BEFORE the generic - # transport-exhausted evidence is recorded, so a died sidecar is never - # misclassified as merely unreachable. - assert dead_sidecar_check < exhausted_branch + # Scoped to start at the preflight loop itself so this cannot + # accidentally match the unrelated, textually-earlier "sidecar exited + # before healthz" branch's own `kill -0 "$sidecar_pid"` check. + dead_sidecar_check = text.index('kill -0 "$sidecar_pid"', loop_start) + # The dead-sidecar diagnosis must run on every failed attempt -- before + # the attempt-budget check even runs, let alone the generic + # transport-exhausted evidence recorded once it is exhausted -- so a + # died sidecar is detected immediately, not only after the retry budget + # is spent, and is never misclassified as merely unreachable. + assert dead_sidecar_check < max_attempts_check < exhausted_branch dead_sidecar_message = text.index( "sidecar process exited after readiness, before gateway preflight completed" ) - assert dead_sidecar_check < dead_sidecar_message < exhausted_branch + assert dead_sidecar_check < dead_sidecar_message < max_attempts_check # Must reuse the same drain-then-read discipline as the healthz branch: # wait() for the confirmed-dead child before reading its stderr tail, and # drain the sanitizer first so the tail is not read mid-flight. diff --git a/tests/test_noema_orchestrator_workflow_contract.py b/tests/test_noema_orchestrator_workflow_contract.py index 3b9c5baee..6a59042c4 100644 --- a/tests/test_noema_orchestrator_workflow_contract.py +++ b/tests/test_noema_orchestrator_workflow_contract.py @@ -4,6 +4,7 @@ import os import json +import re import shutil import subprocess import textwrap @@ -185,6 +186,25 @@ def test_noema_review_credentials_and_llm_use_orchestrator_free() -> None: assert "secrets: inherit" not in workflow +def test_noema_review_job_has_an_explicit_timeout_matching_its_llm_budget() -> None: + """The job's own timeout must be explicit, not just GitHub's implicit default. + + Devin Review (ContextualWisdomLab/.github#1438): NOEMA_LLM_TOTAL_BUDGET_SECONDS + (19800s = 5.5h, scripts/ci/noema_review_gate.py) is budgeted against the + platform's 360-minute (6h) hosted-runner ceiling, but that relationship + previously relied on nothing being set here at all -- an unrelated future + edit to this job could silently shrink or drop that relied-upon margin + with no contract catching it. + """ + workflow = workflow_text("noema-review.yml") + marker = "\n noema-review:\n" + start = workflow.index(marker) + len(marker) + remainder = workflow[start:] + next_job = re.search(r"\n [a-zA-Z_-]+:\n", remainder) + job_block = remainder[: next_job.start()] if next_job else remainder + assert "timeout-minutes: 360" in job_block + + def _expected_head_from_workflow_run_event(event: dict) -> str: """Mirror EXPECTED_HEAD's ``||`` fallback chain for a ``workflow_run`` event. diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index d9978bddc..d5613914c 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -7,6 +7,7 @@ import subprocess import sys import textwrap +import time from pathlib import Path import pytest @@ -2163,6 +2164,90 @@ def open(self, request, timeout): # pragma: no cover - must never be reached ) +def test_call_llm_enforces_absolute_deadline_against_a_trickling_response(monkeypatch): + """A response that keeps sending bytes must not block past the shared deadline. + + Devin Review (ContextualWisdomLab/.github#1438): urllib's own + ``timeout=`` parameter bounds only per-socket-operation inactivity, not + the request's total wall-clock duration. A gateway that trickles at + least one byte before each such window elapses could keep call_llm + blocked indefinitely, exceeding NOEMA_LLM_TOTAL_BUDGET_SECONDS with + nothing left to enforce the reserved cleanup margin. This uses real + wall-clock time (not a monkeypatched clock) because it is asserting on + actual blocking behavior, not on the value passed as a timeout argument. + """ + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example/v1/chat/completions") + monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") + monkeypatch.setattr(noema, "validate_substantive_verdict", lambda *_args: None) + + class TricklingResponse: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def read(self): + # Far longer than the deadline below -- simulates a connection + # that keeps resetting its own per-operation inactivity timeout. + # Returns a well-formed verdict so the old, unfixed code path + # (which would simply block here for the full 2s) completes + # normally instead of failing for an unrelated reason. + time.sleep(2.0) + return json.dumps( + { + "choices": [ + { + "message": { + "content": json.dumps( + {"decision": "approve", "summary": "clean", "findings": []} + ) + } + } + ] + } + ).encode() + + class Opener: + def open(self, _request, timeout): + return TricklingResponse() + + monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) + + deadline = noema.time.monotonic() + 0.05 + start = noema.time.monotonic() + with pytest.raises(TimeoutError, match="shared wall-clock budget"): + noema.call_llm( + "owner/repo", 7, make_pr(), "diff", False, "head", deadline=deadline + ) + elapsed = noema.time.monotonic() - start + assert elapsed < 1.0, f"call_llm blocked past its absolute deadline: {elapsed}s" + + +def test_call_llm_propagates_a_transport_error_raised_on_the_background_thread(monkeypatch): + """A genuine transport failure inside the background request thread must + still surface to the caller unchanged, not be swallowed by the queue. + + ``_open_llm_request`` captures any exception from ``opener.open()``/ + ``response.read()`` onto a queue so ``call_llm``'s own absolute-deadline + join can preempt a trickling connection (see the sibling deadline test + above); this proves that plumbing does not alter or discard a genuine + ``URLError`` -- the realistic outcome when the per-request socket + inactivity timeout itself fires with no data at all. + """ + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example/v1/chat/completions") + monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") + + class Opener: + def open(self, _request, timeout): + raise noema.urllib.error.URLError(TimeoutError("timed out")) + + monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) + + with pytest.raises(noema.urllib.error.URLError): + noema.call_llm("owner/repo", 7, make_pr(), "diff", False, "head") + + def test_substantive_approve_requires_exact_changed_lines_and_falsified_probes(): diff = """diff --git a/tool.py b/tool.py --- a/tool.py From f00d0396c1801cff5c25ccb212da99722279e281 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 04:03:54 +0000 Subject: [PATCH 58/58] fix(noema): join on the per-request timeout, not the full shared budget Devin Review found a real regression in the prior daemon-thread deadline fix (e1862035): call_llm joined the request thread with timeout=remaining_budget (the full shared 5.5h budget) instead of timeout=request_timeout (this call's own cap, already min()'d against the shared budget). A single trickling response could then consume the entire shared budget, leaving nothing for a validator-rejected repair call -- exactly the failure NOEMA_LLM_REQUEST_TIMEOUT_SECONDS was introduced to prevent. Fixed to join on request_timeout, matching Devin's suggested one-line change. Also corrected a stale comment claiming the noema-review job has no explicit timeout-minutes (this PR already added one in e1862035). Verified genuine RED (a 0.05s per-request cap was ignored while 10s of shared budget remained, letting a 0.3s trickle complete normally) before the fix, GREEN after. Full suite: 2215 passed, 1 skipped, 21 subtests, coverage 100%, interrogate 100%. --- CHANGELOG.md | 15 +++++++ docs/product-technical-gap-baseline.md | 26 +++++++++++ scripts/ci/noema_review_gate.py | 16 ++++--- tests/test_noema_review_gate.py | 60 +++++++++++++++++++++++++- 4 files changed, 111 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41137eb0d..3f361e6bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- **(Devin 리뷰 대응, 🟡 실제 결함 1건 + 🔍 문서 정정 1건) daemon-스레드 데드라인 fix 자체에 대한 + Devin의 후속 지적 2건 반영.** (1) 🟡 `call_llm`이 `request_thread.join(timeout= + remaining_budget)`(공유 5.5시간 예산 전체)으로 join하고 있었는데, 이는 개별 호출의 상한이어야 + 할 `request_timeout`(`min(NOEMA_LLM_REQUEST_TIMEOUT_SECONDS, remaining_budget)`, 이미 공유 + 예산에 맞춰 min된 값)을 사실상 무시하는 것이었다 — trickle 응답 하나가 3시간이 아니라 공유 + 예산 전체(5.5시간)를 다 써버릴 수 있어, `NOEMA_LLM_REQUEST_TIMEOUT_SECONDS`가 애초에 막으려던 + "한 호출이 repair 재시도 몫까지 다 태운다"는 바로 그 실패를 재도입하고 있었다. `join(timeout= + request_timeout)`으로 수정(Devin의 제안과 동일). 실제 wall-clock을 쓰는 새 테스트로 진짜 + RED(공유 예산에 여유(10초)가 있으면 개별 호출 상한(0.05초)을 무시하고 0.3초 trickle이 정상 + 완료됨) → GREEN(0.05초 내 TimeoutError) 확인. 에러 메시지도 "shared wall-clock budget"에서 + "per-request wall-clock budget"으로 정정(기존 트리클 테스트의 match 문자열도 함께 갱신). + (2) 🔍 `NOEMA_LLM_TOTAL_BUDGET_SECONDS` 위 주석이 "noema-review job에 명시적 + timeout-minutes가 없다"고 말하고 있었는데, 바로 이 PR이 그 job에 `timeout-minutes: 360`을 + 추가했으므로 이미 낡은 서술이 되어 있었다 — 주석을 현재 상태를 반영하도록 갱신. 전체 스위트 + 2215 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%. - **(Devin 리뷰 대응, 실재 결함 3건) 지난 timeout/deadline 수정 자체에 대한 Devin의 후속 지적 3건 반영.** (1) 🟡 `scripts/ci/noema_review_gate.py::call_llm`의 공유 데드라인이 `urllib.request`의 `timeout=` 인자만으로 강제되고 있었는데, 그 인자는 소켓 단일 연산의 diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 7085ac1ea..eea096212 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3622,6 +3622,32 @@ Devin 후속 지적 3건 — 모두 실재, 모두 수정 세 항목 모두 `bash -n` 문법 확인, 전체 스위트 2214 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%. +## 2026-09-01 웨이크업: daemon-스레드 데드라인 fix 자체에 대한 Devin 4번째 라운드 — join이 +공유 예산 전체로 걸려 있어 개별 호출 상한이 무효화되던 실재 회귀 1건 확인, 수정 + +바로 직전 패스에서 daemon 스레드 + `Thread.join`으로 trickle 취약점을 고친 그 커밋(`e1862035`) +자체에 대해 Devin이 다시 2건을 지적했다 — 둘 다 코드를 직접 추적해 실재 확인했다. + +1. **🟡 3시간 개별 호출 상한이 실제로는 강제되지 않았다.** `call_llm`이 + `request_thread.join(timeout=remaining_budget)`로 join하고 있었는데, `remaining_budget`은 + 공유 총예산(초기 호출 시점엔 5.5시간) 전체다. 반면 `request_timeout = + min(NOEMA_LLM_REQUEST_TIMEOUT_SECONDS, remaining_budget)`(3시간, 이미 공유 예산에 맞춰 + min된 값)은 옆에서 계산만 되고 join에는 전달되지 않았다 — 즉 trickle 응답 하나가 3시간이 + 아니라 공유 예산 5.5시간 전체를 다 써버릴 수 있어, `NOEMA_LLM_REQUEST_TIMEOUT_SECONDS`가 + 애초에 막으려던 "한 호출이 repair 재시도 몫까지 다 태운다"는 바로 그 실패(이전 owner + 지적사항의 원래 문제)를 다른 경로로 재도입하고 있었다. `join(timeout=request_timeout)`으로 + 수정(Devin이 제안한 것과 동일한 한 줄 수정). 실제 wall-clock을 쓰는 새 테스트로 진짜 + RED(공유 예산에 10초 여유가 있으면 0.05초 개별-호출 상한을 무시하고 0.3초 trickle 응답이 + 정상 완료됨, `DID NOT RAISE TimeoutError`) → GREEN(0.05초 내 TimeoutError) 확인. 에러 + 메시지도 "shared wall-clock budget"에서 "per-request wall-clock budget"으로 정정, 기존 + 트리클 테스트의 match 문자열도 함께 갱신. +2. **🔍 주석이 이 PR 자체가 만든 변경으로 낡아 있었다.** `NOEMA_LLM_TOTAL_BUDGET_SECONDS` 위 + 주석이 "noema-review job에 명시적 timeout-minutes가 없다"고 서술했는데, 몇 커밋 전 이 PR이 + 바로 그 job에 `timeout-minutes: 360`을 추가했으므로 이미 낡은 서술이었다 — 현재 상태를 + 반영하도록 갱신. + +전체 스위트 2215 passed/1 skipped/21 subtests, coverage 100%, interrogate 100%. + ## 6. Compliance and data boundary - PII 원문을 무조건 masking하여 업무를 끊지 않는다. 대신 purpose-bound access lease, field-level encryption/tokenization, consented minimal-disclosure consequence, audited access, revocation/deletion을 사용한다. `COPILOT_GITHUB_TOKEN`은 사용하지 않는다. diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 89fe12cc8..87798208d 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -53,9 +53,10 @@ # the entire required review. 10800s gives the gateway's own internal # retry/failover machinery room to land on a working agent instead. NOEMA_LLM_REQUEST_TIMEOUT_SECONDS = 10800 -# The noema-review job carries no explicit timeout-minutes, so it is bounded -# only by the 360-minute (6h) maximum job execution time GitHub-hosted -# runners allow. call_llm can recurse once for a single validator-rejected +# noema-review.yml's noema-review job now states timeout-minutes: 360 +# explicitly, matching the 360-minute (6h) maximum job execution time +# GitHub-hosted runners allow in the first place. call_llm can recurse once +# for a single validator-rejected # repair (see the repair_error branch below); two independent # NOEMA_LLM_REQUEST_TIMEOUT_SECONDS calls would total 21600s -- exactly that # 6h ceiling, leaving zero room for sidecar provisioning or job cleanup and @@ -1032,10 +1033,15 @@ def call_llm( daemon=True, ) request_thread.start() - request_thread.join(timeout=remaining_budget) + # Joins on request_timeout (this call's own cap, already MIN'd against + # remaining_budget), not remaining_budget itself: joining on the full + # shared deadline would let one trickling call consume the whole + # shared budget and leave nothing for a validator-rejected repair call + # (Devin Review, ContextualWisdomLab/.github#1438). + request_thread.join(timeout=request_timeout) if request_thread.is_alive(): raise TimeoutError( - "Noema LLM review exceeded its shared wall-clock budget " + "Noema LLM review exceeded its per-request wall-clock budget " "(the connection kept receiving data past the deadline)" ) status, payload = result_queue.get_nowait() diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index d5613914c..2ff3cf453 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -2216,7 +2216,7 @@ def open(self, _request, timeout): deadline = noema.time.monotonic() + 0.05 start = noema.time.monotonic() - with pytest.raises(TimeoutError, match="shared wall-clock budget"): + with pytest.raises(TimeoutError, match="per-request wall-clock budget"): noema.call_llm( "owner/repo", 7, make_pr(), "diff", False, "head", deadline=deadline ) @@ -2248,6 +2248,64 @@ def open(self, _request, timeout): noema.call_llm("owner/repo", 7, make_pr(), "diff", False, "head") +def test_call_llm_enforces_the_per_request_timeout_not_just_the_shared_budget(monkeypatch): + """A single call must not run past its own per-request cap just because + most of the shared total budget is still unspent. + + Devin Review (ContextualWisdomLab/.github#1438): the deadline-join fix + joined on ``remaining_budget`` (the full shared deadline) instead of + ``request_timeout`` (the per-call cap, already MIN'd against the shared + budget). A single trickling call could then consume the entire shared + budget, leaving nothing for a validator-rejected repair call -- exactly + the failure ``NOEMA_LLM_REQUEST_TIMEOUT_SECONDS`` exists to prevent. Real + wall-clock time is used deliberately, as with the sibling deadline test. + """ + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example/v1/chat/completions") + monkeypatch.setenv("NOEMA_LLM_API_KEY", "test-key") + monkeypatch.setattr(noema, "validate_substantive_verdict", lambda *_args: None) + monkeypatch.setattr(noema, "NOEMA_LLM_REQUEST_TIMEOUT_SECONDS", 0.05) + + class TricklingResponse: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def read(self): + # Longer than the 0.05s per-request cap, but well inside the 10s + # of shared budget still remaining below. + time.sleep(0.3) + return json.dumps( + { + "choices": [ + { + "message": { + "content": json.dumps( + {"decision": "approve", "summary": "clean", "findings": []} + ) + } + } + ] + } + ).encode() + + class Opener: + def open(self, _request, timeout): + return TricklingResponse() + + monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) + + deadline = noema.time.monotonic() + 10 + start = noema.time.monotonic() + with pytest.raises(TimeoutError, match="per-request wall-clock budget"): + noema.call_llm( + "owner/repo", 7, make_pr(), "diff", False, "head", deadline=deadline + ) + elapsed = noema.time.monotonic() - start + assert elapsed < 1.0, f"call_llm ignored its per-request cap: {elapsed}s" + + def test_substantive_approve_requires_exact_changed_lines_and_falsified_probes(): diff = """diff --git a/tool.py b/tool.py --- a/tool.py