chore: sync fork to upstream protoAgent v0.77.0 (189 commits)#3
Conversation
…bsAI#1294) `_theme_path()` derived the theme file straight from `_live_config_dir()`, skipping the `_config_scope` that config YAML, secrets, and the setup marker all pass through. So co-located instances that differ only by `PROTOAGENT_INSTANCE` (the default instance + the `scripts/dev.sh` sandbox) shared a single `config/theme.json` and clobbered each other's theme — breaking the ADR 0004 isolation contract for this one store. Add `THEME_JSON_PATH = _config_scope(_LIVE_CONFIG_DIR / "theme.json")` next to the sibling path constants and route `_theme_path()` through it. `_config_scope` preserves the explicit-`PROTOAGENT_CONFIG_DIR` carve-out (desktop sidecar / fleet member: the dir is already the isolated leaf, so don't double-scope), so the only behavior change is that `PROTOAGENT_INSTANCE`-scoped instances now get `config/<instance>/theme.json`. Unscoped default is unchanged. Closes protoLabsAI#1293 Co-authored-by: Josh Mabry <artificialcitizens@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rotoLabsAI#1297) * Sync the tab favicon + theme-color with the active per-agent theme The per-agent theme (/api/theme, ADR 0042) repaints the whole console on an agent switch, but the browser tab didn't follow: the favicon SVG and `<meta name="theme-color">` were both frozen at the brand default (#9b87f2), so every agent looked identical in the tab strip and PWA/mobile chrome stayed lavender regardless of theme. Add `syncBrowserChrome()` in agentTheme.ts: it reads the resolved `--pl-color-accent` and rebuilds the favicon as a recolored data-URI SVG and updates the theme-color meta. Wire it into the existing `watchThemeChanges()` MutationObserver — the single broadcast point that already catches agent switches, saves/resets, and the ThemePanel's live picker edits — plus one initial sync. A static favicon SVG can't read page CSS vars (browsers render it in an isolated context where `currentColor` resolves to black), hence the runtime data-URI. Closes protoLabsAI#1295 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review: gate favicon sync to active themes + validate the accent Fixes the Web E2E failure and CodeRabbit's MAJOR finding on protoLabsAI#1297. - E2E (`e2e/assets.spec.ts` "favicon link resolves to the icon asset") fetched the `<link rel=icon>` href and broke on `Protocol "data:" not supported`: the unconditional initial sync swapped the static favicon to a data-URI even on a default, un-themed load. Now `syncBrowserChrome()` only recolors when a theme is actually active (`data-theme` / inline `--pl-*` overrides present), and restores the shipped static favicon when a theme is cleared. Un-themed loads keep index.html's real, fetchable asset — and the base-path regression guard that test exists for. 124/125 passed before; this was the one. - Harden the color before embedding it into SVG/markup (CodeRabbit): the accent comes from the opaque, agent-supplied theme blob, so `safeColor()` validates via `CSS.supports("color", …)` + a probe element and bails on anything that isn't a real color, instead of interpolating it raw into `stroke="…"`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Josh Mabry <artificialcitizens@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rd-stop on cancel (protoLabsAI#1298) delegate_to and the delegate health prober spawn CLI coding agents (codex-acp, claude-agent-acp, …) over ACP. Two lifecycle bugs leaked their subprocesses until hundreds of ppid-1 orphans piled up holding ~20 GB: 1. Teardown signalled only the DIRECT child. The backend each ACP adapter spawns reparented to init and survived — close() did proc.terminate() on the node adapter only, never its child tree. 2. dispatch() awaited a POOLED, long-lived client and never reaped it on cancel: stopping the turn only sent a soft session/cancel and re-raised, leaving the agent running ("I stopped the main thread and the delegate didn't stop"). 3. The prober's probe spawns + handshakes with a 45s wait_for; a timeout cancelled _start mid-initialize after the subprocess was already spawned, orphaning it. Fixes: - Spawn ACP agents with start_new_session=True (own process group), and TERM→KILL the whole group in close() via os.killpg. Grandchildren die with the agent. - Add AcpClient.kill_now() — a synchronous group SIGKILL safe to call from a CancelledError handler (no awaits). dispatch() now drops the pooled client and kill_now()s its tree on cancel before re-raising. - _start() self-reaps (close()) if the handshake raises or is cancelled after the subprocess exists. - Add coding_agent.close_all() + _drop_client(); the delegate-health surface's stop() drains the whole client pool on server shutdown. Regression tests (tests/test_acp_reaping.py) spawn a real grandchild and prove it dies with the group, that kill_now is synchronous, that a cancelled dispatch hard-reaps + drops, and that close_all drains the pool. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…und-trip logging (protoLabsAI#1301) Closes protoLabsAI#1300, protoLabsAI#1296. protoLabsAI#1300 — the delegate health prober ran a real ACP session (session/new or session/load) every 120s per acp delegate, despite the probe documenting itself as "no session, side-effect-free". Add AcpClient.handshake() that runs ONLY the ACP `initialize` round-trip (refactor _start(open_session=)), and point the probe at it. A status probe is now genuinely cheap + inert — it no longer spawns a full backend or touches persisted session state on a timer. protoLabsAI#1296 (mode 1, respawn loop) — the ACP launch env inherited CLAUDECODE / CLAUDE_CODE_* from a protoAgent server itself started inside a Claude Code session, so the spawned claude-agent-acp backend hit the "inside another Claude Code session" guard and respawned every ~2 min with no surfaced error. Strip the whole marker family in _launch_env() (an explicit delegate-env value still wins) — no more partial-strip footgun. protoLabsAI#1296 (mode 2, idle freeze) — add round-trip logging (initialize OK → session/prompt send → request_permission outcome) so a stalled prompt is diagnosable from the server log instead of a silent hang. Tests: handshake() opens no session (wire-level), launch env strips inherited markers but keeps explicit ones (wire-level); existing probe tests repointed _ensure_started → handshake. Docs: coding-agents + delegates guides, ADR 0024. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…egate env PATH (protoLabsAI#1302) Closes protoLabsAI#1299. Primary (Tauri): a macOS app launched from Finder/Dock/launchd inherits only launchd's minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin), not the user's login-shell PATH — so Homebrew (/opt/homebrew/bin), nvm, Volta, and asdf bin dirs (where npx, node, and ACP coding-agent adapters live) are invisible to the bundled server, and a `command: npx` delegate fails with "binary not on PATH". spawn_sidecar now augments the sidecar's PATH (login-shell PATH via `$SHELL -ilc`, plus Homebrew/local fallbacks and the already-inherited PATH) before .spawn(). macOS-only; web/terminal launches are unaffected. Secondary (probe consistency): AcpAdapter.probe resolved `shutil.which(d.command)` against os.environ only, while the real ACP spawn merges the delegate's env — so a delegate that supplies its own PATH spawned fine yet the Test button still red-X'd it. The probe now resolves against the merged PATH (`shutil.which(..., path=…)`), so probe and spawn agree. Tests: probe resolves the command against the delegate env PATH (captures the path= kwarg). Docs: coding-agents guide (macOS PATH warning + probe-vs-spawn note), react-tauri-ui guide (sidecar PATH augmentation). cargo check ✓. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sAI#1103) (protoLabsAI#1303) The mid-turn steering ✕-cancel was shipped in protoLabsAI#1104 (backend dequeue + DELETE /steer/{msgId}, api.cancelSteer, the wired onCancel with optimistic-drop + turn-end reconcile) and is covered by unit tests (tests/test_steering.py, tests/test_chat_routes.py). The one outstanding protoLabsAI#1103 acceptance item was an e2e that actually clicks the ✕ — this adds it. - mock-server: a "hold the turn open" prompt streams the opening frames then holds the SSE open (no terminal frame) so the surface stays "streaming" — the steering state — deterministically, instead of racing the ~40ms-gapped frames; plus a DELETE /steer/{msgId} handler returning {removed:true} (cancel-before-drain path). - spec: start a held turn → queue a steer (dimmed `.pl-message--queued` bubble) → click "Cancel queued message" → assert the DELETE fires and the bubble is gone. Verified locally against a fresh build: the new spec + the chat e2e batch + web unit tests all pass. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oLabsAI#1304) Several user-facing PRs merged since v0.66.0 never added a CHANGELOG bullet, so the Unreleased section was incomplete ahead of cutting a release. Backfill them so the rolled changelog + marketing /changelog are accurate: - Added: per-agent ACP launch overrides (protoLabsAI#1289) - Fixed: ACP probe initialize-only + nested-Claude env strip + logging (protoLabsAI#1301), macOS desktop sidecar PATH (protoLabsAI#1302), workspace _pick_port skips OS-occupied ports (protoLabsAI#1290), per-agent theme instance-scoped (protoLabsAI#1294), tab favicon/theme-color follow the active theme (protoLabsAI#1297) - Docs: codex needs the codex-acp adapter (protoLabsAI#1287) Test-only PRs (protoLabsAI#1291, protoLabsAI#1303) are intentionally omitted. 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>
…ixes protoLabsAI#1167) (protoLabsAI#1306) The Web E2E smoke job ran `playwright install --with-deps chromium` on every run with no cache, no timeout, and no retry — so a stalled CDN download for the ~100MB chromium binary hung the step ~10min until the job-level orphan-kill fired, a recurring false-red that cost a manual `gh run rerun --failed` each hit. - Cache `~/.cache/ms-playwright` keyed on the resolved Playwright version; on a hit skip the binary download and run only the fast apt system-deps (`install-deps`). - Hard-cap each attempt with `timeout 240` and retry up to 3× so a stalled download fails fast and self-heals instead of hanging to the orphan-kill; step capped at `timeout-minutes: 8`. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oLabsAI#874) (protoLabsAI#1309) Problem 1 — the Docker image never shipped the React console. apps/web/dist is .gitignored, the Dockerfile had no Node stage, and .dockerignore dropped the npm workspace manifests, so COPY shipped no built console and mount_react_app silently returned False — `-e PROTOAGENT_UI=console` 404'd at /app. Added a node:20-slim web-builder stage (npm ci + npm run build --workspace @protoagent/web) and COPY --from into the runtime image; unblocked the package manifests in .dockerignore (node_modules still excluded, so they never reach the final image); and made the server warn LOUDLY when the console tier is requested but the build is absent (was a single quiet log line). Problem 2 — requirements-core.txt (what the image installs) drifted from the pyproject [project.dependencies] source of truth: it had silently lost pypdf, youtube-transcript-api, and markdown-it-py, so the prod image lacked the ingestion deps while CI (which installs via pyproject) stayed green. Restored the three deps and added tests/test_requirements_core_sync.py, which fails CI if requirements-core.txt misses any core pyproject dependency. Verified with a real `docker build` (web-builder + full image) on Docker 28.5.1: the running container serves /app (200 + HTML), /app/assets/*.js (200), and /_ds/plugin-kit.css (200); the ingestion deps import; no node_modules leak into the final image. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…AI#875) (protoLabsAI#1308) * perf: off-loop console handlers + inbox/activity retention (protoLabsAI#875) Address the SAFE subset of the perf-audit tail (protoLabsAI#875): - Item 8: `_operator_runtime_status` now awaits `asyncio.to_thread` for the per-poll co-location (`ps` shell-out) and fleet version-skew probes, matching the startup-path co-location check. The `/api/runtime/status` route awaits a coroutine accessor and still accepts a sync dict (test doubles / forks). - Item 7: the inbox/activity console handlers (`_operator_activity_list`, `_operator_inbox_add`, `_operator_inbox_list`, `_operator_inbox_deliver`) offload their sync SQLite store calls via `asyncio.to_thread`, mirroring the scheduler/goals neighbors. Return shapes unchanged. Item 2 (inbox/activity retention) was already shipped on main (protoLabsAI#1059); items 5 and 9 are deferred; items 1/3/4 were already done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(changelog): trim protoLabsAI#875 bullet — retention pruning already shipped (v0.42.0/protoLabsAI#1059) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
protoLabsAI#1234) (protoLabsAI#1307) The utility-bar left/right panel-toggle buttons now disable (greyed out, non-interactive, with a "No panels in the <side> rail" title) when their rail holds no views — matching the bottom-dock toggle's existing gate, instead of appearing active but doing nothing when clicked. - App.tsx: gate the left/right toggles on `leftMembers.length === 0` / `rightMembers.length === 0` (the rail's resolved view set, already computed). - theme.css: add a `.util-btn:disabled` rule (opacity + default cursor) and guard `:hover` against disabled buttons, so a disabled toggle is visibly greyed and shows no hover feedback (also tidies the bottom toggle). - layout.spec.ts: assert the populated left/right toggles stay enabled and interactive (the gate doesn't over-disable). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…O(N²) rescan (protoLabsAI#1310) (protoLabsAI#1311) `_chat_langgraph_stream` recomputed the visible `<output>` and the live reasoning view by re-running regexes over the ENTIRE accumulated text on every token chunk, so a turn streaming N chars over ~N chunks cost O(N²) (`stream_visible_output` / `stream_visible_reasoning` over `accumulated_raw` each step). Add `StreamingOutputView` / `StreamingReasoningView` (graph/output_format.py): same "full visible-so-far" contract, but they scan only the newly-appended tail — - a cheap tail-only scan for the `<output>` / `<scratch_pad>` opener (the scratch_pad is never re-scanned), returning "" meanwhile; - a fast-append in the steady answer-body stream (open, unclosed, no `<` in the region); - a fall back to the authoritative pure function on any `<` (close / `<think>` / `<confidence>` / partial tag) or the closed state. The pure functions stay as the oracle: a parametrized equivalence test (curated cases × chunk sizes 1..50) plus a 300-doc random fuzz test assert the views return EXACTLY what the pure functions return at every growing prefix — so downstream streaming behavior is byte-identical, just ~O(N). Addresses the O(N²) item of protoLabsAI#1310; the goals/beads polling→event-bus item remains (it needs backend task-change events first). ruff + lint-imports clean; full suite (2503) + live smoke green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he 5s poll (closes protoLabsAI#1310) (protoLabsAI#1312) The Goals and Tasks right-sidebar panels each held a `refetchInterval: 5_000`, so the console polled goals + the tasks board every 5s per mounted panel forever. The inbox panel already does this the right way — invalidate off an `/api/events` push. Backend (the single store seam, so it fires for BOTH agent-tool and console-API writes): - graph/goals/store.py: `set()` / `clear()` publish `goal.changed` (covers create, each goal-loop iteration, finish, and clear — the controller routes them all through the store) via the best-effort `HOST.publish` seam. - tasks/store.py: `create` / `update` / `close` / `delete` publish `task.changed` (delete only when a row was actually removed). Both are no-ops when no publisher is wired (unit tests / standalone) and never break a write on a bus hiccup. Frontend: - GoalsPanel / TasksPanel subscribe to `goal.changed` / `task.changed` and invalidate their query (mirrors InboxPanel). - queries.ts: drop the 5s `refetchInterval` from goalsQuery + tasksQuery. Live updates are now immediate (agent files a task → it appears at once) and steady- state polling is gone. Tests: goal/task store publish on each mutation + no-op on a no-op clear/delete; the goal-hooks bus test now finds goal.achieved among the new goal.changed pushes rather than assuming it's first. ruff + lint-imports clean; full suite (2507) + live smoke + web vitest (115) + the goals/tasks e2e batch (20) all green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nstance (closes protoLabsAI#1159) (protoLabsAI#1313) A first-class factory-reset for the default ("prod") instance — wipe its data + local config back to a clean slate so the next boot runs the setup wizard, for testing the fresh-user flow. CLI-only by design; there is intentionally NO in-app self-wipe (a running server deleting its own WAL-locked data then relaunching is too risky — descoped per the issue discussion). SAFE on a multi-instance machine (validated against a real ~/.protoagent with 5 sibling instance roots + dozens of scoped team leaves): it preserves EVERY other instance — any `~/.protoagent/<name>` carrying an `.instance-uid`/checkpoints.db (the dev sandbox, fleet members, forks) is left untouched, and inside shared store dirs every `<store>/<instance>` leaf (a subdirectory) is preserved. Only prod's own unscoped top-level DBs + the direct files in those store dirs are removed. Tracked `config/` files are `git checkout`-restored to pristine; only gitignored local config (langgraph-config.yaml, secrets.yaml, .setup-complete, plugins/) is deleted. Flags: `--dry-run` (print the exact plan, delete nothing — ALWAYS run first), `--yes`, `--backup` (timestamped tar.gz), `--keep-secrets` (preserve secrets.yaml + langgraph-config.yaml so no gateway re-auth), `--include-dev` (also wipe the dev sandbox), `--force` (SIGTERM a server still bound to :7870 — a live process holds WAL handles). Refuses to wipe while a server is bound unless --force. Tests: tests/test_reset_script.py drives `--dry-run` against a synthetic tree and asserts the plan targets prod-only, preserves siblings + leaves, and deletes NOTHING (plus --keep-secrets / --include-dev plan variants). Verified the real deletion path end-to-end in an isolated tmp HOME. Docs: PROTO.md run-commands. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(issues): issue forms + silent validation gate
Add a GitHub issue standard and enforce it without blocking authors.
- .github/ISSUE_TEMPLATE/{bug,enhancement}.yml — issue forms whose required
fields render as the exact headings the gate checks; auto-label bug/enhancement.
- .github/ISSUE_TEMPLATE/config.yml — keep blank issues enabled (maintainer
escape) + contact links (CONTRIBUTING, security advisory).
- .github/workflows/issue-gate.yml — runs on issues opened/edited/reopened;
flags non-conforming issues with `needs-info` and NOTHING else (silent, never
comments, never closes). Removes the label on edit once the issue conforms.
Skips issues labeled `gate-exempt`. Ensures both labels exist (idempotent).
- CONTRIBUTING.md — the issue standard, surfaced by GitHub on the New-issue page
(the discoverable home for the silent gate's rules).
- PROTO.md — short "Filing issues" note pointing at the gate + CONTRIBUTING.
Gate contract verified against protoLabsAI#1159/protoLabsAI#1300 (pass) and a thin issue (flag);
form-filed issues always pass by construction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci(issue-gate): annotate runs-on for the workspace-config verifier
The Verify-workspace-config check requires every workflow's runs-on to carry the
'# workspace-config: allow-hosted-runner …' annotation; issue-gate.yml was missing
it (the 1 error blocking the PR). Mirrors the other workflows in checks.yml.
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#1315) * feat(chat): /issue — user-only GitHub issue-creation control command Add a server-handled `/issue` chat control command (like `/goal`): the user types it, the dispatcher short-circuits the turn, and an issue is filed via the `gh` CLI. It is deliberately NOT a LangChain tool — the read-only GitHub tools (plugins/github) stay agent-facing; creating an issue is a write kept in the user's hands, so the agent can't open issues autonomously. - tools/gh_issue.py — parse + scaffold + gate-conformance check + `gh issue create`. The body is validated against the SAME rules as the CI issue gate (>=80-char body + Problem section; bugs need repro/expected; features need proposed-direction/acceptance), so anything filed here clears the gate. Repo resolution: --repo > github.default_repo > GITHUB_DEFAULT_REPO/GH_REPO env > ask (no silent misrouting). --dry-run previews without calling gh. - server/chat.py — dispatch in both the streaming + non-streaming paths. - graph/slash_commands.py — reserve the `issue` token (can't be shadowed). - operator_api/console_handlers.py — surface /issue in the palette. - graph/config.py + settings_schema.py — github.default_repo (UI-editable, Settings > GitHub) + the three roundtrip goldens. - tests/test_gh_issue.py — 14 unit tests (parse, conformance, dry-run, create). - docs/guides/file-github-issues.md (+ sidebar) + README feature-map row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(console): New-issue form dialog for /issue + share file_issue The console UX for the user-only /issue command. Picking /issue from the chat composer's slash menu opens a form dialog (DS Dialog, like the add-task dialog); inline `/issue <title> …` still works. - tools/gh_issue.py — refactor: extract file_issue() (validate + gh create, returns a structured result) + labels_for()/resolve_repo() helpers, shared by the chat command and the new route so they can't diverge. - operator_api/github_routes.py (new) — GET /api/github/config (default repo + gh availability) and POST /api/github/issue (→ file_issue). Operator-surface only, not an agent tool. Wired in server/__init__.py. - apps/web NewIssueDialog.tsx + issueBody.ts — the form; buildBody() assembles the exact `##` headings the gate checks, so a dialog-filed issue always conforms. ChatSurface opens it from runClientSlash("issue"); on success a "✓ Filed: <url>" note is posted to the thread. - api.ts — githubConfig() + createIssue(). - tests: test_github_routes.py (6, HTTP contract + validation), issueBody.test.ts (6, body assembly + completeness). - plugins/docs/nav.json — regenerate for the file-github-issues guide added with the command (fixes the docs-nav-sync test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: fix dead link in file-github-issues guide (github plugin has no docs page) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
protoLabsAI#1316) Builds on the /issue command + dialog (protoLabsAI#1315): - Multi-repo picker. New `github.repos` config (list) drives a quick-toggle dropdown in the New-issue dialog; `github.default_repo` is the preselected one (and the command's default), falling back to the first repo when blank. A "Custom…" option swaps in a free-text field (inline × to return to the list) for one-off repos. Pairs with the portfolio manager's many-repo setup. `effective_default_repo()` keeps the command + dialog agreeing on the default. - Util-bar bug action. A 🐛 button next to Settings opens the same dialog, store-driven (`uiStore.newIssueOpen`) so the button and the chat `/issue` command share one mount; on success it notes "✓ Filed … <url>" in the chat. - Tooltip popovers. Settings, the new bug action, and the three panel-toggle buttons now use the DS Tooltip (like the Inbox/Activity widgets) instead of a native title. - Settings fix. The GitHub section was orphaned (unmapped sections default to the "Plugins" category, which only renders installed-plugin config). Route it to System so Settings ▸ System ▸ GitHub renders the repos list + default editor. Config roundtrip goldens + tests updated; docs (guide + README) refreshed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…protoLabsAI#1317) The last util button still on a native `title` — give it the same hover popover as the Inbox/Activity/Settings/issue actions, with a stateful label (N running / N finished / idle). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rotoLabsAI#1318) Adopts the DS fix (protoContent#290 → ui 0.46.0): Tooltip is now Radix-backed and collision-aware, so the utility-bar tooltips (Settings, /issue, background-agents, inbox, activity, panel toggles) auto-flip/shift to stay within the viewport instead of clipping at the bottom corners — no per-button `side` workaround needed. - apps/web/package.json: ^0.45.0 → ^0.46.0 (pulls @radix-ui/react-tooltip transitively). - e2e: the DS Tooltip is now portaled + shown on hover, so the plugin-view utility widget test hovers the pill and asserts the page-level `role=tooltip` (was a static child of `.pl-tip-wrap`). Verified: tsc clean · web unit 121 · web e2e 127 · build OK. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ls into a chip (protoLabsAI#1319) * fix(graph): failed/cancelled task delegations close as error cards A subagent crash returned an 'Error: …' string and a user-cancelled delegation returned a '[cancelled…]' string — both rode the green 'done' card because the tool-call frame's error flag is read from the ToolMessage status, which a plain string never sets. Now _run_subagent raises SubagentError on a hard failure and the task tool converts failure + cancellation into ToolMessage(status='error'), so the card shows the X (the lead still gets a readable 'continue without it' result). task_batch reports a failed sub-delegation inline and continues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): fold settled tool cards into a summary chip + label subagents Spotlight active work: running top-level tool cards render full; finished ones fold into one expandable 'N tools' chip (aggregate status → 'N failed' when any errored), so a fan-out turn no longer buries the answer under a wall of cards. Once the turn ends nothing is running, so the whole run collapses into the chip. A 'task' card now shows which subagent ran ('task → researcher', read from the call args) instead of a bare 'task'. ToolCardSummary is a thin local mirror of the proposed DS primitive (protoContent#292) — swaps to @protolabsai/ui on release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(graph): document subagent tool frames don't nest live (audit #4) A subagent's own tool calls DO propagate into the parent turn stream, but the task tool's on_tool_end is emitted before them (the delegation is detached via ensure_future for Tier-2 cancellation), so the console's 'last open task wins' nesting can't attach them — subagent tools render as top-level cards, not nested. Pins the ordering so a future parent-id linkage fix trips this test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): only fold at ≥2 settled tools; keep single tools inline E2E caught two things: (1) a lone finished tool folding into a '1 tool' chip hid a useful card and broke the single-tool specs — fold now engages only at ≥2 settled cards; (2) splitting running-vs-settled into separate lists remounted a card when it settled (losing its expanded state) — so the no-fan-out path now renders every card inline in emission order, identical to before. Adds a FANOUT mock scenario + e2e asserting the ≥2 fold collapses to a '2 tools' chip that expands to the cards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): summary chip shows a resting border + a running total of the block The fold chip now reads as a card at rest (solid border + raised fill, not just on hover), and its count is the running total of ALL tool calls in the block (running + settled) so the tally ticks up live as tools fire rather than only counting what's folded. DS chip border mirrored in protoContent#292. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): pinned-spotlight tool block — stable height, no bounce The tool block bounced as cards popped in/out mid-turn (each running tool mounted a row, then unmounted into the chip; the chip popped in at the 2nd tool) and the chat auto-scroll reflowed on every change. Now, while the turn streams, the active card(s) sit in a fixed-height '.tool-spotlight' slot that reserves one row and never collapses between tools, and everything finished folds into the running-total chip below — so the block holds a stable height for the whole turn and a finishing tool just crossfades from the slot into the chip (reduced-motion respected). Settled state is unchanged: a lone tool inline, a fan-out folded. Threads message streaming-status into ToolCalls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…itive (protoLabsAI#1320) @protolabsai/ui 0.46.0 → 0.47.1 (now ships ToolCardSummary). ToolCalls imports it from @protolabsai/ui/tool-card; the local mirror (ToolCardSummary.tsx) and its duplicate .pl-toolcard-summary* CSS are deleted — the DS styles bundle supplies the chip frame (incl. the resting border). Host-only .tool-spotlight/.tool-subagent stay. Completes the contribute-back loop; rendering is unchanged (e2e green). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rotoLabsAI#1321) The console nested a subagent's own tool cards under the `task` card via a timing heuristic ('last open task wins'), which breaks when the delegation's on_tool_end races ahead of the subagent's child frames (the delegation runs detached via ensure_future) and mis-attributes concurrent task_batch delegations. Now the subagent's run is tagged with the delegating tool-call id (`_run_subagent` sets it as run metadata → propagates to every child event); server/chat.py emits it as `parentId`, the A2A executor rides it on the tool-call DataPart as `parentToolCallId`, and the console nests by that id (heuristic kept as a fallback for older servers). task_batch gains an InjectedToolCallId so its sub- delegations nest too. Tests: nesting integration test now asserts the wire linkage; new e2e proves the child nests even when its frames arrive after the task closes; task_batch tests invoke via the tool-call envelope. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…absAI#1323) (protoLabsAI#1324) Comments out the show_component tool (inline component rendering, ADR 0051) and removes it from get_all_tools(), so the agent can't call it and it's gone from the console Tools tab. The component-v1 pipeline (graph/components.py codec, the server/chat.py sentinel extraction, the console component registry) is left intact — re-enabling is just uncommenting the tool. Tracked by protoLabsAI#1323. TestShowComponentTool skipped (codec tests stay). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce) (protoLabsAI#1322) * fix(web): collapse a delegation's nested tools (stable height, no bounce) The subagent-nesting fix put the subagent's tools in the DS always-on `nested` rail, so the task card GREW as each child streamed in and then COLLAPSED when it folded into the chip — the up/down bounce the user reported. Now the nested tools live in the card's COLLAPSIBLE body (revealed on expand) and the header shows a running count ('task → researcher · 3 tools'), so the card holds a stable one-row height while the subagent works. Nesting is unchanged (still by explicit parent id); only its default visibility moved from always-on to on-expand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): keep the most-recent tool in the slot until replaced (no blank gap) The spotlight reserved an empty row whenever no tool was running — a blank gap between tools and during the answer-streaming tail. Now the slot holds the LAST tool that ran (running or just-finished) until a newer one replaces it, so it's never empty; the previous tool drops into the running-total chip as the new one crossfades in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
protoLabsAI#1325) 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>
…protoLabsAI#1328) * feat: native reasoning — drop the scratch_pad/output protocol Stream the gateway's native reasoning instead of a prompted <scratch_pad>: - _ReasoningChatOpenAI (graph/llm.py) lifts the gateway's reasoning_content delta into the message's additional_kwargs (langchain-openai drops it by design — 'use a provider-specific subclass'). The model reasons natively; nothing is forced. - server/chat.py streams that reasoning_content on the reasoning channel and the model's content directly as the answer — the <scratch_pad>/<output> parsing + StreamingViews are gone. extract_output stays as a terminal fallback (still strips any stray tag). - graph/prompts.py drops the protocol from the lead + subagent prompts. The model now behaves natively (reasons, answers, calls tools) with no text protocol. extract_output's pass-through fallback keeps the non-streaming path backward-compatible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): render reasoning as a collapsed tool-style card The native reasoning blocks rendered as big always-expanded boxes (the DS Reasoning component auto-opens and locks open while streaming) — cramped and distracting between the tool cards. ReasoningCard now renders each reasoning run as a DS ToolCard: same chrome, COLLAPSED by default, a spinner while the model is still thinking (done-glyph hidden — reasoning isn't pass/fail), stacking evenly with the tool cards. Expand to read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): fold an agentic turn's reason→tool timeline into one WorkBlock With native reasoning interleaved between every tool step, a turn exploded into a forever-stack (reasoning → tool → reasoning → tool …) burying the answer, with bogus per-fragment 'N tools' chips. WorkBlock now folds the whole intermediate timeline behind ONE collapsed disclosure ('Working… / Worked · N tools') and renders the trailing answer prominently below it — expand to replay the timeline (collapsed reasoning + flat tool cards). Only engages when a turn interleaves reasoning WITH tools; tool-only / reasoning-only / plain turns keep their inline rendering (so the e2e mocks are unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): keep the latest tool exposed below the WorkBlock while streaming The WorkBlock folded the WHOLE timeline even mid-turn, so you couldn't see what the agent was doing. Now while streaming it spotlights the most-recent tool below the 'Working… · N tools' summary (kept until a newer tool replaces it) — the block isn't fully collapsed while it works. On completion it all folds into the one block, answer below. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): render chat markdown with streamdown (streaming-hardened) Swap the assistant-message markdown renderer from react-markdown + remark-gfm + rehype-highlight to **streamdown** — a renderer built for streaming AI output. It HARDENS incomplete markdown (a half-written code block / table / link no longer flashes broken mid-stream) and memoizes blocks instead of re-parsing the whole answer on every streamed token. XSS-safe (rehype-sanitize/harden); Shiki highlighting + a code-block copy button; mermaid/katex support. - Markdown.tsx: render via <Streamdown>; LazyMarkdown still code-splits it out of the initial chunk (streamdown pulls Shiki/mermaid/katex — heavy). - tailwind.config.cjs: scan node_modules/streamdown/dist so streamdown's utility classes (code-block chrome, emphasis) generate; the existing shadcn→token bridge themes them on-brand (ADR 0037 already maps --muted/--border/--primary/etc. to --pl-* tokens). - main.tsx: import streamdown/styles.css (its stream-in animations) before the app theme. - theme.css: drop the dead rehype-highlight `.hljs-*` token rules — Shiki emits inline colors. The `.markdown` typography stays, so common text is visually unchanged. - chat.spec.ts: streamdown renders inline emphasis as `[data-streamdown="strong"]` (styled span) rather than <strong>; align the selector. Block tags (h2/li/pre/code) stay semantic and assert unchanged. DEPENDENCY RESOLUTION (.npmrc + root @types/react pin): A transitive dep (@docsearch/react, VitePress docs site) pins react@18 while the console runs react@19. npm's strict peer resolver ERESOLVE-rejects that valid nested split when re-resolving the tree to add streamdown, so add `.npmrc legacy-peer-deps=true` (so `npm install` and CI's `npm ci` agree). Legacy mode would otherwise drop the root @types/react@18 the console's tsc resolves for the global `JSX` namespace, so pin it explicitly in the root package.json devDependencies. streamdown deduped onto react@19; react/react-dom/vite/ui versions unchanged (lockfile churn is legacy-mode reserialization). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): stop the WorkBlock spotlight strobing on rapid tool fan-outs The spotlight slot rendered its current tool via `group(call)`, which keys by tool id — so every new tool (e.g. each of task_batch's 100+ children) REMOUNTED the card, replaying its mount animation and flashing the previous tool's output for a frame before the next took over. On a big fan-out this strobed. Add a `spotlight` mode that renders only the most-recent tool under a STABLE key (`__spotlight__`), so React updates one card in place — name/status/output swap smoothly, the entrance animation fires once. WorkBlock's spotlight uses it; the inline streaming spotlight is switched to the same stable key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(tools): offload blocking web_search/fetch_url work off the event loop A fan-out research turn (task_batch → N parallel researchers) pegged the server at ~99% CPU and made it UNRESPONSIVE — even the cancel/tasks API couldn't answer. Root cause: two tools did synchronous work directly on the asyncio event loop, so under concurrency they serialised on the loop and starved everything else: - web_search: `ddgs.text()` is a blocking network call — wrap it in `asyncio.to_thread`. - fetch_url: `_extract_text_from_html` (BeautifulSoup over up to 2MB) is CPU-heavy and synchronous — offload it too. The httpx GET was already async. Now the loop stays free to service other subagents' IO, SSE streaming, and cancellation while a search/parse runs in a worker thread. No change to research depth or output — the detailed reports are unaffected; the server just stops wedging under load. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): tally reasoning + tool + skill work in the WorkBlock header The "Working…/Worked" card counted only tools ("· N tools"). Replace that with an icon-and-count tally of the turn's actual work, in the DS ToolCard name slot: - 🧠 reasoning steps (reasoning parts in the timeline) - 🔧 tool calls (timeline tool ids, minus skill loads) - 📚 skill loads (`load_skill` calls, broken out; the skill name rides its JSON input) Each kind shows only when present. Hovering the header opens a DS Tooltip breakdown — "2 reasoning steps", "5 tool calls · web_search ×3, fetch_url ×2", "1 skill load · deep-research" — so the collapsed card still tells you what happened without expanding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otoLabsAI#1329) Renders a MARKDOWN_SMOKE fixture exercising every markdown construct (headings, inline emphasis, nested lists, task lists, blockquotes, Shiki code, mermaid, KaTeX math, tables, images, footnotes, hr) through the real streamdown pipeline. Hard-asserts the structure + the interactive chrome the DS must theme (code-block copy button, table wrapper), and annotates the harder constructs' current render state instead of flaking — plus saves a screenshot of the rendered message for the brand/style audit. Findings it pins (feeding protoContent#297/protoLabsAI#298): - table chrome: 3 off-brand action buttons render (protoLabsAI#297) - task-list checkboxes: render OK - KaTeX math: 0 nodes — math is NOT wired in our streamdown setup (protoLabsAI#298) - mermaid: 0 SVG — diagrams fall back to a code block, NOT wired (protoLabsAI#298) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oses protoLabsAI#1330 (protoLabsAI#1331) The DS now ships a brand-styled streaming-markdown renderer (`@protolabsai/ui/markdown`, 0.48.1), the DS-owned version of what the console hand-rolled with streamdown (protoLabsAI#1328). It themes BOTH prose and streamdown's interactive chrome (code/table action buttons) to `--pl-*` via the stable `[data-streamdown]` hooks, wires KaTeX math, and renders mermaid as a themed code block. Closes the contribute-back from protoContent#297/protoLabsAI#298. Adoption: - Markdown.tsx → `<Markdown className="markdown">` from `@protolabsai/ui/markdown`; the `.markdown` class rides the DS `.pl-markdown` element so existing selectors still match. - theme.css: delete the hand-rolled `.markdown` element CSS — the DS owns prose + chrome. - tailwind.config.cjs: drop the streamdown dist `@source` scan — the DS markdown.css themes the chrome via `[data-streamdown]` in plain CSS, no Tailwind reliance. - main.tsx: import `katex/dist/katex.min.css` (math glyphs); `streamdown/styles.css` stays for the streaming token-fade. `@protolabsai/ui/styles.css` now carries `.pl-markdown`. - Chrome defaults to copy-only (download/fullscreen off for a chat bubble — protoLabsAI#297); mermaid stays a themed code block (DS `renderMermaid` is opt-in — it's heavy). - markdown-smoke.spec.ts: now asserts KaTeX math renders + the DS chrome; the prior gaps (math unwired, 3 off-brand buttons) are resolved. Dependency plumbing (the awkward part): - 0.48.1 lists `@types/react@^19` as a dependency and ships .tsx SOURCE our tsc compiles, so the tree must resolve a SINGLE @types/react or the DS source and our code disagree on `ReactNode`. Align the app to @types/react@19 (the DS is React-19-native): bump apps/web back to ^19, drop the root @18 pin, fix the 5 spots that relied on @18 (`JSX` global → `import type { JSX }` in ChatComponent/PluginsSurface; type the react-error-boundary `reset` param in ErrorBoundary/GoalsPanel/TasksPanel). - Drop `.npmrc legacy-peer-deps=true` (it silently disables npm `overrides`) and instead resolve the @docsearch/react react@18-vs-19 ERESOLVE with a targeted override pointing it at react@19. `npm ci` validates cleanly. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…LabsAI#1499) Build the previously-unbuilt ADR 0030 D5 slice so a monitor-mode goal is no longer an unbounded forever-tick: - deadline (ISO-8601 or epoch): a not-met monitor goal past its deadline finishes with terminal status `expired`, firing on_failed + the goal.failed bus event like exhausted/unachievable. - stall_after (N): after N consecutive checks with unchanged verifier evidence, fire a new on_stalled hook WITHOUT ending the goal — once per stall episode, re-armed when evidence changes — plus a best-effort goal.stalled bus event. A signal the external engine stopped moving. Both are plain data fields on the goal spec (not verifiers), so the Phase 1 chat trust-gate is unaffected. register_goal_hook (real + testkit) gains on_stalled; parse_control/set_goal_safe accept the two new fields; "expired" joins TERMINAL_STATUSES and the finish glyph map. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on concurrent search) (protoLabsAI#1500) `docs_search` dispatches `DocsIndex.search` onto thread-pool workers via `asyncio.to_thread`, but `DocsIndex` holds ONE `sqlite3.connect(":memory:", check_same_thread=False)` connection shared across all of them. A single sqlite connection is not safe for concurrent use across threads (`check_same_thread=False` only silences the guard — it adds no locking). When the agent fires two docs_search calls back-to-back, they race on the connection and corrupt cursor state, so the computed `bm25()` column reads back as NULL → `float(None)` → the observed `TypeError: float() argument must be a string or a real number, not 'NoneType'` (plus intermittent InterfaceError/IndexError), which escaped the narrow `except sqlite3.OperationalError` and surfaced to the agent as a failed tool call with "nothing came back". Fix + hardening: - Serialize every connection touch (seed + search) on a `threading.Lock`. A `:memory:` DB can't be shared via per-thread connections (each opens its own empty DB), so a lock is the right tool; the corpus is tiny and in-memory, so contention is nil. - Defense in depth: `float(score or 0.0)` guards a stray NULL, and search's `except` now catches any error (degrade to no-results, never raise into the tool). - `docs_search` wraps the call and returns a graceful "Docs search failed …" message so the agent is told it errored instead of getting a raw traceback / empty result. - Regression test hammers 12 threads × 200 concurrent searches: must never raise. Verified it fails on the pre-fix code (float/InterfaceError) and passes after. Note: graph/skills/index.py mirrors this shared-connection pattern and is queried every turn — same latent race, tracked separately. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion race as docs) (protoLabsAI#1501) SkillsIndex holds ONE `sqlite3.connect(..., check_same_thread=False)` connection and reuses it across threads. It's read on the per-turn hot path (`skill_summaries` / `discoverable_count` via KnowledgeMiddleware, always-on since ADR 0060) while the curator writes to it (`update_confidence`/`delete_skill`/`rebuild_index`). A single sqlite connection is not safe for concurrent use — `check_same_thread=False` only silences the guard, it adds no locking — so concurrent turns racing on the connection corrupt cursor state: `cur.fetchone()` returns None mid-flight → `['n']`/`int(None)` subscripting, plus `InterfaceError`/`IndexError`. Same class as the docs-index bug (protoLabsAI#1500); a 14-thread reader+writer stress harness reproduces 246 errors on the old code. Fix: a small `@_locked` decorator serializes every connection touch on a per-instance `threading.RLock` (reentrant so `replace_disk_skills`/`rebuild_index` → `add_skill` don't self-deadlock). Read-path `except` clauses broadened from `sqlite3.OperationalError` to `Exception` so a stray error degrades to empty/None/0 instead of raising into the turn. The connection is tiny + queries are fast, so serialization cost is negligible. Regression test hammers 10 readers + 4 writers × 150 iters: must never raise (verified 246 errors → 0). Full skills suite (index/curator/persistence/slash/layered/loader/ crud/structured) green: 125 passed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…al channel (ADR 0066) (protoLabsAI#1503) * feat(security): federation token + /api operator ceiling; operator goal channel (ADR 0066) Goal trust-gate Phase 2, Option B (dedicated operator channel). Phase 1 (protoLabsAI#1492) refused command/test/ci/data-expr goal verifiers from a /goal CHAT message for everyone — including the operator, who lost a legitimate feature. Phase 2 restores it via a dedicated operator-tier channel, and adds the path ceiling that makes a federation token meaningful. - auth: optional `auth.federation_token` (env A2A_FEDERATION_TOKEN). The middleware classifies each request by which secret matched (constant-time compare) → operator | federation, and DENIES a federation credential the whole /api operator surface (403) — plugin install/enable, config/SOUL rewrite, subagent runs, the operator goal set-path. /a2a + /v1 stay open to either tier. The ceiling lives entirely in the middleware, so no per-request trust threading is needed (Option B's simplification over A). - goals: GoalController.set_goal_operator accepts ANY verifier type; POST /api/goals now routes to it (was plugin-only). Safe because /api is operator-tier by the ceiling. set_goal_safe (agent/SDK/plugin) stays plugin-only. - Backward-compatible + opt-in: no federation_token ⇒ single-token mode, byte-for-byte unchanged (every bearer holder is the operator; in that mode a bearer holder already has RCE via /api/plugins/install, so the loosened goal endpoint adds no capability — the ceiling is the control, per ADR D4). Tests: federation denied /api (+ fleet-proxied variants) / allowed /a2a+/v1, operator full access, single-token + open-mode unchanged, _requires_operator classification; set_goal_operator accepts dangerous verifiers; config-roundtrip golden + REST-handler contract updated. 379 passed, ruff clean. Follow-ups (ADR 0066 slices): CLI `goal set`; console Goals set-form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(nav): regenerate nav.json for ADR 0066 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(adr): link ADR 0066 to federation-token follow-up protoLabsAI#1504 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d watches (ADR 0067) (protoLabsAI#1505) * feat(watches): standalone watch primitive — many concurrent supervised watches (ADR 0067) Resolves the ADR 0030 fork-in-the-road ("a separate watch primitive … open to the inverse call"): a supervisor agent needs to babysit MANY external processes at once, which the monitor-goal disposition can't (a goal is one-per-session). Adds a first-class `watch`. - graph/watches/: Watch type + WatchStore (keyed by watch id, MANY per instance, PROTOAGENT_INSTANCE-scoped) + WatchController (create/list/clear/evaluate/tick_all), reusing graph/goals/verifiers verbatim. Out-of-band _watch_loop tick (server), no agent turn. - Reaction on met (D3): enqueue an optional follow-up prompt as a one-shot agent turn via sdk.run_in_session (protoLabsAI#1494) + fire on_met hooks. deadline → expired (on_expired); stall_after unchanged checks → on_stalled once per episode (watch stays active). - Agent tools create_watch/list_watches/clear_watch — plugin-verifier only (like set_goal); shell/test/data watches are operator-only (the trusted=True path, next slice's /api/watches). watch + run_in_session compose into the parallel-supervision engine: N watches, each with its own trip-action. Slice 1 of ADR 0067. Follow-ups: operator /api/watches + sdk.create_watch/register_watch_hook (PR2); console Watches panel (PR3); migrate start_goal_loop monitor path onto watches + deprecate goal `monitor` mode. Tests: trust gate, MANY concurrent watches, met/not-met/expired/stall/tick, the run_in_session reaction. 15 pass (incl. nav sync); ruff + tool-inventory + import layering clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(watches): use the current infra.paths API (instance_paths().store) in WatchStore _resolve_base imported `scope_leaf`, removed by the instance-paths redesign (ADR 0065) — so WatchStore() blew up at server boot with ImportError. Unit tests missed it because they inject an explicit base_dir (skipping _resolve_base); live-smoke caught it (its whole point). Mirror the current GoalStore: `instance_paths().store("watches")`, WATCH_PATH override. Add a test that exercises _resolve_base so the boot path is covered. 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#1498) Subscribe the console Goals panel to the `goal.iteration` bus event (in addition to `goal.changed`) so a drive goal's `iter N/max` + last-reason row updates live on each continuation. The backend already publishes `goal.iteration` on the plugin bus for every drive-goal continuation (graph/goals/controller.py — the HOST.publish block in `evaluate`, ADR 0051 Slice 3), and the SSE bridge (operator_api/routes.py `_sse_frames`) streams *every* bus event with no server-side topic allow-list; the client filters by topic in apps/web/src/lib/events.ts. So the event already reaches the browser — the panel just wasn't consuming it. This mirrors the existing `goal.changed` subscription and invalidates queryKeys.goals so the already-rendered iteration count + last reason refresh while the loop runs. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tch_hook (ADR 0067 PR2) (protoLabsAI#1507) Slice 2 of the watch primitive — the operator + plugin surfaces on top of the protoLabsAI#1505 engine. - Operator REST: GET/POST/DELETE /api/watches → _operator_watches_{list,set,clear}. POST accepts ANY verifier type (command/test/ci/data) via WatchController.create(trusted=True), safe because /api is operator-tier by the ADR 0066 path ceiling. Mirrors /api/goals. - SDK: sdk.create_watch(*, condition, verifier, run_prompt=…, …) — plugin-verifier-only watch registration for plugins (hold many at once). - Plugin seam: registry.register_watch_hook(on_met/on_expired/on_stalled) → loader collects PluginLoadResult.watch_hooks → agent_init set_watch_hooks (mirrors the goal-hook seam), so a plugin reacts in-process when a watch trips. Tests: operator set accepts a command verifier / list / clear / disabled-when-off; create_watch plugin path + unavailable; registry+loader hook seam. 160 pass; ruff + boot-path imports clean. plugins guide SDK list updated. Follow-up: console Watches panel (PR3). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a right-rail Watches panel mirroring GoalsPanel: lists passive
verifier-only watches (condition, status pill, id · verifier.type ·
last_reason meta, Clear button), live-refreshed off the watch.* bus,
consuming GET /api/watches + DELETE /api/watches/{id} (PR protoLabsAI#1507).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lenames (CodeRabbit protoLabsAI#1505) (protoLabsAI#1509) Two findings from the protoLabsAI#1505 review: - Race (Major): WatchController.evaluate did an unlocked read-mutate-write, so the cadence tick_all and an event-driven evaluate_now on the SAME watch could interleave (lost stall increment, or a just-finished watch re-activated / its reaction double-fired). Serialize per watch id with an asyncio.Lock (evaluate → _evaluate_unlocked); distinct ids never block. - Filename collision (Minor): _safe_name mapped every unsafe char (incl. / and \) to _, so distinct ids like "a/b" and "a_b" shared one JSON file. Append a short hash of the raw id when sanitization changed it; already-safe ids stay human-readable. Tests: concurrent evaluate finishes once (fails without the lock); no filename collision across sanitized ids. 25 pass, ruff clean. (GoalController/GoalStore share the same latent pattern — out of scope here.) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1510) Add a compact, collapsed-by-default "New goal" form at the top of the Goals panel. It POSTs to the operator `/api/goals` endpoint (ADR 0066), which accepts any verifier type behind the /api operator ceiling. - api.setGoal({session_id, condition, verifier}) → POST /api/goals, mirroring the existing clearGoal/POST request() patterns. - <details> disclosure with session_id (default "operator"), a required condition, and a small verifier JSON textarea (default {"type":"llm"}) parsed on submit — invalid JSON shows an inline error and blocks submit. - Result surfaced via the shared DS useToast (success/error), goals query invalidated to refresh the list, condition cleared on success. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y (ADR 0067 migration) (protoLabsAI#1511) Watches (ADR 0067) replace the goal `monitor` disposition, so remove it entirely: a goal now does one thing — the agent drives a bounded loop toward a verifier — and `GoalState` sheds its union fields. Removed: - GoalState monitor fields (mode, deadline, stall_after, stall_streak, stalled_notified, last_checked) + the `expired` terminal status. - GoalController: the monitor branch in evaluate, evaluate_now, tick_monitor_goals, _parse_deadline/_parse_stall_after; mode/deadline/stall_after params on parse_control / set_goal_safe / set_goal_operator. - graph/goals/hooks.fire_stall_hook + on_stalled from register_goal_hook (registry/testkit). - server _monitor_goals_loop + its task wiring; config goal_monitor_interval. - sdk.start_goal_loop / stop_goal_loop / _to_cron (the OODA-loop helper) — create_watch is the replacement. - /api/goals + /goal chat set-paths drop mode/deadline/stall_after. Moved: the deadline/stall parsers now live on WatchController (watches use them; the /api/watches handler updated). BREAKING for forks that used start_goal_loop or a monitor goal → move to sdk.create_watch + register_watch_hook. ADR 0030 superseded by 0067; goal-mode + plugins guides updated; test_goal_monitor / test_goal_evaluate_now deleted, test_goal_loop trimmed to run_in_session. Full suite: 2547 passed. ruff + nav-sync clean. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ch rework (protoLabsAI#1512) The rework (13 PRs, protoLabsAI#1491–protoLabsAI#1511) shipped without CHANGELOG entries and left a few stale docs. True them up: - CHANGELOG [Unreleased]: watch primitive (ADR 0067), run_in_session, federation token + /api ceiling (ADR 0066), goal continuation tools, RCE-via-chat fix, and the BREAKING monitor-mode removal + CodeRabbit watch fix — grouped Added/Changed/Security/Fixed/Docs. - New docs/guides/watches.md (+ vitepress sidebar + guides index) — the watch subsystem had no guide. - README + starter-tools reference: the starter tool list now names the 3 goal + 3 watch tools (was just `set_goal`). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `.beads/issues.jsonl` tracker held 67 stale issues (111 across history) — long-closed work, superseded epics, and a few desktop/CI/portfolio tasks that no longer reflect active work. Cleared them all: `br delete --force --hard` on every id, then `br sync --flush-only --force` to flush the now-empty DB out to the tracked JSONL (overriding beads' empty-DB export guard). `br ready` / `br list --all` → 0. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) (protoLabsAI#1517) Mermaid and SVG artifacts now render into a shared transform-driven viewport instead of a static centered graphic: - scroll-wheel / pinch to zoom, cursor-anchored (the point under the pointer stays put) - click-drag to pan - a Reset control that re-fits the diagram to the panel - mermaid re-fits automatically after its async render (window.__artFit) The issue framed this as "copy the SVG renderer's existing zoom/pan" — but SVG had none either (it only did place-items:center). Both graphic kinds render an <svg> into the sandboxed frame, so one `viewport(...)` wrapper covers both. Self-contained JS/CSS styled off the base() token carry — no DS stylesheet needed in the graphic frames. Bumps the artifact plugin to v0.14.0. Adds a regression test for the viewport wiring; the existing 2-</script> guard confirms the injected scripts escape their close correctly. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bsAI#1502) (protoLabsAI#1518) The Knowledge panel's Upload (source ingest) and Add (typed entry) buttons expanded a form inline inside the narrow sidebar panel — cramped for file pickers / URL + metadata fields, and it pushed the knowledge list out of view. Wrap both in a centered DS Dialog (width min(680px,94vw)) so the forms have room and the list stays visible behind the overlay. The form bodies are unchanged (issue non-goal), and per-row EDIT stays inline — it belongs next to the chunk it edits. UI-only; no API change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oLabsAI#1457) (protoLabsAI#1519) * feat(web): expose registerKeybinding on the fork extension seam (protoLabsAI#1457) The keybinding registry describes itself as "the fork/plugin seam, mirroring the other src/ext/ registries" — but it was the one register* not re-exported from src/ext/index.ts, so a fork following the ext README couldn't `import { registerKeybinding } from "./index"` like it does for surfaces / slash / composer / palette. Re-export registerKeybinding + registeredKeybindings + the Keybinding type from the seam index, and document the pattern in the ext README. The rest of the keybinding feature already shipped in protoLabsAI#1364: registered binds auto-appear in Settings ▸ Keyboard, are user-rebindable, fire through the global host, and the rebind UI already blocks conflicting combos in overlapping scopes. This completes the discoverable public surface for the fork path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(adr-0061): list registerKeybinding among the fork-seam registries (protoLabsAI#1457) The keybinding registry is a peer of registerSlashCommand / registerComposerAction / registerPaletteCommand on the same src/ext seam (exposed on src/ext/index.ts in this change). Enumerate it in ADR 0061 so the registry list stays complete, cross-referencing ADR 0063. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bsAI#1496) (protoLabsAI#1523) ↑ recalls previously-submitted messages into the composer (newest first), ↓ walks back toward the live draft — readline/shell style. Recalled messages are editable before resending; on submit the (edited) text becomes the newest history entry. - History only triggers at the edges: ↑ recalls only when the caret is on the first line, ↓ only on the last line — so multi-line editing keeps normal caret movement. - The last 100 submitted messages persist in localStorage (one ring shared across chat slots, consecutive-deduped), survive reloads. - Recording covers both send() and the mid-turn queueSteer() path. New `chat/inputHistory.ts` (cached, crash-safe store) + unit tests, two e2e specs (recall/walk + edit-and-resend), and a composer placeholder hint. Full web unit suite 215 green; e2e verified locally against the built app. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1506) (protoLabsAI#1531) * docs(adr): 0068 — developer flags & the Developer panel (protoLabsAI#1506) A design for a local/static feature-flag system (off/dev/beta/on) to gate pre-release functionality, plus a Developer panel to view/toggle flags. Grounds every decision in existing machinery to avoid a parallel system: one backend registry shaped like the config FIELDS list, a runtime channel the tier is measured against, resolution precedence mirroring the plugin-config env>UI>default ladder, a device-local CONSOLE_SECTIONS panel, and a remove_by cleanup contract. Articulates why a flag is neither a plugin (load-time capability) nor a setting (permanent user config). Status: Proposed. Also backfills the ADR index, which had drifted (missing 0065/0066/0067). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: regenerate nav.json for ADR 0068 (gen_docs_nav.py) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce (protoLabsAI#1524) The portfolio guide covered only the federated (register standing remotes) model and omitted team spawning entirely — portfolio_spinup_team wasn't even mentioned. Add a 'Spinning up teams on demand' section (spinup/dispatch/autodispose/teardown/teams) and document v0.14 gateway inheritance: a spawned team inherits the PM's resolved gateway + OPENAI_API_KEY, so it runs real turns with no team_template / creds prep; spinup preflights and fails loudly on an unreachable model. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-v0.77.0 # Conflicts: # uv.lock
|
👀 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.77.0 (189 commits)
VERDICT: WARN
CI Status
- build: in_progress
- Python tests: in_progress
- Lint (ruff + import contracts): in_progress
- A2A live smoke (lean tier): in_progress
- gitleaks (tree): in_progress
- Web E2E smoke: in_progress
- Verify workspace config: in_progress
- Fleet integration (multi-instance): in_progress
Diff Review
- Diff too large for GitHub API (300+ files) — expected for a 189-commit upstream sync; cannot perform line-level review.
- PR description is specific: claims only merge conflict was
uv.lock(resolved to upstream), no conflict markers, fork customizations verified present, 40 tests pass.
Observations
- Gap — unverified: fork customizations (
config/SOUL.md,scripts/team-up.sh, etc.) could not be verified present post-merge;path_existsnot available via pr_inspector. Author's claim accepted at face value pending CI. - Gap — structural review skipped: clawpatch unavailable for
protoLabsAI/leadEngineer(not in project registry). No cross-file structural analysis performed on this 189-commit sync. - Gap — non-terminal CI: all 8 checks still running. Formal PASS/FAIL deferred until checks complete. If any check fails, this becomes FAIL.
- LOW:
uv.lockmerge conflict resolution to upstream is the correct choice (fork adds no Python deps), but the resolution cannot be verified without the diff.
⚠ Merge note: PR description correctly recommends merge-commit over squash — squashing would flatten upstream lineage.
— Quinn, QA Engineer
|
Submitted COMMENT review on #3. |
There was a problem hiding this comment.
QA Audit — PR #3 | chore: sync fork to upstream protoAgent v0.77.0 (189 commits)
VERDICT: FAIL
CI Status
- Python tests: failure
- build: success
- A2A live smoke (lean tier): success
- gitleaks (tree): success
- Web E2E smoke: success
- Verify workspace config: success
- Lint (ruff + import contracts): success
- Fleet integration (multi-instance): success
- deploy: skipped
Diff Review
- 189-commit upstream sync from protoAgent v0.66.0 → v0.77.0; diff too large for automated summary (GitHub 406).
- PR claims 4 additive fork commits preserved; only merge conflict was
uv.lock(resolved to upstream). - Includes SQLite thread-safety fixes (protoAgent protoLabsAI#1500, protoLabsAI#1501) the fork previously carried unfixed copies of.
Observations
- HIGH: Python tests CI check is failing. PR description claims 40 tests pass locally (
test_docs_plugin.py,test_skill_index.py) but CI disagrees. Blocking — need a passing CI run before merge. - LOW: Clawpatch structural review unavailable (leadEngineer not in project registry) — cannot perform cross-file analysis on the 189-commit sync.
- LOW: Cannot verify fork customizations (
config/SOUL.md,protoagent.bundle.yaml,scripts/team-up.sh,scripts/team-down.sh,config/langgraph-config.example.yaml) survived the merge — diff too large for path-level inspection (GitHub 406 on diff endpoint).
— Quinn, QA Engineer
|
Submitted CHANGES_REQUESTED review on |
…nfig The upstream sync pulled in the stricter config-roundtrip golden, which pins the EXAMPLE config's parsed fields. leadEngineer's fork customizes config/langgraph-config.example.yaml (the Lead Engineer setup), so the golden must track it: agent_runtime=acp:claude, identity= engineering-team, plugins.enabled=[delegates,project_board,agent_browser], acp_agents.claude, filesystem.allow_run=false + a REPLACE_ME project. 26 config-roundtrip tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
QA Audit — PR #3 | chore: sync fork to upstream protoAgent v0.77.0 (189 commits)
VERDICT: WARN (non-terminal CI — formal PASS/FAIL deferred)
CI Status
- gitleaks (tree): queued
- Web E2E smoke: in_progress
- build: queued
- Fleet integration (multi-instance): queued
- Verify workspace config: queued
- Lint (ruff + import contracts): queued
- A2A live smoke (lean tier): queued
- Python tests: queued
⏳ All 8 checks still running. Formal verdict deferred until CI is terminal.
Diff Review
- 300+ files changed (189 commits). GitHub API rejected diff fetch — expected for a sync of this scale.
- PR is a clean upstream sync: fork customizations are 4 additive config/scaffold commits. Only merge conflict was
uv.lock, resolved to upstream. - No unresolved CodeRabbit threads.
Observations
- GAP: CI non-terminal — cannot verify test suite, lint, build, or smoke tests. Deferring PASS/FAIL.
- GAP: Diff not reviewable via API — 300+ file limit exceeded. Relies on PR author attestation that no conflict markers remain and fork customizations are intact.
- GAP: Clawpatch structural review unavailable — repo not in project registry.
- LOW: Merge strategy note in PR body is correct — squash would flatten upstream lineage and break future sync merge-bases.
Bottom line: Looks like a clean, honest sync PR. Once CI goes terminal-green, this should be a straightforward PASS. Re-review will confirm.
— 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).
What
Syncs the leadEngineer fork up to upstream protoAgent v0.77.0 — 189 commits behind (fork point was v0.66.0). Brings in ~11 minor versions of runtime fixes and features, including the two SQLite thread-safety fixes this fork was carrying unfixed copies of:
How clean
The fork's customization is 4 commits, all additive config/scaffold —
config/SOUL.md(Lead Engineer persona),protoagent.bundle.yaml,scripts/team-up.sh/team-down.sh,config/langgraph-config.example.yaml. The only merge conflict wasuv.lock(resolved to upstream's — the fork adds no Python deps). All customizations verified present post-merge; version → 0.77.0.Verify
tests/test_docs_plugin.py+tests/test_skill_index.py→ 40 passed (confirms the race fixes landed).⚠ Merge note
Merge with a merge commit, not squash — squashing flattens the upstream lineage and breaks the merge-base for future syncs.
🤖 Generated with Claude Code