Local Mirrors
A mirror is the bridge between a node in Portuni’s graph and an actual folder on your disk. Mirrors are per device: each machine has its own view of which nodes are present and where, so two collaborators can have totally different layouts without stepping on each other.
What’s inside a mirror
Section titled “What’s inside a mirror”When you call portuni_mirror, Portuni creates a folder structure for you:
{PORTUNI_WORKSPACE_ROOT}/{org-sync-key}/{type-plural}/{node-sync-key}/ outputs/ -- final deliverables wip/ -- work in progress resources/ -- reference materialFor organization-level workspaces, Portuni adds type-based subdirectories on top so child nodes can attach beneath them:
workflow/ projects/ processes/ areas/ principles/The folder layout is anchored on each node’s immutable sync_key, not
on its display name. Renaming a node does NOT move its folder or change
its remote path.
Per-device registry
Section titled “Per-device registry”The shared Turso graph DB does NOT track mirror paths anymore (migration
011 dropped the local_mirrors table from Turso). Each device keeps its
own SQLite registry at ~/.portuni/sync.db (local_mirrors table),
together with file_state (local hash cache) and remote_stat
(short-lived remote metadata cache).
This split has two consequences:
- No global state about your laptop. Personal disk paths don’t leak into a shared database, and you can keep different parents on different machines.
- Stale rows are tolerated. When a node is purged on one device,
the corresponding registration on another device sticks around until
that device next looks at it. Readers (
portuni_get_context,/context,portuni_status) skip stale rows and fire a fire-and-forget cleanup; the user-visible result is correct, the database self-heals.
How a path gets resolved
Section titled “How a path gets resolved”When something needs to know where a node lives on disk, Portuni:
- Looks up the registration in the per-device
local_mirrorstable. - Verifies the node still exists in the shared graph — if not, the row is treated as stale (skipped + cleaned up).
- Returns the registered path. There is no on-disk existence check at this layer; the caller decides whether absence means “not yet created” or “deleted out from under us”.
For files, the on-disk path is derived at read time:
{mirror_root}/{section}/{subpath}/{filename}, computed from the
file’s remote_path minus the node’s remote root prefix. The files
table no longer stores local_path (migration 012); persisting it
across devices and renames was actively misleading.
Intentional file storage
Section titled “Intentional file storage”Files are saved the same way you’d make a git commit — on purpose, with meaning, and bound to a remote. The relevant tools:
| Tool | What it does |
|---|---|
portuni_store | Copy a file into the mirror folder, upload it via the routed remote, register it in files. |
portuni_pull | With file_id, download the remote version into the mirror. With node_id, preview each file’s status without modifying anything. |
portuni_status | Scan tracked files + (optional) discover new local / new remote files. |
portuni_list_files | List files across every node with derived local_path. |
Every file has one of two statuses:
- wip — stored under
wip/; still being worked on. - output — stored under
outputs/; the final, shareable version.
The context hook
Section titled “The context hook”Portuni exposes a /context endpoint that resolves a filesystem path
back to the graph node it belongs to. The SessionStart hook
(scripts/portuni-context.sh) uses this to automatically surface the
right graph context when you start work inside a mirror folder. See
Claude Code -> The SessionStart hook
for how to register it — including how to handle multiple Portuni
instances at once.