164 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- Native
my-pipermission child lifecycle bridge. Child sessions now publishsubagents:child:session-createdsynchronously beforebindExtensions()andsubagents:child:disposedafter actual teardown. Every descendant keeps the interactive root session as its permission authority, and child prompts replace inherited identity tags with their stable agent key. This letspi-permission-systemapply root whole-session grants to child-fixed access facts, run auto-review against the trusted root transcript, and forward unresolvedaskdecisions to the root UI without treating model-generated child prompts as direct user authorization. - Host-owned mandatory extensions with fail-closed loading.
createPiSubagentsExtension({ mandatoryExtensionPaths })lets a bundle protect exact canonical extension entries from Agent-controlledextensions: false,isolated, andexclude_extensions. Missing or substituted entries abort before session creation. Mandatory handlers bind without automatically surfacing their tools, preserving the separation between security lifecycle and Agent tool visibility.
Changed
- Enabled in the
my-pibundle. The root package now installs this source throughfile:./pi-subagents, loads the host-owned wrapper by default, and assigns the three orchestration tools to Tool Search's checked-insubagentsgroup. Pi host peers are optional in this maintained package so the bundle continues to use the host runtime instead of installing a duplicate. The off-screen mention clone now disables extension/resource discovery so Tool Search and other active-tool owners cannot remove its single syntheticAgenttool duringsession_start.
[0.18.0] - 2026-08-20
⚠️ Breaking — an
Agentcall that doesn't say now runs in the background (backgroundByDefault). Following Claude Code, where the agent backgrounds unless the caller passesrun_in_background: false. An unqualified spawn no longer blocks the turn and no longer returns the agent's output inline: it returns an ID immediately, and the completion notification carries a preview (500 chars solo, 300 grouped) with the full text behindget_subagent_result. SetbackgroundByDefault: falseto restore the previous behaviour globally; an explicitrun_in_backgroundon the call or in frontmatter overrides the setting in both directions. Nested spawns are unchanged — an agent spawning its own agent still defaults to foreground, because a detached child is stopped byabortOwnedChildrenwhen its parent settles and has no notification path of its own. Two consequences worth planning for:maxConcurrentnow applies to nearly every spawn where foreground used to bypass it (its default is raised 4 → 10 to compensate), and Esc interrupts the turn without stopping the agents it started — stop those from/agents → Running agents, which is also how Claude Code separates the two.
Added
reportUsage— subagent tokens and cost count towards this session's own totals (#193, #203 — thanks @johnstegeman and @unrelentingfox). Subagents run in their own pi sessions, so nothing they spent reached the parent'sgetSessionStats(): a session that delegated most of its work read as nearly free in the footer, the statusline and/cost. With the setting on, eachAgent/get_subagent_result/steer_subagentresult carries the spend accumulated since the last one, which pi folds into the session totals and attributes to/cost's "Tools/summaries" bucket. Off by default — it changes numbers you may already be tracking. Agents that finish in the background have no result of their own to ride on, so their spend lands on the next call rather than the moment they finish; nested agents are covered, counted once each, on the top-level result. Cost is pi's own per-message figure, and the context-window percentage is unaffected. Implemented independently in #202, whosecacheReadreasoning corrected this one: the cached prefix is genuinely re-read and re-billed per call, and pi counts it that way for the session's own messages, so the report includes it even though the extension's own token displays still exclude it (#38).showCost— an estimated cost beside subagent token counts (#194 — thanks @daanzu, whose PR is the display design here). Shown in the widget, FleetView, the conversation viewer, foreground results,get_subagent_resultand completion notifications, with a batch total when several background agents finish together. Off by default, and shown only when there is a cost to show: a model pi has no pricing data for reports zero, and$0.00beside its tokens would say the run was measured and found free rather than never measured. Figures keep cents at minimum and four decimals at most (~$0.0042,~$1.24), because rounding everything to cents printed one number for runs that differed fourfold.- Lifecycle events carry the run's spend as a pi
Usage(#137, #138 — thanks @leset0ng).subagents:completedandsubagents:failedgain ausagefield alongsidetokens, carrying the token components —cacheReadincluded — andcost.totalin USD. It is pi's ownUsage, the shape pi puts onToolResultEventandAssistantMessage, so the money is where a listener already looks and anything pi adds toUsageneeds no change here. Omitted when nothing was spent, so spent nothing stays distinguishable from never ran.tokensis unchanged and still the display total, which excludescacheRead(#38) — the two answer different questions and neither derives from the other. This is the aggregate only — the per-message{timestamp, provider, model, usage}records those asked for, and the per-model attribution they enable, remain out of scope (#137 was declined and #138 withdrawn by its author).
Changed
- The pi peer floor moves from
>=0.80.0to>=0.81.0.reportUsageis built on pi foldingtoolResult.usageintogetSessionStats(), which pi only started doing in 0.81.0 — every 0.80.x sums assistant messages alone and drops the field, so the setting would have been present and silently inert there. The CI floor job installs 0.81.0 and runs the suite against it, which is what caught this; the e2e that pins the behaviour runs unconditionally rather than skipping, so the range cannot quietly stop being true. Nothing else in the extension needed 0.81. - Top-level
Agentspawns default to background, behind a newbackgroundByDefaultsetting (see the breaking note above). Configurable at/agents → Settings → Background by defaultand insubagents.json; applied live. Nested spawns passfalseunconditionally and ignore the setting. - The background concurrency limit's default is raised from 4 to 10. Foreground agents bypass the
maxConcurrentpool entirely, so while foreground was the default a six-way fan-out ran six; with background as the default every top-level agent takes a slot, and a limit of 4 would have silently queued the tail of exactly the parallel fan-outs theAgenttool description tells the model to send. Still configurable — the ceiling is unchanged. - The agent-file writer emits
run_in_background: falserather than dropping it. It wrote the field only when truthy, which was lossless while omission andfalsemeant the same thing; underbackgroundByDefaultthey no longer do. No current path reaches it —serializeAgentFile's only caller is Eject, which is offered for built-in defaults, and those omit the field — so this is the writer half of the format keeping pace with the loader, not a fix for observable behaviour.
Fixed
- Subagent sessions now close the extension lifecycle they open (#242 — thanks @bsv9). Every child session binds extensions so
session_startfires and they can set up per-session state, but nothing emitted the matchingsession_shutdown:AgentSession.dispose()only invalidates theExtensionRunner. Whatever an extension armed insession_start— timers, fs watchers, sockets, temp dirs — therefore leaked one set per spawn, teardown asession_shutdownhandler owns (killing runs started by that child, say) never ran, and the 10-minute record sweep turned the leak into a crash: disposing invalidated the runner while a leaked timer was still armed, so its next tick threwassertActive()from a bareTimeout._onTimeout— anuncaughtExceptionthat took interactive pi down with it. Both eviction and quit now emitsession_shutdown(reasonquit) before disposing, which is what pi itself does inAgentSessionRuntime.dispose(). Quit waits for those handlers rather than exiting past them, bounded at three seconds per session so one that hangs cannot strand you at a torn-down TUI; a session whose extensions register no handler is untouched and still disposes synchronously. Independently fixed in #228 and #170 — thanks @v-kuskov and @liu-qingyuan. - The
Agenttool no longer tells the model that foreground agents run one at a time (#232 — thanks @willfenton). "Foreground calls run sequentially — only one executes at a time" was never true: pi's agent loop dispatches a message's tool calls throughPromise.allunless the whole batch opts out viatoolExecution: "sequential"or some tool in it declaresexecutionMode: "sequential", and this extension sets neither — two foregroundAgentcalls in one message start within microseconds of each other. Nothing serialized them at this layer either;agent-managerdeliberately exempts foreground agents from themaxConcurrentqueue, since they block the parent anyway. The claim was self-inflicted rather than inherited:d0cb511replaced a correct bullet with it, and the later upstream-alignment pass grafted it onto the end of Claude Code's real sentence instead of replacing it, which also left an inventedwith run_in_background: true on eachqualifier in the middle — upstream says a single message with multiple tool uses runs concurrently, full stop. Both are gone, restoring upstream's wording (minus its build-validator/test-runner example, consistent with the examples already omitted for token cost). The cost of the error was steering: an orchestrator that wanted parallelism was pushed into background spawns it did not need, paying a queue slot and a notification round-trip for concurrency it already had. Investigating where the sentence came from is what surfaced the default divergence in the breaking note above, so the two ship together — the remaining foreground/background prose is now Claude Code's own, including theDon't racebullet that only earns its place once background is the default. - Scheduled and RPC-spawned agents show their token counts in the widget, and finished agents keep theirs in the conversation viewer. Both surfaces read spend from the live activity tracker, which only
Agent-tool spawns get and which is deleted the moment an agent finishes — so those agents rendered with no token stats at all. Every surface now reads it from the agent's record instead: the record is the only total that outlives the run and the only one a nested child's spend is folded into, so the figure no longer jumped upward at completion as the read switched from one to the other. The tracker keeps what is genuinely live — tool activity, turn count, context percentage — and no longer accumulates a second copy of the totals. Spotted in #194 — thanks @daanzu. - Agents started outside the
Agenttool say what they are doing, instead ofthinking…for their whole run (#181 — thanks @xz-dev). The widget's activity line and turn counter come from an activity tracker that only theAgenttool handler created, so an agent started through cross-extension RPC (the pathTaskExecuteuses), through an@handlemention, or through theSymbol.for("pi-subagents:manager")registry showed a permanentthinking…while the same row's tool-use count climbed beside it and the conversation viewer showed the real work. The tracker now belongs to the one funnel all three spawn paths pass through, so none can be missed and none can supply half-wired callbacks of its own; theAgenttool reaches the manager directly, so nothing is double-tracked. Its turn ceiling is resolved exactly the way the run resolves the limit it enforces — explicit value, else the agent's ownmax_turns, else the project default — rather than read off the caller's options, which a mention deliberately omits so the agent file can decide: the row would otherwise render↻3where the tool renders↻3≤20. Scheduled jobs still spawn through the manager and so still render without per-tool detail.
[0.17.1] - 2026-08-18
Fixed
- A
model-mode mention now actually reaches the agent it starts. The off-screen clone was built with an empty tool allowlist, which stripped its ownAgenttool, so every start fell back to a direct spawn with a warning. Fixing that surfaced a second bug behind it: the clone spawned in the foreground, answering into a session disposed moments later, so the agent ran and reached nobody. Its spawn is now forced to the background. @offers files again. Handle rows are merged into pi's own suggestion list instead of replacing it — the provider asked pi only when no agent matched, and prefix matching meant an empty token matched every handle, so a bare@listed no files at all.
[0.17.0] - 2026-08-17
⚠️ Note — an agent file's frontmatter
name:now substitutes for the filename as itssubagent_type. Following Claude Code, the declared name is the dispatch identity and the filename is only the fallback, soblubb.mdwithname: code-reviewis spawned, mentioned and listed ascode-review. Any value is accepted except one containing:, which Claude Code reserves for plugin scoping; such a file — like any unparseable one — is skipped with a warning rather than loaded under a name nothing honours, andstrictAgentFilesturns that into a startup failure.
⚠️ Breaking — subagent sessions now persist to disk by default (
rememberAgents). Transcripts that used to be in-memory are written to the session dir and appear nested under their spawner in pi's/resume. Per-agentpersist_session:overrides it in both directions —falsekeeps that agent in memory,truepersists it even with the setting off. SetrememberAgents: falseto restore the previous behaviour globally.
Added
- Subagents are addressable from the prompt:
@handle messagegoes to that agent instead of the main model. Reaching one previously meant spending a main-model turn onsteer_subagent, or walking the FleetView. The handle names the agent, not one process, so a running or queued agent is messaged, a finished one is resumed in the background, and one that never ran is started — the reply arrives as the ordinary background-completion notification either way. Every agent gets a handle from its type, numbered on collision (explore,explore-2), offered by@completion alongside pi's file completion. Only a leading@handlefollowed by a message is a send, so a bare@explore, a mid-sentence mention and@src/index.tsstill reach the model; toggle withagentMentions. - Mentioning an agent that isn't running now starts it through an off-screen clone of the conversation. A throwaway copy of the session takes a turn holding only the
Agenttool, so the model writes the agent's prompt from the conversation instead of forwarding your typed line — the context-aware prompt Claude Code gets from its own mention flow, without a visible turn narrating a decision the handle already made. Nothing enters the chat but aStarting @plan…toast, and what starts is an ordinary top-level agent with a transcript, widget detail and a handle.agentMentionsaccordingly takes"model"(default),"direct"— the previous behaviour, started from your text with no model call — or"off"; the old booleans still read as"model"/"off". Messaging a running agent and resuming a finished one are direct in both modes, a clone that cannot run falls back to a direct start rather than losing the mention, and"model"lifts the TUI-only restriction for starts, sopi -p '@plan the migration'now works. @handlekeeps working after the agent's record is gone. Handles used to expire with the in-memory record ~10 minutes past completion, silently flipping@explore anything else?from resume to start fresh. An evicted agent now leaves a tombstone and the mention reopens its session from disk (requiresrememberAgents, below). Only the definition is re-resolved, so a continuation runs under the type's current frontmatter — and a resume whose type has since been deleted or disabled is refused rather than falling back to another agent. Tombstones cap at 100 and clear on/newand session switch.- The model can
namean agent, and names work wherever ids do.Agenttakes an optionalname, so an agent can be@auth-auditinstead of leaving you to tell@explore-2from@explore-3. Naming is additive — the type-derived handle is still assigned, and both draw from one namespace, so neither can shadow the other.steer_subagentandget_subagent_resultnow accept a handle as well as an id, ids first, so existing calls are unchanged. @main <message>forces text to the main model;@agent-<type>is a synonym.mainis reserved and can never be allocated to an agent, so a leading@mainis stripped and the rest passes through with its attachments — the escape hatch for text that only looks like a mention.@agent-explore fix itis Claude Code's manual spelling; the handle as written wins, so an agent actually calledagent-explorestays reachable. Handles cap at 64 characters.worktreeIsolationproject setting — turn worktree isolation off for a whole repo (#184 — thanks @tinysnake). On a repo large enough that every worktree costs real time and disk, the only defence was asking the model not to request one. Set"worktreeIsolation": falseinsubagents.json(or/agents → Settings → Worktree isolation) and theAgenttool'sisolationparameter is dropped from the schema entirely, along with the usage-note bullet describing it and the/agentsgenerator's frontmatter field — the same tradeschedulealready makes when scheduling is disabled, and necessary, since prose left behind teaches the model to pass a parameter that is no longer declared and to report a branch that was never created. The refusal is enforced below the tool boundary as well, so it also covers agent files, scheduled jobs and the cross-extension RPC path, and a requested worktree is downgraded to a normal run rather than failing the call. Default is unchanged (true); the parameter and its prose appear or disappear on the next pi session, since both are built at tool registration, while the refusal applies immediately.
Fixed
- The
Agenttool no longer tells the model a worktree's path is returned in the result. It never was:cleanupWorktreeremoves the worktree directory before returning and the record keeps only{ hasChanges, branch }, so a model acting on the promised path would have been sent to a deleted directory. The neighbouring claim that the worktree is "automatically cleaned up if the agent makes no changes" was misleading in the same way — the directory is removed in both cases; what is conditional is whether a branch survives. Both had been in the tool description since worktree isolation shipped, inherited from Claude Code's wording; the README's completion summary said the same thing loosely and has been corrected too. isolationaccepts"off", so a model that fills every optional parameter can decline a worktree (#231, #184, via #201 — thanks @Munsen). As a single-value optional literal,isolationwas the one parameter whose only expressible value had an expensive side effect, and every other optional field has an inert filler — so a model that fills everything in reflexively ended up reviewing a staged diff inside a fresh copy of the repo that contained none of the work, over and over, against its own stated intent to omit the parameter."off"is now a legal value — listed first and described as the default — and is also accepted in agent frontmatter (none,noandfalsetoo), where it becomes a genuine veto: frontmatter outranks tool-call parameters, so it refuses a worktree even when the caller asks for one, which no value could previously do. The parameter description now also warns that a worktree cannot see uncommitted or staged work, the specific trap here. One behaviour change to note: an agent file already carryingisolation: none/no/falseused to parse to nothing at all, leaving a caller free to supplyworktreeanyway; it now parses tooffand refuses one.
Changed
- Frontmatter
name:is now the agent's type rather than a display-label alias (see the note above).display_name:still sets the label, and a file setting onlyname:badges as that name, since the label falls back to the type. A file whosename:differs from its filename changessubagent_type; rename the field todisplay_name:to keep the old identity. - Subagent sessions are persisted by default (
rememberAgents) (see the note above). They previously ran in-memory unless the agent file setpersist_session: true, which left nothing for a handle to reopen once the record was evicted — the reason handles expired. Nested children are excluded, since nothing can address them and so nothing could reopen their transcript. - The selected FleetView row renders in the theme's primary
textcolor (#230, #234 — thanks @kunaaal13). Agent name, description and the elapsed/token stats previously kept their muted/dim tokens while selected, so the highlighted row read as several emphasis levels rather than one selection; the●marker keeps its accent. A configured agent color still badges on the selected row and is bolded there, matching Claude Code's own FleetView — the color is what identifies the row you are reading, and leaving the badge in place keeps the row's width fixed as the selection moves.
[0.16.1] - 2026-08-15
Changed
- The pi compile baseline is declared instead of inferred. The
@earendil-works/pi-*packages were peers only, so 0.80.6 came from npm's peer auto-install and the committed lockfile rather than from any decision — the emergent pin the 0.14.0 entry below had to diagnose. They are now exact devDependencies, pinned to the current release (0.84.2); the peer range stays>=0.80.0and consumers are unaffected. compat-latest-pinow runs typecheck and the full suite instead of two hand-picked e2e files, so drift can't hide in an unlisted test — against pi 0.84.2 the job reported green while typecheck and four e2e suites were failing. Stillcontinue-on-error, but both steps pass as of the new baseline, so a failure there is now fresh breakage rather than known debt.- The suite runs against pi 0.84.2, so the
modelRegistry→modelRuntimemigration is exercised by default rather than only by acontinue-on-errorjob. Two places had been carrying it as known-broken: themodelRuntimecompat shim insrc/agent-runner.tstypechecked only pre-migration (an opaqueunknownread off the private facade field can't satisfy the newerModelRuntimeparameter type), and the e2e faux backend faked auth through a structuralmodelRegistrythat post-migration pi no longer accepts, so every faux run died with "No API key found for faux". The faux registry and its post-migration counterpart — auth viagetAuth/hasConfiguredAuth, the turn itself streaming throughmodelRuntime.streamSimple— now live together intest/helpers/faux-model-backend.ts, and both are passed tocreateAgentSession, each pi ignoring the option it doesn't know. - CI covers the floor of the declared peer range, not just the pin.
>=0.80.0used to be tested incidentally, because the inferred pin was a 0.80.x; moving the baseline to 0.84.2 would have left the range asserting support for versions nothing built against. Acompat-floor-pijob now installs 0.80.1 — the earliest published release satisfying the range — and runs typecheck plus the full suite. Unlike the latest-pi canary it is blocking: that run is the only evidence behind the range, and if the floor becomes unsupportable the peer range is what should change. publishConfig.accessis set topublic, so publishing this scoped package no longer depends on--access publicor on the access already set from a prior publish.
[0.16.0] - 2026-08-14
Added
- Custom agents can set
nameandcolor, so agent files shared with Claude Code keep their display identity (#216 — thanks @HerbertGao).name:is accepted as a display-name fallback (display_name:still wins, and the filename still determines thesubagent_typeand dispatch identity), andcolor:renders the agent name as a badge — the color is the background — in theAgenttool header, the widget, FleetView, and the conversation viewer. Claude Code's eight color names render with Claude Code's own values; six-digit hex and the Agency Agents palette aliases are accepted too, quantized to the xterm-256 palette on non-truecolor terminals the way pi's own theme does. A missing or invalid color preserves each surface's existing theme styling. Badge text is black or white by WCAG contrast against the rendered background, where Claude Code uses a single inverse color for every badge. In theAgenttool header the line paints the tool block's row tint itself and leaves it open: the badge has to close its own background, so it restores that tint, and the TUI pads a line to width after rendering it, so closing the line would leave that padding untinted. The tint is painted only when there is a badge, so an uncolored agent's header renders exactly as it did before. run_in_backgroundnow works onresume, so continuing an agent no longer blocks the session (#214 — thanks @akram-ahrardi). TheAgenttool's resume branch returned before its background branch, so the flag was accepted and silently ignored: every resume ran inline and held the main loop for the whole turn, however long that agent took. A resume withrun_in_background: truenow detaches exactly like a background spawn — it returns immediately, queues behindmaxConcurrent, streams to the widget and FleetView, and notifies on completion — while a resume without the flag still runs inline and returns its result, unchanged. Detaching required the differences from a spawn to be handled rather than inherited: the run is not wired to the tool-call abort signal (a background spawn omits it too, and forwarding it would kill the agent when the parent turn is interrupted while spawns from that same turn kept going); a second resume of an agent whose run is still in flight is refused instead of replacing the live run's abort controller, which would have put it beyond the reach of/agentsstop; the transcript is appended to rather than truncated, since the path is deterministic per agent and the previous run's turns — including any the session has since compacted away — are what the completion notification points at; output streaming is wired when the run actually starts, so a resume stopped while still queued leaves no subscription behind; and thesubagents:createdevent reports the record's own type, since resume ignoressubagent_typeand a mirror keyed by id would otherwise re-register the agent under a type it never had. The widget's finished-agent age is cleared on resume, or an agent that had already completed once would vanish instead of showing the new run's completion line.
Fixed
- RPC-spawned agents now appear in the live Widget and FleetView while they are running (#224 — thanks @dereknex). Cross-extension RPC calls entered through
AgentManager.spawn()instead of theAgenttool handler, but only that handler started the Widget/Fleet refresh timers. The manager still tracked RPC agents and the completion callback refreshed the UI, so a long-running RPC agent was invisible until it finished and could appear only as a terminal row. The shared manager-start callback now starts and refreshes both native surfaces for every top-level spawn source, including RPC and scheduled agents; nested agents remain intentionally hidden.
Documentation
- The cross-extension RPC spawn example named an option that does not exist. It passed
run_in_background: true, theAgenttool's parameter name; the RPC handler forwardsoptionsverbatim toAgentManager.spawn, whose field isisBackground, so the flag was silently dropped — an RPC agent nobody meant to exempt never occupied amaxConcurrentslot and never queued behind one, andsubagents:createdreported no background flag for it. The example now passesisBackground, which the RPC test suite has always pinned, and the surrounding text says what the flag does and does not change: every RPC spawn returns its id immediately and runs detached either way. It also states the visibility rule that changed above, so the widget/FleetView behavior is documented where callers read about spawning rather than only in this file.
[0.15.2] - 2026-08-14
Documentation
- Three README statements corrected against the code they describe, each now pinned by a test so the doc and the behavior can't drift apart again. (1) Scheduling was documented as forcing
run_in_backgroundtotrue; it does not — an explicitrun_in_background: falsealongsidescheduleis refused, and always has been. The 0.10.0 entry below carries the same error:git log -Sshows the refusal shipped in the very commit that introduced scheduling, so both documents were written from the design sketch and never matched what was released. Refusing is also the intended behavior — silently flipping a parameter the caller explicitly set is the failure mode #37 was filed about. (2) The completion-notification example showed atranscript: .pi/output/agent-abc123.jsonlline; that path was replaced by the OS-tmpdir layout in #146 and the example was never updated, so it contradicted the paragraph six lines above it. (3) Theskillsfrontmatter row read as though naming skills preloaded them in addition to the parent's; naming any list turns skill inheritance off and the agent gets only the named ones.
Fixed
/agents → Create agentproduced an unloadable agent when the description contained a colon. The wizard interpolated the description and the custom model straight into YAML frontmatter, both of which come from unvalidated free-text prompts. A description likeScout: find thingsmade the generated file unparseable — and since #212 an unparseable agent file is skipped with a warning, so the wizard reportedCreated <path>for an agent that then silently did not exist — and, understrictAgentFiles, aborted the next startup too, that setting gating startup only. A description containing a#was quietly truncated at the#, YAML treating the rest as a comment. Both values are now quoted, the same way the eject serializer already quoted its description. Aprovider/model:thinkingsuffix is unaffected either way — YAML only splits on a colon followed by a space — and is pinned so the quoting cannot mangle it./agents → Scheduled jobscancelled the wrong job when two names looked alike. The menu formats each job into a row, then resolves the user's pick by matching the returned string back against the row list. Rows truncate the job name to 18 characters for column alignment, and job names come from theAgentcall'sdescription, which is LLM-authored and routinely shares a prefix — so two jobs likereview the auth module Aandreview the auth module B, on the same schedule and agent type, produced byte-identical rows and the match always resolved to the first. Picking the second cancelled the first, and because the confirmation dialog was built from the same wrong job, its details looked correct and the user had no way to notice before confirming. Rows are now numbered and carried alongside the job they describe rather than in a parallel array, so labels are unique by construction and no data-dependent collision path exists. The same string-matching pattern in/agents → Running agents— where the consequence was only opening the wrong agent's read-only viewer — now goes through the same helper. Pi's dialog API returns the selected string with no index or value form, so uniqueness at the call site is the only available fix. The whole menu had no test coverage at all, which is how a destructive action shipped with this in it.- The agent widget hid queued agents without counting them. Past its 12-line cap the widget hands out a line budget — running agents first, then the one-line
N queuedsummary, then finished agents — and increments a hidden-count for running and finished rows that don't fit. The queued summary was dropped with no counter at all, so with five running agents, one queued and one finished the widget rendered+1 more (1 finished): two things hidden, one reported, and no indication anywhere that work was waiting to start. That is precisely the state the queue matters in, since it only appears oncemaxConcurrentis saturated. The queued line's row is now reserved before the running agents are laid out, so it can no longer be dropped; the trade is that one running agent may yield its two rows, and that agent is counted in the overflow total. Counting the queued line instead was rejected: it summarizes N agents, so counting it as one under-reports and counting it as N inflates a total denominated in agent rows. /agentsEnable/Disable now agree with the loader aboutenabled: false(found while closing test-suite gaps). Loading parses frontmatter with a real YAML parser; the menu edited it with regex, and the two disagreed. Enable's pattern was anchored to byte 0, so it only matched whenenabled: falsewas the first frontmatter line — the shape this extension writes, not the shape README tells users to hand-author, wheredescription:comes first — leaving a hand-written agent disabled forever while reportingEnabled <name>. Disable's idempotence check was byte-exact, so a trailing space defeated it and inserted a secondenabled: false; duplicate keys are a YAML error, and since #212 an unparseable agent file is skipped, so the agent vanished from/agentsinstead of showing as disabled — and would abort the next startup understrictAgentFiles, which gates startup only, every mid-session reload skipping whatever it is set to. Deciding whether a file is disabled now calls the parser rather than mirroring it — which was always a read — fixing both the spellings YAML also accepts (False,FALSE, a trailing# comment, a quoted key) and pi's fence scan, which ends the block at any line merely starting---: below a----line the regex read body text as frontmatter and refused to disable a running agent. Editing stays line-wise by design, since re-serializing a parsed document would strip the comments, key order, and quoting of a file users hand-author. That leaves removal recognizing only a lowercase barefalse; the other spellings are detected but not yet rewritable, and now say so instead of claiming success, with/agents → Editas the way through.- The
Agenttool description no longer tells the orchestrator that a zero-tool agent has every built-in.tools: noneand atools:listing onlyext:selectors both parse to an empty built-in list, and the runtime honors that — the shipped e2e fixture pins that such an agent really is handed noread,bash,edit,write,grep,find, orls. The description builder collapsed "empty list" into the same(Tools: *)it uses for an absenttools:, which genuinely does mean all built-ins, so the only capability statement the orchestrator sees before spawning contradicted what the agent would actually get, and work was routed to agents that could only fail at it. An omittedtools:still renders*. An empty one rendersnoneonly when the agent can genuinely call nothing —isolated, orextensions: false— because zero built-ins is not zero tools:tools: nonealongsideextensions:still surfaces every extension tool, and the shipped fixture for that case expects three of them. That configuration now rendersno built-ins, extension tools only, since describing it asnonewould understate the agent and route work away from the only one able to do it — the same failure as the original bug, pointed the other way. The suffix has only ever described built-in scope and still does: extension tools are resolved when the agent runs (extensions may register asynchronously) and cannot be enumerated while the description is built, which is whytools: "*, ext:mcp/search"renders*and always has. The same absent-vs-empty conflation is corrected in the eject serializer, which wrotetools: allfor an empty list; that path is not reachable from the menu today, since eject is offered only for built-in defaults and none ships with an empty tool list, so it is hardening rather than a fix.
[0.15.1] - 2026-08-13
Added
strictAgentFiles— fail startup on a broken agent file instead of skipping it. Off by default. When on, an unreadable or unparseableagents/*.mdaborts extension load and names the file, so a checked-in.pi/agents/can't silently fall through to a different agent. Startup only: mid-session reloads (one perAgentcall) keep warning, since a bad edit shouldn't kill the session on an unrelated spawn. Settable insubagents.jsonor via/agents → Settings → Strict agent files.
Fixed
- A
persist_sessionsubagent's session now records the session that spawned it (#205 — thanks @0xbentang).SessionManager.create(...)was called without pi'sNewSessionOptions, so a persisted subagent wrote a session header with noparentSession— indistinguishable from one started by hand. pi's session picker builds its tree from exactly that field, so every persisted subagent appeared as its own top-level root: a plan → review → implement orchestration left a flat run of sessions with nothing tying them to the session that produced them, and no way to tell them apart but their first message. The spawning session's file path is now passed through, so those sessions nest under their parent in the/resumepicker. Grouping still needs both sessions in the same listing — pi lists per session directory, derived from the cwd unless one is named — so asession_diroverride, or anisolation: worktreeagent running in the copy's cwd, lands in a different directory and keeps showing as a root; the header link is written either way and travels with the file. A spawning session with no file behind it (any in-memory session — the default for a subagent) leaves the field unset, as before. - A subagent that never starts now fails the tool call instead of reporting success (#179 — thanks @xz-dev). A strict
isolation: "worktree"spawn in a directory that is not a git repo, has no commits, or wheregit worktree addfails throws before any child session exists — and theAgenttool caught that throw and returned the diagnostic as ordinary result text. Pi marks a tool result failed only whenexecutethrows; anisErrorflag on a returned result is discarded. The parent model therefore sawisError: falseand read the message as a subagent that had run and reported this, then retried the same doomed call. Both spawn paths now let the throw out. Nothing else changes: a subagent whose run fails, or which is aborted or stopped, still settles on its record and keeps its existing result text and status note — the only calls that now fail are the ones where no agent was ever created. The foreground spinner and widget cleanup moved into afinallyso they cannot be skipped by an early return. - An
Agentcall refused before it ran no longer reports a turn limit that never happened (#199 — thanks @Tonours, fixed in #195 by @xz-dev). Pi reports every pre-execution failure — an extensiontool_callhook returning{ block: true }, an abort, a schema violation — as{ content: [reason], details: {} }withisErrorset. The result renderer's guard tested only for absentdetails, and{}is truthy, so those results walked past every status branch into the tail and printed✗ Aborted (max turns exceeded). The operator debugged a phantom turn limit while the real reason went only to the model, which alone received the tool result text. A result carrying no status now renders that reason verbatim, and the turn-limit wording is reached only by a genuinelyabortedrun instead of serving as the catch-all for anything unrecognised. isolation: "worktree"agents are told to stay in the worktree (#187 — thanks @cad0p). The agent was told its working directory twice — the copy, and the main checkout named by the inherited parent prompt or by the task prompt — and followed the latter, editing and committing in the shared tree. Worktree spawns now carry a<worktree_isolation>block naming the copy as theirs and the parent as off-limits. A prompt directive, not enforcement: local trials went from 5/5 escapes to 1/7, soisolationis still not a hard sandbox.- One malformed agent file no longer takes pi down with it (#212 — thanks @daromaj). A YAML error in any discovered
agents/*.mdaborted extension activation: pi exited 1 before the TUI, naming no file. Unreadable and unparseable files are now skipped, with a warning naming the file and the error. If the skipped file was overriding a same-named agent, a second line names the file that loads in its place.
[0.15.0] - 2026-08-10
Added
fallbackSubagent— choose what happens whensubagent_typedoesn't resolve, including failing closed (#183 — thanks @0xbentang). Dispatch previously repaired every unresolvable type by substitutinggeneral-purpose, so a typo silently ran a different agent, model and tool policy — and for a background or scheduled call, execution began before the caller saw any indication. The new project setting names the substitute instead of assuming one: omitted keeps today'sgeneral-purposebehavior, any enabled agent name routes unresolvable calls there, andnone(or the booleanfalse, which would otherwise be dropped as the wrong type and silently leave the fallback in place) disables it entirely so dispatch fails closed with an error listing the available types. Resolution now happens at one point shared by every caller-supplied spawn — theAgenttool, the scheduler at fire time, cross-extension RPC, and nested delegation — so a refused type never reachesrunAgent, where the old three-tier fallback ingetConfigwould have substituted an agent anyway. Case-ambiguous names (two enabled agents differing only in case, reachable because agents are keyed by filename across three directories) are treated as unresolvable rather than resolved to whichever was registered first. The fallback note is emitted at resolution rather than after the run, so the background and schedule paths carry it too, and a scheduled job now stores the type that was actually requested instead of the substitute. Nested delegation keeps rejecting unconditionally whatever this is set to, so a project-level fallback cannot hand a nested caller an agent outside its allowlist. An explicitly configured fallback that is itself unknown or disabled is reported as the misconfiguration it is.- Opt-in nested subagent delegation — a subagent can delegate to child agents of its own (#164 — thanks @codesoda). Default-off and per-agent: a custom agent receives scoped
Agent,get_subagent_result, andsteer_subagenttools only when its frontmatter setsallowed_subagents(a comma-separated type list, orall—trueand"*"are accepted the wayextensions:/skills:take booleans), and never when it isisolated. The allowlist is a privilege boundary rather than a routing hint: a child runs with its owntools:,extensions:, andisolated:, so delegating grants the parent the union of what the listed agents can do —allreachesgeneral-purpose. Nesting is depth-capped from the main session — main is 0, its subagents 1, their children 2 — throughmaxSubagentDepthinsubagents.json(default2;0or1disables nesting project-wide) or/agents → Settings → Nested depth. A per-agent cap is deliberately not exposed yet: at the default project cap it could only restate what omittingallowed_subagentsalready says, and a tighten-only field is safe to add once someone raises that cap. An agent already at the cap is given no nested tools at all, rather than tools whose every call would fail. Everything a parent can do is ownership-scoped — it can fetch, resume, and steer only its own children — and unknown, disabled, or out-of-list types are refused rather than falling back to general-purpose. Nested spawns resolve agents from a registry built for their own config root (never the process-global registry the main session uses) and revalidatemodel:against Model Scope exactly like a top-level spawn. Nested records stay internal to their parent, absent from top-level tools, lifecycle events, and the agent UI, and are stopped when that parent finishes, is stopped, or ends a resumed turn — a hidden child must not outlive the only agent able to reach it. They still write their own.outputtranscript under the root session's directory (sameoutput_transcriptgate), so a nested run remains inspectable, and their token usage is folded into every ancestor's totals so nested spend stays attributable at any depth, and a nested result that endsstopped,aborted, orsteeredis labelled partial, the same guarantee top-level results carry. Nested children do not occupymaxConcurrentslots: their parent already holds one, and queueing them behind it would deadlock a parent waiting on its own child; the depth cap bounds how deep nesting goes, not how wide, so amax_turnson the delegating agent is what ceilings its fan-out. A subagent session no longer activates this extension at all — that is what keeps a child from building a second agent manager, and it is why the nested tools are injected directly instead; a subagent consequently also gets no/agentscommand, no cross-extension RPC handlers, and nosubagents:readyevent.
Fixed
- A disabled agent is no longer dispatchable, and no longer runs with a mixed identity (found while implementing #183). The
Agenttool resolved types withresolveType(), which reports a canonical name without checkingenabled— so an agent disabled viaenabled: falseor/agents → Disableresolved successfully, produced no fallback note, and then split downstream:getAgentConfigreturned the disabled agent's config to build the system prompt whilegetConfigreturned general-purpose for tools, extensions, skills and prompt mode. The run therefore carried the disabled agent's instructions with a different agent's tool policy. Dispatch now requires the type to resolve to exactly one enabled agent, so a disabled type takes the fallback path — or is refused outright underfallbackSubagent: none. - A foreground agent result now says its output is complete, not partial — it is the task whose completion is in question (#174 — thanks @killianMei). A foreground run that ended early was annotated
output may be partial, which reads as truncated, go retrieve the rest. It isn't: a foreground result already carries the agent's entire output inline, and its parent has no agent id to retrieve anything with — the id travels in the tool result's rendererdetails, which is never serialized to the model, and only the background path printsAgent ID:in text. A parent acting on that reading therefore passed an id it had invented and got backAgent not found: "<id>". It may have been cleaned up., which looks exactly like the record was evicted. It wasn't: foreground completion mutates the record in place, and nothing removes it outside the 10-minute cleanup timer (which keys oncompletedAt, so a long run doesn't age it),clearCompleted()on a session start/switch, or shutdown — none of which the report hit. Foreground results now carry their own outcome note stating that everything the agent produced is above and that it is the task whose completion is in question — hedged for asteeredrun, which was told to wrap up and did, so it may well have finished at the limit, and stated outright forabortedandstopped, neither of which ever delivered a final answer; the background notification andget_subagent_resultkeep the existing note, because their caller holds a 500-char preview and a real id, where the same sentence would be false. Every clause is a statement about state rather than an instruction to act: nothing here can measure whether a wording change improves parent behavior, so removing a false cue (which cannot induce new behavior) and adding an instruction (which can) are not equally safe — and an instruction like "ask before restarting" would also presume a user is present to ask, which is false underpi -p, in scheduled jobs, and in background-driven runs. Regression tests pin the record-retention contract, the absence of an id in foreground result text, and that a subagent session's own activation lifecycle can't evict the parent's records.
[0.14.3] - 2026-07-23
Fixed
- Subagents can use extension tools that register asynchronously, e.g. over MCP (#141 — thanks @philipmw). MCP-backed extensions usually can't enumerate their tools until their servers connect, so they call
registerToolfromsession_start(pi-mcp) orbefore_agent_start(context-mode) rather than at load — eagerly connecting during extension discovery would orphan child processes on pi's non-agent code paths (--help, config, trust probing). The agent runner, however, snapshottedextension.toolsinto a statictools:allowlist immediately afterloader.reload()and beforebindExtensions()firedsession_start. Because pi'sallowedToolNamesgates tool registration — not merely the initial active set — a name missing from that snapshot was dropped permanently, so those tools never reached a subagent even though the main agent had them. The runner now leaves the allowlist unset so pi's live gate admits tools whenever they register, and expresses the name-stable part of the scope (this extension's own orchestration tools, built-ins the agent didn't ask for,disallowed_tools:) as a denylist that pi re-applies on every registry refresh.extensions: false/isolated: truekeep the static allowlist unchanged — nothing can register asynchronously there, and a hard registry gate is the right boundary. ext:selectors keep narrowing correctly when tools register late (fixes #125). Admitting late-registered tools is only half the problem: anext:selector is an allowlist, and a not-yet-registered tool cannot be listed in one — which is whytools: "*, ext:context-mode/ctx_execute", the exact configuration reported in #125, still saw nothing.ext:narrowing is now enforced on the active tool set (what the model actually sees) rather than the registry, re-derived from the loader's live extension maps — the same mapsregisterToolwrites into — so a tool is judged against the selectors whenever it appears. Scope is re-applied on everyturn_end, and the hooks live on the session rather than the spawning call, so steered and resumed turns stay scoped too. Turn 1 is guarded at call time instead:before_agent_startfires insideprompt()and can widen the tool set after that turn's tools are already snapshotted, leaving no window to narrow in, so an out-of-scope call there is refused rather than executed. Selecting a lazy extension (ext:mcp) now surfaces its tools; leaving one out still mutes it, no matter when it registers. Vetoing a call means wrapping thebeforeToolCallhook pi installs on the session (pi exposes the veto to extensions aspi.on("tool_call"), but there is no equivalent for an SDK caller constructing a child session); the wrapper chains to pi's own hook so extensiontool_callhandlers still fire, and api@latestCI job guards that the hook stays reachable, so a future pi that moves it surfaces as a test failure rather than a silently missing veto.- FleetView selection markers now match the rest of the UI (#155 — thanks @xz-dev, closes #122). FleetView was the only view using
⏺(U+23FA) /◯(U+25EF) for its selected/unselected rows; everywhere else — the conversation viewer and agent widget — uses●/○(U+25CF/U+25CB), the base geometric circles with the broadest terminal-font coverage (◯renders visibly oversized in many fonts). Selection is already carried by the accent-vs-dim color, so the exotic glyphs added nothing. FleetView now uses●selected /○unselected, converging on the codebase's existing pair. get_subagent_result(wait: true)is now cancellable (#159 — thanks @exoulster, fixes #158). The tool received the tool-callAbortSignalbut ignored it, so pressingEscduring a wait couldn't cancel it — the call stayed blocked until the child finished, and pi could log a spuriousNo running process to background.. Cancelling now stops only the wait: the background agent keeps running, its result stays unconsumed, and its normal completion notification still arrives. Queued waits are abortable the same way. Because the wait no longer pre-marks the result consumed (a cancelled wait never consumes it), a successful wait still suppresses its own redundant notification by cancelling the held nudge on completion — the queued-wait poll interval was tightened so that cancellation reliably lands inside the notification hold window.- User-scope agent memory honors
PI_CODING_AGENT_DIR(#166 — thanks @biowaffeln). Theusermemory scope hardcoded~/.pi/agent-memory/<name>, ignoringPI_CODING_AGENT_DIReven though every other config consumer (custom agents, enabled models, settings, skills) already routes throughgetAgentDir()— so anyone who relocated their config dir got a stray~/.piresurrected the first time amemory: useragent ran. User-scope memory now resolves to<agentDir>/agent-memory/<name>(default~/.pi/agent/agent-memory/). Existing memories aren't orphaned: when the legacy~/.pi/agent-memory/<name>directory exists and the new location doesn't, it keeps being used — symlinked legacy dirs are ignored, consistent with the file's other symlink defenses — and once the new location exists it wins. Lazy fallback rather than auto-migration, so no files are moved.
[0.14.2] - 2026-07-17
Added
output_transcriptfrontmatter +outputTranscriptproject setting — opt out of a subagent's.outputtranscript (#146 — thanks @Thoughts-One). Every subagent streams its full conversation to a per-subagent JSON-lines transcript under the OS temp dir (<tmpdir>/pi-subagents-<uid>/…/<agent-id>.output, owner-only0700, cleared on reboot); until now that write was unconditional. Setoutput_transcript: falseon a custom agent to write no transcript file or path for it, oroutputTranscript: falseinsubagents.jsonto make transcripts opt-in for the whole project (a custom agent's frontmatter overrides the project default). Useful when run transcripts shouldn't sit on disk for backup or DLP tooling to ingest. Scope is deliberately narrow — it governs only the.outputtranscript, not the persisted pi session (persist_session), worktree commits (isolation: worktree), or memory files — so keeping a run fully off disk means setting those too. Default is unchanged: with neither flag set, transcripts are written exactly as before. The write decision is centralized onrecord.outputFile, so every downstream consumer (streaming, notifications, the transcript footer) keys off a single gate.
Fixed
- Bordered conversation-viewer rows stay exact-width at double-width truncation boundaries (#153 — thanks @xz-dev). The conversation viewer pre-pads each row to the inner width and truncates it to fit between the
│borders, buttruncateToWidthwas called without itspadflag — so when a truncation boundary fell mid-way through a double-width character (CJK, wide emoji), the result came back one column short and the right border shifted left by a column on that row. The row builder now truncates with padding on, restoring the trailing column, so every bordered row renders at exactly the box width regardless of where a wide glyph lands. A regression test sweeps a double-width character across every truncation boundary and asserts each rendered line is exactly the requested width. - Isolated subagents keep extension-registered custom providers on pi 0.80.8+ (fixes #151 via #152 — thanks @0xbentang). pi 0.80.8 replaced
createAgentSession'smodelRegistryoption withmodelRuntime, and the agent runner still passed the now-ignoredmodelRegistry— so pi built a fresh model runtime from disk for the child session. Because isolation also disables extension loading, that fresh runtime had neither the extension-registered custom provider nor its auth: anisolated: trueagent pinned to such a provider failed preflight withNo API key found for <provider>, while the same agent withisolated: false— or on any pi <0.80.8 — worked. The runner now forwards the parent session'sModelRuntime(read off theModelRegistryfacade onctx.modelRegistry) asmodelRuntimewhen the running pi exposes one, and still passesmodelRegistryfor the pre-0.80.8 range — so the fix spans the whole>=0.80.0peer range without changing it: older pi omits the new field and takes the legacy path, 0.80.8+ inherits the parent runtime. Extensions and tools stay isolated exactly as before — only the model providers and their auth are inherited. Api@latestCI job guards the reachability of that runtime accessor (aprivatefield the fix reaches through), so a future pi that renames or hides it surfaces as a test failure instead of a silent return of this bug.
[0.14.1] - 2026-07-14
Added
maxthinking level is now advertised in the frontmatter/tool/wizard choices (#147 — thanks @justin-ramirez-gametime). pi 0.80 addedmaxto itsThinkingLevel, and the extension already forwarded the value unchanged, but the Agent tool description, generated-agent template,/agentscreation wizard, and README all stopped atxhigh— hiding a valid capability. Those four surfaces now come from one shared list so they can't drift behind pi again. Actual availability still depends on the host pi version and the selected model; pi clamps unsupported levels down.
Fixed
- Runs whose final assistant turn failed are now reported as
error, notcompletedwith an empty result (fixes #144 — thanks @possibilities for the diagnosis). pi resolves an exhausted-retries provider failure normally — the final assistant message carriesstopReason: "error"plus anerrorMessage, no rejection — so the manager mapped such runs tocompletedand every consumer (foreground tool result,get_subagent_result, background notifications, resume, scheduler, RPC events) saw a clean success readingNo output.— or worse, an earlier turn's text presented as the fresh answer, since the history fallback walks past empty messages. The runner now inspects the final assistant message and reports a failure in two cases: the turn stopped withstopReason "error", or it hit the output-token ceiling (stopReason "length") having produced no text at all — a silent max-token death that reproduced the same empty-No output.symptom. Status still derives from how the final turn stopped, never from whether earlier turns produced text: empty-but-clean finals (tool-call-only or thinking-only endings) staycompleted, alengthstop that did produce text is a legitimate truncated answer and stayscompleted, partial-text provider errors are still failures, and the walk-back fallback keeps preserving partial output for aborted/steered runs. (Anabortedfinal turn needs no special-casing here — the manager's hard-abort flag andstoppedguard already surface it asaborted/stopped.) The response-text collector also no longer resets on user/tool-resultmessage_startevents (it tracked the last message, not the last assistant message). A failed run still surfaces any output it did produce: the tool result showsAgent failed: <error>followed by that text under aPartial output before the failure:label — and the history fallback is now bounded to the current invocation, so a failed resume no longer returns the previous turn's answer as this run's result (it returns empty). Behavior change: runs that previously endedcompletedwith an empty result now enderrorcarrying the provider message, andsubagents:failedfires wheresubagents:completeddid; scheduled jobs recordlastStatus "error"for them. - A subagent can allowlist a package-installed extension by its package name, not just its source directory (fixes #143 — thanks @possibilities). Our extension-scoping (
extensions:/exclude_extensions:/tools: ext:…) named an extension from its file path, and for anindex.tsentry it used the parent directory — so a package installed viapi.extensions: ["./src/index.ts"](like this one) only ever matched assrc, an unstable, collision-prone name that no user would guess;extensions: [pi-subagents]silently matched nothing. An extension now also answers to its package's unscoped short name (@tintinweb/pi-subagents→pi-subagents), read from the nearestpackage.jsonwhosepi.extensionsmanifest actually declares that entry. This is an added alias — the path-derived name keeps working, so nothing that already matchedsrcbreaks — and it fires only for genuinely manifest-declared package entries, so a loose extension is never misattributed to a co-located project's name. - A pi-subagents activation that a child session filtered out no longer advertises or answers cross-extension RPC (fixes #142 — thanks @possibilities). pi runs every extension factory before applying an agent's
extensions:filter and only delivers lifecycle events (session_start, …) to the survivors, but thepi.eventsbus is shared with the filtered-out activations too. Because we registered the RPC handlers and emittedsubagents:readyat factory time, a child agent whoseextensions:omitted pi-subagents still saw asubagents:readybroadcast and a successfulsubagents:rpc:ping, yet everysubagents:rpc:spawnfailed withNo active session— itssession_startnever fired, so the spawn handler had no context. The RPC handler registration and thesubagents:readybroadcast now happen on the first boundsession_startinstead of at factory time, so a session that excludes pi-subagents stays completely silent on the RPC channels — behaving like a session where it was never installed, rather than advertising a spawn service it can't provide. Emitting readiness after all factories have loaded also closes a latent race where a consumer whose factory ran after ours could miss the event. No change for sessions that do load pi-subagents:subagents:readystill fires and RPC still works, just atsession_start.
[0.14.0] - 2026-07-13
Added
- Project custom agents are also discovered from
.agents/agents/<name>.md(#133 — thanks @wenerme; closes #132). Projects that keep their agent assets in the shared cross-tool.agentsworkspace (the same convention this extension already reads for.agents/skills/) can now define subagents there instead of duplicating files into.pi/agents/. Discovery precedence isglobal < .agents/agents < .pi/agents: on a name clash between the two project locations,.pi/agents/wins —.piremains the project authority, and the/agentscreate/eject/disable flows keep writing there;.agents/agents/is a read location only.
Changed
- BREAKING: Dev/test toolchain now tracks pi 0.80.x; pi peer floor raised to
>=0.80.0(as diagnosed by @philipmw in #129). The committed lockfile had pinned the@earendil-works/pi-*peers to 0.75.5 (the latest published when it was generated), so tests and typecheck exercised an older API surface than the pi that actually runs the extension — including 22 tests that silently never ran on 0.75.5 and now do (suite skip count 26 → 4 between the two lockfiles). The lockfile now resolves the peers to 0.80.6, and the test suite imports pi-ai's relocated faux/model helpers (registerFauxProvider,getModel—/compat-only since pi-ai 0.80) through a single re-export module,test/helpers/pi-ai.ts, so the next relocation touches one file. Because the test surface no longer resolves on ≤0.75.x,peerDependenciesmove from>=0.74.0to>=0.80.0to match what is actually tested. Migration: installs under pi <0.80 may emit unmet-peer warnings (or fail under strict peer resolution such asnpm --strict-peer-deps/ pnpm defaults) — upgrade pi to ≥0.80; runtime behavior itself is unchanged on any pi version, since pi substitutes its own bundled modules for extension imports at load time. Live-mode e2e (PI_E2E_LIVE=1) now fails fast with a clear error when the pinnedPI_PROVIDER/PI_MODELisn't in pi-ai's builtin catalog, instead of silently letting the session resolve a different model.
Fixed
- Output-file streaming survives session compaction (fixes #145 — thanks @possibilities for the diagnosis). Compaction replaces
session.messageswith a shorter, summarized array, which stranded the streamer's write index past the new end — the flush loop never matched again and the agent's output file silently froze for the rest of the run, exactly on the long background runs that auto-compact. The streamer now flushes any not-yet-written tail when compaction starts and re-anchors its index to the rebuilt array after a successful compaction — deferred one microtask, because on the overflow-retry path pi trims the trailing error assistant message after emittingcompaction_end, and a synchronous anchor would skip the first post-compaction message. Aborted and failed compactions leave the session untouched and change nothing. Verified against a real piAgentSessiondriving a realsession.compact()in the new e2e regression. - FleetView no longer steals arrow/Enter/Esc keys from other interactive components (fixes #123 — thanks @TommyC81 for the report). pi delivers terminal input to extension listeners before the focused component, and selector/input dialogs (
ctx.ui.select& co.) swap the prompt editor out whilegetEditorText()still reads the detached — empty — editor. So while subagents were running, FleetView's empty-prompt gate passed and it consumed the navigation keys that belonged to whatever dialog was actually focused: other extensions' pickers (e.g. rpiv-ask-user-question), pi's own menus, and even/agents → Settingsitself. FleetView now checks that pi's prompt editor is the focused component before touching any key (pi's editor is anEditorsubclass; every dialog is not), and an in-progress list navigation is dropped the moment something else takes focus. Unknowable focus errs toward the editor, so list activation keeps working; non-Editorcustom editor components simply flow keys through untouched. - Widget and viewer lines keep their dim styling after nested color annotations (#136 — thanks @xz-dev). pi themes close a foreground color with a bare
\x1b[39m, so the threshold-colored context-fill percent insideformatSessionTokens(warning at ≥70%, error at ≥85%) also terminated the surrounding dim style — the closing)and any following text on the widget's running-agent line and the conversation-viewer header bled to the terminal's default color. A smallfgPreservingNestedStyleshelper re-opens the outer color after each nested reset (derived from the theme itself, so it stays theme-agnostic and is a no-op underNO_COLOR). get_subagent_resultwithwait: truenow waits for queued agents (#127 — thanks @benrhodeland). A background agent past the concurrency ceiling sits in the queue with no run promise yet, so the wait path (gated onstatus === "running" && record.promise) skipped it and returnedNo output.immediately — the orchestrator read a queued agent as "finished with nothing". The wait now also coversqueued: it polls until the queue starts (or stops) the agent, then awaits the run like any running agent. Most visible with parallel background spawns in one message, where spawns beyondmaxConcurrent(default 4) queue.- Subagent activations no longer clobber the
Symbol.for("pi-subagents:manager")registry (#128 — thanks @benrhodeland). Child sessions re-activate the extension in the same process (session.bindExtensionsin the agent runner), and every activation overwrote the global registry slot — pointing cross-package consumers (RPC extensions, headless hosts) at a short-lived child manager whose shutdown could then delete the root session's entry entirely. The first activation now claims the slot, child activations leave it alone, and only the owning activation releases it (identity-checked) on shutdown.
[0.13.0] - 2026-06-30
Added
- Steer a running agent from the conversation viewer. The live conversation overlay (FleetView's
Enter, or/agents → Running agents) now lets you redirect an agent without leaving the view: pressEnterto open an inline composer, type a message,Enterto send —Escor an empty submit just returns. The message is delivered through the same path as thesteer_subagenttool (AgentManager.steer()→session.steer, or queued ontopendingSteersif the session isn't ready yet), so it appears as a user message and redirects the agent after its current tool execution; feedback is the message showing up in the live transcript you're already watching. The affordance is offered only while the agent is still running/queued (mirrors thex/stop affordance), and the viewer stays modal — every existing shortcut (x/xstop, arrows/j/kscroll,qclose) is untouched, andEnterwas previously inert here so nothing is overridden. The idle footer was reorganized to actions-left / navigation-right so the full scroll-key hint (↑↓ scroll · PgUp/PgDn or Shift+↑↓ · Esc close) stays fully visible down to 80-column terminals; theN lines · %readout returns on the left whenever there's spare width. - Forgiving model resolution for agent
model:pins.resolveModelnow tolerates cosmetic id variations and falls back across providers, so a qualified or date-pinned config resolves widely instead of silently dropping to the parent model:.and-are treated as equivalent in version numbers (claude-haiku-4.5≡claude-haiku-4-5); a trailing-YYYYMMDDdate stamp is optional (anthropic/claude-haiku-4-5-20251001matches an undated registry id); and aprovider/modelIdthat isn't available under the named provider retries the bare id against every provider (the named provider is still preferred when present). An exact match still wins over a tolerant one, so dated snapshots aren't conflated — the precedence is exact → fuzzy-under-named-provider → same model under any provider → unavailable. /agents → Agent typesshows each agent's full description and what its model resolves to. The list now renders withSettingsList(like the Settings menu) instead of a flat selector: the highlighted agent's full description shows on its own line below the list, so long descriptions no longer wrap and push the agent names out of alignment. The model column shows the configured model, flags it(unavailable, fallback: inherit)when it can't be resolved against the registry (it would silently inherit the parent model at runtime), and surfaces the resolved target(→ provider/id)when resolution lands on a different provider or version than configured.widgetModesetting — control what the above-editor widget shows:all/background/off(#117 — thanks @Alan-TheGentleman; fixes #118). Foreground agents already render inline as theAgenttool result, so also listing them in the persistent widget double-rendered the same run (most visible in tmux/zellij).widgetMode(via/agents → Settings → Widget, orsubagents.json) selects the widget's contents:allshows every agent (the previous behavior),backgroundshows background/queued/scheduled/RPC runs but hides foreground, andoffhides the widget entirely (agents still appear inline and in FleetView). Applied live — toggling refreshes immediately. Filtering keys off a new tri-stateAgentRecord.isBackgroundcaptured at spawn (true= background,false= foreground,undefined= undeclared, e.g. a cross-extension RPC spawn), independent of the UI-onlyinvocationsnapshot — so scheduler- and RPC-spawned background agents stay visible instead of vanishing; only runs known to be foreground are dropped. The running-status line was also refactored from one multilineTextinto two component rows, so rapid partial updates replace cleanly instead of leaving stale rows behind in terminal multiplexers — identical on-screen output, no more ghost lines.
Changed
- The built-in
Exploreagent's model pin is nowanthropic/claude-haiku-4-5(was…-4-5-20251001). The stale date stamp is dropped to match theanthropic/claude-sonnet-4-6/anthropic/claude-opus-4-6convention used by the create-agent wizard; combined with the forgiving resolution above,Explorenow picks up its fast/cheap model across registry variations (dated, undated, dotted, or under a non-anthropicprovider) instead of silently falling back to the parent model. - The above-editor widget now hides foreground agents by default (
widgetModedefaults tobackground). Foreground runs still render inline as theAgenttool result (and in FleetView); set/agents → Settings → Widgettoallto restore the previous show-everything view, oroffto hide the widget. Existingsubagents.jsonfiles load unchanged (absent →background).
[0.12.0] - 2026-06-24
Added
- FleetView — a Claude Code-style subagent navigator below the editor. A persistent, navigable list of
main+ every active subagent renders beneath the editor whenever agents are running — auto-shown, no keypress needed — mirroring Claude Code's bottom fleet bar:⏺/◯selection markers, agent type + description, right-alignedelapsed · ↓ tokens, and a↓ N moreoverflow once past five rows. Press↓(or←) at an empty prompt to move focus into the list,↑/↓to select,Enterto open the selected agent's live, auto-updating conversation overlay, andEsc(or↑abovemain) to return to the prompt. Implemented as abelowEditorwidget with all key handling routed throughonTerminalInput(which fires before the editor); it only captures arrow keys at an empty prompt — and acts on key-press only (kitty-protocol release events are ignored, otherwise each tap moved twice) — so typing, history, and cursor movement are untouched. Rows are ordered earliest-launched first; only openable agents (those with a session) are shown, so pending/queued agents appear once they start andEnternever dead-ends; finished agents linger ~4s before dropping out (their elapsed freezes at completion); and a viewer stays open through its agent's completion so the final output remains readable. Selection follows the viewed agent by id, so closing a viewer returns you to the same agent even if the list reordered while it was open. Every rendered line is width-clamped — the narrow-terminal crash/flicker class previously fixed in v0.2.7 and #7. Toggle via/agents → Settings → Fleet view(default on; pure-UI, so no LLM-context cost).
[0.11.0] - 2026-06-23
Added
persist_session/session_diragent frontmatter — persist a subagent as a real pi session (#111 — thanks @codesoda).persist_session: trueruns the subagent throughSessionManager.create(...)instead ofSessionManager.inMemory(...), so its full transcript is written to pi's normal session location (~/.pi/agent/sessions) — inspectable and resumable after the fact, like a top-level session — rather than living in memory only. Useful for long-running, multi-round orchestrations (plan → review → implement → verify) where each subagent's conversation is worth keeping.session_diroptionally overrides where the persisted session is written (absolute,~, or agent-cwd-relative path); omitted, persistence follows pi's own precedence —PI_CODING_AGENT_SESSION_DIR, then the settings manager'sgetSessionDir(), then pi's default location. Both default off/unset, so existing agents are unchanged: the in-memory path is byte-identical to before, and the sidechain.outputtranscript is still written either way.
[0.10.4] - 2026-06-23
Fixed
- Background agent records lost before result is read (#108 — thanks @philipmw). On session switch or
/new//resume,clearCompleted()removed completed agent records regardless of whether the LLM had retrieved the result, causingget_subagent_resultto return "Agent not found" for agents that had finished but hadn't been checked yet.clearCompleted()now accepts askipUnconsumedflag; session event handlers passtrue, so records withresultConsumed=falseare preserved across session transitions. The 10-minute cleanup timer handles eventual eviction. Note: a full session shutdown (session_shutdown) callsdispose()which clears all records unconditionally — that path is not affected by this fix.
Added
- Foreground agent lifecycle completion and conversation logging (#105 — thanks @benrhodeland). Two gaps closed: (1)
onCompletenow fires for foreground agents, emittingsubagents:completed/subagents:failedlifecycle events and writing asubagents:recordentry to the parent JSONL — previously only background agents emitted these, leaving cross-extension observers with an orphanedsubagents:startedevent and no matching completion.resultConsumedis pre-set so the callback skips notifications (the result is returned inline); no change to the tool's return value. (2) Foreground agent conversations are now streamed to.outputfiles (same.pi/output/agent-<id>.jsonlpath as background agents) — inline subagent transcripts were previously permanently lost afterspawnAndWaitreturned.
[0.10.3] - 2026-06-12
Added
SpawnOptions.cwd— spawn a subagent in a different working directory (#96 — thanks @madeleineostoja). For RPC/programmatic callers (not exposed on theAgenttool — the LLM-visible surface is unchanged). The agent's tools operate in the target directory and the prompt's environment block describes it, but.piconfig keeps loading from the parent session's project (newRunOptions.configCwdsplit): the target's.piextensions never execute, and its agents/skills/settings/memory are not picked up — spawning into an untrusted directory sends a worker there with the parent's toolbox, rather than "opening pi there." Composes withisolation: "worktree": the worktree is created from the target directory's repo, the agent works at the equivalent subdirectory inside the copy (a monorepo-package cwd keeps its scoping instead of silently widening to the repo root — newWorktreeInfo.workPath), and the resultingpi-agent-*branch lands in that repo, with the completion message naming it so the orchestrator merges in the right place. Validation is strict, typed, and early — non-strings, relative paths, nonexistent paths, and files all throw curated errors atspawn()(before queueing) and are re-checked at queue drain, surfacing as RPC error envelopes (nullis treated as unset). On dispose, worktree registrations are pruned in every repo that received one; only a hard crash can leave a stale entry (then:git worktree prunein the target repo).
[0.10.2] - 2026-06-10
Added
exclude_extensions:agent frontmatter — extension denylist for subagents (#94 — thanks @ramhaidar). Applied after theextensions:include set; exclude wins, including overtools: ext:selectors (an excluded extension never loads, so itsext:reference becomes the usual orphan warning). The key use case:extensions: true+exclude_extensions: pi-notify— all extensions except a noisy one, without hand-maintaining an allowlist. Plain canonical names only (case-insensitive); paths,*, and unmatched names fireextension-error:…warnings (warn-not-abort, as withextensions:mismatches);extensions: false+ an exclude warns that the exclude has no effect. Not a sandbox: excluded extensions' factory code still executes once during loading — exclusion suppresses handler binding and tool registration, not load-time side effects. The negation syntaxextensions: ["*", "!name"]was deliberately rejected: an unquoted!nameis a YAML tag and silently mis-parses.toolDescriptionModesetting — opt-in compact Agent tool description (#91 — thanks @tiberiuichim). The full Claude Code-style description costs ~1,400 tokens with the default agents and grows with each custom agent (the type list embeds full agent descriptions) — significant for small/local models.toolDescriptionMode: "compact"(via/agents → Settings → Tool descriptionorsubagents.json) swaps in a ~75% smaller description: one-line type list (first sentence of each agent description), terse usage notes, per-option details left to the parameter descriptions. Default"full"is byte-identical to before — the rich description's guardrails are deliberately load-bearing and stay the default. A third mode,"custom", registers a user-authored description from<cwd>/.pi/agent-tool-description.md(project) or<agentDir>/agent-tool-description.md(global; project wins), with{{placeholder}}substitution keeping the dynamic parts live —{{typeList}},{{compactTypeList}},{{agentDir}},{{scheduleGuideline}}— so a hand-written description can't drift out of sync with the registered agents (the advertised-vs-spawnable staleness #92 just fixed). Unknown placeholders are left verbatim with a stderr warning; a missing/empty file falls back to"full". Only the prose is customizable — the parameter schema stays code-owned. A ready-made starting point ships atexamples/agent-tool-description.md, reproducing the full description exactly (CI-enforced byte-identical, so the example can't go stale). LikeschedulingEnabled, the mode is read at tool registration — changing it applies on the next pi session. The issue's original ask (move the description to a skill) isn't possible in pi: tools must register their description in the tool schema for the model to call them; skills are lazily-loaded instructions, not tool registrations.
Fixed
- Conversation viewer honors custom
tui.select.*keybindings (#99 — thanks @owenniles). The viewer hardcoded its scroll keys and discarded theKeybindingsManagerpi injects intoctx.ui.custom(), so user bindings (e.g. emacs-stylectrl+p/ctrl+nontui.select.up/down) worked in pi core selectors but not here. Scrolling now resolves throughtui.select.up/down/pageUp/pageDown; the viewer-specifick/jandshift+arrowaliases still work alongside, and behavior without custom bindings is unchanged (thetui.select.*defaults are the previously hardcoded keys).
[0.10.1] - 2026-06-10
Added
disableDefaultAgentssetting (#92 — thanks @TommyC81). When on, the three built-in default agents (general-purpose, Explore, Plan) are skipped at registration — only user-defined.pi/agents/*.mdagents are advertised and spawnable. User agents are unaffected, including ones overriding a default by name; with no user agents defined, spawning falls back to the hardcoded generic config. Off by default; toggle via/agents → Settings → Disable defaultsordisableDefaultAgentsinsubagents.json. LikeschedulingEnabled, the Agent tool's type list reflects the change on the next pi session (tool schema is registered at startup).
Fixed
- Agents with
enabled: falseare no longer advertised in the Agent tool description (#92).buildTypeListTextlisted every registered agent, including disabled ones thatisValidTypethen refused to spawn — the LLM was offered types it could never use. The type list now filters throughgetAvailableTypes(), matching thesubagent_typeparameter description. - Agent tool type list no longer built from pre-settings state. The description text was captured into a variable before persisted settings were applied; it's now built at tool-registration time, after
subagents:settings_loaded. - Committed work from
isolation: "worktree"subagents is now preserved (#68 — thanks @rylwin). If an isolated subagent creates its own commit, cleanup previously saw a cleangit status, treated it as "no changes", and removed the detached worktree — silently discarding the commits. The worktree now records its base SHA at creation, and cleanup creates the expectedpi-agent-*branch whenever HEAD moved past it, even with a clean tree. - Automatic commits in isolated worktrees skip local Git hooks (#68). The preservation commit at worktree cleanup now uses
--no-verify, so a failing local pre-commit hook can't abort it (which previously surfaced ashasChanges: false— the agent's work lost).
[0.10.0] - 2026-06-01
⚠️ Breaking:
extensions:andtools:in agent frontmatter semantics changed. Theextensions: [...]array now selects which extensions load, not which tool names surface. Agents that previously used the array form will behave differently — see migration below. Thetools:field also grew newext:and*selector forms; existingtools:values without these selectors are unchanged.
extensions: [...]is now an extension allowlist applied at load time, not a tool-name substring filter. Each entry is an extension name, a path (absolute,~/-prefixed, or relative-to-cwd), or"*". Migration:extensions: ["mcp"]previously loaded every extension and then surfaced only tools whose names containedmcp. To keep all extensions, useextensions: trueorextensions: "*". To narrow, name the extensions or point at their files."*"composes:extensions: "*, /abs/path/extra-ext.ts"is all defaults plus one path-loaded.tools:now acceptsext:selectors and*. Gotcha: atools:value containing onlyext:entries yields zero built-in tools — add*(e.g.tools: "*, ext:foo") to keep the built-ins. And anyext:entry flips extension tools to an explicit allowlist (non-listed extensions stay loaded but expose no tools). Atools:with noext:entries is unchanged.extensions:is the sole loading authority.ext:fooonly narrows tool exposure within the already-loaded set; it cannot pull an extension in.extensions: false+tools: "ext:foo"loads nothing and warns thatext:foois orphaned. To expose one extension's tool from an otherwise-narrow agent, name the extension explicitly:extensions: [foo]+tools: "ext:foo/bar".
⚠️ Heads-up — widget glyphs changed (visual only): turn count now renders as
↻N(was⟳N) and compaction count as⇊N(was↻N). Fix for #84 —⟳overflowed its cell in common monospace fonts. No API, behavior, or output-format changes — only the glyphs. If you grep agent stats lines or pipe widget output through scripts, update your patterns:⟳→↻(turns),↻→⇊(compactions).
Added
tools:acceptsext:extension-tool selectors and a*built-in wildcard. Entries in thetools:CSV are now partitioned: plain names are the built-in allowlist (unchanged);*expands to all built-ins (symmetric withextensions: "*");ext:foo/ext:foo/barselect extension tools. Anyext:entry flips extension tools to an explicit allowlist — only tools named by anext:selector reach the LLM, and extensions not named stay loaded (theirsession_startetc. handlers still fire) but expose no tools.ext:fooexposes all offoo's tools;ext:foo/barnarrowsfooto justbar(multipleext:foo/xentries union; a bareext:fooalongsideext:foo/barlets narrowing win).ext:is narrowing-only — it does not load extensions.extensions:remains the sole loading authority; anext:fooagainst an extension thatextensions:excluded (includingextensions: false) is orphaned and warns viaonToolActivity(extension-error:ext:foo …). With noext:entry present, extension-tool behaviour is unchanged.ext:is name-only (matched by canonical name, so it composes with path-loaded extensions); paths still go inextensions:.isolated: trueignoresext:selectors.- Stop a running agent from the conversation viewer. In
/agents → Running agents, select an agent and pressx(thenxagain to confirm) to abort it. The two-press guard prevents an accidental kill; the footer showsx stop→x again to STOP. This works for background agents — which a globalEsccan't unambiguously target — whileEscstill stops a blocking foregroundAgentcall. Wires the existingAgentManager.abort(id)to the viewer (onStopcallback); the affordance only appears while the agent isrunning/queued. Addresses the common "how do I stop a background subagent?" question (#88).
Changed
- BREAKING:
extensions: [...]in agent frontmatter is now a loader-level extension allowlist, not a tool-name filter. Previously astring[]value filtered exposed tool names by substring (t.startsWith(e) || t.includes(e)) while every discovered extension still loaded and ran its handlers. Now each entry selects an extension: a bare name keeps the matching default-discovered extension, a path (absolute,~/-prefixed, or relative-to-cwd) loads that extension fresh viaadditionalExtensionPaths, and"*"keeps all default-discovered extensions. Entries compose —["*", "/abs/foo.ts"]is all defaults plus foo,["mcp", "/abs/foo.ts"]is just those two. Excluded extensions no longer bind handlers or register tools (their factory still runs once duringreload()). Directory extensions (foo/index.ts) match by the parent directory name. Extension names match case-insensitively (extensions: [Mcp]resolves the same as[mcp]); tool names withinext:foo/barselectors remain case-sensitive (they're matched against pi-mono's registered identifiers). Unmatched names and failed paths warn viaonToolActivitybut do not abort the subagent (see the heads-up above for migration). - Non-normal subagent outcomes are now stated explicitly in the text delivered to the parent, so the orchestrator can't mistake a stopped/incomplete agent for a completed one. The foreground
Agentresult,get_subagent_result, and the<task-notification>summary all append a clear note forstopped(user abort) →(STOPPED BY THE USER before completion — output is partial; the task was NOT finished),aborted(turn limit) →(aborted — hit the turn limit before completion; output may be incomplete), andsteered→(wrapped up at the turn limit — output may be partial).stopped(human intervention) is kept distinct fromaborted(turn-budget cutoff); a cleancompletedadds no note. Extracted asgetStatusNoteinsrc/status-note.ts. BUILTIN_TOOL_NAMESis derived from pi's tool factories (createCodingTools+createReadOnlyTools) rather than a hardcoded list, so the built-in set tracks pi-mono automatically. Internal; no behavior change (the resolved set is the same seven names).
Fixed
- Turn-count glyph in the agent widget no longer overflows its monospace cell (#84 — thanks @linozen).
formatTurnsused⟳(U+27F3 CLOCKWISE GAPPED CIRCLE ARROW) from the Miscellaneous Mathematical Symbols-A block, where common monospace fonts (Iosevka Nerd Font Mono, Menlo, SF Mono, JetBrains Mono) draw the glyph visually wider than one cell despite its Neutral East Asian Width — making the next character (the digit) overlap the glyph. Replaced with↻(U+21BB CLOCKWISE OPEN CIRCLE ARROW) from the standard Arrows block, which renders cleanly at one cell in those fonts. To avoid colliding with the existing compaction indicator (which previously also used↻), the compaction glyph moves to⇊(U+21CA DOWNWARDS PAIRED ARROWS) — same Arrows block, also single-cell, visually distinct. Widget vocabulary now reads:↻5≤30for turns,⇊2for compactions. Pi UI consumers / scripts grepping for the glyph in stats lines must update. tools: nonenow actually yields zero built-in tools.getToolNamesForTypetreated an explicit emptybuiltinToolNames([], produced bytools: none) as "unspecified" and fell back to all 7 built-ins. It now distinguishes an omitted field (undefined→ all built-ins, for default agents) from an explicit empty list ([]→ zero), consistent withgetConfig. Same fix makestools:values containing onlyext:selectors yield zero built-ins as documented.tools:typos no longer silently break tool-calling (#75). Two parts: (a)allwas previously parsed as a literal tool name, producing a one-element allowlist of the non-existent tool"all"— the model then returned an empty response or emitted raw XML tool calls, all withstatus: completedand no error.parseToolsFieldnow treatsall(case-insensitive) as an alias for the*wildcard, both standalone and inside a CSV. (b) Plain entries intools:are expected to be built-in names (extension tools route throughext:), so an unknown name there is unambiguously a typo.runAgentnow emits atools-error:tool "X" requested by agent "Y" is not a known built-inevent viaonToolActivityfor each unrecognized plain entry — same surfacing channel as the existingextension-error:warnings.- Subagents with
extensions: truenow actually expose extension-registered tools (MCP, etc.) (#47).runAgentpreviously passed only the built-in tool names as thetools:allowlist tocreateAgentSession, so pi-mono'sallowedToolNamesgate rejected every extension-registered tool at registration —extensions: trueagents silently got only the 7 built-ins.runAgentnow enumerates extension tool names from the resource loader afterreload()and builds the full master allowlist (built-ins + permitted extension tools), so pi-mono's gate admits them from the first instant of the session.disallowedToolsand the internalAgent/get_subagent_result/steer_subagentexclusions are applied uniformly to built-in and extension tools at construction — no post-constructionsetActiveToolsByNamenarrowing. - Append-mode subagents no longer defeat the LLM's KV cache (#73 — reported by @jeffutter). The assembled child prompt placed the per-spawn-varying
<active_agent>tag and# Environmentblock before the ~8k-token inherited parent prompt, and wrapped the parent prompt in<inherited_system_prompt>tags. Because KV caches key on a byte-identical prefix, every subagent spawn reprocessed all ~8k shared tokens from scratch (~40s on slower hardware). The parent prompt is now emitted verbatim at the start of the prompt (wrapper dropped), so it forms an identical, cacheable prefix with the parent session and across every spawn; the static<sub_agent_context>bridge follows, then the varying<active_agent>tag and env block.replacemode is unchanged (it inherits no parent prefix). The<active_agent>tag stays present and is parsed position-independently, so downstream permission resolution is unaffected. Mirrors the fix in gotgenes/pi-packages#180.
[0.9.1] - 2026-05-30
Added
Agent,get_subagent_result, andsteer_subagentnow surface in pi's default system prompt (#87 — thanks @that-yolanda). AddspromptSnippetto all three (a line in the prompt'sAvailable tools:section) andpromptGuidelinestoAgent(bullets inGuidelines:). The tools were always callable via the tool-call API; this only adds system-prompt reinforcement for prompt-following models. No schema or tool-call changes.
[0.9.0] - 2026-05-30
Heads-up — orchestrator behavior may shift. This release substantially rewrites the
Agenttool description and the three default-agent descriptions (general-purpose,Explore,Plan) to mirror Claude Code's upstream wording. No API, schema, or tool-call shape changes — purely a prompt-engineering shift, but a load-bearing one:
- Agent selection may drift. The new agent descriptions carry richer positive ("Use it to …") and negative ("Do NOT use it for …") guidance plus search-breadth hints for
Explore("quick"/"medium"/"very thorough"). For ambiguous tasks where the orchestrator previously picked one default agent, it may now pick another — typically more correctly, but the choice may differ from prior releases.- Subagent briefings will skew longer and more contextual. The restored upstream guardrails and the new
## Writing the promptsection actively coach "smart colleague who just walked into the room"-style prompts. Expect more context, more constraint, more upfront framing in theprompt:field the orchestrator passes to subagents.- Parallel/background patterns more strongly enforced. The merged bullet on parallel execution now explicitly says
run_in_background: trueis required on each tool call for actual concurrency, and that the orchestrator MUST send a single message with multiple tool uses when the user says "in parallel." Workflows relying on sequential-foreground default behavior are unaffected.- If you have tests or workflows that depend on the prior agent-selection or briefing behavior, pin to a v0.7.x release.
Added
scopeModelssetting — opt-in subagent model-scope enforcement (off by default). New setting toggleable via/agents → Settings → Scope models. When enabled, the effective model of each subagent spawn is validated againstenabledModelsfrom pi's settings (which pi manages via its own/scoped-modelsUI; pi-subagents only reads it). Both pi settings files are honored: global<agentDir>/settings.jsonplus project-local<cwd>/.pi/settings.json, with project overriding global — mirrors pi'sSettingsManagerdeep-merge and our ownsubagents.jsonprecedence. Out-of-scope handling depends on source: caller-supplied viaAgent({ model: "..." })→ hard error to the orchestrator with the allowed list; frontmatter-pinned or parent-inherited → warning toast + the agent runs anyway (preserves "frontmatter is authoritative" guarantee from v0.5.1;scopeModelsis a guardrail against runtime LLM choices, not user-level config). Limitation: only exactprovider/modelIdentries inenabledModelsare honored — globs (*sonnet*), bare model IDs, and:thinkingsuffixes that pi itself supports are silently dropped here. Matches pi's/scoped-modelspicker output, so the limitation is invisible to UI users.
Changed
Agenttool prompt restructured to mirror Claude Code's upstream Agent tool description format. Section headings now match upstream (## When not to use,## Usage notes,## Writing the prompt); the auto-generated agent list renders as a flat list (noDefault agents:/Custom agents:sub-headers) with a per-agent(Tools: …)suffix derived from each agent'sbuiltinToolNames(or*when the agent has the full built-in set). Restored upstream's load-bearing guardrails that were missing or compressed in the old prompt: "result is not visible to the user → summarize", "trust but verify", "fresh agent / self-contained prompt" on resume, "tell the agent whether to write code or do research", "use proactively when the description says so", "MUST send a single message for parallel", and the worktree auto-cleanup behavior detail. The three redundant "Use Explore / Plan / general-purpose for …" shorthand bullets were dropped — the agent descriptions themselves now carry the canonical (and richer) selection guidance. Upstream's two<example>blocks at the end of "Writing the prompt" are also intentionally omitted: the per-orchestrator-turn token cost is recurring, the abstract guidance + the now-rich agent descriptions cover the same pedagogical ground, and the examples embed Anthropic-specific<thinking>framing that doesn't generalize across pi-ai's provider surface (OpenAI, Bedrock, Gemini, Mistral, …). All pi-specific bullets (resume,steer_subagent,model,thinking,inherit_context,isolation: "worktree",${scheduleGuideline}) preserved.- Default agent descriptions (
general-purpose,Explore,Plan) replaced with upstream Claude Code's verbatim wording. Previously one-line labels (e.g."Fast codebase exploration agent (read-only)"); now multi-sentence descriptions that include positive ("Use it to …") and negative ("Do NOT use it for …") guidance plus, for Explore, search-breadth hints ("quick"/"medium"/"very thorough"). The LLM-facing selection signal is now substantially stronger. /agents → Ejectnow emits YAML-safedescription:frontmatter. The new Explore description contains a:colon-space pattern (the search-breadth hint) and embedded quote characters — emitting it raw would have produced malformed frontmatter that theyamlparser would mis-parse.ejectAgentnow wraps the description withJSON.stringify(a valid YAML 1.2 double-quoted scalar), so any description string round-trips cleanly through eject → re-load. Latent bug: previously unreachable because old descriptions were YAML-plain-safe./agents → SettingsUI rewritten to inline-editableSettingsList. Replaces the previous modalctx.ui.selectchain. All settings visible at once;↑/↓to navigate,Spaceto cycle preset values on numerics (Max concurrency,Default max turns,Grace turns),Enterto type a custom value,Escto exit. Functionally equivalent — same fields, same valid ranges, same persistence behavior — but the interaction model is different. Users scripting against the old screen flow may notice..gitignoreadditions. Added.pi/subagents.json(project-local subagents settings — written by/agents → Settings, shouldn't be committed) plus pi-runtime working files (progress.md,AGENTS.md,CLAUDE.md). Migration: if you previously committed.pi/subagents.jsonto your repo, rungit rm --cached .pi/subagents.jsonto untrack — gitignore only blocks new additions.
[0.8.0] - 2026-05-26
⚠️ Breaking: peer dependencies moved from
@mariozechner/pi-*to@earendil-works/pi-*. The upstream Pi runtime relocated npm scopes on 2026-05-07; the@mariozechner/pi-*packages are deprecated. This release pins@earendil-works/pi-{ai,coding-agent,tui}at>=0.74.0. Hosts on the old scope must update their pi installation first (pi update --selfhandles the rename automatically) before installing this version.Note on Node: this release is tested against
@earendil-works/pi-coding-agent@latest(currently0.75.x), which requires Node>=22.19.0because its bundledundicicalls Node 22+ APIs. CI runs on Node 22. The peer range (>=0.74.0) technically also matches the upstreamlegacy-node20line (0.74.x, Node 20 compatible) and this extension contains no Node 22+ API calls of its own, but the legacy line is not exercised in CI — consumers pinning it do so at their own risk.
Changed
- Peer deps migrated from
@mariozechner/pi-*to@earendil-works/pi-*(#76 — thanks @SEHANTA for the report). On 2026-05-07 the upstream Pi runtime moved npm scopes —@mariozechner/pi-coding-agent@0.73.1was the final publish (now deprecated on npm), and@earendil-works/pi-coding-agent@0.74.0shipped 30 minutes later from the same monorepo (same author, same code).peerDependenciesnow target@earendil-works/pi-{ai,coding-agent,tui}at>=0.74.0, and allsrc/**andtest/**imports are renamed to the new scope — pure rename, no API changes. Consumers pinning the new scope no longer hit the peer-dep conflict warnings reported in #76. ThinkingLevelnow imported from@earendil-works/pi-aiinstead of…/pi-agent-core.src/types.tspreviously reached past the public API intopi-agent-core(an internal package), which only resolved because npm flat-hoisted it as a transitive ofpi-coding-agent— under pnpm or strict-resolver setups the import failed (TS2307: Cannot find module '@mariozechner/pi-agent-core').pi-aire-exportsThinkingLevelfrom its public surface (export * from "./types.ts"), so the import goes through the documented entry point and no extra peer dep is needed.
Fixed
.pi/subagent-schedules/is no longer created in every working directory.ScheduleStore's constructor previously ranmkdirSyncunconditionally, so any session with scheduling enabled left an empty.pi/subagent-schedules/dir behind even when nothing was ever scheduled. Directory creation is now lazy — deferred to a new privateensureDir()invoked at the top ofwithLock, so the dir (and its<sessionId>.json) appear only when a job is actually persisted. Additionally,update/removenow short-circuit on an unknown id (in-memoryjobs.has(id)check) before taking the lock, so no-op mutations never touch disk. Read-only use (list/get/hasName) and constructing the store never create the dir. Pre-existing leftover dirs are not cleaned up — remove them manually.
[0.7.3] - 2026-05-14
Added
<active_agent name="…"/>tag prepended to every child system prompt (#73 — thanks @chris-lasher).buildAgentPromptnow emits<active_agent name="${config.name}"/>as the first line of the assembled prompt in bothreplaceandappendmodes, before the env block. Downstream extensions (e.g. permission/policy systems) can parse it from inside the child session to resolve per-agent policy. The tag uses the agent'sconfig.nameverbatim — no escaping or normalization — and does not couple this extension to any specific downstream consumer; ignoring it is harmless.
Changed
- Subagent sessions now get a stable, type-derived name with an id suffix for parallel spawns (#51 — thanks @forcepushdev).
runAgentcallssession.setSessionName(agentConfig?.name ?? type), and when the manager assigns anagentId(always, in production), the name is suffixed with an 8-char slice — e.g.Explore#a1b2c3d4— so concurrent spawns of the same agent type are distinguishable in the overlay instead of all collapsing onto the same bare name. DirectrunAgentcallers without anagentId(e.g. tests) get the bare name.
Fixed
- Cross-extension spawn RPC now accepts a string
options.model(#59, fixes #60). Cross-extension callers (e.g.@tintinweb/pi-tasks@>=0.4.3'sTaskExecute) naturally forwardmodelas a serializable"provider/modelId"string. Previously the spawn handler passed strings straight through torunAgent(), which expects aModelobject — the spawned agent then crashed withNo API key found for undefined. The handler now resolves strings via the sameresolveModel(ctx.modelRegistry)path the scheduler uses;Modelobjects pass through unchanged. Unresolved strings surface the human-readableModel not found: "…"error instead of the auth-lookup crash. Thanks @any-victor.
[0.7.2] - 2026-05-12
Heads-up — behavior changes in skill preloading:
.txtand extensionless flat skill files are no longer loaded. Only<name>.mdflat files and<name>/SKILL.mddirectory skills resolve now. Rename any<name>.txtor extensionless skill files to<name>.md.
Added
- Pi-standard
<name>/SKILL.mddirectory layout is now discovered alongside flat<name>.mdfiles. Top-level and nested matches both resolve via BFS — for skillfoo, the loader checks<root>/foo/SKILL.md, then recursively descends looking for*/.../foo/SKILL.md. Recursion skips dotfile directories andnode_modules; a directory that itself containsSKILL.mdis treated as a single skill (Pi's "skills don't nest" rule). - Five discovery roots, checked in precedence order:
<cwd>/.pi/skills/(project, Pi)<cwd>/.agents/skills/(project, Agent Skills spec)$PI_CODING_AGENT_DIR/skills/— default~/.pi/agent/skills/(user, Pi)~/.agents/skills/(user, Agent Skills spec)~/.pi/skills/(legacy global, kept for backward compatibility)
- Symlink rejection broadened to the new layouts: symlinked skill roots, nested skill directories, and
SKILL.mdfiles inside otherwise-real directories are all rejected (intentional deviation from Pi, which follows symlinks). - Deterministic traversal order — entries are sorted byte-order so collisions resolve identically across filesystems. Pi's iteration order is
readdirSync-dependent. - Resolved spawn args are now shown in the dedicated conversation viewer (#62). Open
/subagent→ Running Agents → select an agent: a second header row displays the effective invocation — model override (when different from parent),thinking: <level>,isolated,worktree,inherit context,background, andmax turns: N. Tags appear when the resolved value is notable (e.g.isolated: true), not just when the caller explicitly set it;max turnsis the one exception and shows only when explicitly configured. Lets you verify the parent agent honored your spawn instructions without scrolling back through the chat. Snapshot stored on the newAgentRecord.invocationfield. The same tag set is also surfaced on theAgenttool-call result render (which previously showed a narrower subset). Shift+↑/Shift+↓scroll a full page in the conversation viewer — same behavior asPgUp/PgDn. Note: some terminal emulators intercept Shift+arrows for text selection or tab switching, in which casePgUp/PgDnremain available.
Changed
.txtand extensionless flat skill files are no longer loaded. Pi only supports.md; we now match. Migration: rename any<name>.txt/<name>skill files to<name>.md.- Conversation viewer no longer fills the full screen. The overlay is now capped at 70% of terminal height (90% width unchanged), and the viewer's internal viewport mirrors that cap so the footer/scroll indicator can't be clipped.
[0.7.1] - 2026-05-07
Heads-up — behavior change:
isolation: "worktree"now fails loud (returns an error) instead of silently falling back to the main tree. Affects users running pi in a non-git directory or a fresh repo with no commits.
Changed
isolation: "worktree"now fails loud instead of silently falling back. Previously whencreateWorktreereturned undefined (not a git repo, no commits yet, orgit worktree addfailed), the agent ran in the maincwdwith a[WARNING: ...]block prepended to its prompt — visible only to the LLM, never surfaced to the caller. Now the failure throws a structured error that propagates back to theAgenttool response; no agent record is created. Failed scheduled fires are recorded aslastStatus: "error"with the reason in thesubagents:schedulederror event. Queued background spawns whose worktree creation fails when they dequeue are marked terminal-error and don't block the rest of the queue.
Fixed
- Headless
pi --printruns no longer hang or crash after background subagents complete. Cleanup timers no longer keep the process alive, and stale completion notifications are treated as best-effort shutdown side effects.
[0.7.0] - 2026-05-04
Heads-up — behavior changes:
subagents:completed/failedeventtokens.totalnow excludescacheRead(previously double-counted across turns) — see Fixed [#38].- Cron
?is now a wildcard (same as*), not "current time value" — affects Quartz-style expressions only.
Changed
@mariozechner/pi-{ai,coding-agent,tui}moved topeerDependencies(>=0.70.5). Avoids duplicate framework instances when the host loads this extension.@sinclair/typeboxpinned fromlatestto^0.34.49so installs are reproducible.cronerbumped 8 → 10. Heads-up: in cron strings,?now means wildcard (same as*) instead of "current time value" — affects Quartz-style expressions only.
Added
- Master switch for scheduling — new
schedulingEnabledsetting (defaulttrue) under/agents → Settings → Scheduling. When set tofalse: thescheduleparameter and its guideline are stripped from theAgenttool spec at registration (zero LLM-context cost), the scheduler does not bind to the session, the/agents → Scheduled jobsmenu entry is hidden, and any in-flight scheduler is stopped immediately. The schema-level removal applies on next pi session; the runtime kill (menu, fire path) takes effect immediately. Persisted at<cwd>/.pi/subagents.json. - Schedule subagent spawns — the
Agenttool now accepts an optionalscheduleparameter. When set, the spawn registers a job that fires later instead of running immediately. Three formats: 6-field cron ("0 0 9 * * 1"— 9am every Monday), interval ("5m","1h"), or one-shot ("+10m"or ISO timestamp). Returns the job ID. Schedules are session-scoped — they reset on/new, restore on/resume(mirrors the persistence model of pi-chonky-tasks). Storage at<cwd>/.pi/subagent-schedules/<sessionId>.json, with PID-based file locking + atomic temp+rename for concurrent-instance safety. Result delivery is identical to today's background-spawn completions: when the scheduled agent finishes, the existingsubagent-notificationfollowUp path emits the result to the conversation — no new delivery code, no new message types. Concurrency: scheduled fires bypassmaxConcurrentso a 5-minute interval can't be deferred behind 4 long-running manual agents. Management:/agents→ "Scheduled jobs" lists active jobs and lets you cancel any one of them. Creation is via theAgenttool only — no parallel manual-create wizard in this iteration. Events:subagents:scheduled({ type: "added" | "removed" | "updated" | "fired" | "error", … }) andsubagents:scheduler_readyfor cross-extension consumers. Restrictions:scheduleis incompatible withinherit_context(no parent at fire time) andresume(schedules create fresh agents); forcesrun_in_background: true. Scheduler engine mirrorspi-cron-schedule(cronerfor cron,setInterval/setTimeoutfor interval/once); past one-shot timestamps and invalid cron expressions are caught at create time. - Context-window utilization indicator in the subagent overlay — token count is now followed by a colored
(NN%)showing how full the subagent's context is right now (estimateContextTokens(messages) / model.contextWindow * 100, sourced from upstreamcontextUsage.percent). Threshold colors: <70% dim, 70–85% warning, ≥85% error. Gracefully omitted when the model has nocontextWindowdeclared, or right after compaction before the next assistant turn (tokensisnullin that window). The same annotation slot also surfaces a compaction count↻Nwhen the agent has compacted at least once — e.g.12.3k token (84% · ↻3)(percent + compactions joined with·),12.3k token (↻1)(compactions only, immediately post-compaction while percent is still null). The compaction glyph stays dim regardless; the percent's threshold color carries the urgency signal. Two live overlays get the annotations (running stats line; inspect-overlay header); post-completion notifications and result/event payloads only get the count (the indicator is no longer actionable once the agent is done). - Token usage and context% exposed to the parent agent at every interaction surface —
get_subagent_resultaddsContext: NN%to its stats line;steer_subagentreturns aCurrent state: 12.3k token · 5 tool uses · context 72% fullline so the steering agent knows whether it has room before sending more context;task-notificationXML adds<context_percent>NN</context_percent>(omitted when null). All plain-text, no ANSI codes — designed for LLM consumption, not human display. - New
subagents:compactedlifecycle event fires when a subagent's session successfully compacts. Payload:{ id, type, description, reason: "manual" | "threshold" | "overflow", tokensBefore, compactionCount }—tokensBeforeis upstream's pre-compaction context size estimate;compactionCountis the running total for this agent (also persisted onAgentRecord.compactionCountand surfaced inget_subagent_result/steer_subagent/task-notificationwhen > 0). Aborted compactions don't fire. Routed through a new manager-levelonCompactconstructor callback, matching the existingonStart/onCompletepattern.
Fixed
- Subagent token count was inflated 5–15× and reset mid-run (#38). Two distinct bugs in the same field. (1) Upstream
getSessionStats().tokens.totalsums per-turncacheReadacross every assistant message — but each turn'scacheReadis the cumulative cached prefix re-read on that one API call, so summing N turns counts the prefix N times (quadratic inflation, very visible on long sessions). (2) Even with that fixed, anything derived fromsession.state.messagesresets at compaction because upstream replaces the array viathis.agent.state.messages = sessionContext.messages. Fix replaces all six display readers with a lifetime accumulator (AgentRecord.lifetimeUsageandAgentActivity.lifetimeUsage—{ input, output, cacheWrite }) fed by a newonAssistantUsagecallback dispatched frommessage_endevents in bothrunAgentandresumeAgent. The accumulator is independent ofstate.messagesmutation, so it survives compaction; total = input + output + cacheWrite by construction (cacheRead deliberately excluded — same prefix-double-counting reason). Thesubagents:completed/failedevent payload'stokensfield is now also lifetime-accumulated forinput,output, andtotaltogether (was:totallifetime,input/outputsession-derived → inconsistent after compaction). - ESC during a foreground
Agentcall now actually stops the subagent (#44 — thanks @Zeng-Zer). Pi's interrupt path isesc → agent.abort()on the parent →AbortSignaldelivered to every tool'sexecute(toolCallId, params, signal, …), but theAgenttool dropped that signal on the floor: subagents ran on their own independentAbortControllerinsideAgentManager, so the parent abort was invisible and the subagent kept running until natural completion ormax_turns. Fix threadssignalthroughAgent.execute→manager.spawnAndWait()→SpawnOptions.signal, andAgentManager.startAgent()now attaches an{ once: true }"abort"listener that callsthis.abort(id)(which setsstatus: "stopped"and aborts the child controller). The listener is detached in both.thenand.catchto avoid leaking on natural settle. Scope: foreground only — background agents intentionally outlive the parent tool call, so their spawn deliberately does not forwardsignal. Resume path (AgentManager.resume()) has the same blind spot and is tracked as a follow-up.
0.6.3 - 2026-04-28
Fixed
run_in_background: true(andinherit_context,isolated) silently ignored on default agents (#37 — thanks @kylesnowschwartz for the diagnosis). The three built-in defaults (general-purpose,Explore,Plan) bakedrunInBackground: false,inheritContext: false, andisolated: falseinto their configs.resolveAgentInvocationConfigusesagentConfig?.field ?? params.field ?? false, and??only falls through onnull/undefined— so an explicitfalsefrom the agent config silently won over the caller'strue. CallingAgent({ subagent_type: "general-purpose", run_in_background: true })returned the result inline instead of backgrounding, blocking the parent UI for the agent's full runtime. Fix drops the three lines from each default (and from the unreachable defensive fallback inagent-runner.ts) — the type already declared each asfield?: booleanwith JSDoc "undefined = caller decides", so the runtime now matches the documented contract. Behavior: custom agents that explicitly set these fields in frontmatter still lock as before (the v0.5.1 "frontmatter is authoritative" guarantee is preserved); the fix only stops defaults from spuriously claiming an opinion on callsite-strategy fields they don't actually have. The unreachable fallback now spreadsDEFAULT_AGENTS.get("general-purpose")instead of duplicating the config inline, so future drift is impossible.
0.6.2 - 2026-04-28
Fixed
Agenttool fails on Windows withENOENTcreating output directory (#27 — thanks @sixnathan for the diagnosis). The cwd-encoding regex inoutput-file.tsonly handled POSIX/separators, so on Windowscwd = "C:\\Users\\foo\\project"survived unchanged andpath.join(tmpRoot, encoded, …)produced an invalid nested-absolute path. Now extracts a smallencodeCwd()helper that handles both/and\\separators, strips the Windows drive-letter prefix, and preserves UNC server/share segments. ThechmodSync(root, 0o700)call is also wrapped in a try/catch that swallows errors only on Windows (where chmod is a no-op and can throw on some filesystems); on Unix the error still propagates so umask-defeating0o700enforcement is preserved.
0.6.1 - 2026-04-25
Added
- Persistent
/agents→ Settings (#24) — the four runtime tuning values (maxConcurrent,defaultMaxTurns,graceTurns,defaultJoinMode) now survive pi restarts via a two-file dual-scope model mirroring pi's ownSettingsManager. Global~/.pi/agent/subagents.jsonprovides machine-wide defaults (edit by hand; the menu never writes here); project<cwd>/.pi/subagents.jsonholds per-project overrides (written by/agents→ Settings). Load merges both with project winning on conflicts. Invalid fields are silently dropped per field; malformed JSON emits a warning to stderr and falls back to defaults so startup always proceeds; write failures downgrade the settings toast to a warning with(session only; failed to persist)so changes aren't silently reverted on next restart. - New lifecycle events —
subagents:settings_loaded(emitted once at extension init with the merged settings) andsubagents:settings_changed(emitted on each/agents→ Settings mutation with the new snapshot and apersisted: booleanflag so listeners can react to write failures).
Fixed
AGENTS.md/CLAUDE.md/APPEND_SYSTEM.mdno longer leak into sub-agent prompts (#26 — thanks @mikeyobrien for the diagnosis). UpstreambuildSystemPrompt()re-appendscontextFilesandappendSystemPromptafter oursystemPromptOverrideruns, which silently defeatedprompt_mode: replaceandisolated: true— parent project context (e.g. autoresearch-mode blocks) was bleeding into freshExplore/ custom sub-agents regardless of frontmatter. Fix uses upstream'snoContextFiles: trueflag (skips the load entirely, introduced in pi 0.68) plusappendSystemPromptOverride: () => [](no flag equivalent for append sources). Behavior change: subagents no longer implicitly inherit parentAGENTS.md/CLAUDE.md/APPEND_SYSTEM.md. To get parent project context into a subagent, useprompt_mode: append(parent's already-built system prompt flows in viasystemPromptOverride), orinherit_context: true(parent conversation), or inline the content into the agent's own frontmatter.- Custom agent discovery respects
PI_CODING_AGENT_DIR(#35, closes #23 — thanks @Amolith for the diagnosis). Two remaining hardcoded~/.pi/agent/agents/paths incustom-agents.tsandindex.tsbypassed the env var, so users who relocated their agent directory (e.g. viaPI_CODING_AGENT_DIR) still had global agents loaded from the default location and help text referencing the wrong path. Both now use upstreamgetAgentDir(), consistent withagent-runner.tsandsettings.ts; tilde expansion is handled by upstream.
0.6.0 - 2026-04-24
⚠️ Breaking: drops support for
pi< 0.68. The upstreampi-coding-agentpackage shipped breaking API changes in v0.68 (and further ones in v0.70). This release migrates to^0.70.2and is not backward-compatible with hosts onpi0.62–0.67. Users on those versions must upgrade theirpiinstallation (npm install -g @mariozechner/pi-coding-agent@latest) before updating this extension.
Changed
- Bumped peer
@mariozechner/pi-coding-agentto^0.70.2(#28) — crosses the v0.68 breaking-change line upstream. Specifically: tools are now passed asstring[](wasTool[]);cwd/agentDirare mandatory onSettingsManager.create()andDefaultResourceLoader;session_switchevent renamed tosession_before_switch;ToolDefinition.paramswidens tounknownunder contextual typing, requiringdefineTool(...). - Tool registrations wrapped with
defineTool(...)— preservesTParamsinference soexecutehandlers get properly-typedparamsinstead ofunknown. Applies to theAgent,get_subagent_result, andsteer_subagenttools.
Removed
- Cwd-bound tool factory registry — the internal
TOOL_FACTORIESclosure table andcreate{Bash,Edit,Read,Write,Grep,Find,Ls}Toolimports are gone. Exported helpers renamed:getToolsForType(type, cwd)→getToolNamesForType(type),getMemoryTools(cwd, set)→getMemoryToolNames(set),getReadOnlyMemoryTools(cwd, set)→getReadOnlyMemoryToolNames(set)— all returningstring[]instead ofTool[]. The host binds cwd when resolving tool names, so the extension no longer instantiates tools directly.
Fixed
- Subagent
SettingsManagerread wrong project settings in worktree mode (#30) —SettingsManager.create()was called without arguments, defaultingcwdtoprocess.cwd(). When the subagent's effective cwd differed (worktree isolation or explicitcwdoverride), its settings manager read.pi/settings.jsonfrom the parent's cwd rather than its own, diverging from the loader and session manager. Now passeseffectiveCwdandagentDirexplicitly, keeping all three managers consistent.
0.5.2 - 2026-03-26
Fixed
- Extension
session_starthandlers now fire in subagent sessions (#20) —bindExtensions()was never called on subagent sessions, so extensions that initialize state insession_start(e.g. loading credentials, setting up connections) silently failed at runtime. Tools appeared registered but were non-functional. Now callssession.bindExtensions()after tool filtering and before prompting, matching the lifecycle used by pi's interactive, print, and RPC modes. Also triggersextendResourcesFromExtensions("startup")so extension-provided skills and prompts are discovered.
0.5.1 - 2026-03-24
Changed
- Agent config is authoritative — frontmatter values for
model,thinking,max_turns,inherit_context,run_in_background,isolated, andisolationnow take precedence overAgenttool-call parameters. Tool-call params only fill fields the agent config leaves unspecified. join_modeis now a global setting only — removed the per-calljoin_modeparameter from theAgenttool. Join behavior is configured via/agents→ Settings → Join mode.max_turns: 0means unlimited — agent files can now explicitly setmax_turns: 0to lock unlimited turns. Previously0was silently clamped to1.
Fixed
- Final subagent text preserved from non-streaming providers — agents using providers that return the final message without streaming
text_deltaevents no longer return empty results. Falls back to extracting text from the completed session history. effectiveMaxTurnspassed to spawn calls — previouslyparams.max_turnswas passed raw to both foreground and background spawn, bypassing the agent config entirely.
0.5.0 - 2026-03-22
Added
- RPC stop handler — new
subagents:rpc:stopevent bus RPC allows other extensions to stop running subagents by agent ID. Returns structured error ("Agent not found") on failure. abortinSpawnCapableinterface — cross-extension RPC consumers can now stop agents, not just spawn them.- Live turn counter — all agents now show a live turn count in the widget, inline result, and completion notification. With a turn limit:
⟳5≤30(5 of 30 turns). Without:⟳5. Updates in real time as turns progress viaonTurnEndcallback. - Biome linting — added Biome for correctness linting (unused imports, suspicious patterns). Style rules disabled. Run
npm run lintto check,npm run lint:fixto auto-fix. - CI workflow — GitHub Actions runs lint, typecheck, and tests on push to master and PRs.
- Auto-trigger parent turn on background completion — background agent completion notifications now use
triggerTurn: true, automatically prompting the parent agent to process results instead of waiting for user input.
Changed
- Standardized RPC envelope — cross-extension RPC handlers (
ping,spawn,stop) now use ahandleRpcwrapper that emits structured envelopes ({ success: true, data }/{ success: false, error }), matching pi-mono'sRpcResponseconvention. - Protocol versioning via ping — ping reply now includes
{ version: PROTOCOL_VERSION }(currently v2). Callers can detect version mismatches and warn users to update. - Default max turns is now unlimited — subagents no longer have a 50-turn default cap. The default is unlimited (no turn limit), matching Claude Code's main loop behavior. Users can still set explicit limits per-agent via
max_turnsfrontmatter or the Agent tool parameter, or globally via/agents→ Settings (0= unlimited). - Stale dist in published package — added
prepublishOnlyhook to build freshdist/on everynpm publish.
Fixed
- Tool name display —
getAgentConversationnow readsToolCall.name(the correct property) instead oftoolName, resolving[Tool: unknown]in conversation viewer and verbose output. - Env test CI failure —
detectEnvtest assumed a branch name exists, but CI checks out detached HEAD. Split into separate tests for repo detection and branch detection with a controlled temp repo.
0.4.9 - 2026-03-18
Fixed
- Conversation viewer crash in narrow terminals (#7) —
buildContentLines()in the live conversation viewer could return lines wider than the terminal whenwrapTextWithAnsi()misjudged visible width on ANSI-heavy input (e.g. tool output with embedded escape codes, long URLs, wide tables). All content lines are now clamped withtruncateToWidth()before returning. Same class of bug as the widget fix in v0.2.7, different component.
Added
- Conversation viewer width-safety tests — 17 tests covering
render()andbuildContentLines()across varied content (plain text, ANSI codes, unicode, tables, long URLs, narrow terminals). Includes mock-based regression tests that simulate upstreamwrapTextWithAnsireturning overwidth lines, ensuring the safety net catches them.
0.4.8 - 2026-03-18
Added
- Cross-extension RPC — other pi extensions can spawn subagents via
pi.eventsevent bus (subagents:rpc:ping,subagents:rpc:spawn). Emitssubagents:readyon load. - Session persistence for agent records — completed agent records are persisted via
pi.appendEntry("subagents:record", ...)for cross-extension history reconstruction.
Fixed
- Background agent notification race condition —
pi.sendMessage()is fire-and-forget, so completion notifications sent eagerly fromonCompletecould not be retracted whenget_subagent_resultwas called in the same turn. Notifications are now held behind a 200ms cancellable timer;get_subagent_resultcancels the pending timer before it fires, eliminating duplicate notifications. Group notifications also re-checkresultConsumedat send time so consumed agents are filtered out.
0.4.7 - 2026-03-17
Added
- Custom notification renderer — background agent completion notifications now render as styled, themed boxes instead of raw XML. Uses
pi.registerMessageRenderer()with the"subagent-notification"custom message type. The LLM continues to receive<task-notification>XML viacontent; only the user-facing display changes. - Group notification rendering — group completions render each agent as its own styled block (icon, description, stats, result preview) instead of showing only the first agent.
- Output file streaming for background agents — background agents now get the same output file transcript as foreground agents, with
onSessionCreatedwiring and proper cleanup on completion/error. NotificationDetailstype intypes.ts— structured details for the notification renderer, with optionalothersarray for group notifications.buildNotificationDetails()helper — extracts renderer-facing details from anAgentRecord.
Changed
- Notification delivery —
sendIndividualNudgeand group notification now usepi.sendMessage()(custom message) instead ofpi.sendUserMessage()(plain text), enabling renderer-controlled display. - Steered status rendering — steered agents show "completed (steered)" in the notification box instead of plain "completed".
Fixed
- Output file cleanup on completion —
agent-manager.tsnow callsrecord.outputCleanup()in both the success and error paths of agent completion, ensuring the streaming subscription is flushed and released.
0.4.6 - 2026-03-16
Fixed
- Graceful shutdown aborts agents instead of blocking —
session_shutdownnow callsabortAll()instead ofwaitForAll(), so the process exits immediately instead of hanging until all background agents complete. Agent results are undeliverable after shutdown anyway.
Added
abortAll()method onAgentManager— stops all queued and running agents at once, returning the count of affected agents.
0.4.5 - 2026-03-16
Changed
- Widget render-once pattern — the widget callback is now registered once via
setWidget()and subsequent updates userequestRender()instead of re-registering the entire widget on everyupdate()call. Eliminates layout thrashing from repeated widget teardown/setup cycles. - Status bar dedup —
setStatus()is now only called when the status text actually changes, avoiding redundant TUI updates. - UICtx change detection —
setUICtx()detects context changes and forces widget re-registration, correctly handling session switches.
Refactored
- Extracted
renderWidget()private method — moves all widget content rendering out of theupdate()closure into a standalone method that reads live state on each call. update()is now a lightweight coordinator: counts agents, manages registration lifecycle, and triggers re-renders.
0.4.4 - 2026-03-16
Fixed
- Race condition in
get_subagent_resultwithwait: true—resultConsumedis now set beforeawait record.promise, preventing a redundant follow-up notification. Previously theonCompletecallback (attached at spawn time via.then()) always fired before the await resumed, seeingresultConsumedas false. - Stale agent records across sessions — new
clearCompleted()method removes all completed/stopped/errored agent records onsession_startandsession_switchevents, so tasks from a prior session don't persist into a new one. steer_subagentrace on freshly launched agents — steering an agent before its session initialized silently dropped the message. Now steers are queued on the record and flushed onceonSessionCreatedfires.
Changed
- Extracted
removeRecord()private helper inAgentManager— deduplicates dispose+delete logic betweencleanup()andclearCompleted().
Added
- 8 new tests covering
resultConsumedrace condition andclearCompletedbehavior (185 total).
0.4.3 - 2026-03-13
Added
- Persistent agent memory — new
memoryfrontmatter field with three scopes:"user"(global~/.pi/),"project"(per-project.pi/),"local"(gitignored.pi/). Agents with write/edit tools get full read-write memory; read-only agents get a read-only fallback that injects existing MEMORY.md content without granting write access or creating directories. - Git worktree isolation — new
isolation: "worktree"frontmatter field and Agent tool parameter. Creates a temporarygit worktreeso agents work on an isolated copy of the repo. On completion, changes are auto-committed to api-agent-<id>branch; clean worktrees are removed. Includes crash recovery viapruneWorktrees(). - Skill preloading —
skillsfrontmatter now accepts a comma-separated list of skill names (e.g.skills: planning, review). Reads from.pi/skills/(project) then~/.pi/skills/(global), tries.md/.txt/bare extensions. Content injected into the system prompt as# Preloaded Skill: {name}. - Tool denylist — new
disallowed_toolsfrontmatter field (e.g.disallowed_tools: bash, write). Blocks specified tools even ifbuiltinToolNamesor extensions would provide them. Enforced for both extension-enabled and extension-disabled agents. - Prompt extras system — new
PromptExtrasinterface inprompts.ts;buildAgentPrompt()accepts optional memory and skill blocks appended in bothreplaceandappendmodes. getMemoryTools(),getReadOnlyMemoryTools()inagent-types.ts.buildMemoryBlock(),buildReadOnlyMemoryBlock(),isSymlink(),safeReadFile()inmemory.ts.preloadSkills()inskill-loader.ts.createWorktree(),cleanupWorktree(),pruneWorktrees()inworktree.ts.MemoryScope,IsolationModetypes;memory,isolation,disallowedToolsfields onAgentConfig;worktree,worktreeResultfields onAgentRecord.- 177 total tests across 8 test files (41 new tests).
Fixed
- Read-only agents no longer escalated to read-write — enabling
memoryon a read-only agent (e.g. Explore) previously auto-addedwrite/edittools. Now the runner detects write capability and branches: read-write agents get full memory tools, read-only agents get read-only memory prompt with only thereadtool added. - Denylist-aware memory detection — write capability check now accounts for
disallowedTools. An agent withtools: write+disallowed_tools: writecorrectly gets read-only memory instead of broken read-write instructions. - Worktree requires commits — repos with no commits (empty HEAD) are now rejected early with a warning instead of failing silently at
git worktree add. - Worktree failure warning — when worktree creation fails, a warning is prepended to the agent's prompt instead of silently falling through to the main cwd.
- No force-branch overwrite — worktree cleanup appends a timestamp suffix on branch name conflict instead of using
git branch -f.
Security
- Whitelist name validation — agent/skill names must match
^[a-zA-Z0-9][a-zA-Z0-9._-]*$, max 128 chars. Rejects path traversal, leading dots, spaces, and special characters. - Symlink protection —
safeReadFile()andisSymlink()reject symlinks in memory directories, MEMORY.md files, and skill files, preventing arbitrary file reads. - Symlink-safe directory creation —
ensureMemoryDir()throws on symlinked directories.
Changed
agent-runner.ts: tool/extension/skill resolution moved before memory detection;ctx.cwd→effectiveCwdthroughout.custom-agents.ts: extractedparseCsvField()helper; addedcsvListOptional()andparseMemory().skill-loader.ts: usessafeReadFile()frommemory.tsinstead of rawreadFileSync.- Agent tool schema updated with
isolationparameter and help text formemory,isolation,disallowed_tools, and skill list.
0.4.2 - 2026-03-12
Added
- Event bus — agent lifecycle events emitted via
pi.events.emit(), enabling other extensions to react to sub-agent activity:subagents:created— background agent registered (includesid,type,description,isBackground)subagents:started— agent transitions to running (includes queued→running)subagents:completed— agent finished successfully (includesdurationMs,tokens,toolUses,result)subagents:failed— agent errored, stopped, or aborted (same payload as completed)subagents:steered— steering message sent to a running agent
OnAgentStartcallback andonStartconstructor parameter onAgentManager.- Cross-package manager now also exposes
spawn()andgetRecord()via theSymbol.for("pi-subagents:manager")global.
0.4.1 - 2026-03-11
Fixed
- Graceful shutdown in headless mode — the CLI now waits for all running and queued background agents to complete before exiting (
waitForAllonsession_shutdown). Previously, background agents could be silently killed mid-execution when the session ended. Only affects headless/non-interactive mode; interactive sessions already kept the process alive.
Added
hasRunning()/waitForAll()methods onAgentManager.- Cross-package manager access — agent manager exposed via
Symbol.for("pi-subagents:manager")onglobalThisfor other extensions to check status or await completion.
0.4.0 - 2026-03-11
Added
- XML-delimited prompt sections — append-mode agents now wrap inherited content in
<inherited_system_prompt>,<sub_agent_context>, and<agent_instructions>XML tags, giving the model explicit structure to distinguish inherited rules from sub-agent-specific instructions. Replace mode is unchanged. - Token count in agent results — foreground agent results, background completion notifications, and
get_subagent_resultnow include the token count alongside tool uses and duration (e.g.Agent completed in 4.2s (12 tool uses, 33.8k token)). - Widget overflow cap — the running agents widget now caps at 12 lines. When exceeded, running agents are prioritized over finished ones and an overflow summary line shows hidden counts (e.g.
+3 more (1 running, 2 finished)).
Changed - changing behavior
- General-purpose agent inherits parent prompt — the default
general-purposeagent now usespromptMode: "append"with an empty system prompt, making it a "parent twin" that inherits the full parent system prompt (including CLAUDE.md rules, project conventions, and safety guardrails). Previously it used a standalone prompt that duplicated a subset of the parent's rules. Explore and Plan are unchanged (standalone prompts). To customize: eject via/agents→ selectgeneral-purpose→ Eject, then edit the resulting.mdfile. Setprompt_mode: replaceto go back to a standalone prompt, or keepprompt_mode: appendand add extra instructions in the body. - Append-mode agents receive parent system prompt —
buildAgentPromptnow accepts the parent's system prompt and threads it into append-mode agents (env header + parent prompt + sub-agent context bridge + optional custom instructions). Replace-mode agents are unchanged. - Prompt pipeline simplified — removed
systemPromptOverride/systemPromptAppendfromSpawnOptionsandRunOptions. These were a separate code path whereindex.tspre-resolved the prompt mode and passed raw strings into the runner, bypassingbuildAgentPrompt. Now all prompt assembly flows throughbuildAgentPromptusing the agent'spromptModeconfig — one code path, no special cases.
Removed
- Deprecated backwards-compat aliases:
registerCustomAgents,getCustomAgentConfig,getCustomAgentNames(useregisterAgents,getAgentConfig,getUserAgentNames). resolveCustomPrompt()helper in index.ts — no longer needed now that prompt routing is config-driven.
0.3.1 - 2026-03-09
Added
- Live conversation viewer — selecting a running (or completed) agent in
/agents→ "Running agents" now opens a scrollable overlay showing the agent's full conversation in real time. Auto-scrolls to follow new content; scroll up to pause, End to resume. Press Esc to close.
0.3.0 - 2026-03-08
Added
- Case-insensitive agent type lookup —
"explore","EXPLORE", and"Explore"all resolve to the same agent. LLMs frequently lowercase type names; this prevents validation failures. - Unknown type fallback — unrecognized agent types fall back to
general-purposewith a note, instead of hard-rejecting. Matches Claude Code behavior. - Dynamic tool list for general-purpose —
builtinToolNamesis now optional inAgentConfig. When omitted, the agent gets all tools fromTOOL_FACTORIESat lookup time, so new tools added upstream are automatically available. - Agent source indicators in
/agentsmenu —•(project),◦(global),✕(disabled) with legend. Defaults are unmarked. - Disabled agents visible in UI — disabled agents now show in the "Agent types" list (marked
✕) with an Enable action, instead of being invisible. - Enable action — re-enable a disabled agent from the
/agentsmenu. Stub files are auto-cleaned. - Disable action for all agent types — custom and ejected default agents can now be disabled from the UI, not just built-in defaults.
resolveType()export — case-insensitive type name resolution for external use.getAllTypes()export — returns all agent names including disabled (for UI listing).sourcefield onAgentConfig— tracks where an agent was loaded from ("default","project","global").
Fixed
- Model resolver checks auth for exact matches —
resolveModel("anthropic/claude-haiku-4-5-20251001")now fails gracefully when no Anthropic API key is configured, instead of returning a model that errors at the API call. Explore silently falls back to the parent model on non-Anthropic setups.
Changed
- Unified agent registry — built-in and custom agents now use the same
AgentConfigtype and a single registry. No more separate code paths for built-in vs custom agents. - Default agents are overridable — creating a
.mdfile with the same name as a default agent (e.g..pi/agents/Explore.md) overrides it. /agentsmenu — "Agent types" list shows defaults and custom agents together with source indicators. Default agents get Eject/Disable actions; overridden defaults get Reset to default.- Eject action — export a default agent's embedded config as a
.mdfile to project or personal location for customization. - Model labels — provider-agnostic: strips
provider/prefix and-YYYYMMDDdate suffix (e.g.anthropic/claude-haiku-4-5-20251001→claude-haiku-4-5). Works for any provider. - New frontmatter fields —
display_name(UI display name) andenabled(default: true; set to false to disable). - Menu navigation — Esc in agent detail returns to agent list (not main menu).
Removed
statusline-setupandclaude-code-guideagents — removed as built-in types (never spawned programmatically). Users can recreate them as custom agents if needed.BuiltinSubagentTypeunion type,SUBAGENT_TYPESarray,DISPLAY_NAMESmap,SubagentTypeConfiginterface — replaced by unifiedAgentConfig.buildSystemPrompt()switch statement — replaced by config-drivenbuildAgentPrompt().HAIKU_MODEL_IDSfallback array — Explore's haiku default is now just themodelfield in its config.BUILTIN_MODEL_LABELS— model labels now derived from config.ALL_TOOLShardcoded constant — general-purpose now derives tools dynamically.
Added
src/default-agents.ts— embedded default configs for general-purpose, Explore, and Plan.
0.2.7 - 2026-03-08
Fixed
- Widget crash in narrow terminals — agent widget lines were not truncated to terminal width, causing
doRenderto throw when the tmux pane was narrower than the rendered content. All widget lines are now truncated usingtruncateToWidth()with the actual terminal column count.
0.2.6 - 2026-03-07
Added
- Background task join strategies — smart grouping of background agent completion notifications
smart(default): 2+ background agents spawned in the same turn are auto-grouped into a single consolidated notification instead of individual nudgesasync: each agent notifies individually on completion (previous behavior)group: force grouping even for solo agents- 30s timeout after first completion delivers partial results; 15s straggler re-batch window for remaining agents
join_modeparameter on theAgenttool — override join strategy per agent ("async"or"group")- Join mode setting in
/agents→ Settings — configure the default join mode at runtime - New
src/group-join.ts—GroupJoinManagerclass for batched completion notifications
Changed
AgentRecordnow includes optionalgroupId,joinMode, andresultConsumedfields- Background agent completion routing refactored: individual nudge logic extracted to
sendIndividualNudge(), group delivery viaGroupJoinManager
Fixed
- Debounce window race — agents that complete during the 100ms batch debounce window are now deferred and retroactively fed into the group once it's registered, preventing split notifications (one individual + one partial group) and zombie groups
- Solo agent swallowed notification — if only one agent was spawned (no group formed) but it completed during the debounce window, its deferred notification is now sent when the batch finalizes
- Duplicate notifications after polling — calling
get_subagent_resulton a completed agent now marks its result as consumed, suppressing the subsequent completion notification (both individual and group)
0.2.5 - 2026-03-06
Added
- Interactive
/agentsmenu — single command replaces/agentand/agentswith a full management wizard- Browse and manage running agents
- Custom agents submenu — edit or delete existing agents
- Create new custom agents via manual wizard or AI-generated (with comprehensive frontmatter documentation for the generator)
- Settings: configure max concurrency, default max turns, and grace turns at runtime
- Built-in agent types shown with model info (e.g.
Explore · haiku) - Aligned formatting for agent lists
- Configurable turn limits —
defaultMaxTurnsandgraceTurnsare now runtime-adjustable via/agents→ Settings - Sub-menus return to main menu instead of exiting
Removed
/agent <type> <prompt>command (useAgenttool directly, or create custom agents via/agents)
0.2.4 - 2026-03-06
Added
- Global custom agents — agents in
~/.pi/agent/agents/*.mdare now discovered automatically and available across all projects - Two-tier discovery hierarchy: project-level (
.pi/agents/) overrides global (~/.pi/agent/agents/)
0.2.3 - 2026-03-05
Added
- Screenshot in README
0.2.2 - 2026-03-05
Changed
- Renamed package to
@tintinweb/pi-subagents - Fuzzy model resolver now only matches models with auth configured (prevents selecting unconfigured providers)
- Custom agents hot-reload on each
Agenttool call (no restart needed for new.pi/agents/*.mdfiles) - Updated pi dependencies to 0.56.1
Refactored
- Extracted
createActivityTracker()— eliminates duplicated tool activity wiring between foreground and background paths - Extracted
safeFormatTokens()— replaces 4 repeated try-catch blocks - Extracted
buildDetails()— consolidates AgentDetails construction - Extracted
getStatusLabel()/getStatusNote()— consolidates 3 duplicated status formatting chains - Shared
extractText()— consolidated duplicate from context.ts and agent-runner.ts - Added
ERROR_STATUSESconstant in widget for consistent status checks getDisplayName()now delegates togetConfig()instead of separate lookups- Removed unused
Tooltype export from agent-types
0.2.1 - 2026-03-05
Added
- Persistent above-editor widget — tree view of all running/queued/finished agents with animated spinners and live stats
- Concurrency queue — configurable max concurrent background agents (default: 4), auto-drain
- Queued agents collapsed to single summary line in widget
- Turn-based widget linger — completed agents clear after 1 turn, errors/aborted linger for 2 extra turns
- Colored status icons — themed rendering via
setWidgetcallback form (✓green,✓yellow,✗red,■dim) - Live response streaming —
onTextDeltashows truncated agent response text instead of static "thinking..."
Changed
- Tool names match Claude Code:
Agent,get_subagent_result,steer_subagent - Labels use "Agent" / "Agents" (not "Subagent")
- Widget heading:
●when active,○when only lingering finished agents - Extracted all UI code to
src/ui/agent-widget.ts
0.2.0 - 2026-03-05
Added
- Claude Code-style UI rendering —
renderCall/renderResult/onUpdatefor live streaming progress- Live activity descriptions: "searching, reading 3 files…"
- Token count display: "33.8k token"
- Per-agent tool use counter
- Expandable completed results (ctrl+o)
- Distinct states: running, background, completed, error, aborted
- Async environment detection — replaced
execSyncwithpi.exec()for non-blocking git/platform detection - Status bar integration — running background agent count shown in pi's status bar
- Fuzzy model selection —
"haiku","sonnet"resolve to best matching available model
Changed
- Tool label changed from "Spawn Agent" to "Agent" (matches Claude Code style)
onToolUsecallback replaced with richeronToolActivity(includes tool name + start/end)onSessionCreatedcallback for accessing session stats (token counts)env.tsnow requiresExtensionAPIparameter (asyncpi.exec()instead ofexecSync)
0.1.0 - 2026-03-05
Initial release.
Added
- Autonomous sub-agents — spawn specialized agents via tool call, each running in an isolated pi session
- Built-in agent types — general-purpose, Explore (defaults to haiku), Plan, statusline-setup, claude-code-guide
- Custom user-defined agents — define agents in
.pi/agents/<name>.mdwith YAML frontmatter + system prompt body - Frontmatter configuration — tools, extensions, skills, model, thinking, max_turns, prompt_mode, inherit_context, run_in_background, isolated
- Graceful max_turns — steer message at limit, 5 grace turns, then hard abort
- Background execution —
run_in_backgroundwith completion notifications get_subagent_resulttool — check status, wait for completion, verbose conversation outputsteer_subagenttool — inject steering messages into running agents mid-execution- Agent resume — continue a previous agent's session with a new prompt
- Context inheritance — fork the parent conversation into the sub-agent
- Model override — per-agent model selection
- Thinking level — per-agent extended thinking control
/agentand/agentscommands