Skip to content

Release v2.57.0 - #6070

Merged
atomantic merged 318 commits into
releasefrom
main
Sep 3, 2026
Merged

Release v2.57.0#6070
atomantic merged 318 commits into
releasefrom
main

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Release v2.57.0

Released: 2026-09-03

Highlights

Rigging & 3D

  • Generated 3D characters can now be auto-skinned to a rig with a measured weight-coverage gate, then have animation clips retargeted onto them with a compatibility contract and a motion proof, so imported motion doesn't visibly break on unfamiliar skeletons.
  • 3D models can be exported as USDZ, so they open directly in AR on an iPhone/iPad.
  • Image-to-3D gained a subject-framing control so limbs and extremities survive conversion, and Blender rigging now resolves through a fail-closed runtime resolver with a readiness probe instead of failing silently mid-job.

Code review & CoS agents

  • Added OpenCode, Kimi, and MTPLX as selectable code reviewers, and reviewers now live under Models instead of a separate list.
  • pr-reviewer stage 3 can run on any enabled CLI/TUI provider (spawned headless), with a documented Claude sandbox recipe.
  • A fork PR's held CI run now releases only after the coordinator's own approval, never before — closing a gap where an untrusted fork's CI could run ahead of review.
  • A failed installer can now queue a CoS agent to investigate the failure automatically, and the merge-gate contract is verified before a completing agent tears down its worktree.
  • Numerous CoS/pr-reviewer robustness fixes: honoring a Stop signal mid Codex RPC, rescuing hook payloads from output.txt for direct CLI runs, keeping the sandbox-write fallback gated on a real git apply (not --check), retrying local code review without reasoning_effort when a model rejects thinking, and screening PRs for hidden/model-directed content instead of keyword co-occurrence.

Video generation

  • FastH3 can now render directly from FastVideo's own checkpoint by converting its DiT locally.
  • Fixed FastMetal's picker showing the wrong download size, clamped FastH3 frame options to the pipeline's actual 5–15s window, and made generate_fastvideo importable on Python 3.9.

Onboarding & settings

  • A fresh install now gets a first-run card that stages one PortOS slice end to end.
  • Added a Settings > Credentials page showing presence and source for configured credentials.
  • Generated assets are now stamped with their source model/LoRA licenses.
  • Scheduled-task cadences collapsed to a simpler On-Demand / Scheduled choice.
  • Local coding runtime is now recommended based on detected hardware.

Reliability & UX fixes

  • Fixed a stuck-forever FLUX.2 venv health-check cache, boot schema DDL races across processes, worktree cleanup after a failed agent spawn, and object-URL leaks in ImageGen.
  • Mobile/UI polish: 44px tap targets on MeatSpace log-row icons, wrapping long tokens in <pre> blocks, clamping dropdown popovers to the viewport, and keeping long record names readable via a title tooltip.
  • App Management updates are now opt-in and route through the detached launcher with proper preflight guards, instead of running unannounced.

Full Changelog

Full Diff: v2.56.0...v2.57.0

atomantic and others added 30 commits September 2, 2026 15:10
Keep a long record name readable in detail-page headings
feat(video-gen): render FastH3 from FastVideo's own checkpoint by converting its DiT locally
… geometry

The "Recommended coding-agent setup" card offered a 48 GB Apple Silicon
machine a 64K launch context with only a vague headroom sentence to justify
it. The number was too small and nothing in the code let a reader check it.

Qwen3.8-27B holds a KV cache on only its 16 full-attention layers (the other
48 are Gated DeltaNet, whose state is constant in the window), which costs
65.5 KB per token at a bf16 cache — the same per-token figure the SGLang
recipe sizes its pools from. Against the ~75% of unified memory macOS gives
the GPU, and the 15 GB MLX 4-bit weights:

  48 GB -> 64K reserved 19 GiB of 36. 128K reserves 23 GiB, still leaving
           ~13 GiB for PortOS, the harness and local image/video work.
  64 GB -> 256K, the checkpoint's own ceiling, reserves 31 GiB of 48.
  128 GB -> the same 256K on the quality checkpoint, with budget to spare.

So each Apple tier moves up one step. A 1M-token window is not reachable on
any of them: its KV cache alone is 65.5 GiB, more than a 48 GB machine has in
total, and the catalog caps this checkpoint at 256K regardless (the
1M-context local model there is Nemotron 3 Nano 30B-A3B).

Profiles now carry contextTokens rather than a display string, the card
renders the label from it, and each note states the arithmetic and the
kv-quant lever that buys more. A parameterized test pins weights + KV under
each tier's GPU budget with an 8 GiB reserve, so the next edit cannot promise
a window the machine will not load.

Claude-Session: https://claude.ai/code/session_01FVBPGvC77m8KA6J4UwDRnv
fix: size the curated coding-agent context from the model's actual KV geometry
…ts (#5914)

## Summary
- Stamp model + LoRA license/sourceUrl onto image sidecars and
video-history rows at finalize (local and federated). Unknown stays
`null` (shown as "unknown") — never a permissive default, never
`disclosure.runtimeLicense`, never Civitai `allowCommercialUse`.
- Persist `license` on LoRA install sidecars so the stamp can be written
from what was known at render time, not a later re-read.
- Roll up distinct sources into an Attribution & licenses section on the
media lightbox, collection pages, and pipeline export.

Closes #5638

## Test plan
- [x] `server`: assetProvenance, civitai sidecar, huggingface LoRA
sidecar, imageGen local.buildSidecarMeta, image/video remote, lib barrel
- [x] `client`: AttributionList, MediaLightbox, lib barrel
- [ ] CI Gate / Server tests / Client tests green
…king baseline (#5693)

The two existing cycle guards (agentImportCycles #2837/#3450,
twinImportCycles #5687) each fail only on a ring touching their own
cluster, so the rest of a 1000-module directory had nothing stopping the
next one. A static ESM cycle is a boot-order hazard: whichever member
evaluates first sees `undefined` for the others' bindings, and no
behavior test notices until an unrelated import-order change surfaces it.

serviceImportCycles.test.js now covers the whole directory against a
baseline of the five components still live, each recorded against the
issue that removes it (#5916-#5920). Both directions are asserted: an
unlisted component fails, and a listed one that is no longer detected
fails too, so the list can only shrink and a fixed cycle cannot leave a
stale entry behind.

The baseline is keyed on strongly-connected components, not on the rings
findImportCycles renders. That walk reports whichever rings it closes
from wherever it enters a component, and it enters wherever readdirSync
put the first file - filesystem order, not alphabetical - so the same
untouched graph yields a different list on another machine. Components
are a property of the edges alone. They are also the truer picture: the
DFS walk names 8 modules in the autopilot ring; the component is 22.

findImportCycleComponents joins the shared parser in
lib/staticImportGraph.js rather than getting its own module, for the
reason that file's header already gives - two copies of a structural
matcher is how a guard rots.
DEPS.md claims to be a living reference of every third-party dependency,
but nothing enforced that. `playwright-core` shipped as a server runtime
dependency 27 days after the document's stated last-audit date with no
row at all, so the newest package in the tree was the one with no
recorded justification.

`docs/deps-doc.test.js` now checks both directions against every
workspace manifest, discovered via `discoverWorkspaces()` rather than a
hardcoded list: a dependency with no Quick Reference row fails, and a
row naming a package no manifest declares fails unless its verdict
records the removal (REMOVED / REPLACED, which the five deliberate
eslint-stack entries carry). A non-vacuity assertion keeps a broken
discovery path or a table-format change from passing over nothing.

Names only, never versions — DEPS.md carries no version for most rows,
and asserting on the few that do would make every Dependabot bump a
doc-edit chore. Manifests are JSON-parsed as files rather than resolved
as modules, since CI never installs root node_modules.

The test lives next to the document it guards, so server/vitest.config.js
picks up ../docs the way it already picks up ../scripts and ../autofixer.

Also fixes the drift the test found: adds the `playwright-core` row, and
drops the stale Biome pin from the prose so the manifest stays the single
source for that version.
The comparator returned 1 for equal elements, which is not a strict weak ordering. SCCs partition the nodes so a tie cannot arise today, but a sort that lies about equality is not something a determinism-critical helper should carry. Compared by code point rather than localeCompare — locale-dependent order is the machine-to-machine variation this function exists to keep out of a baseline.
… meatspacePostStats (#5709)

meatspacePostStats.js carried byte-identical copies of trainingEntryTask and
summarizeSkillEvidence from meatspacePost.js, so a change to how a legacy
training entry maps to accuracy had to be made twice — and if it wasn't, the
stats endpoint and the session view would report different accuracy for the
same drill.

summarizeSkillEvidence is now exported from meatspacePost.js (where its
deriveTaskAccuracy/deriveTaskCompletion inputs are declared) and imported by
the stats module. trainingEntryTask stays private: with the duplicate gone its
only cross-module consumer reaches it through summarizeSkillEvidence.

The edge is downward-only, so the #5690 layering guard still holds.
Review pass on the guard added in the previous commit:

- `optionalDependencies` now counts as declared. An optional package is
  still installed third-party code with a supply-chain surface, so one
  added there would otherwise have slipped past undocumented. No manifest
  declares any today, so this closes a hole rather than fixing a miss.
  `peerDependencies` stays out: a peer is declared for a consumer to
  install, and no PortOS workspace is a published library.
- The package-name cell is matched on its first backticked token instead
  of requiring the whole cell to be one. Wrapping a name in a link or
  appending a footnote marker dropped the row from the scan, which failed
  the parity assertion for a purely cosmetic doc edit.
…er test

The conventional-update-script test wrote update.sh and expected it to be
spawned directly, but appUpdater deliberately looks for update.ps1 on
Windows and runs it through powershell. On the Windows CI shard the script
was therefore never found, no command was spawned, and the assertion failed
with zero calls — which in turn cancelled every sibling CI job.

Assert the shape for the host running the suite instead: update.sh executed
directly on POSIX, update.ps1 passed to powershell on Windows. Production
behavior is unchanged; only the test was platform-blind.

Claude-Session: https://claude.ai/code/session_01GQRKyv4WzTmSRwMs2q6PWH
…useAutoRefetch (#5697)

Every one of these polls kept firing while the tab was hidden, so a PortOS
window left open on a second machine over the tailnet held a continuous
request stream — including a 1.5s pm2-status poll and 5s Ollama residency
probes nobody was looking at. useAutoRefetch already short-circuits its tick
on document.visibilityState === 'hidden' and re-fires once on return.

Each site keeps its existing gate as `enabled` instead of an early return,
and passes `immediate: false` where another effect already owned the first
fetch. ThreejsModelDetail and WalkWorkflow keep their raw intervals: one runs
a bounded in-flight poll pool with a per-tick AbortController, the other
counts ticks to self-cancel, and the hook models neither.

pollingConventions.test.js pins the rule tree-wide — no setInterval under
components/ or pages/ outside a documented allowlist, with a burn-down check
that fails on a stale entry.
…tedCategories (#5705)

The parity test stubbed `getSupportedCategories` to return `[]` and then
iterated a hardcoded copy of the route's own `categoryParam` enum, so it
compared the route to itself. The #730 regression it exists to catch — a
category registered in `dataSync.CATEGORIES` but forgotten in the route enum,
which 400s before the handler runs — produced a green suite.

The service mock now delegates that one export to the real module
(`vi.importActual`); the three I/O entry points stay stubbed so the route test
still never touches a store. `categoryParam` is exported so the test compares
the two lists symmetrically instead of re-typing one, which also covers the
previously unguarded direction: an enum entry the service retired, which
reaches a 404 at the service. The per-category cases are generated from the
real list, and the assertion moves from `not.toBe(400)` — which a 500 from a
broken handler also satisfies — to the actual contract.

The apply verb parses the same enum as the reads, so it gets no second sweep;
its existing case is tightened to assert the exact `applyRemote` arguments
rather than only that it was called.

Pulling the real export loads the whole dataSync graph, so the settings mock
spreads the real module rather than listing one export. Same reason for the
explicit timeout on the privacy guard's lazy `import('../dataSync.js')`: that
import runs inside the test body and was flaking against the default 10s cap.
…un changes (#5697)

The useAutoRefetch conversion removed the per-effect `ignore` flag, so a poll
still in flight when the user switched looms could overwrite the fresh run with
the previous loom's, restarting the poll against the wrong run and firing the
terminal-run banner for it. Stamp each poll with the loom:run it asked about,
mirroring LoomProductionPanel's productionIdentityRef.
…e drift (#5706)

.env.example is the only place a fresh install can discover what it can
configure, and every one of these variables is read lazily with a silent
fallback — so an undocumented one is invisible: the feature just uses its
default and nothing says otherwise. The file had rotted in both directions.

Forward: 16 variables the server reads were undocumented, including the two
that made the gap actively confusing. signalSync.js reads SIGNAL_DIR,
SIGNAL_CONFIG_PATH and SIGNAL_DB_PATH in one three-line block and only the
middle one was documented, so relocating a Signal install configured half of
it. Each new entry names its reading module and its default.

Reverse: PORTOS_UI_MAX_MEMORY was still advertised long after #5322 made the
Vite ceiling a fixed constant. Setting it did nothing; it is now removed.

server/envExampleDrift.test.js checks both directions so this cannot recur.
The forward scan covers server runtime modules with string and comment content
blanked, so a `process.env.X` inside a vite config PortOS GENERATES for a user
is not mistaken for a PortOS setting. The reverse scan is deliberately wide —
every tracked code file in the repo, any language — because a false "this is
dead" would block CI over a real setting; prose and tests are excluded so a
doc mention cannot vouch for a key nothing reads. Its INHERITED_ENV allowlist
is a category list with a reason per entry (OS-, toolchain- and test-harness-
provided names a user would never put in .env) rather than a snapshot of
today's diff, so it keeps meaning something as the tree grows.

Registered in ALWAYS_RUN_TESTS because any server file can add a process.env
read and .env.example is not a scope the selector routes anywhere.
…ger (#5697)

The guard scans the tracked tree via test/trackedFiles.js, so CI's import-graph
selection can never reach it — repo-scan-guards.test.js fails until it is
registered next to the other client structural guards.
Make the /api/sync category parity test read the real getSupportedCategories
Route eleven hand-rolled polling intervals through useAutoRefetch so hidden tabs stop hammering the API
Reconcile .env.example with the environment variables the server reads, and guard the drift
Guard every static import cycle in server/services with a shrinking baseline
test: guard docs/DEPS.md against dependency drift (#5708)
refactor: import summarizeSkillEvidence instead of re-copying it into meatspacePostStats (#5709)
…dless

Stage 3's provider picker offered only its placeholder while the note beside
it listed three "eligible" providers. Both were reading the same vendor
postures, but the eligible records on a typical install are the disabled CLI
siblings (codex, antigravity-cli, grok-cli) of the TUI records the user
actually enabled — and TUI records were excluded from every posture outright,
so the stage resolved to "no eligible provider" and failed closed.

A public-review stage never runs interactively, so a TUI record can carry its
vendor's postures and be spawned through the same headless recipe as its CLI
sibling (`isDirectBinaryProvider`); the lifecycle now keeps `isTui` truthful
and decides the transport with a separate `spawnHeadless` flag. The dead TUI
posture branch in `applyCommandDefaults` / `buildTuiSpawnConfig` goes with it.

On the client, `selectableProviders` is the one visibility rule for both the
picker and the "eligible on this install" note, so the note can no longer
name a provider the dropdown hides. The Stage 1 card copy describes the
deterministic screen and the optional classifier.
…ord co-occurrence

Stage 1 withheld PR #5906 (a two-line docs fix) as an "encoded instruction"
because "payload" appeared in one hunk and "agent" in another — the
deterministic checks matched trigger words anywhere in a whole PR, which on
an agent-orchestration codebase is nearly every diff. Prompt Guard was also
not installed here, so a scan that cleared the heuristics could never finish.

The deterministic layer is now the boundary the stage exists for: invisible or
direction-control Unicode (zero-width, bidi overrides, tag block, variation
selectors; emoji joiners exempt), model-directed instructions hidden in
comments GitHub never renders, and obvious model-directed harm expressed as
single-line, proximity-bounded shapes. The classifier runs as an optional
second layer only when installed on Models > LLMs > Abuse Guard, and reports
name what actually ran. On the last 60 commits to main the new rules flag
nothing; PR #5906 passes.

Stage 2 no longer rejects a "Review this PR" request for lacking an open
linked issue assigned to the opener: the preflight stamps
`maintainerTargeted` on that PR's normalized facts and the gate honors it —
a maintainer choosing to spend the review is not the unattended spend that
prerequisite bounds. The model's own quality verdict still applies.
fix: pr-reviewer offers enabled TUI providers, screens for hidden content, and honors targeted reviews
atomantic and others added 29 commits September 3, 2026 00:21
fix: resolve ollama thinking support per model before sending reasoning_effort
…odel refresh

`grok-cli`, `grok-tui`, `antigravity-cli` and `antigravity-tui` all carry a
`*-configured-default` in their models list AND as their default. That is not a
model the vendor will ever print — it is the "send no `--model`, let the CLI use
its own default" marker — so one Refresh click dropped it, silently repinned the
provider onto a concrete model, and removed the option from the editor's picker
(which renders it from that same list), leaving no way back from the UI.

Also from the review:
- `usesHarnessCatalog` now refuses a record that hand-declares its own OpenCode
  provider entry. The `*Backed` markers are only ever written by PortOS's editor,
  so a hand-written config was indistinguishable from a plain Zen wrapper and its
  curated list would have been replaced with ids that config cannot resolve.
- The action modal stays open on success. Its terminal frame is the result, and
  unmounting on completion made a removal and a no-op update look identical.
- An update reports the version transition it produced (`1.18.27 -> 1.19.0`, or
  "left it on X") instead of asserting "up to date" from an exit code — which
  contradicted the Update-available badge on the row behind it.
- Per-row refresh state, so one row's completion cannot re-enable another's
  button mid-flight, and a stale banner is cleared when its row re-runs.
- `listProviders()` instead of reaching past the `getAllProviders()` envelope.
… pins

`npm audit` was red in server/ and autofixer/: the `qs` override sat at
6.15.3, which is the top of the vulnerable range for GHSA-x5fp-wj9c-mxmx
(array-limit bypass via bracket-key comma parsing) and GHSA-4mjr-xmp4-gh2g
(DoS via attacker-controlled isBuffer). 6.16.0 is the first fixed release,
and both express and googleapis-common resolve through it. All four
workspaces now audit clean.

Alongside that, the routine currency pass:

- server: undici 8.10.0 -> 8.10.1, postcss override 8.5.26 -> 8.5.27,
  ip-address override 10.5.0 -> 10.7.0 (root override matched)
- client: @biomejs/biome 2.5.11 -> 2.5.12,
  @testing-library/user-event 14.6.6 -> 14.6.7, lucide-react 1.37.0 -> 1.40.0
- client also gains the postcss pin, because dependency-overrides.test.js
  requires every tracked lockfile to resolve a pinned package to the pin --
  bumping it in server/ alone left client's tree drifting at 8.5.26.

Left alone deliberately: the js-yaml (4->5), protobufjs (7->8) and nanoid
(3->6) overrides all have newer majors, but each is a security *floor* on a
transitive dependency whose consumer still asks for the old major, so
forcing the major would break the consumer for no audit benefit. node-pty
and jsdom are intentionally pinned ahead of their `latest` dist-tags.
…pdates-mtld0om6/agent-2a22166f

chore(deps): clear the qs moderate advisories and refresh patch-level pins
Prevent .DS_Store timestamps from keeping installs permanently marked out of sync after successful reconciliation. Add a public install-state regression covering newer Finder metadata.
…5871)

All three FastMetal rows named a download size that was only the MLX DiT
file, while the entries pull a whole-repo snapshot that also carries a
bundled T5 text encoder and VAE. A user picking "~3.5 GB" was handed a
13.4 GB pull, and on a nearly-full disk it failed partway.

#5860 corrected `estimatedDownloadGb` in the disclosure panel. The name
is the number read BEFORE that panel is ever opened, so it is corrected
here — 1.3B to ~13.4 GB and 5B to ~19.5 GB. Each name now quotes its
disclosure verbatim rather than rounding away from it, since the two
disagreeing was the bug.

The 14B repo ships its DiT twice: `mlx_dit.safetensors` at the root and
an `ema/` copy of the same 14.14 GB tensor. Only the root one is ever
read — generate_fastvideo.py defaults `mlx_checkpoint` to the model root
for the fastmetal family and always forwards it, so the entry script
takes `resolve_mlx_checkpoint`'s explicit branch and loads the DiT from
that one directory. Nothing on that path names `ema/`. That row
therefore declares `repoFiles` and drops the duplicate, making its
honest figure ~27.1 GB rather than 42.3 GB.

Names are persisted per install, so migration 336 rewrites them, with a
load-time twin (`upgradeFastMetalDownloadSizes`) for the boot that runs
it. Every rewrite is guarded on a byte-for-byte match with the value
PortOS itself shipped, so a user rename, a re-pointed fork, an existing
`repoFiles` narrowing, or a hand-tuned estimate is left alone. The
migration also repairs a stale persisted `estimatedDownloadGb`, which
#5860 shipped no migration for.

Claude-Session: https://claude.ai/code/session_014HkS5yBXfiNQL2t2k6RPRb
… refresh

`opencode models` prints every namespace the local OpenCode is authenticated
for, not just `opencode/*`. On a box where the user has run `opencode auth login
<vendor>`, an unfiltered refresh wrote `anthropic/*` ids into a record named
"OpenCode Zen CLI" whose key field is `OPENCODE_API_KEY` — its picker then
offered models that bill a different account. A refresh updates a record's
catalog; it does not widen what that record is for. A filter that matches
nothing leaves the record alone rather than blanking a working list.

Also:
- `npm view` now spawns through `prepareCliSpawn`, like the version and models
  probes beside it. `npm` is a `.cmd` shim on Windows and `execFile` under
  `shell: false` never resolves it, so `latestVersion` was permanently null
  there and the staleness detection this page exists for was silently dead.
- The availability probe keys on `typeof === 'string'` rather than `!== null`,
  so anything outside the string-or-null contract fails safe as not-installed.
- `RuntimeInstallModal` takes a `doneText`, so a removal no longer ends with
  "is ready" printed under a log line saying the CLI was just deleted.
- Escaped the pipes in the docs/API.md action row, which GFM was splitting into
  extra table columns.
fix(video-gen): quote the real FastMetal download size in the picker
add a Harnesses page so coding-agent CLIs can be updated from PortOS
The ChiefOfStaff page suite passed in isolation but flaked under a full
`npm test`, timing out in `findByRole('button', { name: /Force Evaluate/i })`.
The page fans out several mocked reads on mount and each resolution is its own
macrotask, so a bare `findBy*` straight after render polls blind through that
whole chain — on a contended worker it ran past Testing Library's async budget
before the config panel had rendered.

Wait on the settle signal the page already publishes instead of buying more
budget: the loading branch renders a `role="status"` busy region labelled
"Loading Chief of Staff". Every test that reaches into a tab's contents now
mounts through one `renderSettledAt(tab)` helper that waits for that region to
clear, so the following query spends its budget on a single render. The
loading-skeleton guards keep mounting bare — they hold a core read open on
purpose so the busy branch is what renders.

`asyncUtilTimeout` (3s, #3474), the two-worker client cap, and `testTimeout`
are all left untouched; raising them was already spent on this file twice.

Also pins the Force Evaluate button's own contract in ConfigTab's suite, where
it renders directly with no page mount to wait on: one `onEvaluate` call per
click, the explanatory title, no API or toast of its own, and availability
while the settings editor is open. The page suite keeps the half that is
genuinely page state — the toast and status-bubble result of the handler the
button invokes.

Closes #5857

Claude-Session: https://claude.ai/code/session_01YHCY5GnrNp66Gprb9JHYvi
test: settle the ChiefOfStaff page mount before querying it (#5857)
…-freshness

fix: ignore Finder metadata in build freshness
…roof (#5893)

Applying an animation clip to a rigged character produces a GLB whether or not
anything in it moves, so "the exporter returned" is worthless as evidence. This
adds the retarget lane behind a measured gate that refuses to publish a file
which cannot be shown to animate: a re-import has to find a named clip of
non-zero duration on the exported armature, and joint displacement has to be
measurable across sampled frames. A file-only export, or one holding a single
pose, is rejected by name instead of shipping as a finished character.

The skeleton contract from #5892 is enforced structurally rather than by
promise. The Blender worker runs twice: a read-only probe pass reports the
clip's animated bones, its clip roster and the character's bones, then the
orchestrator runs the existing all-or-nothing reduceBoneMapping() and refuses
before the apply pass — the only pass that can write a file — is ever spawned.
The bone tables stay owned by skeletonMapping.js instead of being duplicated in
Python where they could drift.

Head/neck skin weights get a conservative cleanup with two modes. Auto-skin's
nearest-bone fill can leave a scalp vertex bound to a shoulder, so vertices
sitting above the neck whose dominant bone is outside the head zone are
re-bound. Diagnostic mode (the default) measures the proposal and the cap and
changes nothing; write mode applies it and refuses outright if the proposal
exceeds the cap. The cap is re-derived from the vertex count on the Node side
rather than read from the report, so a worker echoing a roomier number cannot
widen it, and a diagnostic run that changed any weight at all is a gate failure.

The retarget report is the Phase 2 rigging report extended — same version, same
thresholds echo-back, same GLB-first/report-last no-replace pair contract — not
a second format. readRiggedArtifact() now resolves the artifact from the
report's own output_file, so one reader serves both lanes. The readiness gate
and the worker spawn are shared by both lanes instead of copied.

New endpoints: GET /api/rigging/clips lists the local clip library with its
CoS-state coverage, POST /api/rigging/models/:id/retarget runs one retarget
inline so the measured refusal is the response.

Closes #5893

Claude-Session: https://claude.ai/code/session_01DZrib5gD5qRfxPpZVkcQxM
…on changed weights (#5893)

A worker that measures an over-cap head-zone cleanup refuses before touching a
weight, so it reports zero changed vertices. Gating only on the changed count
let that run pass the cleanup check and fall through to the generic "the worker
exited 2 without a usable report" — throwing away the sentence naming the
proposal and the cap, which is the only thing the user can act on. Write mode
now refuses the proposal itself; the changed-count check stays as the
defense-in-depth case of a worker that wrote past its cap anyway.

Also pins the worker's scene fps after the factory reset rather than before it,
so the frame-to-second conversion behind every reported duration reads the value
the worker intended instead of whatever Blender's default happens to be.

Claude-Session: https://claude.ai/code/session_01DZrib5gD5qRfxPpZVkcQxM
feat: retarget animation clips onto rigged characters with a motion proof
… failures

A stage-3 review posted an ✅ Approved verdict carrying three ❌ test-evidence
rows, none of which was a failure of the change: a deliberate mutation probe
that had to fail to prove a new test was not vacuous, and two whole-server-suite
runs (patched, then unpatched) whose 3533 failures were the review sandbox
denying sockets, out-of-tree writes, GPU access, and language toolchains. That
same suite is green outside the sandbox — 38409 passing — so the two runs cost
~76k tests for no signal about the patch, and the reader is left to parse three
red marks and their exculpations under an approval.

Two causes, fixed at their own layers:

- `testEvidence[].status` only offered pass|fail|not-run, so an honest model had
  to stamp `fail` and explain it away in `detail`. Add `expected-fail` (a probe
  that had to fail) and `blocked` (the command ran, the environment produced the
  failures), leaving `fail` as the sole red mark and the sole claim that the
  change is broken. The icon map is now the single source of truth, with the
  accepted-status list derived from it. Both review producers interpolate this
  one contract, so the issue-watcher reasoning pass inherits it.
- Stage 3's step 3 invited the full run with "broader tests when practical".
  It now scopes runs to the suites covering the patched files, names what the
  sandbox denies, and forbids re-running a whole suite at the base to explain
  environmental noise — re-run only the failing files there.
stop pr-reviewer stage 3 marking sandbox and probe failures as review failures
…en the branch is writable

pr-reviewer stage 3 posted its review and, when everything lined up, merged.
A PR that came back with blockers — or that was approved but not merge-ready
(red CI, a conflict, a rebase the forge refused) — was left with nobody holding
it: the contributor got a review notification, PortOS kept re-polling, and the
PR appeared in no one's assigned queue.

Stage 3 now resolves an owner for every PR it does not merge. When PortOS can
push to the head branch (same-repo, or a fork with maintainerCanModify) and the
review is a concrete work order, it queues an agent to implement the review,
get the PR green, and merge it. Otherwise the opener is assigned and the next
move is theirs. A deferred verdict or findings that could not be anchored to a
diff line never become an agent work order — there is nothing to implement.

Fails closed on both axes: an unknown head-repository relationship counts as
no write access, and a per-PR ledger caps remediation at 3 attempts and stops
a scheduled sweep re-dispatching for a revision it already handed back.

The remediation prompt carries no contributor prose — it points the agent at
the PR to read the review PortOS already posted, so screened titles, bodies,
and diffs stay on the far side of the Stage 1 model-abuse boundary. Its
CI-gate/merge tail reuses buildCiMergeGateSteps, which gains a deleteBranch
option: push rights on a fork are not permission to delete someone else's
branch.
…fork claim

The remediation prompt asserted "@<login> left the head branch writable by
maintainers" unconditionally. For a same-repo head there is no contributor who
did that, so the instruction handed the agent a false claim about a real
person. Both halves of the sentence are now conditional on the resolved write
access, and each branch's wording is covered by a test.

Also drops three dead fallbacks the review surfaced: a headRefOid alternative
the sole caller never passes, a taskId seed both branches always overwrite, and
a comment naming a prompt-template placeholder that does not exist in this file.
…double-owning a PR

Two reviewer findings, both reachable in one sequence.

The ledger was written wholesale from a snapshot taken before the write queue
was entered. processTaskOutput and processPendingApprovals both own entries in
it and can be in flight together for one app -- cos.js fires the perpetual
refill on agent:completed before the completing task's own output hook has
settled -- so one pass silently erased the other's entry. That is not a benign
re-observation: losing an entry drops the same-revision dedup and the attempt
budget, so the next sweep spawns a second remediation agent for a PR one is
already working. persistState now accepts a patch function evaluated against
the freshly-read state inside its serialized queue, and a pass applies only the
entries it produced, merged by PR number.

(The wholesale-replace shape is pre-existing on approvedPullRequests, which is
left alone: losing a poll entry only costs a re-review, while losing a ledger
entry spends an agent.)

spawnPrRemediationFollowUp returned null both for a duplicate -- meaning a task
is already queued and an agent OWNS the PR -- and for a genuinely failed write.
The caller could not tell them apart and assigned the opener on top of a
running agent, putting one PR in two queues. It now returns a discriminated
status; only a real failure falls back to the opener, and an already-queued
observation no longer burns an attempt no new agent consumed.

Both regressions are covered by tests verified to fail against the prior code.
The lib README merges with git's union driver, so #6066's edit to that row and
this branch's insertion beside it both survived the rebase. Keeps #6066's
current text and restores alphabetical order.
Resolve & merge on an app's Pull Requests tab queued the review-loop
follow-up as an auto-approved SYSTEM task and then waited for the CoS
dequeue to pick it up. That tier only spawns system tasks while CoS
auto-run is in `execute` and under its daily action budget, so the task
sat `pending` until it was started by hand from the task page — the one
thing the button existed to avoid.

Pressing the button is the approval, so the route now dispatches the
follow-up itself through the same force-spawn path as the task list's
Run now button, suppressing the racing dequeue. When the spawn can't
proceed (no agent slots, daemon stopped/paused, runner unreachable) the
task stays queued and the response reports the reason instead of
claiming an agent is on it — the tab's toast says "Started…" or names
what it is waiting for.

Also fixes a related silent gap: a follow-up the task store rejected as
a duplicate persisted nothing under the id the service had minted, so
callers reported a task that did not exist. The service now returns the
already-queued record.

Claude-Session: https://claude.ai/code/session_01JErGn4inLCwQiWtXc99m2o
hand a reviewed public PR back to its opener, or land it ourselves when the branch is writable
fix(cos): start the PR page's Resolve & merge agent immediately
@atomantic
atomantic merged commit 55462f2 into release Sep 3, 2026
12 checks passed
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.

4 participants