Commit Graph

59 Commits

Author SHA1 Message Date
Michael Ramos 467c11b29f fix: discover symlinked workspace repositories (#1060) 2026-07-16 14:15:27 -07:00
renovate[bot] 969f5b2141 chore(deps): update github actions (#593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-15 21:04:37 -07:00
Michael Ramos 977f4ce582 fix: final QA sweep fixes — file-browser cap priority, OpenCode project scoping, spotlight Alt-Alt dismiss
Fixes for the three-agent adversarial sweep findings:

- reference-handlers.ts: seed the user's modified/untracked files BEFORE
  the bulk walk — the 5000-file cap (d3c9de1e) filled in raw readdir order
  and could silently drop the exact files the user just edited from the
  annotate browser (and the truncated latch broke the merge loop on its
  first iteration).
- opencode commands.ts: pass `project` to startAnnotateServer at both
  call sites (annotate + annotate-last) via detectProjectName, matching
  the hook and Pi runtimes — OpenCode annotate history no longer lands in
  the shared "_unknown" bucket.
- App.tsx: the Alt-Alt destination double-tap now dismisses the
  DestinationSpotlight — the coachmark advertised that exact gesture but
  its own keydown handler deliberately ignores modifiers, so performing
  the tip left the dim overlay stranded.
- annotate.ts + pi mirror: degradation notice now reads "warning: annotate
  history unavailable" so OpenCode's logCliWarnings forwarder (which
  filters on \bwarn(ing)?\b) actually surfaces it.
- cli-bridge.ts: a rejected showToast un-marks the URL (the other delivery
  path can retry) and logs the failure instead of being fully silent.
- test.yml: register packages/editor/editableDocumentsHook.test.tsx (7
  draft/conflict tests, DOM-gated since #936, never ran in CI). Repo-wide
  sweep confirms all 19 DOM-gated test files are now registered.

Gates: DOM batch 102/0 across 19 files, review+server 507/0, opencode 70/0,
install harness 85/0, full typecheck incl. strict-consumer, review build OK.

Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
2026-07-10 06:50:40 -07:00
Michael Ramos 8350299bce ci: register 5 orphaned DOM-gated test files in the DOM_TESTS batch
QA audit found four DOM-gated test files added since v0.22.0 (srcdoc,
InlineMarkdown.resolveLinkedDoc, MarkdownDiff.frozen,
MarkdownEditor.extensions) that self-skip under plain `bun test` and were
never added to the DOM_TESTS batch — so the mounted-seam contracts for the
published markdown-editor/atomic-editor packages ran nowhere in CI. Also
registers components/sidebar/FileBrowser.test.ts (pre-existing gap; its new
filtering tests from 1267cd50 were equally unexercised). All 5 files pass
locally: 40 tests, 0 fail.

Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
2026-07-10 05:53:17 -07:00
Michael Ramos e20fd97be7 Consumer enablement for @plannotator/ui (0.24.0) (#1017)
* feat(ui): AnnotationPanel renderCardFooter + readOnly host props

Per-card footer slot for host reply/resolve UI (clicks inside don't
select the card); readOnly hides delete/edit on all card kinds.
Both optional, both no-op by default — Plannotator unchanged.

* feat(ui): bless 6 exports into the strict-consumer surface

TableOfContents, ResizeHandle, useResizablePanel, useActiveSection,
useScrollViewport, utils/annotationHelpers — verified strict-clean and
backend-free (useResizablePanel persists via the storageBackend seam);
added to the gate and the HANDOFF supported-imports table.

* feat(ui): Viewer allowImages + readOnly host props

allowImages threads to both CommentPopover sites (popover already gated
its attach affordance; Viewer never exposed the knob). readOnly
suppresses every composer entry point — selection toolbar (highlighter
'enabled'), pinpoint, global comment, attachments, checkbox toggles —
while existing annotations still render and select.
Both default to today's behavior.

* fix(ui): lazy-import Viewer/CommentPopover in the consumer test (DOM-less bun test crashed on web-highlighter)

* feat(ui): strict-consumer gate gains verbatimModuleSyntax + noUnusedLocals/Parameters

Fixed the 24 violations the flags surfaced across the supported-import
graph: type-only imports (verbatimModuleSyntax) and dead imports/locals.
Consumers with stricter tsconfigs no longer have to relax them.

* feat(ui): opt-in content-verifying annotation restore

verifyRestoredContent on useAnnotationHighlighter: after a meta-based
fromStore restore, the painted text is checked against originalText
(whitespace-normalized). Mismatch -> highlight removed, text-search
fallback re-anchors; if that fails too, onRestoreMismatch(annotation,
restoredText) fires and nothing is painted. Default off — today's
trust-the-positions behavior. Workspaces hit this live after document
drift; correctness upgrade for every consumer.

* chore(ui): 0.24.0 — HANDOFF consumer-enablement notes + version bump
2026-07-07 19:09:52 -07:00
Michael Ramos 070d9a5f6d Make the document UI reusable as published building blocks (#957)
* docs(adr): revert failed document-ui cutover, add ADR 004 with corrected reuse plan

The document-ui extraction/cutover (ADRs 002/003) was an AI-driven rewrite that
broke the app; the code was reverted. Add ADR 004 as the source of truth: share
@plannotator/ui as published building blocks for the Workspaces app, keep
Plannotator's app unchanged, gate on human-verified parity. Banner the reverted
ADRs and point AGENTS.md/CLAUDE.md at 004 so future agents don't rebuild the mess.

* docs(adr): add verified document-ui extraction plan, supersede draft inventory

36-agent verification of the reuse inventory: confirmed the /api coupling but
found the draft missed Viewer's transitive backend call, the cookie settings
layer, 3 React contexts + identity singleton, SSE transports, and harder
packaging blockers. Adds the verified per-subsystem extraction plan with a
parity guardrail on every step; flags the draft inventory as superseded.

* docs(adr): add document-ui extraction roadmap + parity checklist

Phase 0-7 execution roadmap (safety net -> packaging -> foundation seams ->
rendering -> navigation -> comments -> extras -> publish) and the reusable
'did it break?' parity checklist run after every step. Both enforce the law:
move + decouple, never rewrite; Plannotator's experience cannot change.

* build(ui): packaging unblock for external install (Phase 1) — no runtime change

Phase 0: captured parity baseline (typecheck/test/build + shipped-bundle hashes).
Phase 1 packaging fixes to packages/ui, metadata only:
- add phantom dompurify ^3.3.3 dep (imported in sanitizeHtml/aiChatFormat, was undeclared)
- align diff ^8.0.3 -> ^8.0.4 with root
- add peerDependencies (react, react-dom, tailwindcss, tailwindcss-animate); keep as devDeps
- add files allowlist (excludes tests); remove dead tsconfig @plannotator/shared alias

Verified byte-identical: typecheck pass, 1620 tests pass/0 fail, all 3 builds OK,
shipped plan+review bundle hashes unchanged from baseline. Remaining Phase 1
blocker (@plannotator/ai + @plannotator/shared workspace:* deps) deferred pending
a publish-vs-inline decision; logged in worklog.

* feat(ui): make image URL resolution host-overridable (Phase 2, seam 1)

getImageSrc now delegates to a module-level resolver defaulting to the verbatim
Plannotator /api/image logic; add setImageSrcResolver/resetImageSrcResolver so a
host (Workspaces) can resolve images via its own backend. All 5 consumers and the
signature unchanged. Verified: default URLs byte-identical, typecheck pass, 1620
tests pass/0 fail, builds OK. No Plannotator behavior change.

* feat(ui): make settings storage backend host-overridable (Phase 2, seam 2)

storage.ts cookie impl is now the default 'cookieBackend'; add setStorageBackend/
resetStorageBackend so a host (Workspaces) can persist settings via its own
storage. getItem/setItem/removeItem delegate to the active backend; the ~24
consumers and literal plannotator-* keys are unchanged. Verified: swap works,
typecheck pass, 1620 tests pass/0 fail, builds OK, theme persists across reload.

* feat(ui): make MarkdownEditor theme mode host-supplyable (Phase 3)

Add optional mode? prop; mode now mode ?? resolvedMode. Plannotator passes no
mode (App.tsx:4261) so it keeps using ThemeProvider's resolvedMode unchanged. A
host without ThemeProvider can supply mode directly. Verified: typecheck pass,
1620 tests/0 fail, builds OK, App.tsx untouched.

* feat(ui): allow hosts to opt out of code-path validation (Phase 3)

Viewer gains optional disableCodePathValidation? threaded to a new disabled? arg
on useValidatedCodePaths; when set, the /api/doc/exists probe is skipped. Default
undefined for Plannotator => validation stays on, /api/doc/exists fires exactly as
today. Verified: typecheck pass, 1620 tests/0 fail, builds OK, App.tsx untouched.
Also logs Phase 3 workflow outcome + remaining scroll/docfetch pieces.

* feat(ui): make code-file hover preview fetch host-overridable (Phase 3)

Add DocPreviewFetcher seam (default = verbatim /api/doc fetch) +
setDocPreviewFetcher/resetDocPreviewFetcher; route handleMouseEnter through it,
useCallback deps unchanged. No caller overrides it => Plannotator fetches /api/doc
identically. typecheck pass, 1620 tests/0 fail, builds OK.

* feat(ui): ship ScrollViewportProvider with the library (Phase 3 scroll)

Add render-transparent ScrollViewportProvider (createElement, keeps .ts) so the
scroll-viewport context travels with @plannotator/ui instead of living only in
App.tsx. Rewire App.tsx provider tags (3-line delta); identical tree/value/
position, sidebar TOC still reads the MAIN viewport. Fix stale OverlayScrollbars
doc-comment. typecheck pass, 1620 tests/0 fail, builds OK, eyeball: TOC tracks.

* fix(ui): disabled code-path validation should keep links clickable (self-review)

The Phase-3 disabled branch set ready=true with an empty map, which makes
gateCodePath demote every code link to plain text. Leave ready=false so the
no-validation fallback renders links optimistically. No Plannotator impact
(never disables). Logs Phase 3 completion + reusability note. typecheck pass,
1620 tests/0 fail, builds OK.

* feat(ui): make file-tree backend host-overridable (Phase 4)

Lift useFileBrowser's three backend wires (load-dir fetch, obsidian-vault fetch,
and the SSE live-watch effect moved VERBATIM) into an injectable FileTreeBackend
with default + setFileTreeBackend/resetFileTreeBackend, same pattern as the image
/storage seams. useFileBrowser() stays zero-arg; default fetch/SSE URLs identical.
Sidebar confirmed noop (zero backend wires, already reused by review-editor).

Verified: useFileBrowser.test.tsx passes 6/0 UNMODIFIED (DOM_TESTS=1), typecheck
pass, 1620 tests/0 fail, builds OK, App.tsx untouched, manual eyeball (annotate
adr/: tree loads, file-switch works, new file appears live via SSE). Plannotator
byte-unchanged. Logs two pre-existing bugs found during testing (not regressions).

* docs(adr): research + synthesis + spec for Phase 5 (comments/annotations/drafts)

Five-probe code research of the comment system. Key finding: most comment UI is
already portable (panel/popover/toolbar/highlighter prop-driven; review-editor
already reuses the hooks). Phase 5 narrows to 3 seams — draft transport (+ the
3-party generation protocol), external-annotation transport (SSE->polling, move
verbatim), and identity/authorship — plus 2 non-extraction items: renderer
coupling (document as a contract) and replies/threading (defer as a new feature).

* docs(adr): accept ADR 005 — make comments/annotations/drafts host-overridable (Phase 5)

Three seams (identity, draft transport, external-annotation transport), each
defaulting to today's behavior; renderer coupling documented as a contract;
replies/threading deferred as a new feature. Locks in the recommended choices
from the Phase 5 spec/synthesis.

* feat(ui): make annotation identity host-overridable (Phase 5 seam 1)

Add IdentityProvider + setIdentityProvider/resetIdentityProvider in identity.ts;
getIdentity/isCurrentUser now delegate to a module-level provider defaulting to
today's ConfigStore tater behavior. The ~9 author-stamp sites and 2 (me)-badge
sites delegate with zero call-site edits. No caller overrides => Plannotator
byte-unchanged. typecheck pass, 1620 tests/0 fail, builds OK.

* feat(ui): make draft persistence transport host-overridable (Phase 5 seam 2)

Add DraftTransport (load/save/remove) + getDraftTransport/setDraftTransport/
resetDraftTransport in useAnnotationDraft.ts, default = today's /api/draft fetches
verbatim. useCodeAnnotationDraft reads getDraftTransport() live. The generation
pre-increment, 500ms debounce, keepalive retry-gate, and pagehide/visibilitychange
flush stay in the hooks; getDraftGeneration() still escapes to the host. save
rejects-on-failure so the gated retry is preserved. No caller overrides =>
Plannotator byte-unchanged. shared/draft.test.ts 10/0, annotationDraftPersistence
13/0, typecheck pass, 1620 tests/0 fail, builds OK.

* feat(ui): make external-annotation transport host-overridable (Phase 5 seam 3)

Add ExternalAnnotationTransport<T> (subscribe/getSnapshot/CRUD) + setters in
useExternalAnnotations.ts; default = today's SSE->polling wire moved verbatim into
createDefaultTransport. The reducer (applyEvent), fallback-once gate, 500ms poll,
versionRef scoping, optimistic-before-await, and [enabled] gate stay in the hook.
A host (Workspaces) can implement the same event contract over Durable Objects.
No override caller => Plannotator byte-unchanged. external-annotations test green,
typecheck pass, 1620 tests/0 fail, builds OK. Logs Phase 5 completion.

* docs(adr): research + synthesis + spec for Phase 6 (versions, settings, sharing, AI)

Five-probe code research. Most of the four subsystems is already portable; the
real work is 5 seams (version fetchers + vscode-diff, config write-back, obsidian
detect, save-to-notes, AI transport) + 1 CSS move (block/raw diff classes from the
app shell into the package's theme.css). Fragile do-not-touch: the AI SSE reader
loop + epoch guards, and configStore debounce/deepMerge. Five Plannotator-only
pieces (OpenInApp, HooksTab, useUpdateCheck, useAgents/useAgentJobs) stay home.

* docs(adr): accept ADR 006 — make extras (versions/settings/sharing/AI) host-overridable (Phase 6)

Five seams + one CSS move, each defaulting to today's behavior. AI reader loop +
epoch guards and configStore debounce/deepMerge stay verbatim. Five Plannotator-
only pieces stay home. Locks the recommended choices from the Phase 6 spec.

* feat(ui): make version fetchers + vscode-diff host-overridable; move diff CSS into package (Phase 6 versions)

usePlanDiff gains optional fetchers (default /api/plan/version(s), error asymmetry
kept: selectBaseVersion alerts, fetchVersions silent). PlanDiffViewer gains optional
onOpenVscodeDiff (default /api/plan/vscode-diff). Relocate .annotation-highlight* +
.plan-diff-* block/raw CSS from editor/index.css into ui/theme.css (next to
.plan-diff-word-*) so the diff/highlights are self-styling from the package.
Verified: relocated CSS gone from index.css, present in shipped bundle (33x), diff
renders identical; typecheck pass, 1620 tests/0 fail, builds OK, App.tsx untouched.

* feat(ui): make config write-back + obsidian-detect host-overridable (Phase 6 settings)

configStore.setServerSync(fn) injects only the terminal POST /api/config; the 300ms
debounce, deepMerge batching, singleton, and eager cookie reads stay verbatim.
Settings gains optional onDetectObsidianVaults (default /api/obsidian/vaults), with
the [obsidian.enabled] effect dep + auto-select-first-vault verbatim. No override
caller => Plannotator unchanged. typecheck pass, 1620 tests/0 fail, builds OK.

* feat(ui): make save-to-notes host-overridable (Phase 6 sharing)

ExportModal gains optional onSaveToNotes (default = verbatim POST /api/save-notes);
showNotesTab = isApiMode && !!markdown kept byte-for-byte. Sharing utils already
parameterized (noop). No override caller => Plannotator unchanged. typecheck pass,
1620 tests/0 fail, builds OK.

* feat(ui): make Ask AI transport host-overridable (Phase 6 ai)

useAIChat gains a module-level AITransport (session/query/abort/permission) +
setAITransport/resetAITransport, default = the five /api/ai/* fetches verbatim. The
SSE reader loop, epoch/createRequest guards, and the supersede-abort position inside
createSession stay untouched. Capabilities + provider-resolution stay host-owned in
App.tsx. No override caller => Plannotator unchanged. ai.test.ts 97/0, typecheck
pass, 1620 tests/0 fail, builds OK.

* docs(adr): log Phase 6 completion (4 seams + diff CSS move)

* docs(adr): research + synthesis + spec for Phase 7 (carve @plannotator/core + publish)

Carve a browser-safe @plannotator/core: move the ~15 pure shared modules in,
extract types from the 3-4 node-bound ones (config/storage/workspace-status) so
nothing duplicates, shim @plannotator/shared so Plannotator's 99 import sites stay
unchanged, re-point @plannotator/ui to depend only on core, move wideMode.ts, then
publish core+ui (source-only). shared + ai stay private. Open: registry, versions,
CI job. Publish is the one outward-facing step — confirm before pushing.

* docs(adr): fold configurePlannotatorUI() front door + precompiled CSS into Phase 7 spec

Add the single typed configure() facade over the 9 global host-override setters
(zero-risk, additive) and an optional precompiled CSS bundle (smooths the
Tailwind-in-shared-lib wrinkle) to the Phase 7 publish scope. Both make the
published surface nicer to consume; neither touches Plannotator.

* docs(adr): lock Phase 7 publish decisions + carry over review fixes

Decided: ship JS as source (single internal consumer on controlled stack, no
build to maintain, no dist drift); precompiled CSS now REQUIRED (the @source glob
is fragile under pnpm symlinks); core CI typecheck node-free; pin ui->core exact.
Recorded the interrogation's carried-over Phase-5 code fixes (useExternalAnnotations
split-transport + fallbackRef reset, per-seam override tests, configStore loadFromBackend)
to do before publish.

* docs(adr): ADR 007 — carve @plannotator/core, complete settings provider, publish

Locks Phase 7 decisions: public npm; lockstep version at repo 0.21.0 (ui->core
pinned exact); JS ships as source + required precompiled CSS; core CI node-free;
ai stays unpublished-to-npm. Settings provider completed (loadFromBackend, prefetch
+sync) is now IN SCOPE — Workspaces uses the same UI settings stored in its own
backend. CI publish job wired but artifacts validated on-branch (pack + dry-run)
before merge; first publish gated. Carries the 2 override-path bug fixes + per-seam
override tests as pre-publish work.

* fix(ui): make external-annotation transport reads consistent + reset fallback on re-enable

Two override-path bugs found by the interrogation pass (both unreachable on
Plannotator's path; harden the host-override path for a real consumer):

1. Split-transport: the effect captured the transport at mount for subscribe/poll
   while the CRUD callbacks read the module global live, so a host swapping the
   transport after mount would split reads and writes across two backends. Capture
   once in a ref and use it in all four spots.

2. fallbackRef/receivedSnapshotRef were not reset on effect re-run, so an
   enabled false->true toggle inherited a stale 'already fell back' flag and
   silently stopped updating. Reset both at the top of the effect.

Plannotator unchanged: it never overrides the transport (same default singleton
captured) and enabled never toggles (reset is a no-op). typecheck clean; full
test suite shows zero delta (1605 pass / 45 pre-existing env failures, identical
with and without this change).

* docs(adr): align Phase 7 spec with ADR 007 (version 0.21.0 lockstep, CSS required, scope completeness)

* feat(core): carve @plannotator/core — move pure modules, extract node-bound types, shim shared (Phase 7 step 1)

* feat(ui): depend only on @plannotator/core — re-point all shared/ai imports (Phase 7 step 2)

* refactor(ui): relocate wideMode helper to @plannotator/ui/utils (Phase 7 step 3)

* feat(ui): add loadFromBackend settings rehydration + configurePlannotatorUI front door (Phase 7 step 4)

* build(ui): precompiled styles.css CSS build + madge circular-dep check (Phase 7 step 5)

* test(ui): per-seam override tests + configure routing test (Phase 7 step 6)

Add one override test per seam (setX(fake)→drive→assert→resetX()) for all
9 seams + loadFromBackend, modeled after the existing seam test pattern.
Fix configure.test.ts to defer mock.module() into beforeAll and restore with
captured real function references in afterAll so sibling seam test files are
not poisoned by spy replacements in the shared Bun worker module registry.

* fix(ui): apply Phase 7 review findings — version lockstep + seam consistency

- Bump @plannotator/ui to 0.21.0 (lockstep with @plannotator/core + repo, per ADR 007) [was the 1 critical review finding]
- useAnnotationDraft: route persistNow/dismissDraft save+remove through getDraftTransport() so all paths read the transport consistently (matches the load path; makes the single-global invariant explicit)
- configStore.loadFromBackend: document it must be called BEFORE init() or server values get overwritten
- packages/core/tsconfig: add explicit types:[] so the node-free invariant is first-class (verified: planted node:fs still fails TS2882)

* docs(adr): Phase 7 implementation plan (workflow-generated, durable artifact)

* fix(ui): reconcile #948 with the draft-transport seam + lockstep 0.21.1

Rebased onto origin/main (picks up #948 draft-deletion fix, the 0.21.1 bump, and
the #949/#950 editor fix). The rebase auto-merged #948's code-draft logic
(hasHadAnnotationsRef, empty-state tombstone, clearTimeout in restore/dismiss) with
the Phase-5 transport refactor cleanly — except the empty-state tombstone delete was
left as a raw fetch('/api/draft', DELETE). Route it through getDraftTransport().remove()
so a host backend tombstones its own stored draft on clear (the #948 guarantee, for
hosts). Plannotator unchanged (default transport hits the same endpoint).

Bump @plannotator/core + @plannotator/ui 0.21.0 -> 0.21.1 to match main's version
(lockstep per ADR 007).

Verified: typecheck clean, madge no-cycles, plain suite 1637 pass / 0 fail, #948
draft-clear test 3/0. (The 45 DOM_TESTS failures are the known server/network
integration tests that need a real OS env — same set on main, not regressions.)

* fix(ui): address review nits — host-path robustness + cleanups

- PlanDiffViewer: wrap onOpenVscodeDiff in try/finally so a host opener that throws
  can't wedge the VS Code button in a permanent loading state (default unaffected)
- useExternalAnnotations: (re-)capture the transport inside the effect on enable so a
  host that installs a transport before enabling annotations is honored, not the stale
  default — keeps the split-transport fix (effect + CRUD share one ref)
- configure.ts: import ServerSyncFn from configStore instead of duplicating the type
- repoint the 2 remaining @plannotator/shared test imports to @plannotator/core
- AGENTS.md/CLAUDE.md: document the new packages/core package

All host-path only — Plannotator behavior unchanged. typecheck clean, no cycles,
full suite green. Skipped (not simple/over-engineering): usePlanDiff prop->module-level
(design change), Obsidian late-bind, getSnapshot guard (inert), transport <any> (variance).

* docs: collapse 29 ADR process docs into one packages/ui/README.md

The branch had accumulated ~6,200 lines of ADR scaffolding (6 decisions, 7 specs,
10 research spikes/synthesis, 6 worklogs/roadmaps/plans) for this one effort. Replace
all of it with a single concise README that ships with the published package: what
@plannotator/ui + @plannotator/core are, why they exist (commercial reuse), how the
host-override seams work (configurePlannotatorUI), how a consumer installs/builds, and
the one rule (don't reimplement from scratch — add a seam). Repoint the CLAUDE.md banner
at the README. No code references the deleted docs; main's pre-existing adr/ docs untouched.

* docs(ui): add packages/ui/AGENTS.md guardrail + CLAUDE.md symlink

Directory-scoped agent guidance for anyone editing @plannotator/ui: don't rewrite from
scratch, add a seam (default = today's behavior, Plannotator byte-for-byte unchanged),
core stays node-free, never delete working code until human parity. Points to README.md
for the architecture. CLAUDE.md -> AGENTS.md symlink mirrors the repo root convention.

* build: remove madge circular-dep check (unmaintained)

madge is unmaintained (~3 years stale) and the check was never wired into CI, so it
was a dormant script + devDependency on a load-bearing path. Drop it: remove the
check:cycles script, the madge devDependency, and .madgerc.

The no-cycle invariant still holds by construction — @plannotator/core imports nothing
(zero @plannotator deps in its package.json), so any accidental core->shared/ui import
fails at publish-time bun pm pack (and review). No automated tripwire, but no stale
unmaintained tooling either.

* fix(ui): address review — TDZ guard, html-viewer export, doc corrections

- useExternalAnnotations: declare unsubscribe as let (not const) + guard calls, so a
  host transport that fires onError synchronously during subscribe falls back to polling
  instead of throwing a TDZ ReferenceError (Plannotator's EventSource fires async, never hit)
- package.json: add explicit ./components/html-viewer export (dir has index.ts; the
  ./components/* -> *.tsx wildcard can't resolve it, so external installers would fail)
- README: fix configurePlannotatorUI sample keys to the real option names
  (storageBackend/identityProvider/imageSrcResolver/externalAnnotationTransport)
- AGENTS.md: point the Ask-AI mapping at packages/core/agents.ts (shared/agents.ts is a shim now)

All publish/host-path/doc only — Plannotator unchanged. (#1 CSS-build font collision
deferred to publish-prep — it needs the asset pipeline + files allowlist, not a one-liner.)

* build(ui): don't bundle fonts in published styles.css — app loads fonts (review #1)

Industry standard for a shared UI package: ship theme + component CSS, let the consuming
app load fonts. Drop the @fontsource imports from styles-entry.css (the publish CSS entry);
the theme still defines --font-sans/--font-mono, and the app provides those families. Fixes
the asset-name collision (every emitted .woff2 was renamed styles.css) and shrinks the
published stylesheet 555kB -> 185kB. README documents the two-line @fontsource install.

Plannotator unaffected: its apps (editor/review-editor index.css) load fonts via their own
entry CSS — styles-entry.css is consumed ONLY by the publish CSS build.

* fix(ui): build styles.css on prepack, not prepublishOnly (review #4)

prepublishOnly doesn't run for npm pack / bun pm pack / git / file: installs, so the
package exported ./styles.css without shipping it. prepack runs on any pack, so the
stylesheet is always present. Verified: bun pm pack now emits styles.css.

* chore(ui): post-rebase reconciliation — version lockstep 0.21.3, awaitable AI abort seam

Rebased onto main (0.21.3). Bump @plannotator/core + @plannotator/ui to 0.21.3
to stay in lockstep with the repo version.

Resolve the useAIChat conflict: main added postServerAbort (an awaitable abort
that prevents session-busy races) using a raw fetch. Route it through the
AITransport seam by making AITransport.abort return Promise<unknown> instead of
void, so the host override is honored AND main's await-the-abort behavior is
preserved. Update the abort mocks in the seam/configure tests accordingly.

* fix(ui): make postServerAbort never reject regardless of AI transport

The await site in ask() relies on postServerAbort resolving so a superseding
query can proceed. main's original guaranteed this with its own .catch on the
fetch; routing through the AITransport seam delegated that guarantee to the
transport. Restore it at the call site (Promise.resolve(...).catch) so a host
override that rejects — or returns void at runtime — can't throw out of ask().

* fix(ui): address review — core import, abort sync-throw, snapshot guards

- useAIProviderConfig: import Origin from @plannotator/core/agents (was the only
  ui file still importing @plannotator/shared); drop the masking shared/* path
  alias from ui/tsconfig.json so a stray shared import now fails typecheck. The
  hook is part of the published surface — a standalone install had no
  @plannotator/shared to resolve.
- useAIChat.postServerAbort: defer the transport call into .then so a host abort
  that throws *synchronously* also can't reject (the .catch only caught async).
- useExternalAnnotations: default getSnapshot returns null (skip) on a malformed
  200 instead of coercing to []/0, so it can't clear annotations or reset the
  version cursor — restoring the pre-seam behavior.

* feat(ui): add upload + identity-editable seams for host backends

Two override points the Workspaces app needs that had no seam:

- UploadTransport (utils/upload.ts): image attachments hardcoded POST /api/upload
  with no override. Add a setX/resetX/getX seam (default = today's /api/upload,
  verbatim) and route AttachmentsButton through it. Workspaces sends bytes to its
  R2 asset API and returns the content-addressed URL.
- IdentityProvider.isEditable() (utils/identity.ts): the Settings rename/regenerate
  controls wrote to the cookie store, bypassing a host identity provider — so a
  host with server-owned identity could split one user across two author names.
  Add an optional isEditable() (default true) and hide the rename controls when a
  host returns false. Plannotator's cookie identity stays editable — unchanged.

Both wired into configurePlannotatorUI(); seam tests added; configure routing test
covers uploadTransport. HANDOFF.md updated with the Workspaces seam mapping from
the repo research (asset layer, identity, realtime, no-AI-infra, the Me
display-name backend follow-up). README publish command corrected to bun pm pack
+ npm publish.

* refactor(ui): capture sessionId synchronously in postServerAbort

Self-review: the deferred .then read sessionIdRef.current a microtask after the
guard checked it. Capture the id synchronously so the abort always targets the
session current at call time and there's no double-read.

* fix(ui): address review — seed host store, browser-safe timer type, harden abort

- configStore.loadFromBackend: seed the host StorageBackend with resolved defaults
  for keys it lacks. The constructor runs at module load (before a host installs
  its backend), so its default-seeding writes went to the cookie backend; without
  this a fresh host store was never populated and generated defaults (e.g.
  displayName) regenerated every reload. [P1, host path]
- Viewer.tsx: replace NodeJS.Timeout with ReturnType<typeof setTimeout> (2 refs)
  so a browser-only consumer compiling the published source doesn't need
  @types/node. Matches the pattern already used in configStore. [P1, published path]
- useAIChat: harden the create-session supersede abort the same way as
  postServerAbort, so a host transport that throws can't surface an unhandled
  rejection. No impact on Plannotator (default self-catches). [nit]
- .gitignore: correct stale 'prepublishOnly' comment to 'prepack'. [nit]

Plannotator behavior unchanged (it never calls loadFromBackend; the timer/abort
changes are behavior-preserving). Strengthened configStore seam test to assert
first-run seeding. typecheck clean, 1773 pass / 0 fail.

* refactor(ui): single-source the never-reject abort via safeAbort helper

Self-review: the hardened abort pattern (defer into .then + .catch so a host
transport that throws can't reject) was duplicated across postServerAbort and the
create-session supersede site — the exact drift the review flagged. Extract a
module-level safeAbort(sessionId) so both call sites share one hardened
implementation and can't diverge again. Behavior unchanged; reads aiTransport at
call time so a late override is honored.

* chore(ui): post-rebase version lockstep to 0.21.4

Rebased onto main (0.21.4, adds markdown math #878 + parser hardening). Bump
@plannotator/core + @plannotator/ui to 0.21.4 to stay in lockstep with the repo.
katex (main's math dep) merged into ui; typecheck clean, 1810 pass / 0 fail.

* docs(ui): consumer-lens handoff hardening + ADR 005

- HANDOFF.md: add supported-imports allowlist vs unsupported (hardcoded
  /api/*) list; document the annotation anchor schema, reattachment
  order, and untested stale-anchor degradation; state that the markdown
  editor cannot take CM6/Yjs extensions yet and the plan of record;
  note AI avoidability re-verified post-rebase; fix stale 0.21.3 ref.
- adr/decisions/005: record the publish-as-packages decision (packages
  over copy/vendor, core/ui split, seam-singleton pattern + SSR revisit
  condition, the law, lockstep publish model).

* fix(ui): make shipped source strict-TS clean for consumers + seam type barrel

Consumers compile the published TS source with their own compiler options,
and strict mode failed with 35 errors inside the package:
- settings.ts: satisfies SettingDef<unknown> is contravariantly illegal
  under strictFunctionTypes (33 errors) — use SettingDef<any>
- useDismissOnOutsideAndEscape: RefObject<HTMLElement> rejects React 19's
  useRef<T>(null) refs — widen to HTMLElement | null
- globals.d.ts: declare *.png / *.webp modules, referenced from each
  asset-importing component so any consumer program that includes one
  gets the ambient declarations

Also unscatter the seam contract types: configure.ts re-exports every
seam type next to configurePlannotatorUI, and ServerSyncFn is now
exported from config/index.ts (it was unreachable through the exports
map). Verified: standalone Vite consumer importing the full supported
surface passes tsc --noEmit under full strict (was 35 errors).

* fix(ui): keep KaTeX fonts out of published styles.css (back to ~187KB, was 1.6MB)

Main's math PR imports katex/dist/katex.min.css in theme.css; the
publish build (Vite lib mode) force-inlines all 60 KaTeX math fonts as
data URIs, ballooning styles.css to 1.6MB (977KB gzip) and breaking the
package's consumer-owns-fonts policy. Alias the katex stylesheet to an
empty stub in vite.css.config.ts only — theme.css stays untouched (no
rebase surface) and Plannotator's own apps, which import theme.css
directly, still bundle KaTeX as before. Hosts that render math load
katex.min.css themselves (bundler import, CDN tag, or self-hosted copy
per HANDOFF.md), which also gets them lazy font loading. Verified:
fresh build is 186.9KB / 30.8KB gzip with zero @font-face data URIs;
consumer vite build CSS drops 1.66MB -> 200KB.

* docs(ui): HANDOFF corrections from adversarial consumer review

- Math rendering section: KaTeX css/fonts excluded from styles.css by
  design; three one-time host setup options (self-hosted recommended,
  CDN tag, bundler import)
- styles.css size claim corrected (~187KB / ~31KB gzip) + strict-TS
  guarantee documented (verified against a standalone consumer)
- AI-avoidability claim made precise: configure.ts statically imports
  useAIChat for its setter; unused AI code tree-shakes to zero (bundle-
  verified) — the runtime claim holds, the static wording was wrong
- Loud warning on the loadSettingsFromBackend ordering footgun:
  configuring before hydration seeds generated defaults into the host
  backend and nothing re-runs hydration
- DraftTransport.load() tombstone-generation contract spelled out
- Seam-type barrel documented on the configure row; 'everything is
  importable' softened (some components/*.ts don't resolve via the
  *.tsx wildcard); stale diff stats refreshed

* docs(ui): math setup pointer in README + pnpm caveat on the katex bundler-import option

* fix(ui): lazy settings resolution — zero cookies on a configured host

The configStore resolved all settings eagerly in its constructor, at
module import — before a host's configurePlannotatorUI() could install
its StorageBackend — writing 17 plannotator-* cookies (including a
generated identity) onto the host origin. Resolution now runs lazily on
first settings access (get/set/init/loadFromBackend): by then the host
backend is live, so the initial reads AND default-seeding writes route
through it. A configured host gets zero cookies, ever.

Plannotator unchanged: same resolution, same cookie seeding, same
values — on first settings read (same page load) instead of at import.
New configStore.lazyInit.seam.test.ts proves the contract from a fresh
module graph; full suite + consumer strict tsc green.

* chore(ui): post-rebase version lockstep to 0.22.0

* fix(ui): round-2 review batch — dedupe asset declarations, CI seam tests, strict consumer gate, doc corrections

- components/types.d.ts: drop the *.png/*.webp declarations that
  globals.d.ts now owns — both shipping was a duplicate-identifier
  error for any consumer with skipLibCheck: false
- untrack packages/ui/styles.css (generated by prepack, gitignored;
  got scooped into the carve commit during the rebase by git add -A
  before the ignore entry existed in the replay)
- CI: the DOM test step now runs ALL packages/ui tests, so the seam
  contract tests (AI/draft/external-annotations/file-tree/inline-
  markdown) actually execute in CI instead of skipping
- new packages/ui/tsconfig.strict-consumer.json wired into root
  typecheck: type-checks the supported-import surface under full
  strict, so the consumer strict-TS guarantee can't silently rot
- HANDOFF: rot-proofed the diff stat, strict guarantee now cites the
  CI gate, CDN katex pinned-version wording, theme-vs-styles.css
  caveats (theme still imports KaTeX + needs Tailwind), Viewer
  required props, Yjs plan-of-record updated to the atomic-editor fork
- README: @source fallback wording (build entry isn't shipped)

* test(ui): make the lazy-resolution seam test deterministic

The test asserted lazy resolution on the module singleton and relied on
its test file getting a fresh module graph — an isolation assumption
that doesn't hold under all bun test orderings (CI failed with zero
observed reads because another file had already resolved the store).
Test the contract on a fresh instance instead: ConfigStore is exported
as @internal ConfigStoreForTest, the spy backend is installed before
construction, and the test asserts construction reads nothing while the
first get() resolves and seeds through the live backend. Deterministic
by construction.

* test(ui): poll for the debounced reconnect refetch instead of a fixed sleep

The reconnect-refresh assertion waited a fixed 150ms against the SSE
watcher's 120ms debounce — a 30ms margin that slower CI runners lose,
flaking 'refreshes after an SSE ready event from reconnect'. The
watched logic is unchanged (verified byte-identical to main's inline
version — the seam only relocated it into the default watchTrees and
added the onChange indirection). Poll for calls.length===2 up to 1s so
the pass/fail is hardware-independent.

* test(ui): poll the committed tree state, not the fetch call count

Prior fix polled calls.length===2, but the fetch call is counted one
tick before its result commits to React state — so the poll exited
early and the next assertion (dirs[0].tree === reconnectedTree) lost the
race on slow CI (toEqual failure). Poll on the committed tree itself,
which is exactly what the assertion checks: now the only way to fail is
a genuine no-refresh, not a timing margin.

* test(ui): give the reconnect-refetch poll a 10s ceiling + 20s test timeout

A CI runner was measured at 6x normal speed (1676ms for a ~275ms test),
blowing through the 1.5s poll ceiling before the 120ms debounce fired —
same commit passed on a faster runner. Raise the poll to ~10s and set an
explicit 20s test timeout (bun's 5s default would otherwise kill the
poll). Root cause is load, not logic: this timing-sensitive test only
started flaking when the CI DOM step was broadened to run the whole ui
suite in one process.

* ci: run the file-browser DOM test isolated; scope the DOM step to DOM files

Root-causes the intermittent 'refreshes after an SSE ready event from
reconnect' failure. The round-2 change ran the ENTIRE ui suite under
DOM_TESTS=1 to catch the seam contracts; that load intermittently
starved the test's 120ms real-timer debounce so the reconnect refetch
never fired (observed failing after a full 10s poll — not a margin
issue). The hook logic is byte-identical to main, and main runs this
test in its own process (green for months).

Fix at the CI layer, not the test: run useFileBrowser.test.tsx isolated
(matching main), and run the seam contracts + remaining DOM-gated tests
as an explicitly-scoped light batch. The test file is reverted to main
verbatim (today's timing-poll experiments dropped). Follow-up issue to
file: the underlying re-subscription race the load exposed.
2026-07-06 20:39:09 -07:00
renovate[bot] 903290e451 chore(deps): update github actions (#791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-28 11:08:06 -07:00
Michael Ramos 740d6fb2eb Add WebTUI agent panel to annotate mode (#941)
* feat(annotate): add WebTUI agent terminal

* feat(annotate): wire WebTUI agent into annotate UI

* docs: recap annotate agent terminal work

* fix(annotate): harden agent terminal runtime

* docs: add annotate agent terminal runtime ADRs

* fix(annotate): polish agent terminal integration

* fix(ui): preserve comment draft on Ask AI failure

* fix(annotate): address terminal review findings

* fix(annotate): harden agent terminal runtime fallback
2026-06-19 09:04:15 -07:00
Michael Ramos f24bb435d3 Fix live file tree startup latency (#935) 2026-06-19 08:01:03 -07:00
Juan Patten 1287bfa5d8 Disable bunfig autoload for release binaries (#937)
Add Bun's compile autoload guard to every release binary build so distributed executables do not inherit bunfig.toml from the caller's current directory.

Extend the binary smoke test to launch --help from a temporary directory with an invalid preload entry, matching the crash reproduction.

Update the Codex sandbox manual compile path so locally rebuilt binaries use the same guard.

Co-authored-by: Codex <codex@openai.com>
2026-06-18 22:28:33 -07:00
Michael Ramos d3fd62297b feat(readme): redesign README (#891)
* feat(readme): redesign README — hero banner, agent icons, simplified install

- New centered hero: banner, tagline, theme-aware agent icon row (picture/prefers-color-scheme), demo links
- Intro paragraph naming all nine supported agents
- Feature cards with screenshots; dedicated Annotate HTML Artifacts, Code review, and Sharing sections
- Install collapsed to one shared installer + per-agent table covering all nine agents, linking each app README
- New Try it section for manual invocation after install
- Fixed Codex plan-mode claim (supported via Stop hook), completed PLANNOTATOR_ORIGIN values, fixed demo links and typos
- Added Integrations, Remote/SSH, Security, and Configuration sections

* feat(readme): consolidate command examples into a Commands section

- Annotate HTML Artifacts is now screenshot-only
- Drop the duplicate code review screenshot (code-review-thumbnail.png removed)
- New Commands section: Annotate, Code review, and CLI groups

* fix(readme): point doc-review demo at Pi video, drop wrong code review demo link

* style(readme): plain-language pass

- No em dashes anywhere; split into sentences or swapped for colons/commas
- Rewrote the intro: integration with the agent session is now explicit (plugs into hooks/commands, feedback lands back in the live session)
- Varied repeated phrasing between the two feature cards
- Integration labels use colons instead of dashes

* style(readme): simplify feedback phrasing in hero and feature card

* feat(readme): add Plan mode note and sessions command to Commands section

* feat(readme): sharing beta link and Workspaces signup CTA badge

* style(readme): drop divider between feature table and HTML artifacts section

* style(readme): bump banner width 512 -> 640

* style(readme): fix agent icon sizing

- Crop Pi icon viewBox to glyph bounds (was 40% padding)
- Render Amp at 32px to offset its thin mark

* style(readme): bump Amp icon to 38px for visual parity

* fix(readme): strip em-based size attrs from Amp icon so it scales

The svg root had width/height of 1em plus an inline style, the only icon
in the set with relative sizing. SVG-as-image resolves em at a fixed 16px
and the drawing stayed pinned there regardless of the img height. Removed
the attrs, back to the uniform 28px row height.

* style(readme): width=100% on feature table images

* style(readme): cmux-style 40/60 feature table with middle-aligned text

* style(readme): split feature rows into separate tables so zigzag keeps 40/60

* style(readme): single feature table, text left 40 / image right 60, no zigzag

* style(readme): tighten intro and feature card copy

* style(readme): remove stray space before period

* style(readme): another round of copy edits

* chore: remove internal docs notes, move readme-assets to .github/assets

- Deleted docs/ (three internal handoff notes, nothing referenced them)
- README media now lives in .github/assets/ instead of a root-level folder
- All 18 asset links in README updated

* style(readme): split intro into two paragraphs

* feat(readme): live collaboration teaser in Sharing, tighten encryption copy

* style(readme): retitle Sharing & Multiplayer, rework CTA badge to banner palette with balloon

* feat(readme): updated annotate and review screenshots

* feat(readme): updated HTML artifact screenshot

* feat(readme): swap HTML artifact screenshot for v2

* feat(readme): restore share workflow sentence and Codex command note

* docs(agents): document bun link workflow for local plannotator command

* style(readme): trim Windows note from Codex command tip

* feat(readme): one-line AI features note under feature table
2026-06-11 13:10:50 -07:00
Michael Ramos b19505efd3 chore: remove the redundant /plannotator-status and /plannotator-archive commands (#873)
Two agent command-surface cleanups. Both remove only the command entry points; all underlying infrastructure stays.

1. /plannotator-status (Pi): removed — it echoed phase/plan-file/progress on
   demand, but that state is already shown ambiently (status bar + live
   checklist widget). The phase/checklist state machine is untouched.

2. /plannotator-archive (all agents): removed the command/skill entry points
   across every surface — Claude/Codex/Kiro skills, Pi, OpenCode (handler +
   dispatch + cli-bridge + embedded + stub), Droid, the Kiro agent prompt, all
   three installers, docs, marketing, and the CI deprecated-command guard. The
   installers also gained a stale-skill cleanup so upgraders drop a previously
   installed plannotator-archive skill.

Kept (infrastructure) — archive browsing stays available in-review via the
sidebar: the `plannotator archive` CLI subcommand (apps/hook/server), the
mode:"archive" server path + /api/archive endpoints, ArchiveBrowser/useArchive,
the sidebar Archive tab, sessions.ts "archive" mode, and ~/.plannotator/plans
storage.

Verified: bun test scripts/install.test.ts → 72 pass; pi-extension typecheck +
build:opencode pass; repo-wide residual scan clean; KEEP-set integrity
confirmed; one orphaned import (opencode commands.ts) caught in self-review and
removed.
2026-06-08 11:08:11 -07:00
Michael Ramos 26ca4e0275 Single-source skills (core/extra), replace Claude Code commands with skills, de-hardcode installers (#850)
* feat: single-source skills into core/extra, replace Claude commands with skills, de-hardcode installers

- apps/skills/core/{review,annotate,last,archive}: single authoritative
  source for the always-installed skills (archive is new); all carry
  disable-model-invocation + agents/openai.yaml sidecars
- apps/skills/extra/{compound,setup-goal,visual-explainer}: no longer
  default-installed (except Kiro); installers print an
  `npx skills add backnotprop/plannotator/apps/skills/extra` suggestion
- Claude Code: apps/hook/commands/ deleted, command heredocs removed;
  core skills in ~/.claude/skills are the slash commands now
- Installers: OpenCode/Gemini command files copied from an extended
  sparse checkout instead of heredocs; install.cmd gains the previously
  missing OpenCode command install; aggressive cleanup of legacy
  ~/.claude/commands and ~/.codex/skills artifacts
- Codex: core skills install to ~/.agents/skills (official path);
  ~/.codex/skills install removed
- Pi: extension no longer bundles skills; #670 settings filter removed

* fix: review findings — old-tag soft guards, cmd replace-not-merge, plugin-update hint, frontmatter test

- install.sh: a --version tag predating apps/skills/core no longer aborts
  the whole copy subshell (which also skipped OpenCode/Gemini commands);
  core skills now soft-skip with an accurate message, matching ps1/cmd
- install.sh: subshell failure message no longer claims "git required"
  when git was present (clone/network errors get their own wording)
- install.cmd: pre-remove skill dirs before xcopy so upgrades replace
  rather than merge (stale files from renamed/deleted skill files no
  longer linger; parity with sh/ps1)
- all installers + docs: tell upgraders to run /plugin marketplace update
  so the plugin's old namespaced plannotator:* commands disappear (#817)
- install.test.ts: assert every core SKILL.md sets
  disable-model-invocation: true — the load-bearing line that keeps core
  skills out of Pi's system prompt (#842 regression guard)

* test: pin old-tag soft-guard behavior, dedupe core-skill list in tests

* fix: interrogation review findings — cross-installer diagnostic parity

- install.ps1/install.cmd: emit the "predates the core/extra skill
  layout" diagnostic on old pinned tags instead of silently skipping
  core skills (parity with install.sh)
- install.ps1: clone/network failure no longer claims "git required"
  (git was already verified present); the outer catch now reports the
  actual exception
- install.sh: "Installed OpenCode/Gemini commands" echoes are guarded
  on the copy actually having a source, so old pinned tags don't print
  false success (ps1/cmd already gated this way)
- AGENTS.md: opencode-plugin commands/ comment now reflects all four
  command stubs
- install.test.ts: shared test asserts the soft-skip diagnostic exists
  in all three installers and pins ps1's honest failure wording

* fix: respect CODEX_HOME for Codex home directory (#852)

Codex stores config and state under $CODEX_HOME when set, falling back
to ~/.codex (developers.openai.com/codex/config-advanced). Plannotator
hardcoded ~/.codex in two places:

- runtime: codex-session.ts scanned ~/.codex/sessions for rollout
  files, so `plannotator last` failed with "No rendered assistant
  message found" when CODEX_HOME pointed elsewhere. Now resolved the
  same way copilot-session.ts handles COPILOT_HOME and session-log.ts
  handles CLAUDE_CONFIG_DIR.
- installers: detection, config.toml/hooks.json paths, manual-setup
  instructions, and the stale-skills cleanup now derive from
  CODEX_HOME in all three scripts.

Tests: codex-session.test.ts covers rollout discovery under a
CODEX_HOME temp dir; install.test.ts asserts all three installers
respect the variable and that the fallback is the only hardcoded
~/.codex path left in install.sh.

* fix: hard-fail skill install, guard command cleanup, one-time extras migration

External review triage on PR #850 surfaced two real installer issues:

P1 — commands deleted before replacement: the Claude command cleanup
ran before the git-gated skill install, so a missing git, a failed
clone, or an old pinned tag deleted the user's slash commands and
installed nothing (a regression — the old installer needed no git).
Now:
- missing git is a hard failure before anything is touched ("install
  git, then run this installer again")
- a failed fetch is a hard failure ("something went wrong — run the
  installer again") instead of a silent skip
- the legacy command cleanup runs AFTER the install and only removes a
  command file when its same-name replacement skill exists on disk
- old pinned tags keep the soft-skip (no deletion happens, commands
  survive, CI e2e against old tags stays green)

P2 — recurring extras deletion: the extras cleanup ran on every
invocation, deleting copies users reinstalled via the suggested
`npx skills add` (the copies are byte-identical, so only provenance
can tell them apart). The cleanup is now a one-time migration recorded
in a migrations ledger under the Plannotator data dir
(<PLANNOTATOR_DATA_DIR|~/.plannotator>/migrations/), the same
record-what-you-did pattern package managers use.

All three installers (sh/ps1/cmd) updated in parity; tests pin the
guard condition, the ledger gating, and the hard-fail messages.

* test: tripwire — install.cmd must never contain /dev/null redirects

* fix: every skill sets disable-model-invocation — no exceptions

Maintainer rule: all Plannotator skills are user-invoked, never
model-auto-invoked. setup-goal (missing since #665) and the three Kiro
skills now carry the flag. The frontmatter test scans every SKILL.md in
apps/skills/core, apps/skills/extra, and apps/kiro-cli/skills
dynamically — with a floor of 10 — so a future skill cannot ship
without it.

* docs: git is a hard installer requirement; clarify post-gate sections complete on re-run

* docs: align ps1/cmd comments with hard-fail semantics

* feat: guided install — extras opt-in via skills CLI, model-invocation picker

Interactive terminals get a two-question wizard on first run:
1. Install the extra skills? Yes delegates to `npx skills add
   backnotprop/plannotator/apps/skills/extra` (its UI picks the agents),
   wired to /dev/tty so piped curl|bash installs still work. Skipped
   when extras already exist on disk.
2. Make any skills callable by the model? Yes opens a space-toggle
   checkbox (sh/ps1) or numbered toggles (cmd), listing all skills if
   extras were chosen, core-only otherwise. Chosen skills get
   disable-model-invocation stripped from their INSTALLED copies and the
   Codex sidecar's allow_implicit_invocation flipped — re-applied every
   run since installs replace skill folders. Repo sources stay locked.

Answers persist to <data dir>/install-prefs (shared format across all
three installers) and re-runs reuse them silently; --reconfigure
re-opens the wizard. Automation is untouched: no terminal means no
prompts and today's defaults; --extras/--no-extras/--model-invocable/
--non-interactive give scripts explicit control.

* fix: self-review of guided install — cmd pipe expansion bug, flag/wizard interplay

- install.cmd: the checkbox preselection used `echo !var! | findstr` —
  each side of a cmd pipe runs in a child WITHOUT delayed expansion, so
  the saved choices passed through as literal !var! text and
  preselection never matched. Replaced with a substring-replace
  containment test (no pipe).
- all three: a wizard question whose answer was already provided by a
  CLI flag (--extras/--no-extras/--model-invocable) is no longer asked
  and then silently overridden — the flag pre-answers it.
- install.cmd: unknown-option usage line now lists the wizard flags.

* feat: guided install question 3 — install Glimpse (native window)

glimpseui (third-party npm package, PR #840) gives Plannotator a native
WebView window instead of a browser tab; the runtime already
auto-detects it on PATH, so a global install is all that's needed.

- Wizard asks "Install Glimpse?" (default yes) after the skills
  questions; skipped when glimpseui is already on PATH
- Yes runs `npm install -g glimpseui` (bun fallback on sh/ps1; printed
  instruction when neither exists) — wizard or explicit flag only,
  silent re-runs never install software
- --glimpse / --no-glimpse flags for automation; choice persisted to
  install-prefs like the others
- docs + tests updated (glimpse detection, install command, flags, and
  persist-condition assertions across all three installers)

* fix: self-review of Glimpse question — cmd bun fallback, stale usage text

* fix: merge-window hardening — guard Codex cleanup, remove old-installer junk dirs

plannotator.ai serves install.sh live from main (public/ symlink,
deployed on push), while the script fetches repo files at the LATEST
RELEASE TAG. Between merging the core/extra restructure and cutting the
release that ships it, the live script runs against the old-layout tag.
Two hazards in that window:

1. The Codex stale-skill cleanup removed working ~/.codex/skills with
   no successor installed (core skills soft-skip on old tags). Now the
   cleanup runs AFTER the install and removes a core skill only once
   its replacement exists in ~/.agents/skills — same guard the Claude
   command cleanup uses. The compound/setup-goal stale copies stay
   unconditional (never Codex's to begin with).

2. The reverse combo (cached OLD script + NEW release tag) wholesale-
   copies apps/skills/* and leaves junk core/ and extra/ directory
   copies in ~/.claude/skills. Never valid skill names — all three
   installers now remove them on every run.

* fix: glimpseui is a devDependency — consumers never use it from node_modules

PR #840 added glimpseui to dependencies in @plannotator/server and
@plannotator/pi-extension, but nothing imports it: both runtimes detect
the CLI on PATH (Bun.which / a manual PATH walk) and spawn it. The dep
only ever mattered in repo development, where `bun run` prepends
node_modules/.bin to PATH. For consumers it was inert download weight —
OpenCode plugin installs and `pi install` pulled a third-party package
that could never be detected (Pi's loader does not expose
node_modules/.bin; verified). Moved to devDependencies in both: dev
flows keep working, published packages stop shipping it. The sanctioned
end-user path is the guided installer's global `npm install -g
glimpseui`.

* fix: clean stale plugin command files from the installed plugin checkout (#817)

The installer already manages hooks.json inside
~/.claude/plugins/marketplaces/plannotator/apps/hook/, so the earlier
"don't reach into plugin storage" rationale for leaving the old
namespaced plannotator:* command files there was inconsistent. All
three installers now remove them — same replacement-skill guard as the
bare ~/.claude/commands cleanup — making the #817 duplicate menu
entries die on a single installer run + restart instead of waiting for
/plugin marketplace update. Hints/docs reworded accordingly.

* Revert "fix: clean stale plugin command files from the installed plugin checkout (#817)"

This reverts commit 3df37da92c.

* fix: Windows CI — assert the new no-commands contract; gate cmd wizard on a real console

The cmd e2e CI step still asserted the OLD contract (installer writes
~/.claude/commands/plannotator-*.md with the ! prefix). Commands are
dead; the step now guards the NEW contract: a fresh install must write
NO plannotator command files. The Gemini TOML assertions stay — they
now verify the verbatim checkout copy delivers intact files (the tag
the e2e pins, v0.17.1, contains apps/gemini/commands).

The failure also exposed that install.cmd ran the wizard on redirected
stdin (set /p falls through to defaults at EOF) — and Q3's default
being yes meant CI silently ran `npm install -g glimpseui`. cmd now
probes for a real console via `timeout /t 0` (errors when stdin is
redirected), matching sh's /dev/tty and ps1's IsInputRedirected gates:
no console, no wizard, no wizard-only installs.
2026-06-05 09:23:45 -07:00
Michael Ramos 8c947c5419 Add Amp plugin integration (#803)
* Add Amp plugin integration

* Use official Amp logo on landing page

* Tighten landing agent selector

* Default landing selector to Claude Code

* Stabilize server ready handoff test

* Create ready handoff directory before writing

* Stabilize server ready handoff tests

* Fix Amp command cancellation and cwd

* Preserve Plannotator browser handling for Amp

* Fix Amp review edge cases

* Add PowerShell installer smoke coverage
2026-05-27 21:49:30 -07:00
Michael Ramos 7db5e9b8d9 Fix Windows Pi shim spawning (#792)
* Fix Windows Pi shim spawning

* Fix Pi smoke process cleanup

* Kill Windows Pi process trees
2026-05-25 11:50:50 -07:00
Michael Ramos 93e8b5dfcf Add Plannotator Workspaces waitlist (page + worker + CI) (#771)
* Add Plannotator Workspaces waitlist (page + worker + CI)

Marketing
- New /workspaces/ page: hero+form sibling layout on desktop; mobile
  reorders to hero -> form -> info -> banners so the CTA is reachable
  on the first scroll. Uses the marketing site tokens/fonts and the
  existing border-x editorial column. Theme-aware via the cookie.
- Adds a "head" slot to Base.astro for per-page <head> additions.
- Nav: new "Workspaces" link with a "soon" pill.
- Landing: small Workspaces CTA band before the final install CTA.

Waitlist worker (apps/waitlist-service)
- Cloudflare Worker + D1 (SQLite). Mirrors the paste-service layout.
- Endpoints: POST /signup (Turnstile-protected, rate-limited, honeypot),
  GET /health, GET /admin/count, GET /admin/list gated by Bearer
  ADMIN_TOKEN.
- Validation: required email + company + team_size; optional note +
  is_contributor; honeypot field; freemail-aware company auto-inference.
- D1 schema includes a non-destructive 001 migration that adds note
  and is_contributor columns. 19/19 validation unit tests pass; tsc
  clean.

CI
- Adds a deploy-waitlist job to .github/workflows/deploy.yml mirroring
  paste-service. Uses the existing CLOUDFLARE_API_TOKEN and
  CLOUDFLARE_ACCOUNT_ID secrets, so no new repo config is required.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Force light mode on /workspaces/

The page is designed and signed off in light. Override the saved theme
cookie via a head-slot inline script that adds the `.light` class
before paint. Base.astro's cookie reader only adds the class (never
removes), so any saved-dark preference can't undo this.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-22 15:01:43 -07:00
Michael Ramos 3f91cd7c0e feat: add --version / -v flag to CLI (#725)
Injects the version from package.json at compile time via Bun's --define
so compiled binaries report the correct version (e.g. `plannotator 0.19.16`).
Uncompiled dev runs fall back to `plannotator dev`.
2026-05-13 16:22:13 -07:00
Leonardo Reis 6e3efe8e7d Update Codex hooks feature flag (#708) 2026-05-11 21:02:19 -07:00
Michael Ramos d0e13d7b07 fix: scope id-token:write to only AWS OIDC deploy jobs (#706)
The workflow-level permissions block granted id-token:write to every
job, including detect-changes which only reads git diffs. Move the
OIDC capability to only deploy-marketing and deploy-portal (which
need it for aws-actions/configure-aws-credentials). deploy-paste
uses a Cloudflare API token and doesn't need OIDC at all.
2026-05-11 20:36:29 -07:00
Andrei Ivanov a22a744749 Add Codex Stop-hook plan review (#577)
* feat: add codex stop hook plan review

* Install Codex plan review hooks

* Remove Codex manual test screenshots

* Update Codex plan mode docs

* Tighten Codex release readiness

* Preserve custom Codex hook wrappers

* ci: smoke test release artifacts

* ci: reduce release smoke flake risk

* fix: keep Codex last-message extraction to output text

* ci: poll release smoke servers on loopback

* ci: skip macOS release smoke jobs

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-05-02 13:21:42 -07:00
Michael Ramos 289a2aa494 chore: bump version to 0.19.0
Bump version across all 7 package/plugin manifests. Also refresh
AGENTS.md (new Block types from #597, Code Tour endpoints from #569),
prune removed flags from the Pi README (--plan-file, /plannotator-set-file
from #595), and pin Renovate off bun-version bumps to keep the macOS
codesign hotfix from v0.17.9 in place.

For provenance purposes, this commit was AI assisted.
2026-04-21 19:36:02 -07:00
Michael Ramos 38ca19ec1a ci: add smoke tests for compiled binaries in release pipeline (#555)
Runs three checks on the linux-x64 binary after compilation, before
artifact upload: --help (binary loads), review server startup (full
import chain + bundled HTML + git integration + HTTP binding), and
annotate server startup. Catches the class of build breakage where
compilation succeeds but the binary crashes on launch.

For provenance purposes, this commit was AI assisted.
2026-04-13 07:34:13 -07:00
Michael Ramos ba80c9f172 fix: disable Renovate automerge to prevent repeat of #548 incident
Renovate's Immortal mode recreates closed PRs and automerges them
immediately. This is how #548 (Bun 1.3.12) landed on main despite
being explicitly closed. Disabling automerge ensures all dependency
PRs require human review.

For provenance purposes, this commit was AI assisted.
2026-04-12 20:46:15 -07:00
Michael Ramos 04848af686 chore: enforce 7-day minimum release age for supply chain hardening (#550)
- Add minimumReleaseAge (7 days) + internalChecksFilter (strict) to Renovate config
- Disable Renovate automerge (was the root cause of #548 re-merging overnight)
- Add minimumReleaseAge (604800s) to bunfig.toml for bun install/update
- Exclude @pierre/diffs from age gate (current ^1.1.12 can't resolve under 7-day gate)
- Bump audited deps: diff 8.0.4, claude-agent-sdk 0.2.92, codex-sdk 0.118.0, marked 17.0.6, @types/node 25.5.2
- Add /update-deps skill for supply chain audited dependency updates

For provenance purposes, this commit was AI assisted.
2026-04-12 20:45:02 -07:00
Michael Ramos d92564fe1c Revert "chore(deps): update github actions (#548)"
This reverts commit 66862a8131.
2026-04-12 20:43:17 -07:00
renovate[bot] 66862a8131 chore(deps): update github actions (#548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 02:29:26 +00:00
Michael Ramos 3f3b0f63cc fix: pin Bun to 1.3.11 to fix macOS binary codesign regression (Bun 1.3.12)
Bun 1.3.12 introduced a regression where cross-compiled macOS binaries
lose their ad-hoc linker signature, causing macOS Sequoia to SIGKILL
them immediately. Pin bun-version to 1.3.11 across all three workflow
jobs (test, build, npm-publish) until the Bun issue is resolved.

Fixes #541

Bumps version to 0.17.9.
2026-04-11 12:00:07 -07:00
Michael Ramos ed254cf3bd Supply-chain hardening: version pinning, SLSA attestations, fix #506 (#512)
* feat(install): supply-chain hardening (#507) + fix Gemini crash on Windows (#506)

Closes #507 items 1-2 and fixes #506.

Issue #507 asked for version-pinned installs from a trusted source, immutable
releases, and build-provenance attestations. This commit ships:

- `--version v0.X.Y` / positional / `-Version` flag across all three installers
  (bash, PowerShell, cmd) so users can pin to reviewed versions. Default stays
  `latest` — every existing `curl | bash` / `irm | iex` invocation works
  unchanged. install.cmd gained `--version` as an alias to its existing
  positional form for surface-area consistency.

- SLSA build provenance attestations via `actions/attest-build-provenance`
  (SHA-pinned to v4.1.0) in release.yml. Covers all 10 compiled binaries
  (5 plannotator + 5 paste-service). Top-level workflow permissions tightened
  from `contents: write` to `contents: read`, with per-job overrides where
  needed. Attestation step is gated on tag pushes so PR dry-runs don't
  pollute Sigstore's transparency log.

- Opt-in provenance verification in all three installers, resolved via a
  three-layer precedence ladder:
    1. CLI flag   `--verify-attestation` / `-VerifyAttestation`
    2. Env var    `PLANNOTATOR_VERIFY_ATTESTATION=1`
    3. Config     `~/.plannotator/config.json` → `verifyAttestation: true`
    4. Default    off
  Off-by-default matches every major ecosystem installer (rustup, brew, bun,
  deno, helm) and avoids a UX failure for the majority of users who don't
  have `gh` installed or authenticated. Security-conscious users get three
  ergonomic opt-in paths. When enabled, the installer hard-fails if `gh` is
  missing so opt-in is never silently skipped.

- `PlannotatorConfig.verifyAttestation?: boolean` added to
  `packages/shared/config.ts`. Pure additive schema change; no runtime
  consumer exists (the field is read only by the install scripts). No UI
  surface — this is an OS-level power-user knob only.

- Documentation rewritten in README.md, apps/hook/README.md, and the
  marketing install page with both pinned-version examples and a "Verifying
  your install" section covering manual `gh attestation verify` and offline
  `cosign verify-blob` paths.

- Release skill updated to note that the release pipeline now emits SLSA
  attestations and (post-merge) GitHub Immutable Releases will be enabled
  on the repo.

Issue #506 (install.cmd crash on Windows when Gemini is present):

Root cause was cmd.exe's `setlocal enabledelayedexpansion` eating `!` chars
in the embedded `node -e "..."` Gemini settings merge script. Cmd's Phase 2
parser treated `!s.hooks)s.hooks={};if(!` as a variable expansion, corrupting
the JS before node saw it. Fixed by rewriting the JS to use the `||` idiom
(`s.hooks = s.hooks || {}`) which contains no `!` characters — semantically
identical, and sidesteps cmd's parser entirely with no escape gymnastics and
no new dependencies.

Added regression test in scripts/install.test.ts that asserts the `||` form
is present and `if(!s.hooks)` is absent, so re-introducing the bug would
fail CI.

Test suite expanded from 23 to 29 tests covering:
- Gemini #506 regression
- Three-layer opt-in wiring assertions for all three installers
- install.sh guard check (the executable `gh attestation verify` call must
  live behind the `verify_attestation -eq 1` gate, so the default path never
  invokes gh)
- PlannotatorConfig schema assertion

Out of scope (tracked):
- Immutable Releases toggle in repo Settings (one-click, post-merge).
- SLSA Build L3 via `slsa-framework/slsa-github-generator` (requires
  restructuring release.yml around a reusable workflow).
- Issue #507 item 3: UI update-cooldown setting.

For provenance purposes, this commit was AI assisted.

* fix(install): address PR #512 review feedback

- docs: drop broken `cosign verify-blob` example. Verified against
  Sigstore's docs that `cosign verify-blob` requires `--bundle` (or
  `--signature` + `--certificate`); the shipped command provided neither
  and could not have worked. Replaced with the canonical
  `gh attestation verify --repo` flow (gh + auth required, but optional —
  only needed if a user wants to manually audit provenance) and a link
  to GitHub's offline-verification docs for advanced workflows. Applied
  to README.md, apps/hook/README.md, and the marketing install page.

- docs: list per-platform binary paths in the manual verification
  examples. The previous snippets hardcoded `~/.local/bin/plannotator`
  even though install.ps1 writes to `%LOCALAPPDATA%\plannotator\` and
  install.cmd writes to `%USERPROFILE%\.local\bin\` — Windows users
  copying the snippet got file-not-found instead of a verification
  result.

- install.sh: capture and surface `gh attestation verify` stderr on
  failure instead of redirecting to /dev/null. Diagnosability matches
  install.ps1, which already prints `$verifyOutput` on failure. The
  most common failure mode (`gh auth login` not run) is now
  immediately actionable instead of presenting as a generic
  "verification failed" error.

- install.cmd: reject any unknown dash-prefixed token before the
  positional fall-through. A typoed `--verify-attesttion` no longer
  becomes `VERSION=--verify-attesttion` and 404s on a nonsensical
  download URL — it fails fast with `Unknown option:` and a usage
  hint. install.sh and install.ps1 already had equivalent guards
  (case `-*)` arm and PowerShell's strict param block respectively).

All 29 install tests still pass.

For provenance purposes, this commit was AI assisted.

* test(ci): add Windows integration job for install.cmd

Address the reviewer gap flagged in PR #512: the unit tests in
scripts/install.test.ts only do file-content string matching on Linux
and never execute cmd.exe, so the #506 fix (rewriting the embedded
`node -e` Gemini merge to use `x = x || {}` instead of `if(!x)x=...`)
was shipped without any runtime coverage. The previous CI only had
ubuntu-latest runners.

New `install-cmd-windows` job on `windows-latest`:

1. Seeds a fake `~/.gemini/settings.json` with a pre-existing non-
   plannotator hook plus unrelated top-level keys (theme, general).
   The fixture mirrors the shape of a real Gemini settings.json but
   uses only obviously-fake values and contains no secrets.

2. Runs `scripts\install.cmd v0.17.1 --skip-attestation` end-to-end
   through real cmd.exe. This exercises the parser under
   `enabledelayedexpansion`, the embedded `node -e` merge script,
   and the full install flow.

3. Parses the post-install settings.json with PowerShell and asserts:
   - The plannotator hook was added to hooks.BeforeTool.
   - The pre-existing fixture hook is still present (regression guard
     for the original #506 bug, where cmd ate the `!` in
     `if(!s.hooks.BeforeTool)s.hooks.BeforeTool=[]` and wiped existing
     arrays).
   - Unrelated top-level keys (`theme`, `general.ciFixtureSentinel`)
     survived the merge untouched.

4. Separately exercises the new unknown-flag rejection added in the
   previous commit: invokes `install.cmd --verify-attesttion` (typo)
   via Start-Process and asserts exit code != 0. Before the review
   fix this would have silently set `VERSION=--verify-attesttion`
   and 404'd on the download.

The job runs in parallel with the existing ubuntu `test` job (no
deps, independent runner). Uses the v0.17.1 release as the binary
source — that release is pre-PR, so the test is stable against
release drift and is testing install.cmd's CODE, not any specific
binary.

This closes the CI gap where install.cmd had effectively zero
runtime coverage and the original #506 bug could have recurred
without anyone noticing until a user reported it.

For provenance purposes, this commit was AI assisted.

* fix(install.cmd): capture gh stderr on failure (consistency with install.sh)

Self-review catch: the PR #512 reviewer flagged install.sh for
redirecting `gh attestation verify` output to /dev/null, which
swallowed actionable error messages (auth missing, network issue,
attestation not yet propagated) behind a generic "verification
failed" line. I fixed install.sh in the previous review-fix commit
but missed that install.cmd had the exact same pattern:

    gh attestation verify "!TEMP_FILE!" --repo !REPO! >/dev/null 2>&1

Same bug, same consequence, same fix. cmd doesn't have bash's
`$(cmd)` or PowerShell's `& cmd 2>&1` output capture, so we redirect
to a temp file and `type` it on failure, then clean up in both
branches:

    gh attestation verify ... > "%TEMP%\gh-output.txt" 2>&1
    if !ERRORLEVEL! neq 0 (
        type "%TEMP%\gh-output.txt" >&2
        del "%TEMP%\gh-output.txt"
        echo Attestation verification failed! >&2
        ...
    )
    del "%TEMP%\gh-output.txt"

All three installers now surface gh's actual error message on
failure, which makes the most common failure mode (`gh auth login`
not run) immediately diagnosable on every platform.

Note: this code path is not exercised by the new Windows CI
integration job because that job passes `--skip-attestation`, and
exercising the gh verify path would require an attestation for
v0.17.1 to exist — which it doesn't, since v0.17.1 was released
before this PR added the attestation step. The fix will first
become CI-testable against the first post-merge release that
carries a provenance bundle.

For provenance purposes, this commit was AI assisted.

* fix(install): address second review pass on PR #512

Five findings, all verified against the actual code:

- **AGENTS.md env var table** (reviewer: "CLAUDE.md"; it's a symlink to
  AGENTS.md) was missing PLANNOTATOR_VERIFY_ATTESTATION. Added with an
  explicit note that it's read by the install scripts only, not the
  runtime binary — since every other entry in that table is a runtime
  env var, the distinction matters.

- **install.cmd unknown-flag guard metacharacter injection.** The
  previous guard ran `echo %~1 | findstr /b "[-]"`, where %~1 is
  unquoted before the pipe. A user passing `install.cmd "--bad&calc"`
  would have cmd expand %~1 to `--bad&calc`, see the `&` as a command
  separator, and execute `calc` as a side effect before the flag
  check. Not a remote exploit (user already has shell exec), but a
  defensive coding weakness in supply-chain hardening code.

  Replaced with a variable-assigned substring test using delayed
  expansion — `set "CURRENT_ARG=%~1"` preserves metacharacters
  literally inside the `"..."` set syntax, and `!CURRENT_ARG:~0,1!`
  extracts the first char without any subprocess. This also fixes the
  same bug in the error-message echo, which previously echoed the
  unquoted `%~1` and re-triggered metacharacter interpretation in the
  error path itself. The echo now uses `"%~1"`.

  Note: the reviewer's proposed one-liner `if "%~1:~0,1%"=="-"` was
  syntactically invalid — cmd's `:~start,length` substring modifier
  does not work on positional parameters, only on regular variables.
  A variable assignment is necessary.

- **install.cmd unquoted --repo argument** in the `gh attestation
  verify` call. TEMP_FILE was quoted but REPO was not. REPO is
  hardcoded to `backnotprop/plannotator` so not exploitable, but
  inconsistent with install.sh (which quotes `"$REPO"`). One-char
  fix: `--repo "!REPO!"`.

- **test.yml intentional-typo drift hazard.** The unknown-flag
  regression test invokes `install.cmd --verify-attesttion`
  (missing an `a`). The only assertion was `$p.ExitCode -eq 0`. If
  a future typo-sweep "fixes" the misspelling to the valid
  `--verify-attestation`, install.cmd would accept the flag,
  proceed to download the latest release, run `gh attestation
  verify` against it, and — because v0.17.1 pre-dates the
  attestation step — fail with a different non-zero exit. Both
  paths exit 1, so the test would silently drift from "guard
  works" to "gh attestation verify fails on pre-PR release"
  without anyone noticing.

  Two-part fix:
    1. Explicit comment marking the misspelling as intentional
       ("do not correct during a typo sweep").
    2. Redirect stderr to a temp file and assert it contains
       "Unknown option:" — the actual discriminator between the
       guard triggering and any other failure mode that happens
       to exit non-zero.

- **README.md verification block was too long** for the main
  README. Trimmed from ~33 lines (intro + 3 code blocks + opt-in
  mechanisms + precedence notes) to a single sentence that links
  to the canonical marketing installation docs where the full
  content already lived. Same treatment applied to
  apps/hook/README.md for consistency. The marketing docs are
  unchanged and remain the single source of truth for
  verification workflows.

All 29 install tests still pass. The Windows CI integration job's
new stderr assertion will exercise the harder guard on the next
push.

For provenance purposes, this commit was AI assisted.

* fix(install): tighten attestation verify with --source-ref and --signer-workflow

Addresses PR #512 review cycle 3 finding that repo-scoped verification
alone doesn't bind the downloaded binary to the specific tag the user
requested. A misattached release asset would pass the old check
because the wrong binary would still carry a valid attestation for
its own (wrong) commit.

GitHub's own docs explicitly recommend both constraints:

  "The more precisely you specify the identity, the more control you
   will have over the security guarantees. Ideally, the path of the
   signer workflow is also validated."
  — https://cli.github.com/manual/gh_attestation_verify

All three installers now pass:

  --source-ref "refs/tags/<requested-tag>"
      Enforces that the git ref the attestation was produced from
      matches the tag the installer asked for. Closes the
      misattached-asset gap.

  --signer-workflow backnotprop/plannotator/.github/workflows/release.yml
      Enforces that the attestation was signed by our release workflow
      file specifically, not any workflow in the repo. GitHub treats
      this flag as a regex (see cli/cli#9507) so future refactors can
      broaden the match without breaking version-pinned installs to
      historical releases.

Also addresses the sibling finding that install.cmd used a fixed
%TEMP%\gh-output.txt temp filename while the rest of the script
uses %RANDOM% for uniqueness. Renamed to
%TEMP%\plannotator-gh-%RANDOM%.txt, matching the established pattern
and removing a theoretical race between concurrent invocations.

New test in install.test.ts asserts all three installers pass
--source-ref and --signer-workflow with the expected values. 30
tests pass.

For provenance purposes, this commit was AI assisted.

* fix(install): address PR #512 review cycle 4 (parser edges, ps1 stream, docs)

Five findings, all verified against actual code. One bonus fix in
install.sh for a sibling bug the reviewer flagged only in install.cmd.

install.ps1: `Write-Host $verifyOutput` on attestation failure wrote
gh's diagnostic to PowerShell's Information stream (stream 6), which
is silently dropped when CI pipelines capture stderr. Replaced with
`[Console]::Error.WriteLine($verifyOutput)` — direct stderr handle,
matches the behavior of `echo ... >&2` in install.sh and `type ...
>&2` in install.cmd.

install.sh + install.cmd: `--version --some-other-flag` used to set
VERSION to the flag name (e.g. VERSION=--verify-attestation), which
then tried to download tag `v--verify-attestation` and 404'd. The
empty-check on `$2`/`%~2` didn't catch dash-prefixed values. Added
an explicit dash-prefix check that returns a clean "--version
requires a tag value, got flag: X" error instead of degrading into
a cryptic download failure.

install.sh + install.cmd: mixing `--version v1.0.0 stray` used to
silently overwrite VERSION with "stray" because the positional
branch unconditionally assigned VERSION=$1. Added a VERSION_EXPLICIT
sentinel that's set to 1 when --version is seen, and the positional
branch now errors with "Unexpected positional argument: X (version
already set)" when it sees a token while the sentinel is set. Same
sentinel is also set by the positional branch itself, so passing
two positional version tokens also errors out cleanly.

Note: the reviewer flagged the positional-overwrite bug only in
install.cmd, but install.sh had the identical issue (same
unconditional `VERSION="$1"` in the `*)` arm) and the same dash-
check gap in both its `--version <val>` and `--version=<val>`
branches. Fixing both installers symmetrically — inconsistency
here would just trigger another review round.

marketing/installation.md: the "Verifying your install" prose
promised a "cryptographic link to the exact commit and workflow
run," but the example commands only passed `--repo`, which just
proves the artifact came from some workflow in our repository.
The installer now constrains with `--source-ref` and
`--signer-workflow` after review cycle 3, so the docs were out of
sync with the actual installer behavior. Updated all three
platform examples (bash, pwsh, cmd) to include the tighter flags
with a placeholder (`vX.Y.Z`) and a sentence explaining what the
extra flags actually buy the user. README.md and
apps/hook/README.md are already link-only after cycle 2 and don't
need changes.

install.test.ts: two new tests.
  - Regression guard asserts install.sh and install.cmd contain the
    VERSION_EXPLICIT sentinel, the dash-prefix error message, and
    the "Unexpected positional argument" guard. Anyone removing
    any of these in a future cleanup would fail CI.
  - Regression guard asserts install.ps1 uses
    [Console]::Error.WriteLine and does NOT use Write-Host for
    verifyOutput.

32 tests pass (was 30). Smoke-tested install.sh with
`--version --verify-attestation` and `--version v1.0.0 stray` —
both now exit 1 with clean usage errors instead of silent
download failures.

For provenance purposes, this commit was AI assisted.

* fix(install): address PR #512 review cycle 5

Five code/doc fixes, all verified against actual code. Finding 1 from
the review (opt-in verification unusable until a post-merge release is
cut) is correct but not actionable — it's inherent to how SLSA
attestations work and the only "fix" is timing + release cadence.

install.ps1: `[Console]::Error.WriteLine($verifyOutput)` silently
converted multi-line gh output to the literal string "System.Object[]"
— the opposite of what cycle 4's Write-Host fix was supposed to do.
`& gh ... 2>&1` captures multi-line output as an object[] array;
passing the array directly to [Console]::Error.WriteLine binds to the
WriteLine(object) overload and calls ToString() on the array. Fixed by
piping through Out-String first (and TrimEnd to drop the trailing
newline it adds). Confirmed against Sigstore/PowerShell docs and the
Delft Stack array-to-string guide.

install.cmd: replaced `echo !TAG! | findstr /b "v"` with a substring
test `if not "!TAG:~0,1!"=="v"`. Same metacharacter-injection class as
the parser bug fixed in cycle 2 — piping an unquoted expanded variable
re-exposes cmd's & | > < operators in the value before the pipe runs.
Inconsistent to leave this one instance using the unsafe pattern when
every other comparable check in the script uses the substring idiom.

install.cmd: randomized the two remaining deterministic temp file
paths — %TEMP%\release.json and %TEMP%\plannotator-<tag>.exe — to
match the %RANDOM% pattern already used by GH_OUTPUT. Closes two
gaps at once: concurrent-invocation collisions (real for automated
upgrade tooling) and same-user symlink pre-placement (the SHA256
check passes on authentic content, but a symlink at the predictable
path would redirect where curl writes the binary before the install
move runs).

All three installers: reject --verify-attestation and
--skip-attestation together as mutually exclusive instead of trying
to guess which the user meant. Previously install.sh/cmd took last-
on-command-line wins and install.ps1 took a fixed-priority Skip-
always-wins (documented but inconsistent with the other two). No
sane user passes both flags — fast-failing with a clear "mutually
exclusive" error is better than silently picking one and hoping it
matches intent. Guards live inline in both arms of the bash/cmd
parsers and right after the PowerShell param block.

test.yml: added a comment block on the install.cmd v0.17.1 pin
explaining why that version was chosen, why `latest` isn't used,
what the prerequisites are for bumping it, and what failure mode
to expect if the pinned release is ever removed. No behavior
change — the existing pin stays. Addresses the reviewer's concern
that the dependency was undocumented.

install.test.ts: four new regression guards.
  - Asserts install.ps1 uses Out-String (not bare [Console] call
    on raw $verifyOutput) for multi-line gh output
  - Asserts all three installers reject the --verify+--skip combo
    with a "mutually exclusive" error and install.ps1 has the
    `$VerifyAttestation -and $SkipAttestation` guard
  - Asserts install.cmd uses randomized temp paths for release.json
    and the binary download, and that the old deterministic paths
    are gone
  - Asserts install.cmd uses the substring test for v-prefix
    normalization and does not pipe echo|findstr for that check

35 install tests pass (was 32). Smoke-tested the bash mutex guard
in both orders — both fail fast with "mutually exclusive" and
exit 1 regardless of which flag appears first.

For provenance purposes, this commit was AI assisted.

* fix(install.cmd): randomize checksum temp path + tighten test assertions

Self-review catch on top of the cycle 5 commit:

- `%TEMP%\checksum.txt` (lines 164/172/174) was still a fixed
  predictable path. Same concurrency + symlink-pre-placement class
  as release.json and TEMP_FILE that cycle 5 fixed. Inconsistent to
  fix two of three and leave the third. Renamed to
  `%TEMP%\plannotator-checksum-%RANDOM%.txt` matching the established
  pattern. The reviewer didn't flag this one — I missed it during
  the cycle 5 sweep.

- Tightened the Out-String regression test from a weak "Out-String
  appears somewhere in the file" check to a regex matching the
  specific `$verifyOutput | Out-String` wiring. Previous assertion
  would have passed even if some future bug accidentally wrapped
  the mutex-guard string literal in Out-String while leaving
  $verifyOutput unprotected.

- Expanded the randomized-temp-paths test to cover all four curl
  download targets (release.json, binary, checksum sidecar, gh
  output capture) rather than the two originally in scope, and to
  assert the old fixed paths (including checksum.txt) are gone.

35 tests still pass.

For provenance purposes, this commit was AI assisted.

* fix(install.cmd): escape ! in Claude Code slash command files

Pre-existing bug flagged in PR #512 review cycle 6. install.cmd writes
the three Claude Code slash command files (plannotator-review.md,
plannotator-annotate.md, plannotator-last.md) via `echo` lines inside
`setlocal enabledelayedexpansion`. cmd.exe's Phase 2 parser strips
unmatched `!` characters — so lines like:

    echo !`plannotator review $ARGUMENTS`

ended up in the written file as:

    `plannotator review $ARGUMENTS`

without the leading `!`. The `!` prefix is what tells Claude Code to
execute the backtick block as a shell command; without it, Claude Code
renders the line as inline markdown code and the slash command is a
silent no-op. The install appeared to succeed, but every Windows cmd
user got three broken slash command files.

install.sh (single-quoted heredocs) and install.ps1 (single-quoted
here-strings) write the `!` correctly because their respective
literal-string idioms bypass shell expansion entirely. install.cmd
has no single-quote-literal equivalent — its escape hatch is `^!`.
The Gemini section of install.cmd (lines 482, 495) already uses
`^!` correctly; the Claude Code section didn't until now.

Fix: three characters — `echo !` → `echo ^!` on lines 334, 351, 368.
Brings install.cmd into parity with the other two installers. No
divergence introduced; existing divergence removed.

Two regression guards added:

  - Unit test in install.test.ts asserts install.cmd contains the
    escaped form for all three command files and does not contain
    the unescaped form.

  - New step in the Windows CI integration job reads back each
    generated .md file from %USERPROFILE%\.claude\commands\ and
    asserts it contains the literal `!`\`plannotator` prefix. Catches
    the bug at the actual file-write level on a real Windows runner,
    not just via source-code grep.

36 install tests pass (was 35).

Note: the broader architectural issue — all three installers carry
hand-typed duplicates of command content that already lives at
apps/hook/commands/*.md — is deferred to a follow-up issue. The
cmd bug is the visibly-broken symptom; the deduplication is the
long-term fix.

For provenance purposes, this commit was AI assisted.

* fix(install.cmd): double-caret escape for ! in slash command echoes

The previous fix used `echo ^!` for the three Claude Code slash command
files. The Windows CI integration job's new file-readback assertion
proved this is wrong: the generated plannotator-review.md still landed
with no `!` prefix, making the slash command a silent no-op as before.

Root cause: cmd has two escape phases under enabledelayedexpansion.
  Phase 1 (parse time): `^` escapes the next char. `^!` → `!`.
                        The caret is consumed.
  Phase 2 (delayed expansion): the remaining bare `!` is an unmatched
                        variable reference and gets stripped.
Single `^!` dies in Phase 2 because Phase 1 already ate the caret.
Double `^^!` survives: Phase 1 reduces `^^` to `^` (leaving `^!`),
Phase 2 treats the caret as an escape for `!` and emits a literal.

Cycle 6's fix got the direction right but the arithmetic wrong. The
new file-readback assertion in test.yml caught it on the first real CI
run, which is exactly why that assertion was added.

Also fixes the Gemini slash command echoes (lines 482, 495) which used
the identical incorrect `^!` pattern. The review comment flagged
Gemini as "correct" based on source-reading alone; there was never
any CI coverage for the Gemini file contents, and the Gemini section
was silently broken for the same reason. Both sections now use `^^!`.

Unit test updated to assert the double-caret form on all five echo
lines (three Claude Code, two Gemini) and reject both the unescaped
and single-caret variants.

For provenance purposes, this commit was AI assisted.

* fix(install.ps1): fall back to x64 on ARM64 Windows instead of 404ing

Pre-existing bug surfaced in PR #512 review cycle 7.

install.ps1 detected ARM64 correctly and set $arch=arm64, constructing
a URL for plannotator-win32-arm64.exe — which doesn't exist in any
release. The release pipeline only builds bun-windows-x64 (release.yml
line 88), so there is no native ARM64 Windows binary to download.
With $ErrorActionPreference=Stop set at the top of the script, the
resulting 404 on Invoke-WebRequest threw a terminating error and the
install aborted with a stack trace. ARM64 Windows PowerShell users
could not install plannotator at all.

Meanwhile install.cmd, which hardcodes PLATFORM=win32-x64 and lets
ARM64 hosts pass the arch check, silently installs the x64 binary
and relies on Windows 11's x86-64 emulation layer to run it. This is
accidentally the useful behavior — imperfect, but the user gets a
working install instead of a hard failure.

This commit brings install.ps1 into parity with install.cmd's
(accidentally correct) behavior:

- On 64-bit Windows, $arch is unconditionally "x64" — no more
  branch for arm64 that would download a nonexistent binary.
- When PROCESSOR_ARCHITECTURE == ARM64, Write-Host prints a notice
  telling the user they're getting the x64 binary via Windows
  emulation so the behavior isn't silent.
- 32-bit Windows still errors out (unchanged).

Both Windows installer paths now produce a working install on both
x64 and ARM64 hosts. No release pipeline changes. No new binaries.

The test `detects ARM64 architecture` used to be a weak string-
presence check that passed whether the ARM64 branch selected arm64
or x64. Rewrote it to assert the actual new contract: ARM64 is
detected (for the notice), $arch is hardcoded to "x64", and the
previous `{ "arm64" }` branch is gone so the regression can't
silently return.

Native ARM64 Windows builds tracked as a follow-up — requires
verifying Bun's Windows ARM64 target support and adding
bun-windows-arm64 to the release matrix.

For provenance purposes, this commit was AI assisted.

* fix(install): pre-flight MIN_ATTESTED_VERSION guard + placeholder docs

PR #512 cycle 7 review surfaced that opt-in provenance verification was
dead-on-arrival for the window between this PR merging and the first
post-merge release:

  - The docs showed `--version v0.17.1` as the pinned example. v0.17.1
    was cut before this PR added attestation generation to release.yml,
    so any user copy-pasting the example AND enabling verification
    would hit a cryptic `gh: no attestations found` error and a hard
    install failure.

  - Default installs with verification enabled (via flag, env var, or
    config file) resolve `latest` to v0.17.1 and hit the same failure
    with no user-visible pinned version to "blame."

Medium fix (better error message) was dismissed as lipstick — the
install still fails, just with nicer wording. This is the maximum fix
that actually prevents the failure path by checking the resolved tag
against a hardcoded floor BEFORE downloading.

## Changes

`scripts/install.sh`:
  - New `MIN_ATTESTED_VERSION="v0.18.0"` constant near the top
  - New `version_ge` helper using `sort -V` (handles v0.9.0 vs v0.10.0)
  - Moved three-layer verification resolution (config → env → flag) to
    before the download so $verify_attestation is known in time to
    gate network work
  - New pre-flight check: if verification is requested and the resolved
    tag is older than MIN_ATTESTED_VERSION, fail fast with a clean
    message listing recovery options (pin to newer version,
    --skip-attestation, or unset the env var / config). No binary
    download, no wasted SHA256 check.
  - Late `gh attestation verify` block now only handles the gh call
    itself — resolution and pre-flight moved upstream.

`scripts/install.ps1`:
  - New `$minAttestedVersion = "v0.18.0"` constant
  - Pre-flight guard in the verification branch using PowerShell's
    [version] class for proper numeric comparison
  - Same error message content as install.sh

`scripts/install.cmd`:
  - New `set "MIN_ATTESTED_VERSION=v0.18.0"` near REPO setup
  - Pre-flight guard shells out to PowerShell for semver comparison
    — Windows 10+ ships `powershell.exe` always, so no new runtime
    dependency. Hand-parsing semver in cmd was tried and rejected as
    too fragile for prerelease tags and non-numeric components.

`apps/marketing/.../installation.md`, `apps/hook/README.md`,
`README.md`, `scripts/install.sh --help`:
  - Replaced every user-facing `v0.17.1` example with `vX.Y.Z`
    placeholder. The placeholder pattern already exists in the
    "Verifying your install" section, so this is just consistency.
  - install.sh --help adds a link to the releases page so users know
    where to find actual tag values.

`.agents/skills/release/SKILL.md`:
  - New Phase 4 checklist step: before shipping the first attested
    release, verify MIN_ATTESTED_VERSION in all three installers
    matches the tag being cut. The constant is bumped ONCE and never
    again — it's a permanent floor, not a moving target. If the first
    post-merge release is not v0.18.0, the skill updates the constant
    in the same commit as the version bump so the installers served
    from plannotator.ai activate the new floor at the same moment
    the first attested release becomes fetchable.

`scripts/install.test.ts`:
  - New test asserts all three installers hardcode MIN_ATTESTED_VERSION,
    use appropriate version comparison for their dialect, and contain
    the "predates" error message
  - New test asserts install.sh and --help text no longer contain
    `v0.17.1` as a pinned example

38 install tests pass (was 36). Smoke-tested install.sh end-to-end:

  - `--version v0.17.1 --verify-attestation` → pre-flight rejects
    cleanly, no download attempted, exit 1 with actionable error
  - `--version v0.18.0 --verify-attestation` → pre-flight passes,
    script proceeds to download (404 as expected since v0.18.0 is
    not yet released)
  - `--version v0.17.1` (no verify) → pre-flight skipped, normal
    download path

For provenance purposes, this commit was AI assisted.

* fix(install): close PS injection + move Windows pre-flight before download

PR #512 review cycle 8 raised three related findings, all verified
against actual code.

## Critical: PowerShell command injection in install.cmd (Finding 2)

Line 228 of the previous install.cmd passed the version comparison
to PowerShell by interpolating delayed-expansion variables directly
into the command string between single-quoted literals:

    for /f "delims=" %%i in ('powershell -NoProfile -Command "try {
      if ([version]'!TAG_NUM!' -ge [version]'!MIN_NUM!') { 'yes' }
    } catch {}"') do set "VERSION_OK=%%i"

The arg parser rejected leading-dash values but not quotes or
semicolons, so a user passing

    install.cmd --version "0.18.0'; calc; '0.18.0"

produced the PowerShell command

    try { if ([version]'0.18.0'; calc; '0.18.0' -ge [version]'0.18.0')
        { 'yes' } } catch {}

PowerShell permits statement sequences inside `if` condition
parentheses — the last value is used — so `calc` executed as a side
effect during the first evaluation phase. Attacker-controlled
--version from a CI/CD wrapper (PR titles, external tag sources,
etc.) equals arbitrary code execution as the invoking user.

Fixed by passing the version strings via environment variables
($env:TAG_NUM, $env:MIN_NUM) instead of interpolating them into
the PowerShell command string. PowerShell reads $env: values as
raw strings and never parses them as code. The [version] cast
throws on invalid input, catch {} swallows it, VERSION_OK stays
empty, and the guard rejects — safe fail with a slightly less
helpful but correct error message.

## Structural: Windows pre-flight ran post-download (Findings 1 & 3)

install.sh was already restructured in the previous commit to run
the three-layer resolution + MIN_ATTESTED_VERSION guard BEFORE the
binary download, so users hit the "predates attestation support"
error without wasting bandwidth.

install.ps1 and install.cmd drifted — their resolution and
pre-flight blocks stayed in their original post-SHA256 positions,
meaning the binary was always downloaded and SHA256-verified even
when the requested tag was doomed to fail provenance verification.
The "Pre-flight: reject the verification request before
downloading" comments were lies copied from install.sh.

This commit moves both Windows installers' resolution + pre-flight
blocks upstream of the download:

  install.ps1: resolution + pre-flight now run immediately after
    `Write-Host "Installing plannotator $latestTag..."`, before
    $tmpFile is created or Invoke-WebRequest runs. The late
    gh-call block keeps only the gh attestation verify call itself.

  install.cmd: same restructure. The late block keeps only the
    where-gh check and gh invocation. The `del "!TEMP_FILE!"`
    calls inside the rejection branch are gone (TEMP_FILE doesn't
    exist yet when the guard runs).

## Tests

Added two new regression guards to scripts/install.test.ts:

  1. Order-aware check for all three installers: the resolution
     block's opening line must appear textually BEFORE the curl /
     Invoke-WebRequest download line. Uses indexOf to compare
     positions. Catches any future regression that drifts the
     pre-flight back after download.

  2. Injection-safe pattern check for install.cmd: asserts the
     PowerShell command references $env:TAG_NUM / $env:MIN_NUM and
     does NOT interpolate !TAG_NUM! / !MIN_NUM! between single
     quotes in any [version] cast.

40 install tests pass (was 38). Smoke-tested install.sh with
--version v0.17.1 --verify-attestation — rejects cleanly with no
download, same as before.

For provenance purposes, this commit was AI assisted.

* fix(install): close cycle-9 gaps — CI coverage, v-strip, prerelease handling

PR #512 cycle 9 review surfaced three real findings, all verified.

## Finding 1 (important): Windows CI never exercised the attestation path

The Windows integration job ran `install.cmd v0.17.1 --skip-attestation`,
which bypasses every bit of logic this PR shipped: three-layer opt-in
resolution, MIN_ATTESTED_VERSION pre-flight, $env:-based PowerShell
version comparison, and the gh attestation verify call. A runtime bug
in any of those paths would not be caught by CI.

`--skip-attestation` was passed intentionally because v0.17.1 predates
attestation support — running without it hits the pre-flight and
rejects. But that's the point: the REJECTION path is a real, valid end
state we can assert against. The previous test conflated "install
should succeed" with "test should pass"; the fix is to assert the
correct behavior for an old version.

Added a new CI step that runs `install.cmd v0.17.1 --verify-attestation`
via Start-Process with stderr redirection to a temp file, then asserts:
  - exit code != 0 (pre-flight rejected)
  - stderr contains "predates" (rejection came from our guard, not
    some other failure mode like a network error or gh missing)

This exercises on a real cmd.exe:
  - setlocal enabledelayedexpansion parser under the guard
  - three-layer resolution reaching the CLI flag layer
  - the :~1 substring (instead of the previous :v= global substitution)
  - the pre-release tag detection (negative path for stable tags)
  - the PowerShell shell-out with $env:TAG_NUM / $env:MIN_NUM
  - the [version] -ge comparison returning false
  - the "predates" error message block

Can't test the success path (valid attested release) until the first
post-merge release exists. Tracked for follow-up.

## Finding 2 (nit): !TAG:v=! is a global substitution, not anchored

cmd's delayed-expansion string-substitution syntax `!VAR:str=repl!`
replaces every occurrence of `str` globally. For all current semver
tags (vX.Y.Z) this happens to strip exactly one `v` by coincidence.
A hypothetical future tag like v1.0.0-rev2 would become 1.0.0-re2,
which [System.Version] can't parse, silently misclassifying the
failure as "predates attestation support" (see Finding 3).

install.ps1 line 121 uses `-replace '^v', ''` which is properly
regex-anchored. install.cmd had no anchored equivalent.

Fixed by using `!TAG:~1!` — substring from index 1 — which drops
exactly the first character. Safe because TAG is guaranteed to start
with `v` by the normalization step upstream (line ~141).

## Finding 3 (nit): Pre-release tags misdiagnosed on Windows

[System.Version] doesn't support semver prerelease or build-metadata
suffixes (e.g. v0.18.0-rc1). It throws on any `-` in the version
string. The catch blocks in both Windows installers handled the
throw but surfaced wrong/confusing errors:

  install.sh: handles prereleases correctly via `sort -V` (POSIX
    version sort is semver-aware) — no issue.
  install.ps1: caught and printed "Could not parse version tags for
    provenance check" — accurate but doesn't explain WHY.
  install.cmd: swallowed silently, VERSION_OK stayed empty, printed
    "predates attestation support" — actively wrong, the problem
    isn't the version's age.

Fixed in both Windows installers by detecting `-` in the tag BEFORE
attempting the [version] cast:

  install.ps1: `if ($latestTag -match '-')` → dedicated error
  install.cmd: `if not "!TAG_NUM!"=="!TAG_NUM:-=!"` (native
    substitution check, no subshell, no metacharacter risk)

Both emit a clear "pre-release tags aren't currently supported for
provenance verification on Windows" message pointing users at
--skip-attestation or a stable tag. Windows has no built-in semver
comparator; adopting one would require NuGet or a custom parser.
Explicit rejection with honest diagnosis is the pragmatic choice.

## Tests

Three new regression guards in install.test.ts:

  1. `install.cmd strips leading v via substring, not global
     substitution` — asserts `!TAG:~1!` is present and `!TAG:v=!`
     is gone.

  2. `both Windows installers reject pre-release tags with a
     dedicated error` — asserts both scripts contain the
     "Pre-release tags" error message and the appropriate
     detection pattern for their dialect.

  3. The new test.yml CI step doubles as a runtime regression
     guard — any break in the cmd pre-flight path that no longer
     matches "predates" in stderr, or returns 0, fails CI.

42 install tests pass (was 40). Windows CI will now exercise the
pre-flight rejection path end-to-end for the first time.

For provenance purposes, this commit was AI assisted.

* fix: cycle-10 review — split attest job, assert binary preservation, misc

PR #512 cycle 10 raised four findings, all verified.

## Finding 1: id-token/attestations permissions granted to build on PRs

The build job in release.yml had `id-token: write` and
`attestations: write` at the job level with no conditional guard.
On PR triggers, those permissions were live for every build step
(checkout, bun install, bun build, compile) even though the
attestation step itself was gated by `if: startsWith(github.ref,
'refs/tags/')`. Narrow-but-real attack surface: a trusted
contributor's malicious PR injecting code into a build step could
mint an OIDC token authenticating as the repo identity. Fork PRs
are automatically protected (GitHub suppresses OIDC tokens on
forks), but same-repo contributor compromise is a realistic risk
in a project with external contributors.

Fixed by splitting attestation into its own job:

  build:   contents: read only. Runs on all triggers. Compiles
           binaries and uploads them as the `binaries` artifact.
           No OIDC capability anywhere in the job.

  attest:  needs: build, if: tag push only. contents: read +
           id-token: write + attestations: write. Downloads the
           binaries artifact and runs attest-build-provenance.
           Permissions are only live when we're actually producing
           an attestation — never on PR dry-runs.

  release: needs: attest (was: needs: build). Still tag-only. The
           dependency chain guarantees the attestation exists in
           the GitHub attestation store before the release's
           binaries are published, closing the race window where a
           user could pull the binary and gh attestation verify
           would fail because the bundle hadn't propagated yet.

  npm-publish: unchanged. Still needs: build. Still has id-token:
           write for `npm publish --provenance`. The reviewer
           flagged only the build job; npm-publish's id-token
           grant is scoped to that one job and is actually used by
           the provenance flag.

## Finding 2: CI test promised a binary-preservation check but didn't do one

The `Attestation pre-flight rejects v0.17.1` step contained a
multi-line comment promising to verify the rejected run didn't
overwrite the previously-installed binary. No assertion code
followed — just a Write-Host success line. The test claimed
more than it delivered.

Added actual baseline capture + comparison:
  - Before running the rejection test, capture the binary's
    SHA256 and LastWriteTime from the prior Gemini-merge step.
  - After the rejection, recompute both and assert they match.
  - Any drift throws: catches future regressions that re-introduce
    the post-download pre-flight pattern (the pre-flight correctly
    rejects but only after downloading and overwriting the file).

## Finding 3: install.ps1 dead-code comment about flag precedence

Line 111 read "-SkipAttestation beats -VerifyAttestation if both
passed" but the upfront mutex guard (lines 13-16) exits 1 if both
flags are present. The "beats" scenario is unreachable. The
comment misleads a future reader into thinking the late ordering
handles the mutual exclusion and is safe to remove the early
guard — which would be backwards.

Replaced with a comment that explicitly notes the mutex guard at
the top of the script makes the two branches mutually exclusive
by construction.

## Finding 4: install.sh `cd` inside `&&` condition leaked CWD on failure

The skills-install block chained `git clone ... && cd ... && git
sparse-checkout set ...`. If clone succeeded but sparse-checkout
failed, the short-circuit skipped the `cd -` and `rm -rf
"$skills_tmp"` later ran with the shell's CWD still inside the
to-be-deleted directory. On Linux/macOS this silently "works" —
the inode is unlinked but the process keeps its cwd reference —
so nothing visibly breaks (all downstream code uses absolute
paths). But it's structurally wrong: install.ps1 and install.cmd
both use Push-Location/pushd for the same logic.

Restructured to run the entire clone → sparse-checkout → verify
→ copy sequence inside a single `(...)` subshell, with `cd`s
scoped to the subshell. The parent shell's CWD is unchanged
regardless of which step fails, so the subsequent `rm -rf`
always runs from a stable location. Any failure in the chain
short-circuits to the else branch with a clean skip message.
Also merged the two `[ -d ]` / `[ ls -A ]` guards into the
chain so the "apps/skills empty" case is now reported in the
skip message rather than being silently suppressed.

42 install tests pass.

For provenance purposes, this commit was AI assisted.

* fix(install): set MIN_ATTESTED_VERSION to v0.17.2, remove skill bump note

Earlier cycles hardcoded MIN_ATTESTED_VERSION="v0.18.0" across the
three installers as a best-guess for the first post-merge release,
and I added a one-time bump instruction to the release skill as
insurance in case the guess was wrong.

The guess was wrong — the next release is v0.17.2 (patch bump,
not a minor bump). Updated the constant in all three installers and
the matching test assertions. No other version references in the
shipped error messages need changing because they read MIN_ATTESTED_VERSION
from the variable at runtime.

Also removed the "⚠️ One-time MIN_ATTESTED_VERSION bump" section
from .agents/skills/release/SKILL.md entirely. With the constant
now set to the actual next release tag, there's nothing for the
release agent to bump at release time — the constant is already
correct. Baking a one-time action into a recurring release skill
was the wrong place for it; every future release agent would read
the warning, confirm it's already set, and move on. Noise in a
workflow that's supposed to be tight.

If the next release version ever differs from v0.17.2 (e.g. we
decide to skip to v0.18.0 or go straight to v1.0.0), the PR cutting
that release will need to update MIN_ATTESTED_VERSION in the three
installers. That's an ad-hoc fix, not a recurring skill concern.

Smoke test with the new value:
  - install.sh --version v0.17.1 --verify-attestation → rejects
    with "first attested release is v0.17.2"
  - install.sh --version v0.17.2 --verify-attestation → passes
    pre-flight, proceeds to download (404 as expected since
    v0.17.2 is not yet released)

42 install tests pass.

For provenance purposes, this commit was AI assisted.

* feat(release): ship native ARM64 Windows binaries

Bun v1.3.10 (February 2025) promoted bun-windows-arm64 from preview to
a stable cross-compile target, which makes native ARM64 Windows builds
a 15-line change instead of a project. Adopted immediately so ARM64
Windows users get native-speed binaries instead of the x86-64
emulation tax.

Earlier cycles of this PR shipped two temporary workarounds for the
absence of a native ARM64 binary:

  - install.ps1 detected ARM64 and fell back to $arch="x64" with a
    Write-Host notice that the user was running via emulation.
  - install.cmd hardcoded PLATFORM=win32-x64 and let ARM64 hosts pass
    the arch check without differentiation.

Both are now obsolete and have been replaced with real architecture
detection that selects the native binary.

## Changes

release.yml: Added `bun-windows-arm64` to the compile matrix for
both apps/hook/server/index.ts and apps/paste-service/targets/bun.ts.
Output files are plannotator-win32-arm64.exe and
plannotator-paste-win32-arm64.exe with matching .sha256 sidecars.
Upload-artifact already globs `plannotator-*` so no change there.

release.yml attest step: Added the two new ARM64 binaries to
subject-path so they're covered by the SLSA build provenance
attestation alongside the x64 builds. Both binaries sign with the
same Sigstore bundle as the rest of the matrix.

install.ps1: Restored the proper ARM64 detection that the earlier
fallback replaced. On 64-bit Windows, $arch is "arm64" when
PROCESSOR_ARCHITECTURE equals "ARM64", otherwise "x64". The
emulation-fallback Write-Host notice is gone — users now get
native binaries and don't need to be told about emulation.

install.cmd: Replaced the unconditional `set "PLATFORM=win32-x64"`
with a set of conditional assignments keyed off PROCESSOR_ARCHITECTURE
and PROCESSOR_ARCHITEW6432 (the latter covers the edge case of a
32-bit tool launching install.cmd on an ARM64 machine via WoW64).
PLATFORM is left empty if neither variable indicates AMD64 or ARM64,
which triggers the "does not support 32-bit Windows" error path.
The :arch_valid label and its gotos are gone — the new logic is
linear and doesn't need a label.

install.test.ts: Updated the install.ps1 ARM64 test to assert the
native arm64 branch (no more "runs via emulation" text) and added a
new install.cmd test verifying both PLATFORM branches are present.
43 install tests pass (was 42).

## CI coverage caveat

windows-latest is x86-64, so the Windows integration job still
exercises install.cmd against the x64 binary path. ARM64 has no CI
runner coverage yet — we're shipping ARM64 binaries on trust that
Bun's cross-compile produces working executables. That's the same
trust we extend to linux-arm64 builds (also x-compiled from an
ubuntu-latest runner). GitHub Actions does offer a windows-11-arm
runner that could be added later; tracked as follow-up since it has
availability and pricing implications.

Closes #517.

For provenance purposes, this commit was AI assisted.

* fix(install.ps1): detect ARM64 host through WoW64 too, matching install.cmd

Self-review catch on top of the ARM64 support commit. My install.ps1
architecture detection only checked \$env:PROCESSOR_ARCHITECTURE, which
reports the architecture the CURRENT PowerShell process is running
under — not the host architecture. On ARM64 Windows, a 32-bit
PowerShell process (rare, but possible) would see
PROCESSOR_ARCHITECTURE=X86, miss the "ARM64" branch, fall through to
\$arch = "x64", and download the emulated x64 binary instead of the
new native arm64 build.

install.cmd already handles this correctly via PROCESSOR_ARCHITEW6432,
which is set only in 32-bit WoW64 processes and holds the host
architecture. install.ps1 was the odd one out.

Fixed by checking PROCESSOR_ARCHITEW6432 first and falling back to
PROCESSOR_ARCHITECTURE. Now both Windows installers follow the same
detection logic regardless of process bitness. Also added an explicit
error branch for unrecognized architectures (anything that isn't AMD64
or ARM64) instead of silently assuming x64.

Test updated to assert both env vars are referenced.

For provenance purposes, this commit was AI assisted.

* fix(install): cycle-12 review — consistency test, dead code, finally, docs

Four findings addressed. Two findings rejected.

## Finding 1 (nit): MIN_ATTESTED_VERSION triplicated without CI consistency

Added a cross-file consistency test in install.test.ts that extracts
the version literal from each of install.sh, install.ps1, install.cmd
via regex and asserts all three match. A future bump that updates
only one or two files now fails CI loudly. The per-file tests still
exist (they check each file contains the current literal), but the
new test catches drift where each file is internally consistent with
itself but differs from the others.

## Finding 4 (nit): Write-Error + exit 1 dead code in install.ps1

Verified against the actual file: $ErrorActionPreference = "Stop" is
set at line 8 and never modified. All six Write-Error sites are dead-
end paths — five outside any try/catch, one inside a catch block
(line 147) where Write-Error raises a new terminating error that
propagates past the catch and exits the script with code 1 (PowerShell
default). The `exit 1` lines that followed were never reachable.

Dropped the six unreachable `exit 1` lines. Added a comment at the
first occurrence explaining the Stop + Write-Error semantics so
future maintainers don't re-add them. Behavior is unchanged at
runtime — every error path still exits with code 1 via PowerShell's
default unhandled-terminating-error handling.

## Finding 5 (nit): Pop-Location not in finally block

Verified the reviewer's claim in install.ps1 lines 384-403. The
skills install wraps git clone, Push-Location, and Copy-Item calls
in a single try block, with Pop-Location on the success path. If
Copy-Item throws under ErrorActionPreference=Stop, catch runs
without popping, and the subsequent Remove-Item deletes a directory
the PowerShell location stack still points into.

A naive `finally { Pop-Location }` would introduce a new bug:
Pop-Location throws on an empty stack, which happens when git
clone silently fails and Push-Location is never reached. Used a
nested-try pattern instead:

  try {
      git clone ...                    # native, no throw
      if (Test-Path "$skillsTmp\repo") {   # guard against clone failure
          Push-Location "$skillsTmp\repo"
          try {
              ...operations...
          } finally {
              Pop-Location                 # always runs IF pushed
          }
      }
  } catch {
      Write-Host "Skipping..."
  }

Traced all four failure modes:
  - clone fails silently → repo dir missing → skip inner block → no
    push, no pop → clean exit
  - clone succeeds → push succeeds → operations fail → finally pops
    → outer catch fires
  - clone + push + operations all succeed → finally pops cleanly
  - push itself throws (permissions) → outer catch fires, nothing
    to pop

## Finding 6 (P1): CMD/ps1 ARM64 breaks pinned pre-v0.17.2 tags

The original plan was a runtime x64-fallback on 404, but the simpler
product-level framing is: v0.17.2 is the first fully-supported version
for pinning. Pre-v0.17.2 tags predate native ARM64 Windows (no
win32-arm64 asset exists) and predate attestation support (pre-flight
rejects). Users pinning to older tags are outside the supported
matrix; the failure modes are explicit (404 / clean rejection), not
silent corruption.

Documented in the three install docs:
  - apps/marketing/.../installation.md: full "Supported versions"
    paragraph explaining the floor, what fails, and recovery paths
  - README.md: one-line note folded into the existing provenance
    sentence ("Version pinning, native ARM64 Windows, and SLSA
    provenance are supported from v0.17.2 onwards — see installation
    docs for details")
  - apps/hook/README.md: same tight one-liner pattern

README.md and apps/hook/README.md stay bloat-free; the canonical
explanation lives in the marketing docs.

## Findings rejected

- **Finding 2 (P3, sort -V misorders prereleases):** plannotator
  doesn't ship prerelease tags. A user pinning to a hypothetical
  vX.Y.Z-rc1 would 404 at the download step before the sort -V
  misordering matters. Moot in practice.

- **Finding 3 (important, sort -V is GNU-only):** FALSE POSITIVE.
  Tested on macOS 26.3.1 running sort 2.3-Apple (197) — both -V
  and --version-sort are supported and work correctly, including
  for prerelease suffixes. Apple forked BSD sort and added -V
  years ago. The reviewer's claim cites outdated reference
  material about historical BSD sort.

44 install tests pass (was 43).

For provenance purposes, this commit was AI assisted.

* test: anchor MIN_ATTESTED_VERSION consistency regexes to line start

Self-review catch: the cross-file consistency test added in the prior
commit matched the assignment form anywhere in each file. No current
comment triggers a false positive, but a future comment like
`# Example: MIN_ATTESTED_VERSION="v0.17.0"` would match first and
shadow the real assignment, causing the test to report the wrong
value or pass when it shouldn't.

Hardened by adding /m flag and ^ anchor. The real assignments in all
three installers are flush-left at the top of their files, so
requiring line-start is both safe (won't reject current code) and
stricter (future comments with leading whitespace or other prefixes
are ignored).

44 tests still pass.

For provenance purposes, this commit was AI assisted.

* fix: cycle-13 review — checksum cleanup leak + Gemini CI coverage

Two findings addressed. Two pre-existing findings flagged but not
in scope.

## Finding 3 (nit): CHECKSUM_FILE leak on download failure

install.cmd's checksum download error path deleted TEMP_FILE but
omitted CHECKSUM_FILE. curl -o creates the output file before it
receives data, so a network failure or HTTP error leaves a 0-byte
or partial file in %TEMP% that the script never cleans up. The
symmetric cleanup for TEMP_FILE elsewhere in the script makes this
an accidental omission, not an intentional design choice.

Added `if exist "!CHECKSUM_FILE!" del "!CHECKSUM_FILE!"` inside the
error block, matching the existing cleanup discipline.

## Finding 1 (nit): Windows CI readback misses Gemini .toml files

The `Verify Claude Code slash command files contain the shell-
invocation prefix` step in the Windows integration job verified
the three `.md` files at %USERPROFILE%\.claude\commands\ but not
the two `.toml` files at %USERPROFILE%\.gemini\commands\. Both
sets of files use the `^^!` cmd escape pattern that this PR added,
and a future regression that drops a `^` from the Gemini echoes
would slip past CI even though install.test.ts catches it
statically.

Extended the readback step to also verify
plannotator-review.toml and plannotator-annotate.toml contain the
`!{plannotator ...}` invocation form. Same regression class, same
guard, same runner — the earlier Gemini-merge fixture step
already seeds ~/.gemini/settings.json, which causes install.cmd's
Gemini block to fire and write the .toml files alongside the
Claude Code ones, so no additional setup is required.

## Findings rejected (out of scope, pre-existing)

- **install.cmd vs install.ps1 install location divergence:** cmd
  installs to %USERPROFILE%\.local\bin while ps1 installs to
  %LOCALAPPDATA%\plannotator. A user who switches between the two
  Windows installers ends up with hooks.json pointing at one
  location and an orphan binary at the other. Pre-existing
  structural divergence, requires picking a canonical location and
  migrating users on whichever installer changes. Out of scope
  for this PR.

- **install.sh Gemini merge throws on user's malformed JSON:** if
  ~/.gemini/settings.json is invalid JSON, the embedded `node -e`
  call exits non-zero, set -e propagates, and the install aborts
  mid-run after the binary is in place but before slash commands
  are written. Pre-existing — the Gemini block predates this PR.
  Worth a follow-up but not in scope here.

44 install tests pass.

For provenance purposes, this commit was AI assisted.

* docs: update stale v0.17.1 references in script comments to vX.Y.Z

Two cosmetic comment fixes flagged during the cycle-13 self-review.
The user-facing examples and docs were updated to vX.Y.Z in cycle 5,
but two inline code comments still referenced the old concrete version:

  - scripts/install.sh:129 — "Positional form: install.sh v0.17.1
    (matches install.cmd interface)"
  - scripts/install.cmd:71 — "Positional form: install.cmd v0.17.1
    (legacy interface)"

Both updated to vX.Y.Z so the in-code comments match the rest of the
documentation. No behavior change.

44 install tests pass.

For provenance purposes, this commit was AI assisted.

* docs(skill): update release skill platform/binary counts for ARM64 Windows

Two stale references in .agents/skills/release/SKILL.md after the
ARM64 Windows binaries were added:

- "5 platforms (macOS ARM64/x64, Linux x64/ARM64, Windows x64)"
  → "6 platforms (macOS ARM64/x64, Linux x64/ARM64, Windows x64/ARM64)"

- "Compiles paste service binaries (same 5 platforms)"
  → "Compiles paste service binaries (same 6 platforms)"

- "Generates SLSA build provenance attestations for all 10 binaries"
  → "Generates SLSA build provenance attestations for all 12 binaries"

The first two predate this PR; the third was added in the cycle-1
commit and not bumped when the ARM64 Windows targets landed in the
ARM64 commit. All three corrected together so the release agent
sees an internally-consistent description of what the pipeline
actually does.

Verified against release.yml — 12 entries in the attest job's
subject-path list, 12 compile commands in the build job, all six
platforms (macOS arm64/x64, Linux x64/arm64, Windows x64/arm64)
for both plannotator and paste-service.

For provenance purposes, this commit was AI assisted.
2026-04-07 20:35:16 -07:00
Michael Ramos 566df72f11 chore: bump version to 0.17.0
For provenance purposes, this commit was AI assisted.
2026-04-06 12:36:15 -07:00
Michael Ramos 70ad54dd70 chore: bump version to 0.16.7
For provenance purposes, this commit was AI assisted.
2026-04-02 15:33:48 -07:00
Michael Ramos a48fd8afb5 chore: bump version to 0.16.6
For provenance purposes, this commit was AI assisted.
2026-04-02 10:57:13 -07:00
Michael Ramos 070f75f804 chore: bump version to 0.16.5
For provenance purposes, this commit was AI assisted.
2026-04-01 22:13:51 -07:00
Michael Ramos 6735266d66 chore: bump version to 0.16.4 2026-04-01 15:08:26 -07:00
Michael Ramos fad822e6ca chore: bump version to 0.16.2 2026-03-30 21:49:08 -07:00
Michael Ramos a633e70597 chore: bump version to 0.16.1 2026-03-30 09:13:52 -07:00
Michael Ramos a88da925de chore: bump version to 0.16.0 2026-03-29 22:13:09 -07:00
Stacey Haffner 15af1c40eb Add GitHub Copilot CLI integration (#409)
* feat: add GitHub Copilot CLI integration

Add full Copilot CLI support via a new plugin (apps/copilot/) with:
- preToolUse hook intercepting exit_plan_mode for plan review
- copilot-plan subcommand with correct permissionDecision output format
- copilot-last subcommand parsing events.jsonl for last message annotation
- Copilot CLI session parser (copilot-session.ts)
- Plugin manifest, hooks.json, and slash commands
- UI origin branding (blue badge for Copilot CLI)
- Marketplace entry for plugin discovery
- README install section

Key design decisions:
- Intercepts exit_plan_mode (not create) to avoid double-review
- Uses sessionId (with fallback) to locate plan.md from session state
- Follows Codex integration pattern: same CLI binary, new plugin dir

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: handle actual Copilot CLI preToolUse input format (toolCalls array)

The Copilot CLI hook input uses { toolCalls: [{ name, args }] } format
instead of the documented { toolName, toolArgs } format. The filter was
checking event.toolName (always undefined), causing exit_plan_mode to
pass through unintercepted. Now extracts tool name from either format.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert "fix: handle actual Copilot CLI preToolUse input format (toolCalls array)"

This reverts commit b214bc02c58fc5d8f305b128457099afb2ea6ecc.

* fix: use absolute path in hooks.json for reliable binary discovery

The hook subprocess inherits the PATH from when Copilot CLI was started,
which may not include the plannotator binary directory. Using the full
absolute path via env vars ensures the hook always finds the binary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Finishing the feature

* fix: remove Copilot CLI plugin from Claude Code marketplace.json

.claude-plugin/marketplace.json is for Claude Code's plugin marketplace.
The Copilot CLI plugin has its own install path and doesn't belong here.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: align Copilot CLI plugin with official spec

- Add .github/plugin/marketplace.json for Copilot CLI plugin discovery
- Change allowed-tools from Bash() to shell() syntax in commands
- Respect COPILOT_HOME env var in session path discovery
- Update plugin.json version to 0.14.5

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Yecats <Yecats@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: stahaff <222254525+stahaff@users.noreply.github.com>
2026-03-27 10:20:49 -07:00
Michael Ramos 401793e35f feat: custom display name + config file foundation (#399)
Adds user-editable display names and persistent config via ~/.plannotator/config.json.

- ConfigStore singleton with precedence: server config file > cookie > default
- Editable identity input in Settings with "Use git name" and regenerate buttons
- POST /api/config endpoint for write-back across all 6 servers
- getServerConfig() reads config fresh per request (no stale cache)
- Eager constructor hydration so the store is safe to read before init()
- vendor.sh as single source of truth for Pi extension vendoring
- Vendor parity test to prevent missing generated modules

Closes #396
2026-03-26 11:54:22 -07:00
Michael Ramos 1175ef65c1 fix(pi): bundle AI backbone into generated/ for published package
@plannotator/ai is a private workspace package unavailable on npm.
Pi's dynamic import silently failed, so AI features never loaded for
published package users. Now copies all AI files into generated/ai/
at build time, same pattern as the shared utils.

For provenance purposes, this commit was AI assisted.
2026-03-25 00:22:33 -07:00
Michael Ramos f96758da0a feat(pi): complete Pi server rewrite — modular architecture, full Bun parity, shared code extraction (#382)
* feat(pi): add missing endpoints to plan, review, and annotate servers

Phase 1-3 of Pi endpoint parity:

Plan server: image, upload, draft, editor-annotations, agents, favicon,
linked documents, Obsidian vaults/files/doc, file browser, VS Code diff

Annotate server: image, upload, draft, favicon, linked documents, file browser

Review server: extract shared handlers, add favicon

Shared utilities extracted from review server inline code into reusable
functions (handleImageRequest, handleUploadRequest, handleDraftRequest,
handleFavicon). Reference handlers (doc, Obsidian, file browser)
implemented using Node.js fs APIs replacing Bun.Glob/Bun.file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(pi): add PR review endpoints and Node.js PR runtime adapter

Phase 4 of Pi endpoint parity:

- Node.js PRRuntime using child_process.spawn (matches Bun adapter pattern)
- GET /api/pr-context — fetch PR summary, comments, checks
- POST /api/pr-action — submit review to GitHub/GitLab
- PR mode guards on /api/diff/switch and /api/git-add
- /api/diff response includes prMetadata and platformUser in PR mode
- /api/file-content fetches from platform API in PR mode
- Build script copies pr-provider, pr-github, pr-gitlab from shared

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(pi): wire AI backbone with Node.js Pi SDK provider

Phase 5 of Pi endpoint parity:

- Create packages/ai/providers/pi-sdk-node.ts — PiProcessNode class
  using child_process.spawn instead of Bun.spawn, same RPC protocol
- Register 4 AI providers in Pi review server (claude-agent-sdk,
  codex-sdk, pi-sdk-node, opencode-sdk) with graceful degradation
- Route /api/ai/* endpoints through createAIEndpoints handlers
- Pipe Web Response → node:http response with ReadableStream support
  for SSE streaming
- Dispose AI sessions and registry on server stop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(pi): address parity audit findings across all three servers

Plan server:
- /api/plan: add repoInfo and projectRoot to response
- /api/approve: pass agentSwitch and permissionMode in decision
- Update decision promise type to include agentSwitch, permissionMode

Review server:
- /api/diff/switch: pass gitContext.cwd to runGitDiff
- /api/file-content: pass gitContext.cwd to getFileContentsForDiffCore
- /api/git-add: add fallback to gitContext.cwd when worktree parse fails

Annotate server:
- /api/plan: add repoInfo and projectRoot to response
- /api/feedback: capture annotations array (was silently dropped)
- Update decision promise type to include annotations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(pi): complete parity — integrations, planSave, save-notes

Ports all remaining missing functionality:

- Node.js versions of saveToObsidian, saveToBear, saveToOctarine
  (Bun.write → writeFileSync, Bun.$ → spawn)
- Node.js detectProjectNameSync (Bun.$ → execSync)
- extractTags, generateFrontmatter, generateFilename, extractTitle
- POST /api/save-notes — decoupled note saving
- POST /api/approve — full implementation: note integrations,
  planSave snapshots, saveAnnotations, saveFinalSnapshot
- POST /api/deny — planSave snapshots on denial
- Import saveAnnotations, saveFinalSnapshot from storage.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(pi): wire domain module imports and fix type errors

- Add all missing imports from ./server/* domain modules to server.ts
- Export interfaces from integrations.ts (ObsidianConfig, BearConfig, etc.)
- Move toWebRequest to helpers.ts, remove duplicate from handlers.ts
- Add git() helper to project.ts (was in server.ts, needed by getRepoInfo)
- Fix os default import → named imports in handlers.ts and network.ts
- Fix readdirSync Dirent type in reference.ts
- Fix Headers.entries() → forEach for Node compat in AI endpoint piping
- Fix ReadableStream type cast in AI SSE streaming
- Fix matchAll iterator compat in integrations.ts (use while + exec)
- Cast pi-sdk provider config to any (PiSDKConfig not in base union)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(pi): move generated shared files to generated/ directory

Moves all build-time copied shared files (feedback-templates, review-core,
storage, draft, project, pr-provider, pr-github, pr-gitlab) from the
pi-extension root into generated/ subdirectory.

Updates build script to output there. Updates all imports in server.ts,
index.ts, and server/ domain modules to use ./generated/ paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(pi): replace hand-maintained utils.ts with generated checklist

utils.ts was a manual copy of parseChecklist, extractDoneSteps, and
markCompletedSteps from packages/shared/checklist.ts. Add checklist
to the build-time copy list and import from generated/checklist.js.
Delete the redundant utils.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(pi): gitignore generated/ and built HTML files

These are build artifacts created by `bun run build:pi`. Untrack them
and add .gitignore to prevent re-adding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(pi): split server.ts into domain-organized modules

- server.ts is now a barrel re-exporting from server/ modules
- server/serverPlan.ts — plan review server
- server/serverReview.ts — code review server
- server/serverAnnotate.ts — annotate server
- server/helpers.ts — add requestUrl() to eliminate non-null assertions
- server/project.ts — linter fix (sanitizeTag import path)
- packages/ai/package.json — add pi-sdk-node export entry
- index.ts — fix waitForDone non-null assertion with guard check,
  update imports for generated/checklist.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(pi): parity audit fixes + shared code extraction

Systematic side-by-side audit of Pi vs Bun servers (A1-A22, B1-B2 complete).

Fixes found during audit:
- PlanServerResult.waitForDecision missing savedPath/agentSwitch/permissionMode
- Missing permissionMode option and /api/plan response field
- editorAnnotations created unnecessarily in archive mode
- repoInfo called per-request instead of cached at init
- Approve handler missing effectivePermissionMode fallback
- Deny handler missing savedPath in decision resolution
- Archive /api/plan response had extra pasteApiUrl
- Missing GET method guards on archive/plans, archive/plan, doc, obsidian/files, obsidian/doc, reference/files
- Review server had stray pasteApiUrl option/response field
- AI getCwd missing worktree support

Shared code extraction:
- packages/shared/favicon.ts — single source for favicon SVG
- packages/shared/integrations-common.ts — note app pure functions
- packages/shared/reference-common.ts — file tree building
- packages/shared/repo.ts — git remote parsing
- Updated all consumers to import from shared sources

For provenance purposes, this commit was AI assisted.

* fix: parity audit B3-C10 — review + annotate server fixes

Review server (B3-B17):
- diff/switch missing try/catch error handling
- git-add parseBody outside try/catch
- feedback missing try/catch error handling
- Unknown /api/ai/* paths now return 404 (both Bun and Pi)

Annotate server (C1-C10):
- Bun annotate server missing pasteApiUrl (short URL sharing broken)
- Added pasteApiUrl to Bun options, response, and both hook callers
- Pi repoInfo called per-request instead of cached at init
- Pi feedback missing try/catch error handling
- Missing GET method guards on doc and reference/files

For provenance purposes, this commit was AI assisted.

* fix: parity audit D3-D5 — draft error handling, editor annotations, resolve-file extraction

D3: Pi draft save handler missing error handling — added .catch() with 500 + console.error
D4: Pi editor annotation POST missing try/catch — added with "Invalid JSON" 400
D5: Extracted resolveMarkdownFile to packages/shared/resolve-file.ts
  - Replaced Bun.Glob with runtime-agnostic walkMarkdownFiles (readdirSync)
  - Made function sync (no longer async)
  - Pi handleDocRequest now uses shared resolveMarkdownFile instead of inline resolution
  - Gains Windows path normalization, isWithinProjectRoot security check
  - Deleted packages/server/resolve-file.ts re-export, consumers import from shared
  - Cleaned up stale await calls in hook entry, reference handler, and tests
  - All 19 resolve-file tests pass

For provenance purposes, this commit was AI assisted.

* fix: parity audit D6-D10 — integrations, PR naming, shared modules

D6: Fixed broken detectProjectNameSync — was using require() for
    non-existent exports. Now uses basename + sanitizeTag directly.
D7: Renamed checkAuth → checkPRAuth, getUser → getPRUser across
    Bun server, hook, and OpenCode plugin to match Pi naming.
    Also fixed stale resolve-file import in OpenCode plugin.
D8-D10: Verified clean — ide, project detection, network.

For provenance purposes, this commit was AI assisted.

* update openpackage.yml

* fix: bump Pi git-add test timeout to 15s for parallel suite stability

For provenance purposes, this commit was AI assisted.

* test: add route parity test — Bun ↔ Pi server route drift detection

For provenance purposes, this commit was AI assisted.

* fix(ci): update Pi generate step to use generated/ directory with full file list

The Pi extension was refactored to use generated/ subdirectory but the CI
generate step still used the old flat layout with a subset of files.

For provenance purposes, this commit was AI assisted.

* fix(ci): update release workflow Pi generate step to match new layout

Same stale generate step as test.yml — old flat layout, missing files.

For provenance purposes, this commit was AI assisted.

* fix(pi): update files array for modular server layout

The files array still referenced the old flat layout (server.ts monolith,
root-level generated files, deleted utils.ts). npm publish would have
produced a broken package missing server/ and generated/ directories.

For provenance purposes, this commit was AI assisted.

* feat: add TypeScript type-checking to CI pipeline

- Fix broken barrel export: buildFileTree/VaultNode re-exported from
  @plannotator/shared instead of reference-handlers (P1 bug)
- Fix server.port type narrowing in all 3 servers
- Fix AI provider type errors (claude-agent-sdk, codex-sdk, opencode-sdk, pi-sdk)
- Extract mapPiEvent to pi-events.ts to break Bun→Node type chain
- Add tsconfig.json to packages/shared, packages/ai, packages/server, apps/pi-extension
- Add `typecheck` script to root package.json
- Add type-check step to test.yml and release.yml CI workflows

For provenance purposes, this commit was AI assisted.

* fix(ci): use bun-types instead of @types/node for typecheck

CI environment has bun-types (includes Node types) but not
@types/node as a standalone package.

For provenance purposes, this commit was AI assisted.

* fix(ci): add @types/node for Node-runtime type checks

Pi extension and packages/shared run on Node, not Bun — they should
type-check against @types/node, not bun-types. Added @types/node as
a dev dependency so CI resolves it.

For provenance purposes, this commit was AI assisted.

* fix: cast Uint8Array.buffer to ArrayBuffer for TS 5.9 compat

crypto.subtle.importKey expects BufferSource, but TS 5.9 is stricter
about Uint8Array.buffer being ArrayBufferLike (includes SharedArrayBuffer)
vs ArrayBuffer. Explicit cast resolves the overload mismatch.

Astro pulls in TS 5.9 transitively, so CI resolves a different
TypeScript version than local dev. This fix works on both 5.8 and 5.9.

For provenance purposes, this commit was AI assisted.

* fix(ci): add bun-types as explicit devDependency

CI's bun install doesn't hoist bun-types to root node_modules when
it's only a transitive dep of @types/bun. Adding it as a direct
devDependency guarantees tsc can resolve it.

For provenance purposes, this commit was AI assisted.

* fix(ci): remove Pi extension from typecheck

Pi extension depends on @mariozechner/pi-* peer dependencies that
aren't installed in CI. Type-checking it requires Pi's runtime
environment. The three packages we check (shared, ai, server) are
sufficient to catch barrel export bugs and type errors. Pi extension
coverage comes from route parity tests and bun test.

For provenance purposes, this commit was AI assisted.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:44:54 -07:00
Michael Ramos a278fdaf77 feat: plan archive browser (#369)
* feat: plan archive browser with in-session sidebar tab (#362)

Add `plannotator archive` subcommand and archive sidebar tab for browsing
saved plan decisions from ~/.plannotator/plans/. Plans show approved/denied
badges and timestamps. In-session mode uses the linked doc overlay pattern
so users can reference old plans while reviewing a current one.

- New archive server (packages/server/archive.ts) following annotate pattern
- New ArchiveBrowser sidebar component, reusable in both contexts
- Archive listing/parsing functions in storage.ts (reads decision snapshots)
- Archive endpoints on plan server for in-session use (/api/archive/plans)
- Remove dead "Other Plans" UI, projectPlans state, /api/plan/history endpoint
- Fix resize handle touch area covering scrollbars in sidebar/main content
- Fix sidebar tab bar overflow when narrow

For provenance purposes, this commit was AI assisted.

* fix: code quality sweep for plan archive

- Remove `as any` cast: add "archive" to SessionInfo.mode union
- Replace inline import() type with proper import for ArchivedPlan
- Replace any[] with ArchivedPlan[] in fetch response types
- Fix infinite re-fetch when archive is empty (use hasFetched ref)
- Cache archive plan list in plan server (avoid re-scanning filesystem)
- Document ResizeHandle side prop behavior
- Remove redundant comment on Viewer archiveInfo prop

For provenance purposes, this commit was AI assisted.

* chore: remove dead marketing components

Step.astro and Landing.astro are unused — landing page inlines
step markup and pages use Base.astro directly.

For provenance purposes, this commit was AI assisted.

* fix: address code review findings for plan archive

- Path traversal: use resolve() + trailing separator guard (matches reference-handlers.ts)
- Thread customPath into in-session archive endpoints via query param
- Sort same-day archive entries by mtime instead of title
- Clear selectedArchiveFile on linked doc back to prevent badge leak
- Hide archive tab in annotate mode (server doesn't serve those endpoints)
- Add targetTab param to useLinkedDoc.open() to preserve calling sidebar tab
- Replace mutable render variable with index-based date grouping

For provenance purposes, this commit was AI assisted.

* refactor: collapse standalone archive server into plan server

Delete packages/server/archive.ts (187 lines) — nearly all duplicated
from the plan server. Add mode:"archive" option to startPlannotatorServer
instead. Fixes two bugs from code review:

- handleArchiveCopy now splits on "# Plan Feedback" marker instead of
  bare "---", preventing truncation at horizontal rules in plan content
- customPath support works in standalone archive mode (was only working
  in-session because the standalone server never received it)

For provenance purposes, this commit was AI assisted.

* refactor: extract useArchive hook from App.tsx

Move archive state (archiveMode, plans, selectedFile, isLoading) and
handlers (select, fetchPlans, done, copy) into a dedicated useArchive
hook. Reduces App.tsx by ~75 lines and makes the archive feature
self-contained.

For provenance purposes, this commit was AI assisted.

* feat: Pi archive parity + eliminate server duplication

Move runtime-agnostic storage, draft, and project functions from
packages/server/ to packages/shared/ — eliminating ~250 lines of
duplicated code in Pi's server.ts. Server package becomes thin
re-exports, preserving all existing import paths.

Add archive mode to Pi's plan review server (mode, routes, waitForDone)
and register /plannotator-archive command in the Pi extension. Consolidate
ArchivedPlan type to single definition in shared/storage.ts.

Simplify archive copy to include full content with feedback.

For provenance purposes, this commit was AI assisted.

* fix: drop -core suffix from Pi shared copies

The -core suffix broke cross-file imports — storage.ts imports from
./project which didn't resolve to project-core.ts. Using the original
filenames (no collision) lets relative imports work naturally.

For provenance purposes, this commit was AI assisted.

* fix: archive custom path bugs, disable sharing, update docs

- Normalize planDir via resolve() in getPlanDir() to handle relative
  paths and trailing slashes in the path traversal guard
- Re-fetch archive plans client-side with cookie-backed customPath
  so standalone archive respects the user's configured save location
- Disable sharing in archive mode (read-only viewer, no need)
- Remove dead /api/plan/history endpoint and listProjectPlans import
  from Pi extension
- Remove dead /api/plan/history mock from dev-mock-api
- Update CLAUDE.md and AGENTS.md: add archive flow, archive API
  endpoints, shared package structure, correct storage location,
  sidebar tab count, remove stale /api/plan/history references
- Update hook server docstring from four to five modes

For provenance purposes, this commit was AI assisted.

* fix: empty archive shows demo content, stale viewer after customPath fetch

- Clear demo markdown when archive opens with no plans (plan: "" was falsy,
  so setMarkdown was never called)
- Remove redundant fetchPlans() from archive init — server already sends
  archivePlans in initial response
- After fetchPlans() resolves with customPath results, auto-select and load
  the first plan into the viewer
- Remove dead listProjectPlans re-export from server barrel

For provenance purposes, this commit was AI assisted.

* refactor: gitignore Pi shared copies, add @generated headers

Pi's copied .ts files (storage, draft, project, feedback-templates,
review-core) are build artifacts generated from packages/shared/. They
looked like editable source files, leading to confusion about which file
to edit. Now gitignored like the HTML copies, with @generated headers
prepended by the build script.

For provenance purposes, this commit was AI assisted.

* fix: generate Pi shared copies in CI before tests

The Pi .ts copies are now gitignored build artifacts. CI needs to
generate them before running tests since server.test.ts transitively
imports them via server.ts.

For provenance purposes, this commit was AI assisted.

* fix: generate Pi shared copies in release pipeline test job

Same fix as test.yml — the Pi .ts copies are gitignored, so the test
job in the release pipeline also needs to generate them before bun test.

For provenance purposes, this commit was AI assisted.

* fix: use block scalar in CI workflow to avoid YAML parse error

The inline `run:` had a colon in the printf string that YAML
interpreted as a mapping key. Switch to `run: |` block scalar.

For provenance purposes, this commit was AI assisted.
2026-03-23 11:31:38 -07:00
Michael Ramos e01526457b feat: landing page revamp (#349)
* feat: revamp marketing landing page

Combine hero (dot grid, noise texture, agent selector, install block) from
prototype 1 with middle content (plan/code review showcases, how-it-works
toggle, key properties, compatibility, final CTA) from prototype 2.

- Hero with animated fade-up, glassy agent buttons, per-agent install commands
- AnnoReplace component: shimmer + white glove hint → click → comment popover
- How It Works section with Plan/Code Review toggle swapping images + copy
- Accurate install instructions (binary + plugin for Claude Code/OpenCode)
- Three-column footer with brand stamp
- Nav with pipe-separated links
- Agent icons with CSS invert for light/dark mode
- Scroll reveal animations for showcase sections

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: interactive terminal demos, install UX, and copy refinements

- Plan review: terminal animation (ASCII art, /plan typing, agent steps,
  submit_plan(), deny → feedback loop) with viewport-aware start/pause
- Code review: same pattern (/plannotator-review, send feedback loop)
- Install instructions: multi-step copyable rows for Claude Code and
  OpenCode, accurate Pi install command, Codex plan mode caveat
- Remove redundant Get Started section (hero handles install)
- AnnoReplace component: shimmer + white glove hint → click → comment popover
- How It Works: centered toggle, notification dot, tighter spacing
- Footer: license line, tighter gaps, @plannotator handle
- Social proof bar: "Zero learning curve"
- Hero trust signal: "free & open source"
- CTA subline: "Shape what gets built. Own what ships."
- Nav: pipe-separated links from prototype 2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: clean up landing page dead code and fix mobile/perf issues

Remove ~160 lines of dead CSS/JS from removed Get Started section,
consolidate duplicate reveal animation system, fix copy buttons
invisible on touch devices, drop unused Source Serif 4 font load,
fix footer responsive breakpoint, and clean up duplicate data attrs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: mobile responsiveness, demo glove pointers, and capabilities grid

Mobile: hide install commands, promote video CTA, reorder terminal
demos above text, fix social proof bar stacking, footer 2-col layout,
glove jabs further on small screens.

Demo gloves: white glove pointer on plan deny button (flipped, right
side) and code review send button (left side) with jab animations.

Capabilities: replace icon cards with 3-column grid (Commands,
Features, Integrations) with shimmer on slash commands.

Also: send feedback button moved to bottom-left, matched deny button
color, title em-dash to hyphen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: optimize images and clean up dead assets

Convert How It Works screenshots to WebP (25MB → 470KB).
Add proper plan-review.webp and code-review.webp for terminal demos.
Remove 6 unused image assets (~27MB). Fix ASCII art alignment.
Update trust signal copy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: contributor strip below footer with build-time GitHub fetch

GraphQL query (CI) fetches code contributors, issue authors, and
discussion participants in one request. REST fallback (local dev)
for code contributors only. Results cached in-process to avoid
blocking repeated renders. Deploy workflow passes GITHUB_TOKEN.

Also: bump brand stamp and avatar opacity, footer copy cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update agent video links and Codex install instructions

OpenCode → dedicated YouTube demo, Codex → X post demo.
Codex detail steps now show !plannotator review and !plannotator last.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: agent install steps leaking across buttons

Step elements now fully cleared (innerHTML + display:none) on every
agent switch. Use inline style instead of class toggle to avoid
CSS specificity conflict with .hero-detail-step display:flex.
Add third step slot for Codex commands. Add !plannotator annotate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add restart instructions and noscript fallback

Add restart step to Claude Code and OpenCode hero install flows so
new users know hooks/plugins require a session restart. Add noscript
style so landing-reveal sections stay visible without JavaScript.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:28:34 -07:00
renovate[bot] 5c5527c9ae chore(deps): update github actions (#305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-15 19:40:56 -07:00
김영준E 41059d8e43 fix: Support Windows markdown paths in CLI annotate flow (#267)
* fix(annotate): support Windows markdown paths in CLI annotate flow

* fix tmp path

* fix resolve-file.ts

* Update command to use EXE_PATH variable

* Update command path for plugin hooks in install.ps1

* test: add core test suite for path resolution, storage, remote detection, and install scripts

- resolve-file: absolute paths, relative paths, case-insensitive search, ignored dirs, extension filtering, ambiguity, Windows separators
- storage: slug generation, tilde expansion, version history, deduplication
- remote: env var detection (PLANNOTATOR_REMOTE, SSH_TTY), port config and validation
- image: tmpdir usage, extension validation
- install scripts: JSON structure validation, checksum verification, arch detection, full exe path in hooks

79 tests, 35ms

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add test workflow and gate release builds on tests

- New test.yml: runs `bun test` on PRs and pushes to main
- release.yml: tests must pass before build job runs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve pre-existing test failures for CI compatibility

- project.test.ts: make repo name assertion portable (works in CI where
  checkout dir differs from local dev)
- vscode mock: add missing APIs needed by editor-annotations.ts
  (comments, languages, Range, CodeActionKind, decorations)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: remove Viewer.test.tsx and happy-dom dependency

Tests didn't exercise any application code — they manually constructed
DOM elements inline and verified DOM API behavior, not Viewer.tsx logic.
The mock highlighter didn't simulate real hljs, and one test could never
fail due to its try/catch structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use targeted markdown glob and restore clean lockfile

- Replace **/* glob with **/*.[mM][dD]{,[xX]} to only scan markdown
  files during case-insensitive search (avoids iterating every file)
- Restore package.json key ordering from main, only removing happy-dom
- Regenerate bun.lock from main's base to eliminate version drift

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: drop redundant test and fix weak assertion

- Remove redundant PLANNOTATOR_REMOTE=TRUE test (already covered by true)
- Fix UPLOAD_DIR assertion that would pass even with hardcoded /tmp

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 17:47:07 -07:00
renovate[bot] d259748645 chore(deps): update github actions (#251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 06:20:48 -07:00
renovate[bot] d23c730d62 chore(deps): update github actions (#250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 06:20:40 -07:00
Michael Ramos 93159240a7 chore: pin deploy.yml actions to SHAs + add Renovate (#244)
* chore: pin GitHub Actions to SHAs and add Renovate config

Pin all actions in deploy.yml to full commit SHAs to prevent tag
force-push supply chain attacks. Upgrade actions/checkout from v4
to v6.0.2 to match release.yml.

Add renovate.json scoped to GitHub Actions only — weekly grouped
PRs with automerge, zero noise from npm dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: move renovate.json to .github/renovate.json

Reduces root directory clutter. Renovate supports this path natively.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:44:25 -08:00
Michael Ramos b1f91936d6 Deploy paste service with E2E encryption (#203)
* feat: Deploy paste service with end-to-end encryption (zero-knowledge)

Add AES-256-GCM client-side encryption to the short URL sharing flow.
Plans are encrypted in the browser before upload — the paste service
stores only ciphertext it cannot read. The decryption key lives only
in the URL fragment (#key=...) and never leaves the browser.

- New packages/shared/crypto.ts with encrypt/decrypt via Web Crypto API
- Encrypt before POST in createShortShareUrl, decrypt on load
- Parse #key= fragment in useSharing.ts for both direct and import URLs
- Deploy Cloudflare Worker with KV namespaces to workers.dev
- Add deploy-paste CI/CD job to deploy.yml
- Fix Cache-Control from public to private, no-store
- Update ExportModal strings with encryption messaging
- Update README, docs, and blog with zero-knowledge encryption details

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review: fix short URL fallthrough and regex anchor

- Return false early when paste fetch fails on /p/<id> path instead of
  falling through to parseShareHash() (which can never succeed since the
  hash contains #key=, not plan data)
- Add end-anchor to importFromShareUrl regex to prevent false matches on
  URLs with extra path segments (regression from PR #188)
- Remove demo plan NOTE from README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:21:53 -08:00
Gunit Bindal c470dcebf1 Add short URL sharing via paste service to fix URL truncation (#188)
* Add short URL sharing via paste service to fix URL truncation on Slack/messaging apps

Share URLs for large plans can be 10-40KB+ because the entire plan + annotations
are compressed into the URL hash. Services like Slack, WhatsApp, and Twitter
truncate these URLs, making shared plans unviewable (related: #142).

This adds an optional paste-service backend that stores compressed payloads and
returns short ~60-char URLs (e.g. share.plannotator.ai/p/aBcDeFgH).

Changes:
- Add Cloudflare Worker paste service (apps/paste-worker/) with KV storage
  and 90-day TTL for stored plans
- Add createShortShareUrl() and loadFromPasteId() to sharing utils
- Update useSharing hook to auto-generate short URLs with 1s debounce
- Update ExportModal to show short URL as primary copy target with
  full hash URL as backup
- Portal automatically supports /p/<id> paths via useSharing hook
- Fully backward compatible: hash-based URLs continue to work unchanged
- Graceful degradation: falls back to hash URLs if paste service is unavailable

* Fix critical bugs found during code review of short URL feature

- Worker: return only { id } so client constructs URL with its own
  shareBaseUrl (fixes self-hosted deployments)
- importFromShareUrl: handle /p/<id> short URLs in addition to hash
  URLs (fixes teammate import via short links)
- Anchor /p/<id> regex with ^ to prevent false matches on nested paths
- Pass shareBaseUrl to loadFromPasteId (was always defaulting)
- replaceState preserves base path instead of hardcoding /
- Clear stale shortShareUrl immediately on debounce to prevent showing
  outdated link during the 1s delay
- Add shareBaseUrl to dependency arrays for loadFromHash and
  importFromShareUrl callbacks
- Remove unused url field fallback from paste API response parsing

* Fix pasteApiUrl/shareBaseUrl confusion and add remote session share URLs

Bug fix (addresses @backnotprop's code review):
- useSharing hook now accepts separate `pasteApiUrl` parameter instead of
  incorrectly passing `shareBaseUrl` to `loadFromPasteId`. These are
  different domains (share portal vs paste backend). When omitted, the
  default `https://paste.plannotator.ai` is used correctly.
- Fixed in all three call sites: loadFromHash, importFromShareUrl, and
  generateShortUrl (via createShortShareUrl options).

Remote session share URLs (implements #192):
- When running on a remote instance (SSH, devcontainer), Plannotator now
  generates a share.plannotator.ai URL and prints it to stderr so the
  user can open the plan review in their local browser.
- Works for all three modes: plan review, code review, and annotate.
- Uses the same deflate-raw + base64url encoding as the client.
- Fails silently if URL generation fails — port forwarding still works.
- New server utility: packages/server/share-url.ts

* Address code review findings: fix spread limit, CORS, deps array

- share-url.ts: Replace btoa(String.fromCharCode(...compressed)) with
  a loop to avoid RangeError on plans >65K compressed bytes
- share-url.ts: Add cross-reference comment noting the server-side
  SharePayload is an intentional subset of the canonical UI type
- paste-worker: Only return CORS headers for allowed origins; disallowed
  origins get no CORS headers instead of misleading partial headers
- useSharing.ts: Remove unused shareBaseUrl from loadFromHash dependency
  array (loadFromHash only uses pasteApiUrl, not shareBaseUrl)

* Restructure paste service with pluggable storage, fix consent flow and security issues

Replaces monolithic paste-worker with multi-target paste-service architecture:
- Core logic (handler, storage interface, CORS) separated from deployment targets
- Filesystem store (self-hosted), KV store (Cloudflare), S3 stub (future)
- Bun binary target + Cloudflare Worker target
- Fix auto-upload removed: short URLs only created on explicit user click
- Clear stale short URL state when content changes
- Fix require('fs') in ESM module, add path-traversal guard in FsPasteStore
- Return 400 (not 500) for malformed JSON in both targets
- Fix compress() spread limit for large plans
- Updated docs: self-hosting guide, sharing guide, env vars, API endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Simplify paste service: extract shared router, remove TOCTOU patterns

- Extract duplicated HTTP routing from bun.ts and cloudflare.ts into
  handleRequest() in core/handler.ts — targets are now thin wrappers
- Move ID_PATTERN regex to core (was duplicated in both targets)
- Remove existsSync guard before mkdirSync({recursive:true}) in fs.ts
- Remove file.exists() check before file.json() in fs.ts get() — the
  try/catch already handles missing files
- Extract DEFAULT_SHARE_BASE constant in sharing.ts (was hardcoded twice)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Deduplicate compression, image parsing, and remote share URL logic

- Create packages/shared/ with compress/decompress (single source of truth)
  — both @plannotator/server and @plannotator/ui import from here
- Export parseShareableImages from sharing.ts, remove duplicate
  parseGlobalAttachments from useSharing.ts
- Extract writeRemoteShareLink() helper in share-url.ts, replace 3
  near-identical blocks in apps/hook/server/index.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix modulo bias in paste ID generation and remove nonexistent doc artifacts

- Use rejection sampling in generateId() to eliminate modulo bias
  (bytes >= 248 discarded, uniform distribution over 62 chars)
- Remove references to install-paste.sh and Docker image that don't
  exist yet; point self-hosting docs to GitHub Releases binaries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:55:55 -08:00
Michael Ramos af18db7aab fix(ci): add NPM_TOKEN to publish steps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:31:07 -08:00