Conversation
…atcher task `queuePendingMerge` is written from the agent-completion path whenever a merge-on-green PR is opened, but the only drain was inside `resolvePrWatcherBlock` — which runs exclusively when an app's `pr-watcher` scheduled task fires. That task ships disabled (its default prompt is a review-and-comment agent), so on the common setup PortOS queued PRs into a list nothing ever read: green PRs sat open forever at `ticks: 0`, and even the bounded MAX_PENDING_MERGE_TICKS escape hatch never fired. Observed with PortOS #3270 (queued Aug 1) and #3602 both stranded fully green. Adds `sweepPendingMergePrs()` and calls it from `evaluateTasks`, before the agent-slot gate (a deterministic merge claims no lane, so a full roster must not wedge the merge queue) and gated on `!paused` to match the autonomous tiers. The pr-watcher path now owns PR discovery only.
…irst match The guard located the pause gate with a plain `.search(/if (!paused)/)`, which assumes `evaluateTasks` has exactly one. The pending-merge sweep legitimately adds an earlier one — it claims no agent lane, so it must run before the slot gate — and the guard then read that as "the tier gate moved to the top" and failed on the very shape it was meant to permit. Anchor on the last pause gate preceding spawnPriority1UserTasks instead, which is what the assertion actually means. Verified by probe: stripping the tier gate's `!paused` still fails the guard.
The author filter previously jumped straight from "only issues I filed" to "any author", so a repo with real collaborators had to choose between ignoring teammates' issues and claiming drive-by issues from strangers. The new value trusts the accounts the forge already grants repo/project access — the same set that could push the code anyway. Neither `gh issue list --author` nor glab's accepts more than one account, so the gate is applied to the listing rather than pushed into the query: the detector resolves the trusted set (self + GitHub collaborators / GitLab members/all, so group-inherited access counts) and filters the normalized issues; the agent prompt spells out the same two-step recipe. An unresolvable author is dropped — the gate is a security boundary, so "can't tell who filed it" resolves to "not trusted". A failed member lookup goes transient with a remedy rather than silently narrowing to self or widening to everyone. Since that failure (a token without push access) repeats identically forever, the remedy rides the existing transient-verdict channel so both the run drawer and the on-demand toast print the way out instead of "try again shortly" — checkGhHealth can't see a per-repo permission gap, only global auth.
add a "me + collaborators" option to the claim-issue author gate
…its slug The Review Loop stores the reviewer as `antigravity`, but the shipped executable is `agy` — no `antigravity` command exists on any PATH. Every prompt that told an agent to invoke it named the slug, so a review-loop follow-up ran `command -v antigravity`, found nothing, reported "the only configured reviewer is antigravity … isn't available in this environment", substituted its own self-review, and merged the PR. Adds REVIEWER_CLI_BINARIES / reviewerCliBinary() / describeReviewerCli() to cosValidation as the one slug -> command mapping, and resolves through it everywhere a prompt names a reviewer as something to run. The multi-reviewer bullet and the CLI Reviewer Procedure heading now list the reviewers this loop actually configured (they were a fixed "codex / antigravity / claude / grok" roster) named by binary, with an explicit slug -> command line for any reviewer whose command differs from its slug. Also closes the half that did the damage: a reviewer whose binary is missing is UNSATISFIED, not clean. Both the dynamic follow-up prompt and the four versioned claim/plan prompts now forbid substituting a self-review and forbid the merge, directing the agent to comment on the PR and stop. The versioned prompts carry the cross-install contract: PROMPT_VERSIONS bumped for plan-task/claim-issue/claim-issue-gitlab/claim-issue-jira with each outgoing body preserved verbatim in PREVIOUS_DEFAULT_PROMPTS and the integrity snapshot regenerated, so other installs auto-upgrade. Follow-up filed as #3606 (detect a missing reviewer binary server-side at configuration time, rather than leaving it to the spawned agent).
name agent prompts the antigravity reviewer's real binary (agy), not its slug
The source chip and view label were 20%-opacity tints over an arbitrary photo, and pinned to opposite corners so a long view label collided with the source chip in a narrow column. Both now sit on their own near-opaque plate in one wrapping row.
make LoRA dataset image labels readable over any training image
A Review Loop reviewer whose CLI isn't installed was only discovered by the spawned follow-up agent at review time, after the PR was already open. Adds a shared commandExists() helper (consolidating two private copies in localLlm.js/ollamaManager.js), a TTL-cached getReviewerCliInstalled() probe in codeReview.js, and an `installed` map on GET /api/code-review/defaults. ReviewerPicker surfaces a "not installed" badge, warn-only per the issue's decision — a missing binary never filters or disables a reviewer.
agy renamed Weekly/Five Hour Limit to … Limit Remaining, so the Usage page Antigravity card scraped the panel but returned zero meters.
fix: parse agy 1.1.x Limit Remaining usage rows
Selecting the grok-tui provider produced an agent that never opened a shell. The runner's command allowlist carries only the CLIs it may spawn, and `grok`/`kimi` were never added even though both ship as providers in data.reference/providers.json — so /spawn-tui 400'd, no PTY was ever created, and the run died without a diagnosis. The reporting around it was the worse half. A rejected spawn threw out of the spawner into subAgentSpawner's task:ready listener, which only logs, so the error never reached the agent record: - TUI: the record sat `initializing` until the zombie reaper finalized it ~1min later as "Agent process terminated unexpectedly". - runner-CLI: worse — the stale `runnerAgents` entry keeps isAgentOwnedLocally() true, so every sweep skips the record while the 3s init timer flips it to `working`. It stays there until the server restarts. Both arms now finalize with the runner's actual error. The runner names the offending command instead of one opaque "missing or invalid fields", and an agent reaped with no pid ever recorded is reported as never having started rather than as a crash. A parity test pins the allowlist against both shipped provider catalogs so the next provider added without an entry fails in CI, not in a silent agent. Note the allowlist stays hand-curated on purpose: data/providers.json is user-writable at runtime with an unconstrained command string, so deriving it would let a config write choose the exec target. PLAN.md captures the remaining gap for user-created providers. The runner runs as its own pm2 process (portos-cos) — it must restart to pick up the new allowlist.
allowlist grok/kimi in the CoS runner and stop losing spawn errors
The reviewer table already chose which model each reviewer runs; how hard that model thinks was stuck at its CLI default, so buying a more careful (or cheaper) review meant swapping the model. Every reviewer row now has an Effort column next to Model, on the AI Providers panel, the task form, the CoS schedule config, and the /do:next run drawer. Each row offers only the tiers that reviewer accepts — the ladders derive from effortLevelsForProvider rather than being restated, so `agy` never shows `max` and copilot/grok/@username rows show no control at all. An out-of-ladder value is dropped rather than clamped: reviewing at a different effort than the row displays is worse than falling back. slashdo's --review-with grammar has no effort suffix, so the pin travels on the invocation instead: `codex -c model_reasoning_effort=high` / `claude --effort high` / `agy --effort low` when an agent drives the reviewer CLI, and an `effort` field on the local review request when PortOS runs it. Also collapses the three token-keyed reviewer-pin normalizers and their resolvers (max-rounds, models, efforts) onto shared factories, and fixes a case-folding divergence where pickCodeReviewDefaults and resolveReviewLoopOptions disagreed on a stored "High".
- The challenge auto-recheck ran its re-review at the model's default effort while threading the model from the same defaults, so the verdict deciding upheld-vs-escalated came from a weaker pass than configured. - reviewerEffortArgs delegated straight to buildEffortArgs, which clamps an out-of-ladder value. It is reached with raw task metadata, so a stale `antigravity: max` emitted `agy --effort high` while the picker rendered the same pin as unsupported. Normalize first: drop, don't clamp. - The local-reviewer jq hint hardcoded `model: "…"`, so an effort-only run told the agent to send a literal ellipsis — and the route prefers a truthy body.model over the configured default, turning the review into a model-not-found error. Build the example from the pinned keys. - Cover the effort roster at the cleanup→follow-up hop: it differs from the model roster (agy takes an effort but no pinnable model), and the wrong one would drop every agy pin with the suite still green.
Adding the effort pin made three existing seams visible as copy-paste, and
this pulls each up to the mechanism the pins already share.
- The three token-keyed pins (caps / model / effort) were hand-copied as a
guard-normalize-assign block per pin in BOTH persist paths, six blocks
across two files. A missed block is silent — the pin is configured,
persisted and displayed but never reaches the reviewer, with nothing
failing. They now iterate one exported KEYED_REVIEWER_PINS table.
- The prompt builder resolved the same six reviewer values at three spawn
paths. Bundled into resolveReviewerConfig(), so a fourth pin kind reaches
all three at once instead of being a three-site edit.
- The client's scalar<->map adapters were two hand-copied loop pairs
differing only in roster, key suffix, and single-value validator — the
exact three axes the server's keyedReviewerPinNormalizer already
parameterizes. Now one pinScalarAdapters factory.
- ReviewerPicker's Effort and Model cells each open-coded the same closed-list
select, including the non-obvious "keep a stored out-of-list value as an
<option> or the select reads as unset" contract. One renderPinSelect, with
the accent passed as a COMPLETE class string (an interpolated
`text-${accent}` would never reach the Tailwind build).
Two fixes fall out of the same pass:
- POST /api/code-review/local validated `effort` against a flat union of
every local level rather than the ladder for the requested backend. The two
backends are separate identities in REVIEWER_EFFORT_LEVELS, so once their
ladders diverge the route would accept a level valid only for the other
one and the service's own normalizer would drop it — a 200 with the effort
silently ignored instead of a 400. Now checked through the same normalizer
the service uses, with tests for both directions.
- The local-LLM jq hint read a Set that the pin note's .map() filled as a
side effect, so hoisting either line past the other would quietly emit a
body with no pinned keys. Both strings now derive from one array.
Also drops a redundant re-normalize in the challenge re-check
(pickCodeReviewDefaults already validated the effort scalars), and renames
reviewerModelEntries/Note to reviewerPin* — they carry effort flags too.
The parity test that pins the client's effort ladders against the server's
imported `components/cos/constants.js`, which imports `lucide-react` for its
tab and agent-state icons. That module isn't installed in the server CI job,
so the drift gate failed with ERR_MODULE_NOT_FOUND — it only passed locally
because a dev worktree has the client's node_modules on hand.
The icons have nothing to do with reviewer pins, so the pin vocabularies move
to `client/src/lib/reviewerPins.js`, a leaf with no client-only dependency
(its only import chain is utils/providers -> utils/formatters, both
dependency-free). `constants.js` re-exports every name, so no client import
changes. Verified the real CI condition by running the suite with
client/node_modules moved aside: 24/24 pass.
Also stops rejecting a blank `effort` on POST /api/code-review/local. The
refine only short-circuited on `undefined`, so `""` fell through to the
ladder check and 400'd the whole review — contradicting the route's own
header ("when omitted or empty we fall back") and leaving the `body.effort ||
configured` fallback on the next line unreachable. It was also asymmetric
with the sibling `model` field, where `""` has always fallen back. Any caller
templating the body with a possibly-blank effort slot hit it.
Drops three now-unused imports left behind by the resolveReviewerConfig
bundling (they survive only in comments).
Pin a reasoning-effort tier per code reviewer
Adds `cursor-cli` (headless `cursor-agent --print`, prompt on stdin) and `cursor-tui` (interactive PTY), both disabled by default. Ships a 27-model catalog curated from the live binary; `auto` is Cursor's own router, so no configured-default sentinel is needed. `--force` clears Cursor's workspace-trust gate as well as auto-approving tools — without it a headless run exits before doing any work. Also inverts the Bedrock model-id rewrite from opt-out to opt-in on the launch command, and collapses the two divergent TUI model-injection ladders into a shared resolveInjectedTuiModel, so a non-Claude-Code TUI provider can no longer have its model id rewritten on a Bedrock box.
- README catalog listed four cursor.js exports that no longer exist - appendModelArgs honored a user-baked --model pin only for opencode, so a pinned claude/codex/cursor TUI spawned a duplicate flag and lost the pin - the shared-helper refactor defeated agentTuiSpawning.test's Bedrock mock (internal call bypasses vi.mock); pin the env instead and drop dead stubs - --trust is not an approval posture; suppressing --force on it stalled an unattended run on the first tool prompt - replace two vacuous migration "deep-copy" tests with catalog/tier assertions - move supportsModelRefresh into utils/providers.js so it is testable
…mports - --auto-review grants approval but does NOT clear cursor's trust gate, so pinning it suppressed --force and every run exited on "Workspace Trust Required". Trust and approval are now checked as the two independent gates they are; --force covers both, --trust covers only the gate left uncovered. - supportsModelRefresh claimed to mirror the server dispatch but reported true for codex and kimi-cli (both shipped), whose Refresh button 404'd on click, and matched the exact command string so a path-configured gemini/claude was wrongly hidden. Rewritten as the allowlist the server actually implements. - drop imports and a vi.mock stub left inert by the shared-helper extraction - changelog the pin-wins and refresh-visibility behavior changes
The previous fix over-corrected: it matched claude/gemini on a command BASENAME while the server compares the raw string, so a renamed, path-configured binary would show a Refresh button the server still refuses — the same 404 in a new place — and a test asserted that wrong contract. Mirror the server's actual two-level shape instead: route on provider.type first, then apply the per-vendor tests exactly as it does, including that its tui arm (unlike its cli arm) never consults the provider name. Adds a seed-driven parity test that walks every shipped provider and compares against a transcription of the server dispatch, so the next provider added without a matching client branch fails here rather than in the UI.
feat: ship Cursor Agent as a CLI and TUI provider
…olding /simplify pass over the cursor fetcher, applying findings the reuse and altitude reviews both raised: - _fetchCursorModels was a near-verbatim clone of _fetchAntigravityModels; everything but the parse step was identical, down to both error strings. Extracted _execCliModelList(provider, defaultBin, parse), which now owns the spawn conventions (Windows-safe invoke, 15s cap, envVars merge, stdin close) and the throw-on-failed/empty posture. Both fetchers delegate; antigravity keeps its sentinel prepend, cursor prepends nothing. No behavior change. - Moved the agy stdout filter to internal/antigravity.js as parseAntigravityModelList, next to parseCursorModelList, so it is directly unit-testable instead of reachable only by spawning a fake binary. - Added internal/commandBasename.js: the basename normalizer was inlined twice inside internal/. The toolkit still may not import server/lib/providerModels (self-containment), but one copy inside the boundary is enough. - Dropped CURSOR_CLI_ID, a dead export whose only consumer was a test asserting it equalled its own literal. - Replaced that tautological constants test with a real drift guard: the test (which may cross the vendoring boundary, unlike the source) now compares the toolkit copy against server/lib/cursor.js on the binary name and every command-matching case. - Test cruft: unused exitCode param on writeFakeCursor, dynamic fs/promises import for chmod when the file already imports from it at the top. - Trimmed the duplicated throw-posture rationale from both fetcher JSDocs; it lives once on the shared helper. Filed #3620 for the deeper altitude finding both reviews raised and agreed was out of scope here: collapsing the vendor dispatch into one table and surfacing it to the client as a canRefreshModels payload field, replacing the hand-transcribed client mirror.
…ing arms
Found by the pre-PR review gate. The command-keyed cursor branch sat below
_refreshCLIProviderModels' name-substring branches, so a cursor provider the
user renamed "Cursor Claude Opus" (or "Cursor Antigravity") matched
name.includes('claude') first and had that vendor's static catalog persisted
onto it -- 15 Anthropic ids cursor-agent rejects, written silently, because
the client's gate is command-keyed and happily showed the Refresh button.
An exact command match is a stronger identity signal than a display-name
substring, so it must win; this is the same reasoning that already puts the
ollama check ahead of the generic claude one. Moved the cursor arm above
claude/antigravity/gemini, mirrored the order on the client, and pinned it
with a regression test that refreshes cursor providers named for three other
vendors.
…l refresh errors Four findings from the pre-PR review pass. 1. (medium) The antigravity COMMAND test still sat fused to its name test below the claude name test -- the same defect the previous commit fixed for cursor, one branch over. An `agy` provider named "Antigravity Claude Sonnet 4.6" (a natural name; agy's own catalog carries claude ids) got Anthropic's static list persisted onto it AND lost the antigravity-configured-default sentinel while defaultModel still pointed at it, blanking the model select. Split the command test out and hoisted it; the name test stays below, so "Claude via Antigravity" + command claude still reaches Anthropic. 2. (medium) The `id === CURSOR_TUI_ID` clause on the TUI arm was unpinned -- every test reached that arm via the command, so deleting the clause kept the suite green while a shipped cursor-tui repointed at a wrapper silently lost its refresh. Added a wrapper-command case per side (antigravity-tui too); verified by mutation -- removing either clause now fails a test. 3. (low) parseAntigravityModelList had no success-path coverage, contradicting the comment I added claiming it now gets direct unit tests. Added internal/antigravity.test.js: real output, prose rejection, sentinel exclusion, CRLF, punctuation, and an upstream-parity check. 4. (low) A failed probe surfaced to the user as "Provider not found or not an API type" -- false, and it buried the real cause in the server log, defeating the point of _execCliModelList refusing to fall back. refreshProviderModels now rethrows (502; 400 for "no fetcher for this CLI") instead of collapsing to null, so the toast carries the actual message. `null` keeps its one meaning: no refreshable branch matched. A legitimately EMPTY list still persists, so empty and failed stay distinguishable. localLlm's fan-out already caught per provider, so it is unaffected.
…atus codes
Round 2 verified all four round-1 fixes by mutation and raised three low items.
1. Three dispatch clauses were deletable with the suite staying green -- one of
them (`name.includes('antigravity')`) created by round 1's command/name
split. Left unpinned, a cleanup deletes it as redundant while the client's
mirrored name test keeps offering the button, and every click 400s -- the
exact client/server drift the parity apparatus exists to prevent. Added the
three missing cases (server: antigravity-NAMED with an unrelated command;
server: antigravity-tui repointed at a wrapper; client: agy-commanded under
an unrelated name). Verified by mutation: each now fails exactly one test.
The round-1 commit message claimed the antigravity-tui case shipped on both
sides -- it was client-only. It is on both sides now.
2. The unsupported-TUI case still fell out as null and rendered as
"404 Provider not found or not an API type" -- the same false message the
rethrow set out to stop showing, just on the arm I did not fix. It now throws
the same 400 as the CLI fall-through. With that, `null` has exactly ONE
meaning out of refreshProviderModels (no such provider), so the route's 404
is narrowed to a plain "Provider not found" instead of guessing at a cause.
3. The SSRF endpoint guard and the missing-Google-key check inherited the 502
default. Neither is an upstream failure -- both are the user's to fix -- and
BAD_GATEWAY reads as "vendor is down, retry", so the user retries instead.
Worse, any status >= 500 makes errorHandler log a full stack, so a plain
config error produced stack spam pointing at the wrapper rather than the
cause. Both tagged 400 at the throw; the `||` guard preserves it.
Also added the route-level tests the reviewer noted were missing: refresh-models
502/400/404 asserted at the HTTP boundary through PortOS's real asyncHandler and
errorMiddleware, including that no stack reaches the response body. That seam is
where the whole fix lives and it had only service-layer coverage.
Plus two stale comments the rethrow invalidated (the _execCliModelList docblock
still said it returns null; VoiceTab's said the route 404s on no models).
feat([issue-3615]): fetch Cursor's model catalog live via `cursor-agent models`
…I dispatch points Adding a coding-agent vendor means teaching ~8 open-coded dispatch branches about it, and a missed one is silent: the provider spawns, then runs the wrong binary or comes up without its unattended-run posture and stalls on an approval prompt. The two model-injection sites had already drifted this way before resolveInjectedTuiModel collapsed them. Adds a discovery test that derives its expectations from data.reference/providers.json rather than a hand-transcribed vendor list: inferTuiCommand must resolve every seeded provider id to that provider's own command; every distinct seeded TUI command must reach an applyCommandDefaults arm that injects a posture, with the two commands whose posture arrives by another channel (claude via seed args, opencode via OPENCODE_CONFIG_CONTENT) recorded explicitly and re-checked against the seed so the exemption can't rot; each arm must be idempotent over the seed args; and a blank-command provider must build the same invocation as the fully-specified one. Verified by planting an unknown vendor in the seed: three of the four checks fail. No divergence found in the currently-shipped catalog. Refs #3618
…in markers exactly The catalog walk filtered out providers that declare no `command` — exactly the shape `inferTuiCommand` exists to resolve, so a new vendor seeded that way would have skipped every check. Every seeded TUI provider is now walked, with the binary resolved through the same fallback the spawners use; the inferTuiCommand parity assert stays scoped to providers that declare a command, since there is nothing to compare against otherwise. Also: the `args` posture marker is matched by exact argv membership rather than a substring of the joined argv (a marker that prefixes a longer flag would have matched anything), and the exemption map gained a documented channel-less form so a future vendor that genuinely needs no posture records a reason instead of inventing a marker string to satisfy the assertion. Re-verified by planting both an unknown-command and a blank-command vendor in the seed: 4 checks fail, including the blank-command one that previously slipped through. Refs #3618
…mparison The blank-vs-explicit buildTuiInvocation check compared the seeded provider as-is, so a provider that already ships a blank command compared blank against blank. The right-hand side is now pinned to the resolved binary. Also scopes the inferTuiCommand parity walk to the process-spawning provider types, so a future non-process type that happens to carry a command field can't fail it spuriously. CLI providers stay in scope deliberately — resolveSlashdoStyle asks inferTuiCommand which binary will be spawned for those too. Refs #3618
…o a shared makeProviderSeedMigration factory The up() body that ships a new data/providers.json entry was byte-identical across 149/152/185/195/201/231, so hardening it meant a six-file sweep — and none of them ever picked up the RESERVED_MAP_KEYS prototype-pollution guard _lib.js already applies in its split-migration path. A def id of 'constructor' reads truthy off any plain object, so the presence probe would treat it as already installed forever. Each migration keeps its own frozen defs literal (a migration is the historical record of what it installed, so nothing reads data.reference at migration time); only the shell moves. The six duplicated shell-behaviour tests collapse into one suite against the factory, plus a case for the new guard; every per-migration payload assertion still runs unchanged. Closes #3617
…provider-seed family
…to one table and ship it as canRefreshModels Adding a vendor to model refresh took four coordinated hand edits: the TUI `else if` chain in `refreshProviderModels`, the CLI `if` chain in `_refreshCLIProviderModels`, the client's `supportsModelRefresh` mirror, and a SECOND transcription of the server dispatch inside the client parity test — so that test only ever proved the mirror matched its own copy. Both server arms now resolve from `internal/modelFetchers.js`, one row per vendor. The three keying conventions that had accumulated as prose above each branch become columns: `cliMatch` (strong — command/structural), `cliNameMatch` (weak — display-name substring, consulted only after no command claims the provider), `tuiMatch` (never consults the name). The route decorates every provider-shaped response with the derived `canRefreshModels`, so the client shrinks to `p?.canRefreshModels === true` and the hand-written mirror is gone. Derived on read and decorated at the route, never in `getAllProviders()`, so it cannot reach `saveProviders` and go stale against the table; a PUT that echoes it back is stripped by `providerSchema.partial()`. PortOS's own sanitized GET handlers shadow the toolkit's, so they decorate too — before redaction, since the ollama row keys partly on `envVars.ANTHROPIC_BASE_URL`. No shipped provider's button visibility changes (pinned by a frozen catalog walk on both sides). The one behavior that does: a provider whose command is literally `gemini` now refreshes as gemini even under a "Claude …" display name — the same "exact command beats a name substring" rule the chain had already grown for cursor and antigravity. Closes #3620 Closes #3616
…nd re-export canRefreshModels from the service shim The toolkit's `GET /samples` was the one provider-shaped response left undecorated, while PortOS's shadowing handler already decorated it — so the two disagreed about the same sample. And `server/services/providers.js` re-exported `isOllamaBackedProvider` but not its new sibling, leaving hosts that classify providers through the shim without the capability predicate.
The row laid the logline and its controls (sync badge, share, sync-to-peer, delete) out as siblings in one flex line. Flex won't shrink those below their min-content width, so on a phone they claimed the row and the flex-1 text column collapsed to roughly one word per line — a two-line logline rendered as a ~30-line ribbon. Group the controls and stack them under the content below sm; sm+ keeps the existing side-by-side row.
The 4B/9B side-by-side that gates #2791 was started and paused at step 563/1200 of the 4B arm; the 9B arm never ran. Captures the method, the step-0 control, the measured 9.4 s/step throughput, and the interim observation that the shipped rank-32/lr-1e-4 defaults bind identity much harder than the older 400-step rank-16 adapter that looked cleaner but encoded the wrong subject. Explicitly records that neither #2791 change is justified yet, so a later reader does not mistake the partial data for the verdict. Refs #2791
…erdict Both arms (4B and 9B-8bit, matched params, 1200 steps) finished and were visually compared at steps 0/300/600/900/1200. Verdict: stay as-is — neither proposed default change is supported. 4B did not lose to 9B on identity (the opposite, if anything, though on a single seed/dataset), and step 600 was weaker on identity than step 900 in both arms, so cutting steps to ~600 would ship a worse result than the current default's own best checkpoint. Separately: both arms peaked at step 900 and regressed by step 1200, which doesn't call for a default change but does point users at the existing checkpoint picker rather than assuming the final step is best. Closes #2791
…ROVIDER_VENDORS registry Adding a coding-agent vendor meant editing ~8 open-coded dispatch branches across 5 files, and two of them (the TUI model-injection sites in tuiHandshake.js and agentTuiSpawning.js) had already drifted apart once before being collapsed into resolveInjectedTuiModel. Introduces server/lib/providerVendors.js: one PROVIDER_VENDORS row per vendor (claude/codex/antigravity/opencode/grok/kimi/cursor, plus a deliberately-incomplete legacy gemini-cli row), consumed by every dispatch site that used to hand-roll its own vendor if-chain — inferTuiCommand, applyCommandDefaults, prepareCliPrompt, buildCliArgs, buildCliSpawnConfig, and allowedCommands.js's allowlist. Also extracts codex.js from tuiHandshake.js to match the existing one-file-per-vendor shape (antigravity.js/grok.js/kimi.js/cursor.js), and collapses the antigravity-vs-everyone-else model+effort injection duplicated between buildTuiInvocation and buildTuiSpawnConfig into one shared injectTuiModelAndEffort helper. No vendor's argv-building logic changed — this only relocates dispatch, verified by keeping every existing test green (25.6k server tests) plus a byte-for-byte allowlist-content check against the prior hardcoded Set.
An agy TUI agent blocked on Google's "We're finishing verifying your account eligibility" banner failed fast (good) but left the provider marked available, so the next dequeued task picked Antigravity again and died identically — a queue of tasks could burn down in under a minute. Worse, the banner was classified actionable, which BLOCKED each task over a condition whose own message says it clears itself. Two changes: - The eligibility signal is now non-actionable, so resolveFailedTaskDecision takes the retry path instead of blocking the task, and carries origin:'provider' (it is provider chrome, not text an agent could print). - agentFinalization benches the provider on ANY provider-origin failure, not just usage/rate limits. The provenance gate (#2642) is what makes this safe: a loose keyword match on a repainted TUI transcript stays 'output-scan' and never benches. The cooldown policy moves to server/lib/providerCooldown.js, shared with promptRunner.js — the two paths previously disagreed on how long the same category was worth (auth-error: 15m via the prompt cascade, never via the agent path).
The bench predicate kept a `|| category === 'usage-limit' || category === 'rate-limit'` fallback alongside the new `origin === 'provider'` check — preserving the old behavior, but contradicting the rule the surrounding comment states. Both categories have loose pattern alternatives (a bare "rate limit" / "quota exceeded"), so an agent's own failing test printing that phrase would bench a healthy provider for 5 minutes off nothing but its transcript. Provenance is now the whole gate. Genuine limits already carry origin:'provider' via their structuredMarker, and every finalizeAgent caller analyzes through analyzeAgentFailure or detectImmediateFallbackSignal — both of which stamp an origin on every branch — so no real provider failure stops benching. The bench test now covers output-scan rate-limit and usage-limit, which is what let this through: it only exercised auth-error. Verified by planting the old predicate back and watching both new cases fail.
bench a provider when the provider is what failed a CoS agent run
…agents
A bare GitHub repo URL captured in the Brain was already saved as a link and
cloned in the background, but nothing said so and nothing followed. Both capture
boxes now name the owner/repo that will be cloned and offer two sticky opt-ins:
- Scan for malware — the same read-only /do:scan audit as the Links tab's Scan
button, now sharing one task shape via services/repoIntake.js
- Study for PortOS ideas — a repo-study run that reads the clone for ideas
worth adopting and files them into the configured work tracker
Both dispatch only after the clone succeeds, and the request is persisted on the
link so a Clone/Retry after a failed clone still honors it.
The study prompt is clean-room by contract: never execute anything from the
clone, never edit it, read its LICENSE first, describe techniques rather than
copying code.
Two supporting changes at the shared layer:
- declaresNoCommitCriterion now also accepts the `workTracker` marker a
tracker-filing dispatch stamps, so a one-off run reaches the no-commit
exemption without claiming a scheduled task type — which would have enrolled
it in taskSchedule's per-type failure ledger and auto-parked a type no
schedule owns.
- resolveTrackerFilingBlock moves from cosTaskGenerator.js into
lib/workTracker.js, next to the wording table it reads, so all three
tracker-filing dispatch paths share one resolution instead of copying it.
Also fixes the Links-tab Scan button recording nothing on the link, which left
it re-arming into a duplicate-task 409 after a reload.
parseGitHubUrl matched both segments as `[^/]+`, so a crafted URL parsed to a dot segment: `https://github.com/../evil` yielded owner `..`, and githubCloner's `join(reposDir, owner, repo)` then resolved OUTSIDE the managed clone root (`/data/repos/../evil` → `/data/evil`). `github.com/foo/..` collapsed to the repos root itself. That localPath is persisted on the link and handed to an agent as the directory to scan or study, so a bad parse aims a run at an arbitrary directory. Anchor both segments to the character sets GitHub actually allows (owner: a login, alphanumeric with internal hyphens; repo: alphanumerics plus _ . -), reject the `.`/`..` repo names that class still admits, and anchor the host so `https://evil.example.com/github.com/o/r` is no longer read as a GitHub repo. Found by the pre-PR review gate. Pre-existing in githubCloner.js, but this branch promotes that parser to the shared lib/ rule and adds the agent-facing consumer, so it lands here.
detect GitHub repos in Brain capture and offer post-clone scan/study agents
… auto-run, keep old Stacker News hashes valid The v2.40.0 release review found five defects in the aggregate diff that each per-PR review missed, four of which only appear on an upgrading install: - A genuine Claude usage limit stopped sidelining the provider. The bench gate was rewritten to key on failure provenance, but `analyzeAgentFailure` only promoted a few phrasings to `origin: 'provider'` — not the banners Claude Code actually prints — so the exact case the bench exists for went unbenched and every subsequent dequeue re-picked the dead provider. Promote the vendor-branded banners; leave the generic phrasings a task's own failing test can print deliberately unpromoted. - The pending-merge drain ran on `!paused` alone, so CoS auto-run set to `off` or `dry-run` still merged PRs — including on the boot-time evaluation. It is the only tier that writes to a default branch and was the only one not gated on the auto-run mode. - Folding image URLs into the Stacker News content digest changed the value for every row already stored, with no compatibility path: the first sync after upgrading would re-analyze the whole back catalogue and re-sort it by arrival, and any action already in review would fail permanently with "Source content changed after review" about content that never changed. Both comparisons now accept the previous digest; rows adopt the new one as they are re-ingested. - `workTracker` and `reviewerEfforts` ride the permissive task-metadata map, so a v2.39.x peer accepts a synced task and then mis-runs it — grading a run that correctly filed issues as a validation miss and pushing that back. Bump `cosTasks` 3 -> 4 so cos-task sync pauses instead. - A failed malware scan carries a reportId with no file behind it, so the Brain "Scan Reports" filter offered a link that 404s; and the X page computed an unsaved-form flag it never used, letting a diagnostic run against the saved handle while the form showed a different one. Regression tests drive the real detector rather than hand-stamping the analysis shape — a hand-stamped `origin` is how the usage-limit regression shipped green. Deferred findings filed as #3630 (the drain has no real cadence), #3631 (an agent that prints a banner can bench a healthy provider), #3632, #3633, #3634, and #3635.
…n a clean tree The TUI idle reaper scored a run `idle-no-changes` whenever the worktree had no UNCOMMITTED changes. That fails every job whose deliverable is a commit: a /do:release or /do:pr agent commits, pushes, and opens its PR, then sits in the reviewer loop until the reaper takes it — with a clean tree *because it succeeded*. Two consecutive release runs on 2026-08-08 each did their whole job (cut the release commit, opened the release PR, then repaired it against review findings) and were both recorded as failures, so the task kept retrying while the release sat unmerged. Evidence of work is now a dirty tree OR at least one commit inside the run window (`git rev-list --count --since`, which filters on committer date so commits merely pulled in from the remote don't count). The #2191 no-op detection and the #3102 `worktreeChangesExpected` opt-out are both unchanged. The report-shaped half of the same problem (/do:review, /do:scan, /do:plan-task, /do:replan, whose deliverable lands outside the repo) is tracked in #3636, and the unsatisfiable [task-<id>] success criterion one layer up in #3637.
count commits as evidence of work so release/PR agents stop failing on a clean tree
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.40.0
Released: 2026-08-08
Highlights
Coding agents
Agent reliability
Code review
Brain & Comms
Fit and finish
agy1.1.x renamed its limit rows.Code Review Defaults
/do:nextrun drawer. Each row offers only the tiers that reviewer actually accepts — Codex'sminimal…ultra, Claude'slow…max, Antigravity'slow/medium/high, andlow/medium/highfor the local LM Studio / Ollama reviewers — so the picker can't offer a level the CLI rejects, and Copilot, Grok, and@usernamereviewers show no control at all because they have no such knob. Because slashdo's--review-withgrammar has no effort suffix, the pinned tier travels on the invocation instead: ascodex -c model_reasoning_effort=high/claude --effort high/agy --effort lowwhen an agent drives the reviewer CLI, and as aneffortfield on the local review request when PortOS runs it. Leaving a row unset keeps that reviewer's own default, exactly as before./do:nextrun drawer now show a "not installed" badge next to any reviewer whose CLI binary isn't found on this machine. It's informational only — the reviewer stays selectable, since a federated peer machine (or a later install) may still be able to run it.Cursor Agent provider
cursor-agent models) and stores everything it reports: 177 ids on a current account, including the-fastpriority-compute twin of most tiers, and Cursor's ownautorouter at the top. Because that catalog varies by account, plan, and binary version, it's the only authoritative answer — which is also why a refresh that can't reachcursor-agent(not installed, or not on the server's PATH) reports the failure and leaves your existing list untouched, rather than quietly re-saving the shipped 27 and telling you it refreshed. The button appears for both the CLI and TUI providers, since--modelapplies to interactive sessions too.Series Pipeline
Added
Cursor's agent CLI ships as a provider, in both CLI and TUI flavors.
cursor-agentis a harness-capable coding agent, but PortOS had no entry for it — using it meant hand-rolling a custom provider and guessing at its flags. It now ships ascursor-cli(headless one-shot) andcursor-tui(interactive PTY), mirroring the Codex/Antigravity/Grok/Kimi pairs. Both ship disabled, so nothing changes until you enable them, and once enabled they're selectable anywhere a process provider is — CoS agents, runners, pipeline stages, TUI sessions. The headless path runscursor-agent --printwith the prompt on stdin; the TUI path runs the interactive agent. Both inject--force, which is doing more work than the name suggests: besides auto-approving tool calls, it clears Cursor's workspace-trust gate, and without itcursor-agentprints "Workspace Trust Required" and exits — which in a fresh CoS worktree is every single run. The model list is a 27-entry slice of the account catalog (Composer, Claude, GPT, Gemini families), each id verified against the live binary; the default is Cursor's ownautorouter, so unlike Grok/Kimi/Antigravity no configured-default sentinel was needed. Cursor bakes reasoning effort into the model id (…-thinking-high,…-xhigh,…-max) rather than exposing a flag, so no Effort control is offered for it. Existing installs pick the pair up on restart via migration231-cursor-providers. If your account offers a model outside the shipped 27 (the full catalog runs to 177 ids, most of them-fasttwins), "Refresh Models" pulls the whole list from the binary — see above.X account diagnostics and review-gated compose handoffs. Comms now has an X page that stores bounded public snapshots, checks profile/search visibility and post reach through the managed browser, captures visible metrics, and routes drafts through Review Hub before opening X for a final manual submission. It does not store X credentials, post automatically, or claim that ordinary low reach proves a shadowban.
Pasting a GitHub repo into Brain capture can now hand it to an agent. A bare repo URL captured in the Brain has always been saved as a link and cloned in the background, but nothing said so and nothing followed. Both capture boxes (the Quick Capture dashboard widget and the Brain → Inbox form) now recognize a repo URL, name the
owner/repothat will be cloned, and offer two opt-in checkboxes: Scan for malware runs the same read-only static audit as the Links tab's Scan button, ending in a CLEAN / CAUTION / DANGEROUS report you open from the link; Study for PortOS ideas queues an agent that reads the clone for implementation ideas worth adopting and files the good ones as issues (or PLAN.md items, whichever tracker this install is configured for). Both preferences stick, so "always scan what I clone" is a one-time tick. The agents only start once the clone actually lands — a failed clone queues nothing — and the study run is clean-room by contract: it never executes anything from the clone, never edits it, reads its LICENSE before proposing anything, and describes techniques in its own words rather than copying code. Re-pasting a repo you already saved re-queues nothing, since there's no new clone to read.Stacker News triage is newest-first and action-oriented. Each community sync is capped to a configurable newest-item count (30 by default), preserves source timestamps for stable ordering, captures direct image posts for Ollama vision analysis, and gives every newest item explicit reply, zap-handoff, moderation-handoff, and analysis actions behind the existing human review gate.
The claim-issue author gate has a "Me + collaborators" setting. Until now the only way to let an agent work on a teammate's issue was "Any author", which also opens the queue to drive-by issues from strangers — so a repo with any real collaborators had to choose between ignoring their issues and claiming everyone's. The new option trusts you plus every account the forge already grants access to (GitHub collaborators, GitLab project members including group-inherited ones), which is the same set that could push the code anyway. It sits between "Filed by me only" (still the default) and "Any author" in the CoS → Schedule author-filter dropdown, in the per-app override, and in the
/do:nextrun drawer, and applies toclaim-issueandclaim-workalike. Neither CLI's--authorflag accepts more than one account, so the gate is applied to the issue listing rather than pushed into the query — both the work detector and the agent prompt resolve the trusted set first, then filter. If that lookup fails (a token without push access can't read the collaborator list), PortOS reports it and retries rather than quietly narrowing back to your own issues or widening to everybody — and because that failure repeats identically on every retry, both the run drawer and the "Run now" toast name the filter and the way out instead of the generic "try again shortly".Changed
Documentation
QUOTA-BURN.md), Three.js procedural 3D models (THREEJS_MODELS.md), Stacker News stewardship (stacker-news.md), and the PortDeck native companion app API contract (COMPANION_APP_API.md) acrossREADME.mdanddocs/README.md. Improved inline JSDoc comments for client and server utility modules.Fixed
agyreporting "We're finishing verifying your account eligibility… please try again shortly" would burn down a whole queue of tasks in about a minute, each in three seconds, with nothing to show for it. A failure the run can attribute to the provider itself now sidelines it for a cooldown scaled to the cause (minutes for a transient block, longer for something needing a human), which is what makes the retries land on a fallback provider instead of repeating the failure; the sideline expires on its own, so the provider comes back without anyone visiting settings. Failures that describe the request rather than the provider — a bad model id, a content refusal, an off-shape response — still never sideline anything, since the provider's other models are fine. And a failure diagnosed only by keyword-matching the agent's own transcript can't sideline a provider either; that takes a structured signal from the provider itself. One shared cooldown policy now answers this for both agent runs and one-shot prompts, which previously disagreed about how long the same failure was worth.