v0.5.6 - 2026-10-09
Added
- Plugin concepts reach the engine. A plugin’s
[[concepts]]- validated against the engine’s own parser at install - were counted in the review and then dropped. They now travel the path profiles use: the store record carries the validated entries, the new staticGptyTerminal.installed_plugin_concepts()shapes the enabled records, andConceptManagermerges them between the shipped defaults and your own rules (the order builtins/plugin/user uses for profiles). A name a shipped default or an earlier plugin holds is not stolen: the plugin yields with a(n)suffix, so uninstall and re-install give the bare name back. A user toggle or edit overlays the plugin’s rule by name - which is what makes the Settings toggle, edit and delete work on installed content - and a deleted plugin rule is written as a disabled overlay rather than removed from a file the plugin owns. The Settings list shows where a rule came from, the visual canvas stays scoped to the rules it can own (defaults and yours), and uninstall/disable drops the rules without a restart through the existingpluginsChangedrefresh. The install review now names each rule and what it would do (capture ✓/notify-only, routing targets, ships-disabled) instead of printing a count.[[events]]and[[link_handlers]]remain declared and reviewed only - nothing consumes them yet, and both are execution paths whose delivery is its own trust decision (queued).
Changed
- The CLI starts a GUI only for commands a fresh one can satisfy.
new-paneandlayout loadstill auto-start the workspace (unless--no-daemon); every other command - queries, pane-targeted mutations,concept toggle, anddaemon stop/status- now fails withno gpty GUI is running (start one with \gpty daemon start`)and exit 1, instead of opening a window and then failing at the command's actual work (gpty list-panesused to launch the GUI it was trying to query).gpty daemon startis the explicit spawner,gpty daemon stopwith nothing running is a no-op, and the MCP server never auto-starts a GUI: itsdaemon-start` tool is the bootstrap, and other tool calls that find none say so. A GUI that cannot start - no display, missing libraries - now reports its own stderr immediately instead of waiting out the timeout in silence. - Declared-only manifest sections now say so in the install review. A plugin’s
[[events]],[[link_handlers]], andbuild/startupare declarations gPTY never dispatches or runs; the review printed “Listens for:” and “not run at install”, which implied delivery at some later point. The sections now read as the declarations they are - the plugin reads the event socket itself, the Markdown view opens onlyhttp(s)/mailtolinks and runs no handler, and neither build nor startup is executed - anddocs/content/plugins.mdstates the model (a plugin that wants events subscribes to the event socket, whose path a program launched in a pane already has). - The five first-party plugin repos validate with the shipped validator. Each repo’s
validate.ymlinstallsgptyfrom the v0.5.5 commit (cargo install --git … --rev 2f17459… --locked; the SHA is the pin, a tag is mutable) and runsgpty plugin validate .- the sameparse_manifestthe install path runs - replacing the provisional inlinetomllib+ id script. Only theid-equals-repo assertion stays inline, because that is the install gate’s contract and the validator cannot know which repo a manifest lives in.
Fixed
- A second gPTY window can no longer hijack the control socket.
IpcServer::serve()unlinked an existing socket after checking only that it was a socket owned by this UID, so a second instance took the path and orphaned the running one: both windows stayed up, every CLI command went to the newcomer, and the older workspace became unreachable with no message anywhere (measured: two processes, two listeners on/run/user/1000/gpty.sock, the older one’s inode unlinked). The bind now probes the path first and refuses when a server answers - replacing only a socket nothing is listening on, so a restart after a crash still works - and the same rule covers the event socket and, on Windows, the named pipe (which previously spun on a taken name instead of refusing). A checkout that cannot serve asks itself to quit through the existing shutdown flag and says why in a toast instead of vanishing: a window without a pane API is worse than no window. - An agent-state observer hears a terminal’s spawn state once. The terminal’s slow poll compared the engine’s tiered state against a shadow that started empty, so the first poll after the workspace’s entry delivery re-announced the spawn state as if it were a change - every follower’s hook (and the titlebar badge) was told
idletwice. The hook now records the delivery and the poll reports only deltas against it, which is the once-on-entry-plus-every-change contractPaneBody.on_agent_state_changedpromises. - A stale CLI on
PATHnow says so. The CLI and the GUI must come from the same release: an oldergptyshadowing the bundle’s CLI failed opaquely (error: unrecognized subcommand 'plugin'while the running GUI was 0.5.5, and its owngpty versionis local-only by design). Every command that talks to a GUI now compares what it reports - IPC protocol first, then version, direction-aware - and prints one stderr warning naming both sides and the remedy; the check rides on probes the CLI already makes, a command that has not probed does one best-effort check (500 ms, never able to fail or gate it), and the MCP server warns the same way at startup. The protocol version also has one definition now (gpty_ipc::protocol::PROTOCOL_VERSION), shared by the server’sversionresponse, the CLI, and the About tab, which had a hardcoded2.0. - An agent-state observer is told a value once, whichever path carried it. The entry scan broadcast the current state to every observer of a terminal, so attaching a second follower re-told the first a value it already held; and a follower that joined while a change was in flight heard the change before its entry, then the same value again from the scan. Each observer now carries
[source, last state]: the scan’s entry goes to the new pane alone, a change reaches only panes already settled for that source, and a repeat of the held value is dropped - the dispatcher-side mirror of the terminal’s_badge_staterule. Three contract tests pin it (a change dispatched before the entry, a second follower attaching, a repeated dispatch). gpty daemon status --jsonanswers JSON on every path. The running case printed the rawversionresponse (parseable), but the not-running, busy, refused and auth paths printed prose - a script that probed with--jsongot unparseable output exactly when the probe mattered. Those paths now answer the shaped vocabulary the MCPdaemon-statustool already used ({"status": "not running"},{"status": "busy"},{"status": "error", "error": …}), still with exit 1; the running shape is unchanged, because the live smoke parsesresult.versionout of it.