Skip to content
v0.5.6 - 2026-10-09

v0.5.6 - 2026-10-09

Release on GitHub

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 static GptyTerminal.installed_plugin_concepts() shapes the enabled records, and ConceptManager merges 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 existing pluginsChanged refresh. 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-pane and layout load still auto-start the workspace (unless --no-daemon); every other command - queries, pane-targeted mutations, concept toggle, and daemon stop/status - now fails with no 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]], and build/startup are 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 only http(s)/mailto links and runs no handler, and neither build nor startup is executed - and docs/content/plugins.md states 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.yml installs gpty from the v0.5.5 commit (cargo install --git … --rev 2f17459… --locked; the SHA is the pin, a tag is mutable) and runs gpty plugin validate . - the same parse_manifest the install path runs - replacing the provisional inline tomllib + id script. Only the id-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 idle twice. The hook now records the delivery and the poll reports only deltas against it, which is the once-on-entry-plus-every-change contract PaneBody.on_agent_state_changed promises.
  • A stale CLI on PATH now says so. The CLI and the GUI must come from the same release: an older gpty shadowing the bundle’s CLI failed opaquely (error: unrecognized subcommand 'plugin' while the running GUI was 0.5.5, and its own gpty version is 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’s version response, the CLI, and the About tab, which had a hardcoded 2.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_state rule. Three contract tests pin it (a change dispatched before the entry, a second follower attaching, a repeated dispatch).
  • gpty daemon status --json answers JSON on every path. The running case printed the raw version response (parseable), but the not-running, busy, refused and auth paths printed prose - a script that probed with --json got unparseable output exactly when the probe mattered. Those paths now answer the shaped vocabulary the MCP daemon-status tool already used ({"status": "not running"}, {"status": "busy"}, {"status": "error", "error": …}), still with exit 1; the running shape is unchanged, because the live smoke parses result.version out of it.