Skip to content

feat(cloud): Phase 5 — Supabase cloud sync for team wiki sharing#172

Merged
nmrtn merged 25 commits into
mainfrom
feat/supabase-cloud-sync
Jul 3, 2026
Merged

feat(cloud): Phase 5 — Supabase cloud sync for team wiki sharing#172
nmrtn merged 25 commits into
mainfrom
feat/supabase-cloud-sync

Conversation

@nmrtn

@nmrtn nmrtn commented Jul 1, 2026

Copy link
Copy Markdown
Owner

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.sqlnanopm_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.sqlnanopm_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. Includes competitors/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 in nanopm_preamble on 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 status shows 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 synced for 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 sources
  • nanopm-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:

NANOPM_SUPABASE_URL=https://YOUR-PROJECT-REF.supabase.co
NANOPM_SUPABASE_KEY=sb_publishable_YOUR-KEY

Resolution order: env vars → .env file. The publishable key (sb_publishable_...) is correct here — RLS is enabled.

/pm-setup-cloud skill

Interactive onboarding: detects existing config, shows instructions to write ~/.nanopm/.env in the terminal (never in chat), verifies credentials without printing them, runs the migration via Supabase MCP if available, does the initial sync.

Security

cmd_pull and cmd_raw_pull now validate every path field received from Supabase against a relative_to() guard before writing — ensuring a tampered row can't write outside .nanopm/wiki/ or .nanopm/raw/. Same pattern already in cmd_push_file.

What's next (Phase 5.5)

  • --force flag on pull to bypass hash check (clean overwrite from a teammate)
  • pgvector semantic search (replace SQLite FTS5 when Supabase is configured)
  • JWT-scoped RLS (per-user access control instead of permissive allow-all)
  • Web viewer (read-only dashboard for stakeholders without the CLI)

For Guillaume — how to sync

One-time setup:

  1. Pull this branch (or wait for it to merge to main):

    git pull && git checkout feat/supabase-cloud-sync
    ./setup
  2. Get the Supabase URL + publishable key from Nicolas (out of band — Slack, 1Password, etc.)

  3. Create ~/.nanopm/.env in your terminal:

    cat > ~/.nanopm/.env << 'EOF'
    NANOPM_SUPABASE_URL=https://YOUR-PROJECT-REF.supabase.co
    NANOPM_SUPABASE_KEY=sb_publishable_YOUR-KEY
    EOF
  4. Push your wiki and raw sources to Supabase (coordinate with Nicolas first):

    nanopm-ingest-agent --project . sync
  5. Nicolas then runs:

    nanopm-ingest-agent --project . pull

From then on, everything is automatic — push happens after every wiki write, pull happens at the start of each skill run.

🤖 Generated with Claude Code

@nmrtn
nmrtn force-pushed the feat/supabase-cloud-sync branch from 966eff0 to 76f9c21 Compare July 1, 2026 12:29
@nmrtn

nmrtn commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

Rebased on the updated #171 (which now gitignores derived files).

One cascading fix here: cmd_sync previously pushed everything including index.md, log.md, and INDEX/LOG/SCHEMA files — which are now gitignored and derived. Added SKIP_ON_SYNC to exclude them. Only source entity pages and doc pages get pushed to Supabase. Team log history goes through the nanopm_wiki_log table, not file sync.

@nmrtn
nmrtn force-pushed the feat/supabase-cloud-sync branch 3 times, most recently from c8b5558 to 7607d90 Compare July 1, 2026 15:43
nmrtn and others added 5 commits July 1, 2026 17:49
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>
@nmrtn
nmrtn force-pushed the feat/supabase-cloud-sync branch from 7607d90 to b37ac6a Compare July 1, 2026 15:49
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>
@nmrtn

nmrtn commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

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 nanopm-ingest-agent apply (which already called _supabase_upsert_page). Doc pages — retro, PRD, roadmap, competitors, feedback, personas, etc. — are written directly by skills using the Write tool and were silently skipped. They only reached Supabase via a manual nanopm-ingest-agent sync.

Fix (ea21f07):

  • New push-file --target <wiki-relative-path> subcommand in nanopm-ingest-agent: reads the file from disk, security-checks it stays inside wiki/, upserts to Supabase.
  • nanopm_wiki_doc_log now calls push-file after the log heartbeat. Every skill already calls this function as its last step after writing a wiki page — so all 20+ skills get the push with zero skill-file changes.
  • The path is extracted from the title arg by stripping the leading verb: "wrote docs/retro-2026-07-02.md""docs/retro-2026-07-02.md".
  • Entity pages pushed twice (via apply + via doc_log) — idempotent upsert, no problem.
  • Best-effort throughout: silent no-op when Supabase isn't configured, silent on failure.

The "everything is automatic" line in the PR description is now actually true.

@guillaumesimon

Copy link
Copy Markdown
Collaborator

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.

cmd_pull (bin/nanopm-ingest-agent) writes pages with dest.write_text(...) without taking wiki_lock — unlike cmd_apply/cmd_reindex — and it runs in nanopm_preamble on every skill start. A hand-edited page whose last_updated wasn't bumped gets silently replaced by a newer remote, plus there's a small race with a concurrent apply. Suggest wrapping the write in wiki_lock and logging overwritten pages. Minor: the PR says "remote wins on conflict" but the code is actually "newest wins, local wins on a tie."

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.

The migration creates nanopm_wiki_pages, nanopm_wiki_log and nanopm_projects, but only nanopm_wiki_pages is ever upserted. The "Fix D" commit says team log history goes through nanopm_wiki_log, yet nothing writes to it. Either wire up the log mirror, or drop the two unused tables from the MVP and adjust the wording so it matches what ships.

3. A few small finishing touches (non-blocking)

Nothing here blocks it — mostly polish plus one Git housekeeping item.

  • Blocking network calls: cmd_pull uses timeout=10 inside the preamble, per-page push uses timeout=5 × N pages — a noticeable stall at skill start on a slow connection; maybe ~2–3s for the pull.
  • cmd_sync's "not configured" error still points to supabase_url in ~/.nanopm/config; since the .env switch it should reference NANOPM_SUPABASE_URL in ~/.nanopm/.env.
  • PR base is feat/wiki-search-team-sharing, but feat: wiki team sharing — git tracking, SQLite FTS5 search, companies/ deprecation, compact index #171 is already merged to main — re-point to main for a clean merge.
  • Tiny/optional: unused project param in _supabase_creds, redundant import argparse in cmd_pull, synced_at not refreshed on upsert, and SKIP_ON_SYNC lives only in cmd_sync (not the auto-push path).

@guillaumesimon

Copy link
Copy Markdown
Collaborator

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 .nanopm/raw/ into the sync — can we scope that, either in this PR or a fast follow-up?

Technical detail (from Claude):
Raw isn't covered by any sync path today: cmd_apply refuses targets outside .nanopm/wiki/, so the auto-push never sees it, and cmd_sync only globs .nanopm/wiki/**/*.md. Raw lives in .nanopm/raw/<type>/<id>.* — a sibling dir, and not necessarily Markdown. Bringing it in would likely mean a dedicated table (e.g. nanopm_raw_sources) plus a separate push path that handles arbitrary file types, reusing the existing content-hash dedup (raw-check / nanopm_archive_raw, 12-hex sha256 id) so identical sources aren't re-uploaded. Worth deciding how large transcripts are handled (size limits).

nmrtn and others added 2 commits July 2, 2026 10:18
- 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>
@nmrtn
nmrtn changed the base branch from feat/wiki-search-team-sharing to main July 2, 2026 08:25
@nmrtn

nmrtn commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Addressed all three points — here's what changed:

Point 1 — hand-edit protection (now implemented, not just noted)
After every push or pull, sha256(content) is stored per page in ~/.nanopm/sync-state/{project}.json. On the next pull, if the local hash has diverged from the stored baseline, the file is skipped with a message instead of silently overwritten. The PR description's "remote wins on conflict" is corrected to "newest wins; local wins on tie".

Point 2 — nanopm_wiki_log wired
cmd_log now calls _supabase_log_entry() after the local write — a POST to nanopm_wiki_log with project_slug, op, title, day. Same best-effort pattern as page upserts. Team activity log now actually reaches Supabase as the PR claimed.

Point 3 — error message + base branch
cmd_sync "not configured" error now references NANOPM_SUPABASE_URL / NANOPM_SUPABASE_KEY in ~/.nanopm/.env. PR base re-pointed to main.

Bonus — status subcommand
nanopm-ingest-agent status reports local sync state offline (no Supabase call): N in sync · M modified locally · K never synced, with file-level detail for the modified ones. Useful before pushing or after a pull to confirm where things stand.

The wiki_lock concern in cmd_pull is valid — deferred to a follow-up PR as it's a narrow race window.

@nmrtn

nmrtn commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

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>
@nmrtn

nmrtn commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Raw sync is in — commit d045510.

Everything under .nanopm/raw/ is now synced: feedback archives, manifests, interview transcripts, competitor intel reports, events.jsonl, and competitors/snapshots/ (included so you get the diff baseline and don't see "BASELINE" results on your next intel run).

sync and pull handle everything — no change to how you use them.

nmrtn and others added 2 commits July 2, 2026 11:08
…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>
nmrtn and others added 5 commits July 2, 2026 11:56
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>
nmrtn and others added 9 commits July 2, 2026 14:21
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>
@nmrtn
nmrtn merged commit 7909649 into main Jul 3, 2026
@nmrtn
nmrtn deleted the feat/supabase-cloud-sync branch July 6, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants