diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index df72f616ca..f2b9fd1f06 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/.github/workflows/strix.yml b/.github/workflows/strix.yml index 505053287b..1a88469672 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -167,15 +167,19 @@ 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 -- 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 + # 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 +733,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 +775,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 @@ -802,14 +806,20 @@ 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" 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/.gitignore b/.gitignore index febbc85c48..5b20d782ed 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ __pycache__/ .coverage .pytest_cache/ .codegraph/ +.claude/ strix_runs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 43020db98e..3f361e6bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,62 @@ 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=` ์ธ์ž๋งŒ์œผ๋กœ ๊ฐ•์ œ๋˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ๊ทธ ์ธ์ž๋Š” ์†Œ์ผ“ ๋‹จ์ผ ์—ฐ์‚ฐ์˜ + 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 + 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. - Fail closed when the first top-level Noema JSON candidate is malformed, preventing a later approval object from overriding malformed preface data; multiple-object output remains supported when its first object is valid. @@ -111,6 +167,13 @@ Semantic Versioning where the repository publishes a release. superseded commit, test path, rule, and line, with an executable contract. - Allow a Contextual Orchestrator-backed Noema review request to run for up to four hours instead of failing long reviews at a hard-coded 120 seconds. +- Reconcile two independent fixes for that same 120-second timeout (this + branch's own `NOEMA_LLM_REQUEST_TIMEOUT_SECONDS` and the flat four-hour + `NOEMA_LLM_TIMEOUT_SECONDS` above) into one shared design: a per-call + request timeout (10800s/3h) capped by a shared `NOEMA_LLM_TOTAL_BUDGET_SECONDS` + (19800s/5.5h) monotonic deadline spanning the initial call and its one + possible repair call together, so two independent long calls can never sum + past the 6-hour GitHub-hosted job execution ceiling. - Stop logging raw (even regex-scrubbed) LLM response text in Noema's malformed-JSON fail-closed diagnostic (Devin Review security finding on PR #1507): `noema-review.yml` is a `pull_request_target` workflow with @@ -141,6 +204,52 @@ Semantic Versioning where the repository publishes a release. then rejected via 429/404/timeout). New regression tests pin the default to the policy module's canonical value and forbid the total-routes constant from reappearing as the account-cap fallback. +- 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) > + 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. (Independently + found and fixed by the org owner in the same window, commit `34c88356`, + with a stronger regression test pinning the arithmetic invariant itself โ€” + adopted the owner's version when reconciling the resulting merge conflict.) +- 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. - Fix a dangling reference #1468 left in `docs/product-goal-directive.md` (flagged by Devin Review on that PR): the standing operating directive still named the removed `free_family_diversity` evidence field instead of @@ -705,6 +814,27 @@ Semantic Versioning where the repository publishes a release. `zero_data_retention` stays `False` as it already was; only the citation and note change. See the 2026-08-30 ZDR/NIM-routing gap-baseline entry for the full architecture review this citation was part of. +- Correct `docs/product-technical-gap-baseline.md`'s "2026-08-30 post-#1486/#1438 wake" entry: Bytez + can never populate `orchestrator/free` regardless of its HTTP status (`_parse_bytez` never sets + `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 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`. +- 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 76d85b949b..a7c90e68c8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1292,6 +1292,523 @@ conflicting** PRs address pieces of this: currently blocked by the sidecar-preflight outage above, so neither could be re-reviewed to a genuine pass yet regardless of which approach wins. +## 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. + +## 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. + +**Correction (recorded below, superseded further still by "2026-08-30 sidecar-preflight outage: +consolidated evidence" further above, landed concurrently on `main` while this entry's own follow-up +investigation was in progress): the "removes what may have been the last remaining candidate" framing +above is wrong** โ€” see the correction entry immediately below for why, and the consolidated-evidence +entry above this one for the actual, evidence-confirmed root cause and its fix. + +## 2026-08-30 correction: Bytez was never free-eligible; the real root cause landed concurrently on `main` + +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. + +**My own working theory at the time (partially wrong, corrected here):** this investigation initially +concluded the mechanism was purely generic โ€” two call sites (`discover_provider_models`'s model-list +fetch, and `_preflight_review_agents`'s live `client.proxy_send_once(...)` warm-up probe) each making +exactly one HTTP attempt with zero retry, so *any* transient failure at either one would be +architecturally sufficient to fail the sidecar closed. That mechanism is real (confirmed by code +reading, not superseded), but it is not what actually happened in this specific incident. **The +"2026-08-30 sidecar-preflight outage: consolidated evidence" entry above โ€” landed on `main` from a +parallel, independent, far more thorough investigation with actual hosted-run artifacts this session +never had access to โ€” found the real, precise, evidence-confirmed cause**: `orchestrator/free`'s +candidate selection (`contextual_orchestrator_review_policy.py`'s `family_cap`) is alphabetical with no +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 +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 +correction does not propose, retrying the completion warm-up probe itself; the consolidated-evidence +entry's fix operates one layer earlier, on *which* candidates are ever offered to that probe. + +**What still stands, independent of the correction above:** + +- Points 1 and 2 above (Bytez's structural non-eligibility; the 413 self-test misattribution) are + unaffected by which of the two root-cause theories is right, and remain the correction to make to + this document. +- `ContextualWisdomLab/contextual-orchestrator#923` โ€” a genuinely independent, still-valid resilience + improvement: one bounded retry (short delay, shortened timeout, gated on the existing + `is_transient_error` classifier reused as-is) on the provider *discovery* model-list fetch, a call + site the consolidated-evidence fix above does not touch. This does not fix, and was never confirmed + to fix, the naruon#1486/.github#1438 incident specifically โ€” it is a defense-in-depth improvement for + a real but different failure mode (a transient 5xx during discovery itself, which would otherwise + zero out a provider's entire contribution for that pass). Full suite: 2765 passed, 1 skipped. +- `ContextualWisdomLab/.github#1438` โ€” originally also added a preflight-rejection console-visibility + fix (`_log_preflight_rejections`); **dropped** on discovering the consolidated-evidence fix above + already ships an equivalent, simpler mechanism (`log "sidecar preflight route evidence: ..."` dumping + the already-bounded-safe `preflight_report` JSON directly) โ€” keeping both would have been duplicate, + redundant code solving the same problem two ways. What remains in #1438: the corrected framing in + this document, and the `SIDECAR_STDERR_TAIL_LINES` failure-path log-tail widening (20โ†’60 lines, + 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. + +## 2026-08-30 CodeRabbit์ด ์กฐ์šฉํžˆ ์ž๋™ ๋ฆฌ๋ทฐ๋ฅผ ํ•˜์ง€ ์•Š๋Š” ๊ทผ๋ณธ ์›์ธ: repo star ์ž„๊ณ„๊ฐ’ + +- `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๋Š” ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ–ˆ๋‹ค. +- `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 + ruleset(`CWL Central required workflows`, id `18156473`)์— ์ƒˆ workflow๋ฅผ ๋“ฑ๋กํ•˜๋Š” ์ž‘์—…์ด๋ผ + 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๋ถ„)์ด ์ง€๋‚˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค. + +## 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"`์„ + ๋กœ๊ทธ์— ๋‚จ๊ฒผ๋‹ค โ€” **์›์ธ์„ ์ •ํ™•ํžˆ ํŠน์ •ํ–ˆ๋‹ค**: `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` ์ž์‹ ์˜ ์ปจํ…์ŠคํŠธ์—์„œ ์‹คํ–‰๋  ๋•Œ๋งŒ + ์ฐธ)๋„ ๋งŒ์กฑํ•˜์ง€ ๋ชปํ•˜๋ฏ€๋กœ, **์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ `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์€ ๋ณ„๋„ ๊ฒฝ๋กœ๋‹ค. **์ถ”๊ฐ€ ์ •์ •(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 + `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์—์„œ ์กฐ์‚ฌํ•œ๋‹ค. +- **์ •์ • (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` + ์กฐํ•ฉ์„ ๋ฌด์กฐ๊ฑด 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`์— ๋ณ‘ํ•ฉํ–ˆ๋‹ค. + +## 2026-08-30 ์‹œ๊ฐ„๋ณ„ ์žฌ๊ฐœ: main๊ณผ์˜ conflict ํ•ด์†Œ + Strix streaming workaround์™€ stream_options ๋ฒ„๊ทธ์˜ ์—ฐ๊ฒฐ ํ™•์ธ + +- ์‹œ๊ฐ„๋ณ„ loop ์žฌ๊ฐœ ์‹œ์ ์— ์„ธ PR์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ–ˆ๋‹ค: `naruon#1486`(`blocked`), + `ContextualWisdomLab/contextual-orchestrator#923`(`blocked`), `ContextualWisdomLab/.github#1438` + (**`dirty`** โ€” ์ƒˆ๋กœ ๋ฐœ์ƒํ•œ merge conflict). `.github`์˜ `main`์ด ์ด ์„ธ์…˜์ด ๋งˆ์ง€๋ง‰์œผ๋กœ ๋™๊ธฐํ™”ํ•œ + ์ดํ›„ 3๊ฐœ ์ปค๋ฐ‹(`34c88356`, `702392a2`, ๋ณ‘ํ•ฉ ์ปค๋ฐ‹ `1d8e8724`) ์•ž์„œ ์žˆ์—ˆ๋‹ค. +- **`34c88356`**: ์˜ค๋„ˆ์˜ (๋‹ค๋ฅธ) Claude ์„ธ์…˜์ด **์ด ์„ธ์…˜์ด ์ด๋ฒˆ pass์—์„œ ๊ณ ์นœ ๊ฒƒ๊ณผ ์ •ํ™•ํžˆ ๋™์ผํ•œ + `scripts/ci/pingora_edge_policy.py`์˜ ์ฃฝ์€ post-loop raise ๋ฒ„๊ทธ**๋ฅผ ์™„์ „ํžˆ ๋…๋ฆฝ์ ์œผ๋กœ + ๋ฐœ๊ฒฌยท์ˆ˜์ •ํ–ˆ๋‹ค โ€” ๊ทผ๊ฑฐ(31ร—100=3,100 ์‚ฐ์ˆ ), ๊ฒฐ๋ก (`# pragma: no cover`), ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€์˜ ๋…ผ์ฆ๊นŒ์ง€ + ๊ฑฐ์˜ ๋™์ผํ•˜๋‹ค. ๋‹ค๋งŒ ์˜ค๋„ˆ ์ชฝ ์ˆ˜์ •์ด ํ•œ ๊ฑธ์Œ ๋” ๋‚˜์•„๊ฐ”๋‹ค: `test_changed_file_pagination_bound_is_provably_unreachable` + ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด `inspect.getsource`๋กœ ์†Œ์Šค์˜ ํŽ˜์ด์ง€ ์ˆ˜ยทper_pageยท3,000 cap ์ƒ์ˆ˜๋ฅผ ์ง์ ‘ ํŒŒ์‹ฑํ•˜๊ณ  + ๊ทธ ๋ถ€๋“ฑ์‹์„ assertํ•จ์œผ๋กœ์จ, ํ–ฅํ›„ ์ด ์„ธ ์ƒ์ˆ˜ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋ฐ”๋€Œ์–ด ๋ถˆ๋ณ€์‹์ด ๊นจ์ง€๋ฉด (์ฃฝ์€ ์ฝ”๋“œ๊ฐ€ + ๋” ์ด์ƒ ์ฃฝ์€ ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๊ฒŒ ๋˜๋ฉด) ํ…Œ์ŠคํŠธ๊ฐ€ ์š”๋ž€ํ•˜๊ฒŒ ์‹คํŒจํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค โ€” ์ด ์„ธ์…˜์˜ ์ˆ˜์ •์—๋Š” + ์—†๋˜, ๋” ๊ฒฌ๊ณ ํ•œ ์•ˆ์ „์žฅ์น˜๋‹ค. Merge conflict๋ฅผ ordinary merge commit์œผ๋กœ ํ•ด์†Œํ•˜๋ฉฐ **์˜ค๋„ˆ ์ชฝ + ๋ฒ„์ „์„ ์ฑ„ํƒ**ํ•˜๊ณ  ์ด ์„ธ์…˜์˜ ๋™๋“ฑํ•˜์ง€๋งŒ ๋œ ์™„์ „ํ•œ ๋ฒ„์ „์€ ๋ฒ„๋ ธ๋‹ค(์•ž์„  `_log_preflight_rejections` + ์ค‘๋ณต ์‚ฌ๋ก€์™€ ๋™์ผํ•œ ํŒจํ„ด). +- **`702392a2`(โ˜… ์ค‘์š”, ์ด ์„ธ์…˜ ์ž์‹ ์˜ ๋ฐœ๊ฒฌ๊ณผ ์ง์ ‘ ์—ฐ๊ฒฐ๋จ)**: ์˜ค๋„ˆ๊ฐ€ Strix ์ž์‹ ์˜ ์ฝ”๋“œ์— **์ •ํ™•ํžˆ + ์ด ์„ธ์…˜์ด `contextual-orchestrator#923`์—์„œ ๋ฐœ๊ฒฌยท์ˆ˜์ •ํ•œ ๋ฐ”๋กœ ๊ทธ `stream_options.include_usage=true` + + `tools` ๊ฑฐ๋ถ€ ๋ฒ„๊ทธ**๋ฅผ ์šฐํšŒํ•˜๋Š” workaround๋ฅผ ์ปค๋ฐ‹ํ–ˆ๋‹ค. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€: "contextual-orchestrator์˜ + ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ stream_options.include_usage=true์™€ tools ์กฐํ•ฉ์„ ์˜๋„์ ์œผ๋กœ ๊ฑฐ๋ถ€ํ•œ๋‹ค(์‚ฌ์šฉ๋Ÿ‰ + ์ง‘๊ณ„๊ฐ€ ์กฐ์šฉํžˆ ๋ถˆ์™„์ „ํ•ด์ง€๋Š” ๊ฒƒ์„ ๋ง‰๋Š” ์ •ํ•ฉ์„ฑ ๋ณด์žฅ; ์—ฌ๊ธฐ์„œ ๋ฐ”๊พธ๋Š” ๊ฒƒ์€ ๋ฒ”์œ„ ๋ฐ–)" โ€” ์ฆ‰ ์˜ค๋„ˆ + (๋˜๋Š” ์˜ค๋„ˆ์˜ ์„ธ์…˜)๋Š” ์ด ๊ฑฐ๋ถ€๋ฅผ **์˜๋„๋œ, ๊ณ ์น  ์ˆ˜ ์—†๋Š” ์ œ์•ฝ**์œผ๋กœ ๋ฐ›์•„๋“ค์ด๊ณ  ํ˜ธ์ถœ์ž + (Strix)์ธก์—์„œ `LLM_DISABLE_STREAMING=true`๋กœ ์ŠคํŠธ๋ฆฌ๋ฐ ์ž์ฒด๋ฅผ ๊บผ์„œ ๋ฌธ์ œ์˜ ์กฐํ•ฉ์„ ์•„์˜ˆ + ๋ณด๋‚ด์ง€ ์•Š๋Š” ๋ฐฉ์‹์œผ๋กœ ์šฐํšŒํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด ์„ธ์…˜์€ `_chat_response_sse_chunks`๊ฐ€ ์ด๋ฏธ + tool_calls delta์™€ ์ •์งํ•˜๊ฒŒ ๋ผ๋ฒจ๋ง๋œ usage chunk๋ฅผ ์™„์ „ํžˆ ์ง€์›ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์ง์ ‘ ์ฝ”๋“œ๋กœ + ์ฆ๋ช…ํ–ˆ๊ณ , ๊ทธ ๊ฑฐ๋ถ€๋Š” "๊ณ ์น  ์ˆ˜ ์—†๋Š” ์ œ์•ฝ"์ด ์•„๋‹ˆ๋ผ **๋ถˆํ•„์š”ํ•œ ์ž์ฒด ๋ฒ„๊ทธ**์˜€๋‹ค โ€” ์ด๋ฏธ + `contextual-orchestrator#923`์— ์„œ๋ฒ„ ์ชฝ ๊ทผ๋ณธ ์ˆ˜์ •์„ ๋„ฃ์—ˆ๋‹ค(์•„์ง `contextual-orchestrator`์˜ + `main`์—๋Š” ๋ณ‘ํ•ฉ๋˜์ง€ ์•Š์Œ). ๋‘ ์ˆ˜์ •์€ **๋ชจ์ˆœ๋˜์ง€ ์•Š๋Š”๋‹ค**: `702392a2`๋Š” ์ด๋ฏธ ๋ณ‘ํ•ฉ๋˜์–ด ์ง€๊ธˆ + ๋‹น์žฅ Strix์˜ org-wide ํ•„์ˆ˜ ๊ฒŒ์ดํŠธ๋ฅผ ๋ณต๊ตฌํ•˜๊ณ  ์žˆ๋Š”(์ง์ ‘ ์ธ์šฉ: "์ด๊ฒƒ์ด ๋ชจ๋“  PR์˜ + opencode-review ์ฒดํฌ๋ฅผ ๊ต์ฐฉ์‹œํ‚ค๊ณ  ์žˆ์—ˆ๋‹ค โ€” ๊ทธ ์ฒดํฌ๋Š” dispatch ์ „์— ์™„๋ฃŒ๋œ Strix evidence๋ฅผ + ์š”๊ตฌํ•˜๋Š”๋ฐ Strix๊ฐ€ ์Šค์บ”์„ ์™„๋ฃŒํ•  ์ˆ˜ ์—†์—ˆ๋‹ค") ์‹ค์‚ฌ์šฉ ์ค‘์ธ fix์ด๊ณ , ์ด ์„ธ์…˜์˜ fix๋Š” ๊ทผ๋ณธ + ์›์ธ(๊ฒŒ์ดํŠธ์›จ์ด ์ž์‹ ์˜ ๋ถˆํ•„์š”ํ•œ ๊ฑฐ๋ถ€)์„ ์—†์•  ํ–ฅํ›„ ์ด workaround ์ž์ฒด๋ฅผ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋งŒ๋“ค + ํ›„์† ์ •๋ฆฌ ๋Œ€์ƒ์ด๋‹ค. **์ง€๊ธˆ ๋‹น์žฅ `702392a2`๋ฅผ ๋˜๋Œ๋ฆฌ๊ฑฐ๋‚˜ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค** โ€” ์•„์ง ์„œ๋ฒ„ ์ชฝ + ์ˆ˜์ •์ด ๋ณ‘ํ•ฉยท์ค‘์•™ vendoring pin์— ๋ฐ˜์˜๋˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ, ์ง€๊ธˆ ์›Œํฌ์–ด๋ผ์šด๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๋ฉด Strix๊ฐ€ + ๋‹ค์‹œ ์ฃฝ๋Š”๋‹ค. `contextual-orchestrator#923` ๋ณ‘ํ•ฉ + `.github`์˜ `ORCHESTRATOR_PIN_SHA` ๊ฐฑ์‹  + ์ดํ›„ ๋ณ„๋„ pass์—์„œ ์ด workaround์˜ ์ œ๊ฑฐ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ์žฌ๊ฒ€ํ† ํ•œ๋‹ค(์ƒˆ Gap ํ•ญ๋ชฉ์œผ๋กœ ๊ธฐ๋ก). +- **์ด ๋ฐœ๊ฒฌ์ด ๋ฐ”๊พธ๋Š” ๊ฒƒ**: `702392a2`๊ฐ€ ์ด๋ฏธ `main`์— ์žˆ์œผ๋ฏ€๋กœ, Strix์˜ org-wide ํ•„์ˆ˜ ๊ฒŒ์ดํŠธ๊ฐ€ + ์ด๋ฏธ ๋ณต๊ตฌ๋˜์–ด ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค โ€” ์ด์ „ ์กฐ์‚ฌ์—์„œ ํ™•์ธํ•œ "`inspect_pr()`๊ฐ€ OpenCode dispatch๋ฅผ + Strix evidence ๋’ค์— ์ˆœ์„œํ™”ํ•˜๋ฉฐ Strix๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์œผ๋ฉด dispatch ์ž์ฒด๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค"๋Š” + ๊ตฌ์กฐ๊ฐ€, naruon#1486ยทcontextual-orchestrator#923๊ฐ€ dispatch์กฐ์ฐจ ๋ฐ›์ง€ ๋ชปํ–ˆ๋˜ ์ด์œ ์˜ ์ƒ๋‹น ๋ถ€๋ถ„์„ + ์„ค๋ช…ํ–ˆ์„ ์ˆ˜ ์žˆ๋‹ค. 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์˜ ์ตœ์šฐ์„  ํ•ญ๋ชฉ์œผ๋กœ ๋‚จ๊ธด๋‹ค. + +## 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๋ณ„ ํ•ด์†Œ์•ˆ์„ ์ •์ฐฐ. + +## 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 ์ตœ์šฐ์„ . +- **์ถ”๊ฐ€**: `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. +- **์ถ”๊ฐ€(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). + ## 2026-08-30 PR #1347 Devin Review 6๊ฑด ๊ฒ€์ฆ: 4๊ฑด ์‹ค์žฌ ๊ฒฐํ•จ ์ˆ˜์ •, 2๊ฑด ํ™•์ธ ํ›„ ํ•ด์†Œ `ContextualWisdomLab/.github#1347` (`fix/sandboxed-web-e2e-isolation-clean`, @@ -1530,6 +2047,33 @@ 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 ์—†์Œ. + +**์ถ”๊ฐ€ 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 @@ -2362,11 +2906,771 @@ not split out, since the revert is unsafe without it. ### 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๋กœ ์—ฐ๊ฒฐํ•œ๋‹ค. -5. `scripts/ci/select_nvidia_nim_model.py`(ํ˜ธ์ถœ์ž ์—†์Œ, ์œ„ ยง5์˜ ์—ฌ๋Ÿฌ ํ•ญ๋ชฉ์ด ์ด๋ฏธ ๋ฌธ์„œํ™”)๋ฅผ ๋ณ„๋„์˜ ์ž‘์€ PR(`fix/remove-orphaned-nim-model-resolver`)๋กœ ๋ถ„๋ฆฌ ์ œ๊ฑฐํ–ˆ๋‹ค โ€” `#1437` ๋ฆฌ๋ทฐ ์Šค๋ ˆ๋“œ๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์š”์ฒญํ•œ ๋Œ€๋กœ direct-NIM cleanup์„ pool-flip ๋…ผ์˜์™€ ๋ถ„๋ฆฌํ–ˆ๋‹ค. `contextual_orchestrator_review_sidecar.sh`์˜ ์ฐธ์กฐ ์ฃผ์„์€ git history๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋„๋ก ๊ฐฑ์‹ ํ–ˆ๋‹ค. +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๋กœ ์ „ํ™˜ ์™„๋ฃŒ. `check_calendar_conflict` + ๋„๊ตฌ ์œ„์— 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 ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ)๋„ + ๊ณ ์ณค๋‹ค. 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 ์ฆ๊ฑฐ๊ฐ€ ๋‚˜์˜ค๊ธฐ ์ „๊นŒ์ง€ ๋ณด๋ฅ˜ํ•œ๋‹ค โ€” ์ง€๊ธˆ ๋‹ค์‹œ ์‹œ๋„ํ•˜๋Š” ๊ฒƒ์€ + ์ถ”์ธก์— ๊ธฐ๋ฐ˜ํ•œ ์žฌ์ž‘์—…์ผ ๋ฟ์ด๋‹ค. + +## 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). + **์™„๋ฃŒ ๋ฐ ๊ฒ€์ฆ ๊ฒฐ๊ณผ**: 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 ๊ฐœ๋… ์ „๋ฌด, + 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 ์ƒํ•œ(ํ˜„์žฌ ๋ถ€์žฌ). + +**์ถ”๊ฐ€**: ์ดํ›„ `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ํ–ˆ๋‹ค. 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 ๋ผ์šด๋“œ๊ฐ€ ๋„์ฐฉํ•ด ์‹ค์ œ ์ฝ”๋“œ ๋Œ€์กฐ ๊ฒ€์ฆ์„ ์ „๋‹ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ +์—์ด์ „ํŠธ์— ์œ„์ž„ํ–ˆ๋‹ค โ€” **์™„๋ฃŒ**: ์œ„์ž„ ์ค‘ ๋‹ค๋ฅธ ์„ธ์…˜์ด ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ผํ•œ 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 ์—†์Œ). ๋‚จ์€ ๊ฒƒ ์ค‘ (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. + +**์ถ”๊ฐ€**: `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 ๋Œ€๊ธฐ๋ฟ. + +**์ฐธ๊ณ (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๋กœ ์›์ธ +์กฐ์‚ฌ๊ฐ€ ํ•„์š”ํ•œ ์ƒˆ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ๋กœ๋งŒ ๊ธฐ๋กํ•œ๋‹ค. + +**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์˜ ์šฐ์„ ์ˆœ์œ„ ํ›„๋ณด๋กœ ๋‚จ๋Š”๋‹ค. + +**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์˜ +์žฌ๋ฐœ๊ฒฌ) ์Šค๋ ˆ๋“œ๋„ ๊ฐ๊ฐ ๋ฌธ์„œ ์ˆ˜์ •๊ณผ ํšŒ์‹ ์œผ๋กœ ์ •๋ฆฌํ–ˆ๋‹ค. + +**์œ„ "ํ›„์† ํ›„๋ณด"๋ฅผ ์‹ค์ œ๋กœ ๋ฐฐํฌํ–ˆ๋‹ค โ€” `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 +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์™€๋Š” ๋ฌด๊ด€. + +**๋™์ผ ์„œ๋ธŒ ์ฆ์ƒ์ด `.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์™€๋„ ๋ฌด๊ด€. + +**๊ฐ™์€ 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๊ฐ€ ์•„๋‹ˆ๋ผ ์ด ๋ชจ๋ธ์— ๋Œ€ํ•œ ๊ฒฐ์ •๋ก ์  ์‹คํŒจ๋ฅผ ์ฆ๋ช…ํ•œ๋‹ค โ€” ๊ทธ ์ด์ƒ์€ ์•„๋‹ˆ๋‹ค. 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๋กœ ์ทจ๊ธ‰ํ•˜์ง€ ์•Š๋Š”๋‹ค. + +**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. + +**์ฐธ๊ณ (main์—์„œ ๋…๋ฆฝ์ ์œผ๋กœ ์™„๋ฃŒ๋œ ๋ณ„๊ฐœ ์ •๋ฆฌ ์ž‘์—…)**: `scripts/ci/select_nvidia_nim_model.py`(ํ˜ธ์ถœ์ž +์—†์Œ, ์œ„ ยง5์˜ ์—ฌ๋Ÿฌ ํ•ญ๋ชฉ์ด ์ด๋ฏธ ๋ฌธ์„œํ™”)๊ฐ€ ๋ณ„๋„์˜ ์ž‘์€ PR(`fix/remove-orphaned-nim-model-resolver`)๋กœ +๋ถ„๋ฆฌ ์ œ๊ฑฐ๋˜์–ด `main`์— ์ด๋ฏธ ๋ฐ˜์˜๋˜์–ด ์žˆ๋‹ค โ€” `#1437` ๋ฆฌ๋ทฐ ์Šค๋ ˆ๋“œ๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์š”์ฒญํ•œ ๋Œ€๋กœ direct-NIM +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 ์กฐ๊ฑด ์ถฉ์กฑ ์—ฌ๋ถ€๋ฅผ ์žฌํ™•์ธํ•œ๋‹ค. + +## 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`๋กœ ํšŒ๊ท€ ์—†์ด ํ†ต๊ณผ. + +## 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๋กœ +๋‹ต๋ณ€ํ–ˆ๋‹ค. + +## 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 ์™„๋ฃŒ. + +## 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 ํฌํ•จ)์€ ๊ณ„์† +๋ฐœ๊ฒฌ๋˜์ง€ ์•Š์•˜์„ ๊ฒƒ์ด๋‹ค. + +## 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`๋Š” + ์ด ์ฟผ๋ฆฌ์—์„œ ์ „ํ˜€ ์ฐธ์กฐ๋˜์ง€ ์•Š์Œ์„ ํ™•์ธ โ€” ์ฆ‰ ์ด ๋ถˆ์ผ์น˜๋Š” ๊ฒ€์ƒ‰ ์ •ํ™•์„ฑ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”(ํ•˜์ง€๋งŒ + ์ง€์ €๋ถ„ํ•œ) ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜ ๊ฐ’์ผ ๋ฟ. ์ฝ”๋“œ ์ˆ˜์ • ์—†์ด ๊ทผ๊ฑฐ์™€ ํ•จ๊ป˜ ๋ฐ˜๋ ค. + +## 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๋ฒˆ)์€ "๋А๋ฆฌ๊ฑฐ๋‚˜ ๊ฒฐํ•จ ์žˆ๋Š” ๋ชจ๋ธ์— ๊ฑธ๋ ค ๋ฉˆ์ถ”์ง€ ์•Š๊ณ  ๋‹ค์Œ ๋ชจ๋ธ๋กœ ๋„˜์–ด๊ฐ„๋‹ค"๋Š” + ์ ์—์„œ๋Š” ๋„์›€์ด ๋˜์ง€๋งŒ, "๊ฐ€์žฅ ๋น ๋ฅธ ๋ชจ๋ธ์„ ์šฐ์„  ์„ ํƒํ•œ๋‹ค"๋Š” ๋ช…์‹œ์  ์ตœ์ ํ™”๋Š” ์•„์ง ์—†๋‹ค. ๋ณ„๋„ + ์ฆ๋ถ„(๊ด€์ธก๋œ ์‘๋‹ต ์ง€์—ฐ์„ ๋žญํ‚น์— ๋ฐ˜์˜ํ•˜๋Š” ๊ฒƒ)์œผ๋กœ ๋ถ„๋ฆฌ ํ•„์š”. +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 ์ค‘ ๋ฐœ๊ฒฌ โ€” ๊ทธ๋ฆฌ๊ณ  ๋‹ค์‹œ 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 ๋งํฌ ํฌํ•จ). + +## 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 ์ฒ˜๋ฆฌํ–ˆ๋‹ค. + +## 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%. + +## 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%. + +## 2026-09-01 ์‹ค์‹œ๊ฐ„ ๊ด€์ธก: contextual-orchestrator#986์˜ noema-review๊ฐ€ main์˜ ์˜› +`extract_json_object`์—์„œ uncaught JSONDecodeError๋กœ ํฌ๋ž˜์‹œ โ€” `.github#1438` ๋ณ‘ํ•ฉ์˜ +๊ธด๊ธ‰์„ฑ์„ ์‹ค๋ฌผ๋กœ ์žฌํ™•์ธ + +`contextual-orchestrator#986`(๋‚ด ์ž์‹ ์ด ์—ฐ PR)์˜ required `noema-review` ์ฒดํฌ๊ฐ€ ์‹คํŒจํ•ด job +๋กœ๊ทธ๋ฅผ ์ง์ ‘ ์ถ”์ ํ–ˆ๋‹ค. Python traceback: `extract_json_object`์˜ `json.loads(stripped)`๊ฐ€ +`json.decoder.JSONDecodeError`๋ฅผ ๊ทธ๋Œ€๋กœ ๋˜์ง€๊ณ , ์ด๋ฅผ ๊ฐ์‹ธ๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์ „ํ˜€ ์—†์–ด ๋ฆฌ๋ทฐ job +์ „์ฒด๊ฐ€ ํฌ๋ž˜์‹œํ–ˆ๋‹ค โ€” ์ด๋ฒˆ ์‹คํ–‰์—์„œ ์„ ํƒ๋œ free-tier ๋ชจ๋ธ(`meta/llama-3.2-11b-vision-instruct`)์ด +malformed JSON์„ ๋ฐ˜ํ™˜ํ•œ ๊ฒƒ์ด ํŠธ๋ฆฌ๊ฑฐ. `workflow_sha: 7b1a028e70...`(`.github`์˜ ํ˜„์žฌ main +tip)์—์„œ ๊ฐ€์ ธ์˜จ ์˜› `noema_review_gate.py`๋ฅผ ์‚ฌ์šฉํ–ˆ์Œ์„ job ๋กœ๊ทธ์˜ `workflow_ref`/`TRUSTED_SOURCE_REF`๋กœ +์ง์ ‘ ํ™•์ธ โ€” ์ด๋Š” sibling repo(naruon, contextual-orchestrator ๋“ฑ)๊ฐ€ ์‹ ๋ขฐ ๊ฐ€๋Šฅํ•œ ์†Œ์Šค๋กœ CI +์‹œ์ ์— tarball๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๋ฒ„์ „์ด๋ฉฐ, ์ด๋ฒˆ ์„ธ์…˜์ด `.github#1438`์—์„œ ์ด๋ฏธ ๊ณ ์นœ ๊ทธ ์ •ํ™•ํ•œ ๊ฒฐํ•จ +("malformed LLM JSON crashed the required check instead of failing closed", 2026-08-31 ํ•ญ๋ชฉ)์ด +์•„์ง main์— ๋ณ‘ํ•ฉ๋˜์ง€ ์•Š์•„ sibling repo ์ชฝ์— ๊ทธ๋Œ€๋กœ ์‚ด์•„์žˆ์Œ์„ ์‹ค๋ฌผ๋กœ ์žฌํ™•์ธํ•œ ๊ฒƒ์ด๋‹ค. + +์ด ์‹คํŒจ๋Š” `contextual-orchestrator#986` ์ž์‹ ์˜ diff์™€ ๋ฌด๊ด€ํ•จ(๊ทธ diff๋Š” +`contextual_orchestrator/orchestrator.py`์™€ ํ…Œ์ŠคํŠธ ํŒŒ์ผ๋งŒ ๊ฑด๋“œ๋ฆผ) โ€” ๊ฒฐํ•จ ์ฝ”๋“œ ์ž์ฒด๊ฐ€ ์ด +์ €์žฅ์†Œ์— ์กด์žฌํ•˜์ง€ ์•Š๊ณ  ์™ธ๋ถ€(`.github` main)์—์„œ ๊ฐ€์ ธ์˜ค๋ฏ€๋กœ ์ด PR์— "ํฌํŒ…"ํ•  ๋Œ€์ƒ์ด ์—†๋‹ค. +์‹คํŒจํ•œ job์— ๋Œ€ํ•ด 1ํšŒ ์žฌ์‹คํ–‰์„ ํŠธ๋ฆฌ๊ฑฐ(due diligence โ€” ์ด๋ฒˆ์—” ๋‹ค๋ฅธ free-tier ๋ชจ๋ธ์ด ์„ ํƒ๋˜๋ฉด +์šฐ์—ฐํžˆ ํ†ต๊ณผํ•  ์ˆ˜๋„ ์žˆ์œผ๋‚˜, ๊ทผ๋ณธ ๊ฒฐํ•จ์€ `.github#1438`์ด main์— ๋ณ‘ํ•ฉ๋  ๋•Œ๊นŒ์ง€ ๋‚จ์•„์žˆ์Œ)ํ•˜๊ณ , +PR์— ๊ทผ๋ณธ์›์ธยท์žฌ์‹คํ–‰ยท๋Œ€๊ธฐ ์ƒํƒœ๋ฅผ ๋ช…์‹œํ•œ ์ฝ”๋ฉ˜ํŠธ๋ฅผ ๋‚จ๊ฒผ๋‹ค. `.github#1438`์˜ ๋ณ‘ํ•ฉ์€ ์ด์ œ +์ž๊ธฐ ์ž์‹ ์˜ ๊ฐœ์„  ์‚ฌํ•ญ์ผ ๋ฟ ์•„๋‹ˆ๋ผ, ์กฐ์ง ๋‚ด ๋‹ค๋ฅธ ์ €์žฅ์†Œ๋“ค์˜ noema-review๊ฐ€ ์ด ์ •ํ™•ํ•œ ํฌ๋ž˜์‹œ๋กœ +๋ฐ˜๋ณต ์‹คํŒจํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๋Š” ์‹ค์ œ ์šด์˜์ƒ ๊ธด๊ธ‰์„ฑ์„ ๊ฐ–๋Š”๋‹ค โ€” ๊ณ„์† ์ตœ์šฐ์„ ์œผ๋กœ ๋ณ‘ํ•ฉ์„ ํ–ฅํ•ด ์ถ”์ง„ํ•œ๋‹ค. ## 6. Compliance and data boundary diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index e4984f643b..bcc4a86227 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}" # Each KV credential is an independent account, including two credentials for # the same vendor or endpoint. The account cap prevents one credential from @@ -352,7 +358,7 @@ until curl -fsSL --max-time 2 "http://${ORCHESTRATOR_HOST}:${ORCHESTRATOR_PORT}/ if [ -s "$preflight_report" ]; then log "sidecar preflight route evidence: $(sed -n '1,80p' "$preflight_report" | tr '\n' ' ')" fi - 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)) # KNOWN GAP, tracked as ContextualWisdomLab/.github#1455 (not yet fixed): @@ -365,7 +371,7 @@ until curl -fsSL --max-time 2 "http://${ORCHESTRATOR_HOST}:${ORCHESTRATOR_PORT}/ # the vendored contextual_orchestrator.model_discovery source: ~7 # sequential HTTP calls at up to 15s each). 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 @@ -396,7 +402,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 @@ -470,9 +476,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" ;; @@ -497,13 +507,66 @@ while :; do if [ "$gateway_http_status" = "200" ]; then break fi + 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 + +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 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, + # 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. + # 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/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index d7ef15a2e0..87798208d6 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -10,10 +10,13 @@ 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 import urllib.request @@ -33,11 +36,35 @@ MAX_FILE_CONTEXT_CHARS = 4000 MAX_REVIEW_CONTEXT_CHARS = 24000 MAX_THREAD_BODY_CHARS = 1200 -NOEMA_LLM_TIMEOUT_SECONDS = 4 * 60 * 60 DIFF_HUNK_RE = re.compile(r"^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@") ORCHESTRATOR_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1"}) ORCHESTRATOR_BASE_ENV = "CONTEXTUAL_ORCHESTRATOR_BASE_URL" +# 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) +# 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 +# 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 +# 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. @@ -885,6 +912,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, @@ -895,20 +945,32 @@ 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. ``expected_head`` is the same normalized (lowercase) SHA ``inspect_and_review`` already checks before model work and before publication. It is threaded through here so the one-time repair-retry - request below โ€” fired only after the first attempt's verdict was - malformed โ€” can also confirm the PR head has not moved before spending a - second, potentially multi-hour ``NOEMA_LLM_TIMEOUT_SECONDS`` call on a - review that ``inspect_and_review``'s own post-call stale-head check would - discard anyway once this function returns. See ``fetch_pr`` for the live - lookup and ``StaleHeadDuringRepairRetryError`` for how that stale + request below -- fired only after the first attempt's verdict was + malformed -- can also confirm the PR head has not moved before spending a + second, potentially multi-hour ``NOEMA_LLM_REQUEST_TIMEOUT_SECONDS`` call + on a review that ``inspect_and_review``'s own post-call stale-head check + would discard anyway once this function returns. See ``fetch_pr`` for the + live lookup and ``StaleHeadDuringRepairRetryError`` for how that stale condition is reported distinctly to the caller. """ + 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") + 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" @@ -964,8 +1026,28 @@ def call_llm( method="POST", ) opener = urllib.request.build_opener(NoRedirectHandler()) - with opener.open(request, timeout=NOEMA_LLM_TIMEOUT_SECONDS) 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() + # 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 per-request 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) @@ -1011,6 +1093,7 @@ def call_llm( review_context, changed_paths, str(exc), + deadline, ) return verdict diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index fdfb34bfb9..35cf96bb37 100644 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -296,11 +296,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" diff --git a/tests/test_contextual_orchestrator_review_runtime_preflight.py b/tests/test_contextual_orchestrator_review_runtime_preflight.py index 7093e8a3d0..99dc557387 100644 --- a/tests/test_contextual_orchestrator_review_runtime_preflight.py +++ b/tests/test_contextual_orchestrator_review_runtime_preflight.py @@ -556,6 +556,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. @@ -572,6 +573,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 @@ -600,12 +611,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' @@ -614,6 +650,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", @@ -639,7 +678,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: @@ -653,6 +694,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 @@ -810,6 +861,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 0a63356dad..0049f2eb6d 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -459,15 +459,44 @@ 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 - # 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: + """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. + + 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"') == 4 + assert "sed -n '1,20p'" not in text def test_noema_review_workflow_provisions_sidecar_with_all_five_secrets() -> None: @@ -536,3 +565,61 @@ 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 -- 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 -- 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") + max_attempts_check = text.index( + 'if [ "$gateway_attempt" -ge "$REVIEW_PREFLIGHT_GATEWAY_MAX_ATTEMPTS" ]; then', + loop_start, + ) + # 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 < 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. + 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 diff --git a/tests/test_noema_orchestrator_workflow_contract.py b/tests/test_noema_orchestrator_workflow_contract.py index 3b9c5baeef..6a59042c41 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 5229605627..2ff3cf453f 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 @@ -1140,7 +1141,7 @@ def test_call_llm_skips_repair_retry_when_head_moves_before_it_fires(monkeypatch model work and before publication, but the one-time repair-retry request inside ``call_llm`` used to fire unconditionally on a malformed first verdict, even if the PR head had already moved. That burns a second, - potentially multi-hour ``NOEMA_LLM_TIMEOUT_SECONDS`` call on a review + potentially multi-hour ``NOEMA_LLM_REQUEST_TIMEOUT_SECONDS`` call on a review ``inspect_and_review``'s own post-call stale-head check would discard anyway. ``call_llm`` must instead re-check the live head via ``fetch_pr`` before the retry request and fail closed with @@ -1507,7 +1508,7 @@ def open(self, request, timeout=None): verdict = noema.call_llm("owner/repo", 1, pr, "diff", True, "head", "extra review context") assert verdict["decision"] == "approve" assert seen["url"] == "https://llm.example.test/chat" - assert seen["timeout"] == 14400 + assert seen["timeout"] == noema.NOEMA_LLM_REQUEST_TIMEOUT_SECONDS assert seen["body"]["model"] == "review-model" assert "extra review context" in seen["body"]["messages"][1]["content"] @@ -2025,7 +2026,7 @@ def read(self): class Opener: def open(self, request, timeout): - assert timeout == noema.NOEMA_LLM_TIMEOUT_SECONDS + assert timeout == noema.NOEMA_LLM_REQUEST_TIMEOUT_SECONDS payloads.append(json.loads(request.data)) return Response(invalid if len(payloads) == 1 else valid) @@ -2037,6 +2038,274 @@ 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()) + monkeypatch.setattr(noema, "fetch_pr", lambda repo, number: make_pr()) + + assert noema.call_llm("owner/repo", 7, make_pr(), diff, False, "head")["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_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, "head", deadline=expired_deadline + ) + + +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="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 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_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 diff --git a/tests/test_repository_branch_coverage_review_schedulers.py b/tests/test_repository_branch_coverage_review_schedulers.py index 4928e18046..c653aebffe 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 == noema.NOEMA_LLM_TIMEOUT_SECONDS + assert timeout == noema.NOEMA_LLM_REQUEST_TIMEOUT_SECONDS return Response() monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) diff --git a/tests/test_strix_backend_unavailable_after_exempted_finding.py b/tests/test_strix_backend_unavailable_after_exempted_finding.py index 650db6d253..131f502c44 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()