← Corpus / augment-it / plan
Build order: the humain-vc unlock flow, step by step
The execution sequence for Flow 1 (Michael + Aniel, side-by-side thesis corpus building on a hosted augment-it) — each step names its repo, files, and verification so any fresh session can pick up mid-sequence. The strategy and scope cuts live in the ai-labs plan; this is the how.
- Path
- plans/Build-Order-Humain-VC-Unlock-Flow.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Fable 5, Claude Code on Claude Sonnet 5
- Tags
- Plan · Build-Order · humain-vc · Didi-Platform · Auth · Deployment · Curator
Build order: the humain-vc unlock flow
Scope of record: [[../../../context-v/plans/Unlock-Humain-VC-Team-Access-To-Augment-It|Unlock-Humain-VC-Team-Access-To-Augment-It]] (ai-labs level) — Flow 1, the single-tenant scope cut, and the deliberately-NOT-built list. Read it first; this doc only sequences. Identity spec of record:
ai-labs/context-v/specs/Id-Didi-Sh-Identity-Service.md.
State as of writing (2026-07-08 — verify, don’t assume)
- Live URLs:
https://id.didi.sh(identity service on Fly — full magic-link loop operator-clicked in production; Resend domain-verified, senderno-reply@didi.sh),https://didi.sh+www(thesite/conversion surface on Vercel), the GitHub splash. - Steps 1–8 DONE (see their sections): real email, orgs + memberships
seeded local AND prod (Michael = superuser, 3 addresses; Aniel pends his
address), the membership gate proven (4401 / admitted / 4403), the
actor attribution envelope proven live (created_by/updated_by on
domains/sources/source_usages + corpus frontmatter), thesis
vocabulary (Corpora Curator rename, operator-defined + per-workspace-
default domain type,
domain.retypemigration —consumer-immunologyis nowthesis:consumer-immunology), curator liveness (domain/source mutations broadcast over NATS; the curator surface refetches on events from a second session — proven viaLIVENESS=1on the prove script), the pre-auth sign-in wall (a real anonymous-visitor gap found and closed via a newGET /config, not just the session frame), and didi chat v0 (persona rename, curator verbs as chat-invokable verbs with a live domain-name resolver, theinbox-curationagent-skill, and a face — the character headshot composited with the shell’s brand gradient). - id.didi.sh went down and came back: the Fly.io trial ended mid-week, suspending the app; billing fixed, machine restarted, verified live again (JWKS serving, aliases + orgs + memberships all intact in prod). A reminder that this dependency needs a real payment method on file before the DO deploy, not a trial.
- Interleaved but separate:
feature/augment-affiliations(the Augment-From-Affiliations MVP,context-v/specs/Augment-From-Affiliations.md) shipped and merged intorebuild/turbo-rsbuildon 2026-07-08, between steps 5 and 6 of this sequence — a different flow, same repo, not part of this build order. - augment-it workspace-service verifies
didi_sessionon WS upgrade (services/workspace/src/didi.ts); shell has the DidiBadge sign-in AND a “Flows” jumbo popdown (“Build Corpora” → strategyCurator, full-screen); strategy-curator (now “Corpora Curator”) promoted to the head of ROTATION; the active-workspace split-brain fixed at the shell level AND, separately, inside the curator itself (each federation remote’s own@augment-it/workspacesingleton needed its own reconciliation listener — see Step 5 / the 2026-07-07 changelog entry). - Local compose currently running
DIDI_AUTH=required REQUIRED_ORG_ID=humain.vc(flipped from theoptionaldev default for live testing this session — flip back in.envwhen done); the prove script’s GATE mode is the repeatable check either way. - The DO droplet plan was abandoned (167.172.42.247 — still prepped, still paid-for, just not used). Steps 9–10 shipped on Railway instead, plus a real custom domain — see their sections for why and the full list of Railway-specific gotchas hit along the way.
- Live URLs, deployed:
https://augment.didi.sh(shell, single-tenant humain-vc),wss://ws.augment.didi.sh/ws(workspace-service). Both verified live via headless-browser checks against the real deployed URLs, not just localhost. - NEXT: step 12 — the dress rehearsal. Aniel’s membership still isn’t
seeded on prod (carried over from Step 2 — his address was never
confirmed), and nobody has completed an actual human sign-in against
augment.didi.shyet (dev-token echo is off in prod by design, so this genuinely needs a person clicking a real email). Step 11 (corpus sync) needs a Railway-native redesign before it’s real, but doesn’t block 12.
Steps 1–8 are local, each verifiable on the laptop; 9–12 are the deploy tail. Steps marked ⚑ need an operator decision or action first.
Step 1 — Real email for magic links (id-didi-sh) ✅ DONE 2026-07-06
Decided: Resend. Hand-rolled Swoosh adapter over Req (the hex package fights the lockfile); proven end to end — production magic links land in a real inbox. Remaining rider (⚑ operator): verify the didi.sh domain in Resend + its DNS records in Vercel, which lifts the only-send-to-account-owner restriction and flips the sender to no-reply@didi.sh. Original scope follows.
- Add the Swoosh adapter dep (
gen_smtp/resendper pick) tomix.exs; configure inconfig/runtime.exs(prod) with the API key from env — dev keepsSwoosh.Adapters.Localand token echo. - ⚑ Sending-domain DNS in Vercel DNS (didi.sh registrar): the provider’s SPF/DKIM/return-path records.
fly secrets set EMAIL_API_KEY=… -a id-didi-sh, deploy.- Verify:
mix id.seeda throwaway with a real inbox; request a magic link against the deployed service; click it; land signed in on the/accessfallback page. Per the open-graph discipline, check spam placement once.
Step 2 — Org + membership seeding (id-didi-sh) ✅ DONE 2026-07-06
Done on local AND production (mix id.org / id.member; prod via release eval — note the ~s() sigil gotcha and the 512MB requirement, both in the id repo changelog 2026-07-06_06). Aniel’s membership pends his address. Original scope follows.
- New mix task
id.org(create org by domain-as-id + name) andid.member <email-or-didi_id> <org_id> <role>; validate role againstMembership.roles/0. - Seed locally: org
humain.vc; Michael →superuser; Aniel (⚑ confirm address) →org_owner. - On prod later:
fly ssh console -a id-didi-sh→/app/bin/id_didi_sh rpcwith the same context functions. - Verify:
/api/me(signed in as Michael) returns the membership; tests for duplicate-membership upsert semantics.
Step 3 — Membership gate (augment-it, workspace-service) ✅ DONE 2026-07-06
Proven via the prove script’s GATE mode against a required-mode container: anonymous 4401, superuser admitted, signed-in non-member 4403. Fails closed on id-service outage; 60s per-session cache; local compose stays optional. Original scope follows.
- Extend
services/workspace/src/didi.ts: after cookie verify, whenDIDI_AUTH=required, GET${ID_BASE}/api/mewith the cookie forwarded; admit only if memberships includeREQUIRED_ORG_ID(new env) or rolesuperuser. Cache the verdict on the session; re-check on reconnect, not per-frame. - Reject →
socket.close(4403, 'membership required'); the shell surfaces a “no access” state (DidiBadge already knows anonymous-vs-signed-in; add rejected). - Keep
DIDI_AUTH=optionalin local compose;requiredis the deployed instance’s posture. - Verify: extend
scripts/prove-didi-auth.mjs: member admitted, non-member (seed a stranger) rejected with 4403, superuser admitted.
Step 4 — Actor attribution envelope (augment-it) ✅ DONE 2026-07-06
Done and proven live against local dev: dispatch()’s NATS envelope
carries { didi_id, via? }, the resolver stamps created_by/updated_by
(+_via) on domains/sources/source_usages without clobbering unattributed
writes, and content-ingest writes created_by into both the domain
index.md and the source file’s frontmatter. Chat-replayed invokes carry
via: 'didi-agent' through workspace.invoke(capability, args, via).
Changelog: 2026-07-06_03_Actor-Attribution-Envelope-Every-Mutation-Knows-Who-Did-It.md.
Original scope follows.
services/workspace/src/ws.ts: the invoke path passesactor: { didi_id }(from the session) intodispatch();capabilities.tsadds it to the NATS envelope beside the tenant context (see [[../specs/Workspaces-as-Tenant-Primitive|Workspaces-as-Tenant-Primitive]] § tenant-aware envelope).- Handlers stamp
created_by/updated_by: resolver (services/record-surrealdb-resolver/src/domains.ts— domains, sources, source_usages rows) and content-ingest (corpus.ts— frontmatter fields). Chat turns stamp acting user +via: didi-agent. - No consumers — no filtering, no views (the flow plan’s rule).
- Verify: run a
source.addthrough the prove script with a cookie; confirm frontmatter + DB row carry the didi_id. Extendedscripts/prove-didi-auth.mjswith anATTRIBUTION=1mode (domain.create+source.addover an authenticated WS session, asserts the response’screated_bymatches the signed-indidi_id); ran it against local docker compose +id-didi-shon :4000 — passed, and the on-disk frontmatter for both the domain and source file carried the didi_id. Test rows/files cleaned up after (shared SurrealDB Cloud + local corpus filesystem).
Step 5 — Thesis vocabulary, minimal (augment-it) ✅ DONE 2026-07-07 (shipped differently than sketched)
Proceeded independently of [[../explorations/Augment-It-Has-Outgrown-One-Flow-The-Choose-A-Flow-Front-Door]] — that exploration questions whether
strategyCurator’s place at the head of shellROTATIONis the right long-term shape (it argues for a “choose a flow” front door instead), but this step’s per-workspacedomain_typeswap was correct either way and didn’t wait on that question resolving.
Done across two sessions (2026-07-06 UI rename + operator-defined type; 2026-07-07 the per-workspace default + the retype migration), ending up more general than the original sketch:
apps/strategy-curatorUI now reads “Corpora Curator” everywhere (display copy only — package/folder/remote id unchanged).DOMAIN_TYPEthe hardcoded constant is gone;curation.svelte.tscarries a reactivedomainTypefield, operator-editable via a Type field on the “New corpus” form (free text — any value, not a fixed enum).- Per-workspace default, done:
WorkspaceSummarygaineddefault_domain_type(services/workspace/src/workspaces.ts, read from each client’sDEFAULT_DOMAIN_TYPE.env var —clients/humain-vc/.envnow hasDEFAULT_DOMAIN_TYPE=thesis; reach-edu has none, falls back to'strategy'). The curator resolves this on bootstrap AND on every workspace switch, so a fresh humain-vc session (or switching into it mid-session) starts on “thesis” without the operator typing it. domain.retypehandler, done:services/record-surrealdb-resolver/src/domains.ts’sretypeDomain()(DB:domains.type+ everysource_usages.domain_typefor the slug, idempotent — safe to re-run after a partial failure) +services/content-ingest/src/corpus.ts’sretypeDomainFiles()(moves<old-type-plural>/<slug>/→<new-type-plural>/<slug>/, patchesindex.md’stype:line and every source file’sdomains:list entry). Wired as thedomain.retypecapability. Ran it once viascripts/prove-didi-auth.mjs’s newRETYPE=1mode:consumer-immunologyis nowthesis:consumer-immunology, DB + filesystem both confirmed. Along the way, found and fixed a real bug inretypeDomainFiles’s first draft — it reused content-ingest’s file-onlyexists()helper (which isreadFile-based and throwsEISDIRon a directory, silently reading as “not found”) to check directory existence; replaced with a properstat().isDirectory()check.- Still open, genuinely deferred: singular/plural noun rendering through the rest of the UI copy (it says “corpus”/“corpora” generically rather than “Thesis”/“Theses” when that type is active) — cosmetic, no functional gap.
- Verify: confirmed live — humain-vc now resolves
domainType: 'thesis'on load;consumer-immunologyis retyped end-to-end (DB rows + on-disk frontmatter, verified by direct SurrealDB query andcat); reach-edu untouched, still resolves'strategy'.
Step 6 — Curator liveness (augment-it) ✅ DONE 2026-07-08
Done as sketched, with the broadcast owned by the resolver alone (the single service that already runs each mutation’s full DB + content-ingest lifecycle end to end, so it’s the one place that knows a mutation actually succeeded) rather than split across resolver and content-ingest:
services/record-surrealdb-resolver/src/domains.ts’sregisterDomainHandlersgained abroadcast(subject, payload)helper (fire-and-forgetnc.publish, same pattern asworkspaces.ts’sworkspace.active.changed) called after each of the six mutations commits:domain.created,domain.retyped,source.added,source.updated,source.removed,extract.added— payload carries the domain/source slugs,client_slug(orclient_slugsfor retype, which can span clients), andactor.- Those six subjects added to
BROADCAST_SUBJECTSinservices/workspace/src/ws.ts. apps/strategy-curator/src/App.svelte(notcuration.svelte.ts— an$effectneeds a component, andrecord-collector’s App.svelte already set the precedent) watchesworkspace.events, dedups byseqthe same wayrecord-collectordoes, and calls the state singleton’sloadStrategies()(domain events, type-and-client-scoped) or newrefreshSources()(source/extract events, domain-and-client-scoped — refetches without resetting focus/tags, unlike the user-drivenselect()).- Verify: ran the protocol-level equivalent of “two browser windows” —
a new
LIVENESS=1mode inscripts/prove-didi-auth.mjsopens two independently-authenticated WS sessions against the live local stack; session A invokesdomain.createthensource.add; session B (idle, never invokes) asserted receipt ofdomain.createdthensource.addedwith matching payloads, no polling. Passed both.apps/strategy-curator(svelte-check) and the two touched services (tsc --noEmit) all clean. Test domain + source cleaned up from SurrealDB and the humain-vc filesystem after the run, same discipline as step 4’s ATTRIBUTION mode.
Step 7 — Instance posture + sign-in wall (augment-it, shell) ✅ DONE 2026-07-08
No dependency on [[../explorations/Augment-It-Has-Outgrown-One-Flow-The-Choose-A-Flow-Front-Door]]‘s proposed header-level “Build Corpora” jumbo popdown — that’s a plain navigate action with no auth interaction, unlike the front-door shape originally sketched there. Independent work either order.
Done as sketched, with a real gap found and closed along the way: an
anonymous WS upgrade against DIDI_AUTH=required is rejected (4401) BEFORE
any session frame ships, so the session frame alone can never tell an
anonymous visitor’s shell “this instance requires sign-in.” Added a plain
GET /config (CORS-open, services/workspace/src/server.ts) the shell
fetches alongside — not instead of — connect(), so the wall renders
correctly for the actual anonymous-visitor case, not just the
already-connected-but-rejected one.
services/workspace/src/server.ts: newGET /config→{ didi_auth_mode }.services/workspace/src/ws.ts: session frame also carriesdidi_auth_mode(belt-and-suspenders for the already-connected case).services/workspace/src/workspaces.ts+capabilities.ts:workspace.listgainedpinned(true whenACTIVE_CLIENT_IDwas set at boot).packages/workspace/src/state.svelte.ts+types.ts: newdidi_auth_mode/pinnedreactive fields,fetchDidiAuthMode().shell/src/SignInWall.svelte(new): full-screen pre-auth wall, same headless sign-in mechanics asDidiBadge.svelte.shell/src/App.svelte:showWallderived gates the entire header+stage behind{#if showWall} <SignInWall />{:else}…{/if};WorkspaceSwitcherhidden whenworkspace.pinned.- Verify: confirmed live in a real browser (headless Chromium,
playwright) — anonymous visitor → full wall, no header/remotes/switcher; signed in → wall gone, header + switcher + remotes all render, real data loads.pinned: true/falseverified via a temporaryACTIVE_CLIENT_IDoverride (no rebuild needed) then reverted.svelte-checkclean onshell(2 pre-existing, unrelated errors confirmed viagit stash).
Step 8 — didi chat v0 (augment-it) ✅ DONE 2026-07-08
Scoped deliberately to augment-it only — see
[[../../../context-v/explorations/Didi-sh-One-Login-One-Agent-Three-Services|Didi-sh-One-Login-One-Agent-Three-Services]]
for the full cross-service vision (one persona, shared skill-library
package, per-app capability registries) this is the local first slice of.
The shared @lossless/in-app-agent package extraction is explicitly
deferred to its own initiative, not bundled into this flow.
- Persona rename (
services/workspace/src/chat.ts’sSTATIC_SPINE): didi introduces itself by name, names both jobs (enrichment + corpus curation), and states plainly it’s scoped to augment-it only — no cross-service claims. - Curator capabilities wired as chat verbs: new
CURATOR_CHAT_VERBSslab (source.add,domain.create,extract.add,tag.apply) plus a live, volatileexistingCorporaSlab()— adomain.listread injected into every turn asTitle → type:slug, so didi resolves a name like “consumer-immunology” against the workspace’s REAL corpora instead of guessing.assembleSystemSlabsis now async to support the live read. Writes ride the existing actor envelope unchanged —via: 'didi-agent'was already threaded fromapps/chat’s invoke path since step 4. - Agent-skill authored:
context-v/agent-skills/inbox-curation/SKILL.md(decile-hub-interface’s format) — the full decision tree (named + existing corpus → invoke directly; new corpus → propose first; unclear → propose-or-inbox), the never-fabricate-an-identifier discipline, and the boundary withcorpus.inbox.add(untriaged parking, not filing). - Didi also got a face: a character headshot (user-provided,
transparent PNG) composited with the shell’s actual brand-duotone
gradient tokens, wired into
DidiBadge.svelte,SignInWall.svelte, and the chat rail’s header (apps/chat/src/App.svelte— “didi · augment-it · <status>”). - Verify: the literal acceptance line — “didi, file this link under
consumer-immunology” — run live against the local stack (a real chat_turn
→ chat_invoke source.add → source filed under
thesis:consumer-immunology,created_bymatching the signed-in didi_id). Also verified in the actual browser UI: didi’s first greeting lists the workspace’s three real corpora by name, pulled live from the same context slab. Test source cleaned up after (DB + filesystem), same discipline as prior steps.svelte-checkclean onapps/chatandshell;tsc --noEmitclean onservices/workspace.
Step 9 — Deploy augment-it, single-tenant ✅ DONE 2026-07-09 (Railway, not DigitalOcean)
Platform changed after re-checking the DO droplet’s live numbers: the
167.172.42.247 box (prepped 2026-07-06) turned out to have only ~112MB
free / 537MB available RAM before running a single one of our own
services — the “escape hatch: resize to 2GB if it strains” language in
the original plan undersold how tight it already was. A leftover
coolify-proxy container was also still holding ports 80/443, meaning
“ports freed” was stale too. Given very few users and no prior DO ops
investment (vs. real Fly.io experience from id-didi-sh this same week),
the operator chose Railway over both DO and Fly — Railway’s
multi-service-project model is the closest 1:1 match to this repo’s
actual docker-compose shape. The DO droplet is abandoned for this flow
(still paid-for and untouched otherwise).
8 Railway services, one project (augment-it, workspace “The
Lossless Group”): nats (official image, custom start command — see
gotcha below), workspace-service, record-surrealdb-resolver,
content-ingest, prompt-runner (all four Dockerfile builds,
rootDirectory = /services/<name>), and three frontends —
shell, strategy-curator, chat — each also Dockerfile-built
(not Railway’s Railpack auto-builder; see gotcha below), full-repo
build context (they need the pnpm workspace), each producing its own
static dist/ served via serve.
Two Railway Volumes, deliberately NOT shared (confirmed via Railway
docs + support: a volume is strictly single-service): content-ingest
owns /clients (the real corpus filesystem, read-write); workspace-service
gets its own tiny /data volume, self-seeded on every boot via its
deploy.startCommand (mkdir -p /data/clients/humain-vc && echo DEFAULT_DOMAIN_TYPE=thesis > .../.env && npm start) rather than
uploading the real clients/humain-vc/.env (which also holds unrelated
Decile Hub credentials workspace-service has no business touching).
Real gotchas hit and fixed, worth knowing before touching this again:
- Railway’s CLI (
environment edit --service-config, dot-path form) silently no-ops in the version used this session —{"committed":false, "message":"No changes to apply"}regardless of value. The JSON patch form (environment edit --json) works reliably; used for everything. railway volume addpanics (Rustunwrap()onNone) in this CLI version — created volumes via direct GraphQL (volumeCreatemutation) instead.nats-serverdoes not accept-max_payloadas a CLI flag (matches this repo’s ownnats.confcomment from months ago) — needed an inline generated config file. First attempt (printf '...\n...\n') corrupted across the Railway CLI → GraphQL → containersh -cchain; the multi-echo, no-embedded-newlines form is what actually survives:echo port: 4222 > /tmp/nats.conf && echo http_port: 8222 >> ... && nats-server -c /tmp/nats.conf.- Railway’s Railpack builder auto-detected this repo as a turborepo
(
turbo.jsonexists at root) and unconditionally ran the root package.json’sbuildscript (turbo run build) for the three frontends, ignoring any custombuildCommandoverride — andturbowas never actually an installed binary here. Fixed by givingshell,strategy-curator, andchattheir own Dockerfiles (directpnpm --filter <pkg> build), same pattern as the four backend services. - A Docker
ARGthat’s declared but never passed resolves to an empty string, notundefined—?? defaultdoesn’t catch it. Two real bugs from this: (1)shell/rsbuild.config.ts’s remote-URL fallbacks had to change from??to||; (2) module-federationassetPrefixwas only set fordev, notoutput(the field that also covers production builds) — missing it meantchat’s andstrategy-curator’s async sub-chunks resolved as relative paths against the shell’s origin instead of their own, 404ing into the shell’s SPA-fallback HTML (“SyntaxError: Unexpected token ’<’”). Only reproduces cross-origin — local federation dev never surfaces it. Both fixed withoutput.assetPrefixenv-configured per remote. - Railway auto-injects its own
PORT(8080) for any service — this silently mismatched the public domain’s configured target port (3001) forworkspace-serviceuntilPORT=3001was set explicitly as a service variable.
Verify, as done: headless-Chromium checks against the live deployed
URLs (not just localhost) — anonymous visitor → full wall, zero
unexpected console errors (only the 12 out-of-scope remotes’ harmless
localhost failures, unchanged from local); workspace-service’s
/config endpoint live; scripts/prove-didi-auth.mjs’s GATE=1 mode
run against the real deployed wss://ws.augment.didi.sh/ws — anonymous
correctly rejected 4401. Member-admitted couldn’t be scripted against
prod id.didi.sh (dev-token echo is deliberately disabled there) — that
leg needs a real human sign-in (Step 12).
Step 10 — DNS + cookie day ✅ DONE 2026-07-09 (Railway custom domains, not DO/Caddy)
Two Railway custom domains, both required under *.didi.sh for the
didi_session cookie (Domain=.didi.sh) to actually reach them — the
shell’s own page origin AND workspace-service’s WS endpoint (every
federated remote connects to workspace-service directly, so it’s the one
that must share the cookie domain; the remotes’ own static-asset
origins don’t need to):
augment.didi.sh→shell(CNAME + TXT ownership-verification record, added at Vercel DNS by the operator; validated + cert issued within minutes).ws.augment.didi.sh→workspace-service(same pattern).id-didi-sh’s prodcors_origins— was empty (config/runtime.exsnever set it; onlydev.exshadlocalhost:3100), meaning every cross-origin browser call toid.didi.shin production had been silently CORS-rejected since it went live, just never surfaced because augment-it wasn’t deployed yet. Added["https://augment.didi.sh"], deployed to Fly.PUBLIC_WS_URLrebuilt intoshell/strategy-curator/chataswss://ws.augment.didi.sh/ws.- Verify:
curl -H "Origin: https://augment.didi.sh" -X OPTIONS https://id.didi.sh/api/magic-linksreturnsaccess-control-allow-origin: https://augment.didi.sh+access-control-allow-credentials: true. Live browser check againsthttps://augment.didi.shclean (see Step 9). Real sign-in + cookie attachment still needs a human (Step 12) — dev-token echo being off in prod is a feature, not a gap to route around.
Step 11 — Corpus sync ⚑ NEEDS REVISITING (Railway volumes replace the DO-box assumption)
Written when Step 9 targeted a DO droplet with a filesystem an rclone
timer could reach directly. On Railway, content-ingest’s corpus lives
on a Railway Volume — reachable via railway volume files (CLI, hit
its own bugs this session — see Step 9) or railway ssh, not a plain
box path a cron job can rclone from directly. The single-writer
discipline (team writes hosted; Michael’s local edits sync deliberately,
never concurrently) still holds as a policy — the mechanism needs a
Railway-native answer (a periodic job inside content-ingest itself
pushing to R2, most likely) before this step is actually done. Not
blocking Step 12 — humain-vc’s corpus is empty-ish today either way.
Step 12 — Dress rehearsal (the acceptance run)
- Seed Aniel’s membership on prod (step 2’s task via fly ssh).
- Two laptops (or two browsers), both on
augment.didi.sh: run Flow 1 end to end — sign-ins via real email, thesis creation, link + file adds, cross-screen liveness, didi triage, attribution spot-check (frontmatter shows who did what). - Changelog entries per shipped chunk along the way (the splash self-updates from them); update this plan’s checkboxes as steps land.
What happens to the existing curator data (nothing bad)
The source curator working locally against SurrealDB today is not disturbed by any step above:
- SurrealDB is already shared. It’s the Cloud instance; the deployed
box points at the SAME connection string. Every canonical row —
sourcesregistry,domains,source_usages, organizations, persons — carries over with zero migration, because it never lived on the laptop. Reads stay workspace-filtered per the client-tagging convention, and the shared registry is a feature here: a URL reach-edu already identified keeps itssource_uuidwhen humain-vc cites it. - The local corpus filesystem stays local.
clients/reach-edu/never goes near the box (isolation by absence); local dev keeps working as the reach-edu workbench exactly as today.clients/humain-vc/is nearly empty (one mis-filed domain) — since the flow’s work is from scratch, either start the box’s volume fresh and letdomain.retype+ re-creation rebuild it, or push the existing files up via step 11’s rclone path. Both are fine; fresh is simpler. - Local dev and the hosted instance coexist against the shared canonical layer — domains created hosted appear in local queries and vice versa (client-tagged). The only discipline is step 11’s single-writer rule for the corpus FILES, which the DB doesn’t need (it’s one database either way).
Sequencing notes
- 1–2 (id) and 3–8 (augment-it) interleave freely; nothing in 3–8 waits on email. 9 waits on 3+7 minimum; 10 waits on 9 plus the id DNS; 12 waits on everything.
- Each step is one commit-or-few on
rebuild/turbo-rsbuild(augment-it) /main(id-didi-sh), pushed per the trunk cadence, changelog on coherent chunks. - If a fresh session picks this up: read the ai-labs flow plan first,
then
git log --oneline -15in both repos to locate the frontier.