mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
feat/opencode2-native-commands
364 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
79cb016828 |
fix(agent-terminal): approve node-pty install scripts for npm 12 (#1411)
npm 12 blocks dependency lifecycle scripts unless the installing project names the package in allowScripts. node-pty ships prebuilds for macOS and Windows only, so on Linux its install script is what compiles build/Release/pty.node. The generated managed-runtime package.json declared no allowScripts, so npm installed the tree, exited 0, and the Agent tab later failed with "Agent terminal runtime could not load WebTUI". Generate the runtime manifest with a name-only node-pty approval, and verify after install that pty.node actually exists (build/Release, build/Debug, or prebuilds/<platform>-<arch>, which is node-pty's own resolution order). A missing binary now triggers one targeted npm rebuild and, if that does not repair it, fails provisioning with the blocked-scripts diagnostic and the manual remedy instead of leaving a runtime that looks installed. Closes #1409 |
||
|
|
9e3af49f84 | chore: bump version to 0.27.9 | ||
|
|
c2950e709f |
fix: pre-release QA findings for 0.27.9 (#1405)
Fixes from the 0.27.9 pre-release review. Servers: an unreadable rendered-HTML root falls back to the startup snapshot on both runtimes with a once-per-process warning instead of hanging (Pi) or answering 500 (Bun); the version diff is recomputed against current bytes on reload and carried through the in-app Refresh instead of being dropped, with no history write on a GET. Client: a Refresh action on the compact touch shell; HtmlSurfaceControls renders Refresh independently of the eye; the dead HtmlSurfaceActions removed. Threading: one linear, cycle-safe reply resolution shared by the annotations panel, its sort, and the export (5,000-chain tests), PATCH ingest on both runtimes rejects self-references and cycles, nothing is ever dropped from feedback. WebMCP and viewer hygiene: bounded tombstone and request memories, per-instance minted ids, nudge id caps, waiter cleanup on unmount, a shared retry epoch for diagram blocks. Docs: HTML Refresh documented, the WebMCP design pointer fixed, marketing pages updated. AI-assisted (Claude) under maintainer direction. |
||
|
|
469046f4e9 |
fix(uninstall): edit the Windows user PATH through the registry with a best-effort change broadcast (#1403)
The Windows uninstaller removed its PATH entry through .NET's SetEnvironmentVariable, whose synchronous settings-change broadcast can stall behind a hung window past the 15 second command timeout and make the uninstaller refuse to proceed (seen three times on one CI runner). The edit now goes through the registry directly (reading unexpanded, preserving the value kind), echoes the original value as proof of the write, and broadcasts the change best-effort with an abort-if-hung timeout that never affects the exit code; the restore path gets the same treatment with a sentinel. A completed write is trusted regardless of how PowerShell ended, while an unproven write still fails closed and preserves the CLI. Tests include a real PowerShell parse check of both scripts. AI-assisted (Claude) under maintainer direction. |
||
|
|
6407ef5d97 |
feat(annotate): manual refresh of rendered HTML from disk (#1232)
Local rendered-HTML annotate sessions get a Refresh action beside Hide tools: the document is re-fetched through /api/doc, the sandboxed viewer remounts, annotations are re-anchored and the ones that no longer match are reported while their comments are kept, and stale diff and share state is reset. Maintainer additions on top of the contributor's work: share-link invalidation no longer keys on the resolver's identity, /api/plan and /api/share-html serve a local root HTML file from its current bytes on both runtimes so a reload does not revert the page under the annotations, the Refresh button keeps keyboard focus via aria-disabled, and the tests were hardened. Verified end to end in a real browser. Thanks @leoreisdias. AI-assisted (Claude) under maintainer direction. |
||
|
|
b381ecbe12 | chore: bump version to 0.27.8 | ||
|
|
34f25e79e2 | chore: bump version to 0.27.7 | ||
|
|
db86d38ca4 |
feat(skills): top-level plannotator knowledge skill, per-host install, and plannotator.ai/llms.txt (#1377)
* feat(skills): add the plannotator knowledge-layer skill with a CLI freshness guard A new model-invocable core skill (apps/skills/core/plannotator) that teaches an agent the whole CLI surface: decision guide, per-command reference with flags and exit codes, env vars, the external-annotations API, and a do-not list. The existing plannotator-* core skills stay lightweight action stubs. A freshness test (apps/hook/server/plannotator-skill-reference.test.ts) parses the skill's documented subcommands and flags and diffs them against cli.ts usage text plus the CLI arg-parsing sources, in both directions, so the reference cannot drift from the real CLI without failing the suite. Installers copy the single-sourced core body into ~/.claude/skills and ~/.agents/skills on all three platforms; uninstall removes it from both scopes. The skill ships model-invocable as a documented exception to the locked-by-default rule, asserted both ways in install.test.ts. * feat(marketing): serve the plannotator knowledge skill as /llms.txt Single-sourced at build time from apps/skills/core/plannotator/SKILL.md per the llmstxt.org spec (H1, blockquote, detail sections, Docs link list), so the CLI freshness guard transitively keeps llms.txt current. * fix(skills): reach every install path with the plannotator knowledge skill The knowledge skill reached Claude Code and ~/.agents but was missing from three install paths. Six fixes from the install-reach review of #1377. Kiro: the installer's Kiro leg copied only the two action skills, so Kiro users got launchers and no CLI reference. One copy line per installer, and "plannotator" joins uninstall.ts's KIRO_SKILLS. OpenCode npm: @plannotator/opencode's postinstall copied only commands/*.md. The package now ships the skill (copied at build time like the HTML assets, gitignored so the shipped copy cannot drift) and postinstall places it under ${XDG_CONFIG_HOME:-$HOME/.config}/opencode/skills/plannotator/, which is a path OpenCode really scans ({skill,skills}/**/SKILL.md under xdgConfig/ opencode). Uninstall sweeps it, skills only, so a user's own opencode/commands/plannotator.md stays out of scope. Pi npm: vendor.sh copies the skill to apps/pi-extension/skills/plannotator/ and package.json declares it under pi.skills, which Pi resolves relative to the package root. Neither vendored copy carries the // @generated header the .ts files use: a SKILL.md must open with its frontmatter on line 1. llms.txt: the endpoint resolved the skill through process.cwd(), which breaks under any invocation but --cwd apps/marketing. new URL(import.meta.url) does not fix it either, because Vite rewrites import.meta.url to the emitted SSR chunk's location. Inlined with Vite's ?raw, resolved by the bundler relative to the source file. Also drops the summary paragraph the required blockquote already carries; SKILL.md itself is unchanged. Uninstall: KNOWLEDGE_SKILLS is a separate list from CORE_SKILLS precisely so the bare name "plannotator" cannot leak into LEGACY_COMMAND_NAMES or STALE_CODEX_SKILLS and delete a user's own files. Nothing tested that; now a test proves the five installed scopes are removed and commands/plannotator.md (Claude and OpenCode) plus ~/.codex/skills/plannotator survive. Also cleanupStaleSkillLayout now knows KNOWLEDGE_SKILLS. Origins: oh-my-pi (#1373) was missing from SKILL.md's PLANNOTATOR_ORIGIN row. The guard now imports AGENT_CONFIG and asserts the row names every key and invents none, and its header comment is narrowed to what it actually proves: bidirectional for subcommands and origins, one-directional for flags. AI-assisted (Claude) under maintainer direction. |
||
|
|
e206a1f5e8 |
fix(review): infer the jj line-of-work base from the fork point (#1365)
* fix(review): detect JJ mutable line-of-work base Use JJ's mutable-stack revset to find the line boundary directly instead of inferring a parent from bookmark ordering, which is ambiguous because JJ has no current bookmark. * fix(review): harden the JJ line-of-work base inference Maintainer follow-up on the line-of-work base detection. Skip the bookmarks `jj git push --change` generates. They name one change, not a line of work, and they do reach the fork point: a colleague's pushed change bookmark arrives as an untracked remote bookmark, which makes its commit immutable and therefore a candidate base, so the reviewer was told they were comparing against `push-vmopwunwxopv@origin`. The commit id is used instead. Pass a full commit id through `jjCompareTargetRevset` as a revision. It has no separators, so the commit-id fallback was being wrapped as `bookmarks(exact:"<sha>")`, which resolves to no revisions and made the whole Line of work diff fail. Fall back to `trunk()` instead of throwing. The only live caller is `getJjContext` on the review startup path, which has no handler above it, so a throw aborted `plannotator review` with a stack trace before the server was built rather than reporting anything. That also covers a `jj` too old for `fork_point`/`reachable`. Make the query explicitly single-record with `latest(..., 1)`. The parser reads one record, and bookmark preference (remote before local) is only meaningful within one commit, so the tie-break belongs in the revset rather than in a silent "first row wins" slice. Isolate the real-jj test behind its own JJ_CONFIG. It was reading the developer's real config, where `[signing] behavior = "own"` alone makes it fail with a GPG error. Live fixtures cover the generated-push-bookmark stack, the untracked remote push bookmark, and a stacked local bookmark. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
8a8d0544c6 |
feat: detect the oh-my-pi harness as its own agent origin (#1373)
* feat: detect the oh-my-pi harness as its own agent origin - omp injects OMPCODE=1 (+ a CLAUDECODE=1 compat shim) into every Bash-tool child; the env chain now maps it to a dedicated oh-my-pi origin labeled "Oh My Pi". - Distinct origin rather than aliasing claude-code, because the claude-code-only gates (permission-mode setup, permissionMode in approve) presuppose a PermissionRequest hook that a bash-invoked plannotator never has; omp has no approve support and no planning integration yet. - Fallback deliberately left "claude-code"; wording unchanged. - No dedicated Ask AI provider for oh-my-pi. * chore: sync lockfile workspace versions * fix: check OMPCODE last so runtimes inside an OMP session keep their label OMP exports OMPCODE into every shell it spawns. With the check at the top of the chain, opencode/codex/... launched from an OMP session inherited OMPCODE and were mislabeled "Oh My Pi". Moving it just above the terminal fallback lets specific runtime env vars win; OMPCODE still beats the claude-code fallback for bare shells. Reported by backnotprop in #1373. |
||
|
|
6e20ec78e8 | chore: bump version to 0.27.6 | ||
|
|
89f0b6628e |
feat(pi): live local app annotation through a shared proxy core and Node transport (#1366)
Phase 2 of live app annotation: full parity on Pi over one shared implementation instead of drifting copies. - Extract every proxy decision into packages/shared/live-proxy-core.ts (HTML injector state machine, loopback/Host/Origin predicates, CSP/X-Frame-Options policy, redirect rewrite, WS origin gate, bridge assembly, liveAppDraftIdentity) and the CLI probe + live-mode messages into packages/shared/live-probe.ts. packages/server/live-proxy.ts is now a thin Bun transport over the core; its test suite passes unmodified. - Add packages/shared/live-proxy-node.ts, the node:http transport the Pi extension runs: streaming request/response piping through the shared injector, and WebSocket (HMR) passthrough that replays the client's handshake upstream over raw TCP and pipes the sockets byte-for-byte. Transport tests run the proxy in a real node child process, because Bun's node:http shim drops writes to an upgrade event's socket. - Wire Pi: /plannotator-annotate probes loopback URLs live-first with the shared probe (same 3s timeout, same <500 gate, same messages), recognizes --app/--static via parseAnnotateArgs's liveFlags opt-in (OpenCode deliberately does not opt in), and serves mode annotate-app from serverAnnotate.ts with the shared per-target draft identity, live sessions excluded from history/submissions, the remote hard-off throw, and guarded live-proxy shutdown. - Vendor live-proxy-core/live-probe/live-proxy-node plus the dependency-free bridge-script constants to generated/. - Docs: AGENTS.md phase-gate passages, marketing annotate page, Pi README. |
||
|
|
b1a46d0a57 | chore: bump version to 0.27.5 | ||
|
|
67f47dbac1 |
fix(annotate): armed-mode interaction fixes from the v0.27.5 QA gate (#1363)
* fix(annotate): pre-release QA fixes for the armed-mode interaction seams
Six confirmed QA findings on the HTML/live annotate surface plus missing
pi-extension resync coverage:
1. Armed pinpoint drifted click (>4px, no selection) was swallowed AND
leaked to the page: the always-on drag work armed the trailing-click
suppression on drift alone. The mouseup arming site now requires the
drag to have actually produced a text selection; drifted clicks pin
normally and never reach the page. Bridge tests for armed drift,
armed real drag, and Interact drift.
2. Esc ladder: hover-clear is no longer its own rung; clearing the
pinpoint outline and posting annotate-exit happen on the same press
when no draft is open. Draft-close keeps its own press.
3. Compact touch layouts no longer apply a restored toolsHidden:true
chrome cookie (both header toggles are desktop-only, so applying it
stranded the user); the cookie value is preserved for desktop.
4. The live-app probe now announces the static-conversion downgrade on
stderr when a loopback probe fails, naming --app to force live mode.
5. Live-app export: page group headers are now '## Page:' with '### N.'
entries nested below them; exports without pageUrl stay byte-identical.
6. Shift+1-4 mode shortcuts no longer fire while the annotation
toolbar's type-to-comment listener owns printable keys, so typing
! @ # $ into a starting comment cannot silently switch modes.
Also adds the missing tests for the two resyncPhaseFromSession
executing->idle fallbacks that arm idleNoticePending (verified by
mutation: flipping either arm fails its test).
* fix(annotate): compact arm/disarm affordance, guarded shutdown, restored chrome guards
Follow-up scope from the forensics sweep, same surface:
- Compact touch layouts get Options-menu actions for the HTML/live
surface: 'Annotate page'/'Interact with page' (the desktop pen and
Mod+Shift+A were unreachable on touch, so every tap annotated with no
way out) and 'Show tools'/'Hide tools' (the desktop eye). With the
menu as the way back, the toolsHidden cookie now applies on compact
again (desktop parity) instead of being ignored.
- The annotate servers' stop() now guards every disposal step
individually (Bun: runGuardedShutdown, mirrored inline in Pi): a
throwing agent-terminal teardown (#1314-class) no longer skips
liveProxy.stop() and the other disposals after it. Unit-tested with a
throwing disposer.
- Re-added the two regression guards dropped in the htmlHideTools ->
htmlChrome test rename: the restore commit never writes stale
pre-restore chrome values to the cookie, and the sidebar stays
reachable via Mod+B while tools are hidden.
* fix(annotate): scope the Agent TUI display reset to display settings only
The Display popover's 'Reset terminal display settings' button also called
onSideChange('left'), durably overwriting a user's chosen right/hidden
placement in config.json with no disclosure — the label scopes the reset
to font/appearance. Position is a layout preference with its own explicit
segmented control right below, so the reset no longer touches it: the
button now resets exactly the display settings through the panel's one
sanitized update path, and the popover no longer has any code path from
reset to the side.
AgentTerminalDisplayPopover is now exported with a defaultOpen test seam
(the surrounding panel needs a live WebTUI session to render it); tests
assert reset restores the display defaults without firing onSideChange,
and that the Position control remains the explicit way to change
placement.
|
||
|
|
271fcefded |
fix(server): live-proxy injection and config write hardening (#1364)
* fix(server): live-proxy injection and config write hardening Four confirmed pre-release QA findings, each with a test that fails on the pre-fix source. live-proxy: the HTML injector scanned for head markers with no notion of comments, so a codegen banner naming <head> before the real tag captured the bridge script into a dead comment span: never executed, annotation silently broken, no warning. The scanner now skips comments and the '>'-terminated markup-declaration / bogus-comment spans (doctype, CDATA-ish, <?...>) before matching, inside the same chunk-boundary state machine. Raw-text element contents are still not tracked; that limit and its degraded outcome are documented in the source. live-proxy: new URL(req.url) ran before Host validation, so a Host-less HTTP/1.0 request threw and served Bun's internal debug page with a stack trace. Host validation now runs first, and URL construction takes the same 403 path on failure. live-proxy: the text/html content-type test was case-sensitive, so a valid TEXT/HTML response skipped injection and the framing rewrites. config: saveConfig was an unlocked read-merge-write, so two processes sharing a data dir dropped each other's keys while both reported success. The read-merge-write now runs under an O_EXCL advisory lockfile with a bounded wait and stale takeover, degrading to the old behavior with a warning rather than ever hanging, and the write itself is temp+rename so lock-free readers cannot observe a torn file. Also consolidates the duplicated agent-terminal side predicate onto the single definition in @plannotator/core. * fix(annotate): give live app sessions their own draft slot mode "annotate-app" resolves markdown to "" by construction (the page lives behind the proxy, not in a string the server holds), and the autosave draft key was contentHash of that body. Every live session on the machine therefore collapsed to the one hash of the empty string and shared a single draft slot: two sessions against different dev servers read and overwrote each other's in-progress annotations, deterministically. A live session's identity is its target, exactly as a folder session's identity is its folder path, so the key is now derived from the target URL (normalized through the URL parser so the same dev server recovers its draft when spelled with or without a trailing slash). Classic file and folder keying is untouched. Pi has no live app mode (no annotate-app, liveApp or live-proxy outside its vendored generated/ tree), so there is nothing to mirror there. |
||
|
|
81ecd67e75 |
feat(annotate): configurable Agent TUI placement with durable config and Hidden state (#1050)
* Allow annotate terminal to dock on either side * Allow annotate terminal to dock on either side * fix(annotate): persist Agent TUI preferences through the settings registry The Position control introduced in #1050 stored its choice in a cookie via hand-rolled helpers that bypassed the settings registry. Every annotate session runs on its own random port, so a cookie is scoped to one session: the placement silently reset on the next annotate. The sibling `plannotator-annotate-agent-terminal-default` cookie (preferred agent) had the same gap. Both now follow the `conventionalComments` precedent exactly: * `agentTerminalSide` and `agentTerminalDefaultAgent` join `SETTINGS` with serverKey/fromServer/toServer, reusing their existing cookie keys so a user who already picked a side keeps it across the upgrade. * `PlannotatorConfig` gains both as flat keys (only diffOptions, theme, reviewAnalysis and prompts deep-merge in saveConfig), emitted from `getServerConfig()` behind an `isAgentTerminalSide` guard so a hand-edited config.json cannot advertise a side that does not exist. * Both keys are added to the two /api/config allowlists: the Bun annotate server and the hand-mirrored Pi one. The side vocabulary moves to @plannotator/core/agent-terminal (widened to include the `hidden` state added next) so the registry can reach it without closing an import cycle through ConfigStore; the ui util keeps its public API by re-exporting. Regenerates the pinned guide viewer manifest, which shifts by 0.1 KB gz because the settings registry now reaches into core/agent-terminal. AI-assisted (Claude) under maintainer direction. * feat(annotate): add a Hidden Agent TUI position and extract its layout Builds on the Left/Right Position control from #1050. Hidden (third state of the Position control) Hidden is a durable preference that the Agent TUI is not part of this user's layout: nothing is docked, and choosing Hidden while the terminal is open closes it (from either surface that offers the control). It is a default, not a lock. The rail toggle, the Shift Shift shortcut and a message routed to the agent all still open the panel for the session, and none of them rewrites the preference, so explicit intent wins now without changing what happens next session. A `hidden` preference owns no dock edge, so a session open falls back to the historic left placement. Because the Position control lives inside the terminal's own popover, and Hidden closes that popover along with the terminal, the same control is now also in the Settings dialog (General tab, annotate mode). That is the way back from Hidden, and it also answers the review note that Position could not be preconfigured before the terminal was ever opened. It is gated on the terminal actually being available in the session so a remote or runtime-less annotate never offers a dead control. Both surfaces write the same `agentTerminalSide` config value and read it through ConfigStore, so they cannot drift. The existing transient hide affordances (header X, resize handle click and drag-snap, rail toggle, Shift Shift) are unchanged and stay session scoped. A running agent still stays mounted off-layout when collapsed, so hiding the panel never kills the PTY. Review fixes * Extract `getAgentTerminalLayout` from App.tsx into packages/editor/agentTerminalLayout.ts with a table test over {side including hidden} x {open} x {running} x {wideMode} x {belowBreakpoint} x {rightPanelOpen}, asserting the invariants that can actually regress: never docked on both edges, never visible below `lg` or in wide mode, a collapsed running terminal stays mounted zero-width on its own edge, and the right panel is suppressed exactly when a VISIBLE right-docked terminal holds the slot. * Fix `aiSurfaceOpen`, which still read `effectivePanelOpen && rightSidebarTab === 'ai'` after its siblings moved to `isRightPanelVisible`. A right-docked terminal visually suppresses the panel but left the Ask AI model-discovery effect firing for an invisible surface, which is exactly the eager provider work that gate exists to avoid. The layout computation is hoisted above the consumer so it can use the same fact the JSX does. * Document the right-slot invariant at both coordination sites. The asymmetry is deliberate: the panel evicts the terminal (which keeps running off-layout, so reopening resumes the same session), while the terminal only suppresses the panel visually so dismissing it restores the user's place. Symmetry would make every short terminal detour cost the reviewer their open surface. * Name the `useIsMobile(1024)` literal `AGENT_TERMINAL_LG_BREAKPOINT`, tied to the panel's own `hidden lg:flex`. * Restore `hideAgentTerminal()` in the resize hook instead of the raw setter, and point the handle at the resolved placement. AI-assisted (Claude) under maintainer direction. --------- Co-authored-by: Michael Ramos <backnotprop@gmail.com> |
||
|
|
2ca55c8332 |
feat(annotate): live local app annotation through a loopback reverse proxy (#1352)
* feat(bridge): additive live-mode gate + LIVE_BRIDGE_BOOTSTRAP Adds the config-gated live branch to BRIDGE_SCRIPT: frame gate, pinned parent origin, token-stamped postToParent, origin+token checks on both inbound handlers, pinpoint-only clamp, vim and resize off, pageUrl on ready, and coalesced page-change reporting for SPA history navigation. With no config present (srcdoc) every branch is inert and behavior is unchanged; the existing html-viewer suites pass unmodified as the regression proof. LIVE_BRIDGE_BOOTSTRAP installs the annotation CSS from the JSON config prelude before the IIFE runs. New package export exposes the string constants without the React barrel. * feat(ui): live-session parent side for proxied app annotation useHtmlAnnotation gains a live option (origin + token validated before parseBridgeMessage; token + concrete targetOrigin on every outbound post) and a validated page-change message with onPageChange. HtmlViewer gains src/liveSession/currentPageUrl/onPageChange: src-mode iframe with no sandbox and no srcdoc, ready pageUrl handling, per-page restore filtering with explicit clear-marks + re-sync on navigation, and one postToBridge choke point for its direct posts. Annotation.pageUrl is additive; exportAnnotations groups by page (with global numbering kept) only when a pageUrl is present, byte-identical otherwise. AnnotationPanel shows the page label; AnnotationToolstrip can hide the input switch. The editor app wires mode annotate-app: full-viewport live surface, forced pinpoint, vim off, diff/share hidden, pageUrl stamping. * feat(server): loopback reverse proxy for live app annotation Whole-origin mirror of a local dev server on a dedicated 127.0.0.1 port: streaming bridge injection (after the head open tag, before a bare </head>, or appended; exactly one per document; 8-byte holdback plus a state machine for tags split across chunks), header hygiene (upstream Host rewrite, X-Forwarded-*, identity Accept-Encoding on document intent only, hop-by-hop strip), CSP drop-and-replace with frame-ancestors listing the editor origins, X-Frame-Options removal, target-origin Location rewrite, byte-identical passthrough for assets and encoded HTML (no injection, once-per-session diagnostic), SSE streaming, and WebSocket passthrough with a bounded pending queue for HMR. Host header validation runs before any upstream contact; the bind is the literal loopback constant and the advertised-URL override is never applied. Tests boot a fake dev server and cover injection, hygiene, fidelity, WS echo, and the security posture. * feat(annotate): annotate-app server mode + CLI live probe with remote hard-off startAnnotateServer gains mode annotate-app and a liveApp option: it throws under PLANNOTATOR_REMOTE, generates the per-session token, composes the proxy-served bridge body (JSON config prelude with both editor origin forms, localhost first, plus bootstrap and bridge supplied by the caller so packages/server never imports @plannotator/ui), starts the loopback proxy after the annotate port is known, serves the live /api/plan payload (no rawHtml, no version fields, sharing off), and stops the proxy with the server. Version history and durable submission records stay excluded via the explicit mode gate. The CLI resolution probes loopback http URLs (3s, accept text/html) and defaults them to live mode when the probe returns HTML; --static forces conversion, --app forces live and fails loudly on non-loopback, https, unreachable, or non-HTML targets; both flags are mutually exclusive transport-shape flags never echoed in the tolerant handoff. A live resolution under PLANNOTATOR_REMOTE is a startup failure suggesting --static. OpenCode and Pi parsers are untouched this phase. * test(live-annotate): protocol, server, and probe suites + smoke script + docs htmlLiveProtocol.test.tsx covers the parent trust boundary (origin and token rejection before parseBridgeMessage, token + targetOrigin on every outbound post, validated page-change and ready pageUrl, per-page restore filtering with full-list numbering) and the bridge live gate, executed as the composed config + bootstrap + bridge body inside a dedicated harness iframe so the srcdoc suites keep running the same script uncontaminated in this process. annotate.test.ts gains annotate-app cases (live payload shape, composed bridge served by the proxy, no-history version endpoints, proxy stopped with the server, remote rejection); annotate-live-resolution.test.ts covers the probe matrix. The two post helpers now drop unmatched-targetOrigin posts silently, matching browser semantics where some DOM environments throw. Adds the manual Vite/Next smoke script and the AGENTS.md live app annotation section (phase gate, security posture, limitations). * test(annotate-cli): cover the CLI layer of the live app remote hard-off Spawns the real CLI entry (async, so the in-process fake app can answer the live probe) with PLANNOTATOR_REMOTE=1 against a loopback HTML server and asserts the startup-failure exit with the --static hint. Completes per-layer coverage of the three-layer hard-off (CLI exit, server throw, unconditional loopback proxy bind). * fix(live-annotate): harden the loopback trust boundary end to end - isLoopbackHostname (now canonical in live-proxy.ts, re-exported by the CLI resolution) requires localhost, ::1, or a LITERAL 127/8 IPv4 address: DNS names like 127.0.0.1.evil.example no longer classify as loopback, so neither the default probe nor --app can start a live proxy against an off-box origin. - The live-eligibility probe judges the FINAL response URL: a target that redirects off its loopback origin falls back to the static pipeline (or fails loudly under --app) instead of opening a live session whose iframe immediately leaves the proxy. - WS upgrades with a browser Origin not naming the proxy itself are refused, so a hostile page's cross-site connect is never laundered into the origin-less shape dev servers trust as a non-browser client (Vite CVE-2025-24010 class). - /__plannotator__/bridge.js refuses cross-site/same-site Sec-Fetch-Site fetches: the per-session token is no longer readable via an off-origin script include on modern browsers. - X-Frame-Options is stripped only on HTML responses (where frame-ancestors replaces it); non-HTML responses keep the app's own framing protection. - Redirect Locations are re-anchored by loopback-host + port equivalence instead of a string prefix: alternate loopback spellings are now caught and lookalike ports (5173 vs 51730) pass through untouched. - --app on a non-URL target fails loudly instead of being silently swallowed. * fix(live-annotate): session correctness for SPA restores, origins, and pathful targets - A live find-and-mark that resolves nothing keeps its record, seeded with unresolved placeholder targets from the durable anchor/text params, so the mutation-driven reconcile re-acquires the pin once a lazy route or data-dependent tree renders (SPA navigation no longer permanently drops pins). Srcdoc restores keep the fail-closed drop. - The bridge posts every outbound message once per listed editor origin; the browser delivers only the one matching the parent document, so an editor opened at 127.0.0.1 instead of localhost no longer silently loses ready and every subsequent message. - The advertised appUrl is the proxy under its localhost spelling with the target URL's own path and query: the framed app stays same-site with the editor, shares the dev app's host-only localhost cookies and storage, and a pathful target opens its page instead of the app root. The proxy still binds the 127.0.0.1 literal. * ci(live-annotate): run the live protocol DOM suite; document the hardened posture htmlLiveProtocol.test.tsx is DOM-gated and was absent from the workflow's DOM_TESTS file list, so none of its trust-boundary assertions ran in CI. Add it, and update the live-app section of the project docs: literal-loopback gate, probe redirect rule, WS Origin check, bridge.js delivery gate, localhost appUrl advertisement, live restore resilience, and the remote-mode behavior change (loopback URL annotate under PLANNOTATOR_REMOTE now exits asking for --static instead of silently converting). * fix(live-annotate): absorb the v0.27 mainline into the live session surface Post-rebase seam work after replaying the branch onto main (v0.27.4 era): - Route the bridge's unanchored-transparency report through postToParent so live sessions deliver it token-stamped to the listed editor origins; the raw '*' post main introduced for srcdoc would be dropped by the live parent's message authentication exactly where restores fail most. New live-harness test pins the contract. - Extend the live remote hard-off to --tailscale sessions (flag postdates the branch): CLI startup failure + startAnnotateServer throw keyed on tailnetPublished, matching how the annotate agent terminal treats tailnet publication. Covered in annotate.test.ts and documented in AGENTS.md. - Keep main's compact-touch input controls and effective mode/input values on the HTML surface while preserving the live pinpoint-only clamps. - Regenerate the pinned guide-viewer manifest (CSS hash moved with the new UI classes; JS unchanged). * feat(live-annotate): Interact/Annotate mode toggle for live app and raw HTML sessions A live app session used to be unusable: the pinpoint capture-phase click handler owned every click, so buttons, checkboxes, inputs, and links never fired. One boolean mode now governs the HTML/live viewer surface: - Interact: the bridge is fully passive. Pinpoint capture, hover outline, drag-selection toolbar, [data-annotate] clicks, and committed-highlight click interception are all gated behind annotateModeActive, so clicks, forms, text selection, and SPA navigation reach the page natively. Committed markers and highlights stay VISIBLE, and marker buttons keep their clicks (a marker click still opens its comment). - Annotate: classic behavior, unchanged. Live sessions annotate exclusively via pinpoint while armed. Control: a single bubble icon button in the editor header (icon never changes; armed = accent + visible border, idle = transparent border of the same width, so the box is pixel-identical in both states), plus a subtle inset accent ring floated over the viewer while armed (pointer-transparent, no layout shift). Keyboard: Mod+Shift+A through the shortcut registry (html-annotate scope; the bridge mirrors the chord inside the iframe and forwards it over the authenticated postToParent path). Esc gains a final ladder rung: draft closes first, then the hover outline clears, then Esc exits Annotate back to Interact (bridge posts annotate-exit; a parent-side listener covers Esc with editor focus). The parent owns the mode and pushes it with the same re-post-on-ready pattern as set-input-method, so it survives live page changes, HMR reloads, and bridge re-injection without ever reloading the iframe. Defaults: live app sessions START in Interact; static/raw HTML sessions START in Annotate (today's behavior preserved, and the srcdoc bridge default keeps behavior byte-identical when no set-annotate-mode ever arrives). Session-only state, no persistence. Vim navigation is available only while Annotate is armed. Covered by new bridge-harness and parent-side DOM tests in htmlLiveProtocol.test.tsx and htmlPinpointProtocol.test.tsx: Interact pass-through, armed capture, the Esc ladder order, mode survival across re-injection, marker clicks in Interact, and both defaults. * feat(live-annotate): pinpoint-armed default, always-on drag comments, comment-only HTML surfaces Simplifies the Interact/Annotate design after live review. The new contract replaces the previous one where they conflict: - BOTH surfaces (raw HTML and live app) now START ARMED with pinpoint; the live-session Interact default is gone. Esc keeps the ladder (close draft, clear hover, then exit to Interact) and the header toggle re-arms. The bridge also paints the pinpoint cursor at init instead of waiting for the parent's first round trip. - The header toggle is a PEN icon: the old bubble sat next to the annotations-panel bubble and the two were indistinguishable. Same box geometry (armed = accent + visible border, idle = transparent border of identical width), aria-pressed, Mod+Shift+A, and the armed ring over the viewer are all unchanged. - Text drag-selection commenting is ALWAYS live on HTML/live surfaces, in BOTH states: the selection pass is ungated from annotateModeActive and from the pinpoint input method. In armed pinpoint, click = pin an element and drag = select text, simultaneously; the >4px drag arming decides which one a gesture was, a completed drag's trailing click never re-pins (one-shot dragEndedClick), and a plain click is never swallowed (the pass only acts on a real selection and never preventDefaults). Esc in Interact still closes an open drag draft before yielding to the page. - HTML/live surfaces are COMMENT-ONLY: useHtmlAnnotation clamps redline/quickLabel (both the host mode and a bridge-posted modeOverride, so a hostile page cannot force a DELETION), the selection toolbar drops Delete and quick labels behind a new commentOnly seam on AnnotationToolbar, and the quick-label picker portal is gone from HtmlViewer. Markdown surfaces keep the full toolbar, and persisted DELETION annotations still restore. - The "Show tools"/"Hide tools" header button is removed. It hid the floating toolstrip (now gone from HTML surfaces entirely: with comment-only plus both input paths live there is nothing left to switch), the collapsed sidebar tab flags, and the viewer's floating action cluster (attachments + global comment + version-diff toggle), all of which are now always visible. htmlChrome persistence keeps only the sidebar/panel state; an old cookie's toolsHidden flag is read tolerantly and ignored, so a stale record cannot strand a user with hidden chrome and no way back. - HTML surfaces pin the viewer input method to pinpoint (the drag/ pinpoint switch is meaningless when both are live); the Alt input switch no-ops there. Vim stays armed-only, as built. No server, proxy, or protocol-security changes; the armed flag stays session-only. Tests: the live-bridge harness is reworked around the armed default (forged-DISARM posture, drag-selection passes in armed and Interact, the trailing-click guard), the pinpoint suite covers the comment-only toolbar and the redline/quickLabel clamp at the trust boundary, a new AnnotationToolbar.commentOnly seam test guards both surfaces' toolbars, App.htmlChrome.test.tsx replaces App.htmlHideTools.test.tsx (no tools button, stale-cookie tolerance, pen armed default), and the htmlChrome tests cover the narrowed persisted shape. * feat(live-annotate): collapsible floating controls cluster The simplification removed the Hide tools toggle, which left the floating comment/attachments cluster permanently over the page. Restore a hide affordance on the cluster itself: a collapse chevron shrinks it to a small expand pill in the same corner, so the page is never obstructed without a way back. Collapsed state persists with the rest of the HTML chrome cookie (sidebar/panel), tolerantly read. Hosts that do not wire the toggle (readOnly viewers, review-editor panels) are unchanged. * feat(live-annotate): header Show/Hide tools replaces the collapse pill The collapse pill was a half measure: it left its own artifact over the page and the sidebar tongue tabs stayed. Revert it and restore the real thing as a header control: an eye toggle immediately left of the pen that removes ALL floating chrome over the page from the DOM (sidebar tongue tabs + the comment/attachments cluster), leaving nothing behind. The toggle lives in the header, so a hidden state always has a way back, which also makes honoring a persisted (or pre-existing) toolsHidden cookie safe again. |
||
|
|
15f8d4fe4c |
feat(review): collapse generated files by default in the all-files view (#1346)
* feat(review): collapse linguist-generated files by default (#1317) Code review now respects linguist-generated (and linguist-generated=true) from .gitattributes, collapsing those diffs by default the way GitHub does. Server (Bun + Pi mirror): a generatedFiles sidecar rides /api/diff and /api/diff/switch, resolved through git's own attribute machinery — one batched 'git check-attr --stdin -z' over the served patch's paths at the review cwd, so stacked and negated rules land exactly as git resolves them. Plain local git sessions only; PR worktrees, workspace multi-repo, jj, GitButler, and P4 omit the sidecar (degrade to no-collapse). Shared logic in packages/shared/generated-files.ts, vendored to Pi. Client: generated files SEED their CodeView item collapsed (the existing Pierre collapse state — same mechanism as commit-diff folding), render the one-line FileHeader bar with a 'generated' tag next to the +/- counts, and expand per file on click. Expansion is session-local App state so it survives remounts and diff switches. Presentation-only: the diff data, annotations, search, and Edit Mode are untouched; the file tree and single-file tabs list generated files normally (tag, no auto-collapse). Guide viewer manifest pin regenerated (AllFilesCodeView/FileHeader are bundled into the guides.show viewer) from a clean frozen-lockfile install. * feat(review): built-in generated defaults, visible collapsed strip, review-round fixes (#1317) Round 2 on PR #1346, per maintainer review. Built-in generated defaults (industry-standard two-layer detection): packages/shared/generated-files.ts (vendored to Pi) now carries DEFAULT_GENERATED_PATTERNS — lockfiles (package-lock.json, yarn.lock, bun.lock, Cargo.lock, go.sum, ...) plus *.min.js / *.min.css / *.map — matched against the path's last segment only. Explicit .gitattributes wins in BOTH directions: linguist-generated (set/true) marks any file, -linguist-generated / =false un-marks even a built-in name, unspecified falls through to the defaults. In plain local git sessions check-attr refines the defaults; the non-git degrade modes (piped patches, PR worktrees, workspace, jj, GitButler, P4) now emit the sidecar from the name-based defaults alone instead of omitting it. Visible collapsed state: a collapsed generated card no longer renders as a bare header — a GeneratedFileNotice strip ('Generated file collapsed', +N/-N, 'Click to view') styled like the other below-header notices sits in the card, and clicking it expands through the SAME reportFileCollapsed funnel as the chevron. Review findings: - F1: search-match and sidebar-comment navigation expanded items without reporting through the funnel, so those expansions died on diff switch. Both now call syncAllCollapsedMirror + reportFileCollapsed; the funnel invariant comment lists the navigation-driven sites. - F2: the check-attr call gets the same 5000ms timeout as review-core's stdin git callers, and Pi's vcs.ts stdin write gets the one-line EPIPE guard (call-flow.ts shape) a timeout kill makes reachable. - F3: removed the dead prevGeneratedRef + collectSetDelta leg — a changed generated set always remounts via fileSetKey, so the delta path was unreachable. Tests: default-list matching (glob + directory-named-bun.lock), both- direction precedence, non-git sidecar from defaults (dual-runtime), the placeholder strip through the funnel, and search expansion surviving a re-seed round-trip. AGENTS.md payload docs updated. Guide viewer manifest pin regenerated from this clean frozen-lockfile worktree. |
||
|
|
2a22e5805a | chore: bump version to 0.27.4 | ||
|
|
e1ce7dabe1 |
feat(ui): Totman/Classic P favicon style switcher (#1325)
Favicon style switcher in Settings > Theme: the Totman mascot or the historical dark-navy P tile (byte-identical to the pre-Totman asset, sha256 pinned). Served server-side from first paint in both runtimes; opt-in for hosts of the published UI package. Contributed by @FNDEVVE |
||
|
|
0255db7b4b |
guide: voice rules in the organizer prompt (#1330)
* guide: voice rules in the organizer prompt (short sentences, plain words, no dashes, no verdicts) Measured on two agent-written guides: overviews averaged 120 to 135 words, two thirds of the sentences ran past 25 words (longest 78), and one guide used ' -- ' as a dash nine times. The prompt's Voice section now gives the same concrete rules the plannotator-guide skill gives, so guides made in-app and guides made by an agent read the same way. * guide: note that GUIDE_REVIEW_PROMPT is mirrored into the plannotator-guide skill |
||
|
|
64062af9a1 |
feat: Portable Guided Reviews — export, share links, agent-authored guides, guides.show (#1324)
A Guided Review can now leave Plannotator: as a single self-contained HTML file that renders exactly like the in-app guide, as an encrypted-by-default share link on guides.show, or authored by any agent through the new guide CLI. Highlights: packages/guide-viewer extracted from review-editor at the injection seam (read-only host, no third renderer); guides.show Worker with R2-backed share storage, per-IP rate limiting on creation, delete tokens hashed at rest, and 128-bit ids; portable exports pin the viewer by SRI hash with budget and manifest gates in PR CI and at deploy; two-runtime parity across Bun and Pi verified; v0.27.x saved guides load unchanged. Retention is indefinite by explicit decision, to revisit with the lean sharing refactor. Decision record: adr/decisions/007-portable-guided-reviews-20260815.md |
||
|
|
e3091331a5 |
feat(review): jj support for Call Flow analysis (#1312)
Adds Jujutsu (jj) as a Call Flow analysis provider: jj-current/jj-last/jj-line/jj-all snapshot revsets with deterministic first-parent resolution across merge revisions, root-anchored filesets so results are cwd-independent, bounded snapshot materialization (base tree + changed-file delta) with a streamed 64MB output ceiling in both the Bun and Pi runtimes, and real-jj regression tests covering merges and subdirectory invocation. Contributed by @graemefolk, who also built the original jj integration. Review fixes pushed in-branch: merge-parent resolution, root-glob filesets, bounded materialization and buffering, plus CI gating guards for runners without jj. |
||
|
|
aa0bf860d8 | chore: bump version to 0.27.3 | ||
|
|
192b026073 |
fix(annotate): stop the folder watcher freezing the server (#1314)
* fix(annotate): stop the folder watcher freezing the server (#1313) The file-browser content watcher built a chokidar scan over the whole workspace synchronously on the request path. Under Bun that scan monopolizes the event loop (a 780-directory nested tree measured 79 seconds), and because teardown was immediate on the last unsubscribe, every EventSource reconnect paid the scan again: the reconnect the freeze itself provoked made the hang self-sustaining. The watcher engine now lives once in packages/shared/file-browser-watch-core and both runtimes keep only their transport: - construction is deferred off the request path, so the SSE ready event and concurrent API requests are served before any scan starts - teardown gets a 30s reconnect grace; a reload reuses the warm watcher - on macOS and Windows the content watcher is the platform's native recursive fs.watch (measured ~0ms for the same tree); chokidar stays the Linux backend and the runtime fallback, with a forced catch-up refresh on the swap so no events are lost - server stop tears every watcher down immediately in both runtimes The responsiveness regression test reproduces the reported freeze on the pre-fix implementation (79s, fails) and passes in under a second on the fix. * docs: folder annotate sessions do write per-file version history The PLANNOTATOR_ANNOTATE_HISTORY row claimed URL, folder, and annotate-last sessions never write to the data dir. The folder /api/doc path deliberately runs the per-file version-history pipeline (lazily, memoized per resolved path, gated on the same flag) to power the per-file version diff, and has since it shipped. The code is the intended behavior; the sentence was stale. URL and annotate-last sessions remain fully stateless, and submit records remain single-file only. * fix(annotate): review follow-ups for the watcher engine Applied from the independent review of #1314: - contentWatchBackend gains a forced 'native' mode and the fallback tests use it, so the native-to-chokidar paths (creation failure and runtime error) genuinely execute on Linux CI; the runtime-error test is no longer macOS-only - a platform-agnostic responsiveness test pins that SSE ready is served before the scan starts on the chokidar backend, via the runtime test hooks; the tight full-scan bound stays macOS-only - watcher construction failures and the native-to-chokidar swap now log one console.error each instead of stranding subscribers silently; the swap also increments the diagnostics start counter honestly - closeEntry guards both watcher close() calls; the Bun annotate stop chain got the same try/finally shape as the plan server; all four stop chains close watchers ahead of throwable disposals so a failing dispose cannot strand a watcher keeping embedded hosts alive - a broadcast that empties the subscriber map by deleting dead subscribers now schedules the teardown grace instead of leaving the entry live until closeAll - bun.lock drift reverted: only the chokidar edge and the workspace version corrections remain (27 unrelated esbuild resolution entries dropped; frozen-lockfile install verified) - stale never-write comments in both annotate servers corrected to match the folder per-file history reality documented in AGENTS.md; the engine header now states plainly that chokidar is a correctness fallback, not a performance one |
||
|
|
8b9dfe7e5f | chore: bump version to 0.27.2 | ||
|
|
d2d2dba7fa |
feat(annotate): configurable extra markdown extensions (#1309)
* feat(annotate): configurable extra markdown extensions (#1307) Adds a config-only `markdownExtensions` key to ~/.plannotator/config.json, e.g. { "markdownExtensions": [".livemd"] } for Livebook notebooks. A listed extension is accepted everywhere .md is on the annotate path: CLI target resolution, folder discovery and the file browser, /api/doc plus relative and wiki-link navigation between sibling docs, the 2MB size cap, and per-file version history. Listed extensions render as markdown with frontmatter stripped, never as raw HTML, and they only widen the accepted set. Design: - packages/core/annotatable.ts stays browser-safe and zero-dep. Its regexes and predicates now take an optional, defaulted-empty list of extra extensions, plus a normalizer and regex builders. - packages/shared/markdown-extensions.ts is the node-side seam: it reads config.json once per process through the existing loadConfig() and threads the normalized list into those pure functions. resolve-file re-exports the config-aware predicates so both runtimes pick them up; the Bun server, the Pi mirror, the OpenCode plugin and the CLI all go through them. - The annotate /api/plan payload ships the resolved list so the renderer can linkify links to sibling documents (module-level UI registry, empty by default, so nothing changes without config). Validation: entries must be dot-led, lowercase-normalized, and free of path separators, globs and whitespace. Invalid entries are dropped silently, built-ins are deduplicated, and `.env` is denylisted so config can never register it (annotate copies file contents into the data dir). Deliberately unchanged: the Pi plan-write allowlist (ALLOWED_PLAN_EXTENSIONS in tool-scope.ts) and Edit Mode source save (SOURCE_SAVE_FILE_REGEX), which keep their own narrower allowlists. * fix(annotate): deny the dotenv family and sandbox config-aware tests Review follow-ups on #1309: - deny the whole dotenv family (.prod.env, .env.local, ...) in normalizeMarkdownExtensions, not just the exact .env name - resolve config.json path per call instead of at module scope so PLANNOTATOR_DATA_DIR sandboxing works in single-process test runs - stop resolve-file.test.ts reading the real user config: pure predicate imports plus pinned empty extras on every resolve call - add the config.json -> memo -> predicate integration test using resetMarkdownExtensionsCache under a temp data dir * test(call-flow): make the stale-read advert test self-sufficient The read-only GET only probes the node runtime while Call flow is enabled. The stale-read test relied on earlier tests' settings POSTs leaking callFlow=true through the process-frozen config path; with lazy config resolution each sandbox is genuinely isolated, so the test now enables Call flow in its own data dir. Locally the dependency was masked by an fnm-shimmed sem sidecar spawning node coincidentally. |
||
|
|
14e5c9ebd1 | Fix folder watcher cold-start refs scan (#1306) | ||
|
|
1d4e490b8e | fix(review): update Codex automatic approval flag (#1231) | ||
|
|
ef49c701c2 | chore: bump version to 0.27.1 | ||
|
|
1aaedf9330 |
fix(review): detach open-in-editor launches and bound the wait (#1289)
Open-in-app launchers are now spawned in their own process group and the request waits only a short grace (2s) for instant failures, with stderr drained concurrently from spawn time. A launcher that is still running at the deadline is treated as launched and the request resolves ok; instant failures keep the existing friendly error shape (not-found, exit code plus stderr). Mirrored in the Pi server (two-runtime law). Fixes two demonstrated defects: the request (and the UI button) was held hostage until the launcher CLI exited, and a launcher in the session's process group could be killed along with the session, taking a cold-started editor down with it. |
||
|
|
d0d971a3bf | chore: bump version to 0.27.0 | ||
|
|
ed6f44bf2e |
fix(release): tailscale gate exit codes and lease gating, conditional SIGHUP, informative guide validation error (#1286)
- annotate --tailscale publish failures now exit through annotateStartupFailureExitCode: exit 2 under a strict gate (--require-approval / --result-file), where exit 1 is reserved for "the reviewer did not approve, decision record published". Non-strict annotate and review keep the documented exit 1. - the annotate client lease (auto-dismiss on abandonment) is forced off while tailnetPublished is set: --tailscale reads as local to the CLI predicate, but clients connect through the serve proxy, and a proxy disconnect longer than the grace would dismiss a live review. Same rationale as remote/shared sessions; decided at the single point both the /api/plan advert and the SSE endpoint read. - the SIGHUP-to-process.exit route moved from an unconditional CLI-entry listener into enableTailscaleServe's success path, installed only once a serve mapping exists. Any SIGHUP listener overrides the ignored disposition nohup depends on, so plain sessions now keep zero listeners and "nohup plannotator review &" survives terminal close again; --tailscale sessions still tear their mapping down on HUP (exit 129). - validateGuideOutput explains a fully-invalidated guide whose refs named files outside the changeset (count plus up to 3 example paths, with a pointer to the Commits panel) instead of the bare generic message; the generic message stays for genuinely structural emptiness. The informative error now flows through onJobComplete to the job failure card; Pi picks the change up via the vendored guide-review copy. |
||
|
|
5f33b72b2f |
feat(remote): tailnet auto-advertise, ready QR code, and a first-class --tailscale mode (#1280)
* feat(remote): resolve urlHost auto from Tailscale for advertised URLs PLANNOTATOR_URL_HOST=auto (or config urlHost: "auto") detects this machine's tailnet host at first use in a remote session: MagicDNS name from tailscale status --json, falling back to the single tailscale ip -4 CGNAT address. Detection is cached per process, never spawns in local sessions, warns once and falls back to localhost on failure, and stays strictly display-only: binding remains governed by PLANNOTATOR_REMOTE. Pure parsers live in the new @plannotator/shared/tailscale module, vendored to the Pi extension; both runtimes mirror the resolution. * feat(remote): render a terminal QR code for remote-ready session URLs Remote sessions print their advertised URL as the lifeline; the usual next step is opening it on another device (iPad, phone, laptop off the VPS). handleServerReady now also renders a compact unicode QR of that URL via the zero-dependency uqr package, TTY-gated so piped stderr and hook transcripts keep only the plain URL line. Pi keeps URL-only parity: its ready surface is an in-chat notification, not a TTY stream, so a QR block would not render there. * feat(cli): first-class --tailscale mode for review and annotate sessions plannotator review --tailscale (also annotate and annotate-last/last) publishes the session over the user's tailnet: the server stays loopback-bound and the CLI orchestrates tailscale serve --bg --https=<port> http://127.0.0.1:<port>, then advertises the HTTPS tailnet URL with a terminal QR code. Nothing listens beyond localhost and nothing is ever public (serve, never funnel). Guarantees: preconditions fail with actionable errors (CLI missing, daemon down or logged out); a pre-existing serve mapping on the chosen port aborts instead of being stolen and other ports are never touched; every mapping the process creates is torn down on normal completion, SIGINT/SIGTERM, and errors via the exit-routed cleanup handler. When combined with PLANNOTATOR_REMOTE or SSH detection, --tailscale wins and forces local mode with a stderr notice, which also restores the random local port so simultaneous sessions get distinct serve mappings. * fix(remote): await tailscale-ready failures, harden serve teardown and conflict detection Review fixes for #1280 (external review plus internal security review). Startup failures no longer hang the session: startReviewServer and startAnnotateServer now await async ready handlers and stop the server on rejection, and the CLI's --tailscale ready path resolves publishing failures itself with an actionable stderr message and exit 1. Under the bang-prefix skill a hanging loopback server blocked the whole Claude Code prompt. Serve teardown is checked, not assumed: a failed off retries once, then warns with the exact manual command, and a port is only forgotten after a successful off. SIGHUP (terminal close) is now routed through process.exit like SIGINT/SIGTERM so exit-time cleanup runs. Docs no longer claim guaranteed cleanup: --bg mappings survive SIGKILL and reboots, and the manual removal command is documented. Conflict detection sees foreground serve sessions (Foreground.*.TCP), which Tailscale prefers over background mappings, and fails CLOSED on unrecognizable serve status output instead of assuming the port is free. The extracted serve URL must match the requested port, so a version-dependent output shape cannot advertise another mapping's URL. The annotate agent terminal is gated off by default under --tailscale behind the existing PLANNOTATOR_AGENT_TERMINAL_REMOTE opt-in: the PTY token is not an auth boundary against network peers, and tailnet reachability implies terminal reachability. Also: --tailscale is rejected with a clear error on unsupported subcommands and documented in review/annotate/annotate-last and top-level help; the remote-ready QR renders only for URLs actually reachable off-machine (never localhost); urlHost is suppressed for --tailscale runs so the local-session warning cannot mislead; the duplicated auto-host resolution moved into the shared vendored module; tailscale-serve tests restore module and process state via a reset seam. |
||
|
|
fc348687bf |
fix(review): contain /api/call-flow analysis throws as JSON error responses (#1272)
* fix(review): contain /api/call-flow analysis throws as JSON error responses
A hard VCS failure during patch materialization escaped the handler in
both runtimes. On Pi the unhandled rejection reached the process-level
handler and killed the user's session; on Bun it surfaced as a non-JSON
500 the client's quiet-failure UX could not parse. Both handlers now
return the standard { status: "error", reason: "analysis-failed" }
envelope.
* fix(review): cut the Call Flow consent copy down to the three facts that matter
Six sentences of disclosure read as noise. The dialog and Settings now
say: what it does, what it installs (languages + size), Node 22+, and
that other languages install as needed. Nothing consent-relevant was
removed.
* test(review): pin consent-copy facts, not prose
The presentation test now asserts the server-derived facts (languages,
size, Node floor); the dialog and Settings tests assert only that the
disclosure prop renders, via a sentinel string. Copy edits no longer
break three test files.
* docs: add Testing Rules to AGENTS.md (no prose-pinning, no round-trip prop tests)
* docs: refine copy-pinning rule — deliberate locks allowed, incidental snapshots banned
* fix(review): use the maintainer's Call flow description in the intro dialog and Settings
* fix(review): Call flow description is the maintainer's exact copy; remove the dynamic disclosure plumbing
The intro dialog and Settings now show only: 'Diffs for function call
stacks across git commits. 22 languages supported (AST-based, built
using Tree-sitter).' The callFlowEnableDescription prop, its App wiring,
and getCallFlowEnableDescription are removed; install size and Node
requirements remain visible in the Call Flow panel itself.
* fix(review): reject empty-path worktree diff types; clean up QA findings
- parseWorktreeDiffType returns null for a worktree diff type with no path.
An empty path resolved to an empty cwd, and Bun.spawn({ cwd: "" }) runs
git in the server's own directory instead of the target repo, so a
malformed 'worktree:' switch returned an unrelated checkout's diff.
Fail closed to the caller's real cwd. (Pre-existing; surfaced by QA.)
- Remove an orphaned JSDoc comment left by the callFlowEnableDescription
prop removal in Settings.tsx.
- Add useCallFlowAnalysis.test.tsx to the CI DOM_TESTS list; its two
tests were silently skipping on every run.
|
||
|
|
caf7ce1ccd | feat(review): install Call Flow automatically in the background on opt-in (#1271) | ||
|
|
9ee2e83287 |
feat(review): make the CallDiff runtime a strictly opt-in, in-UI install (#1270)
* feat(review): make the CallDiff runtime a strictly opt-in, in-UI install
The merged CallDiff integration eagerly installed a ~784MB runtime for
every user at install time, for a feature that is off by default. The
runtime is now strictly opt-in and the opt-in lives in the review UI:
toggle Call flow, click Install in the panel, watch staged progress, and
use the analysis in the same session.
Installers: the default sequence no longer installs the runtime. Opt in
with --with-call-flow (PowerShell: -WithCallFlow),
PLANNOTATOR_INSTALL_CALLDIFF=1, or { "installCallFlow": true } in
config.json (flag > env > config). PLANNOTATOR_SKIP_CALLDIFF_INSTALL is
deleted; --minimal keeps excluding the runtime; the installer prints an
honest note pointing at the in-app install. The headless CLI path
(plannotator install-runtime call-flow) is unchanged.
Server (both runtimes, contract-identical): POST /api/call-flow/install
starts installCallFlowRuntime() in the background via a single-flighted
coordinator (concurrent POSTs join the in-flight install), runs a
Node 22+ preflight before any download (distinct node-unavailable
error), and rejects cross-origin POSTs with 403. GET
/api/call-flow/install-status reports idle/running/done/error with
stage: downloading, verifying, installing-deps, building. Install
completion invalidates the 30s runtime probe cache so the next
capability advert resolves available without a server restart.
Client: the Call flow Dock's runtime-missing state is now the opt-in
funnel with an honest disclosure (about 800 MB on disk, Node 22+,
one-time), staged reduced-motion-safe progress, and error + retry with
a no-node hint. On done the advert is refreshed through
POST /api/review-analysis and the existing available-change refetch
starts the analysis for the current snapshot with no reload. The intro
dialog and Settings toggle note the separate first-use runtime.
Docs: AGENTS.md env table + Review Server API table, marketing
environment-variables / installation / ui-settings / code-review /
api-endpoints pages, and the CallDiff ADR runtime-boundary and server
contract sections.
* test(review): stop leaking PLANNOTATOR_DATA_DIR from the install endpoint tests
The call-flow install endpoint tests overrode PLANNOTATOR_DATA_DIR at
module-eval time and never restored it. bun runs CI's full suite in one
process and evaluates every test file's module before running tests,
while Pi's generated/storage.ts caches its data dir at import time; the
override therefore made storage's cached dir and later files' live
getPlannotatorDataDir() calls disagree, failing the Pi annotate-history
unwritable-dir test and both durable-submit-record tests.
An afterAll restore alone is not enough: it reproduces the same three
failures with the mismatch inverted (storage caches the leaked dir at
module eval, tests then run against the restored one). The env var is
now never touched at module-eval time at all; it changes only inside
tests and is restored to its original value in afterEach, exactly like
the PORT/PATH pattern. The config writes the advert tests persist
through the process's frozen config module are snapshotted at load and
restored in afterAll so a standalone run never flips a real
config.json setting, and the process-global scope of the mock.module
seams is documented.
Regression proof (previously failing in either mismatch direction, now
green in both orderings):
bun test packages/server/call-flow-install-endpoint.test.ts \
apps/pi-extension/server/annotate-history.test.ts \
apps/pi-extension/server/annotate-submission.test.ts
* feat(review): install CallDiff grammars selectively
* fix(review): harden CallDiff worker environment
* fix(review): close CallDiff verification gaps
|
||
|
|
3245310aa8 |
feat(review): add optional CallDiff call-flow analysis (#1268)
* feat(review): add optional CallDiff call-flow analysis * fix(review): harden CallDiff integration |
||
|
|
98113182b5 |
feat(guide): reviewer-supplied extra instructions for Guided Review (#1267)
* feat(guide): reviewer-supplied extra instructions for Guided Review (#1265) Adds a quiet, collapsed-by-default Custom instructions affordance to the guide launch page. The text is APPENDED to the built-in organizer methodology as a clearly delimited section (composeGuideMethodology) and never replaces it; absent or blank instructions produce byte-identical prompts to before. Persisted in a dedicated cookie (plannotator-guide-instructions) so a standing team preference survives sessions without bloating the plannotator.agents blob past the browser's per-cookie limit. Server side, the launch body gains an optional guide-only instructions field (both the Bun and Pi node:http agent-jobs handlers accept and thread it); prompt composition lives in the shared guide-review.ts that vendor.sh already vendors to Pi, so both runtimes compose identically. Text is capped at GUIDE_EXTRA_INSTRUCTIONS_MAX_CHARS (2000) server-side and mirrored by the textarea maxLength. Repair launches deliberately ignore instructions: a repair is a mechanical JSON fix, not a rewrite. Tests pin the regression contract (empty input keeps prior prompt bytes), appended-not-replacing composition, the length cap, repair isolation, and the cookie round-trip via the storage backend seam. * refactor(guide): store standing instructions server-side, not in a cookie Review findings on the cookie approach (silent write failure past the encoded 4KB per-cookie limit for multi-byte text) pointed at the real design problem: the instructions are consumed by the SERVER at launch time, so they belong in the data dir like review-skills.json, where no size ceiling or encoding inflation exists and the preference follows the machine instead of one browser profile. New GET/PUT /api/agents/guide-instructions in both runtimes backed by shared guide-instructions-store (vendored to Pi). Guide launches apply the stored text when the body carries none; the launch page still sends its live textarea value (explicit wins), so a just-typed preference can never race the debounced save. The sidebar surface sends nothing and inherits the stored text server-side. All cookie machinery removed. Also folds in the review fixes: marker-tag-shaped strings in instructions are defanged so first-match nonce recovery cannot be hijacked by pasted examples. |
||
|
|
2fff8756d9 | chore: bump version to 0.26.8 | ||
|
|
62c1eab119 | chore: bump version to 0.26.7 | ||
|
|
d579ff8db2 | chore: bump version to 0.26.6 | ||
|
|
9c40ffadcc | chore: bump version to 0.26.5 | ||
|
|
e24bd8464f |
fix(annotate): persist submitted feedback before deleting the draft (#678) (#1237)
* fix(annotate): persist submitted feedback before deleting the draft (#678) * fix(annotate): scope durable submit records to single local files Adversarial verification found the durable record had no mode gate: an annotate-last or URL session, which was completely stateless before, would persist submitted feedback quoting the agent's message or the fetched page under history/, widening the documented annotateHistory contract without a docs change. The record now shares the exact eligibility gate the version history uses (mode annotate, non-URL path), so previously-stateless modes stay stateless. Also makes persistSubmittedDecision defensive about body types: /api/feedback does no validation (unlike /api/approve), and a non-string feedback previously flowed through settle() untouched with a 200; the new .trim() guard turned that into a thrown 500 after the decision had already settled. Malformed values now degrade to the exact legacy behavior (settle, delete draft, 200) instead of throwing. Both changes mirrored in the Pi server, with regression tests in both runtimes: stateless modes write no record, and a malformed feedback body returns 200 with the draft deleted and nothing persisted. |
||
|
|
d5ae439f7a | chore: bump version to 0.26.4 | ||
|
|
c760fc522b | chore: bump version to 0.26.3 | ||
|
|
ffd49080ee | fix(skills): harden skill references before first release (#1235) | ||
|
|
7ad4d39ed9 |
feat(comments): reference agent skills with / or $ in plan review and annotate comments (#1229)
* feat(comments): reference agent skills with / or $ in plan and annotate comments
Typing / or $ at the start of a word in the document-UI comment composer
opens a picker of the user's global agent skills (~/.claude/skills,
~/.codex/skills, ~/.agents skills roots), served by a new GET /api/skills
on the plan and annotate servers in both runtimes (Bun + Pi mirror).
Multiple references per comment are supported; references live in the
comment text itself and are appended to exported feedback as a
'Skills referenced' block so the acting agent knows which skills to apply.
Human-invocation-only skills (disable-model-invocation: true frontmatter)
stay listed and selectable but render dimmed with a badge, warn in the
menu and composer, and are marked in the export so the agent is never
asked to invoke something it cannot.
Discovery reuses the review-skill loader (same roots, precedence, and
skip-and-log discipline), reads only an 8KB head per SKILL.md, caps the
catalog at 500 skills, takes no client input, and is never persisted;
any failure degrades to plain typing.
* fix(comments): harden skill references per review (trigger, IME, seam, fail-closed frontmatter)
Blockers:
- B1: a trigger now requires at least one query character. A bare / or $
no longer opens the catalog, so Enter stays a newline and Tab still
leaves the field ("This costs $" + Enter, "cd /" + Tab, bullets).
- B2: the menu ignores keys mid-IME-composition (nativeEvent.isComposing),
matching the 16 existing guards; Enter committing a Pinyin/Telex/Korean
candidate can no longer insert a skill.
- B3: the catalog request is a host seam (skillCatalogTransport via
configurePlannotatorUI), defaulting to the existing GET /api/skills.
- B4: resetSkillCatalogCache() invalidates outstanding requests
(generation counter), and a late-resolving stale request can no longer
overwrite a newer cached value or the export registry. The catalog
tests reset in beforeEach, so they hold in any file order.
Also:
- F1: skillReferences={false} is fully inert — the human-only notice memo
and the cache seed are gated on the prop.
- F4: frontmatter flag parsing no longer fails open: trailing YAML
comments are stripped, on/1 (and TRUE/yes etc.) read as true, the head
read is 64KB, and truncated unterminated frontmatter fails CLOSED on
disable-model-invocation.
- F5: extraction ignores markdown link destinations ([x](/name)), shell
redirects (cat /x > out), and /-triggered FHS root names (/run, /tmp);
menu insertion switches / to $ for those names so inserted references
always survive extraction.
- F6: the 500-skill cap slices after sorting, so which skills survive no
longer depends on readdir order.
- F3: /api/skills wiring guards for the Bun and Pi plan + annotate
servers (skills-endpoint.test.ts).
- Keyboard state machine tests against the real CommentPopover in
happy-dom (bare trigger, insertion, composition, Escape, highlight
bounding, opt-out inertness), added to the CI DOM step.
- The insertion path dismisses the trigger start so the menu close is
ordering-safe against React's select-plugin re-reading a stale caret.
* feat(comments): redesign the skill reference menu (bare triggers, no preselection, highlighted tokens)
Per maintainer direction, reversing the earlier bare-trigger opt-out
deliberately: typing a bare / or $ at the start of a word now opens the
full skill catalog immediately, and the safety story moves from the
trigger to the menu itself.
No preselection (the load-bearing rule): the menu opens with NO row
active, and while nothing is active every key behaves exactly as if the
menu were closed. "This costs $" + Enter is a newline; "cd /" + Tab
leaves the field (the proven regression that must never return). A row
activates only via ArrowDown/ArrowUp (Down from none lands on the first
row, Up on the last); only then do Enter/Tab insert. Pointer hover never
activates a row, because the menu floats exactly where the mouse rests
over the composer; a click inserts directly and never arms Enter.
Continuing to type re-filters and disarms any active row. Escape clears
the active row and dismisses when the user engaged (query typed or row
active); an unengaged bare-trigger menu passes Escape through so closing
the composer still costs one press.
Menu redesign to the reference look: icon, bold name, dimmed inline
description with ellipsis, right-aligned source column (Agents / Claude
/ Codex from the discovery roots), rounded generously padded rows, and a
subtle active-row background; human-only rows stay dimmed with their
badge and the warning now shows while such a row is ACTIVE.
Inserted references render highlighted in the composer via a mirrored
aria-hidden overlay behind a transparent-text textarea (identical font,
padding and wrapping metrics; scroll synced; tokens change color and
background only, drawn from the --primary theme token so every palette
works in light and dark). The caret keeps --foreground, selection uses a
translucent primary wash, and IME composition temporarily restores
native textarea text so composition underlines render normally.
skillReferences={false} still renders the plain pre-feature textarea.
Also, per review:
- extraction: dropped the over-broad shell-redirect exclusion (false
negatives on prose like "use /animate <- this one"; the motivating
case stays covered by the reserved-path rule)
- frontmatter: an unterminated frontmatter block now fails CLOSED on
disable-model-invocation even in complete (untruncated) files
- the reserved-path / to $ insertion switch stays: extraction still
reads /run as a path, and the new token highlight makes the switch
self-explanatory (an unhighlighted insert would look broken)
The composition guard, transport seam, catalog generation counter,
enabled gating, and export rules are unchanged and re-covered by the
rewritten DOM test matrix.
* fix(comments): give the skill reference menu adaptive, viewport-clamped placement
The menu rendered bottom-full with a fixed max-h-64: always upward, up to
256px, with no viewport awareness. With the comment popover near the top of
the viewport (annotating near the top of a document), typing a trigger ran
the menu off the top of the screen with its upper rows unreachable.
Placement now mirrors the popover's own computePosition idiom: measure the
space above and below the composer wrapper against window.innerHeight,
prefer above (the shipped direction; keeps the action row and human-only
notice visible), flip below when the list fits below but not above, and when
neither side fits pick the roomier side. The list's max height is clamped to
the available space (still capped at the former 256px), so the menu never
extends past a viewport edge. Recomputes on every commit (drag moves,
popover flips, filtering changing the item count, warning-footer toggles)
plus capture-phase scroll and resize listeners, matching the popover's
tracking. Visual design of the menu and rows is unchanged.
* feat(comments): inject human-only skill instructions into exported feedback
A human-only skill (disable-model-invocation: true) referenced in a review
comment used to export as a dead name the agent could do nothing with. A
human referencing a human-only skill IS the human invocation, so the export
now injects the skill's SKILL.md body verbatim (frontmatter stripped) inside
clearly delimited BEGIN/END SKILL INSTRUCTIONS markers, with the absolute
skill directory and the resolve-relative-paths pointer so references/,
scripts/, and assets/ stay actionable. Model-invocable skills keep exporting
as names the agent can invoke itself.
Transport is lazy: a new GET /api/skills/content?name= endpoint (Bun and Pi)
serves one discovered skill's body, capped at 20k chars with an explicit
truncation notice pointing at the file; the client fetches contents only for
the human-only skills actually referenced, keyed off comment state, and the
catalog now carries each skill's absolute dir so every failure path (deleted
skill, unreadable file, race with submit) degrades to naming the skill plus
its directory. Names are matched against discovery only and never used as
paths, so traversal cannot escape the skill roots. A per-export dedupe
injects each skill once even when several comments reference it, and
GLOBAL_COMMENT annotations run through the same block.
The referenced-skills header now says the reviewer is asking for the
invocation, and the human-only menu footer and composer notice explain that
the skill's instructions will be included with the feedback instead of
warning that the reference will not work.
* polish(comments): quiet, progressive human-only skill treatment
The human-only surfaces shipped with too much emphasis: a dimmed row plus
a bordered uppercase badge, an amber warning footer, and a persistent
amber notice in the composer after insertion. Human-only is a property of
a skill, not an error state, so the treatment is now quiet and
progressively disclosed:
- Menu rows render at full strength with a small muted 'human-only' pill
(bg-muted / muted-foreground tokens; no border, no dimming).
- The plain-language explanation (a model cannot invoke it, so its
instructions will be included with your feedback) appears as a muted
footer only while a human-only row is active (keyboard) or hovered
(pointer). Hover disclosure is purely visual state local to the menu;
it never touches activeIndex, so the no-preselection invariant and the
hover-never-arms-Enter rule are unchanged and re-asserted by a new test.
- When not disclosed, the same sentence stays in the DOM sr-only and
human-only rows point at it with aria-describedby, so the state reaches
assistive tech as text rather than as a purely visual badge (this does
not attempt the #1233 combobox semantics, and does not worsen them).
- After insertion, the highlighted token itself carries the quiet inline
marker (a dotted primary underline; text-decoration cannot move glyphs,
so overlay alignment is untouched) and the standing amber notice is
replaced by a native <details> disclosure: a single muted 'Includes
skill instructions' summary line that expands to the full accurate
sentence, operable by pointer, keyboard, and AT alike.
No amber remains; every color is a theme token (muted, muted-foreground,
border, primary, ring), so the treatment follows every palette in light
and dark. Copy is unchanged where it was accurate. Behavior is unchanged:
human-only skills stay selectable and injection still happens.
* fix(comments): harden human-only skill injection per adversarial review
Three findings on the injection path, each with tests that fail pre-fix:
1. Marker forgery: an injected SKILL.md body containing our own
`--- BEGIN/END SKILL INSTRUCTIONS ---` markers (or an
`[Instructions truncated:` notice) could close the block early — making
everything after it read as the reviewer's own words — forge a block for
a skill nobody referenced, or forge a truncation notice pointing at an
attacker-chosen path. Body lines matching the structural marker forms
(leading-whitespace and case variants included) are now visibly
neutralized before injection: kept verbatim but prefixed, never silently
deleted (neutralizeSkillMarkerLines).
2. Forged human invocation: POST /api/external-annotations is
unauthenticated on localhost, so any local process could submit a
comment referencing a human-only skill and cause its instructions to be
injected "at the reviewer's request". Annotations carrying a `source`
now still LIST their skill references but never cause verbatim
injection — human-only references fall back to naming the skill plus
its directory, with an honest reason. The content-prime effect skips
external texts for the same reason. A human referencing a human-only
skill IS the human invocation; a tool is not.
3. Unbounded read: readReferenceSkillContent read the whole SKILL.md
before slicing to the 20k cap, so an unauthenticated no-cors fetch loop
could balloon RSS by file size per request (measured +64.4MB for a 64MB
file). It now uses the same bounded readFileHead as the catalog,
reading only frontmatter allowance + 4 bytes per capped char + slack;
truncation detection is unchanged for any file whose frontmatter fits
the catalog bound, and frontmatter that overflows the read falls back
to null rather than serving raw YAML. Measured: 12 reads of a 64MB
SKILL.md now cost +5.1MB total.
Also: the fast-fail guard no longer rejects legitimately discovered names —
`name.includes("..")` 404'd a real `v1..2` skill dir forever (and `\` is
legal in POSIX names) while defending nothing, since the name is only ever
matched against discovery output and never joined into a path. It now
rejects exactly the names that can never be a readdir entry: empty, `.`,
`..`.
|
||
|
|
b69742c3bf |
feat: add PLANNOTATOR_URL_HOST display-only override for advertised URLs (#1225)
* feat: add PLANNOTATOR_URL_HOST display-only override for advertised URLs Remote mode binds 0.0.0.0 but every advertised URL hardcoded http://localhost:<port>, so a session opened from another device (e.g. a phone on the same tailnet) got an unopenable link (#657). - resolveUrlHost() in packages/shared/config.ts: PLANNOTATOR_URL_HOST env var over config.json urlHost, validated host-only (bare hostname, IPv4, bracketed IPv6); invalid values warn once and fall back to localhost. - buildAdvertisedUrl(port) in packages/server/remote.ts and its Pi mirror in apps/pi-extension/server/network.ts; all 7 construction sites use it. - Strictly display-only: binding stays governed by PLANNOTATOR_REMOTE, and agent-review jobs get a pinned http://127.0.0.1:<port> API URL. - Remote-ready copy says "open on your device" when the host is overridden; local sessions with an override warn it is unreachable. - Tests for validation, precedence, and URL composition in both runtimes; docs in CLAUDE.md and the marketing site. * fix(review): ignore urlHost in local sessions, harden warning output Review follow-ups on #1225: - Local (loopback-bound) sessions no longer honor the advertised-host override: honoring it auto-opened http://<host>:<port> against a server nothing was listening on, openBrowser still reported success, and the agent blocked on waitForDecision. Local sessions now advertise and open localhost, warning once that PLANNOTATOR_REMOTE=1 is required. - The invalid-host warning JSON-encodes the echoed value so an embedded newline cannot forge extra stderr lines (hosts surface session-ready lines as clickable links); warn-once is now per value. - Docs: local-session behavior reworded, the empty-env-suppresses-config semantic documented, secure-context note generalized. |
||
|
|
bbae458e5a | chore: bump version to 0.26.2 |