feat(cloud): Phase 5 — Supabase cloud sync for team wiki sharing#172
Conversation
966eff0 to
76f9c21
Compare
|
Rebased on the updated #171 (which now gitignores derived files). One cascading fix here: |
c8b5558 to
7607d90
Compare
Schema: - supabase/migrations/001_nanopm_wiki.sql: nanopm_wiki_pages, nanopm_wiki_log, nanopm_projects tables + pgvector + RLS Push path (automatic): - nanopm-ingest-agent apply now upserts to Supabase after local write (_supabase_upsert_page — best-effort, silent on failure) Bulk sync commands: - nanopm-ingest-agent sync: push all wiki pages to Supabase (initial setup) - nanopm-ingest-agent pull: fetch remote pages, remote wins on conflict, runs reindex after (throttled via preamble, manual override available) Pull path (automatic in preamble): - nanopm_supabase_configured(): checks supabase_url + supabase_key in config - nanopm_pull_from_supabase(): throttled (10 min), called after wiki_ensure in nanopm_preamble so context is always fresh before skills load it Setup skill: - pm-setup-cloud/SKILL.md: interactive onboarding — detect existing config, prompt for URL + key, run migration (MCP/CLI/manual), initial sync, confirm Register: - pm-setup-cloud added to _SKILL_LIST (setup) and plugin.json skills Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ly input Credentials must not appear in conversation history. Phase 2 now tells the user to run nanopm_config_set commands themselves in their terminal (! prefix for in-session), never paste URL/key into the chat. Phase 3 verifies credentials are present without printing them. Teammate instructions updated: share credentials out-of-band (Slack, 1Password). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Credentials (NANOPM_SUPABASE_URL, NANOPM_SUPABASE_KEY) now live in ~/.nanopm/.env — the standard pattern developers already know. Resolution order: env vars → .env file. This keeps secrets out of ~/.nanopm/config (which is plain key=value and less obviously secret-scoped). - _supabase_creds(): reads env vars first, then parses ~/.nanopm/.env - nanopm_supabase_configured(): same resolution order in shell - pm-setup-cloud skill: instructs user to write ~/.nanopm/.env themselves in their own terminal — credentials never appear in the conversation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…able_...) Supabase renamed "anon key" to "publishable key" with sb_publishable_ prefix. Update skill instructions and migration comment to use the new terminology. Code unchanged — REST API accepts both legacy JWT and new key formats. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iew) Derived files (index.md, log.md, and entities/*/INDEX|LOG|SCHEMA.md) are gitignored and regenerated on every reindex — they must not be pushed to Supabase. SKIP_ON_SYNC extends the existing HOUSEKEEPING constant (already used for entity dirs) to also cover the wiki root files. Team log history goes through the nanopm_wiki_log Supabase table, not file sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7607d90 to
b37ac6a
Compare
Every skill already calls nanopm_wiki_doc_log as its final step after writing a wiki page. Hook the Supabase push there so doc pages (retro, PRD, roadmap, competitors, feedback, etc.) reach Supabase automatically — no manual 'nanopm-ingest-agent sync' needed. How it works: - nanopm_wiki_doc_log now extracts the wiki-relative path from the title arg (convention: "wrote docs/X.md", "add entities/Y.md") and calls push-file when Supabase is configured. - New push-file subcommand in nanopm-ingest-agent reads the file from disk and upserts it. Security: path is checked to stay inside wiki/. - Entity pages written via `apply` already pushed on write — this closes the gap for doc pages written directly by skills. - Best-effort, non-blocking: failure is always silent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Fixed a gap in the auto-push coverage. What was missing: the PR description said "push happens after every wiki write" but that was only true for entity pages written via Fix (ea21f07):
The "everything is automatic" line in the PR description is now actually true. |
|
Went through the PR — really nice work, and I'm glad the wiki finally becomes something we share. Quick note on format: I reviewed this with Claude. The plain-English intro on each point is mine; the technical detail underneath is generated by Claude. 1. We could lose a manual edit without noticing If I tweak a page by hand and the cloud has a newer version, mine can get quietly overwritten — no warning, no trace. Fine if we always go through the skills, but I'd like a guardrail.
2. One thing the description promises isn't wired up yet The PR says the team activity log is shared through the cloud, but as far as I can tell it isn't — only page content syncs. Mostly want to make sure we don't assume it's live when it isn't.
3. A few small finishing touches (non-blocking) Nothing here blocks it — mostly polish plus one Git housekeeping item.
|
|
Following up on my own review — I'd also like us to sync the raw sources, not just the wiki. To me the raw content (archived feedback, interview transcripts, etc.) is at least as important as the wiki pages — arguably more, since it's the ground truth everything else is derived from. If the whole point of Phase 5 is that our shared PM memory lives in one place, then a fresh clone (or anyone relying on the cloud) is missing half of it without the raw. I'd love to bring Technical detail (from Claude): |
- Add _supabase_log_entry(): mirrors every cmd_log heartbeat to the nanopm_wiki_log table (project_slug, op, title, day). Same best-effort pattern as _supabase_upsert_page — silent no-op when not configured. - Call _supabase_log_entry() in cmd_log after the local write, so team activity log now actually reaches Supabase as the PR description claims. - Fix cmd_sync "not configured" error: reference NANOPM_SUPABASE_URL / NANOPM_SUPABASE_KEY in ~/.nanopm/.env (not the old ~/.nanopm/config path). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ommand
Problem: cmd_pull compared last_updated frontmatter timestamps to decide
whether to overwrite. Hand edits don't bump last_updated, so a local edit
could be silently overwritten on the next pull if Supabase had a newer ts.
Fix:
- After every successful push (_supabase_upsert_page) and every pull write
(cmd_pull), record sha256(content) per wiki path in
~/.nanopm/sync-state/{project_slug}.json — the "last synced baseline".
- cmd_pull now checks the stored hash before overwriting: if the local file
diverges from the stored hash (hand edit), it skips with a message instead
of silently overwriting. Falls back to timestamp comparison for untracked
files (first sync, pre-existing wikis).
- New 'status' subcommand: offline diff of local wiki vs stored hashes —
reports pages in sync / modified locally / never synced. No Supabase call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressed all three points — here's what changed: Point 1 — hand-edit protection (now implemented, not just noted) Point 2 — Point 3 — error message + base branch Bonus — The |
|
working on raw content sync now. I'll let you know when ready. |
Adds raw source sync so teammates share the full PM memory — not just the wiki pages but also the archived feedback, interview transcripts, competitor intel reports, and snapshots that everything else is derived from. Extends cmd_sync to push all raw files, adds cmd_raw_pull (and wires it into cmd_pull), and updates cmd_status to report raw alongside wiki. New migration 002_nanopm_raw.sql creates the nanopm_raw_sources table with content_hash for dedup and hand-edit protection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Raw sync is in — commit Everything under
|
…ed check - cmd_pull and cmd_raw_pull now validate row["path"] from Supabase stays inside the target directory before writing (same guard already in cmd_push_file) - pm-setup-cloud preamble used nanopm_config_get (reads config file) to detect credentials stored in ~/.nanopm/.env — replaced with nanopm_supabase_configured Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RL crash CSO audit findings: - cmd_raw_pull: row["content"] crashes with TypeError on NULL content (schema: nanopm_raw_sources.content is nullable); use .get() with empty fallback - cmd_pull: same null guard added for defense-in-depth - cmd_sync: IndexError if SUPABASE_URL has no '://' (outside try/except); wrap the hint extraction with try/except - pm-setup-cloud: add chmod 600 to .env creation snippet so credentials are not world-readable on multi-user machines Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows a green "In sync" dot or amber "N to sync" count in the viewer's sidebar footer when Supabase cloud sync is configured. Runs offline via nanopm-ingest-agent status — no network call. Refreshes on project open, every 60 s, and immediately after any skill run completes. Hidden when ~/.nanopm/.env is absent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tasks on SyncStatusBadge were silently dropped in release builds when the badge body was EmptyView() (status still idle). Moving .task, .onReceive, and .onChange to the sidebarFooter HStack guarantees they fire regardless of badge render state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… registry)
The wiki/raw tables are already multi-tenant (every row carries project_slug;
one Supabase instance holds many projects, no DB-per-project), but nothing
populated nanopm_projects — the registry that enumerates which projects live
in a shared instance. So a teammate's viewer couldn't list projects; it had
to already know each slug.
Add _supabase_ensure_project(): on the first push of a project (via bulk sync
or the per-write auto-push), upsert one nanopm_projects row. Guarded by a
'__project_registered__' sentinel in the per-project sync-state so it fires
exactly one insert per project and adds no per-page HTTP on bulk sync. Uses
resolution=ignore-duplicates so an existing/edited registry row is never
clobbered. Best-effort — never raises; the page/raw push already succeeded.
Verified against the live project: nanopm_projects gains {slug,name,created_at};
two syncs leave exactly one row (idempotent); raw-archive / skill-syntax /
plugin-manifest green.
Follow-up (not in this commit): derive project_slug from the git remote rather
than the local folder name, so two identically-named folders can't collide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ↺ button now refreshes both the wiki store and the sync badge in one tap — no more stale "N to sync" after a terminal skill run. Timer dropped from 60s to 15s so the badge self-corrects within one skill-run's worth of time without a network call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sequential await meant the sync badge check waited for the full wiki scan to complete before running. Two separate Tasks kick off concurrently so the badge updates immediately on button tap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Timer.publish inside a computed var sidebarFooter was reset on every SwiftUI re-evaluation, so the 15s countdown never completed. A while loop inside .task(id: project.path) is stable across re-renders and only cancelled when the project changes or the view disappears. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ndpoints Without the on_conflict param, every re-push generated a fresh UUID primary key, hit the (project_slug, path) secondary unique constraint with HTTP 409, and was silently swallowed by `except Exception: pass` — leaving the local sync-state hash forever out-of-date and causing the badge to show pages as pending indefinitely even after a successful sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…able, not derived INDEX.md and LOG.md are regenerated on every reindex and must not be synced. SCHEMA.md is written once at bootstrap and is user-editable — it should be pushed and pulled like any other wiki page. Incorrectly including it in HOUSEKEEPING caused it to silently disappear after a clean pull, making the viewer show "Bootstrap the database" even with a fully populated opportunity DB. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…able badge - bin/nanopm-ingest-agent: add `push-pending` subcommand — pushes only modified/ untracked wiki and raw files (O(pending) vs O(all) for bulk sync). Used by the viewer as a fast catch-up after a failed inline auto-push. - SyncStatusMonitor: add push() method (runs push-pending then re-checks status); store lastProjectPath so push() needs no argument at call sites. - SyncStatusBadge: make the "N to sync" pending state a clickable button that triggers push(); shows "Syncing…" spinner while in flight. - ProjectView: after a skill run completes, call push() instead of check() so any files the skill wrote but failed to auto-push are caught up automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…refresh While refresh is in flight the arrow icon becomes a ProgressView spinner, the button is disabled, and the tooltip updates. The refresh now also runs push-pending concurrently with the artifact store reload so any stranded local changes are synced to Supabase in one click. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bin/nanopm-ingest-agent: add `remote-status` subcommand — fetches only path+last_updated metadata from Supabase (no content), counts pages where remote is newer than local, prints "remote status: N page(s) to pull". - SyncStatusMonitor: add pullPending counter + checkRemote() (runs remote-status) + pushAndPull() (push-pending → pull → refresh both badges). push() now delegates to pushAndPull() so all existing call sites get pull for free. Badge handles 4 states: to sync only / to pull only / both / in sync. - ProjectView: refresh button and post-skill-run handler both call pushAndPull() so remote changes are fetched automatically without manual intervention. - lib/nanopm.sh (nanopm_preamble): push-pending runs before the pull so each skill starts with local edits already in Supabase, then pulls remote changes. Ensures bi-directional sync on every skill invocation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, skip binaries, normalize timestamps - URL-encode project_slug in all three PostgREST query params (urllib.parse.quote) - Treat stored_hash=None (never-synced local files) as hand-edit protection during pull - Skip binary/unreadable files in raw sync instead of corrupting them with errors='replace' - Normalize last_updated timestamps to [:10] before comparison to avoid date vs datetime mismatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… state collision, throttle, Swift race - Guard null/empty content from Supabase before writing (would empty local files) - Move dest path construction inside try-block to catch null bytes in remote paths - Use full-path hash in sync-state filename to prevent collision between repos with same dirname (one-time migration from old slug-only filename) - Throttle push-pending in nanopm_preamble to once per 10 min (same as pull) — was hashing all wiki+raw files on every skill invocation - Capture lastProjectPath before first suspension in pushAndPull() to prevent wrong-project badge refresh if polling loop mutates it mid-await - Remove unused project parameter from _supabase_creds() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused nanopm_wiki_search to return NO_RESULTS for valid queries:
1. Type mismatch: the FTS index stores type as the directory name (plural:
"opportunities", "personas", …) but skill files pass singular forms
("opportunity", "persona", …). Added a TYPE_ALIASES map at query time
so both forms work without touching any skill file.
2. Keyword miss with a known type: a hardcoded keyword that doesn't appear
in any document (e.g. "pain" after outcome-framing rewrites) would kill
the entire query even when the type filter alone would find the right
files. Added a type-only fallback scan when FTS+type returns empty, so
discovery queries (load all opportunities) never silently fail.
Verified with five tests on a live project:
- singular type + bad keyword → now returns results (type-only fallback)
- singular type + good keyword → FTS match works
- plural type (existing callers) → still works
- garbage keyword + type → fallback returns all docs of that type
- garbage keyword, no type → still returns NO_RESULTS (correct)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vision
nanopm's wiki is the shared PM memory of a product — opportunities, personas, decisions, strategy. Until now it lived only on your machine. This PR makes it a team asset.
The architecture is local-first: every write lands on disk first (fast, offline-safe). Supabase is a sync layer on top — push after every write, pull at startup. If Supabase goes down, nanopm keeps working; you're just back to git-based sharing until it comes back.
Three modes, one clean model:
What this PR does
Schema
Two migrations:
001_nanopm_wiki.sql—nanopm_wiki_pages(one row per page, upserted on every write),nanopm_wiki_log(team activity log, mirrored after every skill run),nanopm_projects(project registry). pgvector extension enabled for future semantic search. RLS enabled with the publishable key.002_nanopm_raw.sql—nanopm_raw_sources(one row per raw source file, upserted on sync). Same RLS policy.Raw source sync
.nanopm/raw/— the content-addressed archive of feedback, interviews, and competitor intel that everything else is derived from — is now synced alongside the wiki. Includescompetitors/snapshots/so teammates get the diff baseline and don't see "BASELINE" results on their next intel run. Same hand-edit protection as wiki pages: hash-diverged files are skipped on pull with a warning.Automatic push
Every
nanopm-ingest-agent apply(the locked wiki page writer) now upserts to Supabase immediately after the local write — best-effort, silent on failure. Local write never blocks.Automatic pull
nanopm_pull_from_supabase()runs innanopm_preambleon every skill start, throttled to once per 10 minutes. Fetches pages updated on Supabase since the last pull. Newest wins on conflict (last_updated comparison); local wins on tie. After a fresh clone or a teammate's sync, context is up to date before the skill runs.Hand-edit protection
After every push or pull,
sha256(content)is recorded per page in~/.nanopm/sync-state/{project}.json. On the next pull, if a local file's hash has diverged from the stored baseline (hand edit), it is skipped rather than silently overwritten — with a message: "local edits detected — run 'sync' to push your changes first.".Sync visibility
nanopm-ingest-agent statusshows which local pages and raw sources have been modified since the last sync — offline, no Supabase call. Reports:N in sync · M modified locally · K never syncedfor both wiki and raw.Bulk commands
nanopm-ingest-agent sync— push all local wiki pages and raw sources (initial setup, force-resync)nanopm-ingest-agent pull— pull all remote pages and raw sourcesnanopm-ingest-agent status— show local sync state for wiki and raw (offline)Credentials
Stored in
~/.nanopm/.env— never in the repo, never in the chat. Format:Resolution order: env vars →
.envfile. The publishable key (sb_publishable_...) is correct here — RLS is enabled./pm-setup-cloudskillInteractive onboarding: detects existing config, shows instructions to write
~/.nanopm/.envin the terminal (never in chat), verifies credentials without printing them, runs the migration via Supabase MCP if available, does the initial sync.Security
cmd_pullandcmd_raw_pullnow validate everypathfield received from Supabase against arelative_to()guard before writing — ensuring a tampered row can't write outside.nanopm/wiki/or.nanopm/raw/. Same pattern already incmd_push_file.What's next (Phase 5.5)
--forceflag onpullto bypass hash check (clean overwrite from a teammate)For Guillaume — how to sync
One-time setup:
Pull this branch (or wait for it to merge to main):
git pull && git checkout feat/supabase-cloud-sync ./setupGet the Supabase URL + publishable key from Nicolas (out of band — Slack, 1Password, etc.)
Create
~/.nanopm/.envin your terminal:Push your wiki and raw sources to Supabase (coordinate with Nicolas first):
nanopm-ingest-agent --project . syncNicolas then runs:
nanopm-ingest-agent --project . pullFrom then on, everything is automatic — push happens after every wiki write, pull happens at the start of each skill run.
🤖 Generated with Claude Code