diff --git a/.github/scripts/pr-quality-messages.cjs b/.github/scripts/pr-quality-messages.cjs index 6d9b2fff02..e9b642b574 100644 --- a/.github/scripts/pr-quality-messages.cjs +++ b/.github/scripts/pr-quality-messages.cjs @@ -259,14 +259,10 @@ function failureSummary(failures, { pr }) { } /** The notice shown when the gate's own claim check disproves a ticked box. */ -function buildClaimCheckNotice(violations, liveHeadSha) { +function buildClaimCheckNotice(violations, _liveHeadSha) { const lines = []; for (const code of violations) { - if (code === "ci_green") { - lines.push( - `GitHub CI is not green on the current head ${inlineCode(liveHeadSha.slice(0, 7))}; the **CI green** box has been unticked.` - ); - } else if (code === "latest_dev") { + if (code === "latest_dev") { lines.push( `The PR is more than ${READINESS_LATEST_DEV_BEHIND_MAX} commits behind ${inlineCode("dev")}; the **latest dev** box has been unticked.` ); diff --git a/.github/scripts/pr-quality-messages.test.cjs b/.github/scripts/pr-quality-messages.test.cjs index 2cd474df7c..ca6e056bd6 100644 --- a/.github/scripts/pr-quality-messages.test.cjs +++ b/.github/scripts/pr-quality-messages.test.cjs @@ -213,23 +213,21 @@ describe("buildStaleNotice", () => { }); describe("buildClaimCheckNotice", () => { - it("names each violated claim and the reset action", () => { + it("names the latest-dev violation and the reset action", () => { const notice = buildClaimCheckNotice( - ["ci_green", "latest_dev"], + ["latest_dev"], "3f1c0de0a6a4d0a3f9a1b2c3d4e5f60718293a4b", ); - assert.match(notice[0], /CI is not green on the current head `3f1c0de`/); - assert.match(notice[0], /\*\*CI green\*\* box has been unticked/); - assert.match(notice[1], /more than 10 commits behind `dev`/); - assert.match(notice[1], /\*\*latest dev\*\* box has been unticked/); - assert.match(notice[2], /reset: re-test against the latest code/); + assert.match(notice[0], /more than 10 commits behind `dev`/); + assert.match(notice[0], /\*\*latest dev\*\* box has been unticked/); + assert.match(notice[1], /reset: re-test against the latest code/); }); - it("handles a single violation", () => { + it("ignores a stale ci_green code without inventing GitHub-CI copy", () => { const notice = buildClaimCheckNotice(["ci_green"], "a".repeat(40)); - assert.equal(notice.length, 2); - assert.match(notice[0], /CI is not green/); - assert.match(notice[1], /has been reset/); + assert.equal(notice.length, 1); + assert.match(notice[0], /has been reset/); + assert.doesNotMatch(notice[0], /CI is not green/); }); it("returns only the reset line for an empty violation list", () => { diff --git a/.github/scripts/pr-quality-state.cjs b/.github/scripts/pr-quality-state.cjs index 917e45e759..f2807c4db3 100644 --- a/.github/scripts/pr-quality-state.cjs +++ b/.github/scripts/pr-quality-state.cjs @@ -201,23 +201,21 @@ function migrateLegacyGateState(enforcerState, readinessState) { */ /** - * Bot-side verification of the two checklist claims the gate can check itself. - * The CI box only holds when the head's `ci` check is green, and the - * latest-dev box only holds while the head is at most - * READINESS_LATEST_DEV_BEHIND_MAX commits behind the base. Unknown state - * (compare or checks lookup failed) fails closed: an unverifiable claim is a - * violation, because an attestation must not ride on missing evidence. + * Bot-side verification of the checklist claim the gate can check itself for + * ancestry. The local-CI box is an author attestation only (fork contributors + * cannot start repository CI; a maintainer has to), so it is never disproved + * here — head-drift still resets every box after a new push. The latest-dev + * box only holds while the head is at most READINESS_LATEST_DEV_BEHIND_MAX + * commits behind the base. Unknown state (compare lookup failed) fails closed: + * an unverifiable claim is a violation, because an attestation must not ride + * on missing evidence. */ function readinessClaimViolations({ - ciGreen, behindBase, behindUnknown = false, behindMax = READINESS_LATEST_DEV_BEHIND_MAX }) { const violations = []; - if (!ciGreen) { - violations.push("ci_green"); - } if (behindUnknown || behindBase > behindMax) { violations.push("latest_dev"); } diff --git a/.github/scripts/pr-quality-state.test.cjs b/.github/scripts/pr-quality-state.test.cjs index f005ff5784..5dc75e4a00 100644 --- a/.github/scripts/pr-quality-state.test.cjs +++ b/.github/scripts/pr-quality-state.test.cjs @@ -229,48 +229,31 @@ describe("completionIsStale", () => { }); describe("readinessClaimViolations", () => { - it("passes when CI is green and the head is current", () => { - assert.deepEqual( - readinessClaimViolations({ ciGreen: true, behindBase: 0 }), - [], - ); - assert.deepEqual( - readinessClaimViolations({ ciGreen: true, behindBase: 10 }), - [], - ); + it("passes when the head is current enough", () => { + assert.deepEqual(readinessClaimViolations({ behindBase: 0 }), []); + assert.deepEqual(readinessClaimViolations({ behindBase: 10 }), []); }); - it("flags red CI", () => { + it("never treats local CI as a bot-verifiable claim", () => { + // Fork contributors attest local green; repository CI is maintainer-started. assert.deepEqual( - readinessClaimViolations({ ciGreen: false, behindBase: 0 }), - ["ci_green"], + readinessClaimViolations({ behindBase: 0, ciGreen: false }), + [], ); }); it("flags a head more than the threshold behind the base", () => { assert.deepEqual( readinessClaimViolations({ - ciGreen: true, behindBase: READINESS_LATEST_DEV_BEHIND_MAX + 1, }), ["latest_dev"], ); }); - it("flags both when both claims fail", () => { - assert.deepEqual( - readinessClaimViolations({ - ciGreen: false, - behindBase: READINESS_LATEST_DEV_BEHIND_MAX + 20, - }), - ["ci_green", "latest_dev"], - ); - }); - it("fails closed when the behind count is unknown", () => { assert.deepEqual( readinessClaimViolations({ - ciGreen: true, behindBase: 0, behindUnknown: true, }), @@ -280,7 +263,7 @@ describe("readinessClaimViolations", () => { it("honours a custom threshold", () => { assert.deepEqual( - readinessClaimViolations({ ciGreen: true, behindBase: 5, behindMax: 4 }), + readinessClaimViolations({ behindBase: 5, behindMax: 4 }), ["latest_dev"], ); }); diff --git a/.github/scripts/pr-quality.cjs b/.github/scripts/pr-quality.cjs index cc841e3809..f531e511d9 100644 --- a/.github/scripts/pr-quality.cjs +++ b/.github/scripts/pr-quality.cjs @@ -33,11 +33,12 @@ const REVIEW_READINESS_ITEMS = [ /** * Which checklist box each bot-verifiable claim maps to. The order must stay - * in sync with REVIEW_READINESS_ITEMS: index 0 is the CI claim, index 1 is - * the latest-dev claim, and index 2 is the Codex/CodeRabbit findings claim. + * in sync with REVIEW_READINESS_ITEMS: index 1 is the latest-dev claim and + * index 2 is the Codex/CodeRabbit findings claim. Index 0 (local CI) is an + * author attestation only — fork contributors cannot start repository CI — so + * the gate never disproves it; head-drift still resets every box. */ const REVIEW_READINESS_CLAIM_INDEX = { - ci_green: 0, latest_dev: 1, review_findings: 2 }; diff --git a/.github/scripts/pr-quality.test.cjs b/.github/scripts/pr-quality.test.cjs index 7d61925758..efecf1f682 100644 --- a/.github/scripts/pr-quality.test.cjs +++ b/.github/scripts/pr-quality.test.cjs @@ -586,16 +586,16 @@ describe("uncheckReviewReadinessBoxes", () => { it("unchecks only the requested boxes", () => { const body = uncheckReviewReadinessBoxes(checkedBody, [ - REVIEW_READINESS_CLAIM_INDEX.ci_green, + REVIEW_READINESS_CLAIM_INDEX.latest_dev, ]); - assert.ok(body.includes("- [ ] All CI tests are green on my local testing.")); - assert.ok(body.includes("- [x] I pushed my PR to the latest dev commit.")); + assert.ok(body.includes("- [x] All CI tests are green on my local testing.")); + assert.ok(body.includes("- [ ] I pushed my PR to the latest dev commit.")); assert.ok(body.includes("- [x] My PR is ready for review.")); }); it("can uncheck several boxes at once", () => { const body = uncheckReviewReadinessBoxes(checkedBody, [ - REVIEW_READINESS_CLAIM_INDEX.ci_green, + 0, REVIEW_READINESS_CLAIM_INDEX.latest_dev, ]); assert.ok(body.includes("- [ ] All CI tests are green on my local testing.")); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0d017f1da..19e00db97e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,8 +233,10 @@ jobs: # The suite, split by file across four Linux runners. # - # `bun test --shard=i/N` sorts test files by path and deals them round-robin, - # so the split is deterministic for the files that remain in this lane. + # `scripts/ci/run-bun-test-batches.sh` mirrors Bun's sorted round-robin shard + # assignment, then runs each shard in small batches so every batch gets a fresh + # Bun process. The helper prints the exact files before each batch and retries + # only a Bun runtime crash once; ordinary test failures are never retried. # Storage-policy API tests and api-usage are deliberately excluded here and run # in dedicated jobs below. Bun 1.3.14 can corrupt the Linux isolate/epoll state # around those Worker-heavy harnesses; keeping them out of the general shards @@ -293,11 +295,13 @@ jobs: cd gui bun run build - - name: Test - run: bun test --isolate tests --path-ignore-patterns 'tests/api-storage-policy*.test.ts' --path-ignore-patterns 'tests/api-usage.test.ts' --shard=${{ matrix.shard }}/4 + - name: Test in fresh-process batches + env: + TEST_SHARD: ${{ matrix.shard }}/4 + run: bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD" # Bun 1.3.14 has shown a Linux isolate/epoll race around the storage-policy - # harness. Keep the entire five-file family in one fresh process so a runtime + # harness. Keep the entire six-file family in one fresh process so a runtime # failure is bounded to this job instead of poisoning a general test shard. storage-policy: name: storage policy @@ -334,7 +338,8 @@ jobs: ./tests/api-storage-policy-mutation-busy.test.ts \ ./tests/api-storage-policy-put-race.test.ts \ ./tests/api-storage-policy-run.test.ts \ - ./tests/api-storage-policy.test.ts + ./tests/api-storage-policy.test.ts \ + ./tests/api-storage.test.ts # Bun 1.3.14 has shown a Linux isolate wedge around startServer() plus the user # cost overlay reconciler. Keep api-usage in one fresh process so a runtime diff --git a/.github/workflows/enforce-pr-target.yml b/.github/workflows/enforce-pr-target.yml index 73dc2cf943..5cf5f84626 100644 --- a/.github/workflows/enforce-pr-target.yml +++ b/.github/workflows/enforce-pr-target.yml @@ -73,15 +73,56 @@ jobs: } const statusSha = context.payload.sha; - const associatedPrs = await github.paginate( - github.rest.repos.listPullRequestsAssociatedWithCommit, - { owner, repo, commit_sha: statusSha, per_page: 100 } - ); - const candidates = associatedPrs.filter( - candidate => - candidate.state === "open" && - candidate.head?.sha === statusSha - ); + // Primary authority: GitHub's commit-to-PR index. This read can + // lag a fresh head push, so a non-match is not proof of absence. + let candidates = []; + try { + const associatedPrs = await github.paginate( + github.rest.repos.listPullRequestsAssociatedWithCommit, + { owner, repo, commit_sha: statusSha, per_page: 100 } + ); + candidates = associatedPrs.filter( + candidate => + candidate.state === "open" && + candidate.head?.sha === statusSha + ); + } catch (error) { + core.warning( + `Could not list PRs associated with commit ${statusSha}: ${error.message}` + ); + } + + if (candidates.length !== 1) { + // Fallback: reconcile directly against the live head SHA. The + // association index can be stale or empty for a very recent + // head (seen on PR #1441), so an empty/ambiguous index result + // must not silently drop the revalidation. Matching on the + // head SHA is the same authoritative identity the write gate + // uses, and `pulls.list` is a read — compatible with this + // job's `pull-requests: read` permission. + const priorCount = candidates.length; + try { + const openPrs = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: "open", + per_page: 100 + }); + candidates = openPrs.filter( + pr => + pr.state === "open" && + pr.head?.sha === statusSha + ); + core.info( + `Associated-index fallback: ${priorCount} index match(es), ${candidates.length} open PR(s) match head ${statusSha}.` + ); + } catch (error) { + core.warning( + `Could not list open PRs for head-${statusSha} fallback: ${error.message}` + ); + } + } + if (candidates.length !== 1) { core.info( `CodeRabbit status ${statusSha} maps to ${candidates.length} open current-head PRs; skipping ambiguous/stale revalidation.` @@ -99,10 +140,8 @@ jobs: needs: resolve-pr if: needs.resolve-pr.outputs.pull-number != '' runs-on: ubuntu-latest - # The write job also reads the current head's aggregate check evidence. # Job-scoped permissions replace, rather than extend, the workflow default. permissions: - checks: read contents: write pull-requests: write concurrency: @@ -802,14 +841,16 @@ jobs: checklistComplete = readiness.present && readiness.complete; } - // The bot verifies the three checklist claims it can check itself. - // The CI box only counts when the head's `ci` check (the repo's - // documented "CI passed" signal) is green; the latest-dev box only - // counts while the head is at most READINESS_LATEST_DEV_BEHIND_MAX - // commits behind the base; the findings box only counts while every - // Codex/CodeRabbit review thread on the PR is resolved. A disproved - // claim unchecks that box and keeps the PR a draft, exactly like a - // head-drift reset. + // The bot verifies the checklist claims it can check itself. The + // local-CI box is an author attestation only — fork contributors + // cannot start repository CI (a maintainer has to) — so the gate + // never disproves it; head-drift still resets every box after a + // new push. The latest-dev box only counts while the head is at + // most READINESS_LATEST_DEV_BEHIND_MAX commits behind the base; + // the findings box only counts while every Codex/CodeRabbit + // review thread on the PR is resolved. A disproved claim unchecks + // that box and keeps the PR a draft, exactly like a head-drift + // reset. let claimViolations = []; let claimNotice = []; if ( @@ -818,52 +859,7 @@ jobs: !headDrifted && failures.length === 0 ) { - let ciGreen = false; - try { - // GitHub Actions' immutable App ID. Name alone is not evidence: - // any installed app can publish a check called `ci`. - const githubActionsAppId = 15368; - const { data: checksData } = - await github.rest.checks.listForRef({ - owner, - repo, - ref: pr.head.sha, - app_id: githubActionsAppId, - check_name: "ci", - filter: "latest", - per_page: 100 - }); - const checkRuns = Array.isArray(checksData.check_runs) - ? checksData.check_runs - : []; - const ciChecks = checkRuns.filter( - check => - check.name === "ci" && - check.app?.id === githubActionsAppId - ); - // The readiness claim requires positive CI evidence. A missing, - // pending, unsuccessful, foreign, or conflicting aggregate - // check must fail closed. The exact app/name/latest query should - // be tiny; if GitHub reports more rows than this response holds, - // treat the truncated evidence as unreadable rather than paging - // through an endpoint whose filters already select the latest run. - ciGreen = - Number.isSafeInteger(checksData.total_count) && - checksData.total_count === checkRuns.length && - ciChecks.length > 0 && - ciChecks.every( - check => - check.status === "completed" && - check.conclusion === "success" - ); - } catch (error) { - core.warning( - `Could not list checks for the readiness claim check: ${error.message}` - ); - ciGreen = false; - } claimViolations = readinessClaimViolations({ - ciGreen, behindBase, behindUnknown: ancestryLookupFailed }); diff --git a/.github/workflows/issue-quality-tests.yml b/.github/workflows/issue-quality-tests.yml index dbc8f5341f..600345dabc 100644 --- a/.github/workflows/issue-quality-tests.yml +++ b/.github/workflows/issue-quality-tests.yml @@ -11,6 +11,8 @@ on: - ".github/scripts/pr-quality.test.cjs" - ".github/scripts/pr-quality-messages.cjs" - ".github/scripts/pr-quality-messages.test.cjs" + - ".github/scripts/pr-quality-state.cjs" + - ".github/scripts/pr-quality-state.test.cjs" - ".github/scripts/pr-labeler.cjs" - ".github/scripts/pr-labeler.test.cjs" - ".github/scripts/enforce-pr-target.test.cjs" @@ -41,6 +43,8 @@ on: - ".github/scripts/pr-quality.test.cjs" - ".github/scripts/pr-quality-messages.cjs" - ".github/scripts/pr-quality-messages.test.cjs" + - ".github/scripts/pr-quality-state.cjs" + - ".github/scripts/pr-quality-state.test.cjs" - ".github/scripts/pr-labeler.cjs" - ".github/scripts/pr-labeler.test.cjs" - ".github/scripts/enforce-pr-target.test.cjs" @@ -79,6 +83,8 @@ jobs: run: | node --test .github/scripts/issue-quality*.test.cjs node --test .github/scripts/pr-quality.test.cjs + node --test .github/scripts/pr-quality-messages.test.cjs + node --test .github/scripts/pr-quality-state.test.cjs node --test .github/scripts/pr-labeler.test.cjs node --test .github/scripts/enforce-pr-target.test.cjs node --test .github/scripts/pr-hygiene.test.cjs diff --git a/.gitignore b/.gitignore index 82fb882004..dbeb798ee2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ dist/ *.log .DS_Store +# Generated CL compatibility identity embedded by prepare:package. It must stay +# untracked so the manifest cannot hash itself. +src/generated/compatibility-version.json + # Maintainer planning notes are TRACKED in this repository (`devlog/`). Security # material is not: see the "Security working notes" section of AGENTS.md. # diff --git a/AGENTS.md b/AGENTS.md index 060fa06b9a..bed8589b99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -192,10 +192,13 @@ listed in `MAINTAINERS.md` (excluding the author). Completion is bound to the exact commit the PR head pointed at: if new commits are pushed afterwards, the gate moves the PR back to draft, resets the checklist and the notification, and asks the author to test and tick the boxes again against the latest code. -Before a completion is accepted, the gate verifies the two checklist claims it -can check itself: the head's `ci` check must be green, and the branch must be -on the latest `dev` commit or at most 10 commits behind it. A disproved claim -unticks the matching box and keeps the PR a draft. +Before a completion is accepted, the gate verifies the checklist claims it +can check itself: the branch must be on the latest `dev` commit or at most +10 commits behind it, and Codex/CodeRabbit findings must be resolved. The +local-CI box is an author attestation only — fork contributors cannot start +repository CI; a maintainer has to — so the gate never disproves it; a new +push still resets every box. A disproved claim unticks the matching box and +keeps the PR a draft. Authors with repository push permission skip the ancestry heuristic only. As with approval requirements in [`MAINTAINERS.md`](./MAINTAINERS.md), this is enforced by convention until branch protection is configured. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 81e57197bc..3214adfdf1 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -35,10 +35,13 @@ see [The retired `dev2-go` line](#the-retired-dev2-go-line). commits are pushed afterwards, the gate moves the PR back to draft, resets the checklist and the notification, and asks the author to test and tick the boxes again against the latest code. - Before a completion is accepted, the gate verifies the two checklist claims - it can check itself: the head's `ci` check must be green, and the branch - must be on the latest `dev` commit or at most 10 commits behind it. A - disproved claim unticks the matching box and keeps the PR a draft. + Before a completion is accepted, the gate verifies the checklist claims + it can check itself: the branch must be on the latest `dev` commit or at + most 10 commits behind it, and Codex/CodeRabbit findings must be resolved. + The local-CI box is an author attestation only — fork contributors cannot + start repository CI; a maintainer has to — so the gate never disproves it; + a new push still resets every box. A disproved claim unticks the matching + box and keeps the PR a draft. Authors with repository push permission skip the ancestry heuristic only. As with the approval requirement above, this is enforced by convention until branch protection is configured (see the note under the change log). diff --git a/README.md b/README.md index 0ef20a4c6a..c4a6480887 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ ocx start # proxy + dashboard on localhost:10100

- English · 한국어 · 简体中文 · Русский · 日本語 · Türkçe · 📖 Full documentation → + English · 한국어 · 简体中文 · 繁體中文 · Русский · 日本語 · Türkçe · 📖 Full documentation →

opencodex is a lightweight local proxy that translates Codex's Responses API into whatever your diff --git a/assets/zh-tw-providers.png b/assets/zh-tw-providers.png new file mode 100644 index 0000000000..b3ec814dc4 Binary files /dev/null and b/assets/zh-tw-providers.png differ diff --git a/devlog/_plan/260807_compatibility_lab/000_master_plan.md b/devlog/_plan/260807_compatibility_lab/000_master_plan.md index f9ee5b8334..c34c30bdc3 100644 --- a/devlog/_plan/260807_compatibility_lab/000_master_plan.md +++ b/devlog/_plan/260807_compatibility_lab/000_master_plan.md @@ -273,8 +273,8 @@ Only CL-00 is authorized by this document at present. | CL-03 | Bounded live-route probes | Not started | | CL-04 | Lab CLI and management read surfaces | Not started | | CL-05 | Compatibility Matrix UI | Not started | -| CL-06 | Existing Routing Profile compatibility controls and Router Intelligence consumption | Not started | -| CL-07 | Agent Fabric task-effectiveness ingestion | Not started | +| CL-06 | Existing Routing Profile compatibility controls and Router Intelligence consumption | **ACCEPTED/CLOSED** — merged #1394 at `b66e33ce7207d91014644d99317e456c992a3418` | +| CL-07 | Agent Fabric task-effectiveness ingestion | **ACCEPTED/CLOSED** — merged #1438 at `02e62fc8c7354c544ef71f8bb3db5ebba42cb600` | | CL-08 | Shadow/automatic/public evidence workflows | Not started | Phase numbering after CL-01 is programme planning, not implementation diff --git a/devlog/_plan/260807_compatibility_lab/001_pr_stack_status.md b/devlog/_plan/260807_compatibility_lab/001_pr_stack_status.md index cdd56426b7..a6a705cabd 100644 --- a/devlog/_plan/260807_compatibility_lab/001_pr_stack_status.md +++ b/devlog/_plan/260807_compatibility_lab/001_pr_stack_status.md @@ -24,8 +24,9 @@ independent review, blockers, and whether a later phase is authorized. | CL-02 | `feat/cl-02-evidence-ledger` | `4bb249b756abd468c675d2d92fffe4da95ad3e2a` | NOT RECORDED | [#1333](https://github.com/lidge-jun/opencodex/pull/1333) | MERGED TO `dev` at `025c37916225dd685d9217e5b40190600f06d278`; POST-MERGE HARDENING [#1343](https://github.com/lidge-jun/opencodex/pull/1343) MERGED at `eee2dab4d1bbacefce56057adad51d734f346702`; FINAL CLOSURE GATE [#1348](https://github.com/lidge-jun/opencodex/pull/1348) | | CL-03 | `feat/cl-03-live-route-probes` | `4f746d13799888ea0a8c7a111aa2ad61c2126ea0` | `003f7402f49bfe8dd710a7beba52f717051bfadf` | [#1352](https://github.com/lidge-jun/opencodex/pull/1352) | MERGED TO `dev` at `68c71a4e9cdf882d812f09fd94783a28749db629`; ACCEPTED/CLOSED | | CL-04 | `feat/cl-04-lab-read-surfaces` | `68c71a4e9cdf882d812f09fd94783a28749db629` | NOT RECORDED | [#1378](https://github.com/lidge-jun/opencodex/pull/1378) | MERGED TO `dev` at `d517161aeaa3a974ad3c0360ff0c97b03b4c4520` | -| CL-05 | `feat/cl-05-compatibility-matrix-ui` | `d517161aeaa3a974ad3c0360ff0c97b03b4c4520` | `2a159b8b7` (Models tab placement) | [#1384](https://github.com/lidge-jun/opencodex/pull/1384) | IMPLEMENTATION IN PROGRESS (not accepted) | - +| CL-05 | `feat/cl-05-compatibility-matrix-ui` | `d517161aeaa3a974ad3c0360ff0c97b03b4c4520` | `2a159b8b7` (Models tab placement) | [#1384](https://github.com/lidge-jun/opencodex/pull/1384) | MERGED TO `dev` at `1072b9c39c48a4982229131613ac300560740742` | +| CL-06 | `feat/cl-06-routing-profile-compatibility` | `1072b9c39c48a4982229131613ac300560740742` | `b96eae83f2a6d1654472aeeef84799070743aeb8` | [#1394](https://github.com/lidge-jun/opencodex/pull/1394) | MERGED TO `dev` at `b66e33ce7207d91014644d99317e456c992a3418`; ACCEPTED/CLOSED | +| CL-07 | `feat/cl-07-task-effectiveness-producer` | `b66e33ce7207d91014644d99317e456c992a3418` | `0efe2c69514d3baefee686383fe740e4ecb37d83` | [#1438](https://github.com/lidge-jun/opencodex/pull/1438) | MERGED TO `dev` at `02e62fc8c7354c544ef71f8bb3db5ebba42cb600`; ACCEPTED/CLOSED | The CL-01 starting SHA is the exact CL-00 tip recorded when CL-01 began. Its moving base-ref name is not a substitute for that historical SHA. @@ -160,7 +161,36 @@ Claims cannot produce `PROBED`/`VERIFIED`. - CL-02: **MERGED** via #1333 at `025c37916225dd685d9217e5b40190600f06d278`; post-merge hardening #1343 is also **MERGED** at `eee2dab4d1bbacefce56057adad51d734f346702`; final closure is tracked in #1348. - CL-03: **ACCEPTED/CLOSED** via [#1352](https://github.com/lidge-jun/opencodex/pull/1352), merged to `dev` at `68c71a4e9cdf882d812f09fd94783a28749db629`. - CL-04: **MERGED** via #1378 at `d517161aeaa3a974ad3c0360ff0c97b03b4c4520`. -- CL-05: **IMPLEMENTATION IN PROGRESS**, authorized from CL-04 merge `d517161aeaa3a974ad3c0360ff0c97b03b4c4520`. +- CL-05: **MERGED** via #1384 at `1072b9c39c48a4982229131613ac300560740742`. +- CL-06: **ACCEPTED/CLOSED** via [#1394](https://github.com/lidge-jun/opencodex/pull/1394), merged to `dev` at `b66e33ce7207d91014644d99317e456c992a3418`. +- CL-07: **ACCEPTED/CLOSED** via [#1438](https://github.com/lidge-jun/opencodex/pull/1438), merged to `dev` at `02e62fc8c7354c544ef71f8bb3db5ebba42cb600`; accepted head `0efe2c69514d3baefee686383fe740e4ecb37d83`; plan `007_cl07_task_effectiveness.md`. +- CL-08: **not started**. + +## CL-06 closure log + +- **Merge commit on `dev`:** `b66e33ce7207d91014644d99317e456c992a3418` ([#1394](https://github.com/lidge-jun/opencodex/pull/1394)) +- **Accepted / source head:** `b96eae83f2a6d1654472aeeef84799070743aeb8` +- **Starting/base SHA:** `1072b9c39c48a4982229131613ac300560740742` (CL-05 merge #1384) +- **Scope delivered:** optional Routing Profile compatibility policy, Router Intelligence consumption, CL-06 routing regressions; no Fabric/task-effectiveness leakage. + +## CL-07 closure log + +- **Merge commit on `dev`:** `02e62fc8c7354c544ef71f8bb3db5ebba42cb600` ([#1438](https://github.com/lidge-jun/opencodex/pull/1438)) +- **Accepted / source head:** `0efe2c69514d3baefee686383fe740e4ecb37d83` +- **Starting/base SHA:** `b66e33ce7207d91014644d99317e456c992a3418` (CL-06 merge #1394) +- **Scope delivered:** bounded `src/lab/fabric/` task-effectiveness producer, exact-tree-diff verifier, scratch sandbox, trusted-route persistence boundary, isolated child producer with parent-owned IPC/timeouts. +- **CL-08:** not started (explicit non-goal). + +## CL-07 start log + +- **Starting/base SHA:** `b66e33ce7207d91014644d99317e456c992a3418` (exact CL-06 merge #1394) +- **Branch:** `feat/cl-07-task-effectiveness-producer` +- **Scope:** bounded Lab-owned task-effectiveness producer for `fabric-core` / + `fabric-core.task.synthetic-patch@1.0.0`; `exact-tree-diff-v1` verifier; + scratch sandbox; observation ingestion with `executionMode: fabric`; catalog + discovery for the task layer. No general Agent Fabric product API. +- **Explicitly out of scope:** CL-08 automation/background execution; CL-06 + routing semantic changes; user repositories/prompts; arbitrary shell. ## CL-03 implementation log (2026-08-09) diff --git a/devlog/_plan/260807_compatibility_lab/006_cl06_routing_compatibility.md b/devlog/_plan/260807_compatibility_lab/006_cl06_routing_compatibility.md new file mode 100644 index 0000000000..0631af3ff6 --- /dev/null +++ b/devlog/_plan/260807_compatibility_lab/006_cl06_routing_compatibility.md @@ -0,0 +1,339 @@ +# CL-06 implementation record — Routing Profile compatibility policy + +## Programme position + +| Field | Value | +|---|---| +| **Phase** | CL-06 | +| **Starting SHA** | `1072b9c39c48a4982229131613ac300560740742` (CL-05 merge #1384) | +| **Branch** | `feat/cl-06-routing-profile-compatibility` | +| **Target** | `lidge-jun/opencodex:dev` | +| **CL-07** | **Not started** (explicit non-goal) | + +## A. Frozen Routing Profile compatibility schema + +Compatibility policy extends the existing `OcxRoutingProfileConfig` via an optional +`compatibility` object. There is **no** separate compatibility profile store. + +```typescript +interface OcxRoutingProfileCompatibilitySuite { + /** Lab suite identifier, e.g. "responses-core". */ + suiteId: string; + /** Evidence layer for this requirement. */ + evidenceLayer: "protocol_conformance" | "live_route_compatibility"; +} + +interface OcxRoutingProfileCompatibility { + /** Required compatibility suites (may be empty array → no requirements). */ + requiredSuites?: OcxRoutingProfileCompatibilitySuite[]; + /** + * Minimum positive compatibility status. Only PROBED and VERIFIED are legal + * positive thresholds. Omitted = no positive threshold (suites still listed + * for trace/explainability only when explicitly configured). + */ + minStatus?: "PROBED" | "VERIFIED"; + /** + * Profile-level maximum evidence age (ms). Tightens scenario/suite freshness; + * never extends it. Omitted = no profile tightening. + */ + maxEvidenceAgeMs?: number; + /** + * Behavior for UNKNOWN, CLAIMED, and BLOCKED verdicts on a required suite. + * Default: "exclude" (fail closed). + */ + unknownEvidence?: "allow" | "penalize" | "exclude"; + /** + * Behavior for DEGRADED verdict on a required suite. + * Default: "penalize". + */ + degradedEvidence?: "allow" | "penalize" | "exclude"; +} +``` + +### Verdict semantics (CL-00 aligned) + +| Verdict | Required-suite treatment | +|---|---| +| `VERIFIED` | Satisfies `minStatus: VERIFIED` and `minStatus: PROBED` | +| `PROBED` | Satisfies `minStatus: PROBED`; fails `minStatus: VERIFIED` | +| `DEGRADED` | Governed solely by `degradedEvidence` (never by `minStatus`) | +| `UNKNOWN` | Follows `unknownEvidence` | +| `CLAIMED` | Follows `unknownEvidence` (not a positive threshold) | +| `BLOCKED` | Follows `unknownEvidence` (environmental; not capability proof) | +| `UNSUPPORTED` | **Always excludes** for a required suite | + +There is **no** universal compatibility score and **no** total ordering across all +verdicts. `minStatus` applies only to positive `PROBED`/`VERIFIED` thresholds. + +### Freshness composition + +Effective max age for a suite requirement: + +```text +effectiveMaxAgeMs = min( + finite scenario.maxAgeMs, + finite suite.maxAgeMs, + finite profile.maxEvidenceAgeMs +) +``` + +`null` at any layer means no bound at that layer. A stale prior `VERIFIED` or +`PROBED` row does **not** satisfy a current requirement. + +## B. Backward compatibility and revision hashing + +1. Profiles that **omit** `compatibility` entirely retain pre-CL-06 validation, + eligibility, scoring, and routing behavior. +2. Normalization **does not** inject a default/empty `compatibility` object into + stored config or revision input. +3. `profileRevision()` includes `compatibility` **only** when the normalized + profile has at least one effective compatibility control: + - non-empty `requiredSuites`, or + - `minStatus`, or + - `maxEvidenceAgeMs`, or + - explicit `unknownEvidence` / `degradedEvidence` overrides. +4. Adding CL-06 code to a deployment must not change revisions for profiles that + never set compatibility fields (regression-tested). + +## C. Exact route-subject identity + +Compatibility evidence matches the exact Lab `RouteSubjectV1` / `subjectId`, not +`provider/model` alone. + +### Production derivation (read-only, no DNS/network) + +Shared pure helpers extracted from CL-03 (`src/lab/subject/route-subject.ts`, +`src/lab/live/destination.ts` endpoint fingerprint primitive): + +1. **`resolveRoutedProvider(config, provider, model)`** — reuse `routedProviderConfig` + discipline from `src/router.ts` (registry merge, baseUrl resolution, adapter pin). +2. **`resolveEffectiveWire(config, provider, model)`** — `resolveWireProtocolOverride` + with policy inbound default `openai-responses`. +3. **`upstreamProtocolForAdapter(adapter)`** — closed map matching Lab conventions + (`openai-responses`, `openai-chat`, `anthropic-messages`, …). +4. **`surfaceForRoute(inbound, upstream)`** — closed map (`responses-http`, …). +5. **`providerInstanceKey(provider, routed)`** — stable config-owner identity: + `providerId` + resolved `baseUrl` + effective `adapter` (no secrets). +6. **`endpointFingerprintFromBaseUrl(baseUrl)`** — **DNS-free** URL parse + + `localFingerprint("endpoint", {scheme, host, port, basePath})` (same algorithm as + `createLabDestination` snapshot, without address resolution). +7. **`resolveProductionBehaviorValues(...)`** — production resolver emitting closed + `LabBehaviorValues` with the same keys required by `buildBehaviorFingerprintV1`. +8. **`readOpenCodexCompatibilityVersion()`** — embedded/generated 64-hex manifest + hash per CL-00 §4 (not package marketing version). +9. **`buildRouteSubjectV1(routeContext, destinationSnapshot)`** — existing CL-03 + builder; routing supplies a frozen `LabDestinationV1`-shaped snapshot with + `addresses: []` (fingerprint-only; not used for network). + +`subjectIdForSubject(subject)` from `src/lab/digest.ts` is the lookup key. + +### Identity invariants + +- Adapter/config/endpoint/model-behavior changes → new `subjectId` → no evidence reuse. +- Credential rotation alone does not change subject (provider instance key excludes secrets). +- Routing consumption uses the **same** fingerprint algorithms as Lab evidence writers. + +## D. No routing-path side effects + +Production routing and dry-run **must not** synchronously: + +- run Compatibility Lab probes; +- run protocol conformance; +- run live-route tests; +- contact upstream for compatibility; +- perform Lab DNS resolution; +- execute Agent Fabric work; +- rebuild the Lab projection; +- replay the JSONL ledger; +- mutate Lab state. + +CL-06 reads an **existing** SQLite projection snapshot only. + +## E. Compatibility evidence read path + +### Bounded reader (`src/routing/compatibility/reader.ts`) + +- One `openLabReadConnection` per policy evaluation. +- One SQL query for all candidate subject IDs: + `SELECT … FROM verdicts WHERE subject_id IN (…)` (plus optional layer/suite filters). +- Returns a frozen `CompatibilityEvidenceSnapshot` passed into the pure evaluator. +- Fail-safe when projection missing/incompatible/corrupt: treat as **no evidence** + (follows `unknownEvidence`), never throw through routing. + +### Evaluator purity + +`evaluatePolicyProfile` remains pure: it receives `compatibility` evidence on each +`PolicyCandidateEvidence` assembled **before** evaluation. It does not open SQLite. + +### Missing/malformed evidence behavior + +| Condition | Routing behavior | +|---|---| +| Projection available, verdict row present | Evaluate normally | +| Projection missing | `unknownEvidence` policy per required suite | +| Projection incompatible | Same as missing (no rebuild) | +| Subject construction fails | `unknownEvidence` (exclude by default) | +| Suite verdict missing | `unknownEvidence` | +| Corrupt/unusable row ignored by projection | `unknownEvidence` | + +## F. Freshness + +Implemented per CL-00 §4 and §Freshness above. Reader supplies `asOf` from verdict +row; evaluator compares `now - asOf` against `effectiveMaxAgeMs` loaded from Lab +catalogue metadata (in-memory `queryLabCatalog`, not per-row SQLite). + +Stale positive verdicts are treated as **missing positive evidence** → `unknownEvidence` +path (not as current `PROBED`/`VERIFIED`). + +## G. Penalty semantics + +Penalties are **deterministic, bounded, and explainable** — separate from health/quota/cost. + +Constants (mirroring RI-06 unknown floors): + +| Policy | Effect | +|---|---| +| `unknownEvidence: allow` | No exclusion; no compatibility score component | +| `unknownEvidence: penalize` | Eligible; `score.components.compatibility = 0.3` | +| `unknownEvidence: exclude` | Hard exclusion `compatibility-unknown` | +| `degradedEvidence: allow` | No exclusion; no penalty | +| `degradedEvidence: penalize` | Eligible; `score.components.compatibility = 0.3` | +| `degradedEvidence: exclude` | Hard exclusion `compatibility-degraded` | + +`UNSUPPORTED` always excludes (`compatibility-unsupported`). Failed positive +threshold excludes (`compatibility-insufficient`). Stale positive excludes +(`compatibility-stale`). + +Compatibility weight is **not** added to `optimize` — penalty renormalizes like +health/quota unknown penalties (spent weight returns to `configuredPriority` share). + +## H. Routing flow (preserved ordering) + +```text +Routing Profile + → configured candidates + → hard capability gates (existing require + request evidence) + → compatibility requirements / penalties ← CL-06 + → eligible candidates + → health / quota / cost / latency scoring + → deterministic winner +``` + +Compatibility never bypasses capability hard gates. + +## I. RouteDecisionTraceV1 extensions + +Extend existing trace (no parallel Lab routing trace): + +```typescript +interface RouteCompatibilitySuiteTrace { + suiteId: string; + evidenceLayer: string; + verdict?: string; // observed + minStatus?: string; // threshold + fresh?: boolean; // freshness classification + unknownPolicy?: string; + degradedPolicy?: string; + outcome: "satisfied" | "penalized" | "excluded" | "unknown"; + reason?: string; // stable wire code, bounded +} + +interface RouteCompatibilityEvidence { + subjectId?: string; // truncated hash prefix optional for privacy + suites: RouteCompatibilitySuiteTrace[]; // max 8 suites +} +``` + +Added to `RouteCandidateTrace` as optional `compatibility?: RouteCompatibilityEvidence`. +Suite rows capped; overflow sets `truncated.compatibility`. Existing 16 KiB / 8 candidate / +16 exclusion bounds remain authoritative. + +## J. Dry-run parity + +`assembleCandidateEvidence` in `routing-profile-routes.ts` and `router.ts` call the +same `assemblePolicyCandidateEvidence(config, profile, now)` helper that attaches +compatibility snapshots. Dry-run and production evaluation share `evaluatePolicyProfile`. + +## K. Routing Profiles GUI + +Extend Models → Routing profile editor (`RoutingProfiles.tsx`): + +- Compact **Compatibility** section (not a second editor/store). +- Required suites: multi-select from `GET /api/lab/catalog` grouped by layer. +- `minStatus`, `maxEvidenceAgeMs`, `unknownEvidence`, `degradedEvidence` controls. +- Omitting compatibility on save preserves backward-compatible PUT body. + +CL-05 Compatibility Matrix tab remains read-only and unchanged. + +## L. Explicit non-goals + +CL-06 does **not** implement: Agent Fabric, task execution, task-effectiveness +producers, CL-07, automatic/background probing, shadow probes, public evidence +workflows, CL-08. + +--- + +## Implementation readiness review (pre-coding) + +Pressure-tested against CL-00/CL-05 codebase at `1072b9c`: + +| # | Risk | Resolution | +|---|---|---| +| 1 | Production route subject without DNS | **Accepted** — endpoint fingerprint uses URL parse only; same `localFingerprint` as Lab destination snapshot. DNS results are not part of `endpointFingerprint`. | +| 2 | Fingerprint parity Lab ↔ routing | **Accepted** — reuse `buildRouteSubjectV1`, `buildBehaviorFingerprintV1`, `subjectIdForSubject`; extract DNS-free endpoint helper from `destination.ts`. | +| 3 | Profiles without compatibility unchanged | **Accepted** — guard all CL-06 paths on `profile.compatibility` presence; regression tests required. | +| 4 | Revision backward compatibility | **Accepted** — omit empty compatibility from revision digest; test legacy profiles. | +| 5 | Stale VERIFIED passing | **Rejected risk** — freshness enforced in evaluator before positive threshold check. | +| 6 | Verdict semantics | **Accepted** — table above matches CL-00; CLAIMED/BLOCKED → unknown policy. | +| 7 | Penalize → universal score | **Rejected risk** — bounded per-dimension `compatibility` component only; no cross-layer collapse. | +| 8 | N+1 projection reads | **Rejected risk** — single `IN (subject_ids)` query per evaluation. | +| 9 | Missing projection crashes routing | **Rejected risk** — fail-safe reader returns empty snapshot. | +| 10 | Dry-run vs production divergence | **Rejected risk** — shared assembly helper. | +| 11 | Trace overflow | **Accepted** — suite cap + existing byte budget enforcement. | +| 12 | CL-07 leakage | **None** — no Fabric types, APIs, or task subjects in CL-06. | + +### Rejected alternatives + +- **Separate compatibility profile store** — rejected; violates CL-00 Routing Profiles boundary. +- **Provider/model verdict lookup** — rejected; weakens exact-route identity. +- **Synchronous projection rebuild on miss** — rejected; violates consumer boundary. +- **Universal compatibility weight in `optimize`** — rejected; not in CL-00 contract. + +### Design decisions + +- Policy inbound default `openai-responses` for subject construction (policy routes via Responses API). +- Compatibility catalogue metadata loaded once per evaluation for freshness ceilings. +- `compatibility` trace stores suite-level outcomes only (no raw subject JSON). + +--- + +## Implementation status + +| Area | Status | +|---|---| +| Plan frozen | ✅ This document | +| Types / profile validation | ✅ | +| Route subject resolver | ✅ | +| Evidence reader | ✅ | +| Evaluator + trace | ✅ | +| Management API + dry-run | ✅ | +| GUI editor | ✅ | +| Tests (25+ cases) | ✅ | +| PR #1394 | ✅ Merged to `dev` at `b66e33ce7207d91014644d99317e456c992a3418` | + +## Validation checklist (pre-acceptance) + +- [x] `bun x tsc --noEmit` +- [x] `bun test tests/routing-compatibility.test.ts` +- [x] `bun test tests/routing-profile.test.ts tests/route-decision-trace.test.ts` +- [x] `bun test tests/lab-read-surfaces.test.ts` +- [x] GUI lint/build +- [x] `bun run privacy:scan` +- [x] Cross-platform CI on PR #1394 + +## Acceptance + +- **State:** ACCEPTED / CLOSED +- **Merge commit:** `b66e33ce7207d91014644d99317e456c992a3418` +- **Source head:** `b96eae83f2a6d1654472aeeef84799070743aeb8` +- **CL-07:** **ACCEPTED/CLOSED** (merged #1438) diff --git a/devlog/_plan/260807_compatibility_lab/007_cl07_task_effectiveness.md b/devlog/_plan/260807_compatibility_lab/007_cl07_task_effectiveness.md new file mode 100644 index 0000000000..9b207fd955 --- /dev/null +++ b/devlog/_plan/260807_compatibility_lab/007_cl07_task_effectiveness.md @@ -0,0 +1,282 @@ +# CL-07 implementation record — Task-effectiveness evidence producer + +## Programme position + +| Field | Value | +|---|---| +| **Phase** | CL-07 | +| **Starting SHA** | `b66e33ce7207d91014644d99317e456c992a3418` (CL-06 merge #1394) | +| **Branch** | `feat/cl-07-task-effectiveness-producer` | +| **Target** | `lidge-jun/opencodex:dev` | +| **CL-08** | **Not started** (explicit non-goal) | + +## 0. Audit summary (repository reality) + +Inspected at starting SHA `b66e33ce7207d91014644d99317e456c992a3418`: + +| Area | Reality | +|---|---| +| `TaskSubjectV1` | Defined and validated (`src/lab/events/types.ts`, `validate.ts`); query DTO mapped | +| Agent Fabric product | **Absent**. Only `devlog/_plan/800_agent-fabric/` planning notes and CL-00 reserved consumer semantics | +| Lab ledger / artifacts / projection | Present; `task_effectiveness` already a legal evidence layer | +| CL-04 catalog | Protocol + live only — no fabric suite discovery yet | +| CL-05 matrix | Already lists `task_effectiveness` column; read-only | +| CL-06 routing | `requiredSuites.evidenceLayer` is **only** `protocol_conformance` \| `live_route_compatibility` | +| Live sandbox | Counter/env limits for probes; **not** a synthetic scratch-tree executor | +| `fabric-core` / `synthetic-patch` / `exact-tree-diff-v1` | Reserved in `020_scenario_contract_and_catalogue.md`; **no runtime implementation** | + +**Decision:** CL-07 implements a **bounded Lab-owned task producer** under `src/lab/fabric/`. It does **not** invent a general Agent Fabric platform, ACP/A2A orchestration, background grind, or user-worktree execution. + +## A. Frozen producer boundary + +### A.1 Subject identity + +Evidence uses existing `TaskSubjectV1` only (no alternate task identity): + +```text +subjectSchemaVersion 1 +subjectKind task +routeSubject RouteSubjectV1 # exact nested route from CL-03/CL-06 builders +taskClassId +taskClassVersion +taskFixtureDigest +verifierManifestDigest +fabricCompatibilityVersion +sandboxProfileDigest +``` + +`subjectId = subjectIdForSubject(taskSubject)` (existing digest helper). + +Any material change to route, task class, fixture, verifier, fabric compatibility version, or sandbox profile yields a distinct subject. Evidence must not reuse across subjects. + +### A.2 Producer outcome schema (`FabricTaskOutcomeV1`) + +Closed, fail-closed on unknown fields. Schema version `1`. + +Required fields: + +| Field | Role | +|---|---| +| `schemaVersion` | `1` | +| `taskClassId` / `taskClassVersion` | Exact class | +| `routeSubject` / `taskSubject` / `subjectId` | Exact identities | +| `taskFixtureDigest` | Fixture identity | +| `verifierManifestDigest` | Verifier identity | +| `fabricCompatibilityVersion` | Executor contract version | +| `sandboxProfileDigest` | Sandbox policy identity | +| `startedAt` / `completedAt` | Execution window (ms epoch integers) | +| `limits` | Declared ceilings | +| `usage` | Actual resource counters used for limit checks | +| `outcome` | Normalized: `pass` \| `fail` \| `blocked` \| `inconclusive` | +| `verifier` | Bounded `exact-tree-diff-v1` result | +| `failure` | Optional typed failure (`class`, `code`, `retryable`, `attribution`) | +| `artifactDigests` | Content-addressed digests only | +| `sourceRefs` | Optional safe IDs (request / route-decision / attempt) — never bodies | + +**Forbidden in outcome and artifacts:** user repositories, arbitrary file trees, prompts, hidden reasoning, credentials, env secrets, host paths, unrestricted logs/stdout/stderr, raw model transcripts, arbitrary response bodies. + +### A.3 V1 executable scope (only) + +| Item | Value | +|---|---| +| Suite | `fabric-core@1.0.0` | +| Scenario | `fabric-core.task.synthetic-patch@1.0.0` | +| Evidence layer | `task_effectiveness` | +| Execution mode | `fabric` | +| Verifier | `exact-tree-diff-v1` | +| Fixture | Lab-owned scratch with `src/value.txt` = UTF-8 `before\n`; requested final `after\n` | + +No additional task classes in this phase. + +### A.4 Limits (non-weakened) + +| Limit | Value | +|---|---| +| Files touched | 1 | +| Aggregate input/output | 64 KiB | +| Patch operations | 1 | +| Total timeout | 30 s | +| Inactivity timeout | 5 s | +| Aggregate artifacts | 1 MiB | +| Network | denied in scratch | +| User MCP | unavailable | +| Arbitrary shell | unavailable | +| User repository | unreachable | + +### A.5 Patch producer seam + +Execution does **not** embed a general coding agent. Production evidence requires a host-issued `TrustedFabricPatchExecutor` invoked through `runFabricSyntheticPatchTaskForRoute`, with `RouteSubjectV1` built from `routeContext` + `destination` via `buildRouteSubjectV1`. Patch producers run in an isolated Bun child with hard termination on timeout. + +- Tests use `runFabricSyntheticPatchTaskHarness` with closed `FabricHarnessProducerKind` values or fixture executor modules; harness outcomes are not persistable via `persistFabricRunResult`. +- A future live route adapter may call a provider **outside** the scratch sandbox and return only a validated `SyntheticPatchV1`; raw prompts/responses never enter Lab storage. +- CL-07 does **not** ship automatic background execution (CL-08). + +## B. Verifier: `exact-tree-diff-v1` + +Deterministic, no LLM. + +1. Walk only the bounded scratch root (no follow). +2. Reject symlinks, special files, path traversal (`..`, absolute, drive prefixes), unexpected paths. +3. Sort repository-relative POSIX paths by UTF-8 bytes. +4. Hash/read allowed file bytes under bounds. +5. Pass iff the sole change is `src/value.txt: before\n → after\n` with no add/delete/rename. +6. Emit bounded structured result: `{ verifierId, manifestDigest, passed, pathSummaries[], reason? }`. + +Verifier manifest bytes participate in `verifierManifestDigest` → `TaskSubjectV1`. Behavior changes require a new digest; historical observations are never reinterpreted with current bytes. + +## C. Sandbox / execution boundary + +Minimal deny-by-default scratch executor: + +- Create ephemeral Lab-owned directory under the Lab paths tree (not the user repo). +- Materialize fixture files only. +- Apply at most one validated patch operation via direct file write (no shell). +- Enforce byte/time/inactivity ceilings. +- Cleanup on success, failure, and timeout. +- Freeze `sandboxProfileDigest` from a versioned sandbox profile object. + +Reuse patterns from `src/lab/artifacts/secure-fs.ts` / live sandbox env stripping where applicable; do not reuse live provider network transport inside the scratch. + +## D. Route identity + +Nested `RouteSubjectV1` must come from existing CL-03/CL-06 builders (`buildRouteSubjectV1` / policy subject helpers). Approximate `provider/model` strings are forbidden. Optional `sourceRefs` may cite `routeDecisionId` / attempt IDs without copying request content. + +## E. Lab ingestion + +`observationFromFabricOutcome` / internal `persistFabricOutcome` (not public): + +- `evidenceLayer: "task_effectiveness"` +- `executionMode: "fabric"` +- Exact scenario/suite IDs + manifest digests + fixture digests +- Exact `TaskSubjectV1` + `subjectId` +- Assertions from verifier +- Typed `failure` attribution +- Bounded environment metadata +- Sanitized artifact refs via existing artifact store + +No second ledger, mutable task DB, or separate verdict store. JSONL canonical; SQLite rebuildable; verdicts projected. + +### E.1 Idempotency + +Event identity uses existing `assignEventId` content-addressing. Replaying the same outcome (same event payload identity) must not create contradictory evidence. A legitimate second attempt uses a distinct `attempt` / timing / outcome payload and remains distinct evidence. + +## F. Verdict / failure mapping + +Layer remains independent of protocol/live. + +| Condition | Observation outcome | Failure class → attribution | Projection effect | +|---|---|---|---| +| Verifier pass (current required task) | `pass` | — | May contribute to `PROBED`/`VERIFIED` per suite rule | +| Verifier semantic fail | `fail` | `behavioral_failure` → `route` | `DEGRADED` path per suite | +| Sandbox violation / containment | `blocked` or `inconclusive` | `sandbox_violation` → `harness`/`environment` | `BLOCKED` / none | +| Timeout / inactivity / budget | `blocked` | matching blocker → `environment` | none (not route incompatibility) | +| Harness/executor defect | `inconclusive` | `harness_failure` → `harness` | none | +| Malformed producer outcome | reject (no event) | — | — | +| Artifact integrity failure | reject or invalidate | `integrity_failure` → `harness` | invalidate | + +Registry/provider claims **cannot** create task-effectiveness `CLAIMED`. + +## G. Freshness / invalidation / artifacts + +Reuse existing Lab freshness, invalidation, sensitive purge, and rebuild. Missing historical contract artifacts make evidence unusable. + +Allowed artifacts: verifier summary, normalized tree-diff summary, bounded execution metadata, sanitized failure summary. Never file bodies, prompts, or raw logs. + +## H. Read surfaces / UI / CL-06 interaction + +- Extend CL-04 `queryLabCatalog` to discover `fabric-core` scenarios. +- Projection/query already accept `task_effectiveness` subjects. +- CL-05 matrix remains read-only; show task layer if already wired. +- **CL-06 unchanged:** do **not** add `task_effectiveness` to routing `requiredSuites`. `TaskSubjectV1` cannot be resolved pre-dispatch (task class/fixture/verifier/sandbox unknown until execution). Historical task evidence may appear in Lab reads; it must not silently alter production routing without a future explicit contract (out of CL-07 scope). + +## I. Rejected alternatives + +| Alternative | Why rejected | +|---|---| +| General Agent Fabric API / worktree runner | Out of scope; unsafe surface area | +| Shell-based patch apply | Violates no-arbitrary-shell; use direct write | +| Pre-dispatch routing on task suites | Subject unknown before execution | +| Numerical effectiveness score / leaderboard | Forbidden by CL-00 | +| Second ledger / mutable verified flag | Breaks Lab persistence authority | +| Embedding prompts in observations | Privacy violation | +| Weakening byte/time limits for tests | Contract non-negotiable | +| CL-08 auto/background probing | Explicit exclusion | + +## J. Security review checklist + +Scratch-root containment; symlink/path-traversal/special-file rejection; max files/bytes; artifact publication bounds; no network/MCP/shell in scratch; minimal env; no credentials/config exposure; cleanup; concurrent isolation; hostile patch paths. Fail closed. Adversarial tests required (§18 of programme request). + +## K. Implementation layout (proposed) + +```text +src/lab/fabric/ + constants.ts + types.ts + subject.ts + fixture.ts + sandbox-profile.ts + scratch.ts + patch.ts + verifier.ts + executor.ts + manifest.ts + observe.ts + index.ts +tests/lab-fabric-task.test.ts +``` + +## L. Validation plan + +Typecheck; focused fabric/subject/sandbox/verifier/observe/ledger/projection/read-surface tests; routing regressions proving CL-06 unchanged; privacy scan; hygiene; GUI only if touched. + +## M. Status + +- **State:** ACCEPTED / CLOSED +- **Merge commit on `dev`:** `02e62fc8c7354c544ef71f8bb3db5ebba42cb600` +- **Accepted / source head:** `0efe2c69514d3baefee686383fe740e4ecb37d83` +- **Starting SHA:** `b66e33ce7207d91014644d99317e456c992a3418` (CL-06 merge #1394) +- **PR:** [#1438](https://github.com/lidge-jun/opencodex/pull/1438) (merged) + +### Authoritative route execution boundary + +- Production evidence uses `runFabricSyntheticPatchTaskForRoute({ routeContext, destination, patchExecutor })`. +- `RouteSubjectV1` is built only via `buildRouteSubjectV1(routeContext, destination)` — callers cannot supply an independent route identity. +- Patch production requires a host-issued `TrustedFabricPatchExecutor` (`createHostIssuedFabricPatchExecutor` in `src/lib/fabric-task-host.ts`). +- **Public ingestion:** `persistFabricRunResult` only; it rejects harness runs (`executionAuthority !== "trusted_route"`). +- `persistFabricOutcome` is internal to `observe.ts` and is **not** exported from the fabric public surface. +- Harness runs (`runFabricSyntheticPatchTaskHarness`) use closed `FabricHarnessProducerKind` values only; they cannot create production ledger evidence. + +### Child isolation / IPC / timeouts + +- Patch producers run in a dedicated Bun child (`producer-child.ts`) spawned by `producer-isolate.ts` with minimal env (`TZ`, `NO_COLOR`, `OCX_FABRIC_SCRATCH_ROOT`). +- Parent↔child protocol is newline-delimited JSON on stdout only (`activity`, `result`, `error` in `producer-protocol.ts`). Arbitrary logging is not mixed into protocol output. +- Parent owns **both** total and inactivity timeouts; both terminate the child via `SIGKILL`. Classification: `timeout` vs `inactivity_timeout`. +- Child stdout is capped at 64 KiB protocol bytes; stderr diagnostic capture capped at 4 KiB. Exceeding protocol limits → `budget_exhausted` and child kill. +- `lastActivityAt` is authoritative in the parent; child `reportActivity()` emits `activity` IPC messages that reset the inactivity deadline. +- `infinite_sync` harness disables inactivity ceiling extension so synchronous CPU spin is classified under total timeout. + +### Sandbox enforcement (honest scope) + +- Scratch containment, symlink/path-traversal/special-file rejection, byte/file limits, and cleanup are enforced in the parent via `scratch.ts`, `patch.ts` (`assertSafeRelativePosixPath`), and `applySyntheticPatch`. +- Child isolation strips proxy env vars on the parent path and runs producers in a separate process with minimal env — **not** an OS-level network/shell sandbox. +- Host-issued executor modules may still perform direct host filesystem operations outside the scratch tree; that is outside the scratch-apply boundary and is not claimed as blocked. +- Declared deny flags (`fabricDeclaredSandboxPolicy`) document intent; runtime enforcement matches the scratch/patch/verifier containment above. + +### Failure attribution + +- Semantic verifier mismatch → `fail` / `behavioral_failure` / `route`. +- Sandbox/containment (`FabricTaskError` from scratch/verifier/patch infrastructure) → `blocked` or `inconclusive` / `sandbox_violation` / `harness` or `environment` — never route-attributed `behavioral_failure`. +- Timeouts / budget exhaustion → `blocked` / `environment`. +- Harness defects → `inconclusive` / `harness`. + +### Inactivity accounting + +- `inactiveMs` = `completedAt - lastActivityAt` where `lastActivityAt` is updated only from parent-received `activity` IPC (or initial start). + +### Local validation (blocker fix head) + +- `bun x tsc --noEmit`: passed +- `bun test tests/lab-fabric-task.test.ts`: 46/46 passed +- `bun run privacy:scan`: passed +- Windows sqlite projection flakes in `lab-evidence-ledger.test.ts` (EBUSY) — environmental, not CL-07 +- CL-08: **not started** diff --git a/devlog/_plan/260810_release_train_and_triage/002_audit_round2.md b/devlog/_plan/260810_release_train_and_triage/002_audit_round2.md index 5cd8b9dd15..9aa0aeb71c 100644 --- a/devlog/_plan/260810_release_train_and_triage/002_audit_round2.md +++ b/devlog/_plan/260810_release_train_and_triage/002_audit_round2.md @@ -68,9 +68,10 @@ any risk acceptance must name that. ## What round 2 confirmed as correct -- **SEC-03 is pre-existing.** `src/oauth/store.ts` resolves to the same blob at +- **SEC-03 is pre-existing.** The file carrying it resolves to the same blob at the v2.11.1 tag and the RC; `git diff --exit-code` succeeds; the path log is - empty. #1369 narrows malformed local imports and does not worsen it. + empty. #1369 narrows malformed local imports and does not worsen it. The path + is withheld because SEC-03 is still unfixed. - **SEC-01's artifact bound holds.** `.npmignore:6` excludes `.github/` and `prepare-package.ts` copies nothing from it. - **The omission risk acceptance is sound.** No open issue names #1398, #1396, diff --git a/devlog/_plan/260810_release_train_and_triage/010_release_execution.md b/devlog/_plan/260810_release_train_and_triage/010_release_execution.md index 162f00cb5a..534fc503ec 100644 --- a/devlog/_plan/260810_release_train_and_triage/010_release_execution.md +++ b/devlog/_plan/260810_release_train_and_triage/010_release_execution.md @@ -92,7 +92,11 @@ dirty tree, and this checkout is untracked-dirty with `.dirfd-probe-29692.ok` run from dedicated clean checkouts: - `preview`: `/Users/jun/.codex/worktrees/260728-preview/opencodex` -- `main`: a separate clean worktree created for this train +- `main`: `/tmp/ocx-main-release-p8K6ss` — created for this train with + `git worktree add`, checked out at `main` (`121f1ad92`), `git status + --porcelain` empty. An earlier revision of this file claimed a main worktree + existed while step 3 still read ``; the audit caught the + gap and the path above is the real one. Nothing in the primary checkout is stashed, reset, or deleted. @@ -180,10 +184,10 @@ The **same RC**, not the preview release commit: ```bash git ls-remote origin refs/heads/main # re-pin -cd +cd /tmp/ocx-main-release-p8K6ss # the clean main worktree git status --porcelain # must be empty git pull --ff-only origin main -git merge --no-ff -m "Merge dev RC into main: promote the v2.12.0 line" +git merge --no-ff 9c051342d -m "Merge dev RC 9c051342d into main: promote the v2.12.0 line" ``` The 2026-08-09 train needed a `commit-tree` merge because `main`'s tree had @@ -224,5 +228,9 @@ normal merge commit and no history is rewritten. ## Out of scope -Fixing #1302, resolving the Bun macOS segfault, merging #1398 into this train, -and touching any contributor PR. +Fixing #1302, resolving the Bun macOS segfault, and touching any contributor PR. + +"Merging #1398 into this train" was listed here while the RC was `dc4dd45b0`. +It is stale: the re-picked RC `9c051342d` already contains #1398, #1396, and +#1010 (see `011` §"Re-pick after remediation"). Nothing is deferred out of this +train on RC grounds. diff --git a/devlog/_plan/260810_release_train_and_triage/011_rc_selection.md b/devlog/_plan/260810_release_train_and_triage/011_rc_selection.md index 86aace83b0..16a0f6165d 100644 --- a/devlog/_plan/260810_release_train_and_triage/011_rc_selection.md +++ b/devlog/_plan/260810_release_train_and_triage/011_rc_selection.md @@ -1,5 +1,13 @@ # 011 — WP1: release-candidate selection under a moving branch +> **SUPERSEDED for the shipped release. The RC below (`dc4dd45b0`) was the +> WP1 choice while the train was blocked on the security gate. After the +> fix-first decision, WP4/WP5 remediation landed and `dev` was pushed to +> `9c051342d`, which is the RC this train actually released.** The analysis +> below is kept because its root cause is still true and still constrains any +> future train. What changed, and what it invalidates, is recorded in +> "Re-pick after remediation" at the end of this file. + The RC rule in `010` is "the newest `dev` commit holding a completed successful Cross-platform CI run on its exact SHA". Applying it required understanding why the newer heads keep failing to produce one. @@ -96,3 +104,44 @@ Windows full shards were skipped by the runner-selection job, while the separate Windows keyring and npm-global smoke jobs passed. Local gates on the same tree: `bun run typecheck` exit 0, `bun run test` 10,526 pass / 0 fail, `bun run privacy:scan` passed. + +## Re-pick after remediation — RC = `9c051342d` + +The security gate returned BLOCK, the owner chose fix first, and the +remediation work-phases (WP4 SEC-02, WP5 SEC-01) plus eight rounds of +re-review produced 20 new local commits. Pushing them moved `origin/dev` from +`0de4fd2d7` to `9c051342d`, and a release must ship the remediated tree — the +whole point of the fix-first decision. So the RC is re-picked: + +**RC = `9c051342d7ff7ad81b71911e359ad5935eaaf235`.** + +Delta against the superseded RC: `git rev-list --count dc4dd45b0..9c051342d` +is **41 commits**, `git diff --shortstat` is **120 files, +8,520 / −225**. + +### This voids the omission risk acceptance above + +The section "Why omitting three commits is acceptable here" asked the owner to +accept shipping without #1398, #1396, and #1010. That acceptance is now **moot** +— all three are ancestors of the new RC: + +``` +$ git merge-base --is-ancestor 277354073 9c051342d # #1398 -> exit 0 +$ git merge-base --is-ancestor 0a76ee854 9c051342d # #1396 -> exit 0 +$ git merge-base --is-ancestor 2beeea654 9c051342d # #1010 -> exit 0 +``` + +Nothing is being omitted from this train, so no risk acceptance is required for +it. `010`'s "Out of scope" line about #1398 is stale for the same reason. + +### Evidence that does NOT carry over + +Every gate result recorded against `dc4dd45b0` — the exact-SHA CI run +`31352564082`, the `10,526 pass` suite, the merge dry runs — describes a tree +that is 120 files different from what ships. None of it is reused. The RC's own +gates are captured in `013_release_record.md`. + +### What still holds from the analysis above + +The branch-keyed `concurrency` group is unchanged, so an older `dev` commit +still cannot reliably be re-driven green while merges continue. The difference +this time is that the RC *is* the live head rather than a commit behind it. diff --git a/devlog/_plan/260810_release_train_and_triage/012_security_gate_record.md b/devlog/_plan/260810_release_train_and_triage/012_security_gate_record.md index 1921b34db2..8b38efc541 100644 --- a/devlog/_plan/260810_release_train_and_triage/012_security_gate_record.md +++ b/devlog/_plan/260810_release_train_and_triage/012_security_gate_record.md @@ -47,6 +47,23 @@ the tree went back through review. | Final verdict | **`READY TO SHIP`** | | Residual findings | 2 Low, both non-blocking and both now disclosed in `structure/09_compatibility-lab.md` | +### The reviewed tree is the released tree + +The RC published by this train is `9c051342d`, one commit past the reviewed +`76c544c65`. That gap is **this file** and nothing else: + +``` +$ git diff --name-only 76c544c65 9c051342d +devlog/_plan/260810_release_train_and_triage/012_security_gate_record.md +``` + +So the reviewed source tree and the shipped source tree are byte-identical; the +only change is the record you are reading. `0de4fd2d7` (the `origin/dev` head at +review time) is a genuine ancestor of both, merged in at `01a29ab6d` — +`git merge-base --is-ancestor 0de4fd2d7 76c544c65` exits 0. A later audit read +that line as claiming a merge that never happened; the ancestry check above is +the disproof. + The final reviewer verified the shipped limits table against the code and found it accurate: every disclosed residual behaves as documented, and nothing the table claims to cover failed. Performance is linear to 400 KB. Three regression @@ -88,16 +105,23 @@ suite 10,526 pass / 7 skip / 0 fail. | ID | Introduced by this delta? | Reaches the npm artifact? | Blocks the release? | |----|---------------------------|---------------------------|---------------------| -| SEC-01 | yes | **no** — repository automation only | yes, pending owner decision | -| SEC-02 | yes | **yes** — shipped runtime code | yes, pending owner decision | +| SEC-01 | yes | **no** — repository automation only | no longer — **CLOSED**, see re-review above | +| SEC-02 | yes | **yes** — shipped runtime code | no longer — **CLOSED**, see re-review above | | SEC-03 | **no** — pre-existing, byte-identical at v2.11.1 | yes (unchanged) | no | | SEC-04 | yes | yes | no — hardening item | -SEC-03's classification was independently re-verified: `src/oauth/store.ts` +> The two "blocks the release" cells above read `yes, pending owner decision` +> until the remediation landed. They are settled now: the owner chose option 1 +> and both findings are CLOSED. The historical BLOCK verdict earlier in this +> file is kept as a record of what the gate caught, not as the current state. + +SEC-03's classification was independently re-verified: the file carrying it resolves to the same blob at the v2.11.1 tag and at the RC, `git diff --exit-code` succeeds, and the path log over the delta is empty. It is a pre-existing defect surfaced by neighbouring work, not a regression this -release introduces. +release introduces. SEC-03 is **unfixed**, so its location is deliberately not +named here — naming the file for an open finding is the same disclosure the +rest of this record avoids. ## Correction: SEC-02 does reach users @@ -113,9 +137,11 @@ normalizes permissions. So: opt-in path. It is an end-user risk and must be named as one in any risk acceptance. -## Owner decision packet +## Owner decision packet — RESOLVED: option 1, fix first -The release does not proceed on this verdict. To continue, the owner picks one: +The release did not proceed on the BLOCK verdict. The owner was given these +three options and **chose option 1**; the remediation and re-review are recorded +in the "gate CLEARED" section above, and the train resumed from there. 1. **Fix first** — remediate SEC-01 and SEC-02, re-run the security review against the new RC, then run the train. Safest; costs a fix cycle. @@ -128,3 +154,7 @@ The release does not proceed on this verdict. To continue, the owner picks one: Generic deploy authorization does not cover this: it was given before either finding existed. That is the whole reason the gate was added. + +The owner's answer was recorded in-session on 2026-08-10: *"고치고 배포 — 두 발견 +수정 후 재리뷰"*, i.e. option 1. WP4/WP5 implemented the fixes, WP6 obtained the +non-BLOCK re-review, and WP7 executed the release on the remediated tree. diff --git a/devlog/_plan/260810_release_train_and_triage/013_release_record.md b/devlog/_plan/260810_release_train_and_triage/013_release_record.md new file mode 100644 index 0000000000..a74a06807b --- /dev/null +++ b/devlog/_plan/260810_release_train_and_triage/013_release_record.md @@ -0,0 +1,113 @@ +# 013 — WP7: the release as executed + +`010` is the runbook; this file is what actually happened. Where the two +disagree, this file is the record. + +## Released artifacts + +| Field | Value | +|-------|-------| +| RC | `9c051342d7ff7ad81b71911e359ad5935eaaf235` | +| preview version | `2.12.0-preview.20260810` | +| stable version | `2.12.0` | +| `preview` head after promotion | `f0306192e` (merge `eb96b3d42` + release bump) | +| `main` head after promotion | `6d881db20` (merge `1ed62f819` + release bump) | + +``` +$ npm view @bitkyc08/opencodex dist-tags --json +{ "latest": "2.12.0", "preview": "2.12.0-preview.20260810" } + +$ gh release view v2.12.0 -> isDraft=false, isPrerelease=false +$ gh release view v2.12.0-preview.20260810 -> isDraft=false, isPrerelease=true + +$ git merge-base --is-ancestor 9c051342d origin/preview # exit 0 +$ git merge-base --is-ancestor 9c051342d origin/main # exit 0 +``` + +Sibling containment holds in both directions from the RC, and `preview ⊆ main` +is deliberately not asserted — see `010` §"Promotion model". + +## Gates on the RC + +| Gate | Result | +|------|--------| +| Exact-SHA Cross-platform CI on `9c051342d` | success (run 31386116765, after one rerun) | +| `bun run typecheck` | exit 0 | +| `bun run test` | 10,679 pass / 7 skip / 0 fail (663 files) | +| `bun run privacy:scan` | passed | +| pre-push isolated suite | 10,686 tests, 0 fail | +| Security review | `READY TO SHIP` (see `012`) | + +## Two red gates, both infrastructure + +The flake budget in `010` allows one `gh run rerun --failed` per gate. Both +were spent, and both failures were diagnosed before the rerun rather than +retried blindly. + +**RC gate — run 31386116765.** One macOS test failed: + +``` +(fail) native profile OpenCodex process-exit phases > hard OpenCodex process + exit after each published transaction phase converges exact auth, vault, + journal, gate, and runtime bearer +error: Was there a typo in the url or port? + path: "http://127.0.0.1:0/v1/responses", code: "FailedToOpenSocket" +``` + +Port `0` means the harness read the child's port before it was published — a +startup race in the test, not in the product. The same test passed locally in +the same suite run, and the three sibling cases in the same file passed on the +same runner. Rerun: green. + +**Stable gate — run 31390767959.** Every test job passed, including macOS. +The aggregate was `cancelled` because the `storage policy` job's +`bun install --frozen-lockfile` produced no output for five minutes and was +killed: + +``` +2026-08-10T13:01:35Z bun install v1.3.14 (0d9b296a) +2026-08-10T13:06:40Z ##[error]The operation was canceled. +``` + +A dependency install that never starts resolving is a runner/registry stall. +Rerun: green. + +Neither failure touched the release delta, and neither is #1302. + +## Recovery path taken for the stable publish + +`release.ts:217-228` exits the moment it sees a completed failed run — after +the version bump is already committed and pushed. That happened here, exactly +as `010` §"Recovery when a gate goes red" predicted, so the documented +continuation was used rather than re-running the helper: + +```bash +gh run rerun 31390767959 --failed # -> success +gh run list --commit 6d881db20… # Cross-platform CI + Service lifecycle both success +git ls-remote origin refs/heads/main # still 6d881db20…, unmoved +gh workflow run release.yml --ref main \ + -f version=2.12.0 -f tag=latest \ + -f expected-sha=6d881db206c6a74da6b64fa22b6980faf05d0122 -f dry-run=false +gh run watch 31391815425 --exit-status # -> success +``` + +The preview publish needed no recovery: run 31389633331 succeeded on the +helper's own dispatch. + +## One surprise worth recording + +The stable release worktree was created fresh, so it had no `node_modules`. +`release.ts` runs its dependency audit before typecheck, and the audit passes +in an empty tree while typecheck then dies on `Cannot find type definition file +for 'bun-types'`. The fix is `bun install` in both the root and `gui/` before +invoking the helper. Worth folding into the next train's runbook: a clean +worktree is a *dependency-less* worktree, and the helper's preflight ordering +does not catch that early. + +## Release-notes annotation + +The publish workflow noted that the preview tag is not an ancestor of the +stable commit and kept `v2.11.1` as the notes baseline. That is the sibling +promotion model working as designed, not a defect: the two release commits are +siblings by construction. The workflow carried the preview notes forward into +`v2.12.0` on its own. diff --git a/devlog/_plan/260810_release_train_and_triage/014_post_release_closure.md b/devlog/_plan/260810_release_train_and_triage/014_post_release_closure.md new file mode 100644 index 0000000000..a9e8f65657 --- /dev/null +++ b/devlog/_plan/260810_release_train_and_triage/014_post_release_closure.md @@ -0,0 +1,79 @@ +# 014 — WP8: closure against the released state + +`020` and `030` triaged against `dc4dd45b0` **before** anything was published, +and concluded zero closable. That conclusion was correct then and stale now, +for two reasons: the shipped RC carries 41 more commits, and an issue whose +only blocker was "fixed on `dev`, not yet released" becomes genuinely resolved +the moment the fix is on npm. So the sweep was re-run from scratch against +`9c051342d` / `2.12.0` rather than inherited. + +## Result + +| | Count | +|---|---| +| Open issues examined | 68 | +| **Closed** | **2** | +| Not closable | 66 | +| Open PRs examined | 22 | +| Superseded (closable) | **0** | +| Stale (left open) | 3 | +| Viable (left open) | 19 | + +## Closed + +**#1366** — imported local CLI credential with invalid `expires_at` adopted and +never refreshed. Fixed by #1369 (`831a120ea`, merged `e8ce2b93d`). + +**#1383** — Command Code 502 `Tool result is missing for tool call`. Fixed by +#1411: `aca275265` pairs calls with results and synthesizes an explicit +missing-result error, and `fc1c729ec` buffers image carriers until tool results +close — a second path to the same 502. + +Both were verified twice, independently of the triage agent's claim: + +``` +$ git merge-base --is-ancestor 831a120ea 9c051342d # exit 0 +$ git merge-base --is-ancestor fc1c729ec 9c051342d # exit 0 +$ git tag --contains 831a120ea | grep -x v2.12.0 # v2.12.0 +$ git tag --contains fc1c729ec | grep -x v2.12.0 # v2.12.0 +``` + +The tag check is the one that matters for a closure comment: ancestry proves +the fix is in the RC, but only the tag proves it reached the version users can +install. Each closing comment cites the PR, the commits, and 2.12.0. + +## Why only two + +The 66 remaining issues fall into a few honest buckets: the fix exists only in +an open or draft PR (#1417→#1418, #1415→#1424, #1354→#1407, #1148→#1397, +#1076→#1357, #657→#1410), the feature was never implemented, or the report +still needs a reproduction from the author. None of those becomes closable +because a release happened. + +Three near-misses were checked specifically and rejected: + +- **#1302** — 2.12.0 does contain a hang mitigation (`183741b82`), but the + broader cross-file Linux hang reproduced *after* it. Partial fix, still open. +- **#822 / #657** — #1396 bounds reset-credit lookup responses; neither auto + redemption nor recovery is implemented. Bounding a parse is not the feature. +- **#1299** — #1010 ships configurable cost overlays, not manufacturer-rate + gateway aliases. + +## Why zero PRs closed + +Supersession was tested by looking for each PR's defining file or behavior in +the released tree. For #1161, #1008, #811, #1397, #1394, #1361, #1357, #1164, +#1422, and #1410 the defining path is simply absent from `9c051342d`, so no +supersession claim survives. + +Three PRs (#1161, #1008, #811) are genuinely stale — old conflicting heads with +no recent author activity — but stale is not superseded, and closing +contributor work for being old is a maintainer judgment this train has no +authorization to make. They stay open. + +## Standing correction to `090` + +`090_outcome.md` records this unit's terminal outcome as `BLOCKED` + `NOOP`. +That was true when the security gate returned BLOCK. It is superseded by +`013` (release published) and this file (2 issues closed). The current terminal +outcome is `DONE`. diff --git a/devlog/_plan/260810_release_train_and_triage/090_outcome.md b/devlog/_plan/260810_release_train_and_triage/090_outcome.md index b884298339..e694105719 100644 --- a/devlog/_plan/260810_release_train_and_triage/090_outcome.md +++ b/devlog/_plan/260810_release_train_and_triage/090_outcome.md @@ -1,6 +1,16 @@ # 090 — unit outcome -**Terminal outcome: `BLOCKED` (release) + `NOOP` (closure sweep).** +> **SUPERSEDED. The outcome below was written while the security gate held the +> train. The owner chose fix first; WP4/WP5 remediated SEC-01 and SEC-02, WP6 +> obtained a `READY TO SHIP` re-review, and WP7/WP8 completed the train.** +> +> **Current terminal outcome: `DONE`.** v2.12.0 and v2.12.0-preview.20260810 +> are published from RC `9c051342d`; 2 issues closed with cited evidence. +> The record of what actually shipped is `013_release_record.md`, and the +> closure sweep is `014_post_release_closure.md`. Everything below is kept as +> the state of the unit at the moment it was blocked. + +**Terminal outcome at time of writing: `BLOCKED` (release) + `NOOP` (closure sweep).** The block is an authorization boundary, not a mechanical failure. Every gate that a machine can decide is green. diff --git a/devlog/_plan/260811_260811-gpt56-cyber-model/000_plan.md b/devlog/_plan/260811_260811-gpt56-cyber-model/000_plan.md new file mode 100644 index 0000000000..963e142c24 --- /dev/null +++ b/devlog/_plan/260811_260811-gpt56-cyber-model/000_plan.md @@ -0,0 +1,133 @@ +# 000 — 260811-gpt56-cyber-model: Plan + +## Objective + +Register OpenAI's two Daybreak models on the keyed OpenAI provider under their +**alias** slugs, `daybreak-red-latest` and `daybreak-blue-latest`. + +### Why the alias, not the snapshot + +The request evolved across three turns: first "Daybreak Red and Blue", then +"just add cyber for now", then — decisively — *put the alias slugs in, don't +pin `gpt-5.6-sol`, because the alias is the name OpenAI keeps swapping the model +behind.* + +That last instruction is the correct read of the source, and it reverses an +earlier decision in this unit. The `-latest` aliases are the stable contract: + +- `daybreak-red-latest` → default snapshot `gpt-5.6-cyber` today. +- `daybreak-blue-latest` → default snapshot `gpt-5.6-sol` today. + +"Today" is the whole point. Both pages carry a Snapshots section whose stated +purpose is that a snapshot "lock[s] in a specific version of the model so that +performance and behavior remain consistent" — which is precisely what we do +**not** want here. Registering `gpt-5.6-cyber` would freeze the row at the +current snapshot and go stale the moment OpenAI repoints the alias; registering +`daybreak-red-latest` inherits every future swap for free. + +It also settles the earlier objection to Blue. Adding `gpt-5.6-sol` a second +time would have been redundant, so Blue was dropped. `daybreak-blue-latest` is +*not* redundant: it is a distinct, separately-provisioned endpoint whose +safeguards are "calibrated for defensive cybersecurity work", and whose target +model will drift away from `gpt-5.6-sol` over time. Both aliases go in. + +Snapshot ids (`gpt-5.6-cyber`) stay out of the registry entirely — no value in +carrying a name that ages badly when the alias covers it. + +### Evidence base (primary sources, opened 2026-08-11) + +| Fact | `daybreak-red-latest` | `daybreak-blue-latest` | +|------|----------------------|------------------------| +| Default snapshot | `gpt-5.6-cyber` | `gpt-5.6-sol` | +| Context window | 400,000 | 1,050,000 | +| Max input tokens | 272,000 | 922,000 | +| Max output tokens | 128,000 | 128,000 | +| Input modalities | text, image | text, image | +| Reasoning tokens | supported | supported | +| Effort ladder | not published | not published | +| Chat Completions | **Not supported** | **Not supported** | +| Responses | Supported | Supported | +| Access | separate Daybreak approval/provisioning | same | + +Sources, all opened 2026-08-11: +`developers.openai.com/api/docs/models/daybreak-red-latest.md`, +`.../daybreak-blue-latest.md`, `.../models.md` (catalog lines 30-31), +`.../pricing.md` (Cyber models table, lines ~200-208). + +Two constraints do real work here: + +1. **Responses-only.** Both endpoint tables mark `v1/chat/completions` as + Not supported. This decides which provider may carry the rows. +2. **Blue's metadata equals `gpt-5.6-sol`'s** (1,050,000 / 922,000), while Red + matches the cyber snapshot (400,000 / 272,000). The alias inherits its + current snapshot's numbers, so these values are themselves snapshot-dated + and will need a refresh when OpenAI repoints an alias. + +### Pricing correction + +An earlier draft of this unit put `gpt-5.6-cyber` in +`OPENAI_GPT56_CONTEXT_MODELS`, inheriting the family's ">272K = 2× input / +1.5× output" long-context tier. Re-reading the grouped pricing table disproves +that: the cyber row's four long-context columns are all `-`, i.e. **no published +long-context tier**. `gpt-5.6-sol` by contrast publishes the full long row +($10/$1/$12.50/$45). So Red gets no tier row, and the earlier defensive-tier +rationale is withdrawn rather than carried forward. + +## Loop-spec + +- Loop archetype: verifier-defined (`bun run typecheck` + `bun run test`). +- Write scope: `src/providers/registry.ts`, `src/usage/expected-prices.ts`, + `tests/provider-registry-parity.test.ts`, `tests/codex-catalog.test.ts`, + `tests/usage-cost.test.ts`, this devlog unit. +- Out of scope, with reasons: + - **`grok-4.6` — disproved, do not add.** The user asked for it, and it does + not exist. `docs.x.ai/developers/models/grok-4.6.md` returns 404, the + non-`.md` page 307-redirects to the model index instead of resolving, + `docs.x.ai/llms.txt` contains zero `grok-4.6` occurrences (it tops out at + `grok-4.5`), and `x.ai/news/grok-4-6` returns 403 with no such announcement. + Four independent research lanes (official docs, GitHub, aggregators, + LiteLLM/OpenRouter/Cursor registries) each reported the same absence. + Inventing the slug would seed a fabricated model id into the catalog, which + is exactly what the registry's docs-backed-refresh convention forbids. + - **Snapshot ids (`gpt-5.6-cyber`, `gpt-5.5-cyber`, `gpt-5.4-cyber`).** The + aliases cover them and keep covering them after a repoint; a pinned snapshot + row would go stale silently. `gpt-5.4-cyber` additionally publishes no + pricing at all (all cells `-`). + - **The Codex-login native catalog** (`src/codex/catalog/native-models.ts`). + Daybreak needs separate API provisioning and is absent from the ChatGPT + Codex-login upstream snapshot; a bare slug there would advertise a model the + login path cannot route. +- Budget: single work-phase, single cycle. + +## Work-phase map (one phase = one full PABCD cycle) + +| WP | Doc | Slice | Depends on | +|----|-----|-------|------------| +| wp-cyber-model | `010_phase1.md` | Register both Daybreak alias slugs on `openai-apikey` + pricing | — | + +## Accept criteria + +- `daybreak-red-latest` (400,000 context / 272,000 max input) and + `daybreak-blue-latest` (1,050,000 / 922,000) are both selectable on the + `openai-apikey` provider with text+image modalities. +- No snapshot id (`gpt-5.6-cyber` and the older `-cyber` rows) enters the + registry: the aliases are the only names carried. +- Neither alias advertises a reasoning-effort ladder: no published ladder exists + on either page, so `modelReasoningEfforts` carries an explicit `[]` for both + ids. Omitting the key would fall back to the full routed ladder + (`src/reasoning-effort.ts:76-78`, `src/codex/catalog/effort.ts:143-149`), so + the catalog rows must assert `reasoningEfforts: []`. +- Pricing is pinned by value, not just by presence: Red's exact + `12.5 / 75 / 1.25 / 15.625` tuple and Blue's `5 / 30 / 0.5 / 6.25`, both with + `status: "verified-derived"` — an alias price is its snapshot's price, and that + status is what keeps the `estimated` marker on (`src/usage/cost.ts:314-315`). +- Long-context tiers follow the published table, not the family default: Blue + gets the 272,000 exclusive tier (it publishes a full long row), Red gets + **none** (its long columns are all `-`). +- The Blue tier is scoped to `openai-apikey` only. It must not join the shared + `OPENAI_GPT56_CONTEXT_MODELS` list, which expands across both `openai` and + `openai-apikey` and would mint a tier for a Codex-login row that cannot exist. +- The model is **not** added to any chat-completions provider (Responses-only). +- `bun run typecheck` clean; `bun run test` green. +- No `grok-4.6` string in `src/`, `gui/src/`, `scripts/`, or `tests/`. This + devlog unit is exempt — it documents the rejection, so it names the slug. diff --git a/devlog/_plan/260811_260811-gpt56-cyber-model/010_phase1.md b/devlog/_plan/260811_260811-gpt56-cyber-model/010_phase1.md new file mode 100644 index 0000000000..2f2ebe5f79 --- /dev/null +++ b/devlog/_plan/260811_260811-gpt56-cyber-model/010_phase1.md @@ -0,0 +1,231 @@ +# 010 — wp-cyber-model: register the Daybreak alias slugs on `openai-apikey` + +## What goes in + +Two alias ids, and no snapshot ids: + +| Registry id | Context | Max input | Modalities | Effort ladder | +|-------------|--------:|----------:|------------|---------------| +| `daybreak-red-latest` | 400,000 | 272,000 | text, image | none published | +| `daybreak-blue-latest` | 1,050,000 | 922,000 | text, image | none published | + +Rationale for aliases over snapshots is in `000_plan.md` §Objective: OpenAI +repoints these names, so the alias is the stable contract and the snapshot is the +thing that goes stale. + +## Why `openai-apikey` and nowhere else + +Both endpoint tables mark `v1/chat/completions` **Not supported** and +`v1/responses` **Supported**. `openai-apikey` is the only provider here that is +both OpenAI-first-party and built on `adapter: "openai-responses"` +(`src/providers/registry.ts:1102-1122`). A chat-completions provider would +produce a selectable model that fails on first call. + +The Codex-login `openai` provider is also `openai-responses`, but its lineup is +the pinned upstream ChatGPT snapshot (`src/codex/catalog/native-models.ts:2-5`, +`src/codex/data/upstream-models.json`). Daybreak is separately provisioned and +absent from that snapshot, so a row there would advertise something the login +path cannot route. It stays out. + +Azure is deployment-named and chat-shaped — not a target. + +## MODIFY `src/providers/registry.ts` + +### 1. Constants, after the GPT-5.6 block (~line 322) + +```ts +/** + * Daybreak program aliases. These `-latest` ids are the stable contract: OpenAI + * repoints them at newer snapshots over time (red -> gpt-5.6-cyber, blue -> + * gpt-5.6-sol as of 2026-08-11), so registering the ALIAS inherits future model + * swaps while a pinned snapshot id would silently go stale. Snapshot ids are + * deliberately absent from this registry. + * Responses-only per both published endpoint tables — never add these to a + * chat-completions provider. Access needs separate Daybreak approval and + * provisioning, so neither is ever a default. + * Verified 2026-08-11: developers.openai.com/api/docs/models/daybreak-red-latest.md + * and .../daybreak-blue-latest.md + */ +const OPENAI_DAYBREAK_MODELS = ["daybreak-red-latest", "daybreak-blue-latest"]; +const OPENAI_DAYBREAK_CONTEXT_WINDOWS: Record = { + "daybreak-red-latest": 400_000, + "daybreak-blue-latest": 1_050_000, +}; +const OPENAI_DAYBREAK_MAX_INPUT_TOKENS: Record = { + "daybreak-red-latest": 272_000, + "daybreak-blue-latest": 922_000, +}; +``` + +Explicit `Record` annotations rather than `Object.fromEntries`: +the two aliases carry different numbers, so a literal map is both clearer and +better-typed (round-1 audit note 2). + +### 2. The `openai-apikey` entry (lines 1110-1121) + +`defaultModel` stays `gpt-5.5` — a provisioned-only model must never be a default. + +```ts +models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS], +liveModels: true, +modelContextWindows: { ...OPENAI_API_GPT56_CONTEXT_WINDOWS, ...OPENAI_DAYBREAK_CONTEXT_WINDOWS }, +modelMaxInputTokens: { ...OPENAI_API_GPT56_MAX_INPUT_TOKENS, ...OPENAI_DAYBREAK_MAX_INPUT_TOKENS }, +modelInputModalities: Object.fromEntries( + ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS] + .map(id => [id, ["text", "image"]]), +), +modelReasoningEfforts: { + ...Object.fromEntries( + [...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS]), + ), + ...Object.fromEntries(OPENAI_DAYBREAK_MODELS.map(id => [id, [] as string[]])), +}, +virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS, +``` + +### 3. Why `[]` and not omission + +Neither page publishes an effort ladder, and advertising an effort the model +rejects is a runtime 400. But **omitting** the key is not "no ladder" — it is the +opposite: + +- `configuredReasoningEfforts` returns `undefined` when neither the model nor the + provider supplies one, and its docstring states `undefined` means "no + override" while an empty array means "intentionally expose no effort control + for this model" (`src/reasoning-effort.ts:76-85`). +- `applyReasoningLevels` then falls back to the FULL routed ladder: + `sanitizeCodexReasoningEfforts(effortsOverride) ?? ROUTED_REASONING_LEVELS.map(l => l.effort)` + (`src/codex/catalog/effort.ts:143-149`). + +So omission would advertise `low|medium|high|xhigh|max`. Explicit `[]` is the +correct encoding. + +`noReasoningModels` is the wrong tool: both pages document reasoning-token +support, so these are not non-reasoning models — they publish no *selectable* +ladder. + +## MODIFY `src/usage/expected-prices.ts` + +### 1. Cost tuples (~line 44) + +From the grouped pricing table (`pricing.md`, Cyber models). Cache write is +1.25× uncached input, matching the table's own cache-write column. + +```ts +/** Daybreak Red (currently gpt-5.6-cyber). Alias pricing tracks its snapshot. */ +const DAYBREAK_RED: Cost4 = { input: 12.5, output: 75, cacheRead: 1.25, cacheWrite: 15.625 }; +/** Daybreak Blue (currently gpt-5.6-sol) — same published rates as that snapshot. */ +const DAYBREAK_BLUE: Cost4 = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 }; +``` + +### 2. Expected-price rows + +```ts +{ provider: "openai-apikey", modelId: "daybreak-red-latest", cost4: DAYBREAK_RED, source: `alias of gpt-5.6-cyber ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-11", status: "verified-derived" }, +{ provider: "openai-apikey", modelId: "daybreak-blue-latest", cost4: DAYBREAK_BLUE, source: `alias of gpt-5.6-sol ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-11", status: "verified-derived" }, +``` + +`verified-derived`, not `verified`. The file's own status semantics reserve +`verified` for "official page opened directly; the 4-tuple is the published API +price" and define `verified-derived` as "mapped from a verified base-model price" +(`src/usage/expected-prices.ts:6-9`). An alias price *is* its snapshot's price — +the pricing table has no `daybreak-*` rows at all, only `gpt-5.6-cyber` and +`gpt-5.6-sol`. The existing OpenAI `-pro` alias rows already use +`verified-derived` for exactly this reason (`expected-prices.ts:103-105`). + +This is not bookkeeping. `isEstimated` treats `verified-derived` as estimated +(`src/usage/cost.ts:314-315`), so marking these `verified` would silently drop +the `estimated` marker from the Logs cost column — and these rows are *more* +likely to drift than a normal row, since the alias can be repointed at a +differently-priced model at any time. Tests pin the status explicitly. + +### 3. Long-context tier — Blue only + +This corrects an earlier draft that gave the cyber model a long-context tier. +The grouped table is explicit: `gpt-5.6-cyber`'s four long-context cells are all +`-`, while `gpt-5.6-sol` publishes `$10.00 / $1.00 / $12.50 / $45.00`. + +`daybreak-blue-latest` therefore needs the 272,000-exclusive +`OPENAI_LONG_CONTEXT` rule, and `daybreak-red-latest` needs **no** tier row. + +It must NOT go into `OPENAI_GPT56_CONTEXT_MODELS`. That list is expanded across +both providers — `["openai", "openai-apikey"].flatMap(...)` +(`expected-prices.ts:246-256`) — so appending the alias would also mint +`openai/daybreak-blue-latest`, a Codex-login row for a model that path cannot +route. The file states the invariant plainly: "No model-level fallback: routed +resellers share model slugs but price independently" +(`expected-prices.ts:206-208`). Inventing a tier for a provider/model pair that +does not exist violates it. + +Instead add one explicit entry to `CONTEXT_TIERS`, scoped to `openai-apikey`: + +```ts +{ + // Alias of gpt-5.6-sol, which publishes the full long-context row + // ($10 / $1 / $12.50 / $45). Scoped to openai-apikey deliberately: Daybreak is + // not routable on the Codex-login `openai` provider, so no tier exists there. + // daybreak-red-latest has NO tier — the cyber row's long columns are all "-". + provider: "openai-apikey", + modelId: "daybreak-blue-latest", + thresholdInputTokens: 272_000, + inclusive: false, + multiplier: OPENAI_LONG_CONTEXT, + source: OPENAI_PRICING_DOC, + verifiedAt: "2026-08-11", +}, +``` + +`PRIORITY_MULTIPLIERS` gets no entry for either alias: the Fast-mode table lists +no Daybreak row and the resolver already falls back to 1×. + +## MODIFY tests + +Three suites assert exact lineups/counts and must move with the change: + +1. `tests/provider-registry-parity.test.ts:86` — the exact eight-id + `openai-apikey` seed. Append both alias ids. +2. `tests/provider-registry-parity.test.ts:95` — `toHaveLength(8)` becomes `10`. +3. `tests/codex-catalog.test.ts:4436` — `exactIds`. Append both. +4. `tests/codex-catalog.test.ts:4482` — the loop + `apiRows.filter(row => row.id.startsWith("gpt-5.6"))` asserts 1,050,000 / + 922,000 / the full ladder. The alias ids do **not** start with `gpt-5.6`, so + they fall outside it naturally — no narrowing needed. Assert both alias rows + separately with their own values, including `reasoningEfforts: []`. + (Choosing aliases over the snapshot id removed the round-3 collision here.) +5. `tests/usage-cost.test.ts:269` — `EXPECTED_PRICE_OVERLAYS.length` 51 → 53, + plus both ids in the membership assertion. +6. Value-level pricing assertions, since count+membership would pass with a + wrong tuple: + - exact tuples for both aliases, per the pattern at `usage-cost.test.ts:113-118`; + - `status: "verified-derived"` pinned for both rows, so a later edit to + `verified` cannot silently drop the `estimated` marker; + - Blue's exclusive 272,000 boundary (272,000 standard, 272,001 long) per `L1` + at `usage-cost.test.ts:573-577`; + - Red has **no** tier: assert 272,001 still bills at the standard rate; + - provider scoping: assert the Blue tier resolves for `openai-apikey` and is + `undefined` for `openai`, so the row cannot drift back into the shared + two-provider list. + +## Entitlement gating (out of scope, deliberate) + +Registry rows are reconstructed into the catalog even when live `/models` omits +them (`src/codex/catalog/provider-fetch.ts:1792`). All ten `openai-apikey` ids +behave this way, including the already-shipped tier-gated `-pro` ids, so these +rows inherit an existing property rather than introducing a leak, and they are +inert without a provisioned key. Entitlement-aware catalog admission is a real +improvement to the provider surface and its own unit; folding it into a +two-file model addition would turn this into a catalog-architecture change. + +## Verification + +1. `bun run typecheck` — clean. +2. `bun run test` — full suite green, including the three updated suites. +3. `rg -n "gpt-5\.6-cyber" src/` — must return nothing (aliases only). +4. `rg -n "grok-4\.6" src/ gui/src/ scripts/ tests/` — must return nothing. + Scoped to code on purpose: `devlog/` contains the string while documenting why + that model was rejected, so an unscoped grep could never pass. + +## Then + +Commit and push to `origin/dev` with `--no-verify`, per the user's explicit +instruction this cycle (no PR). diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index f17c886831..747819be14 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -58,101 +58,102 @@ export default defineConfig({ baseUrl: "https://github.com/lidge-jun/opencodex/edit/main/docs-site/", }, lastUpdated: true, - // English at the site root; Korean under /ko, Simplified Chinese under /zh-cn, Russian under /ru, Japanese under /ja. + // English at the site root; Korean under /ko, Simplified Chinese under /zh-cn, Traditional Chinese under /zh-tw, Russian under /ru, Japanese under /ja. defaultLocale: "root", locales: { root: { label: "English", lang: "en" }, ko: { label: "한국어", lang: "ko" }, "zh-cn": { label: "简体中文", lang: "zh-CN" }, + "zh-tw": { label: "繁體中文", lang: "zh-TW" }, ru: { label: "Русский", lang: "ru" }, ja: { label: "日本語", lang: "ja" }, }, sidebar: [ { label: "Getting Started", - translations: { ko: "시작하기", "zh-CN": "开始使用", ru: "Начало работы", ja: "はじめに" }, + translations: { ko: "시작하기", "zh-CN": "开始使用", "zh-TW": "開始使用", ru: "Начало работы", ja: "はじめに" }, items: [ - { label: "Installation", translations: { ko: "설치", "zh-CN": "安装", ru: "Установка", ja: "インストール" }, slug: "getting-started/installation" }, - { label: "Quickstart", translations: { ko: "빠른 시작", "zh-CN": "快速开始", ru: "Быстрый старт", ja: "クイックスタート" }, slug: "getting-started/quickstart" }, - { label: "How It Works", translations: { ko: "동작 원리", "zh-CN": "工作原理", ru: "Как это работает", ja: "仕組み" }, slug: "getting-started/how-it-works" }, - { label: "Agent Quickstart", translations: { ko: "에이전트 퀵스타트", "zh-CN": "Agent 快速上手", ru: "Быстрый старт для агентов", ja: "エージェント向けクイックスタート" }, slug: "getting-started/for-agents" }, + { label: "Installation", translations: { ko: "설치", "zh-CN": "安装", "zh-TW": "安裝", ru: "Установка", ja: "インストール" }, slug: "getting-started/installation" }, + { label: "Quickstart", translations: { ko: "빠른 시작", "zh-CN": "快速开始", "zh-TW": "快速入門", ru: "Быстрый старт", ja: "クイックスタート" }, slug: "getting-started/quickstart" }, + { label: "How It Works", translations: { ko: "동작 원리", "zh-CN": "工作原理", "zh-TW": "運作原理", ru: "Как это работает", ja: "仕組み" }, slug: "getting-started/how-it-works" }, + { label: "Agent Quickstart", translations: { ko: "에이전트 퀵스타트", "zh-CN": "Agent 快速上手", "zh-TW": "Agent 快速上手", ru: "Быстрый старт для агентов", ja: "エージェント向けクイックスタート" }, slug: "getting-started/for-agents" }, ], }, { label: "Guides", - translations: { ko: "가이드", "zh-CN": "指南", ru: "Руководства", ja: "ガイド" }, + translations: { ko: "가이드", "zh-CN": "指南", "zh-TW": "指南", ru: "Руководства", ja: "ガイド" }, items: [ - { label: "Providers", translations: { ko: "프로바이더", "zh-CN": "提供商", ru: "Провайдеры", ja: "プロバイダー" }, slug: "guides/providers" }, - { label: "Model Routing", translations: { ko: "모델 라우팅", "zh-CN": "模型路由", ru: "Маршрутизация моделей", ja: "モデルルーティング" }, slug: "guides/model-routing" }, - { label: "Codex Integration", translations: { ko: "Codex 통합", "zh-CN": "Codex 集成", ru: "Интеграция с Codex", ja: "Codex 連携" }, slug: "guides/codex-integration" }, - { label: "Codex App Model Picker", translations: { ko: "Codex App 모델 선택기", "zh-CN": "Codex App 模型选择器", ru: "Выбор модели в Codex App", ja: "Codex App モデルピッカー" }, slug: "guides/codex-app-models" }, - { label: "Model Ordering", translations: { ko: "모델 정렬에 관하여", "zh-CN": "模型排序", ru: "Сортировка моделей", ja: "モデルの並び順" }, slug: "guides/model-ordering" }, - { label: "Combos", translations: { ko: "콤보", "zh-CN": "组合", ru: "Комбо", ja: "コンボ" }, slug: "guides/combos" }, - { label: "Claude Code", translations: { ko: "Claude Code", "zh-CN": "Claude Code", ru: "Claude Code", ja: "Claude Code" }, slug: "guides/claude-code" }, - { label: "Grok Build", translations: { ko: "Grok Build", "zh-CN": "Grok Build", ru: "Grok Build", ja: "Grok Build" }, slug: "guides/grok-build" }, - { label: "opencode", translations: { ko: "opencode", "zh-CN": "opencode", ru: "opencode", ja: "opencode" }, slug: "guides/opencode" }, - { label: "Pi", translations: { ko: "Pi", "zh-CN": "Pi", ru: "Pi", ja: "Pi" }, slug: "guides/pi" }, - { label: "Integrations", translations: { ko: "연동", "zh-CN": "集成", ru: "Интеграции", ja: "連携" }, slug: "guides/integrations" }, - { label: "Sidecars: Web Search & Vision", translations: { ko: "사이드카: 웹 검색 & 비전", "zh-CN": "边车:网络搜索与视觉", ru: "Сайдкары: веб-поиск и зрение", ja: "サイドカー: ウェブ検索 & ビジョン" }, slug: "guides/sidecars" }, - { label: "Image Bridge", translations: { ko: "이미지 브릿지", "zh-CN": "图像桥接", ru: "Image Bridge", ja: "画像ブリッジ" }, slug: "guides/image-bridge" }, - { label: "Video Bridge", translations: { ko: "비디오 브릿지", "zh-CN": "视频桥接", ru: "Video Bridge", ja: "動画ブリッジ" }, slug: "guides/video-bridge" }, - { label: "Web Dashboard", translations: { ko: "웹 대시보드", "zh-CN": "网页控制台", ru: "Веб-дашборд", ja: "ウェブダッシュボード" }, slug: "guides/web-dashboard" }, - { label: "Sub-agent Surface", translations: { ko: "서브에이전트 서피스", "zh-CN": "子代理界面", ru: "Интерфейс подагентов", ja: "サブエージェントサーフェス" }, slug: "guides/sub-agent-surface" }, + { label: "Providers", translations: { ko: "프로바이더", "zh-CN": "提供商", "zh-TW": "供應商", ru: "Провайдеры", ja: "プロバイダー" }, slug: "guides/providers" }, + { label: "Model Routing", translations: { ko: "모델 라우팅", "zh-CN": "模型路由", "zh-TW": "模型路由", ru: "Маршрутизация моделей", ja: "モデルルーティング" }, slug: "guides/model-routing" }, + { label: "Codex Integration", translations: { ko: "Codex 통합", "zh-CN": "Codex 集成", "zh-TW": "Codex 整合", ru: "Интеграция с Codex", ja: "Codex 連携" }, slug: "guides/codex-integration" }, + { label: "Codex App Model Picker", translations: { ko: "Codex App 모델 선택기", "zh-CN": "Codex App 模型选择器", "zh-TW": "Codex App 模型選擇器", ru: "Выбор модели в Codex App", ja: "Codex App モデルピッカー" }, slug: "guides/codex-app-models" }, + { label: "Model Ordering", translations: { ko: "모델 정렬에 관하여", "zh-CN": "模型排序", "zh-TW": "模型排序", ru: "Сортировка моделей", ja: "モデルの並び順" }, slug: "guides/model-ordering" }, + { label: "Combos", translations: { ko: "콤보", "zh-CN": "组合", "zh-TW": "組合", ru: "Комбо", ja: "コンボ" }, slug: "guides/combos" }, + { label: "Claude Code", translations: { ko: "Claude Code", "zh-CN": "Claude Code", "zh-TW": "Claude Code", ru: "Claude Code", ja: "Claude Code" }, slug: "guides/claude-code" }, + { label: "Grok Build", translations: { ko: "Grok Build", "zh-CN": "Grok Build", "zh-TW": "Grok Build", ru: "Grok Build", ja: "Grok Build" }, slug: "guides/grok-build" }, + { label: "opencode", translations: { ko: "opencode", "zh-CN": "opencode", "zh-TW": "opencode", ru: "opencode", ja: "opencode" }, slug: "guides/opencode" }, + { label: "Pi", translations: { ko: "Pi", "zh-CN": "Pi", "zh-TW": "Pi", ru: "Pi", ja: "Pi" }, slug: "guides/pi" }, + { label: "Integrations", translations: { ko: "연동", "zh-CN": "集成", "zh-TW": "整合", ru: "Интеграции", ja: "連携" }, slug: "guides/integrations" }, + { label: "Sidecars: Web Search & Vision", translations: { ko: "사이드카: 웹 검색 & 비전", "zh-CN": "边车:网络搜索与视觉", "zh-TW": "邊車:網路搜尋與視覺", ru: "Сайдкары: веб-поиск и зрение", ja: "サイドカー: ウェブ検索 & ビジョン" }, slug: "guides/sidecars" }, + { label: "Image Bridge", translations: { ko: "이미지 브릿지", "zh-CN": "图像桥接", "zh-TW": "圖像橋接", ru: "Image Bridge", ja: "画像ブリッジ" }, slug: "guides/image-bridge" }, + { label: "Video Bridge", translations: { ko: "비디오 브릿지", "zh-CN": "视频桥接", "zh-TW": "影片橋接", ru: "Video Bridge", ja: "動画ブリッジ" }, slug: "guides/video-bridge" }, + { label: "Web Dashboard", translations: { ko: "웹 대시보드", "zh-CN": "网页控制台", "zh-TW": "網頁儀表板", ru: "Веб-дашборд", ja: "ウェブダッシュボード" }, slug: "guides/web-dashboard" }, + { label: "Sub-agent Surface", translations: { ko: "서브에이전트 서피스", "zh-CN": "子代理界面", "zh-TW": "子代理介面", ru: "Интерфейс подагентов", ja: "サブエージェントサーフェス" }, slug: "guides/sub-agent-surface" }, ], }, { label: "Benchmarks", - translations: { ko: "벤치마크", "zh-CN": "基准测试", ru: "Бенчмарки", ja: "ベンチマーク" }, + translations: { ko: "벤치마크", "zh-CN": "基准测试", "zh-TW": "基準測試", ru: "Бенчмарки", ja: "ベンチマーク" }, collapsed: true, items: [ - { label: "Overview", translations: { ko: "개요", "zh-CN": "概览", ru: "Обзор", ja: "概要" }, slug: "benchmarks" }, - { label: "Coding", translations: { ko: "코딩", "zh-CN": "编程", ru: "Кодинг", ja: "コーディング" }, slug: "benchmarks/coding" }, - { label: "Frontend", translations: { ko: "프론트엔드", "zh-CN": "前端", ru: "Фронтенд", ja: "フロントエンド" }, slug: "benchmarks/frontend" }, - { label: "Terminal", translations: { ko: "터미널", "zh-CN": "终端", ru: "Терминал", ja: "ターミナル" }, slug: "benchmarks/terminal" }, - { label: "Security", translations: { ko: "보안", "zh-CN": "安全", ru: "Безопасность", ja: "セキュリティ" }, slug: "benchmarks/security" }, - { label: "Intelligence", translations: { ko: "인텔리전스", "zh-CN": "智能", ru: "Интеллект", ja: "インテリジェンス" }, slug: "benchmarks/intelligence" }, + { label: "Overview", translations: { ko: "개요", "zh-CN": "概览", "zh-TW": "概覽", ru: "Обзор", ja: "概要" }, slug: "benchmarks" }, + { label: "Coding", translations: { ko: "코딩", "zh-CN": "编程", "zh-TW": "程式設計", ru: "Кодинг", ja: "コーディング" }, slug: "benchmarks/coding" }, + { label: "Frontend", translations: { ko: "프론트엔드", "zh-CN": "前端", "zh-TW": "前端", ru: "Фронтенд", ja: "フロントエンド" }, slug: "benchmarks/frontend" }, + { label: "Terminal", translations: { ko: "터미널", "zh-CN": "终端", "zh-TW": "終端", ru: "Терминал", ja: "ターミナル" }, slug: "benchmarks/terminal" }, + { label: "Security", translations: { ko: "보안", "zh-CN": "安全", "zh-TW": "安全", ru: "Безопасность", ja: "セキュリティ" }, slug: "benchmarks/security" }, + { label: "Intelligence", translations: { ko: "인텔리전스", "zh-CN": "智能", "zh-TW": "智慧", ru: "Интеллект", ja: "インテリジェンス" }, slug: "benchmarks/intelligence" }, ], }, { label: "Reference", - translations: { ko: "레퍼런스", "zh-CN": "参考", ru: "Справочник", ja: "リファレンス" }, + translations: { ko: "레퍼런스", "zh-CN": "参考", "zh-TW": "參考", ru: "Справочник", ja: "リファレンス" }, items: [ { label: "CLI", - translations: { ko: "CLI", "zh-CN": "命令行", ru: "CLI", ja: "CLI" }, + translations: { ko: "CLI", "zh-CN": "命令行", "zh-TW": "命令列", ru: "CLI", ja: "CLI" }, items: [ - { label: "Overview", translations: { ko: "개요", "zh-CN": "概览", ru: "Обзор", ja: "概要" }, slug: "reference/cli" }, - { label: "Lifecycle & Service", translations: { ko: "라이프사이클 & 서비스", "zh-CN": "生命周期与服务", ru: "Жизненный цикл и служба", ja: "ライフサイクル & サービス" }, slug: "reference/cli/lifecycle" }, - { label: "Providers, Accounts & Models", translations: { ko: "프로바이더, 계정 & 모델", "zh-CN": "提供商、账户与模型", ru: "Провайдеры, аккаунты и модели", ja: "プロバイダー・アカウント・モデル" }, slug: "reference/cli/providers-accounts" }, - { label: "Agents, Routing & Integrations", translations: { ko: "에이전트, 라우팅 & 통합", "zh-CN": "代理、路由与集成", ru: "Агенты, маршрутизация и интеграции", ja: "エージェント・ルーティング・連携" }, slug: "reference/cli/agents" }, + { label: "Overview", translations: { ko: "개요", "zh-CN": "概览", "zh-TW": "概覽", ru: "Обзор", ja: "概要" }, slug: "reference/cli" }, + { label: "Lifecycle & Service", translations: { ko: "라이프사이클 & 서비스", "zh-CN": "生命周期与服务", "zh-TW": "生命週期與服務", ru: "Жизненный цикл и служба", ja: "ライフサイクル & サービス" }, slug: "reference/cli/lifecycle" }, + { label: "Providers, Accounts & Models", translations: { ko: "프로바이더, 계정 & 모델", "zh-CN": "提供商、账户与模型", "zh-TW": "供應商、帳號與模型", ru: "Провайдеры, аккаунты и модели", ja: "プロバイダー・アカウント・モデル" }, slug: "reference/cli/providers-accounts" }, + { label: "Agents, Routing & Integrations", translations: { ko: "에이전트, 라우팅 & 통합", "zh-CN": "代理、路由与集成", "zh-TW": "代理、路由與整合", ru: "Агенты, маршрутизация и интеграции", ja: "エージェント・ルーティング・連携" }, slug: "reference/cli/agents" }, ], }, { label: "Configuration", - translations: { ko: "설정", "zh-CN": "配置", ru: "Конфигурация", ja: "設定" }, + translations: { ko: "설정", "zh-CN": "配置", "zh-TW": "設定", ru: "Конфигурация", ja: "設定" }, items: [ - { label: "Overview", translations: { ko: "개요", "zh-CN": "概览", ru: "Обзор", ja: "概要" }, slug: "reference/configuration" }, - { label: "Providers", translations: { ko: "프로바이더", "zh-CN": "提供商", ru: "Провайдеры", ja: "プロバイダー" }, slug: "reference/configuration/providers" }, - { label: "Routing", translations: { ko: "라우팅", "zh-CN": "路由", ru: "Маршрутизация", ja: "ルーティング" }, slug: "reference/configuration/routing" }, - { label: "Agents", translations: { ko: "에이전트", "zh-CN": "代理", ru: "Агенты", ja: "エージェント" }, slug: "reference/configuration/agents" }, - { label: "Server & Runtime", translations: { ko: "서버 & 런타임", "zh-CN": "服务器与运行时", ru: "Сервер и рантайм", ja: "サーバー & ランタイム" }, slug: "reference/configuration/server" }, + { label: "Overview", translations: { ko: "개요", "zh-CN": "概览", "zh-TW": "概覽", ru: "Обзор", ja: "概要" }, slug: "reference/configuration" }, + { label: "Providers", translations: { ko: "프로바이더", "zh-CN": "提供商", "zh-TW": "供應商", ru: "Провайдеры", ja: "プロバイダー" }, slug: "reference/configuration/providers" }, + { label: "Routing", translations: { ko: "라우팅", "zh-CN": "路由", "zh-TW": "路由", ru: "Маршрутизация", ja: "ルーティング" }, slug: "reference/configuration/routing" }, + { label: "Agents", translations: { ko: "에이전트", "zh-CN": "代理", "zh-TW": "代理", ru: "Агенты", ja: "エージェント" }, slug: "reference/configuration/agents" }, + { label: "Server & Runtime", translations: { ko: "서버 & 런타임", "zh-CN": "服务器与运行时", "zh-TW": "伺服器與執行階段", ru: "Сервер и рантайм", ja: "サーバー & ランタイム" }, slug: "reference/configuration/server" }, ], }, - { label: "Adapters", translations: { ko: "어댑터", "zh-CN": "适配器", ru: "Адаптеры", ja: "アダプター" }, slug: "reference/adapters" }, - { label: "Architecture", translations: { ko: "아키텍처", "zh-CN": "架构", ru: "Архитектура", ja: "アーキテクチャ" }, slug: "reference/architecture" }, - { label: "Proxy API Formats", translations: { ko: "프록시 API 형식", "zh-CN": "代理 API 格式", ru: "Форматы API прокси", ja: "プロキシAPI形式" }, slug: "reference/proxy-formats" }, - { label: "Management API", translations: { ko: "관리 API", "zh-CN": "管理 API", ru: "API управления", ja: "管理API" }, slug: "reference/management-api" }, + { label: "Adapters", translations: { ko: "어댑터", "zh-CN": "适配器", "zh-TW": "適配器", ru: "Адаптеры", ja: "アダプター" }, slug: "reference/adapters" }, + { label: "Architecture", translations: { ko: "아키텍처", "zh-CN": "架构", "zh-TW": "架構", ru: "Архитектура", ja: "アーキテクチャ" }, slug: "reference/architecture" }, + { label: "Proxy API Formats", translations: { ko: "프록시 API 형식", "zh-CN": "代理 API 格式", "zh-TW": "代理 API 格式", ru: "Форматы API прокси", ja: "プロキシAPI形式" }, slug: "reference/proxy-formats" }, + { label: "Management API", translations: { ko: "관리 API", "zh-CN": "管理 API", "zh-TW": "管理 API", ru: "API управления", ja: "管理API" }, slug: "reference/management-api" }, ], }, { label: "Troubleshooting", - translations: { ko: "문제 해결", "zh-CN": "故障排除", ru: "Устранение неполадок", ja: "トラブルシューティング" }, + translations: { ko: "문제 해결", "zh-CN": "故障排除", "zh-TW": "疑難排解", ru: "Устранение неполадок", ja: "トラブルシューティング" }, collapsed: true, items: [ - { label: "Windows Memory Growth", translations: { ko: "Windows 메모리 증가", "zh-CN": "Windows 内存增长", ru: "Рост памяти в Windows", ja: "Windows メモリ増加" }, slug: "troubleshooting/windows-memory" }, + { label: "Windows Memory Growth", translations: { ko: "Windows 메모리 증가", "zh-CN": "Windows 内存增长", "zh-TW": "Windows 記憶體增長", ru: "Рост памяти в Windows", ja: "Windows メモリ増加" }, slug: "troubleshooting/windows-memory" }, ], }, - { label: "Contributing", translations: { ko: "기여하기", "zh-CN": "贡献", ru: "Как внести вклад", ja: "コントリビュート" }, slug: "contributing" }, + { label: "Contributing", translations: { ko: "기여하기", "zh-CN": "贡献", "zh-TW": "貢獻", ru: "Как внести вклад", ja: "コントリビュート" }, slug: "contributing" }, ], }), ], diff --git a/docs-site/public/screenshots/cockpit-account-import.png b/docs-site/public/screenshots/cockpit-account-import.png new file mode 100644 index 0000000000..ce9e05af0f Binary files /dev/null and b/docs-site/public/screenshots/cockpit-account-import.png differ diff --git a/docs-site/src/components/FrontierBoards.astro b/docs-site/src/components/FrontierBoards.astro index 2a2937d869..fadefb626c 100644 --- a/docs-site/src/components/FrontierBoards.astro +++ b/docs-site/src/components/FrontierBoards.astro @@ -10,7 +10,7 @@ import data from "../data/frontier-benchmarks.json"; import { FRONTIER_STRINGS } from "../data/frontier-i18n"; interface Props { - locale?: "en" | "ko" | "zh-cn" | "ru" | "ja"; + locale?: "en" | "ko" | "zh-cn" | "zh-tw" | "ru" | "ja"; /** Comma-separated board ids to render; omit for all boards. */ boards?: string; /** Show the page-level subtitle (overview pages only). */ @@ -26,7 +26,7 @@ const fill = (key: string, vars: Record): string => Object.entries(vars).reduce((acc, [k, v]) => acc.replace(`{${k}}`, v), t(key)); // The PR i18n has no column-header key for "score" — keep a tiny local map. -const SCORE_HEADER: Record = { en: "Score", ko: "점수", "zh-cn": "得分", ru: "Балл", ja: "スコア" }; +const SCORE_HEADER: Record = { en: "Score", ko: "점수", "zh-cn": "得分", "zh-tw": "得分", ru: "Балл", ja: "スコア" }; /** Dataset tags are kebab-case; i18n keys are camelCase (cheap-subagent → cheapSubagent). */ const tagKey = (tag: string): string => tag.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); diff --git a/docs-site/src/components/Header.astro b/docs-site/src/components/Header.astro index ea8ee26f9a..2de37c606e 100644 --- a/docs-site/src/components/Header.astro +++ b/docs-site/src/components/Header.astro @@ -19,14 +19,15 @@ const prefix = locale ? `${base}${locale}/` : base; type NavLink = { label: string; href: string }; type NavGroup = { label: string; links: NavLink[] }; -const t = (en: string, ko: string, zh: string, ru: string, ja: string) => - locale === 'ko' ? ko : locale === 'zh-cn' ? zh : locale === 'ru' ? ru : locale === 'ja' ? ja : en; +const t = (en: string, ko: string, zh: string, ru: string, ja: string, zhTW: string) => + locale === 'ko' ? ko : locale === 'zh-cn' ? zh : locale === 'zh-tw' ? zhTW : locale === 'ru' ? ru : locale === 'ja' ? ja : en; -type LanguageOption = { code: 'root' | 'ko' | 'zh-cn' | 'ru' | 'ja'; label: string }; +type LanguageOption = { code: 'root' | 'ko' | 'zh-cn' | 'zh-tw' | 'ru' | 'ja'; label: string }; const languages: LanguageOption[] = [ { code: 'root', label: 'English' }, { code: 'ko', label: '한국어' }, { code: 'zh-cn', label: '简体中文' }, + { code: 'zh-tw', label: '繁體中文' }, { code: 'ru', label: 'Русский' }, { code: 'ja', label: '日本語' }, ]; @@ -42,60 +43,61 @@ const languageHref = (code: LanguageOption['code']) => `${code === 'root' ? base : `${base}${code}/`}${routeSuffix}`; const themeLabels = { - auto: t('Auto', '자동', '自动', 'Авто', '自動'), - light: t('Light', '라이트', '浅色', 'Светлая', 'ライト'), - dark: t('Dark', '다크', '深色', 'Тёмная', 'ダーク'), + auto: t('Auto', '자동', '自动', 'Авто', '自動', '自動'), + light: t('Light', '라이트', '浅色', 'Светлая', 'ライト', '淺色'), + dark: t('Dark', '다크', '深色', 'Тёмная', 'ダーク', '深色'), }; -const themeName = t('Theme', '테마', '主题', 'Тема', 'テーマ'); +const themeName = t('Theme', '테마', '主题', 'Тема', 'テーマ', '主題'); const themeCycleHint = t( 'Click to use the next theme', '클릭하여 다음 테마로 전환', '点击切换到下一个主题', 'Нажмите, чтобы включить следующую тему', 'クリックして次のテーマに切り替え', + '點選切換到下一個主題', ); const groups: NavGroup[] = [ { - label: t('Getting Started', '시작하기', '开始使用', 'Начало работы', 'はじめに'), + label: t('Getting Started', '시작하기', '开始使用', 'Начало работы', 'はじめに', '開始使用'), links: [ - { label: t('Installation', '설치', '安装', 'Установка', 'インストール'), href: `${prefix}getting-started/installation/` }, - { label: t('Quickstart', '빠른 시작', '快速开始', 'Быстрый старт', 'クイックスタート'), href: `${prefix}getting-started/quickstart/` }, - { label: t('How It Works', '동작 원리', '工作原理', 'Как это работает', '仕組み'), href: `${prefix}getting-started/how-it-works/` }, - { label: t('Agent Quickstart', '에이전트 퀵스타트', 'Agent 快速上手', 'Быстрый старт для агентов', 'エージェント向けクイックスタート'), href: `${prefix}getting-started/for-agents/` }, + { label: t('Installation', '설치', '安装', 'Установка', 'インストール', '安裝'), href: `${prefix}getting-started/installation/` }, + { label: t('Quickstart', '빠른 시작', '快速开始', 'Быстрый старт', 'クイックスタート', '快速入門'), href: `${prefix}getting-started/quickstart/` }, + { label: t('How It Works', '동작 원리', '工作原理', 'Как это работает', '仕組み', '運作原理'), href: `${prefix}getting-started/how-it-works/` }, + { label: t('Agent Quickstart', '에이전트 퀵스타트', 'Agent 快速上手', 'Быстрый старт для агентов', 'エージェント向けクイックスタート', 'Agent 快速上手'), href: `${prefix}getting-started/for-agents/` }, ], }, { - label: t('Guides', '가이드', '指南', 'Руководства', 'ガイド'), + label: t('Guides', '가이드', '指南', 'Руководства', 'ガイド', '指南'), links: [ - { label: t('Providers', '프로바이더', '提供商', 'Провайдеры', 'プロバイダー'), href: `${prefix}guides/providers/` }, - { label: t('Model Routing', '모델 라우팅', '模型路由', 'Маршрутизация моделей', 'モデルルーティング'), href: `${prefix}guides/model-routing/` }, - { label: t('Combos', '콤보', '组合', 'Комбо', 'コンボ'), href: `${prefix}guides/combos/` }, - { label: t('Codex Integration', 'Codex 통합', 'Codex 集成', 'Интеграция с Codex', 'Codex 連携'), href: `${prefix}guides/codex-integration/` }, - { label: t('Codex App Model Picker', 'Codex App 모델 선택기', 'Codex App 模型选择器', 'Селектор моделей Codex App', 'Codex App モデルピッカー'), href: `${prefix}guides/codex-app-models/` }, + { label: t('Providers', '프로바이더', '提供商', 'Провайдеры', 'プロバイダー', '供應商'), href: `${prefix}guides/providers/` }, + { label: t('Model Routing', '모델 라우팅', '模型路由', 'Маршрутизация моделей', 'モデルルーティング', '模型路由'), href: `${prefix}guides/model-routing/` }, + { label: t('Combos', '콤보', '组合', 'Комбо', 'コンボ', '組合'), href: `${prefix}guides/combos/` }, + { label: t('Codex Integration', 'Codex 통합', 'Codex 集成', 'Интеграция с Codex', 'Codex 連携', 'Codex 整合'), href: `${prefix}guides/codex-integration/` }, + { label: t('Codex App Model Picker', 'Codex App 모델 선택기', 'Codex App 模型选择器', 'Селектор моделей Codex App', 'Codex App モデルピッカー', 'Codex App 模型選擇器'), href: `${prefix}guides/codex-app-models/` }, { label: 'Claude Code', href: `${prefix}guides/claude-code/` }, - { label: t('Sidecars: Search & Vision', '사이드카: 검색 & 비전', '边车:搜索与视觉', 'Сайдкары: поиск и зрение', 'サイドカー: 検索 & ビジョン'), href: `${prefix}guides/sidecars/` }, - { label: t('Image Bridge', '이미지 브릿지', '图像桥接', 'Image Bridge', '画像ブリッジ'), href: `${prefix}guides/image-bridge/` }, - { label: t('Video Bridge', '비디오 브릿지', '视频桥接', 'Video Bridge', '動画ブリッジ'), href: `${prefix}guides/video-bridge/` }, - { label: t('Web Dashboard', '웹 대시보드', '网页控制台', 'Веб-дашборд', 'ウェブダッシュボード'), href: `${prefix}guides/web-dashboard/` }, - { label: t('Sub-agent Surface', '서브에이전트 서피스', '子代理界面', 'Интерфейс подагентов', 'サブエージェントサーフェス'), href: `${prefix}guides/sub-agent-surface/` }, + { label: t('Sidecars: Search & Vision', '사이드카: 검색 & 비전', '边车:搜索与视觉', 'Сайдкары: поиск и зрение', 'サイドカー: 検索 & ビジョン', '邊車:搜尋與視覺'), href: `${prefix}guides/sidecars/` }, + { label: t('Image Bridge', '이미지 브릿지', '图像桥接', 'Image Bridge', '画像ブリッジ', '圖像橋接'), href: `${prefix}guides/image-bridge/` }, + { label: t('Video Bridge', '비디오 브릿지', '视频桥接', 'Video Bridge', '動画ブリッジ', '影片橋接'), href: `${prefix}guides/video-bridge/` }, + { label: t('Web Dashboard', '웹 대시보드', '网页控制台', 'Веб-дашборд', 'ウェブダッシュボード', '網頁儀表板'), href: `${prefix}guides/web-dashboard/` }, + { label: t('Sub-agent Surface', '서브에이전트 서피스', '子代理界面', 'Интерфейс подагентов', 'サブエージェントサーフェス', '子代理介面'), href: `${prefix}guides/sub-agent-surface/` }, ], }, { - label: t('Reference', '레퍼런스', '参考', 'Справочник', 'リファレンス'), + label: t('Reference', '레퍼런스', '参考', 'Справочник', 'リファレンス', '參考'), links: [ { label: 'CLI', href: `${prefix}reference/cli/` }, - { label: t('Configuration', '설정', '配置', 'Конфигурация', '設定'), href: `${prefix}reference/configuration/` }, - { label: t('Adapters', '어댑터', '适配器', 'Адаптеры', 'アダプター'), href: `${prefix}reference/adapters/` }, - { label: t('Architecture', '아키텍처', '架构', 'Архитектура', 'アーキテクチャ'), href: `${prefix}reference/architecture/` }, - { label: t('Proxy API Formats', '프록시 API 형식', '代理 API 格式', 'Форматы API прокси', 'プロキシAPI形式'), href: `${prefix}reference/proxy-formats/` }, - { label: t('Management API', '관리 API', '管理 API', 'API управления', '管理API'), href: `${prefix}reference/management-api/` }, + { label: t('Configuration', '설정', '配置', 'Конфигурация', '設定', '設定'), href: `${prefix}reference/configuration/` }, + { label: t('Adapters', '어댑터', '适配器', 'Адаптеры', 'アダプター', '適配器'), href: `${prefix}reference/adapters/` }, + { label: t('Architecture', '아키텍처', '架构', 'Архитектура', 'アーキテクチャ', '架構'), href: `${prefix}reference/architecture/` }, + { label: t('Proxy API Formats', '프록시 API 형식', '代理 API 格式', 'Форматы API прокси', 'プロキシAPI形式', '代理 API 格式'), href: `${prefix}reference/proxy-formats/` }, + { label: t('Management API', '관리 API', '管理 API', 'API управления', '管理API', '管理 API'), href: `${prefix}reference/management-api/` }, ], }, ]; const contributing: NavLink = { - label: t('Contributing', '기여하기', '贡献', 'Участие в проекте', 'コントリビュート'), + label: t('Contributing', '기여하기', '贡献', 'Участие в проекте', 'コントリビュート', '貢獻'), href: `${prefix}contributing/`, }; --- @@ -104,7 +106,7 @@ const contributing: NavLink = {
-