Skip to content

Commit 8eb7b2e

Browse files
committed
Merge remote-tracking branch 'origin/main' into next/issue-2199
# Conflicts: # .changelog/NEXT.md
2 parents 39568af + 8429334 commit 8eb7b2e

34 files changed

Lines changed: 2557 additions & 46 deletions

.changelog/NEXT.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## CoS Agents
44

5+
- **Swarm claim-issue runs now write the completion sentinel so the task actually finishes.** In `/do:next --swarm` mode the orchestrator ships every issue via a PR and never touches the working tree, so it was skipping the `.agent-done` completion sentinel — leaving the CoS task hanging with no summary as if the run never ended. Phase C of the swarm directive now explicitly instructs the orchestrator to write the sentinel (with a run summary of the issues claimed, their PRs, and merge outcomes) after the serialized merges, while still skipping the per-agent `/simplify`/push steps. (`server/services/cosTaskGenerator.js`)
6+
- **[issue-2190] OpenCode Ollama providers now accept any --model flag.** The static `OPENCODE_CONFIG_CONTENT` lacked a models map, so OpenCode rejected every model with "not valid" and the agent sat idle. The config is now built dynamically at spawn time, declaring the provider's configured models (plus the model being run) under `provider.ollama.models` with bare ids — the exact shape OpenCode's `@ai-sdk/openai-compatible` provider validates `--model ollama/<id>` against. Applied across every OpenCode spawn path (CoS CLI/TUI agents, the toolkit CLI/TUI runners, and the light fire-and-collect CLI run), and a migration backfills existing installs' `data/providers.json` so a configured model's map is persisted too. Fixes the TUI eval path for local Ollama models.
7+
- **[issue-2191] TUI agents with zero file changes are now marked failed instead of silently succeeding.** A TUI agent that idled out after showing working indicators but made no uncommitted file changes was previously finalized as `success: idle-complete` — deleting the worktree and any evidence of what happened. Now the idle-complete path checks `git status` in the worktree: if the tree is clean, it finalizes as `failure: idle-no-changes` with the TUI buffer tail as error context. Any uncommitted diff is also captured to `worktree-diff.txt` in the agent archive dir before cleanup, so post-mortems can see what (if anything) was left behind.
8+
- **Scheduled improvement tasks with no configured model now follow the active provider's default instead of a stale hardcoded one.** When a self-improvement or managed-app improvement task had no explicit model in its schedule config, the generator pinned a hardcoded `claude-opus-4-5-20251101` into the task — a model that had since dropped out of the `claude-code-tui` provider config, so the scheduler spawned `claude --model claude-opus-4-5-20251101` even after the provider default moved to Opus 4.8. Since a per-task model pin is honored above the provider default (it's passed straight through to the CLI), that stale literal overrode the configured default on every scheduled run. The generator no longer pins a literal: with no configured model it leaves the task's model unset, so model selection resolves the active provider's tier/default model at spawn time and always uses a model the provider actually lists. An explicit per-task or per-schedule model pin still wins as before. (`server/services/cosTaskGenerator.js`)
59
- **An explicit per-task model pin is now always honored — no more silent, expensive downgrades.** When you set a specific model on a CoS task, PortOS validated it against the provider's `models` list and, on any mismatch, *silently fell back to the provider's default (heaviest) model*. Because `claude-code-tui` lists the dated id `claude-haiku-4-5-20251001` while `claude-code` lists the undated `claude-haiku-4-5`, picking "haiku" on the TUI provider quietly ran **Opus** instead. Explicit user-specified models are now passed through to the CLI even when they're not in the convenience list (the CLIs accept any valid id; a genuine typo surfaces as a provider error rather than a costly silent substitution). Auto-selected models still fall back to the tier default as before.
610
- **Worktree tasks now default to opening a PR instead of auto-merging to your default branch.** A CoS task run in an isolated worktree with "Open PR" off merges its commits straight back to the main development branch on completion — fine for trusted agents, dangerous for unreviewed local-model work. Creating a worktree task now defaults "Open PR" on (in the UI, enabling Worktree checks it; via the API, a worktree user-task without an explicit `openPR` defaults to `true`). You can still turn it off per-task for a quick auto-merge, and internal automation (autopilot, self-improvement) keeps its existing auto-merge behavior so nothing starts silently waiting on a human to merge a PR.
711

@@ -12,6 +16,7 @@
1216

1317
- **[issue-2165] Editorial review now catches recognizable "AI slop" writing tells automatically.** Four new deterministic editorial checks join the existing 68: a tiered banned-word scan (hard-bans like "delve"/"myriad"/"utilize"; suspicious words like "robust"/"seamless" only flagged when 3+ cluster in one paragraph), fiction-specific AI-tell idioms ("a sense of dread", "couldn't help but", "eyes widened", "a wave of relief washed over him"), rhetorical structural tics ("not just X, but Y", runs of "did not [verb]", punchy three-sentence-in-a-row bursts like "Fast. Precise. Deadly."), and structural burstiness (em-dash overuse, essay-style transition-word openers, mechanically uniform paragraph lengths). Each surfaces as an editorial finding with the exact offending quote, same as the existing prose checks — configurable thresholds and house-style allowlists included.
1418
- **[issue-2166] Editorial review now flags an issue whose narrative voice statistically drifts from the rest of the series.** A new deterministic `style.voice-drift` check measures each issue's prose fingerprint — sentence-length rhythm (mean, spread, variation), fragment and long-sentence rates, paragraph shape, dialogue ratio, em-dash rate, abstract-noun and simile density, and how often sentences open the same way — then compares every issue against the series average and flags the outliers (e.g. "Issue 7's sentence-length variation is 0.18 vs the series 0.41 — prose has gone metronomic"). It's the cheap, deterministic complement to the existing LLM voice-consistency check: no model cost, and it names the exact issue and metric that drifted. You can tune the sensitivity (σ threshold), and optionally declare vocabulary "wells" — register categories like `trade: forge, anvil; body: pulse, sinew` — so it also flags an issue that drops a register the series leans on. It stays off until at least 4 issues are drafted (with fewer, the statistics are too small to distinguish real drift from a normal series).
19+
- **[issue-2168] A new "adversarial cuts" editorial check and mechanical applier tightens prose by identifying what a ruthless editor would cut.** The `prose.adversarial-cuts` check asks an LLM editor persona to cut 8–12% of the text as 10–20 passages, classifying each cut by type: FAT (purple prose), REDUNDANT (repeated information), OVER-EXPLAIN (spelling out what's clear), GENERIC (stock phrases), TELL (emotion-telling after showing), or STRUCTURAL (scenes that don't advance the story). Each cut lands as a manuscriptReview finding with the exact verbatim quote. The Manuscript Editor gains an "Apply safe cuts" action that batch-removes OVER-EXPLAIN and REDUNDANT cuts mechanically — exact string match with whitespace-normalized fallback, refusing ambiguous or too-short quotes — through the same snapshotted stage-write path fixes use (full undo via version history). The check also reports `fat_percentage`, `tightest_passage` (protected), and `loosest_passage` for a quick health read.
1520

1621
## Human Activity Tracking
1722

@@ -36,10 +41,13 @@
3641

3742
## Media
3843

44+
- **[issue-2196] Image-to-Video sizing now accepts custom width/height inputs.** The Video Gen page's resolution picker showed only preset aspect ratios — users who wanted an exact I2V size had to set it via URL params, remix, or wait for the auto-snap from an uploaded image. The resolution field now includes editable width and height inputs directly below the preset dropdown, accepting any value (the server floors to a 64-pixel grid and enforces the FFLF pixel budget as before). Picking a preset, typing a custom value, or auto-snapping to a source image all update the same state, and the "custom" option appears in the dropdown whenever your dimensions don't match a preset.
3945
- **Installing a video LoRA from HuggingFace now shows live download progress.** The LoRA Manager's "Install video LoRA from HuggingFace" form (and the curated video quick-install buttons) used to freeze on a static "Downloading…" label with no indication of how far along a multi-GB LoRA was — unlike image-model downloads, which show a percentage. The install now streams byte-level progress over SSE: the button shows `Downloading 42%` and a progress bar with `received / total` bytes fills as it downloads (an indeterminate pulse when the server can't read a Content-Length). Errors — including the "couldn't detect the model family" case that offers the inline "Install as LTX-Video" retry — still surface exactly as before.
4046
- **The LoRA Manager's Installed list is now grouped by Video / Image.** The curated "Video LoRAs" suggestion cards sat directly above a single flat "Installed" grid, so installed *image* LoRAs looked like they belonged to the video section. The Installed section is now visually divided from the suggestions and, in the "All" view, splits into labeled **Video LoRAs** / **Image LoRAs** subsections (mirroring the suggestion panel) so it's obvious which renders each LoRA applies to. The Image/Video filter still collapses it to a single scoped list.
4147
- **[issue-2036] Sketch & Annotation Canvas (phase 2): re-render an image guided by your annotations.** The annotate page (`/media/annotate/:mediaKey`) gained a "Re-render" action: draw over a generated image, then feed your markup back through local img2img so the marks reshape the render. A confirmation dialog names the exact local model it will run (no surprise AI calls) and lets you add an optional prompt and tune how much to change before it starts; the new render is queued and appears in Media History. Requires a local FLUX img2img runner — the action explains when one isn't available. Phase 3 (blank-canvas storyboard) remains open on #2036.
4248
- **[issue-2199] LoRA downloads are now verified for corruption before install.** A truncated or corrupt `.safetensors` download from Civitai or HuggingFace used to install silently and then render garbage ("mosaic") output at generate time. PortOS now checks every freshly downloaded LoRA's structure (safetensors header + size) and, when Civitai provides a SHA-256 for the file, compares it against the bytes on disk. On any mismatch the partial file is deleted and the install fails with a clear error so you re-download instead of training or rendering against corrupt weights.
49+
- **[issue-2196] Video Gen now takes an exact custom Width×Height.** The `/media/video` page only offered a 9-item resolution preset dropdown, so a size the presets didn't cover was only reachable via URL params, remix/continue, or aspect-snap on upload. Two numeric Width and Height inputs now sit beside the preset select — type any dimensions and the render uses them (the server still rounds each down to a multiple of 64 and enforces the per-tier pixel budget). Uploading a source image still auto-snaps the size to the image's aspect ratio by default, until you take the dimensions into your own hands.
50+
- **[issue-2197] First-run model downloads now show GB progress instead of raw text.** The video and image generation handlers now parse byte sizes from `DOWNLOAD:` markers and tqdm progress bars, displaying a formatted "Downloading model · first run · X.X GB / Y.Y GB" message instead of the raw protocol output. Progress frames include `downloadedBytes`/`totalBytes` numeric fields so the UI can build download progress bars. Phase tracking (`isDownloading`, `currentPhase`) ensures the formatting only applies during actual download phases.
4351

4452
## MeatSpace POST
4553

@@ -78,3 +86,4 @@
7886
## AI Providers
7987

8088
- **A wrong local-model default now self-corrects instead of failing the run.** When an Ollama or LM Studio provider is pointed at a model that isn't actually installed (a stale or mis-typed `defaultModel`, or a model you've since deleted), an API call against it returned a dead-end "model not found" — the user had to notice, open settings, and pick a real model by hand. The shared `callProviderAISimple` path now catches that error, lists the backend's actually-installed models, picks the best fit (the editorial recommender's family/size ranking, skipping embedding-only weights), repoints the provider's `defaultModel` (and any tier slot pointing at a missing model), tells the user via a low-priority notification + activity log, and retries the call once — so the run succeeds and the bad config is fixed going forward. Remote/API and CLI providers are left untouched, since their installed models can't be enumerated. (`server/services/localModelHealing.js`, `server/lib/aiProvider.js`)
89+
- **[issue-2192] OpenCode TUI prompt paste no longer gets silently swallowed during slow startup.** When pasting a task prompt into a non-Claude TUI (OpenCode, Codex, Gemini), the paste could be sent while the TUI was still initializing — the prompt never appeared in the input buffer and the agent sat idle for 185s before being reaped as a false success. The TUI paste-commit path now verifies the pasted text actually rendered in the buffer (extracting a unique prefix from the prompt and checking for it after the paste-marker or fallback timeout). If verification fails, the paste is retried with exponential backoff (800ms, 1.6s, 3.2s); after 3 attempts the agent is finalized as a startup failure with a clear `paste-not-rendered` reason instead of a misleading `idle-complete` success. Claude TUIs already had positive input-ready detection and are unaffected. (`server/lib/tuiHandshake.js`, `server/services/agentTuiSpawning.js`)

client/src/components/apps/tabs/GitTab.jsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ export default function GitTab({ appId: _appId, appName, repoPath }) {
328328
};
329329

330330
const mergedBranchCount = remoteBranches.filter(rb => rb.merged && !rb.isDefault).length;
331+
const localMergedCount = branches.filter(b => b.merged).length;
331332

332333
const handleCleanupMerged = async () => {
333334
if (!repoPath || cleaningUp) return;
@@ -552,7 +553,39 @@ export default function GitTab({ appId: _appId, appName, repoPath }) {
552553

553554
{/* Local Branches */}
554555
<div className="bg-port-card border border-port-border rounded-xl p-4">
555-
<h3 className="text-sm font-medium text-gray-400 mb-3">Local Branches</h3>
556+
<div className="flex flex-wrap items-center justify-between gap-2 mb-3">
557+
<h3 className="text-sm font-medium text-gray-400">Local Branches</h3>
558+
<div className="flex flex-wrap items-center gap-2">
559+
{localMergedCount > 0 && !cleanupConfirm && (
560+
<button
561+
onClick={() => setCleanupConfirm('local')}
562+
disabled={cleaningUp}
563+
className={`flex items-center gap-1 text-xs ${touchBtnCls} text-port-warning hover:text-port-error disabled:opacity-50`}
564+
>
565+
<Trash2 size={12} />
566+
{cleaningUp ? 'Cleaning...' : `Clean ${localMergedCount} merged`}
567+
</button>
568+
)}
569+
{cleanupConfirm === 'local' && (
570+
<div className="flex flex-wrap items-center gap-1">
571+
<button
572+
onClick={handleCleanupMerged}
573+
disabled={cleaningUp}
574+
title="Deletes merged branches both locally and on the remote"
575+
className={`px-2 py-1 ${touchBtnCls} text-xs bg-port-error/20 text-port-error rounded hover:bg-port-error/30 disabled:opacity-50`}
576+
>
577+
{cleaningUp ? 'Deleting...' : 'Delete all merged (local + remote)'}
578+
</button>
579+
<button
580+
onClick={() => setCleanupConfirm(false)}
581+
className={`px-2 py-1 ${touchBtnCls} text-xs text-gray-400 hover:text-white`}
582+
>
583+
Cancel
584+
</button>
585+
</div>
586+
)}
587+
</div>
588+
</div>
556589
<div className="space-y-2 max-h-64 overflow-auto">
557590
{branches.map((branch) => (
558591
<div
@@ -568,6 +601,12 @@ export default function GitTab({ appId: _appId, appName, repoPath }) {
568601
<span className={`text-sm truncate ${branch.current ? 'text-port-accent font-medium' : 'text-gray-300'}`}>
569602
{branch.name}
570603
</span>
604+
{branch.merged && (
605+
<span className="flex items-center gap-1 text-xs text-port-success px-1.5 py-0.5 bg-port-success/10 rounded shrink-0">
606+
<GitMerge size={10} />
607+
merged
608+
</span>
609+
)}
571610
{branch.tracking && (
572611
<span className="text-xs text-gray-500 shrink-0">
573612
{branch.ahead > 0 && <span className="text-port-success">+{branch.ahead}</span>}
@@ -670,22 +709,23 @@ export default function GitTab({ appId: _appId, appName, repoPath }) {
670709
<div className="flex flex-wrap items-center gap-2">
671710
{mergedBranchCount > 0 && !cleanupConfirm && (
672711
<button
673-
onClick={() => setCleanupConfirm(true)}
712+
onClick={() => setCleanupConfirm('remote')}
674713
disabled={cleaningUp}
675714
className={`flex items-center gap-1 text-xs ${touchBtnCls} text-port-warning hover:text-port-error disabled:opacity-50`}
676715
>
677716
<Trash2 size={12} />
678717
{cleaningUp ? 'Cleaning...' : `Clean ${mergedBranchCount} merged`}
679718
</button>
680719
)}
681-
{cleanupConfirm && (
720+
{cleanupConfirm === 'remote' && (
682721
<div className="flex flex-wrap items-center gap-1">
683722
<button
684723
onClick={handleCleanupMerged}
685724
disabled={cleaningUp}
725+
title="Deletes merged branches both locally and on the remote"
686726
className={`px-2 py-1 ${touchBtnCls} text-xs bg-port-error/20 text-port-error rounded hover:bg-port-error/30 disabled:opacity-50`}
687727
>
688-
{cleaningUp ? 'Deleting...' : `Delete ${mergedBranchCount} merged`}
728+
{cleaningUp ? 'Deleting...' : 'Delete all merged (local + remote)'}
689729
</button>
690730
<button
691731
onClick={() => setCleanupConfirm(false)}

0 commit comments

Comments
 (0)