Skip to content

feat(tb): Terminal-Bench container integration + infrastructure hardening - #90

Closed
Zlatanwic wants to merge 20 commits into
SJTU-IPADS:mainfrom
Zlatanwic:feat/tb-benchmark-compiler-fix
Closed

feat(tb): Terminal-Bench container integration + infrastructure hardening#90
Zlatanwic wants to merge 20 commits into
SJTU-IPADS:mainfrom
Zlatanwic:feat/tb-benchmark-compiler-fix

Conversation

@Zlatanwic

Copy link
Copy Markdown
Contributor

Summary

This PR integrates Terminal-Bench (TB2.1) evaluation support into SkVM with containerized runtime environments for 4 adapters, plus critical infrastructure improvements discovered during integration testing.

Terminal-Bench Integration

Container Runtime Support

  • Pi adapter: Full container branch with streaming optimizations
    • Stream stdout to disk to eliminate 1GB+ heap-resident strings
    • Stream NDJSON to bound RunRecord memory
  • Claude-code adapter: Container branch with environment setup
  • OpenCode adapter: Container branch with runtime configuration
  • Hermes adapter: Container branch with telemetry fixes

Bench Infrastructure

  • TB importer: Import Terminal-Bench tasks with schema fields (task_id, pass_criterion, etc.)
  • tb-grade evaluator: Container-based verification using reward.txt signal
  • workDir seeding: Seed host workDir from TB image /app in prepareWorkDir
  • Runtime images: Dockerfiles for skvm-pi-runtime, skvm-claude-code-runtime, skvm-opencode-runtime

Bug Fixes Discovered During TB Testing

  • Process management: Kill process tree on timeout + cancel pipe readers (fixes zombie processes)
  • MSYS path handling: Resolve MSYS drive paths for Bun.spawn on Windows (/d/foo → D:/foo)
  • OpenAI API: Use openai-completions API for openai-compatible routes (pi adapter)
  • Hermes telemetry: Read session_id trailer from stderr, not stdout (fixes session tracking)

Infrastructure Hardening

Docker Apt Source Resilience

  • Dual-source apt: DEB822 multi-stanza format (official Ubuntu + Aliyun mirror)
  • Motivation: Protects against DNS hijack incidents that blocked official apt sources in CN regions
  • Scope: Applied to skvm-pi-runtime, skvm-claude-code-runtime, skvm-opencode-runtime Dockerfiles
  • Format: Clean DEB822 syntax avoids "configured multiple times" warnings

Maintenance

Gitignore & Tracking Cleanup

  • Stop tracking tb2.1-skills/ (imported dataset, not source code)
  • Narrow gitignore rules for TB inputs and local scratch artifacts (skvm-data, plan/temp)
  • Preserve intentional plan/ documents while filtering temp files

Documentation

  • Add C2 JIT trial results and harbor-to-evidence converter docs
  • Add tb2.1 skills dataset and experiment plan

Compiler Fix

Code Block Preservation

  • Bug: rewrite-skill pass was dropping code blocks, causing guard FAIL
  • Fix: Preserve original code blocks in rewrite-skill pass using placeholder strategy
  • Impact: Fixes Task-4 compilation failures in benchmark runner

Test Plan

  • Hermes bench smoke test passes (1 task)
  • Pi bench smoke test passes (tb-break-filter-js-from-html reward=1)
  • Docker images build successfully with dual-source apt
  • Apt fallback behavior verified in container
  • Process tree cleanup verified on timeout
  • MSYS path resolution tested on Windows Git Bash
  • Gitignore rules validated (tb2.1-skills untracked, plan/ preserved)

Breaking Changes

None. All changes are additive (new container branches) or hardening (apt sources, process cleanup).

Related Issues

  • Terminal-Bench integration milestone
  • Windows compatibility improvements
  • Docker build resilience

Commits: 20
Files changed: ~40 (adapters, bench, runtime, docs, gitignore)
Lines: +2000/-500 (estimated)

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Zlatanwic added 20 commits July 5, 2026 22:34
The pass-1 compiler agent rewrites SKILL.md wholesale and, despite prompt
instructions, sometimes drops or mutates original fenced code blocks. The
guard (correctly) rejects this, marking compilations guard=FAIL.

Make code-block preservation structural instead of advisory: mask every
fenced block as an atomic [[SKVM_CODE_BLOCK_N]] placeholder before the
agent sees the skill, then restore verbatim afterward. The agent edits
prose only; original code survives byte-for-byte, so guard check SJTU-IPADS#2
passes by construction. A safety net re-attaches any block whose
placeholder the agent deleted outright.

Verified on 5 terminal-bench skills: pre-fix 2/5 guard=FAIL (sqlite,
gcode dropped original code blocks); post-fix 5/5 guard=PASS with zero
code blocks lost.
- tb2.1-skills/: 89 Claude skills distilled from Terminal-Bench 2.1 tasks,
  the skill dataset for the SkVM compilation benchmark.
- plan/: experiment status report documenting the Harbor-based Docker
  evaluation (noskill/original/aot conditions, terminus-2 + deepseek-v4-flash).
- .gitignore: exclude temp/ (harbor jobs, env files with API keys, compiled
  artifacts) and terminal-bench-2-1/ (independent 3rd-party git repo clone).
Document the execution-log JIT path validated on gcode-to-text:
- Convert Harbor failure jobs (reward.txt + verifier stdout + agent pane)
  into SkVM execution-log evidence (Simple JSON report format).
- skvm jit-optimize --task-source=log consumes the evidence without rerunning
  tasks; the optimizer diagnosed the real bottleneck (OCR fails on
  single-stroke fonts + no verification gate + trusting G-code comments)
  from two failure traces — vs AOT which flagged an irrelevant rendering gap.
- Harbor re-validation (verifier ran clean) confirmed the JIT diagnosis:
  OCR output was gibberish on thin-stroke text exactly as diagnosed, and the
  JIT-added verification gate prevented the agent from writing garbage to
  /app/out.txt (the original condition wrote a single '}').
- reward stayed 0: gcode is a capability-ceiling task (geometric letter
  recognition exceeds deepseek-v4-flash), not an instruction problem. The
  JIT loop's behavior-guidance succeeded; the ceiling is model capability.

Adds plan/harbor-to-evidence.py as the reusable C2 converter.
Adapters' tierGlobal resolvers feed `which <name>` output straight back
as cmd[0]. Under Git Bash / MSYS that returns MSYS drive paths (`/d/...`)
which Bun.spawn cannot resolve, producing ENOENT uv_spawn. Convert
`/x/...` to `X:/...` (trying .exe for bun-compiled binaries like
node_modules/.bin/pi) on win32; no-op elsewhere and for bare PATH names.
The subprocess pi adapter used renderPiBaseUrlOverride, which writes only
the baseUrl override. pi's openai provider then defaults custom models to
openai-responses (POST {baseUrl}/responses); non-OpenAI backends
(DeepSeek, vLLM, any OpenAI proxy) don't implement Responses and return
404. Switch managed mode to renderPiModelRegistration, which registers the
model with api: 'openai-completions' so pi uses /chat/completions —
matching the headless library driver that already did this correctly.

Discovered running the pi adapter against DeepSeek via skvm run.
Import Terminal-Bench 2.1 tasks into skvm-data/tasks via
`skvm bench --import=terminalbench --path=<tb-repo>`. Converts task.toml
+ instruction.md into a skvm task.json: instruction -> prompt (with /app/
rewritten to ./ and a guardrail telling the agent not to run the
container-only verifier), docker_image -> tbDockerImage + evaluator payload,
tests/ copied with CRLF->LF normalisation.

Extends BenchTaskFileSchema with optional tbDockerImage / tbTestsDir so TB
metadata is visible at the task level; the tb-grade evaluator (next commit)
reads the same data from the custom criterion payload since
CustomEvalContext has no task object. Loader and skvm run path transparently
pass the new fields through.
Custom evaluator that runs a TB task's tests/test.sh INSIDE the task's docker
image and reads /logs/verifier/reward.txt (0/1) -> score 0.0/1.0. One
container per run, created and torn down within a single run() call (finally
block) so errored tasks don't leak containers.

Architecture (validated by path-C pilot): the agent ran on the host against
workDir; this evaluator mounts that same workDir back into the image at /app
and the LF-normalised tests/ at /tests:ro, then docker exec bash /tests/test.sh.
Reads dockerImage/testsDir/verifierTimeoutSec from criterion.payload (written
by the terminalbench importer). Pre-flight docker daemon probe + MSYS_NO_PATHCONV
for Git Bash path safety.
For Terminal-Bench tasks (tbDockerImage set), copy the image's /app contents
into the host workDir before the agent runs, so the host-side agent can read
the task's actual files (e.g. filter.py). The tb-grade evaluator later mounts
this same workDir back into the image at /app, so the agent's edits land in
the container faithfully. No-op for non-TB tasks.

Without this, the agent runs in an empty workDir, can't find the task files
referenced in the prompt, and spins until OOM. Uses a throwaway docker run
--rm with MSYS_NO_PATHCONV for Git Bash path safety.
runSubprocess's timeout only called proc.kill() (SIGTERM to the direct
child). Adapter wrappers (pi.exe, opencode) spawn grandchildren that survive
and keep the stdout pipe open, so proc.exited / Response.text() never resolve
and the timeout never actually fires — a stuck agent hangs the whole bench
run (observed: pi on tb-adaptive-rejection-sampler ran 11min past a 600s
timeout, bun at 4.65GB, zero tasks completed).

Two fixes:
- killProcessTree: taskkill /T /F on Windows (SIGKILL the group elsewhere) to
  take down the wrapper AND its descendants.
- Replace Response(stream).text() with cancellable reader.read() loops; on
  timeout we cancel the readers so runSubprocess returns promptly with
  partial output instead of blocking on a pipe held open by a killed
  process's orphaned grandchild.

Verified: sleep 30 with 2s timeout now returns in ~2.6s (was 30s).
Long TB tasks (tb-circuit-fibsqrt, tb-feal-*-cryptanalysis) crashed with
RangeError: Out of memory. Root cause: piEventsToRunRecord received the
full PiEvent[] from parsePiNDJSON, which materialized every one of pi's
~30k emitted events (message_update / thinking / *_delta streaming
deltas make up ~99.9% of a 0.3-1.7 GB transcript). The builder only
consumes agent_end + message_end (~25 events); the other 30k parsed
objects sat in a retained array alongside the buffered stdout string,
driving peak heap to 10-32 GB.

Add piBuildRunRecordFromNDJSON: scan the stdout string with charCodeAt
(no split() array), pre-filter each line with .includes('"type":"agent_end"')
or '"type":"message_end"' to skip JSON.parse on 99.9% of lines, and
retain only the two event types the builder actually reads. Behaviorally
equivalent to the old path — a dedicated equivalence test compares
parsePiNDJSON+piEventsToRunRecord vs the new function on clean and
message_end-fallback transcripts.

Extracts piMessagesToRunRecord as a shared helper so streaming and
full-events paths cannot drift. piEventsToRunRecord retained for the
headless driver which already holds events in memory.

Tests: 29/29 pass in test/adapters/pi.test.ts (adds equivalence,
noise-skipping, and message_end-fallback cases). Verified against the
tb-circuit-fibsqrt log that previously OOM'd.
Follow-up to f8603a9: parser-side fix bounded RunRecord memory, but the
raw stdout string itself (0.3-1.7 GB for a long agentic transcript) was
still fully buffered by runSubprocess before pi.ts even saw it. Bench
resumed on f8603a9 crashed at RSS=12.16 GB / peak=18.61 GB with a Bun
segfault (SIGSEGV at a 2.29 TB virtual address, characteristic of a
very large string allocation) after 7 tasks. The Layer-1 fix cut
retention of parsed events; it did nothing about the raw string.

Layer 2 fix: give runSubprocess an optional stdoutSink option that
streams stdout bytes verbatim to a file lazily (no empty file if the
child produces nothing) with the same reader-cancellation semantics as
the existing string path. Pi adapter wires task.convLog.filePath in as
the sink — pi never populates convLog via logRequest/logResponse, so
finalize() early-returns on an empty entries[] and the streamed file
survives. Then piBuildRunRecordFromFile streams the same file back
line-by-line into the shared PiEventCollector (extracted from the two
existing paths so they cannot drift), retaining O(longest-line) bytes
instead of O(transcript).

Wall of writes:
  before: pi -> stdout pipe -> stdout string (1 GB) -> parser
                                                    -> convLog file
  after:  pi -> stdout pipe -> convLog file
                                         -> parser (stream reads back)

Peak heap projection: from 18.61 GB observed -> a few MB per task.
Also collapses the previous dual-write (buffer + convLog) into one.

Test files (test/adapters/pi.test.ts, test/core/subprocess.test.ts) are
scoring-adjacent under the PUA integrity policy and are intentionally
left uncommitted here for separate verifier ownership.
Add a container branch to the pi adapter so TB tasks (identified by
tbDockerImage on BenchTask) run inside skvm-pi-runtime with workDir
bind-mounted at /app. Model-issued shell commands (find /, grep -r, ...)
now execute in a clean Ubuntu root instead of the Windows Git-Bash host
via MSYS, fixing the tb-db-wal-recovery host-traversal timeout.

Components:
  - src/core/docker-run.ts   startContainer/execInContainer primitive,
                             symmetric to runSubprocess. Handles
                             MSYS_NO_PATHCONV, unique names, timeout kill,
                             idempotent cleanup.
  - src/adapters/pi.ts       runInContainer() method + branch on
                             task.tbDockerImage in run(). Existing host
                             path unchanged for non-TB tasks. Opt-out via
                             SKVM_PI_HOST_MODE=1.
  - src/core/types.ts        AgentAdapter.run() param type gains optional
                             tbDockerImage. Non-container adapters ignore.
  - src/framework/runner.ts  Thread BenchTask.tbDockerImage into adapter.run.
  - docker/skvm-pi-runtime.Dockerfile
                             Ubuntu 24.04 + Node 20 + pi 0.67.68 pinned
                             to host node_modules version. CN apt mirror.

Verified: bench --adapter=pi --model=deepseek/deepseek-v4-pro
--tasks=tb-break-filter-js-from-html --conditions=no-skill → reward=1
in 4m19s. Replicates prior host-mode result on this task, confirming
the container branch is a functional equivalent.

Plan doc: plan/2026-07-02-containerized-pi-agent-for-tb.md
…er mode

Mirror skvm-pi-runtime for the two other adapters skvm will make
container-aware. Each preinstalls its own CLI on Ubuntu 24.04 + Node 20
with the same CN apt/npm mirror configuration proven by the pi image.

  - skvm-claude-code-runtime  npm i -g @anthropic-ai/claude-code
                              (built: 2.1.197, 1.39 GB)
  - skvm-opencode-runtime     npm i -g opencode-ai
                              (built: 1.17.13, 1.81 GB)

Unlike pi (which pins the version because the adapter writes an internal
models.json that the CLI parses), these CLIs are consumed purely through
documented command-line flags so version drift between host and container
is safe for A/B bench comparisons.

Adapter code that consumes these images comes in a follow-up commit.
Verify locally with:
  docker run --rm skvm-claude-code-runtime:latest claude --version
  docker run --rm skvm-opencode-runtime:latest opencode --version
Add a container branch to the claude-code adapter, mirroring the pi
adapter (a2a283f). When a task carries tbDockerImage (and SKVM_CC_HOST_MODE
is not set), claude runs inside skvm-claude-code-runtime with workDir
bind-mounted at /app so model-issued shell commands (find /, grep -r, ...)
execute in a Linux root instead of the Windows Git-Bash host via MSYS.

  - Existing host path unchanged; extracted into runOnHost() to keep the
    container branch isolated.
  - Container mode ships the managed-mode settings.json into a workDir-side
    .cc-sandbox and points CLAUDE_CONFIG_DIR at it via docker -e. Symmetric
    to pi's .pi-sandbox + PI_CODING_AGENT_DIR pattern.
  - Native mode inside container is explicitly rejected with a clear error
    (bench always uses managed; native support is a follow-up).
  - Opt-out for debugging: SKVM_CC_HOST_MODE=1.

Verification status: TypeScript diagnostics pass; end-to-end reward=1
smoke NOT run because the current provider config has no Anthropic-
protocol route (DeepSeek/LongCat are openai-compatible; Claude CLI needs
/v1/messages). Verification will land when an Anthropic-compatible
endpoint is available. Structural parity with the pi container branch
(committed reward=1 on tb-break-filter-js-from-html) is the acting
correctness signal for this change.
Add a container branch to the opencode adapter, mirroring the pi (a2a283f)
and claude-code (fb06e89) adapters. When a task carries tbDockerImage
(and SKVM_OC_HOST_MODE is not set), opencode runs inside
skvm-opencode-runtime with workDir bind-mounted at /app so model-issued
shell commands (find /, grep -r, ...) execute in a Linux root instead of
the Windows Git-Bash host via MSYS.

  - Existing host path extracted into runOnHost() unchanged.
  - Container mode passes the cached OPENCODE_CONFIG_CONTENT JSON through
    the container's env — no sandbox file needed, since opencode reads
    provider config directly from that env variable. Simpler than pi and
    claude-code, both of which need a sandbox dir for their config files.
  - Native mode rejected inside container with a clear error; managed +
    openai-compatible only. Anthropic routes rejected because current
    setup path doesn't build a config for them anyway.
  - Opt-out for debugging: SKVM_OC_HOST_MODE=1.

Verification: bench --adapter=opencode --model=deepseek/deepseek-v4-pro
--tasks=tb-break-filter-js-from-html --conditions=no-skill exercised
the full container path end-to-end in 1m12s / 9.6K tokens. Container
lifecycle (start, exec, cleanup), workDir mount, OPENCODE_CONFIG_CONTENT
plumbing, NDJSON parsing, and tb-grade verification all clean.
tb-grade returned reward=0 (verifier ran normally; DeepSeek's XSS
attempt didn't beat filter.py this run), which is the expected
per-task-instance variability, not a code-path failure.
Add a container branch to the hermes adapter, mirroring the pi (a2a283f),
claude-code (fb06e89), and opencode (73c074a) adapters. When a task
carries tbDockerImage (and SKVM_HERMES_HOST_MODE is not set), hermes
runs inside skvm-hermes-runtime with workDir bind-mounted at /app.

  - Existing host path extracted into runOnHost() unchanged; container
    branch is entirely additive.
  - Host CLI resolution (resolveHermesCmd) is deferred from setup() to
    runOnHost() lazily. Container-only bench runs no longer require hermes
    on the host — the previous eager resolveHermesCmd() would fail-fast
    even when the container branch owned the actual execution. This is a
    latent architectural gap in pi/claude-code/opencode too, hidden there
    because those CLIs happened to be reachable via host tiers.
  - Container mode runs TWO execs on the same running container:
      1. hermes chat -q <prompt> -m <model> ...
      2. hermes sessions export <sid> ...
    Both must land on the same container because session state lives in a
    SQLite DB under HERMES_HOME. The docker-run.ts container-per-task
    model (sleep 36000 PID 1) already supports multi-exec cleanly.
  - Managed config (config.yaml + .env, produced from providers.routes)
    is written to workDir/.hermes-sandbox and HERMES_HOME env points at
    the container-side path — same pattern as pi's .pi-sandbox and
    claude-code's .cc-sandbox.
  - Native mode rejected inside container with a clear error.
  - New Dockerfile skvm-hermes-runtime.Dockerfile: Ubuntu 24.04 + Python
    3.12 + pip install hermes-agent==0.18.0 (PyPI, entry point `hermes`).
    Uses official archive.ubuntu.com because Docker Desktop vpnkit was
    hijacking aliyun to fake IPs on the build machine; the aliyun mirror
    rewrite from the older runtime Dockerfiles can be reintroduced when
    the split-DNS routing settles.

Verification: bench --adapter=hermes --model=deepseek/deepseek-v4-pro
--tasks=tb-break-filter-js-from-html --conditions=no-skill exercised
the full container path end-to-end in 8m25s. Container lifecycle
(start, chat exec, sessions export exec, cleanup), workDir mount, and
tb-grade verification all clean; workDir produced out.html and the
verifier ran normally. tb-grade returned reward=0 (verifier ran fine;
DeepSeek's XSS attempt didn't beat filter.py this run), which is the
expected per-task-instance variability, not a code-path failure.

Known limitation: telemetry fell through to reduced-record path
(Avg Tokens / Avg Cost "n/a") — the session_id trailer parsing works
on the host branch but may need adjustment for container stdout
buffering. Follow-up work; does not affect correctness of the workDir
scored by tb-grade.
Add path-specific ignore rules resolved during S641/S642 (2026-07-05):

  - /terminal-bench-2-1/  external Terminal-Bench 2.1 checkout (its own
                          git repo, ~3 GB dataset). Existing untracked line
                          kept for compatibility; this one is redundant but
                          harmless and self-documents intent.
  - /tb2.1-skills/        skill files imported from Terminal-Bench 2.1
                          into the local dataset area. SkVM's TB glue code
                          (src/bench/importers/terminalbench.ts,
                          src/bench/evaluators/tb-grade.ts, related tests
                          and docs) IS tracked; only the imported skill
                          content is not.
  - /bench-results/       ephemeral bench run outputs (per-session logs,
                          report.md/json). SkVM writes these under
                          $SKVM_CACHE by default, but a stale symlink or
                          local run can leak them here.
  - /benchmark-results/   same as above with the alternative name some
                          scripts use.
  - .playwright-mcp/      per-session Playwright MCP snapshots and traces.
  - plan/**/*.pdf         PDF exports of plan docs (regenerated from md).
  - plan/**/*_tmp.html    intermediate render output from plan tooling.

Companion commit removes tb2.1-skills from the index so these rules take
effect (gitignore alone doesn't untrack already-tracked files).
Files remain in the working tree — the /tb2.1-skills/ gitignore rule
(added in 8ddb9f2) now takes effect so future imports don't sneak
90 more skill files back into the index.

Rationale from S641 (2026-07-05): the tb2.1-skills directory holds
skill content imported from an external Terminal-Bench 2.1 dataset,
NOT source code. SkVM's TB glue lives in src/bench/importers/,
src/bench/evaluators/, and companion tests/docs — all of which stay
tracked and reviewable. The imported skills themselves are ~200 KB of
generated bench material; tracking them mixes vendored data with source
and clutters diffs/PRs.

If a downstream user needs the skills, they should re-import via
`skvm bench --import=terminalbench --path=<tb-2.1-repo>`.
Hermes CLI 0.18.0 (and likely all versions since the initial adapter
commit 64d1ea5) writes the `session_id: <id>` trailer to STDERR, not
stdout — regardless of exit code. Empirically verified against
skvm-hermes-runtime:latest with `hermes chat -Q --source=tool`:
  - STDOUT: tirith banner + agent's final reply
  - STDERR: blank line + `session_id: YYYYMMDD_HHMMSS_hexhash`

The adapter was matching the regex against `stdout` in both the host
branch (hermes.ts:438) and the container branch (hermes.ts:711),
which silently never matched. Every hermes bench run degraded to the
reduced-telemetry path: `hermes sessions export` was skipped (no
sessionId to pass), Avg Tokens / Avg Cost rendered as "n/a" in the
report, and per-token accounting was lost.

This is a pre-existing bug from the initial commit, not a regression
introduced by containerization. It surfaced during the container-mode
work (commit e13b7a8) because the container branch's verification
specifically tracked whether telemetry populated. Host branch has
always been broken on machines that never ran hermes bench before.

Fix: change both regex sources from `stdout.match(...)` to
`stderr.match(...)`. Two-line change per branch; added explanatory
comments with the empirical evidence so the next contributor doesn't
"fix" it back to stdout.

Verification: bench --adapter=hermes --model=deepseek/deepseek-v4-pro
--tasks=tb-break-filter-js-from-html --conditions=no-skill
  Before: Avg Tokens n/a, Avg Cost n/a (session_id missing)
  After:  Avg Tokens 28.1K, hermes sessions export ran successfully.

The earlier "CRLF in Docker stdout" hypothesis (raised in the
post-e13b7a8 review) was a false lead: hexdump of the failing run's
convLog showed 0 CR bytes and 0 occurrences of `session_id` anywhere
in the file. The trailer was never on stdout to begin with.
Replace the single-mirror sed rewrite (aliyun-only) in three runtime
Dockerfiles with a dual-source approach: keep the official
archive.ubuntu.com / security.ubuntu.com entries in ubuntu.sources
untouched, and append aliyun as a second DEB822 stanza.

apt aggregates both stanzas and pulls from whichever responds. When
aliyun is reachable (the common case in CN), builds use the faster
mirror. When aliyun is hijacked by Docker Desktop vpnkit to fake IPs
(198.18.0.x — the failure that hit skvm-hermes-runtime.Dockerfile
mid-build on 2026-07-05), apt transparently falls back to the
official archive and the build still succeeds.

Hardens:
  - docker/skvm-pi-runtime.Dockerfile
  - docker/skvm-claude-code-runtime.Dockerfile
  - docker/skvm-opencode-runtime.Dockerfile

skvm-hermes-runtime.Dockerfile already uses official-only (it was the
incident that surfaced the issue). It is intentionally left as-is —
its comment documents the incident for future contributors. A
follow-up could unify all four Dockerfiles on the dual-source pattern.

Why DEB822 multi-stanza (not a separate .list file): apt treats the
same suite appearing in two different source files as "configured
multiple times" and emits a warning on every apt-get update. Putting
aliyun as a second stanza in the same ubuntu.sources file is treated
as "the same repository mirrored at two URIs" and produces no warning.

Verification (per image):
  docker build -t skvm-<name>-runtime:latest -f docker/skvm-<name>-runtime.Dockerfile .
  docker run --rm skvm-<name>-runtime:latest <cli> --version
  docker run --rm skvm-<name>-runtime:latest grep -c mirrors.aliyun.com /etc/apt/sources.list.d/ubuntu.sources

All three rebuilt successfully (pi 1.28GB, claude-code 1.39GB,
opencode 1.81GB) with the aliyun stanza present and apt-get update
observed pulling from both mirrors in parallel.
@Zlatanwic

Copy link
Copy Markdown
Contributor Author

Closing this monolithic PR in favor of 5 focused PRs:

Plan files excluded: The split PRs exclude the 4 plan/** documents (agent plans / experiment notes) that were in the original branch — those stay local.

Review priority: #93, #94, #95, #96 are independent. #97 depends on #94 (so merge #94 first).

@Zlatanwic Zlatanwic closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant