release: 0.1.5 (#134)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-30 19:23:41 +08:00
1 parent ba0860f8d1
commit 6664edf890
44 files changed
+596 -151

No files matched your search

@@ -0,0 +1,15 @@
# Isolated Agent tuning pipeline
The Web App now includes a runnable example that coordinates Agent creation, Benchmark construction, and score-driven Agent optimization without sharing private evaluation context between phases.
## Web App
The new draft-screen example launches each phase in an independent Penguin CLI Session derived from the active Project environment. Its Benchmark uses hidden context-to-action mappings so the optimizer can demonstrate measurable improvement from score-linked feedback.
Benchmark construction now uses provisional Pilot evaluations to adjust one difficulty dimension at a time before freezing and recording the Formal Baseline.
## Skills
The Agent creation, Benchmark design, evaluation, and optimization Skills now define clearer ownership and access boundaries. Benchmark builders and optimizers delegate each Case run through an explicit Evaluator request protocol, while private Rubrics and Gold answers remain confined to the evaluation worker.
Benchmark design now separates mutable Pilot calibration from the frozen Formal Baseline and prevents Rubric-only score reductions around observed answers.
@@ -0,0 +1,21 @@
# Images reach every input: mid-run steering, goal objectives, and no dead action button
Steering — the message you send to an agent that is already running, delivered between turns as a `[user_steering]` user message — used to be text and nothing else. That was a limitation of the channel, not of the model: the very moment an image is most useful is mid-run, when the agent has just produced the wrong layout and the fastest correction is a screenshot. Steering now carries images the way a prompt does, and an image with no caption is a complete steering message on its own.
Core queues the images with the text — `session.steer(input)` takes an OmniMessage list, the same shape `session.run` takes a prompt in — and delivers them as ordinary user image messages directly behind the `[user_steering]` block — the same shape a prompt uses, so the LLM client, the Trace, and replay all already know what to do with them. On a model without vision they fold into `[attached image: <path>]` lines **inside** the block, exactly as a prompt's images fold, because the block has to remain the whole text: a line appended after the closing tag would cost the message its steering identity and every render layer would read it as a new Task. A scratchpad that cannot be written to ends the run — the picture usually arrives *because* the run is going the wrong way, so carrying on without it would spend the rest of the task heading further that way.
The renderers follow the same grouping rule they already apply to a prompt's images — everything up to the next non-image message belongs to the message in front of it. In the web app the images land inside the steering chip instead of becoming bubbles of their own, and neither the chat stream nor the Trace timeline opens a new Task for them; an images-only prompt sent later is still a genuine new turn.
## A goal can be stated with a picture
"Make the page match this mockup" is a goal, and a screenshot states it better than a paragraph — but a goal objective used to reject images outright. It accepts them now, with one deliberate difference from every other input: they are folded to scratchpad paths **whatever the model's vision**. A goal objective is re-injected as the text of every round's `[goal]` block, so an image cannot ride along as an image. Sending it in round 1 alone would leave every later round pointing at something compaction has since removed — a silent failure, since the objective still *reads* correct. As a path line it survives every round and every compaction, and the model spends tokens on it only when it actually looks. Text is still required: a picture alone states no objective, so a text-less goal input is refused.
In the chat, round 1 shows the attachments in full under its bubble and later rounds collapse them into a one-line chip that expands on click — they really are in every round's input, but a twenty-round goal should not repeat the same picture twenty times.
The three input paths now share one fold, bound once per Session, with each path deciding for itself whether to apply it: a prompt and a steering message fold only without vision, a goal objective always. The gate used to be implicit — "was the scratchpad directory configured" stood in for "does the model lack vision" — which worked only while there was exactly one rule to encode.
## The mid-run button stops being able to strand you
While a task runs, the composer's single action button flips from Stop to its send face as soon as the draft has any content. Steer mode could not carry every draft, so a draft it could not carry — an image with no text, back when images were not steerable — produced a **permanently disabled** send button that had also displaced Stop: the message could not be sent, and the run could not be stopped from the composer either. The only exit was deleting the attachment.
With images steerable, that specific draft simply steers. What steering still cannot carry — selected skills with no text or image (a `[use_skills]` block is task-level setup, not something to hand a turn already under way), or an `@` handoff target — now falls back to the **follow-up queue** for that one send: the button stays live and labeled as the queue action, the whole draft goes out as one message held server-side until the run finishes, and an `@` handoff still opens its new chat directly. Stop is what's left over: the button's face whenever neither channel will take what is in the box — an empty composer, but also a goal objective, any draft at all once the model key has been rejected, and a staged `/model` fork the running session is still blocking. "There is something in the box" and "this send has somewhere to go" are different questions, and Stop belongs to the second one.
@@ -0,0 +1,7 @@
# Sites: the 0.1.4 release post
A release post for **0.1.4** in both languages, covering the batch's three themes — Windows becoming a first-class install, goal mode looping Tasks until an objective is actually reached rather than merely replied to, and the agents panel turning subagent fan-outs into a live call graph.
The post is deliberate about limits rather than quiet on them: the Windows package is x64-only for now, `input_command`'s Ctrl-C hard-kills the command tree instead of interrupting the foreground command, and upgrading on Windows means re-running the installer because in-place `penguin update` still refuses there.
A capture script for the post's screenshots joins the existing ones, staging its output in the gitignored blog-assets directory for upload to the sibling community repo rather than committing images into this clone.
@@ -0,0 +1,29 @@
# Windows: the package bundles its own bash, so the Agent's shell no longer depends on the machine
`penguin-win32-x64.zip` now ships **MinGit** under `git/`, and `exec_command` uses it when the machine has no Git for Windows of its own.
## Why
The Windows shell was whatever happened to be installed: `bash` if the user had Git for Windows, otherwise PowerShell. That made the same Agent running the same Skill behave differently on two Windows machines, and the degradation was silent — a Skill written for a POSIX shell does not fail loudly under PowerShell, it fails strangely. The sharpest case: `curl -fsSL <url>` under Windows PowerShell 5.1 resolves the built-in `curl` → `Invoke-WebRequest` alias and returns a cmdlet parameter-binding error, which a model recovers from far less easily than "command not found".
## What is bundled
MinGit's `usr/bin/sh.exe` **is GNU bash** — the minimal Git for Windows build installs bash under the name `sh`. So the bundle costs 37MB compressed (~91MB installed) for a real bash, roughly sixty core utilities, and `git.exe`, rather than the ~350MB extracted that full PortableGit would have needed for complete parity.
No PATH plumbing is involved. MinGit's `etc/profile` defaults to `MSYS2_PATH_TYPE=inherit`, so a login shell (`-lc`, unchanged) yields `/mingw64/bin:/usr/local/bin:/usr/bin:/bin:<inherited Windows PATH>` — bundled tools and git first, the Windows PATH still behind them. That is why MinGit carrying no `curl` or `tar` does not matter: System32's `curl.exe` and `tar.exe` keep resolving, and inside bash they are the real binaries rather than PowerShell aliases.
## Resolution order
`PENGUIN_SHELL` → `bash` on PATH → **bundled** → `pwsh` → `powershell`.
The bundle sits deliberately *after* the PATH probe: a user's own Git for Windows carries the complete MSYS userland (curl, tar, less, perl …) while MinGit carries about sixty tools, so when both exist theirs is the better shell. The bundle is the floor, not the preference. `pwsh` / `powershell` stay reachable for npm installs, which bundle nothing. The resolved shell is reported to the model as `bash`, since that is what it is and what the Skill ecosystem targets.
The installer treats `git/` like the other payload directories — replaced on upgrade, never touching the data directory — and the launcher shims advertise the path as `PENGUIN_BUNDLED_SHELL`.
## Licensing
MinGit is GPLv2, so a new root `THIRD-PARTY-NOTICES.md` records both bundled components — the Node runtime and MinGit — with where each one's license text sits inside the bundle and where its corresponding source lives. The Git for Windows release is pinned to one exact tag so the notice names a single version, and the bundled bytes are the unmodified published asset.
## Cost, stated plainly
The Windows download roughly doubles (~65MB → ~100MB) and the install grows about 91MB; `Expand-Archive` on PowerShell 5.1 has 368 more files to unpack, so a fresh install is noticeably slower. The bundle also means a *hybrid* environment — MSYS coreutils beside native node and git — not a POSIX one, so MSYS path translation remains a thing to watch. `MSYS_NO_PATHCONV` / `MSYS2_ARG_CONV_EXCL` are deliberately left unset: matching real Git Bash semantics is the least surprising default, and diverging would make the bundled shell behave unlike the PATH one.
@@ -0,0 +1,11 @@
# Web App and CLI: duration and byte abbreviations carry into the next unit
Two display helpers chose a unit — or split a value into minutes and seconds — from the raw input while printing a rounded one, so a duration could read `1m60s` and a file size `1024KB`. Both now round before deciding what to print.
## Durations no longer print a 60-second remainder
`humanizeDuration` floored the minutes but rounded the seconds remainder, two opposite rounding directions applied to one split. A remainder in `[59.5, 60)` therefore rounded up to 60 without the minute following it: 119.7s rendered as `1m60s` instead of `2m0s`, and 3599.7s as `59m60s`. Compact mode had the same mismatch one unit down, picking the sub-minute branch from the unrounded value and then printing `Math.round(59.6)` as `60s` rather than `1m0s`. Every whole-second form now rounds the total first and splits that integer, so the minutes and the remainder always come from the same number; sub-minute values outside compact mode keep their tenths and are unaffected. The fix lands in both copies — `packages/web/src/lib/format.ts` and the CLI's `packages/cli/src/render.ts`, whose abbreviations are deliberately identical — so the Web App and the `[stats]` line cannot drift apart. Visible wherever a settled duration is shown: benchmark evaluation, case and run timings, Trace turn durations, tool-call cards, the task statistics footer, and the agent topology view.
## Byte counts no longer print an out-of-range unit
`formatBytes` selected the unit by comparing the raw byte count against the magnitude thresholds, but rendered the value through the shared one-decimal rounding. A count just under a boundary was rounded past it after the unit had already been fixed, so 1048570 bytes printed as `1024KB` instead of `1MB`, and the MB/GB boundary behaved the same way. The unit is now chosen from the rounded value. Visible on Trace file sizes and in the workspace file browser.
+48
View File
@@ -0,0 +1,48 @@
# Web App: config forms stop inviting saved logins, panels and pages stop growing scrollbars, blank replies and duplicated outcomes disappear, and Benchmark costs follow the selected currency
Four rendering and input-handling fixes: the browser stops filling account credentials into model configuration, a closed docked panel stops painting a stray 1px divider next to the open one, a provider-signature message that carries no text stops drawing an empty reply bubble, and Benchmark costs use the display currency selected in settings.
## Browser autofill no longer drops account credentials into model configuration
Opening the model dialog could hand the browser's saved login straight into the configuration: the account password landed in the API-key box, the username in whichever field sat above it. Two causes, both of them the browser doing what it thinks is helpful:
- The dialog's fields belong to no `<form>` element — the app has exactly one real form, on the login page — so the browser groups the page's unowned fields into a synthetic one and picks a "username" box on its own.
- The API-key box already declared `autocomplete="off"`, which **Chrome and Safari ignore on a password field**. They offer the saved login anyway.
Every shared form control now opts out unless its caller declares a real credential role. Fields go out as `autocomplete="off"`, and an opted-out *secret* field as `autocomplete="new-password"` — the one value password managers read as "not the account password" — with the manager-extension opt-outs (1Password, LastPass, Bitwarden, Dashlane) alongside, since those read their own attributes rather than `autocomplete`. Nothing in the app's forms wanted autofill in the first place: they hold API keys, model ids, URLs, directories and prices.
The login page and the password dialogs are untouched — they declare `username` / `current-password` / `new-password`, and a declared role passes through as the caller's decision, so saving and filling a real password keeps working.
The policy lives in the shared `Input` / `Textarea` rather than at each call site, so it also covers the pages that were never reported (Agents, skills, schedules, the vault); the handful of raw inputs that bypass those components — the model and skills menu search boxes, the goal budget, the Workspace path editor — carry the same opt-out explicitly.
## The closed docked panel no longer paints a stray divider
Opening the Workspace files panel showed what looked like a second, empty panel wedged between the conversation and the files: a hairline, a gap, then the real divider.
Both docked panels (Workspace files, agents) stay mounted while closed — the width transition needs the node — collapsed to a zero-width clipping window. Under border-box sizing their left border still painted its 1px there, so the closed agents panel left a line stranded beside the open one, with the open panel's resize gutter as the gap between them. With both panels closed, the two leftover lines stacked at the window's right edge.
The divider now belongs to the open state only, so a closed panel takes no width at all and exactly one divider separates the conversation from whichever panel is open.
## No more empty reply bubble after a thinking segment
An empty `assistant:` bubble sometimes appeared right after the model finished thinking — no text, just a blank block taking up a line.
It was not a stray empty string. Core deliberately emits a **complete text or thinking message with an empty body** when a provider attaches an opaque `fidelity` payload to an otherwise empty part — a Gemini `thoughtSignature` on a text part, a GPT-5 `fidelity.phase` segment marker, GPT-5's encrypted reasoning on the thinking side. That message has to exist, or the signature is lost from history and the next request goes out without it. It simply has nothing to display, and it lands at the thinking→text boundary, which is why the gap showed up after thinking and only on some providers and some turns.
A blank body now produces no item at all, on both paths that can deliver one. When the message arrives on its own — a history rebuild, or joining a stream mid-flight — it is skipped rather than appended. When a whitespace-only segment actually streamed (core opens a text segment on the first *truthy* delta, and `"\n\n"` is truthy), the in-flight fragment is discarded rather than settled in place, so a live view and a reloaded page render the same thing instead of the blank bubble disappearing on refresh. Whitespace-only counts as empty throughout, matching what the reply-copy path already treated as nothing.
## Benchmark costs follow the selected display currency
Benchmark costs used to stay in USD after the display currency was switched to CNY. The page called the shared money formatter without the selected currency, so its USD default remained active while the model library, Cost center, chat, and Trace views followed the setting.
The cost trend axis and tooltip, evaluation totals, case totals, and individual run costs now all receive the selected display currency. Score and duration formatting are unchanged.
## The tool card row states an outcome once
The collapsed tool card said the same thing twice: the status icon on the left already distinguishes done from failed from aborted, and a pill to the right of the duration spelled the word out again. Both pills are gone — the call's and the output's — leaving the icon, with its title and accessible name, as the single source.
## A page can no longer grow the document into a second scrollbar
With a second Agent below a long session list, the Traces page and the Agent settings page grew the **document**: a second scrollbar appeared, and dragging it pulled the whole app shell up, leaving blank space at the top. Both import controls keep their file input visually hidden but Tab-focusable, which means `position: absolute`, and neither scroller was a containing block — so nothing clipped those boxes, and their position past the fold became document-level overflow. One Agent hid it entirely: that control sits at the top of the tree.
The rule now lives in one place instead of in each scroller — every scrollable container is its own containing block, declared once in the stylesheet — and the visually-hidden file input became a shared control carrying its own containing block wherever it is dropped. An end-to-end sweep guards the invariant across the pages: with two Agents and a list long enough to push the second past the fold, no page may grow the document.
@@ -0,0 +1,29 @@
# Web App: file attachments in the composer, and `/agent` in place of the `@` mention
The composer gains a second upload entry and loses its `@` trigger. A message can now carry arbitrary files the way it already carried pasted images, and the two commands that move a conversation elsewhere — `/agent` and `/model` — stage their pick as a chip instead of acting on it, so the message that opens the new conversation is the one the user meant to write.
## File attachments
The "+" menu holds a paperclip entry directly below image upload: any file type, up to 20 files per message, 10MB each and 12MB decoded in total (an oversize pick is refused in the browser before it is read, so nothing is uploaded to earn the rejection; the server answers 413 `file_too_large` / `too_many_files` for anything that gets past that). Selected files show as removable chips beside the image thumbnails, in the order they were picked, and — like images — an attachments-only message with no text is sendable. Goal mode clears and blocks them, since the server takes text-only goal input.
The request body cap those limits sit under is now measured rather than read: it compared `content-length`, which a chunked request simply omits, so a body of any size passed. It counts bytes off the stream instead.
There is no upload endpoint: a chat that is still a draft has no Session to upload to, so the file rides the task request itself as a base64 `data:` URL, exactly like a pasted image. `TaskInputPart` gains a `{type: "file", fileName, dataUrl}` variant; the server validates it without touching the filesystem, then writes the bytes into the Session scratchpad — deleted with the Session, so cleanup stays free — and appends one `[attached file: <absolute path>]` line to the message text. The model opens the file with its ordinary file tools; the bytes never enter the conversation.
That line is appended **after** the user's text rather than prefixed as a `[tag]…[/tag]` block: `[use_skills]` and the origin blocks are parsed at the start of a message, and a second leading block would break that chain. Appending has its own rule for the same reason — `[handoff_from]` and `[model_switch_from]` are only recognized when they are the *whole* message, so a message that is nothing but one of those blocks is skipped and the lines become a message of their own. Without that, a handoff carrying attachments and no text turned its own origin block into unparseable text, and the new conversation opened with the raw marker in a user bubble instead of a banner.
The line's spelling lives in one core module shared with the existing `[attached image: …]` producer, so the two conventions cannot drift; the Web renderer pulls both back out of the body text, images becoming pictures and files an "Attached files" notice — and, like an image line, only when the path actually resolves into the Session scratchpad, so a line typed by hand stays ordinary text instead of rendering inside system chrome.
An attachment keeps the name it was given. Only ASCII outside `[A-Za-z0-9._-]` is rewritten — a space or shell metacharacter inside a path the model is about to paste into a command is a genuine footgun — while everything above ASCII survives, minus Unicode category C (invisible controls, and the bidi overrides behind file-name spoofing): `报告 2026.pdf` lands as `报告-2026.pdf` instead of an anonymous `file.pdf`. Windows hygiene comes with it — trailing dots and spaces dropped, device names prefixed (`con.txt` → `_con.txt`) — and the stem is capped by UTF-8 **bytes** on a character boundary, since a CJK character costs three of them and a character count would let a Chinese name blow the filesystem's real limit.
The scratchpad read endpoint changed to match: its traversal guard was a character whitelist, which a non-ASCII name could not pass. It now runs a structural check (no separators, no control characters, not a relative marker) and confirms it by resolving the path and requiring its parent to be exactly that Session's directory — strictly stronger than the charset was, since it also rejects the shapes a character class misses, such as a Windows drive-relative `C:evil.png`. Responses from that endpoint now carry `X-Content-Type-Options: nosniff`, because arbitrary user-uploaded bytes are served from the App's own origin.
## `/agent`, and both switches staged until send
Typing `@` no longer opens an agent picker; the leading-`@` send-time parsing is gone with it. The same handoff is now reached through `/agent`, which consumes its slash token and opens a picker built like the `/model` one — a search box matching agent id and display name, avatar rows, keyboard navigation, Escape or a click outside to dismiss. Both pickers share one panel component rather than a copy. Like `/model`, `/agent` is offered in an active session only: a draft has no conversation to hand over, and picks its Agent in the draft page's own selector.
Both switch commands stage rather than act. Picking an agent or a model sends nothing: it pins a chip above the text body, the user keeps typing, and Enter (or Send) performs the switch — the handoff into a new chat for that Agent, or the fork of this conversation onto the picked model. Only an empty composer falls back to the default auto-message; selected skills keep riding along in a `[use_skills]` block. The two chips are mutually exclusive with each other and with goal mode, either is dropped with its × or with Backspace at the start of the text, and both are cached with the draft text they belong to — so a session switch or a reload restores the intent whole instead of leaving the text without its chip.
A staged model fork additionally waits for the session to be idle, and says so above the composer: it branches a new session off a Trace that a run or a compaction is still appending to, so firing it mid-run would point the new model at a truncated conversation. Every send path treats a staged model switch the way it already treated the handoff target: it alone makes the composer sendable, it keeps the single action button on Send rather than Stop, and it blocks mid-run steering — a staged switch is not a message to the agent running here.
The `[handoff_from]` origin block told the receiving model that it had been "@-mentioned". The tag and its field lines are a persisted format that old Traces still parse through, so only the prose changed: it is now trigger-agnostic and survives the composer changing how a handoff is started.
@@ -0,0 +1,27 @@
# Cost center: the error table pages back, reads shorter, and stops logging ordinary command exits
Three changes to the error panel, which had become hard to use for the thing it exists for — finding the error that actually matters.
## The table pages back
It showed the newest 20 rows with no way further, so an error from an hour ago was simply unreachable.
A new endpoint returns any window of the history newest-first, with the filtered row count alongside it so the pager knows where the end is. Page 0 costs nothing extra — it is the batch the dashboard response already carries — so only stepping back issues a request.
The endpoint takes the dashboard's **date and agent filter only**. The model filter is deliberately not accepted: it never applied to errors in the first place (HTTP and process errors have no model dimension), so honouring it here would imply a narrowing that the summary above the table does not do. Admin-only visibility of unattributed errors carries over unchanged — a regular member paging back must not start seeing another tenant's login failures.
The pager sits outside the scroll box so it stays reachable without scrolling to the bottom of the rows, and appears only when there is more than one page. A page that fails to load shows its message and keeps the rows already on screen rather than blanking the table.
## Shorter source labels
A row's source now reads `[env] tool_failed:exec_command` instead of `environment · tool_failed:exec_command`. Only `environment` needed shortening — every other source (`http`, `llm`, `session`, `usage`, `title`, `subagent`, `process`, `schedule`) already reads at a glance — so there is one abbreviation rather than a scheme. The "most common error code" statistic uses the same shape, so the two agree.
## A non-zero exit from a command tool is no longer an error
Core mapped every non-zero exit code to a failure, and every failure was recorded. But a non-zero exit is how shell commands return information, not a fault: `grep` exits 1 when nothing matches, `test -f` when the file is absent, `diff` when files differ. Recording those turned the error table into a log of ordinary Agent work, burying real errors and consuming both the row cap and the deduplication window that exist to protect them. The Agent already sees the exit code and adjusts; nothing in it needs a human.
Both command tools are covered, not just the obvious one: the tool that polls a backgrounded command reaches the same exit-code mapping, so excluding only the foreground path would record the same command's non-zero exit depending on how it happened to finish.
What is dropped is keyed on the note the tool appends (`[exit code: N]`), not on the tool's name — the same `failed` status also covers faults no Agent can adjust its way out of: a command killed by a signal (an OOM-killed build, a segfaulting test binary), a spawn failure (nonexistent working directory, EMFILE, an unresolvable shell), a tool timeout, or a missing command-session manager. Those still record, and they are exactly the "needs a human" cases the table exists for.
The exclusion happens where errors are captured rather than where they are queried, so the noise never reaches the table, the row cap, or the deduplication key. The paged route's tenant isolation is now covered by tests as well: an outsider gets a 404, a traversal id gets a 404, and a member paging through offsets never reaches another tenant's rows even with foreign records interleaved into the same pages.
@@ -0,0 +1,27 @@
# Core: a tighter default system prompt, a reply-language rule, and a shared tooling directory
The built-in Agent template is re-read by the model on every turn, and had grown wordy in exactly the sections that repeat most. It loses about a tenth of its words (1087 → 969) with no rule going with them, and two behaviours the prompt never stated are now in it.
## What the trim changed
Personality pins the reply language to the user's own — the tool schema already demanded that of every call description, so the two agreed in practice while only one of them said so. It scopes to prose: code, identifiers and commit messages keep their own conventions. The process/port constraint collapses from two sentences into one, with the same guarantees: don't kill what you didn't start (PenguinHarness's own services included) unless asked, don't take a service port, pick another free port when the one you want is busy.
API auth handling stops being split across two sections. Constraints carried "retry at most once" and Stop rules carried a fourth rule for what to do afterwards; it is now stated once, as the special case of the existing "an error you cannot resolve" rule — retry at most once, then stop calling tools and ask the user to update the key in the Agent's vault or the model settings outside the chat. The facts that made the old wording long are intact: the secret is never pasted into the conversation, and a new key only takes effect in the next conversation, so further retries cannot succeed.
System markers lose about half their words while keeping all four markers and their instructions. File system goes from eight bullets to seven and 262 words to 243: two merges — the workspace-relative-path convention into the `CWD` bullet, another agent's state into the description of your own — against one bullet gained for shared tooling. Suggested workflows already recommended dispatching independent subtasks in parallel; it now says out loud that this is the fast way through a large task. Its Playwright/curl line folds into the Tool use bullet, which keeps both halves the two lines used to carry separately: prefer Playwright when it is installed, otherwise `curl`.
## Tooling installs once
File system gains a convention, and the Skills section loses the "There is no skill tool" filler that explained an absence. It sits in File system rather than Skills because it governs any task that installs a tool, not only a skill run, and it is written as the choice the model actually faces: install into the project's own environment when it has one; otherwise keep the reusable ones — Python virtualenvs, model and package caches — under `<app_data_dir>/agents/<agent_id>/shared_env/<name>/` and reuse them across Sessions.
Node is called out separately, with pnpm preferred **in the project itself** — both halves matter. Its shared store keeps repeated installs from duplicating on disk, and the location is what keeps the install resolvable: `node_modules` resolves from the project upward, so a package placed under the agent directory would be invisible to code running in `CWD`.
`shared_env/` is a prompt-level convention, not a path the code creates: `paths.ts` is untouched, and Agent State snapshots still package `agent_state/` alone, so a virtualenv can never bloat an export. The data-layout tree in the Sessions & Traces documentation lists the directory with that caveat spelled out, and the per-Agent layout in `01-PRINCIPLES.md` records it too.
## The App Data Dir bullet stops contradicting itself
The same section's App Data Dir bullet said the directory "is NOT the task's directory" and that deliverables must never be left there. A temporary Workspace is `<app_data_dir>/agents/<agent_id>/workspaces/tmp-<8hex>` — inside that very tree, and `CWD` for any task that is given one — so the rule broke exactly where it was needed. It now carries the input rule alone (the App Data Dir's contents were not supplied by the user and are never task input) and says plainly that `CWD` may sit inside the tree, that one folder being the task's and the rest not. Where deliverables go was already stated positively by the scratchpad bullet below it, which holds wherever `CWD` points.
## Existing Agents
Nothing migrates and nothing is rewritten. An Agent always runs with its on-disk `agent_state/system_config.yaml` verbatim, so this reaches **newly created** Agents only. An existing Agent adopts it through the settings page's *Restore default configuration* action, which overwrites the whole configuration — custom system prompt, tool list, model and compaction settings, MCP Servers — keeping only `name`, `description` and `version`.
@@ -0,0 +1,21 @@
# The harness's own port stops colliding: out of the Agent's environment, off the dev server
Two places where PenguinHarness's listen port reached somewhere it should not have.
## `PORT` no longer leaks into commands the Agent runs
`penguin web` writes `PORT` and `HOST` into its own `process.env` as the channel to the server module, and the command Environment built child environments straight from `process.env`. Every `exec_command` therefore inherited `PORT=7364` — and `npm run dev`, Vite, Next and most Express templates read `PORT`, so an Agent asked to start a dev server tried to bind **the harness's own port** instead of picking one.
The child environment now drops `PORT`, `HOST`, and the internal `PENGUIN_CLI_ENTRY` / `PENGUIN_WEB_DIST`. Keys are removed rather than blanked, since a program may test `PORT` for presence rather than value. Stripping applies even when the value came from the user's shell (`PORT=3000 penguin web`): it still means "the port PenguinHarness is on", which is precisely the port a spawned server must avoid. The Agent's vault is applied afterwards, so setting `PORT` there deliberately still reaches commands.
`PENGUIN_HOME` and the other user-facing `PENGUIN_*` settings are deliberately kept — an Agent working on PenguinHarness itself may legitimately want the same data root, and that is a configuration decision rather than a leak.
## The development backend moves off 7364
`pnpm dev:server` bound the same port as an installed `penguin web`, and the two routinely run at once. Either the dev server failed to bind, or — quieter and worse — the Vite proxy talked to the **installed** server instead of the one being worked on, so code changes appeared to do nothing.
The development backend now listens on **7368**; 7365, 7366 and 7367 are already the web, landing and docs dev servers. `pnpm dev:web` is unchanged at 7365 and remains the address to open — only what it proxies to has moved. The full port allocation is documented in `core/internal/ports.ts`, which is where a reader looks, even though the dev ports themselves have to be literals in vite configs and package.json scripts.
Overrides still work and now move together: the dev scripts apply `PORT` only when it is unset, and the Vite proxy reads `PORT` with 7368 as its fallback, so changing the backend port carries the proxy with it instead of leaving it pointed at the old one.
The dev *data root* was separated from the installed one for exactly this reason; this finishes the job for the port.
@@ -0,0 +1,23 @@
# Core: what a failed request does next
## Every failure but a rejected credential now retries
The engine used to reconnect on `timeout` and `malformed` only. `failed` — the bucket for "the classifier did not judge this transient" — ended the turn.
The trouble is what that classifier is: an allowlist of known error codes, HTTP statuses and message vocabulary. A gateway that words a transient fault its own way (`Upstream HTTP/2 stream failed`, say) falls straight through it and lands in `failed`. Retrying a genuinely permanent error costs the backoff ladder and then ends the same way; aborting a transient one destroys the turn. So the policy widened: **`failed` retries too, and `auth` is the only LLM status that stops the run** — a rejected credential cannot be retried into working. This changes the policy, not the taxonomy: a `failed` request is still reported as `failed` on its `request_end` and in the Cost center, not relabelled a timeout.
The retry is visible, because a retry the user cannot see is a stalled session with no explanation and no way out: `failed` renders exactly like the other two — the Web App's countdown with "retry now" and "give up", the CLI's `[retry]` line — and the attempt number keeps counting across a mixed ladder instead of restarting at #1.
Compaction retries the same set. It used to stop on `failed` while the turn loop retried it, which had the trade backwards — a compaction that gives up keeps the full context, so the next request re-triggers it against the same wall with less headroom. What stays narrower is the budget: compaction runs on its own shorter cap (3 retries) rather than the turn loop's 5.
Observability follows the policy. A `failed` the ladder recovered from is no longer filed as an operator-facing incident — it records as expected under `llm_failed_retried`, while an exhausted ladder stays unexpected under `llm_failed`. Authentication failures move to their own `llm_auth` code: they used to share the `llm_failed` deduplication bucket, so a genuine credential failure landing just behind a recovered blip was dropped outright. Trace segmentation carried a second copy of the old rule and read a retried `failed` as a turn boundary, smearing one turn's Tokens, duration and TPS across two Tasks; it now follows the engine's loop.
## A provider that ignores Stop can no longer wedge a Session
Pressing Stop mid-request could leave a Session running forever — no way to send, no way to compact, and a second Stop did nothing. Short of restarting, the Session was finished. Reported against Kimi.
The request loop already guarded half of this. Interrupting while the loop sits suspended at a `yield` — the common case, where the engine is blocked waiting for a tool approval — is caught by the abort check it runs before pulling from upstream again. Interrupting while the loop is **blocked inside the pull itself** was not covered: the provider's stream promise never settled, so nothing came back to check. The idle timer could not rescue it either, because by then the request's internal AbortController was already aborted and the timer's own abort was a no-op. Nothing was left to end the run.
The pull is now raced against a promise that settles the moment that controller aborts — whether the trigger was the user or the idle timer — so the request always closes out regardless of what upstream does. The abandoned stream is asked to close on a best-effort basis and deliberately not waited on: a stream that ignored its abort signal may well ignore that too. The terminal state is classified by which trigger fired, so Stop still ends the run as interrupted and an idle stall still ends it as a timeout that reconnects.
This is provider-agnostic on purpose. Whether a request terminates when its signal fires is the harness's guarantee to keep, not something to inherit from whichever SDK happens to be underneath.
@@ -0,0 +1,7 @@
# Multi-platform offline installer bundles
- Release builds now wrap each Windows, Linux and macOS platform archive with its checksum and native installer, producing five self-contained offline bundles.
- `install.sh` and `install.ps1` can install a verified local Release archive without network access while preserving their existing online behavior.
- POSIX offline bundles use a dedicated entry point that passes their payload explicitly, so the online installer never trusts archives discovered beside a temporary script.
- Platform packages carry a target manifest, allowing explicit local archive paths to be renamed without relying on the filename for compatibility checks.
- Windows offline bundles include `install.cmd` as a double-click entry point; Linux and macOS bundles keep `install.sh` executable.
@@ -0,0 +1,17 @@
# Web App: the Session's elapsed time is taken from Trace timestamps, so a reload stops restarting it
The elapsed chip in the chat header restarted from zero whenever a running Session was reloaded, and the figure it eventually settled on depended on the browser clock. Both came from the same root: the running Task was the one part of the number not derived from the Trace.
## Reloading mid-run resumes the chip instead of restarting it
The chip renders the settled cross-Task total plus the running Task's wall clock so far, and that second half ticks from a local-clock anchor. A history rebuild feeds every replayed message the same "now", so the anchor was stamped with the instant the page loaded: a Task that had already been running for five minutes was treated as having just started, and the chip dropped back to the settled total and climbed from zero again.
The anchor is now back-dated, once the replay finishes, by the elapsed already behind the Task, so the ticking value resumes where it left off. That elapsed is measured entirely in server time and only then applied to the local clock, which keeps a client/server clock offset out of the result and lets the chip keep ticking smoothly. A live stream is unaffected: it pushes one message at a time with the real current clock, where the elapsed is still zero when the Task opens.
It is measured against the server's own clock at read time, taken from the messages response's HTTP `Date` header, rather than against the Trace's last entry. That distinction is what makes an event still in flight count: while a tool is executing, a Request is streaming or a compaction is running, nothing has been appended to the Trace since it began, so a reload measuring only what the Trace records would show none of the time that event has already taken. The Trace's own span remains the floor — it is the fallback when no `Date` header comes back, and a cached or rewritten header can only be older than the true present, so it can under-report but never overshoot. The header carries whole seconds, which is finer than a chip that ticks in whole seconds can show.
## A settled round reads the same live and replayed
A round's duration is the span from its first message to its last non-compaction `request_end`. One case escaped that: a round with no `request_end` at all — interrupted before its first Request even ran — was measured with the local clock when it happened to be watched live, and with the message span when it was replayed later. The same round therefore showed one number before a refresh and a different one after, with idle-detection and mid-join latency folded into the first. It now settles to its message span on both paths, which the interrupting abort's own timestamp still bounds.
The local clock now reaches the elapsed figure in exactly one place — animating the Task in flight — and never a settled one. The meaning of the statistic is unchanged: still the sum of each Task's wall clock. Compaction handling is unchanged too, with a mid-round compaction inside the span and a post-round one outside it, and the chat page keeps its deliberate difference from the Trace page's total.
@@ -0,0 +1,23 @@
# Backward compatibility in this batch
Per the repo rule, every compatibility decision of the batch is recorded here once; the feature entries reference this file instead of re-telling it.
## The two per-panel width preferences are adopted into one shared key
The Workspace files panel and the Agents panel each persisted their own dragged width — `penguin.filesPanelWidth` and `penguin.subagentsPanelWidth` in localStorage. They now share a single width under `penguin.panelWidth`, so a stored value has to be carried across or every user who had ever dragged a panel would silently snap back to the default on first load after upgrading.
**Old shape tolerated:** either legacy key. On the first read after upgrading, both are consulted and the **wider** of the two is adopted into the new key, then both legacy keys are deleted. Widest wins because the two panels were sized independently: taking whichever key happened to be read first could hand the merged panel the narrower of the user's two choices, which reads as a regression on whichever panel used to be wide.
**Scope:** browser localStorage only — no server-side data, no config file, no Trace. A user with neither key (or with storage unavailable) gets the proportional default, as before.
**User action:** none. The migration is silent and one-way; nothing needs re-dragging.
**Removal:** the block is `LEGACY_WIDTH_KEYS` and `storedWidth()` in `packages/web/src/features/chat/use-panel-width.ts`. It is self-cleaning — it deletes the legacy keys as it reads them — so it is dead code for anyone who has opened the Web App once since this release. **It should be deleted in the release after next** (i.e. two releases from the one shipping this batch), by whoever prepares that release; the comment at the site says the same. Nothing else in the repo reads those keys, so removing it is a pure deletion with no visible effect. Leaving it longer costs only the dead branch — there is no correctness reason to keep it.
## Panel visibility and the draft page's example layout are not compatibility surfaces
Both changed behavior in this batch, and neither reads persisted state: panel open/closed is in-memory per session (it was never stored), and the example folders' open state is component state that resets on mount. An upgrading user sees the new behavior immediately with nothing to migrate.
## The renamed navigation entries and removed strings need no handling
`nav.railAgents` and `benchmark.maxScore` are deleted from both dictionaries, and several `nav.*` values changed wording. UI copy is compiled into the bundle rather than persisted or referenced by stored data, so there is no old shape to tolerate. Recorded here only to state that the check was made.
@@ -0,0 +1,9 @@
# Docs: the Tools and Configuration references match the code again
Three reference blocks had fallen behind the codes they document
`run_subagent`'s argument block listed `model_id` alone, though a model is always referenced by the complete `(provider, model_id)` pair: the tool schema declares both and rejects a call carrying only one, and the page's own prose already said as much. Only the code block was stale, and it is the part a reader copies — both language editions now list `provider` beside `model_id`.
The provider credential table covered nine of the twelve groups in `MODEL_PROVIDERS`, omitting the `fireworks`, `qwen-token-plan` and `qwen-pay-as-you-go` gateways. All three read `OPENAI_API_KEY` / `OPENAI_BASE_URL`, for the same reason `openrouter` and `siliconflow` were already on that row: a gateway's model ids cannot be auto-routed, so its entries go through the OpenAI client. For a group missing from the table the natural guess is wrong twice over, since neither a vendor-shaped `FIREWORKS_API_KEY` nor the variable of the vendor whose model the gateway resells is ever read.
The Project model entry table skipped `max_tokens`, the per-model output cap that overrides the Agent's `model.max_tokens` when set. It is what makes a narrow-context model usable at all — the seeded Agent default of 32000 cannot fit a small window alongside any prompt, and the upstream rejects such a request outright — and the CLI reference already documented the flag that writes it.
@@ -0,0 +1,17 @@
# Install documentation and landing refresh for 0.1.5
The 0.1.5 release repackages how the product introduces and installs itself, in both READMEs and on the landing site.
## One tagline pair everywhere
README and landing unify on the same two lines: **"Your Automated Agent Builder, Right on Your Desktop / Server"** (the landing hero keeps rotating the Desktop/Server word; the README writes the pair with a slash) and the subtitle **"Create Self-Evolving Agents in One Click"** — 中文为「全自动 Agent 构建平台,运行在你的桌面 / 服务器上」与「一键创建自进化 Agent」. The hero's "Agents building agents" badge is removed; the phrase moves to the LangChain comparison section, retitled **"Building Agents with Agents"** (用 Agent 构建 Agent).
## README: every install method spelled out
The Installation section now gives each method its own complete, copy-paste code block — Linux, macOS and Windows online one-liners, npm — each ending in `penguin web`, with the five offline bundles documented per-OS inside a collapsible `<details>` block (extract-and-run commands, architecture hints, the unconditional SHA256 note). The Web-App feature pitch folds into the section intro; the CLI & SDK section is unchanged.
## Landing: switch, don't list
- The hero's install box and the quick start's install step both switch by OS (Linux / macOS / Windows) instead of stacking every command; the quick start adds a second online/offline toggle, with the offline side carrying the bundle note, per-OS commands, architecture hints and a Releases link. Offline commands live beside the online one-liners in `lib/links.ts`.
- The announcement bar drops from four rotating items to two: Kimi K3 with the free models, and the AMD Developer Program credits.
- The 0.1.5 release post ships in both languages (`penguinharness-0-1-5`), covering offline installs, attachments and input images, in-run LLM recovery, and the skill-library upgrades.
@@ -0,0 +1,9 @@
# CLI: pasting CJK or emoji into chat no longer corrupts characters at chunk boundaries
`penguin chat` decoded each stdin chunk on its own, so a multi-byte character that a terminal split across two reads was destroyed in both halves. Pasting a few thousand Chinese characters was enough to lose one.
`PasteFilter` sits between raw-mode stdin and readline, and a raw-mode chunk ends wherever the terminal's buffer ended — routinely mid-character, since a paste is delivered in fixed-size blocks that have nothing to do with character boundaries. Decoding a chunk in isolation turned its incomplete trailing bytes into U+FFFD immediately, and the leading bytes of the next chunk into more of them: a single 3-byte character came out as three replacement characters rather than one, so the text also silently grew. Pasting about 1400 Chinese characters — roughly one 4KB terminal block — was already enough to hit it, and the loss was invisible until the model answered about text the user never sent.
Decoding now spans chunks via `StringDecoder`, which holds an incomplete trailing sequence until the following chunk completes it. This is what `setEncoding("utf8")` already does for the output of commands the Agent runs; the input path was the one place decoding was hand-rolled. The existing `leftover` mechanism, which reassembles a bracketed-paste marker split across chunks, is untouched: it works on decoded text and could only ever see characters that were already intact.
Tests feed byte slices rather than strings, since a string chunk is by definition a whole number of characters and cannot express the bug: a typed CJK character cut in half, a pasted emoji cut two bytes in, a character delivered one byte at a time, and a marker and a character torn across the same chunk boundaries.
@@ -0,0 +1,21 @@
# web-design and penguin-sdk skills: editorial theme, output contracts, thinking and images
Two of the built-in skills were reworked across three iterations (web-design v3 → v6, penguin-sdk v14 → v17), with the goal of getting better results from shorter user prompts.
## web-design
- A second complete visual language, the opt-in **paper editorial** theme: warm paper/ink/hairline tokens with one warm accent plus a status green, system-serif display headings over a sans body (CJK serif fallbacks, still no CDN fonts), uppercase mono micro-labels as the hierarchy tool, tighter radii with soft warm shadows, and signature shapes (asymmetric-corner brand mark, echoed user-bubble radius, orbit-medallion flourish). Light-only; the GitHub-style default remains, and the two never mix.
- Theme-agnostic chat/RAG upgrades: an optional knowledge side panel with index-status rows, a fuller empty-state composition (eyebrow → one-accent-word title → subtitle → pill chips or numbered example cards), a retrieval status line while answers stream, a sources accordion as an alternative citation surface, and composer refinements (kbd hints, disclaimer, gradient fade).
- A **ship-complete contract**: the one-line request is the whole spec — the skill never asks how a page should look, routes the recipe by request shape, and every delivery includes `lang`/viewport/title, persisted dark mode, designed loading/empty/error states, a working keyboard path, alt text and zero external requests.
- Chat recipes for the two message kinds modern models add: a collapsible reasoning block (auto-collapsed at the first answer delta, never restyled as answer prose) and composer image attachments (paperclip/paste/drag-drop, thumbnail queue, in-message previews).
## penguin-sdk
- A **Thinking and image messages** section, verified against core's OmniMessage protocol: stream `partial_thinking` into its own collapsed channel and ignore `fidelity` (core's replay bookkeeping); build image input with `imageUrlMessage` beside `userText`, with the config `vision` flag degrading gracefully (`[attached image: <path>]` folding read back through the project's `vision_model`); and a cheat sheet for the remaining app-relevant payloads (`request_end` error signals, `token_usage`, compaction events, tool activity).
- The streaming-loop guidance covers `opts.thinkingLevel` (`none…xhigh`, a per-turn override of `model.thinking_level`) and names Session lifetime as the app's memory model — one long-lived Session for stateful chat, one per request for stateless QA.
- An **output format and language** block in the RAG recipe: fix the format in the persona and per-request prompt (plain text, bare `[n]` citations — or a sanitized HTML whitelist when structure matters) instead of shipping a Markdown renderer, and bridge cross-language lexical retrieval with a small ingest-time bilingual keyword map expanded in `search()`.
- The overlapping "before you build" / "check the model first" sections merged into one keys-and-data-root section, each rule stated once.
## Web draft-page examples
With the knowledge moved into the skills, the draft page's example prompts shrink in both languages: the RAG example drops its BM25 cross-language lecture and empty-state instruction (keeping the bilingual self-test step), and the game-center / music-player examples drop the theme-persistence and responsive details the web-design skill now guarantees.
@@ -0,0 +1,57 @@
# Web App: one name per navigation entry, one update row, one panel width, and a fixed-height example shelf
A pass over the surfaces you touch on every visit. Navigation entries settle on a single name each, the user menu's three update rows collapse into one, the two docked panels stop behaving like two different panels, Project display names become editable, and the draft page's examples become a fixed-height shelf that can keep growing.
## Navigation entries have one name each
The sidebar, the collapsed rail and the page titles had drifted apart — the rail said 智能体 where the pinned nav said 智能体仓库, and English mixed Costs with Trajectory. Each entry now has exactly one name per locale, used everywhere:
| | |
| --- | --- |
| 新建对话 / New chat | 智能体 / Agents |
| 技能库 / Skills | 模型库 / Models |
| 成本中心 / Cost Center | 轨迹观测 / Trajectories |
| 评估中心 / Evaluation Center | |
The rail-only `nav.railAgents` string is gone, since both dictionaries now word the entry the same way as the pinned nav.
## The sidebar's "New chat" stops colliding with the scrolled list
Scrolling the sidebar slid nav entries right up against the pinned "New chat" button, the two labels touching. The gap below the button was padding *inside* the scroll container, so it belonged to the scrollable content and travelled away with it. It now belongs to the pinned block, which keeps it at every scroll offset — the same text-to-text rhythm two adjacent nav rows have.
## One update row instead of three
At worst the user menu stacked a release-notes link, an admin "Update now" row and a "Check for updates" row on top of each other. There is now a single row with two states: it reads "Check for updates" and runs the manual check until a newer release is known, then names that version with a leading accent dot and opens the update dialog. The superscript badge beside the version number is gone — the label already names the new version.
The dialog absorbed what the rows carried: it now shows the release-notes link, and offers the self-update only to admins. Non-admins see the same version and link, read-only, instead of an action the endpoint would refuse.
## The two docked panels behave as one panel
The Workspace files panel and the Agents panel are mutually exclusive — opening one closes the other — so to the eye they are a single right-hand panel that swaps its content. They now behave like one:
- **One width.** Dragging either panel resizes the other immediately and persists as a single layout preference. A width stored under either previous per-panel key is adopted once, taking the wider of the two so the merged panel never comes out narrower than either panel had been.
- **A wider default**, ~40% of the window rather than ~1/3 (the cap is unchanged at half the window, 720px ceiling). One panel now has to hold a subagent transcript as well as a file tree, and the transcript is the demanding tenant — a third of the window renders it as a narrow column of wrapped tool output.
- **One visibility rule.** An open panel survives switching conversations; starting a **new chat** is the single point that closes both, so a Session created from the draft begins with neither open. This retires the Agents panel's task-scoped auto-close, which had been the one deliberate divergence between the two. The panel keeps its auto-open on the current Task's first live subagent spawn; task boundaries now only re-arm that attempt rather than closing anything.
## The Agents panel says what a subagent was sent to do
The call graph named each child's Agent but never its assignment, so a child transcript opened with no account of its own purpose. Each node in the graph is now two lines: the Agent name with its elapsed time and status, and beneath it the spawning `run_subagent` call's model-written `description`. The sentence is free-form and the box is a fixed size, so it is truncated to a single line with the full text in the node's tooltip. Node height stays uniform — a node without a description (the root, a standalone child, an omitted one) drops the second line and centers the first rather than shrinking, which would drag row placement and edge geometry along with it.
## Project display names are editable
The Project settings dialog showed the display name as static text; renaming meant recreating the Project. The owner can now edit it (`PATCH /api/projects/:projectId`, writing `name` into `project_config.toml` and preserving everything else in the file). The id stays immutable — it names the directory, the Workspace paths and every stored reference — and now sits below the field as a muted caption. Members see the name read-only.
## The draft page's examples become a fixed-height shelf
Three example cards took a fixed slice of the page and left nowhere to add a fourth. They are now two bookmark-style folders, **搭建网页应用 / Build web apps** and **搭建和优化智能体 / Build and optimize agents**, with exactly one open at all times: selecting a folder closes the previous one, and the open folder cannot be collapsed. The block is therefore a constant height — two folder rows plus one folder's rows — so nothing below it shifts as you switch folders, and no scrollbar is ever needed inside a six-line showcase.
Each example is a single-line title now, with its one-sentence description moved into the row tooltip and its icon moved onto the folder row, which is what you actually scan to pick a category.
Two additions to the catalogue:
- **A mini-game center built by multiple agents** — ten games with no two sharing a mechanic, each a single-file `games/<slug>/index.html`, built in parallel by subagents behind one index page.
- **The Claude Code docs RAG agent** example now calls out query/corpus language matching. The docs are English while questions are often Chinese, and with a lexical retriever such as BM25 a Chinese query must be bridged to English first — otherwise not a single term matches and retrieval silently degrades to nothing rather than failing. Its self-test now covers one Chinese and one English question.
## Evaluation Center
The case rows and the statement preview both printed "Max 100". The scale is not the UI's to assert — a benchmark's total is defined by its own scoring rubric — so the label is gone along with its string. "View task" also stops rendering in the accent link color: the row itself is the button, so an accent-colored label inside it read as a second, separately clickable target. It now matches the Workspace download link's quiet gray, with hover feedback left to the row.
+39
View File
@@ -0,0 +1,39 @@
# Version 0.1.5
Released on 2026-07-30.
- [2026-07-30] Skills: web-design (v3 → v6) gains an opt-in paper editorial theme, a ship-complete delivery contract, and chat recipes for the thinking block and composer attachments; penguin-sdk (v14 → v17) documents the thinking and image message kinds, a plain-text output contract, cross-language BM25 retrieval, and a merged keys/data-root setup flow — with the Web draft page's example prompts shortened now that the skills carry the knowledge. ([details](2026-07-30-skills-web-design-and-penguin-sdk.md))
- [2026-07-30] Sites and docs: the README and landing page unify on the new tagline pair ("Your Automated Agent Builder, Right on Your Desktop / Server" · "Create Self-Evolving Agents in One Click"); the README spells out every install method as a complete per-OS code block with the offline bundles behind a collapsible; the landing install sections switch by OS and method instead of listing everything; the announcement bar drops to two items; the LangChain comparison is retitled "Building Agents with Agents"; and the 0.1.5 release post ships in both languages. ([details](2026-07-30-install-docs-and-landing.md))
- [2026-07-30] Backward compatibility: the batch's compat decisions in one place — the two per-panel localStorage width keys adopted into one shared key (widest wins, self-cleaning, with its removal release named), plus the changes checked and found to need no handling. ([details](2026-07-30-backward-compatibility.md))
- [2026-07-30] Web App: navigation entries settle on one name each across the sidebar, the collapsed rail and page titles; the user menu's three update rows collapse into one; the Workspace and Agents panels share a single (wider) width and a single open/closed lifetime, closing only on a new chat; the Agents panel says what each subagent was sent to do; Project display names become editable; and the draft page's examples become a fixed-height, always-one-open folder shelf with a multi-agent game-center example added. ([details](2026-07-30-web-app-refinements.md))
- [2026-07-30] CLI: pasting CJK or emoji into `penguin chat` no longer corrupts a character wherever the terminal split its stdin blocks — each chunk was decoded on its own, so a character torn across two reads became three replacement characters instead of one, and about 1400 Chinese characters was enough to trigger it. ([details](2026-07-30-paste-filter-utf8.md))
- [2026-07-30] Docs: three reference blocks catch up with the code they document — `run_subagent`'s argument block lists the `provider` that `model_id` must be paired with, the provider credential table covers the three gateway groups it had been missing, and the Project model entry table documents `max_tokens`. ([details](2026-07-30-docs-tools-and-configuration-reference.md))
- [2026-07-29] Core: every LLM failure except a rejected credential now retries inside the run — the classifier separating transient from permanent is an allowlist, so a gateway wording a transient fault its own way used to kill the turn — with the retry visible in both frontends, compaction on the same set under its own shorter budget, and a recovered failure no longer reported to the operator as an incident. Separately, pressing Stop mid-request can no longer leave a Session running forever when the provider's stream neither yields nor rejects after the abort. ([details](2026-07-29-llm-request-lifecycle.md))
- [2026-07-29] Cost center: the error table can page back through the whole history instead of showing only the newest 20, its source column reads `[env]` rather than `environment ·`, and an ordinary non-zero exit from a command tool is no longer recorded as an error — `grep` finding nothing had been crowding out the failures the table exists to show. ([details](2026-07-29-cost-center-errors.md))
- [2026-07-29] Core and tooling: `PORT` / `HOST` and the internal CLI plumbing no longer reach commands the Agent runs, so a dev server started by `exec_command` picks its own port instead of binding the harness's; and the development backend moves to 7368, so `pnpm dev` no longer collides with an installed `penguin web` — or, quieter and worse, proxies to it. ([details](2026-07-29-harness-env-and-dev-ports.md))
- [2026-07-29] Web App: the composer can attach files of any type — written to the Session scratchpad and handed to the model as `[attached file: <path>]` lines, keeping non-ASCII names — and the `@` mention becomes an `/agent` command; both switch commands are session-only and stage their pick as a chip, cached with the text, until Enter sends. ([details](2026-07-29-composer-attachments-and-switch-commands.md))
- [2026-07-29] Core: the default system prompt is about a tenth shorter (1087 → 969 words), with the trim concentrated in the sections re-read every turn. It now pins replies to the user's language, states the API-key stop rule once, points at parallel subagents for large tasks, and has tooling installed once into a shared per-Agent `shared_env/` directory while project dependencies stay in the project. The App Data Dir rule also stops forbidding deliverables in a tree that a temporary Workspace lives inside. Existing Agents keep their own prompt. ([details](2026-07-29-default-system-prompt.md))
- [2026-07-29] Web App: the chat header's elapsed chip no longer restarts from zero when a running Session is reloaded and now counts an event still in flight, and a round interrupted before its first Request settles to the same figure whether it was watched live or replayed — the running Task is anchored to the server's own clock instead of the browser's. ([details](2026-07-29-session-elapsed.md))
- [2026-07-28] Web App: model configuration stops inviting the browser's saved login — every form control opts out of autofill unless it declares a real credential role, and a secret field says `new-password`, the only value Chrome and Safari honor on a password box; a closed docked side panel no longer paints its 1px divider next to the open one, which read as a second, empty panel beside the Workspace files; a provider-signature message carrying no text stops drawing an empty reply bubble after a thinking segment; Benchmark costs now follow the display currency selected in settings; the tool card states a call's outcome once instead of twice; and no page can grow the document into a second scrollbar that drags the whole shell. ([details](2026-07-28-web-app.md))
- [2026-07-28] Web App and CLI: duration and byte abbreviations now carry into the next unit instead of printing `1m60s` or `1024KB` — both helpers rounded the value they displayed but chose the unit (or the minute/second split) from the raw input. ([details](2026-07-28-duration-and-byte-formatting.md))
- [2026-07-27] Input images: mid-run steering carries them (an uncaptioned image is a complete steering message), a goal objective accepts them as scratchpad paths on every model since it is re-injected as text each round, and the composer's mid-run action button can no longer sit disabled with Stop displaced. ([details](2026-07-27-input-images.md))
- [2026-07-27] Windows: the `win32-x64` package bundles MinGit under `git/`, so `exec_command` has a real bash even on a machine with no Git for Windows — the shell stops depending on what happens to be installed. A user's own Git for Windows still wins (its MSYS userland is the fuller one); the bundle is the floor, and PowerShell is now reached only by npm installs. GPLv2 obligations are recorded in a new root `THIRD-PARTY-NOTICES.md`. ([details](2026-07-27-windows-bundled-shell.md))
- [2026-07-27] Sites: the 0.1.4 release post in both languages, with a capture script for its screenshots. ([details](2026-07-27-sites-and-blog.md))
- [2026-07-25] Agent tuning: the Web App gains an end-to-end example for creating, benchmarking, and optimizing an Agent through isolated CLI Sessions, while the built-in tuning Skills tighten phase ownership, private evaluation boundaries, and Evaluator dispatch contracts. ([details](2026-07-25-agent-tuning-pipeline.md))
+50
View File
@@ -0,0 +1,50 @@
PenguinHarness 0.1.5 — offline installs for every platform, attachments and images in every input, and runs that recover instead of dying.
## Install
```sh
curl -fsSL https://penguin.ooo/install.sh | sh
penguin web
```
Windows (PowerShell):
```powershell
irm https://penguin.ooo/install.ps1 | iex
penguin web
```
Linux, macOS and Windows, with a bundled Node runtime. Or via npm (needs Node >= 24):
```sh
npm install -g @prismshadow/penguin-cli
```
**New: fully offline installs.** This release attaches five self-contained offline bundles — `penguin-linux-{x64,arm64}-offline.tar.gz`, `penguin-darwin-{x64,arm64}-offline.tar.gz`, `penguin-win32-x64-offline.zip` — each carrying the program archive, its SHA256 checksum and the platform's installer. Download on a networked machine, copy to the target, extract, and run `./install.sh` (POSIX) or double-click `install.cmd` (Windows); offline installs verify the checksum unconditionally.
## Highlights
**Offline install everywhere.** Every platform archive now ships wrapped as a self-contained offline bundle, `install.sh` / `install.ps1` accept a verified local archive (`--archive` / `PENGUIN_ARCHIVE`) while keeping their online behavior, POSIX bundles pass their payload explicitly so the online installer never trusts archives found beside a temporary script, and renamed archives still install thanks to an embedded target manifest. The Windows package also bundles MinGit, so `exec_command` has a real bash even on a machine with no Git for Windows — your own installation still wins when present, and GPLv2 obligations are recorded in `THIRD-PARTY-NOTICES.md`.
**Attachments and images reach every input.** The Web composer attaches files of any type — written to the Session scratchpad and handed to the model as `[attached file: <path>]` lines, non-ASCII names preserved. Images now ride mid-run steering (an uncaptioned image is a complete steering message) and goal objectives (as scratchpad paths, re-injected as text every round, so they work on every model). The `@` mention becomes an `/agent` command, and both switch commands stage their pick as a chip until Enter sends.
**LLM failures recover in-run.** The transient-vs-permanent classifier is inverted: every failure except a rejected credential now retries inside the run — visible in both frontends as a live countdown — with compaction retrying under its own shorter budget and recovered failures no longer reported as operator incidents. Pressing Stop mid-request can no longer strand a Session when a provider's stream neither yields nor rejects after the abort.
**A skill library that designs and builds better apps.** web-design gains an opt-in paper editorial theme and a ship-complete contract (a one-line request is the whole spec — dark mode, loading/empty/error states, keyboard path and zero external requests included unasked), plus recipes for collapsible thinking blocks and composer attachments; penguin-sdk documents the thinking and image message kinds, a plain-text output contract, and cross-language BM25 retrieval. The Web draft page adds an end-to-end agent tuning example.
## Notable in this release
- **A shorter default system prompt.** About a tenth smaller (1087 → 969 words), pinned to the user's language, with shared tooling installed once into a per-Agent `shared_env/` directory. Existing Agents keep their own prompt.
- **Web App refinements.** Navigation names unified; the Workspace and Agents panels share one width and lifetime; Project display names editable; the draft examples become a fixed-height folder shelf; the session elapsed chip survives reloads and counts in-flight events.
- **Cost center, honest errors.** The error table pages through the whole history, ordinary non-zero exits are no longer recorded as errors, and environment-sourced entries read `[env]`.
- **CLI paste fix.** CJK and emoji pasted into `penguin chat` no longer corrupt when the terminal splits stdin blocks mid-character.
- **Formatting carries.** Duration and byte abbreviations roll into the next unit instead of printing `1m60s` or `1024KB`.
- **Cleaner harness env.** `PORT` / `HOST` and internal CLI plumbing no longer reach commands the Agent runs, and the development backend moves to port 7368, out of an installed `penguin web`'s way.
- **Docs catch up.** `run_subagent`'s `provider` argument, the gateway credential groups, and the Project model entry's `max_tokens` are documented.
- **A refreshed front door.** README and landing unify on the new tagline pair, the README spells out every install method per-OS with offline bundles in a collapsible, and the landing page switches install methods by OS instead of listing them all.
## Requirements
Linux or macOS (x64 / arm64), or Windows 10+ (x64). The installers bundle their own Node runtime; installing from npm needs Node >= 24. All data stays under `~/.penguin/data`.
Full detail: [changelog/0.1.5/](https://github.com/Prism-Shadow/penguin-harness/tree/main/changelog/0.1.5).