Conversation
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.
fix: make managed app updates opt-in
…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)
test: cover video display wake
…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
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.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release v2.57.0
Released: 2026-09-03
Highlights
Rigging & 3D
Code review & CoS agents
pr-reviewerstage 3 can run on any enabled CLI/TUI provider (spawned headless), with a documented Claude sandbox recipe.output.txtfor direct CLI runs, keeping the sandbox-write fallback gated on a realgit apply(not--check), retrying local code review withoutreasoning_effortwhen a model rejects thinking, and screening PRs for hidden/model-directed content instead of keyword co-occurrence.Video generation
generate_fastvideoimportable on Python 3.9.Onboarding & settings
Reliability & UX fixes
<pre>blocks, clamping dropdown popovers to the viewport, and keeping long record names readable via a title tooltip.Full Changelog
Full Diff: v2.56.0...v2.57.0