chore: sync fork to upstream protoAgent v0.81.0 (80 commits)#3
Merged
Conversation
…R 0068 slice 1) (protoLabsAI#1533) * feat(flags): developer-flags backend foundation (protoLabsAI#1506, ADR 0068 slice 1) Slice 1 of the developer-flags system: the registry + resolution. - runtime/flags.py — a frozen `Flag` dataclass, the `FLAGS` registry (single source of truth, empty for now), and `flag_enabled(id)` / `resolved_flags()`. Tiers off<dev<beta<on resolve against a runtime `channel` (prod ⊂ beta ⊂ dev); precedence is PROTOAGENT_FLAG_<ID> env override > tier-vs-channel > off (fail-closed on an unregistered id). - channel derivation: explicit PROTOAGENT_CHANNEL > the dev sandbox instance (PROTOAGENT_INSTANCE=dev, ADR 0065) > the new `developer.channel` config field > prod. Read live (graph.sdk.config, imported lazily) so a Settings save applies without a restart, and degrades to env/default when there's no graph state (ACP/headless). - `developer.channel` added to LangGraphConfig + the settings schema (select prod/beta/dev, section "Developer" → Behavior domain), and the config-roundtrip goldens updated. 22 flag unit tests + the config-roundtrip/settings suites green; ruff clean; import contracts kept (runtime→graph.sdk/infra.paths is allowed). The /api/flags route (slice 2) and the Developer panel (slice 4) build on resolved_flags(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: add 'developer' to config_to_dict top-level section set (protoLabsAI#1506) test_config_io.py::test_config_to_dict_mirrors_yaml_shape hardcodes the full top-level key set config_to_dict emits; the new developer.channel field adds a 'developer' section. Verified against the full suite (2574 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te) (protoLabsAI#1538) Adding a LangGraphConfig field used to require updating FOUR parallel hand-maintained lists of the config surface, and they'd already drifted: - test_config_roundtrip.CONFIG_TO_DICT_GOLDEN (exhaustive nested dict) - test_config_roundtrip.EMITTED_ATTRS (round-trip attr set) - test_config_roundtrip.FROM_YAML_EXAMPLE_FIELDS (value map) - test_config_io section set (top-level key set) config_to_dict is FIELDS-driven, so the SHAPE ones are redundant with the schema. Derive them: - EMITTED_ATTRS = {f.attr for f in FIELDS} | <18 non-FIELDS §B legacy attrs>. This FIXES a latent gap: EMITTED_ATTRS was silently missing 7 fields (developer_channel, checkpoint_vacuum, commons_path, egress_allowed_hosts, identity_org, knowledge_scope, skills_scope) — their round-trip wasn't tested. - the test_config_io top-level section set = {FIELDS sections} | {subagents, plugins}. - drop the exhaustive CONFIG_TO_DICT_GOLDEN; replace its test with a focused shape + redaction check. Value coverage stays in FROM_YAML_EXAMPLE_FIELDS (the flat view of the same cfg) and test_round_trip_preserves_emitted_fields (field-agnostic drop/mis-parse guard). Net: adding a config field now touches ONE golden (FROM_YAML_EXAMPLE_FIELDS, the value gate), not four. -314/+63 lines. Full suite 2574 passed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…8 slice 2) (protoLabsAI#1539) Serves runtime.flags.resolved_flags() — the active channel + every registered developer flag with its resolved enabled state + source — so the console Developer panel (slice 4) can render and toggle them. Read-only, gated by the /api/* operator bearer like every operator route. operator_api/flags_routes.py + registered in server bootstrap next to the fleet/theme routes. 3 route tests; import contracts kept (operator_api -> runtime.flags is allowed). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…68 slices 3–4) (protoLabsAI#1540) The console half of developer flags, on top of the /api/flags route (protoLabsAI#1539): - flags/flags.ts — useFlag(id) gates console UI (fail-closed while loading); useFlags()/useDeveloperChannel() read /api/flags. Two frontend override layers on top of the server's channel resolution: a device-local panel toggle (createUISlice, persisted per-agent) and a shareable ?flag:<id>=on|off query param. Precedence: query-param > panel toggle > server channel state. - settings/DeveloperPanel.tsx — Settings ▸ Developer: each flag with tier badge + resolved state + a per-device Switch + Reset; "Reset all" clears overrides. - SettingsSurface wires it into "This console" ONLY off prod (developerPanelVisible: dev build / non-prod channel / ?dev|?flag: reveal), so production operators never see it. - lib/types + api.flags() + flagsQuery. Verified: tsc clean; vitest (override store) + full web unit suite 217; a new developer-flags.spec.ts e2e (panel lists flags, toggle persists an override, Reset clears) run against the built app; settings.spec section-list updated. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ADR 0068 slice 5) (protoLabsAI#1541) The cleanup contract (ADR 0068 D6): a developer flag with an ISO-date remove_by in the past is overdue debt — graduate it to `on` and delete the flag + old code path. test_no_flag_is_past_its_remove_by fails CI so a stale gate is visible instead of accreting. Non-date remove_by values (e.g. a version) are skipped (can't be auto-compared). No-op today (FLAGS is empty); a guard for as the registry grows. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gutter (protoLabsAI#1542) Code blocks in chat rendered wonky (a huge left gutter, code shoved sideways, not responsive). Root cause: streamdown 2.5.0 defaults `lineNumbers: true` and renders the line-number gutter PURELY via `before:` Tailwind utility classes with NO `data-streamdown` hook. The console styles streamdown's markdown via the DS `@protolabsai/ui/markdown` attribute-selector CSS and deliberately purges streamdown's inert Tailwind classes — but the line-number gutter has no attribute selector to hook, so it can be neither themed by the DS nor generated by Tailwind → an unstyled, half-broken gutter. Line numbers are unthemeable in this model, so turn them off (`lineNumbers={false}`). Every other part of the code block (border, header, body, copy button, shiki colors) is already themed by the DS CSS and renders clean. Follow-up: the DS `<Markdown>` should default this off since it can't theme the gutter (protoContent gap).
… slice 6a) (protoLabsAI#1543) The wrap-and-delete workflow end to end: define a flag in runtime/flags.py, gate a backend path (flag_enabled) and console UI (useFlag), flip it via env / ?flag: / the Developer panel, and graduate+delete under the remove_by cleanup contract. Explains tiers × channel and why a flag is neither a plugin nor a setting. Wired into the Console & UI guides (sidebar + index) and nav.json regenerated. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…I#1547) The macOS (10×) and Windows (2×) legs of desktop-build.yml were the repo's dominant CI cost — the full three-platform matrix fired on EVERY semver tag (~99 tags/month at current cadence), even though desktop drops don't need to track the tag firehose. Retire the `push: tags` trigger; desktop builds are now on-demand via workflow_dispatch. Two dispatch modes, keyed off the existing `tag` input: - WITH a tag (vX.Y.Z) → a real desktop RELEASE: binaries attach to that GitHub Release, the fan-in composes latest.json, and the release is promoted to 'Latest' (release.yml still creates releases --latest=false; promotion happens in the fan-in). This is the path that reaches users' in-app updater. - WITHOUT a tag → a test build: workflow artifacts only, no release/manifest. Tag pushes still ship the Docker image + a (non-Latest) GitHub Release via release.yml — only the desktop binaries wait for a dispatch. 'Latest' now tracks the last DESKTOP release (the one carrying latest.json), so the in-app updater never 404s on a manifest-less release. Also: the "unsigned release must fail" guard now keys on `inputs.tag` (a tagged publish still requires the full Apple secret set); fan-in checkout/TAG read from `inputs.tag` instead of github.ref_name. Docs updated (releasing.md § Desktop, apps/desktop/README.md, react-tauri-ui.md). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ystem text (protoLabsAI#1536) (protoLabsAI#1548) The wait tool returned raw "Yielding for 300s — turn ending now. You'll be re-invoked at <ISO>… to: <then>", which agents recited verbatim into chat as a raw system/engineering message. Return a concise, human-friendly summary instead — "Wait scheduled: 5 minutes. Will resume to: <then>." — via a new _humanize_duration helper (300s → "5 minutes"), and add docstring guidance telling the agent not to echo the return value verbatim but to paraphrase it conversationally. The yield/re-invoke scheduling mechanism is unchanged. Also refreshes the now-stale "Yielding…" references in server/chat.py's _last_tool_text docstring and the wait-yield test fixtures. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… just an instance id (protoLabsAI#1553) When an agent boots a local server for someone to test against while their real instance(s) are running, sharing only the box root (plain dev.sh) is data-safe but trips the desktop's co-residence warning (protoLabsAI#1552) and collides on box-level resources (mDNS, scheduler owner-lock). Document the fully-isolated boot (PROTOAGENT_BOX_ROOT + PROTOAGENT_INSTANCE + free port), its config-inheritance tradeoff, and the worktree dist-serving note (_bundle_root anchors to the loaded server package). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… of bricking the console (protoLabsAI#1554) Adding a fleet member that fails to start (or navigating to a stopped one) left the console stuck at the boot gate: the focused agent's slug routes EVERY API call through the hub proxy, which returns 409 "agent not running" for a down member, so the boot probe just retried ~30× into a generic "isn't responding" gate whose only escape ("Continue anyway") opened a fully 409-broken app. No indication it was the ONE agent, and no way back to a working console. Now: when a NON-host slug keeps 409'ing past a normal spawn window (≥6 retries ≈ ~6s), the gate switches to targeted recovery — "Agent '<slug>' isn't running" with **Return to host** (navigate to the host console, which always works) and **Try to start it** (re-activate + refetch). Uses TanStack Query v5 `failureReason`/`failureCount` so it appears DURING retries, not only after the probe gives up. host / 502 / cold-start paths are unchanged (502 stays a cold-start retry, not a down-agent — a booting proxy will come up). + `isAgentNotRunning(err)` helper (409-only, distinct from `isColdStart` which also rides 502/no-response) + unit tests. Build + 218 unit tests green. NOTE: builds + typechecks; the recovery UI itself wants a quick manual check — point a window at a stopped agent's slug (/app/agent/<slug>/) and confirm the card + both buttons. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, command bubbles (protoLabsAI#1530, protoLabsAI#1528, protoLabsAI#1529) (protoLabsAI#1549) protoLabsAI#1530: the slash popover now triggers MID-INPUT at any caret position (not only when "/" is char 0), via a caret-aware slashTokenAt parser + native caret listeners; completing inserts/removes only the token so surrounding text is preserved at start/middle/end. protoLabsAI#1528: the keyboard-focused item auto-scrolls into view (scrollIntoView block:nearest tied to the active index). protoLabsAI#1529: an issued (sent) slash command renders as a distinct user bubble (violet tint + monospace + "/" badge), detected via slashCommandName which ignores file paths. Review fix: slashTokenAt now returns the token's true `end` (scan to next whitespace), so completing with the caret in the MIDDLE of a token replaces the whole token instead of leaving a trailing suffix. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…I#1526) (protoLabsAI#1551) A user-configurable keybinding (default mod+o, scope "chat", allowInInput) that toggles the latest top-level tool-call block in the current agent output: first press expands the newest block; pressing again collapses it and walks the cursor upward (newest → oldest); idle targets the most recent block in the last message; no tool blocks → no-op. Registers through the ADR-0063 keybinding system so it appears in Settings ▸ Keyboard and is rebindable; the host preventDefaults the browser's native Cmd/Ctrl+O while focus is in chat. The DS ToolCard exposes only uncontrolled `defaultOpen` (no controlled open/onToggle — DS gap noted), so the action walks the rendered chat DOM and clicks the disclosure toggle, working uniformly across every render path with no changes to ToolCalls/WorkBlock/ChatSurface. Review fix: exclude reasoning cards (.reasoning-card, which render a DS ToolCard but aren't tool calls) so a reasoning-only turn is a correct no-op and the walk skips a leading reasoning card. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oLabsAI#1546) Bumps the DS from 0.52.0 → 0.53.0 and removes the console workarounds it now makes unnecessary: - Markdown line numbers: 0.52.1 (protoContent#376) made the DS `<Markdown>` default `lineNumbers` off + theme the gutter for Tailwind-purging consumers, so the console no longer forces `lineNumbers={false}` (drops the protoLabsAI#1542 workaround). - Rail-background context menu: 0.53.0 adds `onRailBackgroundContextMenu(side, e)` to AppShell — the "Hidden views" restore menu now uses it instead of catching the shell right-click and recovering the side via `closest(".pl-rail")` + `classList`. - Chat-tab context menu: 0.53.0 adds `onTabContextMenu(id, e)` to TabBar — the per-tab menu now gets the session id directly instead of mapping the clicked tab to a session by DOM sibling index. A minimal background-only wrapper remains for the empty-tab-bar "New chat" affordance (which the per-tab hook doesn't cover), bailing on tab hits. Lockfile bump is minimal (0.53.0 has identical deps to 0.52.0). `npm ci` installs 0.53.0 clean; `tsc` build green; 217 unit tests pass. NOTE: the context-menu wiring typechecks + builds but I could not click-test right-click menus — worth a quick smoke: right-click a rail icon, empty rail space, a chat tab, and empty tab-bar space; plus Rename/Close/New from the tab menu. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l container (protoLabsAI#1544, protoLabsAI#1545) (protoLabsAI#1550) * feat(console): header brand settings menu + unified settings sub-panel container (protoLabsAI#1544, protoLabsAI#1545) protoLabsAI#1544: the header brand mark is now a compact settings menu — click or right-click opens a DS Menu (Radix) with Agent settings / Fleet settings / Theme, each deep-linking the settings overlay via openGlobalSettings(section). Keyboard-accessible (Enter/Space/arrows/Esc) with a hover/focus chevron affordance. New BrandMenu component (the DS app-shell Header exposes no brand-slot onClick — contribute-back candidate noted). protoLabsAI#1545: the Keybinds and Delegate panels now render through a shared SettingsSubPanel container (StagePanel + DS PanelHeader + scrolling body), matching the sibling panels so the chrome can't drift again. Content unchanged. Review fix: removed now-dead .settings-group / .settings-group-title CSS (unreferenced after DelegatesSection moved to the shared container). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(e2e): fix delegates title locator after SettingsSubPanel rewrap (protoLabsAI#1545) The Delegates panel title now renders in the shared SettingsSubPanel header (a DS PanelHeader heading) outside .delegates-section, so the old `.delegates-section >> text=Delegates` assertion missed it. Assert the heading via getByRole (the pattern the fleet/knowledge/mcp specs already use); rows stay scoped to .delegates-section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ool work (protoLabsAI#1555) Adds [Unreleased] entries for work that merged without its own changelog note: slash-command UX (protoLabsAI#1530/protoLabsAI#1528/protoLabsAI#1529), header brand menu (protoLabsAI#1544), Cmd/Ctrl+O tool-block toggle (protoLabsAI#1526), the shared settings sub-panel container (protoLabsAI#1545), the conversational wait-tool output (protoLabsAI#1536), manual desktop builds (protoLabsAI#1547), and the isolated-dev-boot / manual-desktop docs (protoLabsAI#1553, protoLabsAI#1552). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… shows + gains Fleet settings (protoLabsAI#1544 follow-up) (protoLabsAI#1556) protoLabsAI#1544's brand-mark settings menu (Agent · Fleet · Theme deep-links on the header logo) wasn't the intended UX. Revert it: the header logo is a plain mark again (delete BrandMenu + its .brand-menu-* CSS, restore the plain ProtoLabsIcon logo). Instead, the existing FleetSwitcher dropdown (on the agent name) now ALWAYS shows — not only when >1 agent is in the fleet — so New agent + fleet nav are reachable even for a solo agent (only a hard fleet-API error falls back to the plain name). It gains a "Fleet settings" item that opens the fleet management dialog (openGlobalSettings("fleet")), alongside the existing agent list + New agent. protoLabsAI#1545 (shared settings sub-panel container) is unaffected. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kpoint (protoLabsAI#1527) (protoLabsAI#1558) * feat(chat): /compact — summarize + archive a thread, rewrite the checkpoint (protoLabsAI#1527) Server-side /compact: summarize the current chat thread, archive the FULL raw transcript to the searchable knowledge store (domain=conversation, namespace=chat-archive:<session_id>), then rewrite the live LangGraph checkpoint to [RemoveMessage(REMOVE_ALL_MESSAGES), summary, *recent_tail] so the agent keeps context at a fraction of the token cost. The manual analogue of the automatic SummarizationMiddleware. Two hard invariants: - Never-lossy: rewrite happens ONLY after the raw history is archived AND a non-empty summary exists. Refuses (no checkpoint change) on no_store / empty_archive / archive_error / no_summary / summary_error / no_checkpointer. - Message-boundary integrity: the retained tail never orphans a ToolMessage from its parent AIMessage(tool_calls) — reuses the middleware's safe-cut. graph/compaction_op.py (host-free) → server.chat.compact_session (under _thread_lock) → POST /api/chat/sessions/{id}/compact; /compact client command + api.compactChatSession. render_transcript gains max_chars=None for the uncapped archive. Tests: test_compaction_op.py (invariants incl. real-SQLite e2e + summarizer-exception refuse) + a route test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(e2e): add /compact to the slash-menu expected list (protoLabsAI#1527) /compact registers as a new client slash command, so commands.spec's exact CLIENT_SLASH list must include it (it sorts after /clear). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rotoLabsAI#1561) Adds [Unreleased] entries for /compact (protoLabsAI#1527, protoLabsAI#1558) and rewrites the reverted protoLabsAI#1544 brand-menu entry to its net shipped result — the always-available agent switcher + Fleet-settings shortcut (protoLabsAI#1544, protoLabsAI#1556). (The /close command was reverted, so it's intentionally not documented.) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l on bug form (protoLabsAI#1562) Removes the "Expected vs. actual" field from the bug report form and makes the "Acceptance" field optional. The bug form's required fields now map cleanly to the issue gate, which for `bug`-labeled issues only requires a Problem section plus Steps-to-reproduce/evidence — it never required Acceptance for bugs. `hasRepro` is still satisfied by "Steps to reproduce / evidence," so dropping expected-vs-actual won't trip the gate. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ce, not prod :7870 (protoLabsAI#1564) apps/web/vite.config.ts proxied `npm run dev`/`preview` backend calls (/api, /a2a, events, /agents, /plugins, /_ds) to http://127.0.0.1:7870 BY DEFAULT — the default/prod instance the desktop app runs on ~/.protoagent. So browser-based console dev silently read AND wrote the real desktop-app data (crossing dev↔prod streams). Yesterday's ADR-0065 instance isolation separated the backends but never covered this frontend-proxy layer. Default is now http://127.0.0.1:7871 (the isolated `scripts/dev.sh` instance) — fail-safe (a clean can't-connect if no dev backend is up, never a silent prod hit) — plus a loud red startup guard whenever PROTOAGENT_API_BASE points at :7870. The correct dev loop is `scripts/dev.sh` (backend :7871) + `npm run dev` (frontend), both isolated. Docs: PROTO.md § Run it + CHANGELOG. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o-end (protoLabsAI#1527) (protoLabsAI#1566) The shipped suite proved the checkpoint rewrite against a real SQLite checkpointer but faked the knowledge store — so "the full transcript is archived and searchable" was asserted only at the add_document call, not that it's retrievable. Add an integration test using a REAL KnowledgeStore (FTS5, no gateway): seed a distinctive token ("pumpernickel") in the head that compaction REMOVES, compact, then assert (1) it actually compacted (archived_chunks>=1, removed=4, kept=2, summary returned), (2) the live checkpoint collapsed to [summary, tail] with the head gone, and (3) store.search finds the removed head in the conversation domain — proving the raw history is preserved + searchable. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (ADR 0042) (protoLabsAI#1565) Archetypes were a hardcoded list in `_archetypes()`, and the built-in "Project Manager" pointed at `protoLabsAI/pm-stack` — a repo since split into product-stack / leadEngineer / portfolio-manager-stack, so the card no longer installed what it promised. - Move built-in archetypes to `config/archetype-catalog.json` (served by GET /api/archetypes), so they can be added/removed with no code change; live config dir overrides the bundled seed like plugin/mcp catalogs. A hardcoded Basic+Custom fallback keeps the picker working if it's missing. - Ship Basic + Custom; installed bundles that declare an `archetype:` block still self-register on top, now DEDUPED by id + normalized bundle URL so a card can't appear twice (fixes a latent duplicate React key). - Fleet "New agent" now applies the archetype's persona: POST /api/fleet gained `soul`, threaded to manager.create(), which writes it to the new workspace's config/SOUL.md — so a bundle agent arrives WITH its persona. - Refresh fleet guide + e2e fixture (dead pm-stack → product-stack) + tests (catalog fallback, dedupe, persona-write). CHANGELOG under [Unreleased]. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…box root (protoLabsAI#1552) (protoLabsAI#1567) The boot "another instance can clobber your chat/knowledge/stores" warning (infra/paths.colocation_warning) fired whenever ANY other protoAgent process was live on the machine — because heartbeats live at the box tier and the check keyed on the shared box_root. So a `dev` instance (~/.protoagent/dev, wholly separate data) tripped a data-loss warning against the desktop/default instance, and the suggested remedy ("give each its own PROTOAGENT_INSTANCE id") was a no-op since they already had distinct ids. Now each heartbeat records its instance_root, and colocation_warning warns only when another live process shares THIS instance's instance_root (a genuine clobber — e.g. the same instance run twice). Box-only co-residents with distinct instance ids are detected but not flagged. A co-resident whose heartbeat predates the field is treated as distinct (favours no false alarm). Tests: same-instance_root → warns; box-only (distinct instance_root) → silent. Closes protoLabsAI#1552. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k persona soul (ADR 0042) (protoLabsAI#1568) Two audit follow-ups on the archetype flow: - The wizard's post-finish bundle-install outcome ("tools ready" / "couldn't auto-enable — turn them on in Settings ▸ Plugins" / install failed) was set as an in-wizard Callout, but onFinished() flips setup_complete and unmounts the wizard immediately — so the message, and the actionable enable/failure guidance, was never actually read. Surface it as a toast, which lives in the top-right stack and survives the unmount. The in-progress "Setting up…" hint stays inline (the wizard is still open during the install await). - Picking a bundle archetype whose manifest declares no inline `soul:` blanked the persona editor (soul === ""). Fall back to the base SOUL (the "basic" archetype's persona) via a new pure `personaSoul()` helper, used by both pickArchetype and the initial seed. Unit-tested. Verified: web unit suite 245 passed (incl. 4 new persona tests); tsc clean for the touched files (the 2 pre-existing DS-skew errors in App/ChatSurface are unrelated and clear under CI's npm ci). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stall (protoLabsAI#1521, protoLabsAI#1522) (protoLabsAI#1569) The Plugins settings panel already had version/update/uninstall; this adds the RAIL context menu path and DRYs the shared logic. Right-click a plugin rail icon → a disabled "Version vX.Y.Z" header, an "Update available" action when the freshness poll reports it's behind, and a destructive "Uninstall…" (confirm: "Uninstall <name>? This cannot be undone."). Update + Uninstall are gated so built-in / bundled-in-tree plugins never offer them (server independently refuses too). Shared usePluginManage() hook (update+uninstall mutations, DS toast, query refresh) consumed by both the panel and the new PluginRailManage root host; rail actions route through ephemeral uiStore triggers so the context-menu registration stays a pure function (mirrors configurePlugin). Closes protoLabsAI#1521, closes protoLabsAI#1522. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…AI#1532) (protoLabsAI#1570) Mirrors the changelog pipeline exactly: ROADMAP.md (## Planned / In progress / Shipped, bulleted) → scripts/roadmap.py (build → sites/marketing/data/roadmap.json; check = drift guard) → sites/marketing/src/pages/roadmap.astro (same BaseLayout + DS tokens as changelog.astro, grouped by status, issue/release-tag ref chips) + a /roadmap nav+footer link before Changelog. Seeded with real open-issue items (protoLabsAI#1520/protoLabsAI#1515/protoLabsAI#1514/protoLabsAI#1504/protoLabsAI#1535/protoLabsAI#1522/protoLabsAI#1521/protoLabsAI#1537) + v0.78.0 shipped highlights. Closes protoLabsAI#1532. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e the checkpoint (protoLabsAI#1535) (protoLabsAI#1572) A destructive "Rewind to here" action on assistant messages: discards everything after the chosen message and, critically, rewrites the a2a:<session_id> LangGraph checkpoint (the agent's real context) so a client-only trim can't leave the agent still remembering discarded turns. Mirrors the /compact plumbing: graph/rewind_op.py (host-free) → server.chat.rewind_session (under _thread_lock) → POST /api/chat/sessions/{id}/rewind; confirm dialog + client mirror on found. Boundary integrity: _safe_cut_end never orphans a tool_call from its ToolMessage. Review fix (was: silent divergence on duplicate assistant content — the server resolved content to the LAST match while the client truncated at the clicked bubble). The client now sends WHICH occurrence of that text it clicked, and the server picks the same occurrence from the start (falls back to last-match only when unaligned). Tested. Closes protoLabsAI#1535. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ule changes (protoLabsAI#1537) (protoLabsAI#1571) The Overview roll-up read goals/tasks/schedule via useSuspenseQuery but never subscribed to the server→client event bus — so while the Overview tab is showing (the individual panels unmounted), nothing refreshed its counts. Subscribe in WorkOverview via the existing onServerEvent SSE client, invalidating the matching query keys on the ADR-0039 topics: goal.changed + goal.iteration, task.changed, scheduler.fired. Push-based, background invalidate (no re-suspend flicker). The scheduler bus has no push on agent schedule add/cancel (mutates the store directly), so the Overview's schedule query gets a gentle per-use refetchInterval (15s) — NOT on the shared factory, to avoid regressing SchedulePanel. Closes protoLabsAI#1537. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 0068) (protoLabsAI#1606) /compact (protoLabsAI#1558) is still pre-release, so it becomes the FIRST real entry in the runtime/flags.py registry: tier "dev", remove_by 2026-09-01 (the staleness guard now tracks it). Prod-channel users no longer see the command; the dev channel — or PROTOAGENT_FLAG_CHAT_COMPACT=1 / ?flag:chat.compact=on / the Settings > Developer toggle — keeps it on. Both ends gate: - Backend: POST /api/chat/sessions/{id}/compact 403s when the flag is off, before compact_session is reached — a disabled gate can't touch the checkpoint. - Console: the client slash-command seam (ADR 0061) gains an optional `flag:` tag; ChatSurface lists + dispatches a tagged command only while its flag resolves ON (new useFlagPredicate(), the list-friendly sibling of useFlag — same ?flag: query > device override > server-state precedence). A flag-off command behaves as if never registered, so forks can flag-gate their own commands through the same seam. Tests: route pass-through now forces the flag on via the env override + a new flag-off 403 test proving compact_session is never called; unit test pins the flag tag on /compact; e2e adds a ?flag:chat.compact=off run showing the command vanish from the slash menu (mock server serves the real flag enabled so the existing CLIENT_SLASH listing still covers it). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…pt with fade, Open CTA, by-id fetch (ADR 0070 D4) (protoLabsAI#1605) * feat(console): background report card — raised surface, clamped excerpt with fade, Open CTA, by-id fetch (ADR 0070 D4) The chat report card becomes a real card instead of the DS system-message pill (near-black bg-inset fill, 100px radius, ghost link): - Raised surface: --pl-color-bg-raised, 1px border, --pl-radius corners, left-aligned block, drop shadow var(--pl-shadow-popover) with the .pl-convo__jump literal fallback. Selectors use STACKED specificity (.pl-message--system.chat-report …, anchored under .chat-session-slot) so neither the DS default nor our own base system-message rule can win by stylesheet load order. - Header row: report title + "Background report" subtitle (FileText glyph). - Excerpt is a teaser: clamped to ~7 lines (max-height + overflow hidden) with a bottom fade-out (mask-image linear-gradient, -webkit- prefixed) so the preview visibly trails off. BackgroundWatch now injects just the result preview for finished jobs (the card header owns the lede; failures keep the explicit failed-lede). - Clear CTA: an outline DS Button "Open report" into the document viewer; the whole card is click-to-open (selection-guarded so copying excerpt text never opens the viewer). - By-id fetch: new api.backgroundJob(id) → GET /api/background/{id} (the ADR 0070 route carrying the FULL result). loadBackgroundReport keeps the legacy list-and-filter ONLY as a 404 fallback (pre-0070 servers / deleted rows) and propagates real errors to the viewer. Tests: unit (by-id fetch, 404-only fallback, placeholder, error propagation — 6 new cases) + e2e report-card.spec.ts (card renders clamped with the fade mask, CTA opens the docviewer with the full report, and the by-id route is the fetch path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(console): fade the report-card excerpt only when it actually clamps (ADR 0070 D4 review) The bottom fade-out mask on .chat-report-excerpt was unconditional, and a CSS mask scales to the element's own box — so a SHORT report that fits entirely had its last line (the conclusion) ghosted toward invisible and misread as truncation. Reproduced: a 4-paragraph result rendered its final line at ~30% opacity with nothing clamped. Gate the mask behind a measured .chat-report-excerpt--clamped modifier (scrollHeight > clientHeight, re-measured via ResizeObserver so rewrapping on width changes moves the overflow point correctly). The e2e now covers both directions: the long report clamps + fades, a short report renders its final line unmasked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…otoLabsAI#1607) Both are only reachable on a non-loopback (tailnet/LAN) hub; a loopback desktop hub was never exposed. 1. WS proxy bypassed hub auth. The default-deny gate is a Starlette BaseHTTPMiddleware, which skips non-HTTP scopes, so /agents/<slug>/* WebSocket ran unauthenticated. For a REMOTE member the hub would attach that member's stored bearer and proxy any caller into its authed sockets (e.g. a terminal plugin PTY). forward_ws now refuses WS to a remote member (close 1008); host/local-peer sockets (no hub-stored credential) are unchanged. Remote live views should use delegate_to / a direct connection. 2. SSE cross-key break. The /api/sse-token was fetched slug-routed (member-signed), but the proxied /api/events is validated at the HUB's middleware first, so on a bearer-gated hub live events 401'd for every non-host member. The console now fetches a hub-signed token; the hub validates it and forwards with the member's own credential attached. Tests: WS proxy refuses a remote (1008) before resolving a target, still serves a live local peer; apiUrl keeps the SSE token on the hub in a member window. Full pytest 2764 passed; console unit 30 passed; tsc clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…tus (protoLabsAI#1608) Phase 2 of the remote-fleet audit (console operability), stacked on the Phase 1 security fix. - Add a remote by URL: the ONLY way to register a token-gated remote from the console (discovery carries no credential) or one on a subnet the scan can't reach. Name + URL + optional token (SecretInput), gated by canAddRemote(). - Reachability feedback: both the manual add and the discovery add now read the server's register-time `reachable` and toast "added, not reachable yet" vs a success toast, instead of silently dropping a dead row in. - Honest remote status: an offline remote reads "unreachable" with a warning dot (its `running` IS its reachability probe) instead of a neutral "stopped" — it isn't stopped, and can't be started. Deferred to a follow-up (needs a backend endpoint + an AuthGate refactor): edit a remote's token/URL in place, and route a proxied-remote 401 to a per-remote "auth failed — update token" state instead of the global hub AuthGate; plus the dead-remote 502 return-to-host boot recovery in App.tsx. Console: tsc clean, 262 unit tests pass, production build clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ened ones (protoLabsAI#1609) * feat(fleet): edit remote members in place + real recovery for down/mis-tokened ones Remote-fleet audit follow-up (round 2), stacked on protoLabsAI#1608. Backend: - supervisor.update_remote(ident, name?/url?/token?) edits a registered remote in place under the remotes lock — omitted fields kept, token="" clears — re-running the same reserved-name / SSRF-egress / collision checks as add (factored into a shared _normalize_remote_url) and invalidating the probe cache. New PATCH /api/fleet/remotes/{ident} re-probes and returns fresh {reachable,version}. Console: - The add-a-remote form doubles as an EDIT form (Pencil on remote rows), pre-filled; blank token = keep. This is how you fix a rotated/wrong token or changed URL without remove+re-add (which would mint a new id/slug and break open windows). - A proxied MEMBER's 401 no longer trips the hub's global AuthGate (which prompts for, and would overwrite, the HUB token). request()/requestForm()/the a2a stream now suppress it for slug-routed member requests (isMemberScoped). - Boot gate gains targeted recovery for the focused member: a 502 (unreachable remote — it never 409s) folds into agentDown with Return-to-host/Try-again; a 401 shows "can't authenticate — update its token in Settings ▸ Agents". Tests: update_remote unit + PATCH route + isAgentUnreachable predicate. Full pytest 2767 passed; console tsc clean, 269 unit tests, production build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(fleet): cover the boot-gate decision + member-401 suppression The high-blast-radius bits of the remote-recovery work were only covered at the predicate level. Add the wiring tests: - Extract bootGatePhase() (pure precedence: memberAuth > unreachable/notRunning > failed > stuck > loading) out of App.tsx's nested boot-gate ternaries and unit-test the full decision table. App now keys title/detail/action off the phase. - Behavioral test that a MEMBER-scoped 401 does NOT trip the global hub AuthGate (notifyAuthRequired stays quiet) while a HOST-scoped 401 does — the load-bearing api.ts change. Plus isMemberScoped() predicate coverage (exported for it). Console: tsc clean, 279 unit tests pass, build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…protoLabsAI#1607/protoLabsAI#1608/protoLabsAI#1609 (protoLabsAI#1611) The v0.35.0 WS amendment said the slug proxy forwards WebSocket upgrades for plugin views "carrying the bearer" — protoLabsAI#1607 deliberately narrowed that: WS proxying to a REMOTE member is now refused (the hub's HTTP-only default-deny auth would otherwise lend the member's stored token to an unauthenticated caller). Add the amendment, note the hub-signed SSE token fix, and document the console add-by-URL/edit-in-place management + the down/mis-tokened recovery UX. Docs-only; `npm run docs:build` clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…mmand settings (protoLabsAI#1612) Two halves of one operator ask — "completely disable run_command for a given agent": 1) The denylist actually works now. tools.disabled was applied only inside get_all_tools(), while the filesystem tools (incl. run_command), plugin/MCP extra_tools, delegation and late-seam tools were appended after it in create_agent_graph — so tools.disabled: [run_command] silently did nothing. New drop_disabled_tools() (tools/lg_tools.py) is the single filter, applied: - after the extra_tools merge (before the subagent tool_map snapshot, so a disabled tool can't ride into a subagent via an allowlist), - after the fs/late-tools appends, before the deferred search_tools build (a dropped name is never advertised), - in run_manual_subagent (the out-of-graph console fan-out path). create_agent_graph also syncs the denylist from ITS config, so eval sweeps / scripts that build graphs directly stop inheriting a stale process global. 2) The knobs are in Settings, per agent. New schema sections (Capabilities): Filesystem — filesystem.enabled / allow_run (the kill switch: the tool is never built when off) / run_requires_approval / bypass_allowed — and Tools — the tools.disabled list. Capabilities renders bespoke panels, so the fields surface as QuickSetting chips on the Tools panel (the ADR 0048 §2.2 pattern Skills/MCP already use). All hot-reload: a save rebuilds the graph. filesystem.projects now round-trips through config_to_dict §B like the other registries. Tests: test_tools_disabled.py pins each seam (fs / extra / delegation) + the config-driven sync; settings-schema test pins the new fields, their cascade of depends_on gates, and the Capabilities routing; roundtrip golden extends automatically (FIELDS-driven) + filesystem_projects joins the legacy emitted set; two e2e specs drive both chips end-to-end (mock fixture gains the fields). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rds + quick-add (no tabs) (protoLabsAI#1610) * feat(console): Work surface — card-first overview navigation, live cards + quick-add (no tabs) Kill the Work hub's tab strip: the landing is always the Overview, now four live cards (Goals · Watches — previously missing — · Tasks · Schedule) in a responsive auto-fill grid. The whole card is the navigation (role=button, Enter/Space, selection-guarded, raised-card hover per the chat report card); nested views render the unchanged panels under a slim "← Overview" back bar, with Escape backing out when no overlay is open. Card anatomy: icon + name + count Badge header; a muted one-line pulse ("2 driving · iteration 3/6" / "1 watching · 1 met today" / "0 ready · 1 in progress" / "next in 25m" — pure helpers in workOverview.ts, unit-tested); a StatusDot micro-list; and a corner "+" quick-add that stops propagation. Empty cards render the DS Empty with the quick-add as the CTA — except Watches, which has no quick-add at all (agent-created; the empty copy says so). Quick-add reuses the panels' creators, one form two hosts: the Goals inline <details> form is extracted into an exported GoalCreateDialog (DS Dialog) used by both the Goals panel's new "New goal" header action and the overview; TaskCreateDialog and ScheduleModal are exported and reused. Payloads and endpoints unchanged. Liveness: one surface-level SSE subscription set in WorkPanel (goal.changed/ goal.iteration, watch.changed/met/expired/stalled, task.changed, scheduler.fired) so every card stays fresh whichever view is mounted; the panels keep their own subscriptions (double-invalidate is a no-op). Schedule has no push for agent-side add/cancel, so the overview's read keeps a gentle per-use 60s refetchInterval (not on the shared schedulesQuery factory). e2e: tasks/schedule/navigation specs now navigate via overview cards (the .pl-tabs__select strip is gone); new work-overview.spec.ts covers counts + pulses, click-through + back + Escape, tasks quick-add updating the card via a stateful page.route, the Watches no-quick-add rule, and both empty-state paths; mock server gains a seeded GET /api/watches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): watches met-today keys off finished_at + Goals-panel host e2e Review fixes for the card-first Work overview: - watchesPulse counted "met today" from last_checked, but the watch controller's met path (_finish) sets finished_at and returns BEFORE the last_checked=now write — a watch met on its first check has no last_checked at all, and one met later carries the PREVIOUS check's time (possibly yesterday). Key off finished_at with a last_checked fallback for older payloads; add finished_at to the console WatchState (it's in the backend to_dict), mirror the real write order in the e2e WATCHES fixture, and pin the first-check-met / stale-pre-met-check / legacy-fallback cases in unit tests. - New e2e: the Goals PANEL's "New goal" header action (the other host of the lifted GoalCreateDialog) — Tasks/Schedule panel create flows had specs, the reworked Goals one didn't. Asserts the dialog gates on a condition, POSTs the same {session_id, condition, verifier} payload to /api/goals as the old inline form, closes + toasts on success, and never navigates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…Discord (protoLabsAI#1516) (protoLabsAI#1615) The in-app updater card and the Discord release embed diverged because they had two different writers: Discord = LLM-themed notes from the commit range (external release-tools action), in-app = the raw human-curated CHANGELOG.md section baked into latest.json. protoLabsAI#1516 wants one source, one voice. release-tools already writes its generated notes to a file (`out-file`) in the SAME run that posts the Discord embed — so this is pure wiring, no external change: - release.yml: the "Generate + post release notes" step now writes `release-notes.md` and uploads it as a release asset (guarded/best-effort — a fork with no gateway key writes no file). - desktop-build.yml: the latest.json fan-in prefers that `release-notes.md` asset for the updater notes, falling back to `changelog.py notes` (curated CHANGELOG), then the release body, then a placeholder. So the in-app "what's new" now renders the identical themed prose as Discord. Takes effect from the next release. Workflow-only; YAML validated. Closes protoLabsAI#1516 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e icons (protoLabsAI#1614) Two console polish items from local testing: - The Memory surface renders its DS Tabs as a direct child of the .stage-panel grid, so the strip landed in the default template's minmax(0, 1fr) row and stretched to fill the whole panel (tabs floating mid-panel, body squeezed to the bottom). Give the surface a .memory-panel row template — auto header, auto tab strip, 1fr scrolling body — the same shape .settings-panel uses for its sub-nav. - Work overview cards: drop the lucide icon next to each card title; the header is now just the name + count badge. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…oLabsAI#1616) The DS (@protolabsai/ui .pl-markdown) styles GFM tables with a fixed 0.85em / 5px·10px and horizontal-scroll-on-overflow — fine in the wide doc viewer, cramped in a narrow chat column where a multi-column table just scrolls. Make tables mobile-first and respond to their CONTAINER (the markdown column width) instead of the viewport, via a container query on the streamdown table-wrapper: compact type/padding at the narrow base (DS overflow-x scroll kept so nothing clips), roomier at 30rem/48rem container breakpoints. The same table is now compact in a narrow chat panel and comfortable in the doc viewer, automatically, wherever markdown renders. Console-side override (DS 0.52 owns the base rules; the `.markdown` alias rides the same element and theme.css loads after the DS styles, so it wins by cascade). The local DS repo is stale (0.47, no markdown.css yet), so this is the interim override — fold back into @protolabsai/ui markdown.css on the next DS sync. Console build clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-transport race (protoLabsAI#1600) (protoLabsAI#1619) test_install_deps_rejects_non_pep508_requires_pip[git+...] failed intermittently on CI runners in its SETUP clone of the local fixture repo: fatal: failed to copy file to '.../objects/pack/tmp_rev_XXXXXX': No such file or directory Root cause (reproduced locally): `git clone <local path>` uses git's "local transport" — a readdir+copy of the source's .git/objects (which also silently ignores --depth). `git commit` spawns a DETACHED `git maintenance run --auto --quiet --detach` in the just-created fixture repo; when that background maintenance renames a tmp_rev_* pack temp file between the clone's readdir and copy, the copy dies with ENOENT. Churning tmp_rev_* files in objects/pack while cloning reproduces the exact CI fatal 2/60 runs via a plain path and 0/60 via file://. Fix (no retries): - installer._clone(): rewrite a plain local path to a file:// URL, so git uses the regular pack transport (upload-pack streams a fresh pack; nothing enumerates the source's raw object files) — exactly what git's own warning recommends. Deterministic for production local-path installs too, and --depth 1 is now honored. - test fixtures: _git() passes -c maintenance.auto=false -c gc.auto=0 so fixture repos never spawn the detached background writer at all. Stability: affected param test 20/20 green, both installer test files 20/20 green, full suite 2795 passed. Closes protoLabsAI#1600 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… flex-col fallback) (protoLabsAI#1618) * fix(chat): code blocks stack vertically — language header on top, body scrolls streamdown lays the code block out as `flex flex-col` (language HEADER above the code body), but the console build only generates the `flex` Tailwind utility, not `flex-col`. So the block fell back to flex-ROW: the language label rendered as a cramped vertical column on the LEFT of the code, and the row grew past the message width (the overflow protoLabsAI#1593 tried to tame). Force the intended vertical stack on `[data-streamdown="code-block"]` (flex-direction: column) + `min-width:0; max-width:100%` on the block and body, so the language sits on TOP as a header and a long line scrolls INSIDE the block instead of stretching the panel. Applies wherever markdown renders (chat, reports, memory, doc viewer), not just .chat-session-slot. Verified headless (playwright against the preview build): code-block flex-direction flips row→column, language moves to a top header, no page-level horizontal overflow at a narrow width. DS-owned — the `[data-streamdown]` selectors should set this; folding back into @protolabsai/ui is queued on the DS side. Console build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): zero the min-width floor so code blocks let the panel go narrow The real bite behind "code blocks pin the panel to ~half width": a wide code line's min-content propagated up the message chain and floored the whole convo — so the resizable chat panel couldn't slide narrow, and past a point the block just overflowed. Walking the ancestor chain at a 480px width showed the code line (1633px) forcing every level from .pl-convo-wrap down to be 1633px wide, because .pl-convo-wrap / .pl-convo-scroll / .pl-message are flex/grid items defaulting to min-width:auto (min-content). The DS zeroes .pl-convo and .pl-message__body but misses those three, so the floor leaked through them. Zero their min-width so the constraint chain reaches .chat-session-slot (the grid correctly sized to the panel) and the overflow lands in the code body's overflow-x:auto. Verified headless: at a 480px window the code body is now 444px (was 1633) with content scrolling internally, and nothing exceeds the window — the panel slides to mobile width cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): markdown tables stack too (same streamdown flex-col fallback) The table-wrapper hits the identical bug as code blocks: streamdown emits it as `flex flex-col` (a copy-action row above the table), but the console build only generates `flex`, not `flex-col`. So it fell back to flex-ROW — the copy cluster sat BESIDE the table, and in a narrow panel the crushed table wrapped cell text one character per line ("M/a/r/k/d"). This also corrects the protoLabsAI#1616 container-query table change, which added the responsive padding but not the stack. Force flex-direction: column on [data-streamdown="table-wrapper"] (+ min-width:0 / max-width:100% on it and its children) so the copy cluster sits on top, the table is full-width, and a wide table scrolls inside its own overflow-x:auto. Verified headless: at a 460px panel the wrapper is flex-column, fits the panel, no page overflow, table renders as a normal 3-col grid instead of per-char wrapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): GFM task-list checkboxes + readable table copy-format menu Two more markdown rendering bugs found while testing: Task lists: the item showed a disc bullet AND the checkbox with the label on the next line. Root cause wasn't streamdown — it's the console's global `input,textarea,select { width: 100% }` blowing the markdown checkbox to the full row width, shoving the label down. Pin the task-list checkbox to width:auto and drop the disc marker; checkbox + label now sit inline. Scoped to .task-list-item so plain / ordered lists (which render fine) are untouched. Table copy menu: streamdown's "copy as" dropdown (Markdown/CSV/TSV) is styled with inert Tailwind (min-w-[120px], px-3, w-full), so the menu collapsed to ~22px and the labels wrapped one character per line (unreadable). Give it a readable surface + real menu items (min-width, padding, nowrap, hover). Verified headless: menu is now 148px, task-list checkbox back to intrinsic size with the label inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chat): anchor the table copy-format menu on-screen (keep all formats) Now that the menu is a readable 150px, streamdown's inline left offset (its right-0/top-full Tailwind is inert) shoved it off the wrapper's right edge and out of view, and the wrapper's overflow:hidden would clip it anyway. Anchor the menu explicitly (top:100%; right:0; left:auto !important — beats the inline style) and set the wrapper overflow:visible so the dropdown can escape (the inner table scroller keeps its own overflow-x:auto + radius, and the p-2 padding means the wrapper clip wasn't needed for corners). Menu now sits under the button, fully on-screen, with Markdown/CSV/TSV all visible. Verified headless at 760px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…+ settle bubbles (protoLabsAI#1617) (protoLabsAI#1620) The composer Stop only worked when the ChatSurface slot had started the turn itself: it cancelled `taskId` state (set by this instance's send loop) and aborted its own fetch controller. A slot rendering a turn it did not start — after a reload, a remount, a turn launched from the ⌘K palette / Work-surface quick-add, or on desktop where the Tauri relay pumps frames into the shared store and ignores the abort signal — had neither, so Stop was a silent no-op: no CancelTask on the wire, nothing aborted, tokens kept streaming. Server-side cancellation is healthy (verified live against an isolated instance: CancelTask lands in ~10ms mid-reasoning and the stream closes), so the fix is entirely client-side: - stop() aborts any locally-owned stream FIRST (the cancel RPC must never gate UI release), resolves the task to cancel from the slot's live taskId OR the streaming message's durable taskId (the same field the self-heal reconciler uses; every send path stamps it), settles every bubble left `streaming`, then sends CancelTask. - stopTurn.ts: resolveStopTarget + finalizeStoppedMessages extracted pure + unit-tested; chat-stop.spec.ts proves the full wire (Stop click → CancelTask RPC with the turn's task id → thread settled). Closes protoLabsAI#1617 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…bsAI#1622) While a request_user_input / ask_human / approval interrupt is pending, a fresh operator message used to invoke the parked LangGraph thread — which abandons the interrupt (dangling tool_call, form unresolvable) and lets the model see the message BEFORE the form answer (protoLabsAI#1560). The hold lives at turn entry (server/chat.py::_hold_if_hitl_pending, called inside the per-thread lock by both the streaming and non-streaming drivers), not at the SteeringMiddleware fold seam: a parked graph makes no model calls, so the fold seam never runs while a form is pending — the interleave happened at turn entry. Unmarked messages are parked in the existing steering queue and the caller re-parks on the same form payload; the operator's actual answer (new `hitl_resume` message metadata from the console, or an A2A message/send on the parked taskId) now resumes the interrupt properly via Command(resume=…). Held messages fold in at the first model call after the form resolves — immediately AFTER the form response, in arrival order. Console: sends while a form is open queue as steers (same pending-bubble + cancel affordances), turn-end reconcile keeps unconsumed steers queued while a form is pending instead of re-sending them as a fresh turn, and form submit/dismiss are marked hitl_resume (streaming metadata + /api/chat fallback body). Edge cases: dismissal is also a marked resume, so held messages release on cancel (no deadlock, nothing dropped); the pending-form state is re-read from the durable checkpoint every turn, so a restart can't strand the hold; with no pending interrupt every path is unchanged; autonomous turns stay exempt (they must never park). Closes protoLabsAI#1560 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The Docker hardening profile lived at the repo root but is only consumed host-side by docker-compose's security_opt — it belongs with the other deployment assets in deploy/. Updates the compose path, the .dockerignore exclusion, and the stale compose header comment (the Dockerfile never shipped the profile; Docker reads it from the host at compose time). Verified with `docker compose config` — resolves cleanly. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…kdown overrides (protoLabsAI#1626) 0.53.1 folded the markdown-chrome fixes into the DS (protoContent#382): the code-block + table vertical stack (flex-direction: column, owned in the DS now, not relying on the consumer generating `flex-col`) and the container-responsive tables. So the console overrides for those are gone (-87 lines in theme.css). What STAYS — three console-specific gaps the DS package can't see: - Task-list checkbox `width: auto` + `list-style: none` — the console's global `input,textarea,select { width: 100% }` blows the markdown checkbox to full width (the DS has no such rule), and the DS leaves the disc marker on. - Table copy-format dropdown (Markdown/CSV/TSV) — streamdown's inert Tailwind collapses it + an inline offset drifts it off-screen; needs a readable surface, explicit anchor, and wrapper `overflow: visible` to escape the DS's clip. - The `.pl-convo-wrap`/`.pl-convo-scroll`/`.pl-message` min-width:0 chain in chat.css — the DS ai.css still only zeroes `.pl-message__body`, so a wide code line would re-floor the panel. (chat.css unchanged.) Verified headless against DS 0.53.1: code blocks stack + scroll internally + panel narrows to mobile; tables render full-width + responsive + copy menu readable on-screen; task lists clean. tsc clean, 299 unit tests pass, build clean. DS bump via surgical package-lock edit + npm ci (npm install won't upgrade a resolved DS — see the recurring gotcha). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…xists there (protoLabsAI#1628) v0.81.0's release run warned "Unexpected input(s) 'out-file'": release.yml passes out-file (protoLabsAI#1615, the release-notes single-source) but the action pin was still v1.1.0, which predates that input — so the notes asset silently never materialized (the guarded upload step just skipped). v2.4.0's action accepts out-file; the underlying CLI was already fetched @latest, so the composite input schema was the only stale layer. v0.81.0's asset was backfilled by generating the notes locally with the same CLI. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e height (protoLabsAI#1629) Two issues in the contextual QuickSetting dialogs (e.g. Settings ▸ Tools ▸ "Shell & filesystem tools"): 1. Field descriptions crammed into a ~210px sliver — the .setting-row base is a grid reserving 220-320px for the control (right for a wide-page text input), which starves the meta column in the 460px dialog, so a long description wrapped one-word-per-line and towered. The dialog override MEANT to stack (per its comment) but `flex-direction: column` is inert on a grid. Make it a real flex column → label + FULL-WIDTH description, then the control. Descriptions go from ~15 lines to ~3. 2. The dialog stretched to nearly full viewport height (the DS `.pl-dialog` max-height is ~100vh), towering over the settings surface it popped from. Cap it at min(72vh, 620px); the DS body already scrolls (overflow-y:auto), so overflow scrolls inside the cap and the footer stays fixed. Two-class selector `.pl-dialog.quick-setting-dialog` to beat the DS rule regardless of stylesheet load order. Scoped to QuickSetting — the main settings overlay keeps its height. Verified headless (Settings ▸ Tools ▸ fs dialog): desc width 210→450px, dialog height 913→618px with the body scrolling + footer pinned. Console build clean. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…or chip removed) (protoLabsAI#1630) * feat(tools): per-row on/off toggles in the Tools panel — the denylist editor chip is gone Every row in Settings ▸ Capabilities ▸ Tools now carries a switch that edits the tools.disabled denylist (the same protoLabsAI#1612-hardened config the YAML route writes), so turning a tool off IS disabling it — the separate "Disabled tools" textarea chip was redundant and is removed. The Shell & filesystem chip stays: it owns the run_command execution policy (approval, /bypass) and the coarse kill switches, while per-tool wiring lives on the rows (run_command et al are ordinary Filesystem rows with switches). Backend: a toggled-off tool must stay visible or it could never be re-enabled — create_agent_graph now stamps graph.disabled_tools (everything the denylist dropped, collected via drop_disabled_tools' new collector param across every assembly seam) next to bound_tools, and /api/tools lists those rows with enabled:false plus the RAW config denylist (so a row toggle edits one name without clobbering stale entries, e.g. a disabled tool from an uninstalled plugin). count stays the WIRED count. Two real bugs surfaced and fixed along the way: - search_tools was appended AFTER the final denylist pass, so tools.disabled: [search_tools] silently re-bound it — now filtered too. - the hot-reload rebuild omitted tasks_store, so ANY settings save dropped task_create/task_list/task_update/task_close until restart (every row toggle would have made those 4 rows vanish). Now threaded like the checkpointer/background_mgr, with a kwargs-capture regression test. Also deflaked nesting.spec.ts: it expanded the task card mid-stream, and the live→history regroup on settle (protoLabsAI#1272-76) remounts the toolcard and drops the expansion — the spec now settles the turn first (was ~40% flaky under a parallel suite). Verified live on an isolated instance: toggle off → row stays (dimmed, "1 off" group badge, kicker "56 wired · 1 off"), rebuild keeps all 57 rows, toggle back on restores; e2e pins add-preserves-denylist + off-row-toggles-back-on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(tools): pin that denylisted plugin/MCP tools stay cataloged toggled-off The row toggles cover every seam — plugin/MCP tools enter as extra_tools, whose denylist drop feeds the same disabled-catalog collector. Pin the contract: dropped from bound_tools, listed enabled:false, still grouped under the owning plugin / MCP server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tools): roll back the optimistic toggle flip on save failure The onMutate snapshot was captured but never restored — the settled refetch usually recovers, but it can fail for the same reason the save did, stranding the never-persisted flip in the cache (and onToggle builds the next payload from that cache, so the stale state would compound). Both failure paths (transport error + ok:false) now restore ctx.prev before the invalidation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The v0.81.0 sync brings upstream's golden (identity_name=protoagent, plugins_enabled=[]); roxy's example config sets identity.name=roxy and plugins.enabled=[delegates, portfolio]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
👀 Quinn is reviewing — verdict (PASS / WARN / FAIL) + findings to follow. |
There was a problem hiding this comment.
QA Audit — PR #3 | chore: sync fork to upstream protoAgent v0.81.0 (80 commits)
VERDICT: WARN (non-blocking — CI still running)
CI Status
- build: 🔄 in_progress
- Python tests: 🔄 in_progress
- gitleaks (tree): 🔄 in_progress
- Verify workspace config: ⏳ queued
- A2A live smoke (lean tier): ⏳ queued
- Web E2E smoke: 🔄 in_progress
- Fleet integration (multi-instance): 🔄 in_progress
- Lint (ruff + import contracts): 🔄 in_progress
Diff Review
- 80-commit upstream sync (protoAgent v0.77.0 → v0.81.0); diff exceeds GitHub API limit (406, >20k lines). Cannot perform direct diff-based review.
- PR description is thorough: catalogs feature categories (work surface, fleet remotes, memory hardening, background results, developer flags), documents fork-delta handling, and reports pre-merge test results (26/26 config roundtrip, 66/66 representative tests, ruff clean).
- No unresolved CodeRabbit threads — review surface is clean.
Observations
- GAP: Diff not accessible — GitHub API 406 (exceeds 20k-line limit). Cannot verify individual file changes against PR claims. The 80-commit upstream sync is inherently large; this Gap is expected for a fork-sync PR.
- GAP: clawpatch structural review not available —
protoLabsAI/roxyis not in the project registry. Cross-file correctness, security, and contract integrity could not be structurally verified. - GAP: CI non-terminal — 8 checks still running. Formal PASS/FAIL deferred until checks settle. The merge-loop will re-dispatch on CI completion.
- LOW: Merge strategy note — PR requests merge-commit (not squash) to preserve upstream lineage. This is correct for a fork sync and aligns with best practice.
— Quinn, QA Engineer
|
Submitted COMMENT review on #3. |
There was a problem hiding this comment.
CI terminal-green, no blockers on prior review — auto-approving on green (protoLabsAI#748).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges upstream
protoAgent/main(v0.77.0 → v0.81.0, 80 commits) into roxy.What came in
Fork delta handling
identity_name: roxy,plugins_enabled: [delegates, portfolio]) — 26/26 pass.ruff check(CI pin 0.15.10) clean; representative tests (docs plugin, skill index, config roundtrip) 66/66 pass.🤖 Generated with Claude Code