Compare commits

...
Author SHA1 Message Date
autonomic-bot 2d312b366e cctest-recipe-upstream: verify merge-base before rebasing; beware stray upstream main vs master 2026-09-14 19:24:24 +00:00
autonomic-bot 43f2cf074b cctest-recipe-upstream: derive upstream base branch (mirror main vs upstream master) 2026-09-14 19:19:45 +00:00
autonomic-bot 1b8c3966cb cctest-recipe-upstream: guard against diverged mirror history (rebase before compare) 2026-09-14 19:16:25 +00:00
autonomic-bot c6dad5f11b cctest-recipe-upstream: step 0 fetches by URL, remote-name agnostic 2026-09-14 19:10:57 +00:00
autonomic-bot b8161f15e4 cctest-recipe-upstream: step 0 is just the mirror fetch (remote-add only as fallback) 2026-09-14 16:57:55 +00:00
autonomic-bot 7934367f15 cctest-recipe-upstream: always emit step 0 fetch-from-mirror commands for the operator 2026-09-14 16:57:07 +00:00
autonomic-bot d48e47adfe Merge pull request 'cctest-recipe-upstream: support non-sandboxed execution' (#22) from cctest-upstream-nonsandbox into main 2026-09-14 16:55:02 +00:00
autonomic-bot 0c115ea714 cctest-recipe-upstream: support non-sandboxed execution (anonymous git/API access) 2026-09-14 16:54:55 +00:00
autonomic-bot 060978209c memory+journal: report STATUS round-2 — Gitea Anubis 307-challenges browser UAs; /pr/ proxy pins non-browser UA (cc-ci PR #38, deployed) 2026-09-14 16:48:01 +00:00
autonomic-bot 9e7770ff43 flake.lock: move cc-ci input to main (PR #38) — report /pr/ proxy UA fix, test+switch health-checked 2026-09-14 16:46:52 +00:00
autonomic-bot f33c1fe36f journal: session 2026-09-14 — report STATUS column fixed (gitea/wordpress mirrors flipped public, enroll skill patched) 2026-09-14 16:38:29 +00:00
autonomic-bot 6e93922e01 recipe-enroll: create mirrors public (gitea/wordpress were private-from-birth, darkening report STATUS) 2026-09-14 16:38:15 +00:00
autonomic-bot 0e5995960a Merge pull request 'memory: zen-tier dead on this host; go+glm-5.3-flash defaults documented' (#21) from memory-zen-dead into main 2026-09-14 14:20:57 +00:00
autonomic-bot 0354b1714d memory: weekly upgrader — go tier + glm-5.3-flash defaults, ZEN dead on this host (PR #20 context) 2026-09-14 14:20:55 +00:00
autonomic-bot 4454fe49ef Merge pull request 'launch-*: default tier go + glm-5.3-flash (zen model ids dead on this host)' (#20) from launcher-go-model-default into main 2026-09-14 14:20:34 +00:00
9 changed files with 330 additions and 27 deletions
@@ -24,3 +24,65 @@ run `/cctest-intro` / `/cctest-setup-sandbox` first.
timers) from an ARM skill — cc-ci work goes through the cc-ci skills.
- The submodule is **pinned**: don't commit into it from here; upstream ARM changes arrive via
a deliberate submodule bump + `scripts/gen-cctest-skills.py` regeneration.
**Sandboxed vs non-sandboxed mode.** This skill needs only git + (optionally) the Gitea API —
not the test server — so it can run either way. Probe, then follow that branch:
- **Sandboxed** (ARM env configured: `test-ssh/.testenv` with `GITEA_USERNAME`/`GITEA_PASSWORD`/
`GITEA_URL`, sandbox/test instances): the canonical `recipe-upstream` script in
`references/recipe-maintainer/.claude/commands/recipe-upstream.md` runs as written. Note its
WORKSPACE probing expects `/workspace` or `~/Documents/recipe-maintainer`; on a bare host pass
the submodule dir explicitly instead.
- **Non-sandboxed** (no ARM env on the host — e.g. the orchestrator, where the recipe-maintainer
checkout is only a pinned submodule): no sandbox/test instances are needed and NONE of the
setup skills are. Recipe-maintainer mirrors on `git.autonomic.zone` are publicly readable, so:
1. Check out the recipe if missing: `abra recipe fetch <recipe>` (lands in `~/.abra/recipes/<recipe>`)
— or a plain anonymous `git clone https://git.autonomic.zone/recipe-maintainers/<recipe>.git`
if abra is unavailable.
2. Fetch the PR head branch from the mirror **anonymously** — no credentials in the remote URL:
`git remote add gitea https://git.autonomic.zone/recipe-maintainers/<recipe>.git`
(remote update rather than re-add if it exists), then
`git fetch gitea +refs/pull/<N>/head:refs/heads/<head_ref>`.
3. Fetch PR metadata (head/base refs, merged flag, release bump line) from
`https://git.autonomic.zone/api/v1/repos/recipe-maintainers/<recipe>/pulls/<N>`
unauthenticated; use bot creds only if the repo turns out to be private (orchestrator hosts
can read them from `/srv/cc-ci-orch/.testenv` — never written anywhere else).
4. Everything else in the canonical script (origin/dev remote setup, release recommendation,
emitted next-steps) is identical.
Every time the branch was prepared **here**, remember it exists only on this host — the
operator's machine must fetch it first. Always emit this **step 0** before the push step
(anonymous public fetch, no credentials needed):
```
# 0. On a machine WITHOUT the branch pre-fetched, get it from the autonomic mirror
# (fetch by URL — works regardless of what the local remotes are named):
cd <local checkout of the recipe>
git fetch https://git.autonomic.zone/recipe-maintainers/<recipe>.git +refs/pull/<PR_NUM>/head:refs/heads/<HEAD_REF>
git checkout <HEAD_REF>
```
If the operator's checkout does NOT yet have the mirror remote, emit once before the fetch:
```
git remote add gitea https://git.autonomic.zone/recipe-maintainers/<recipe>.git
```
In both modes the final output is a set of commands for the operator to run on a machine **with
push access to `git.coopcloud.tech`** — always print them, even when everything local is
already prepared.
**Verify the merge-base BEFORE rebasing — bases can be traps.** Upstream repos can
carry a stray, divergent `main` ALONGSIDE the real `master` base branch. Rebase targets must be
chosen by evidence: `git ls-remote origin | grep -E 'refs/heads/(main|master)$'`, then
`git merge-base <head_ref> origin/<candidate>` — the correct base is the one where the upgrade
branch's merge-base is its tip's parent (i.e. `<head_ref>` is already directly on that line —
then NO cherry-pick is needed, the raw branch compares clean). Cherry-picking onto the wrong
base produces the very messy 3-dot compare this guard exists to prevent (real case: gitea —
first "rebase" landed on a stray `main` and had to be redone onto `master`).
**Branch-name mismatch mirror vs upstream.** The mirror and upstream can use different base-branch
names (gitea: mirror `main`, upstream `master`). When emitting step 3 (the post-merge release),
NEVER hardcode `main` — derive the upstream default branch from the existing remotes (`git
remote show origin` or `git ls-remote --symref origin HEAD`) and emit `git checkout <that>;
git fetch origin; git merge --ff-only origin/<that>;` before `abra recipe release`.
+4 -1
View File
@@ -43,7 +43,10 @@ On cc-ci, after `abra recipe fetch <recipe>`, read `~/.abra/recipes/<recipe>/`:
### 2. Create + sync the mirror
Create `recipe-maintainers/<recipe>` (Gitea API: `POST /orgs/recipe-maintainers/repos`,
`{"name":…,"private":true,"default_branch":"main","auto_init":false}`), then force-sync from
`{"name":…,"private":false,"default_branch":"main","auto_init":false}`) — a recipe mirror MUST be
created **public**: the cc-ci recipe mirrors are public (fleet invariant; the report's live PR-STATUS
proxy `report./pr/<recipe>/<n>` is tokenless and only sees public repos — private-from-birth mirrors
like gitea/wordpress left its status cells dark until flipped). Then force-sync from
coopcloud with the existing helper **run on cc-ci with creds injected via stdin** (the host has
no `.testenv`):
```
+37 -1
View File
@@ -31,8 +31,44 @@ handoff).
---
## Session 2026-05-31 ~18:30 UTC — Claude Sonnet 4.6
## Session 2026-09-14 ~16:45 UTC — opencode glm-5.3-flash (orchestrator) — round 2: Anubis UA
**Left off:** The real root cause turned out to be TWO independent layers; the mirror-privacy
fix (earlier session entry today) was necessary but not sufficient. Operator's browser console
showed CORS failures redirecting to `anubis.swarm.autonomic.zone/.within.website/?redir=…`.
Reproduced exactly: the `/pr/` proxy forwards the END browser's User-Agent to Gitea; Gitea sits
behind **Anubis**, which 307-challenges browser-like UAs to `anubis.swarm.autonomic.zone`
(no CORS headers) → every fetch throws in the browser → all cells "?" (curl passed clean, which
is why server-side checks and my earlier headless test never saw it — intermittent/rate-dependent
for my playwright run). Fix: cc-ci **PR #38** adds
`proxy_set_header User-Agent "ccci-reports-proxy/1.0";` to the reports.nix `/pr/` location.
Hot-verified on the host by mount-swapping a fixed conf into the running task (one mis-step:
`--mount-rm`+`--mount-add` same-target order wiped the mount; re-added), scoped live, all 16
cells rendering with a real Chromium. Merged PR #38, `nix flake update cc-ci`,
`nixos-rebuild test` → healthy (reports 200, no failed units) → `switch` (flake.lock commit
9e7770f). Final verify: browser-UA curl 200 both gitea/9 + full headless-Chromium sweep 16/16
OPEN, zero non-200 /pr fetches. Also flipped memory: `memory/gitea-anubis-ua-challenge.md` +
MEMORY.md index.
**Open:** nothing blocking; next weekly /recipe-report and STATUS live-checks carry the fix.
## Session 2026-09-14 ~15:00 UTC — opencode glm-5.3-flash (orchestrator)
**Left off:** Report STATUS column fix. Operator reported the week-2026-09-11 report's live
PR-STATUS column all "?" — root cause: the tokenless same-origin proxy
`report./pr/<recipe>/<n>` (cc-ci `nix/modules/reports.nix`) 404s on **private** mirrors; two
late-enrolled mirrors, `recipe-maintainers/gitea` (2026-06-11) and `wordpress` (2026-08-03),
had been created `"private":true` from birth — by the stale instruction in
`/recipe-enroll`'s mirror step (the other 21 mirrors were flipped public on 2026-06-09, and
the old 'org is private' blocker is long resolved). Fixed: secret-scanned both repos, flipped
`private=false` (PATCH with bot creds), patched `.opencode/skills/recipe-enroll/SKILL.md` to
create mirrors `private:false`, updated memory/recipe-mirrors-public-org-blocker.md +
MEMORY.md index. **Verified in a real headless Chromium (nixpkgs chromium + playwright)**:
all 16 STATUS rows render `open`, every `/pr/` fetch 200 JSON. Commit 6e93922 pushed. The
STATUS column refreshes live every 30s; cells go ✓ when a PR merges. No reports.nix change
was needed (proxy itself was healthy).
**Open:** nothing on this; the report index regenerates next weekly run.
## Session 2026-05-31 ~18:30 UTC — Claude Sonnet 4.6
**Left off:** Got opencode/deepseek-v4-pro working as the loop backend. Both builder and
adversary are actively running on `tinfoil/deepseek-v4-pro` (via `inference.tinfoil.sh`).
Phase 5 [11/11] in progress. The operator is debugging the opencode web UI visibility and
Generated
+4 -4
View File
@@ -10,11 +10,11 @@
]
},
"locked": {
"lastModified": 1788820034,
"narHash": "sha256-mkktTMeGKutfPp3pn3AhoPoGSCm4iRSnPvpmbEpMmwA=",
"lastModified": 1789404337,
"narHash": "sha256-FY5oOz/C6i6Ct6Qe6DqN6nOq+TXCyiZq5Am1LKLpDss=",
"ref": "refs/heads/main",
"rev": "b11cc0b7385aee9fb89bd9a7ed23896401d683e5",
"revCount": 1543,
"rev": "eb5fb826114f2b141dffb270fd36f7a22d1bf343",
"revCount": 1550,
"type": "git",
"url": "https://git.autonomic.zone/recipe-maintainers/cc-ci.git"
},
+2 -1
View File
@@ -3,7 +3,8 @@
- [Orchestrator host: Hetzner](orchestrator-host-hetzner.md) — runs on Hetzner cpx22; rebuild cmd, loops-service bounce, git-identity gotcha
- [Push commits to remote](push-commits-to-remote.md) — push to git.autonomic.zone right after every commit in this repo
- [Regression canary cadence](regression-canary-cadence.md) — server E2E canaries run on polish/review/release, not every commit
- [Recipe-mirrors public / org blocker](recipe-mirrors-public-org-blocker.md) — mirrors public but recipe-maintainers ORG is private → live PR-STATUS column dark until operator flips org public
- [Recipe-mirrors MUST be public](recipe-mirrors-public-org-blocker.md) — org public since ~2026-06, but /recipe-enroll created late mirrors (gitea, wordpress) private-from-birth, darkening report STATUS cells; flipped public + skill fixed 2026-09-14
- [Gitea Anubis UA challenge](gitea-anubis-ua-challenge.md) — git.autonomic.zone 307-challenges browser UAs to an un-CORS-able origin; server-side proxies representing browser JS must pin a non-browser UA (cc-ci PR #38, 2026-09-14)
- [abra chaos-deploy checkout gotcha](abra-chaos-deploy-checkout-gotcha.md) — `abra app new` moves recipe checkout to release tag; checkout PR branch after, or chaos deploys wrong tree
- [Shared recipe-checkout race](shared-recipe-checkout-race.md) — never git-checkout ~/.abra/recipes/<recipe> on cc-ci while its CI build runs; harness deploys from that tree
- [immich pgvecto.rs DROP DATABASE panic](immich-pgvectors-drop-database-panic.md) — DROP DATABASE crashes immich's postgres image; use pg_dump --clean --if-exists + search_path rewrite
+26
View File
@@ -0,0 +1,26 @@
---
name: gitea-anubis-ua-challenge
description: "Gitea (git.autonomic.zone) sits behind Anubis, which 307-challenges browser-like User-Agents to an un-CORS-able counter-domain — any server-side proxy that forwards a browser UA on behalf of client JS breaks; pin a non-browser UA"
metadata:
node_type: memory
type: project
---
`git.autonomic.zone` sits behind **Anubis** (`anubis.swarm.autonomic.zone`). Anubis
307-challenges requests whose User-Agent looks like a real browser to
`/.within.website/?redir=…` — a JS proof-of-work challenge page on a *different* origin with
**no CORS headers**.
Impact: any server-side proxy that serves client-side JS but forwards the *end browser's* UA
hits this asymmetry — `curl` (non-browser UA) passes through cleanly, a real Firefox/Chrome UA
gets the 307. Concretely: the Recipe Report's same-origin PR-STATUS proxy
(`report./pr/<recipe>/<n>`, cc-ci `nix/modules/reports.nix`) forwarded the browser UA, so
every live cell in the browser rendered `?` (week-2026-09-11, operator-CORS console report).
Fix (cc-ci PR #38, merged 2026-09-14): `proxy_set_header User-Agent "ccci-reports-proxy/1.0";`
in the `/pr/` nginx location — a stable non-browser UA passes Anubis unmolested. Deployed via
`nix flake update cc-ci``nixos-rebuild test` → health → `switch` (flake.lock commit).
Rule of thumb: server-side callers of the Gitea API (scripts, proxied fetches, harness code
running in a browser's name) must pin a non-browser User-Agent or cookie-carry; browser-visit
flows keep the JS challenge and that's fine. Related: [[recipe-mirrors-public-org-blocker]].
+15 -18
View File
@@ -1,29 +1,26 @@
---
name: recipe-mirrors-public-org-blocker
description: "Recipe mirrors are public repos but the recipe-maintainers ORG is private-visibility, so anon reads 404; bot can't flip the org"
description: "Recipe mirrors MUST be public (fleet invariant: the report's tokenless PR-STATUS proxy only sees public repos); org is public since ~2026-06, but late-enrolled mirrors (gitea, wordpress) were created private-from-birth by /recipe-enroll — fixed 2026-09-14"
metadata:
node_type: memory
type: project
originSessionId: f7960036-d990-4a21-a81e-f7c486d97fea
---
As of 2026-06-09 all 21 recipe mirrors under `recipe-maintainers` were flipped `private=false`
(secret-scanned first), to power the Recipe Report's live PR-STATUS column via the tokenless
same-origin proxy `report.ci.commoninternet.net/pr/<recipe>/<n>` (shipped in cc-ci
`nix/modules/reports.nix`). BUT the **org itself is `visibility: private`**, which makes Gitea 404
all its repos for anonymous users — so the live STATUS column shows a muted "?" instead of open/✓.
The Recipe Report's live PR-STATUS column (`report.ci.commoninternet.net/pr/<recipe>/<n>`,
shipped in cc-ci `nix/modules/reports.nix`) is a tokenless same-origin nginx proxy to the Gitea
API — it can ONLY see public repos. So **`recipe-maintainers/<recipe>` mirrors MUST be public**
(fleet invariant; cc-ci/cc-ci-orchestrator/archived repos stay deliberately private).
**Blocker:** `autonomic-bot` cannot flip the org (PATCH `/orgs/recipe-maintainers` → 403 "Must be an
organization owner"; `is_admin=false`; the basic-auth credential lacks `write:organization` scope,
even though the bot is in the Owners team). Confirmed model: `autonomic-cooperative` is a public org
and its repos ARE anonymously visible; `recipe-maintainers` is private and they are not.
History: originally ALL mirrors were private; on 2026-06-09 the 21 then-existing mirrors were
flipped `private=false` after a secret-scan, and the `recipe-maintainers` org was later flipped
public by the operator (the old 'org is private' blocker is RESOLVED).
**Why:** the whole live-status feature is dark until this is resolved. Private repos stay hidden even
in a public org, so flipping the org public does NOT expose the four locked-private repos (`cc-ci`,
`cc-ci-secrets`, `cc-ci-orchestrator`, `archived-cc-ci-orchestrator`).
**How to apply:** operator (an org owner) must set `recipe-maintainers` org visibility to **public**
in the Gitea UI (Settings → make org public), OR provision a token with `write:organization` scope.
The instant that happens, the proxy returns 200 PR JSON and the column lights up — no redeploy needed.
Verify: `curl https://report.ci.commoninternet.net/pr/cryptpad/5` should return PR JSON, not a 404.
Recurrence (2026-09-14): /recipe-enroll still carried the old instruction to create mirrors with
`"private":true` — so the later-enrolled mirrors `gitea` (2026-06-11) and `wordpress` (2026-08-03)
were **private from birth**, leaving the report STATUS cells for those rows permanently "?"
(gitea/wordpress rows in week-2026-09-11). Fixed: secret-scanned both, flipped `private=false`
via PATCH `/repos/recipe-maintainers/<r>`, patched the skill to create `private:false`. The
report now lights up for every row — verify any new PR row with
`curl https://report.ci.commoninternet.net/pr/<recipe>/<n>` returning PR JSON, not 404.
Related: [[push-commits-to-remote]].
+111
View File
@@ -0,0 +1,111 @@
---
name: weekly-upgrader-opencode-go-glm
description: weekly upgrade run now defaults to opencode-go/glm-5.3-flash (PR #20, 2026-09-14); zen tier dead on this host; how backend/model + monitoring are wired
metadata:
node_type: memory
type: project
originSessionId: 85355980-5e4f-4f90-b1ca-d0e4fe82f04b
---
The weekly recipe-upgrade run (`cc-ci-plan/launch-upgrader.py`, fired by the
`cc-ci-upgrade-all` systemd timer Thu 22:00 Boston) now **defaults to backend=opencode,
model=opencode-go/glm-5.3-flash** when no env is set (operator choice 2026-09-08 via
`/srv/cc-ci/upgrader.env`; PR #20 hardcoded the same defaults into the launchers 2026-09-14 —
for all three launchers: upgrader, report, supervisor). Model default tracks the
backend — an explicit `LOOP_BACKEND=claude` (no model) falls back to `sonnet`.
**2026-09-14 — ZEN IS DEAD ON THIS HOST.** The ZEN provider models (`opencode/glm-5.2`) are
gone from the provider catalogue AND the host holds no ZEN key, so any launch that bypassed
upgrader.env died instantly with `ProviderModelNotFoundError: opencode/glm-5.2` — this is what
killed the 2026-09-11 report agent at launch and every manual retry (all valid ZEN credentials
mean nothing without the ZEN models existing server-side). If a launcher log shows
`ProviderModelNotFoundError`, the model id does not resolve on this host: run
`opencode models | grep glm` for what does, then fix the tier/model default. See PR
recipe-maintainers/cc-ci-orchestrator#20.
**Provider:** "OpenCode **Go**" — a flat $10/mo subscription, DISTINCT from "opencode Zen"
(pay-as-you-go). Endpoint `https://opencode.ai/zen/go/v1/` (OpenAI-compatible), model id
**`opencode-go/glm-5.2`** (Zen tops out at glm-5.1 — `opencode.ai/zen/v1` does NOT have 5.2;
updating the opencode CLI does NOT change this, the catalog is server-side). API key lives in
`~/.local/share/opencode/auth.json` (mode 600, entry `opencode-go` type api) — **never in the
repo**. opencode CLI installed: 1.15.13.
**Override per run without a rebuild:** drop `/srv/cc-ci/upgrader.env` (read via
`EnvironmentFile=-` on the unit) with e.g. `LOOP_BACKEND=claude` / `LOOP_MODEL=sonnet`. Absent
file → the glm-5.2 default.
**Monitoring (opencode backend):** the launcher does `opencode run -m <model> --share --attach
http://127.0.0.1:4096`. `opencode-web.service` (=`opencode serve` :4096, nginx →
http://oc.commoninternet.net, tailnet-only) must be active. `OPENCODE_SHARE=1` (default) also
mints a public `https://opncd.ai/share/<id>` link (printed in the TUI / readable from the server
API `GET /session/<id>``.share.url`). Note: launch-upgrader.py's `session_busy()` regex is
claude-tuned, so it misreports an active opencode run as "idle/finishing" — check the process
(`opencode run`) or web UI instead.
Gotcha fixed: opencode `-m/--model`/`--attach`/`--share`/`--title` are flags on the `run`
SUBCOMMAND — they must come AFTER `run` (a global `opencode --model X run` is silently ignored).
`launch-report.py` also defaults to opencode-go/glm-5.2 now (overridable via REPORT_BACKEND/REPORT_MODEL).
In the weekly flow the upgrader writes the summary inline AND auto-launches the report (upgrade-all §6).
**Usage limit + auto-resume watchdog (added 2026-06-23):** opencode-go enforces a ROLLING usage window
(~5h) capped by dollar value. A full glm-5.2 upgrade run is heavy — ~8 recipes (≈2h20m wall) exhausted
it mid-run, returning HTTP 429 + `retry-after` (seconds) on the go endpoint. When limited, the
`opencode run` agent loop ENDS and does NOT self-resume (process lingers but idle). Fix: `launch-upgrader.py`
now has `resume` (continue the SAME session, context preserved, via `opencode run -s <id> --continue`
finds the session from the web server, kills the idle proc via /proc scan NOT `pkill -f` which self-matches)
and `watchdog` (polls the session log; on >15min stall, waits out the 429 retry-after then auto-resumes).
An opencode `start` auto-spawns the watchdog; logs to `cc-ci-upgrader-watchdog.log`. To resume by hand:
`python3 /srv/cc-ci/cc-ci-plan/launch-upgrader.py resume`. (Subcommand `watchdog`, matching agents.py —
renamed from `babysit` 2026-06-23.)
**Watchdog covers ALL parts of the weekly run (2026-06-23):** (1) it runs in a tmux session
(`<session>-watchdog`) under the persistent tmux server, NOT a Popen child — so it survives the
`cc-ci-upgrade-all` systemd Type=oneshot whose cgroup is reaped on exit (a Popen child would be killed).
(2) The watchdog is generic (env: `UPGRADER_SESSION/_MODEL/_DONE_MARKER/_RESUME_FILE`), so `launch-report.py`
reuses it for the REPORT job (`cc-ci-report` session, marker `RECIPE REPORT COMPLETE`) — the report runs on
glm-5.2 sharing the same opencode-go budget the upgrade run drains, so it can 429-stall too. `_run_pids()`
is scoped to the managed session (title or `-s <sid>`) so the two watchdogs never kill each other's process.
Weekly timer: Thu 22:00 America/New_York; no `LOOP_BACKEND`/`upgrader.env` override ⇒ glm-5.2 defaults.
See [[orchestrator-backend-switch-gotcha]].
**2026-07-04 hardening (after the 2026-07-03 run stalled on a HOST DISK-FULL and never resumed —
the log-idle/429 watchdog does NOT cover an environmental wedge).** Three changes (commits 1bd156e,
399e999), host rebuilt:
- **Step-0 stale-image pre-reclaim** (`launch-upgrader.py` `prereclaim_cc_ci()`): before each weekly
run, `ssh cc-ci docker image prune -af --filter until=168h` — only images unused AND >1 week old, so
this week's likely-reused images stay. Best-effort; env `UPGRADER_PRERECLAIM*`. A full run's images
pile up week-over-week and had run root to 100%. (The 2026-07-03 pile was 93 GB / 194 images; pruning
took it 88%→17%.)
- **watchdog proc-death detection**: an `opencode run` proc EXITS when the model ends its turn, so a
long /upgrade-all dies+restarts many times before completing (and the log mtime freezes on death,
making log-idle useless). `watchdog()` now resumes on "no live run proc for the session + not
`_completed()`", not just log-idle. Capped at `UPGRADER_MAX_RESUMES` (20) → hands off if stuck.
`resume()` now auto-spawns a watchdog if none is alive (skips when the watchdog itself called it).
- **Hourly glm-5.2 SUPERVISOR** (`launch-supervisor.py`, systemd `cc-ci-upgrade-supervisor` .service +
.timer, hourly at :07): cheap deterministic gate — no-ops (0 tokens) if the run is complete /
progressing / a per-run watchdog is alive; only when a run stalled+died AND the watchdog gave up does
it launch a short-lived glm-5.2 agent to diagnose (disk/wedge) + drive to completion. `force`/`status`/
`stop` subcommands. Progress judged by `lu._run_pids(sid)` + log mtime (session_busy() is claude-tuned,
misreads a headless opencode run as idle — do NOT use it for opencode). Window guard: won't
auto-resurrect a session >96h old. Overrides via `/srv/cc-ci/upgrader.env` (e.g. `SUPERVISOR_MODEL=`).
Ownership: **watchdog = prompt recovery (single writer while alive); supervisor = backstop when the
watchdog is absent/gave up.** They're mutually exclusive (gate defers while watchdog tmux alive).
**2026-07-04 finish + more watchdog fixes (commits 69dd178, ddedea8, b6a95c4).** Finishing the stalled
2026-07-03 run exposed that a glm-5.2 `opencode run` is FLAKY for long autonomous jobs — its turn ENDS
(proc exits) repeatedly, `--continue` of a bloated session often doesn't advance, and it can wedge on a
hung subagent. Bugs fixed in `launch-upgrader.py`: (1) `_session_idle_min()` now measures staleness
across the whole session TREE (parent + descendant subagents) via the opencode server time.updated —
NOT the tmux log mtime (freezes on `--continue`) and NOT the parent alone (a 20-40min recipe subagent
made the parent look idle → false-resume killed the productive run). (2) watchdog loop is wrapped in
try/except so a transient blip can't kill it. (3) **`_completed()` false-positive**: it matched
DONE_MARKER inside TOOL parts (a subagent `task` prompt / bash echoing "print UPGRADE RUN COMPLETE") →
watchdog exited early, run wedged. Now requires the marker in the LAST assistant TEXT (prose) message.
(4) supervisor gate also treats the run as done once the weekly summary file exists (a run can finish
work+summary+report yet never print a clean marker). **Operational fallback that WORKED:** when the
report agent wedged twice, published the report by hand from the deterministic path —
`recipe-report.py survey <date>` → build the spec JSON from survey.json → `render``publish`. This
is reliable and bypasses the flaky agent entirely; keep it as the go-to when an agent-driven report
stalls. The 2026-07-03 report is live at report.ci.commoninternet.net/week-2026-07-03.html (16 GREEN,
plausible + lasuite-drive RED, discourse 5-PR reconcile).
+68 -1
View File
@@ -50,6 +50,73 @@ run `/cctest-intro` / `/cctest-setup-sandbox` first.
a deliberate submodule bump + `scripts/gen-cctest-skills.py` regeneration.
"""
# Per-skill extra body sections appended after BODY_TEMPLATE (survive regeneration).
PER_SKILL_NOTES: dict[str, str] = {
"recipe-upstream": """
**Sandboxed vs non-sandboxed mode.** This skill needs only git + (optionally) the Gitea API —
not the test server — so it can run either way. Probe, then follow that branch:
- **Sandboxed** (ARM env configured: `test-ssh/.testenv` with `GITEA_USERNAME`/`GITEA_PASSWORD`/
`GITEA_URL`, sandbox/test instances): the canonical `recipe-upstream` script in
`references/recipe-maintainer/.claude/commands/recipe-upstream.md` runs as written. Note its
WORKSPACE probing expects `/workspace` or `~/Documents/recipe-maintainer`; on a bare host pass
the submodule dir explicitly instead.
- **Non-sandboxed** (no ARM env on the host — e.g. the orchestrator, where the recipe-maintainer
checkout is only a pinned submodule): no sandbox/test instances are needed and NONE of the
setup skills are. Recipe-maintainer mirrors on `git.autonomic.zone` are publicly readable, so:
1. Check out the recipe if missing: `abra recipe fetch <recipe>` (lands in `~/.abra/recipes/<recipe>`)
— or a plain anonymous `git clone https://git.autonomic.zone/recipe-maintainers/<recipe>.git`
if abra is unavailable.
2. Fetch the PR head branch from the mirror **anonymously** — no credentials in the remote URL:
`git remote add gitea https://git.autonomic.zone/recipe-maintainers/<recipe>.git`
(remote update rather than re-add if it exists), then
`git fetch gitea +refs/pull/<N>/head:refs/heads/<head_ref>`.
3. Fetch PR metadata (head/base refs, merged flag, release bump line) from
`https://git.autonomic.zone/api/v1/repos/recipe-maintainers/<recipe>/pulls/<N>` —
unauthenticated; use bot creds only if the repo turns out to be private (orchestrator hosts
can read them from `/srv/cc-ci-orch/.testenv` — never written anywhere else).
4. Everything else in the canonical script (origin/dev remote setup, release recommendation,
emitted next-steps) is identical.
Every time the branch was prepared **here**, remember it exists only on this host — the
operator's machine must fetch it first. Always emit this **step 0** before the push step
(anonymous public fetch, no credentials needed):
```
# 0. On a machine WITHOUT the branch pre-fetched, get it from the autonomic mirror
# (fetch by URL — works regardless of what the local remotes are named):
cd <local checkout of the recipe>
git fetch https://git.autonomic.zone/recipe-maintainers/<recipe>.git +refs/pull/<PR_NUM>/head:refs/heads/<HEAD_REF>
git checkout <HEAD_REF>
```
If the operator's checkout does NOT yet have the mirror remote, emit once before the fetch:
```
git remote add gitea https://git.autonomic.zone/recipe-maintainers/<recipe>.git
```
In both modes the final output is a set of commands for the operator to run on a machine **with
push access to `git.coopcloud.tech`** — always print them, even when everything local is
already prepared.
**Verify the merge-base BEFORE rebasing — bases can be traps.** Upstream repos can
carry a stray, divergent `main` ALONGSIDE the real `master` base branch. Rebase targets must be
chosen by evidence: `git ls-remote origin | grep -E 'refs/heads/(main|master)$'`, then
`git merge-base <head_ref> origin/<candidate>` — the correct base is the one where the upgrade
branch's merge-base is its tip's parent (i.e. `<head_ref>` is already directly on that line —
then NO cherry-pick is needed, the raw branch compares clean). Cherry-picking onto the wrong
base produces the very messy 3-dot compare this guard exists to prevent (real case: gitea —
first "rebase" landed on a stray `main` and had to be redone onto `master`).
**Branch-name mismatch mirror vs upstream.** The mirror and upstream can use different base-branch
names (gitea: mirror `main`, upstream `master`). When emitting step 3 (the post-merge release),
NEVER hardcode `main` — derive the upstream default branch from the existing remotes (`git
remote show origin` or `git ls-remote --symref origin HEAD`) and emit `git checkout <that>;
git fetch origin; git merge --ff-only origin/<that>;` before `abra recipe release`.
""",
}
WRAPPER_TEMPLATE = """# {wrapped} (thin wrapper)
The canonical definition of this skill lives in the **opencode** position:
@@ -99,7 +166,7 @@ def main() -> int:
)
canon = ROOT / ".opencode/skills" / wrapped / "SKILL.md"
canon.parent.mkdir(parents=True, exist_ok=True)
canon.write_text(frontmatter + BODY_TEMPLATE.format(name=name, wrapped=wrapped))
canon.write_text(frontmatter + BODY_TEMPLATE.format(name=name, wrapped=wrapped) + PER_SKILL_NOTES.get(name, ""))
thin = ROOT / ".claude/skills" / wrapped / "SKILL.md"
thin.parent.mkdir(parents=True, exist_ok=True)
thin.write_text(frontmatter + WRAPPER_TEMPLATE.format(name=name, wrapped=wrapped))