293 Commits

Author SHA1 Message Date
Michael Ramos 421c6af4cd chore: bump version to 0.27.14 2026-09-11 10:14:43 -07:00
Michael Ramos d74c289d89 chore(deps): bun minor+patch group (holds: @types/vscode, pi-coding-agent, katex) (#1501) 2026-09-10 18:09:26 -07:00
FND cad745d61a fix(codex): fall back across rollout files for annotate-last (#1493)
* fix(codex): resolve annotate-last across multi-rollout threads (#1367)

* fix(codex): stop hook takes only the newest existing rollout segment

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-09-10 16:33:28 -07:00
Michael Ramos 6d46395454 chore: bump version to 0.27.13 2026-09-09 21:35:54 -07:00
Michael Ramos 9130d2d6a3 feat(review): open a review on a specific base and diff type (#1484)
Adds two session-only flags to plannotator review, parsed in the shared
parser so every host inherits them together:

- --base <ref> opens the session against a caller-chosen compare target
  (branch, origin/<branch>, tag, SHA, HEAD~N), probed with git rev-parse
  --verify --end-of-options before the server starts so a typo'd ref is a
  startup error with near-match suggestions instead of a silently
  mislabelled merge-base->HEAD diff.
- --diff-type <id> opens the session in one of the nine flat git diff
  modes (REVIEW_OPEN_DIFF_TYPES, pinned against GIT_DIFF_TYPES).

The flags are a seed, never a setting: nothing writes config.json or any
review cookie, and the UI stays fully mutable. Validation is pure in
packages/shared/review-open-state.ts (provider matrix errors on
jj/GitButler/P4/workspace/PR mode, promote-with-notice when the saved
default is base-irrelevant, fatal explicit contradiction).

A flagged base rides explicitBase semantics: the new initialBaseExplicit
server option (both runtimes) seeds baseExplicitlyChosen, suppressing the
startup origin/* upgrade and canonicalization, and openStatePinned rides
/api/diff so the client neither offers the first-run setup dialog (its
one-time cookie is NOT consumed) nor runs the panel-pair self-heal for a
pinned session. The since-base dropdown label now renders from the live
active base, matching the adjacent base picker.

Coverage: Bun CLI, opencode-review bridge, OpenCode embedded plugin, and
the Pi extension (re-vendored; strict validation on the slash-command
path only, programmatic callers unchanged). Skills, command stubs, help
text, and docs updated across every host surface.
2026-09-09 21:35:05 -07:00
Michael Ramos ea36ea2183 fix(review): strict argument parsing for unknown review flags (#1483)
parseReviewArgs now reports argument-shape problems through an always-present
errors[] field instead of letting unknown dash-prefixed tokens fall into the
ignored positional list. All four host surfaces refuse to start a session on
a parse error: the CLI and the opencode-review bridge exit 1 with the errors
on stderr, and the Pi and OpenCode plugins notify through their hosts. Plain
non-dashed words stay tolerated for slash-command hosts that forward raw user
prose. The loop is index-based so value-taking flags can consume their value
token.
2026-09-09 20:17:24 -07:00
FND 6414b785d9 fix(amp): relay structured decisions without prose classification (#1476)
Closes #1456. Adds plannotator review --json emitting one { decision, message } record from the same builder as the plaintext output, routes Amp solely on the decision field, and removes the substring classifier that could silently drop feedback mentioning phrases like 'no feedback'. Invalid or legacy CLI output produces a recoverable update notice, never a guessed decision.

Claude-Session: https://claude.ai/code/session_019GV6EKtzh8Pf9GA2rrBLNf
2026-09-07 10:26:05 -07:00
Michael Ramos d749c55c02 chore: scrub personal paths and untrack local-only artifacts
- Anonymize the real project paths and ticket prefix in session-log
  test fixtures (slug expectations updated to match).
- Untrack scripts/convert-themes.ts: a one-shot migration script
  hardcoding a path into an unrelated private project; its output in
  packages/ui/themes/ is already committed.
- Delete the three unreferenced sprite_package_*/index.html preview
  pages, which also shipped in the @plannotator/ui npm tarball via the
  wholesale directory entries in files.
2026-09-03 13:48:05 -07:00
Michael Ramos 83493a1a4c chore: bump version to 0.27.12 2026-09-03 13:28:28 -07:00
Michael Ramos 18a6451dcc Merge remote-tracking branch 'origin/main' into feat/decision-control-pr7
# Conflicts:
#	packages/core/guide-viewer-manifest.ts
2026-09-02 13:22:10 -07:00
Michael Ramos 2f9b831617 fix(review): stage-review fixes — with-notes framing, bridge handshake, PR-payload advert, note fold
Applies the PR5 stage-review rulings:

M0: approve-time feedback is no longer appended raw after the approved
prompt ("no changes requested" beside a change-request-shaped export read as
a contradiction). composeReviewApprovedMessage now resolves the new
DEFAULT_REVIEW_APPROVED_WITH_NOTES_PROMPT (configurable as
prompts.review.approvedWithNotes; field added to the config review section),
which frames the notes as non-blocking guidance and says not to revise or
reopen. Signature is now (runtime, feedback, config) so one function fixes
all four consumers; re-vendored to Pi; prompts.test.ts pins the framing,
the config override, the byte-identical bare approval, and the legacy
placeholder filter; cli-bridge.test.ts asserts the bridge routes through
the composer.

M1: fail-closed approval-notes handshake for the OpenCode CLI bridge. The
plugin declares supportsApprovalNotes: true on the opencode-review stdin
JSON; the binary adverts approvalNotesSupported for opencode ONLY when the
declaration is present, so a new binary + old plugin (advert in the binary,
delivery in the independently-versioned plugin) renders no approve-carrying
items instead of silently dropping the reviewer's note.
supportsReviewApprovalNotes stays the seam; documented at both ends beside
the existing version-skew reasoning. Pinned end to end by
apps/hook/server/opencode-review-advert.test.ts, which spawns the real
entrypoint and reads /api/diff: stdin without the declaration serves false,
with it true.

m1: applyPRResponse re-applies the advert from the PR-family payloads
(pr-diff-scope, pr-switch, load-full-diff), so the client stays in lockstep
with whatever diff payload it last applied — the "whole diff family" comment
is now literally true.

m2: buildReviewApprovalBody folds a note in ahead of the export when
annotations also ride, so a future combined item cannot lose data; pinned
with a pure assertion.

i1: AGENTS.md corrected — the standalone dev server emits raw decision JSON
with unfiltered feedback and does not route through the composer; the
consumer list, framing, and handshake are now described accurately.

Claude-Session: https://claude.ai/code/session_01Drrzd1x4EfnH9N3z7nNwo9
2026-09-02 11:50:45 -07:00
Michael Ramos 1d7c4b906d feat(consumers): deliver approve-time review feedback in all four discarding consumers (PR5)
The four waitForDecision consumers that threw result.feedback away on the
approved branch (spec §6.3) now emit composeReviewApprovedMessage — the
approved prompt, then the note when one rides the decision:

1. Claude Code CLI `plannotator review` (apps/hook/server/index.ts); the
   amp/droid plugins relay its stdout and inherit the delivery.
2. OpenCode native (apps/opencode-plugin/commands.ts) — also fixes the
   delivery gate: it rode on the LGTM placeholder making feedback truthy, so
   with the placeholder gone a bare approval would have been silently dropped;
   the gate is now `feedback || approved`.
3. OpenCode CLI bridge (buildReviewPromptFromBridgeOutcome) — the CLI's JSON
   record always carried the feedback; the bridge stops discarding it.
4. Pi (apps/pi-extension/index.ts) via the vendored prompts module.

Each consumer's startReviewServer call now passes the matching advert:
supportsReviewApprovalNotes(origin) for the hook CLI (new seam in
apps/hook/server/review-output.ts — every origin shares the one stdout
relay today), Boolean(sessionId) for OpenCode native (no session, no
delivery — the annotate precedent), unconditional true for Pi and the
standalone dev server (which already emitted feedback on approve).

Claude-Session: https://claude.ai/code/session_01Drrzd1x4EfnH9N3z7nNwo9
2026-09-02 11:19:24 -07:00
zhangjinzan1 85c4fa6cc2 docs(skills): require the annotate gate for file approval (#1453) 2026-09-02 08:11:53 -07:00
Michael Ramos e5d49f8689 chore: bump version to 0.27.11 2026-09-01 13:48:46 -07:00
Michael Ramos 990f3e8905 feat(server): durable feedback archive for every submitted review (#1438)
* feat(server): archive every submitted review to a durable local feedback store

Submitted feedback was only as durable as the agent session that asked for
it. Code review persisted nothing at all: /api/feedback deleted the draft,
settled the decision promise, and if the invoking agent had already timed
out the review existed nowhere (the failure #678 fixed for annotate). Plan
decisions only reached plans/ while the client-side planSave setting was on,
and repeat decisions on one plan overwrote each other. Annotate kept the
#678 record for single local files only.

Every submission now appends one record to
${PLANNOTATOR_DATA_DIR}/feedback/{project}/index.jsonl, plus a
records/{stamp}-{surface}-{decision}.md sidecar when it carries content,
written at decision settlement time inside the servers so all nine agent
frontends are covered by two implementations.

Surfaces wired in both runtimes: plan approve and deny, code review
/api/feedback (Send Feedback, Approve, LGTM) and /api/exit, annotate submit,
approve and exit. Bare approvals, LGTMs and dismissals are decision-only
JSONL lines with no sidecar.

Records are cheap by design. Code review carries diff identity (vcsType,
diffType, base, gitRef, snapshotId, cwd, PR metadata, changed-file count,
patch byte count) and never the patch bytes; plan records carry the decision
text plus a reference to the history/{project}/{slug}/NNN.md version the
decision was made on rather than a second copy of the plan. Annotation
provenance (source, author) is preserved, so external, review-agent and
WebMCP findings stay tagged and source == null selects the reviewer's own
comments.

The shared module never throws: an archive failure is logged, degrades
silently for the user, and keeps the annotation draft as the recovery copy.
The append happens before deleteDraft, generalizing the #678 ordering.

Controlled by PLANNOTATOR_FEEDBACK_HISTORY / feedbackHistory (default on).
PLANNOTATOR_ANNOTATE_HISTORY=0 additionally suppresses records for every
annotate surface, so the documented stateless-annotate promise still holds.
"feedback" is added to PURGE_OWNED_TOP_LEVEL so uninstall purge removes it.

AI-assisted (Claude) under maintainer direction.

* fix(server): stop the feedback archive from writing into the real data dir in tests

Review findings on the durable feedback archive.

1. The archive is default-on, and most server tests boot a real plan, review,
   or annotate server without redirecting PLANNOTATOR_DATA_DIR, so `bun test`
   deposited records in the contributor's own ~/.plannotator/feedback (24 files
   across 12 buckets from two test files alone) on CI and every machine. A new
   bunfig test preload, tests/setup/feedback-archive-off.ts, turns the archive
   off for the suite; the archive's own tests opt back in inside their test
   bodies, which is also how they exercise the opt-out. Those tests now use
   distinctive project names and remove the annotate history they leave in the
   real data dir, since storage.ts fixes its data directory at import time.

2. PR reviews bucketed under feedback/pr-<n>/. PR mode never sets gitContext
   and --local points agentCwd at a pool/pr-<n> checkout, so deriving the
   project from the review cwd was wrong. ReviewServerOptions now takes a
   `project` option, mirroring the annotate server, preferred over the cwd
   derivation on both runtimes; the Claude Code, OpenCode, and Pi entry points
   pass their already-computed detectProjectName() result.

3. changedFiles overcounted renames: extractChangedFiles unions the a/ and b/
   sides so a reader can resolve either path. The record now counts b-side
   paths through countChangedFiles, so a rename is one file.

4. Docs: the feedback archive is added to the privacy page and
   PLANNOTATOR_FEEDBACK_HISTORY (plus PLANNOTATOR_ANNOTATE_HISTORY) to the
   environment variables reference. The overclaim that every submitted review
   is archived is corrected: a review posted straight to GitHub or GitLab
   through /api/pr-action is not archived locally yet. Three behaviors are now
   written down: O_APPEND is not atomic on NFS or SMB and a genuine interleave
   damages both records that raced, folder-session records carry the folder
   path rather than the open document, and URL-session records store the full
   URL including its query string.

5. Pi parity: the Node mirror now has the failed-archive-write test (the one
   invariant its handler copies by hand) and the PR-mode bucketing test.

Comments only, no behavior change: the pool checkout recorded in
target.review.cwd can be cleaned up before anyone reads the record, and
getPlanVersionPath resolves the data directory storage.ts captured at import
while the archive resolves it per call.

AI-assisted (Claude) under maintainer direction.

* docs(server): make the feedback index an explicit multi-client contract

plannotator-tui will append to the same feedback/{project}/index.jsonl with
client "plannotator-tui", so the module's stance of "a client tool may emit
this shape under its own clients/ namespace" is out of date. The index is one
shared source of records, labeled by client.

1. The module docstring and the FEEDBACK_RECORD_CLIENT comment now describe the
   shared index: several tools append to the same file, separated by `client`;
   plannotator-tui is a known second writer, herdr-annotate is reserved, and
   `client` is an open set rather than an enum to validate against.

2. Two optional fields are declared so v1 reserves their names across clients:
   target.agent ({ host, session, transcript }) for surfaces whose subject is
   an agent session rather than a file or a diff, and top-level clientVersion.
   Neither is populated here. clientVersion stays unset deliberately: there is
   no runtime-agnostic version constant in packages/shared, and reading
   package.json from a vendored module would be a new filesystem dependency
   for cosmetic data.

3. Sidecar naming is documented at the naming site and in AGENTS.md: other
   clients suffix their id ({stamp}-{surface}-{decision}-plannotator-tui.md),
   so recordFile values carrying such suffixes are valid and nothing may parse
   a sidecar name. Nothing in this repo did: every consumer treats recordFile
   as an opaque handle and no test pins a filename pattern. A new test appends
   a foreign line (unknown client, unknown fields, suffixed recordFile) and
   pins that the reader keeps it.

4. Honesty fix to the atomicity comments, in code and in AGENTS.md:
   appendFileSync loops internally, so "one write syscall" was wrong even on a
   local filesystem. The real model is that a line-sized buffer handed to a
   single append-mode write completes without interleaving in practice
   locally, with the reader's skip-unparsable tolerance as the backstop and
   the NFS/SMB caveat unchanged.

5. Exhausting the sidecar collision counter now throws a named error instead of
   re-throwing a bare EEXIST, so the server log says what actually happened:
   100 taken names in one millisecond means a stopped clock or a runaway
   writer, not a transient disk problem.

6. AGENTS.md and the parseFeedbackIndex doc state the reader contract: lines
   are gated on a numeric `v` and unparsable ones are skipped, so analyzers
   that depend on v1 semantics should filter v <= 1 themselves. Fields are
   added, never repurposed, so a v2 would mean a real shape change.

AI-assisted (Claude) under maintainer direction.
2026-09-01 10:56:39 -07:00
Michael Ramos 9682a05556 fix(test): give the unknown-subcommand e2e spawn its dist placeholders on CI
The e2e case from #1444 spawns the real CLI, whose module load imports
../dist/index.html and ../dist/review.html. Dev machines always have
those built, but CI's test job does not build the apps, so the spawn
died at import time with 'Cannot find module' before the guard ran and
the assertion on the unknown-command message failed, breaking main.

The test now drops empty placeholders for any missing artifact before
spawning and removes exactly what it created afterwards; a real dist is
never touched (verified with sentinel content). The guard under test
exits before either page is served, so placeholder content is
irrelevant.
2026-09-01 10:22:33 -07:00
Martin Sumera 58017a03c7 fix(cli): exit on an unknown subcommand instead of blocking on stdin (#1444)
A typo'd subcommand fell through the dispatcher to the plan-hook path,
which reads the hook payload from stdin. From a terminal that stdin never
closes, so `plannotator annotatte README.md` hung with no output instead
of reporting the typo.

Reject an unrecognized first token before the dispatcher runs: print the
offending word, the nearest documented command, and a pointer to --help,
then exit 1. Flags and the no-argument hook invocation are left alone so
the real hook path is untouched.
2026-09-01 10:12:35 -07:00
Michael Ramos 9f9ee27529 chore: bump version to 0.27.10 2026-08-31 14:13:25 -07:00
Michael Ramos 9e3af49f84 chore: bump version to 0.27.9 2026-08-27 16:05:28 -07:00
Michael Ramos b381ecbe12 chore: bump version to 0.27.8 2026-08-24 09:33:56 -07:00
Michael Ramos 34f25e79e2 chore: bump version to 0.27.7 2026-08-23 07:35:06 -07:00
Michael Ramos 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.
2026-08-22 12:07:42 -07:00
FND 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.
2026-08-21 18:06:23 -07:00
Michael Ramos 6e20ec78e8 chore: bump version to 0.27.6 2026-08-21 10:30:17 -07:00
Michael Ramos 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.
2026-08-21 10:29:29 -07:00
Michael Ramos b1a46d0a57 chore: bump version to 0.27.5 2026-08-21 09:06:36 -07:00
Michael Ramos 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.
2026-08-21 08:55:30 -07:00
Michael Ramos 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.
2026-08-19 10:44:21 -07:00
Michael Ramos 2a22e5805a chore: bump version to 0.27.4 2026-08-17 11:09:04 -07:00
FND 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
2026-08-16 21:58:46 -07:00
Michael Ramos 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
2026-08-16 12:17:13 -07:00
Michael Ramos aa0bf860d8 chore: bump version to 0.27.3 2026-08-13 16:06:13 -07:00
Michael Ramos 8b9dfe7e5f chore: bump version to 0.27.2 2026-08-13 11:36:16 -07:00
Michael Ramos 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.
2026-08-13 09:47:18 -07:00
Michael Ramos e181b824cc Mobile-safe plan and code comment composition (#1297)
* feat: harden mobile comment composition

* fix(ui): keep mobile app inside Safari viewport

* docs: record physical mobile triage

* fix(ui): extend plan canvas behind Safari controls

* fix(ui): let mobile plans drive Safari chrome

* fix(ui): release Safari top edge on mobile plans

* docs: triage mobile feedback and close phase 1b

* fix(ui): harden compact touch behavior
2026-08-13 08:58:55 -07:00
Michael Ramos d3633c9c52 Mobile foundation and quieter first run (#1295)
* feat: establish mobile foundation and simplify onboarding

* fix(ui): finish mobile foundation cleanup
2026-08-13 08:45:00 -07:00
Michael Ramos ef49c701c2 chore: bump version to 0.27.1 2026-08-12 17:07:36 -07:00
Michael Ramos d0d971a3bf chore: bump version to 0.27.0 2026-08-12 14:14:35 -07:00
Michael Ramos 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.
2026-08-12 14:04:59 -07:00
Michael Ramos 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.
2026-08-12 12:07:30 -07:00
Michael Ramos 3245310aa8 feat(review): add optional CallDiff call-flow analysis (#1268)
* feat(review): add optional CallDiff call-flow analysis

* fix(review): harden CallDiff integration
2026-08-11 13:18:35 -07:00
Michael Ramos 2fff8756d9 chore: bump version to 0.26.8 2026-08-10 17:00:33 -07:00
Michael Ramos 62c1eab119 chore: bump version to 0.26.7 2026-08-09 23:02:38 -07:00
Michael Ramos d579ff8db2 chore: bump version to 0.26.6 2026-08-09 21:07:30 -07:00
Michael Ramos 9c40ffadcc chore: bump version to 0.26.5 2026-08-09 17:49:14 -07:00
Michael Ramos d5ae439f7a chore: bump version to 0.26.4 2026-08-07 15:13:27 -07:00
Michael Ramos c760fc522b chore: bump version to 0.26.3 2026-08-07 14:32:16 -07:00
Michael Ramos bbae458e5a chore: bump version to 0.26.2 2026-08-06 00:50:56 -07:00
Michael Ramos c08b188812 perf(ui): single Shiki highlighter, palette-matched code blocks, drop highlight.js (#1218)
* perf(build): stub out the dead Oniguruma WASM in every bundle

@pierre/diffs picks its Shiki engine with a runtime ternary:

    engine: preferredHighlighter === "shiki-wasm"
      ? createOnigurumaEngine(import("shiki/wasm"))
      : createJavaScriptRegexEngine()

Plannotator pins `preferredHighlighter: 'shiki-js'` (and Pierre's own
default is 'shiki-js'), so the Oniguruma branch never executes. Because
the choice is a runtime ternary, bundlers keep the `import("shiki/wasm")`
edge anyway and inline `@shikijs/engine-oniguruma/wasm-inlined`, a
~622 KB base64 blob, into the single-file HTML builds. The review app
paid for it twice: once on the main thread (via
`highlighter/shared_highlighter.js`) and once inside the `?worker&inline`
Pierre worker.

Alias `shiki/wasm` to a stub that throws if it is ever reached. Wired via
`resolve.alias` rather than a plugin because `resolve.alias` is shared
with Vite's worker build and `plugins` are not.

Highlighting output is unchanged: the JS regex engine was already the one
doing the work. Opting back into 'shiki-wasm' now fails loudly instead of
silently costing every user a megabyte of dead bytes.

    apps/review/dist/index.html  19,424,646 -> 18,180,545  (-1,244,101 raw / -463,348 gzip)
    apps/hook/dist/index.html    23,032,467 -> 22,410,416    (-622,051 raw / -233,485 gzip)

* perf(ui): consolidate code highlighting onto Shiki, drop highlight.js

The app shipped two highlighters. Shiki already tokenised the code-review
diff pane (via @pierre/diffs, JavaScript regex engine); highlight.js
separately coloured markdown fences and review suggestion snippets at
~982 KB minified for a full build of ~190 grammars. That second
highlighter is now gone.

Every call site moves onto `packages/ui/utils/codeHighlight.ts`, a thin
wrapper over Pierre's SHARED Shiki instance:

  CodeBlock, Viewer, PlanCleanDiffView   markdown fences
  InlineMarkdown                          code-file hover preview
  HighlightedCode                         review suggestion snippets

Reusing Pierre's instance rather than standing up a second fine-grained
one is deliberate. Pierre imports Shiki's full bundle, so every grammar
and theme is ALREADY inlined in the single-file builds: a separate
highlighter with a curated language list would have duplicated a subset
of bytes that are already there. Sharing costs nothing, gives every
language Shiki bundles instead of a shortlist, and — the point of the
change — guarantees fences resolve the exact same theme the diff pane
resolves.

Theming. `SHIKI_THEME_MAP` / `resolveSyntaxTheme` move from
`packages/review-editor/hooks/usePierreTheme.ts` to
`packages/ui/utils/syntaxTheme.ts`; usePierreTheme re-exports them, so
the review editor's imports are unchanged. `useFenceTheme()` feeds the
components and re-highlights on palette or mode change. Code blocks now
follow the active palette across all ~52 themes in both light and dark,
instead of always rendering github-dark and relying on hand-written
`.hljs-*` override stacks to stay legible. Those stacks are deleted:
`packages/editor/index.css`'s light-mode token palette, and
`colorblind.css`'s hand-tuned tokens which existed to APPROXIMATE
@pierre/theme's protanopia-deuteranopia themes that are now simply used.

Behaviour held fixed:

  - Language-less fences stay plain text (#1212). No auto-detection
    anywhere, including the hover preview, which previously called
    `hljs.highlightAuto`. `HighlightedCode` derives its language from
    the caller's file path; an unknown extension renders plain.
  - `applyHighlight(el, ...)` keeps the imperative `hljs.highlightElement`
    DOM contract the annotation layer reaches into, and writes plain text
    at final size first so async highlighting causes no layout shift.
    Already-attached grammars highlight synchronously — no flicker on
    cached highlights.
  - It also verifies the rendered text is byte-identical to the source
    and falls back to plain otherwise, because annotations address code
    blocks by text offset.
  - `@plannotator/ui`'s public API is unchanged: the highlighter is a
    module-level default like the package's other seams, no new props.

The `hljs` class on fenced `<code>` becomes `pn-code` (it is a
structural hook for blockTargeting, vim navigation and print.css, and it
named a library we no longer ship). `language-*` stays.

    apps/review/dist/index.html  18,180,545 -> 17,270,889  (-909,656 raw / -291,921 gzip)
    apps/hook/dist/index.html    22,410,416 -> 21,704,434  (-705,982 raw / -238,096 gzip)

Verified the diff pane is untouched: the rendered Pierre shadow-DOM
markup is byte-for-byte identical between an origin/main build and this
one (SHA-256 aa1ee88a…).

* fix(ui): strip stray NUL bytes from the code-highlight source

Two U+0000 bytes slipped into comments in the previous commit, which made
git treat the file as binary. Replaced with spaces; no behaviour change.

* fix(ui): keep code-block annotation marks across highlight swaps

Fenced code is annotated by hand: one `<mark data-bind-id>` inside the
`<code>` element, which `applyHighlight` also owns. Every highlight swap
(palette change, dark/light toggle, or the first async grammar attach
after load) replaces that element's children, so the mark was silently
wiped and nothing put it back. Annotation state, the sidebar panel and
exports were unaffected; the loss was purely visual, and deterministic.

`applyHighlight` now publishes every write through `onCodeHighlightSwap`,
synchronously, immediately after it. `Viewer` subscribes and re-paints the
fence's mark, so a swapped block ends up with BOTH the new theme's tokens
and its annotation. The shared painter (`paintCodeBlockMark`) moves the
token spans into the mark instead of flattening them to text, so creating
an annotation no longer costs a block its colours either.

Being driven by the swap also fixes the cousin race by ordering rather
than timing: share/draft restore runs on a timer after load, and on a slow
machine the first async swap could land after it and wipe the restored
marks per block. A restore that painted before the swap is now
re-established in the same task the swap ran in, and one that runs after
finds the mark already there.

Removal tombstones the id before re-highlighting, because the host drops
the annotation from state a tick later — without it the swap listener
would paint the just-removed annotation back in, and a fence carrying a
second annotation would end up bare.

Also closes the named gap in the WASM coverage: entry-assets only grepped
source, so a future @pierre/diffs bump could reintroduce the inlined blob
through a different import specifier unnoticed. It now greps the built
`apps/{review,hook}/dist/index.html` for the base64 WASM magic, skipping
on an unbuilt checkout and running for real in the CI job that builds the
bundles.
2026-08-05 21:54:40 -07:00
Michael Ramos 50a54c872b chore: bump version to 0.26.1 2026-08-05 11:39:02 -07:00