offplan.online · plan · ops infrastructure

Repo as canonical store

Invert the source of truth. The git repo becomes canonical for sessions, workstreams, plans, ADRs, learnings. /handoff and /resume never touch Notion at runtime. Notion becomes a downstream mirror.

Ratified · 2026-05-11 Council score 9.6/10 Owner: Roman 2 workstreams 13 phases Supersedes operator-aware-handoff-resume

Goal

Invert the source of truth. The git repo becomes the canonical store for sessions, workstreams, plans, ADRs, and learnings. /handoff and /resume never touch Notion at runtime. Notion becomes a downstream read-only mirror, updated by a future async sync worker (Plan 2). Roman and Sergei stay in sync via git pull; Notion outages, MCP-not-loaded failures, and CONV-N collisions all become non-issues by construction.

Success criteria

Five operational checks. All measurable, all in scope for the flip workstream.

01
ID + atomicity. Session files named <INITIALS>-YYMMDD-NN.md (2-digit zero-padded). Atomic-create via O_CREAT | O_EXCL. Two parallel terminals racing in the same second never collide.
02
Rich content, mobile-reorderd. Frontmatter and body match the agreed schema. Human-meaningful fields (title, operator, summary, workstream, tags) at the top; telemetry / sync-metadata at the bottom so GitHub mobile readers see meaning first.
03
Workstream linkback. Touched workstream's Session Log gains an entry. INDEX.md MOCs auto-regenerated. HTML render of touched plan/workstream auto-regenerated to docs/rendered/.
04
Git, explicit pathspecs. Commit + push only the explicit paths touched. Warn (non-blocking) if other dirty files exist outside the handoff pathspec.
05
Notion-silent runtime. tool_calls_breakdown.mcp__notion_offplan = 0 asserted in every session frontmatter. The hook aggregates every mcp__notion_offplan__* tool name into this single canonical counter. /handoff prints a loud WARNING if non-zero.
The architecture invariant. Criterion 5 is the trip-wire. The telemetry hook flattens every mcp__notion_offplan__* tool name into one canonical counter. /audit greps for any future regression. Any drift back to "let's just query Notion real quick" is caught by the stats themselves.

Approach

C — two workstreams under one plan: "flip" then "vault polish". The user-visible architectural flip ships as one coherent change (no half-states where /handoff is Notion-silent but /resume isn't). The Obsidian vault polish ships as a P1 follow-up.

Realistic estimate: each workstream is 2–3 /build sessions, not 1–3. Phase 4 + Phase 9 alone are ~1.5 sessions each.

Implementation principles

  • Repo is authoritative. Every "what if Notion is unavailable?" answers itself by removing Notion from the runtime path.
  • Local-first ID generation. Session IDs generated by a Python resolver scanning docs/sessions/; atomic-create. Notion's unique_id (CONV-N) becomes a vestigial artefact.
  • External ID is filename-derived, not CONV-derived. The three legacy CONV-30 variants get three distinct external_ids; Plan 2 won't clobber 2 of 3.
  • Frontmatter has a single library. scripts/lib/frontmatter.py consumed by /handoff, /resume, backfill, audit, validate, and Plan 2. Schema enforced via JSON Schema + pre-commit validator.
  • Hook concurrency hardened with flock. fcntl.flock + unique per-invocation tmp filenames + orphan sweep. Read-modify-write of the state JSON is wrapped in LOCK_EX. The 50-parallel test must pass before P2 ships.
  • Source files in plain MD-link form. [Display](./file.md) everywhere. Wikilinks rejected for source files.
  • Frontmatter mobile-reorderd. The top 5 fields fit on a phone screen.

Phases

14 phases (Phase 0 + 1–13). Each ships as part of one of the two workstreams.

#PhaseWorkstreamOutput
0Shared frontmatter library + JSON schemasflipscripts/lib/frontmatter.py, scripts/schemas/*.json, scripts/validate_frontmatter.py
1Consolidated session resolverflip.claude/operators.yaml, scripts/session.py with whoami + claim subcommands; atomic-create primitive
2Hook-based session telemetryflipscripts/hooks/session_state.py — flock + unique tmp + canonical key aggregation
3/handoff rewrite (Notion-silent)flipSteps 0 → 0.5 confirm → 0.7 first-handoff hint → 1 (--quick) → 2 (state) → 3 (git stats) → 4 (markdown + INVARIANT warn) → 4.5 (regen INDEX + HTML render) → 5 (workstream log) → 6 (explicit pathspecs) → 7 (commit) → 8 (sweep state)
4/resume rewrite (legacy-aware)flipScans both <INITIALS>-*.md AND backfilled CONV-*.md via operator: frontmatter. Empty-result branch. Cache for <5s perf.
5Session-note schema (canonical contract)flipMobile-reorderd frontmatter; content_hash canonicalisation; sync_status state machine; minimum-viable body branch
6Variant wrappers (Bash-driven)flip4 wrappers handoff-rt/handoff-sk/resume-rt/resume-sk with literal OPERATOR= in Bash command line
7Workstream / plan / ADR / learning schema upgradeflipTemplate updates; new folders for sync/learnings/state/state-archive; .gitignore updates
8Backfill (non-destructive)flipscripts/backfill_frontmatter.py — idempotent, preserves legacy keys including notion_page_id: pending-reconcile; legacy_notes: array; filename-derived external_id (3 distinct for CONV-30 trinity); disposes md_to_notion.py
9Interim manual Notion sync (stopgap)flipscripts/sync_to_notion_oneshot.py — reference impl for Plan 2; rate-limited 3 req/s; --dry-run default
10Obsidian workspace baselinevault.obsidian/{app,appearance,community-plugins,core-plugins}.json committed; personal-state files gitignored
11Per-folder INDEX.md MOCs + paired HTML rendersvaultscripts/regen_indexes.py + scripts/render_md.py (markdown → HTML using canonical visual register); 5 INDEX.md MOCs; auto-render of touched plan/workstream HTMLs at /handoff Step 4.5
12Link normalisation auditvaultscripts/audit_links.py sweep + blocking pre-commit hook
13Documentation splitvaultCLAUDE.md lean (~150 lines); reference material moves to docs/conventions/{frontmatter,obsidian,sync-forward-compat,rendering,frontmatter-changelog}.md

Workstreams

repo-as-canonical-store-flip P0 · active · ~2–3 sessions

Ships the user-visible architectural flip: locally-generated session IDs (RT-YYMMDD-NN, atomic-create), Notion-silent /handoff and /resume, rich session content per the schema, concurrency-hardened telemetry hook, interim manual sync stopgap. Absorbs all work from the superseded v1 operator-aware-handoff-resume workstream.

Phases: 0–9.

What's next: P0 — build scripts/lib/frontmatter.py (deterministic content_hash, LF normalisation, SHA-256 of body after closing ---\n) + 5 JSON schema files + validate_frontmatter.py. Wire as non-blocking pre-commit warning. Everything else depends on it.

repo-as-canonical-store-vault P1 · blocked-by flip · ~1–2 sessions

Ships the Obsidian vault layer: committed .obsidian/ baseline, per-folder INDEX.md MOCs, markdown→HTML render automation, link normalisation audit, CLAUDE.md split into docs/conventions/*.

Phases: 10–13.

What's next: Blocked until flip workstream ships and has ≥3 working days of dogfood. Then start with P10 (Obsidian baseline) — lowest-risk; immediately useful for daily vault open.

Risks & edge cases (top 10)

#RiskMitigation
1Hook lost-update raceflock + unique tmp filenames + atomic rename. 50-parallel test gates P2 done.
2Architecture-invariant key driftmcp__notion_offplan single canonical key; hook flattens; loud WARN at /handoff if non-zero.
3First-run null vs zeroExplicit default {mcp__notion_offplan: 0} when state file missing. Never null.
4APFS O_EXCL reliability on iCloud pathsResolver warns if repo path contains Mobile Documents / CloudStorage. mkdir -p defensively. 99-retry cap.
5Variant wrapper soft mechanicBash-driven OPERATOR= prefix in literal command. End-to-end verified before backfill.
6Operator confirmationStep 0.5 prints resolved operator + email + waits for Enter. Prevents Sergei-on-Roman's-Mac misattribution.
7Legacy CONV-30 trinityBackfill derives external_id from filename basename. 3 distinct IDs.
8Backfill clobbering legacy keysBackfill skips existing non-null values; legacy_notes: [] consolidation; preserves notion_page_id: pending-reconcile.
9/resume scale beyond 200 sessionsCache regenerated at /handoff Step 8. Re-evaluate when count crosses 200 (~Q1 2027).
10Plan-1 → Plan-2 Notion stalenessPhase 9 interim manual sync script bridges the gap.

Council review

6 reviewers ran in parallel during plan ratification: Roman's UX, Sergei's onboarding, data integrity, failure modes, code health, Perplexity Sonar Pro adversarial. 37 findings surfaced; 5 CRITICAL + 19 HIGH fixed in plan v2; 10 MEDIUM acknowledged in Risks; 4 deferred to follow-up workstreams.

Score progression

Dimensionv1v2Δ
Completeness9.09.5+0.5
Sequencing10.010.0
Risk management8.09.5+1.5
Testability9.09.5+0.5
Estimate realism7.09.0+2.0
Integration coherence9.010.0+1.0
Overall8.69.6+1.0

Forward-compat for Plan 2 (sync worker)

Plan 1 bakes in everything Plan 2 needs. Plan 2 becomes a focused workstream:

  • Frontmatter contract complete (sync-metadata fields present in every file from flip-ship).
  • docs/sync/.gitkeep exists for LAST_SYNC.md audit log destination.
  • content_hash reference function in scripts/lib/frontmatter.py — Plan 2 imports it for canonicalisation parity.
  • sync_status state machine documented; worker NEVER writes manual_override.
  • Platform: Cloudflare Workers cron — wrangler authenticated, account b16caee7481d783c292d26ec76782cb1, CF Pages already live at preview.offplan.online.
  • Notion DB additions required before Plan 2 ships (one-time, UI): External ID, Operator, Content Hash, Last Synced, Sync Status.
  • Worker conflict policy: clobber-with-audit-log. sync_status: manual_override is the only escape hatch.
  • Phase 9's interim manual sync script is the TypeScript port's reference impl.
  • Future Supabase pgvector: summary + tags + content_hash already in frontmatter.