* ci(macos): budget macos-swift by runner so fork PRs finish
runs-on falls back to hosted macos-26 for several cases: workflow_dispatch,
run_attempt > 1, fork pull requests, and (as of the runner-backend circuit
breaker landed on main the same day this PR was authored) breaker-routed
GitHub-hosted fallback. timeout-minutes did not cover the fork-PR case, so
fork PRs ran on the slow hosted runner with the Blacksmith-tuned 20-minute
budget.
Measured on PR #118989 (a fork PR): macos-swift was cancelled after 20m25s,
killed mid-compile at step 1365/1416. No test executed, and the log has no
swift compile error - only 'The operation was canceled.' ci-gate then fails
because it lists macos-swift as required, so the PR reads red for a reason
unrelated to its diff, and a contributor cannot rerun it.
Make the budget follow the runner instead of the trigger: every hosted path
gets 30 (folded into the same budget the circuit breaker's own hosted-fallback
timeout extension already established for this job), so this doesn't
reintroduce a second, competing hosted-timeout value. Blacksmith paths keep
20 unchanged. Replaces the single pinned-string guard with a table-driven
test covering runs-on and timeout-minutes together across every trigger
context that can route to a hosted runner.
* chore: refresh PR head (keep open for maintainer review)
* ci(macos): track main's author-association runner routing
Main now routes macos-swift by pull_request author_association rather than
fork-ness, so the timeout predicate and its guard scenarios follow it.
* ci(macos): tighten hosted runner budget coverage
Co-authored-by: harjoth <harjoth.khara@gmail.com>
* docs(ci): document hosted macOS budgets without merge conflicts
Co-authored-by: harjoth <harjoth.khara@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(scripts): see SAFETY comments after template substitutions
The assertion ratchet scanned each file with a raw ts.createScanner, which
reads the `}` closing a template-literal substitution as a block close. The
scanner desynced there, so every `// SAFETY:` comment after a file's first
`${...}` was invisible and its annotated assertions were counted as bare.
Track substitution brace depth and rescan the closing brace the way the parser
does. The fix uncovers already-annotated assertions in eight files, so the
baseline shrinks accordingly.
* fix(skills): record skill usage again and retire dead curator tables
Skill lifecycle curation shipped in 2026.7.1 with two producers: a trusted
`skill.used` consumer writing `skill_usage`, and a daily sweep aging skills
into `skill_lifecycle`. The weekly collection review (#121653) replaced that
policy and deleted both producers, but left every reader in place. Since then
`skill_usage` has had no writer, so curator status reported `lastUsedAtMs:
null` and `useCount: 0` forever, and `skill_lifecycle` had no writer either, so
pin/unpin/restore either threw "not found" on fresh installs or, worse,
"succeeded" on upgraded ones while gating nothing at all.
`skill_workshop_proposal_origin_runs` was never read in any revision; proposal
provenance is authoritative in `record_json`.
Restore the usage producer at its owner and retire what has no owner:
- `skill.used` events populate `skill_usage` again, registered with the
collection-review maintenance it belongs beside. Curator status derives
curated skills from applied Workshop create proposals and reports real usage.
- Weekly review receives bounded `useCount` / `lastUsedDaysAgo` evidence, with
prompt text stating usage supports keeping a procedure and never alone
justifies a drop.
- State schema 10 drops `skill_lifecycle` and
`skill_workshop_proposal_origin_runs`. Previously archived skills return to
the active collection, where review judges them by content; the migration
logs how many. Reconcile now clears usage only for skills it actually drops.
- `skills.curator.pin`/`unpin`/`restore` stay registered for existing clients
but fail with an explicit retirement message instead of silently doing
nothing.
Retirement code moves to openclaw-state-db-table-retirements.ts to keep the
schema-repair module under max-lines; that split is a pure relocation.
Production delta is +23 raw: about -46 for the change itself, +44 for the file
split, +25 for the ratchet fix in the previous commit.
* fix(e2e): suppress update checks inside Docker E2E containers
The runner's CI variable does not cross into `docker run`, so containers kept
reporting daily update checks and drowned real operators in the telemetry
aggregates. Inject the existing suppression switch from the shared helper so
every lane inherits it; callers that exercise update behavior keep their own
value.
* test(e2e): record the injected suppression in docker run contracts
* fix(release): preserve VCR mirror source digests
Transport only attestation-verified digests across secret-scanned job outputs, reconstruct immutable GHCR refs inside the VCR mirror, and add an approved mirror-only recovery path.\n\nCloses #129466
* fix(release): verify VCR recovery sources
Revalidate attestations and release-version labels before any VCR registry write so manual recovery preserves the immutable source boundary.
* test(release): keep VCR regression scoped
Leave global workflow-to-test routing cleanup for a follow-up; this PR directly changes and runs both VCR regression suites without forcing metadata-complete CI.
* fix(ci): preserve caches after warmer failures
Finish every selected cache-warm group, save content-keyed transform and compile caches, then fail visibly after the save steps. Ordinary CI remains fail-fast.
Replace the checker-only broad Octokit shape with exact issue request and response contracts, and make the test harness reject impossible missing issue responses.
Co-authored-by: Dallin Romney <dallinromney@gmail.com>
* fix(parallels): skip packaging for macOS dev upgrades
* fix(test): sanitize compile cache for all vitest children
* test(node-host): honor disabled worker compile-cache fixtures
* refactor(state): retire six dead shared-state tables at schema v10
agent_model_catalogs, android_notification_recent_packages,
command_log_entries, diagnostic_stability_bundles, media_blobs, and
model_capability_cache landed with the database-first squash but their
runtime writers never reached main; every stable since v2026.6.10 created
them empty (agent_model_catalogs held only rebuildable catalog cache rows
until #111173 removed its writer). State schema 10 drops all six tables
and seven indexes through both the runtime-open and doctor migration
paths, records the retirements, bumps the native reader ceiling, and
corrects stale database-first doc claims that still named these tables
as canonical stores.
* test: move cross-lane schema-version pins to v10
The v10 retirement missed current-version pins outside src/state: the
native guard vitest wrapper, placement-move and node-worker-launch
same-version assertions, and the audit outbound-progress tripwire. The
pinned pre-C04 audit reader is a v9-era build that now refuses v10
databases by the version contract, so the test projects the file back to
the exact v9 shape with the documented 10-to-9 downgrade fixture before
the reader proof; the shared fixture also seeds the v10 retirement
regression.
* test: keep only the used downgrade fixture export
* fix(i18n): keep surrogate pairs intact when bounding process output tail
appendBoundedProcessOutput used nextText.slice(-maxChars) to keep the
newest maxChars of captured process output. When the boundary landed
inside a UTF-16 surrogate pair (e.g. emoji in stderr), the retained
tail began with a dangling low surrogate, corrupting downstream JSON
serialization and fatal TextDecoder paths.
Switch to sliceUtf16Safe(nextText, -maxChars) from normalization-core,
which adjusts the boundary off the surrogate pair. The helper was
already imported elsewhere in the dependency graph.
* fix(i18n): count actually-dropped units when bounding surrogate-safe tail
Address ClawSweeper P2 finding on PR #120114: sliceUtf16Safe may
return fewer than maxChars code units when it advances past a low
surrogate at the boundary, so truncatedChars must derive from the
actual retained tail length rather than maxChars. For the emoji
case ("ab😀cdef", maxChars=5) the safe slice retains "cdef" (4
units), so 4 units are dropped, not 3.
Compute truncatedChars from nextText.length - text.length and
update the regression test expectation to 4.