hosystem Engagements

ho-06.3 — First-run, age-key generation, GUI ingest

created 2026-07-16
status complete
type ho-document
project sharibako
ho 06.3
kamae 5
shape ha
title First-run wizard, age-key generation, and GUI ingest
description The GUI's front door: a paged first-run wizard (prereq check, key generate/import, verified backup, scan root, optional remote) flowing into ingest as a real SwiftUI surface, plus orphan-marker remediation — the ho the 15-minute criterion tests.
splits-from ho-06
agent-tasks
  • ho-process/agent-tasks/Ho-06.3-AT-01.md
  • ho-process/agent-tasks/Ho-06.3-AT-02.md
  • ho-process/agent-tasks/Ho-06.3-AT-03.md

The GUI's front door. Today a fresh install opens to a bare "no vault" empty state naming a path — and stops. There is no way to create a vault, generate a key, or bring a project's .env into the vault from the Workshop; CLI init is still the only repos-to-vault path (the premise gate-validated at 06.1: the operator could not find it in the GUI because it does not exist). This builds the journey the 15-minute first-version criterion actually tests (Checkpoint 2): install → first-run wizard → first ingest → materialized .env in a real project, by a non-expert, unaided.

Three pieces, all app-layer:

Resolves deferred decisions (from the ho-overview): Backup-nudge UX (Deferred Decision #2), first-run experience full (Deferred Decision #9), vault-location default, default scan-root suggestion. All four ratified with the practitioner 2026-07-16 before authoring.

Out of scope:


Phase 1 —

Four decisions ratified with the practitioner (2026-07-16, batched); four supporting calls logged from the codebase's established patterns.

Decision 1 — First-run flow: paged wizard ending in an ingest hand-off

One flow, gated on vaultState == .noVault at launch, rendered as the main window's content (replacing the empty state — no separate modal window to juggle). Ordered pages:

  1. Welcome + prerequisite check. Probe for age and age-keygen using the same PATH-plus-fallback-directories discipline Core's Shell uses (GUI-launched apps inherit a bare PATH — the fallback list exists for exactly this). Missing → a copy-paste brew install age line and a Re-check button; the wizard does not advance without the binaries (even key import needs age for every later encrypt).
  2. Age key: generate or import. Generate → age-keygen to a temp file, store in the Keychain under .userPresence (mirroring the CLI's AgeKeyBootstrap.generateToKeychain), scrub the temp file. Import → file picker for an existing identity file, validated (AGE-SECRET-KEY-1 line present), same Keychain store. If a Keychain key already exists (CLI got there first), this page says so and moves on — never a second key.
  3. Backup nudge (Decision 2).
  4. Where you keep code. Initial scan root, detected and suggested (Decision 4). States the vault location plainly (Decision 3) — informative line, not a control.
  5. Optional remote. A git URL field, validated by the existing Conduit.setRemote grammar; skippable. Plain copy: the remote is the vault's backup.
  6. Finish. Creates the vault (VaultCore.createVault + Conduit.initializeRepository + setRemote when given), persists the scan root, flips the model to .open, runs the launch scan — and if the scan root holds .env-bearing directories, invites the first ingest immediately, seeded with those candidates. The wizard delivers a working vault with the user's first scope in reach, which is what the 15-minute criterion measures.

Alternatives declined: a minimal two-screen flow (hides the remote — the actual backup story — and strands the key-import user), and a wizard that ends at the empty Workshop (re-runs the exact 06.1 gate failure: the user cannot find the repos-to-vault path unaided).

Decision 2 — Backup nudge: guided save + verify

After key generation, a save panel writes an identity backup file (suggested name sharibako-key-backup.txt, suggested location iCloud Drive or Documents). The wizard re-reads the saved file and verifies it matches the stored identity before Continue enables — the wizard knows the backup happened rather than trusting a checkbox. The public recipient is shown for reference. Plain-talk copy names the stakes: lose the key, lose the vault.

Declined: print-first (printer friction inside a 15-minute test; save-as-PDF inside the print dialog is guided-save with extra steps), copy-with- confirmation (clipboard managers retain secrets; the user still has to invent a storage location — the step a non-expert can't do). The import path skips the nudge: the user's identity file already exists outside the Keychain.

Decision 3 — Vault location: fixed ~/.sharibako/vault, no chooser

The wizard states the location plainly and moves on. The CLI resolves SHARIBAKO_VAULT → ~/.sharibako/vault and this ho cannot touch CLI files — any non-default location silently breaks sharibako on the same machine unless the user exports an env var, a footgun aimed at exactly the persona this ho serves. The non-expert's backup story is the git remote (its own wizard page), not Finder-copying a folder. Relocation stays the power-user path (SHARIBAKO_VAULT); a chooser can arrive post-v1 if anyone asks.

This K4's earlier lean ("offer both, default user-visible"), recorded before the CLI-parity constraint existed. Named here, not hidden.

Decision 4 — Initial scan root: detect and suggest

Probe a short candidate list — ~/Projects, ~/Developer, ~/Code, ~/Vaults, ~/src, ~/dev — for directories that exist; preselect the most likely (existence, tie-broken by which contains git repositories), with a folder picker to override. Pure logic over an injected home, fully testable. The suggestion is usually right, which is what keeps a non-expert inside 15 minutes. Persisted through the existing WorkshopConfig.persistScanRoot.

Decision 5 — GUI key bootstrap mirrors, never imports, the CLI's

The Workshop gets GUIAgeKeyBootstrap (and a minimal GUIShell for the age-keygen shell-out) in Sources/Sharibako/Support/, mirroring the CLI's AgeKeyBootstrap/CLIShell the way GUIAgeKeyProvider already mirrors KeychainAgeKeyProvider — same Keychain service/account/access-group constants, same .userPresence access control, same kSecAttrAccessibleWhenUnlockedThisDeviceOnly. The established reasoning holds unchanged: SharibakoCLI is a closed executable target the GUI cannot depend on, and SharibakoCore stays portable (no Security / LocalAuthentication imports; its Shell is deliberately internal). This ends GUIAgeKeyProvider's read-only era — the comment saying "the CLI owns key storage" updates to name the wizard as the GUI's write path.

Decision 6 — Ingest presents as a sheet over the main window

The ingest flow is a .sheet (K4's call): scan result → per-key decision list → scope ID/type confirmation → commit. Unlike the Add forms (auxiliary windows since 06.1 — the operator needed the main window mid-add), ingest is a focused, self-contained pass; modal is right. Three entry points, one surface: the wizard's finish hand-off (seeded with detected candidates), an Ingest Project… verb in the action panel's create group, and orphaned- marker rows (Decision 7). Committing encrypts, so acceptIngest rides the existing Touch ID provider flow (one prompt, inside the 06.1 reuse window). Per-key decisions mirror CLI init's five verdicts — import as scope-local, link to shared (only when shared entries exist), move to shared, leave alone, skip — with the same defaults.

Decision 7 — Orphan remediation: two verbs on orphaned-marker rows

The 06.2 "Unlinked markers" section surfaced orphans and stopped. Each .orphaned row now offers Create Scope from Marker — the ingest sheet seeded with the marker's directory and its recorded scope ID — and Remove Stray Marker — deletes the .sharibako file, rust-toned, behind a system-rendered confirmation naming the file path (the 06.7 destructive pattern; dialogs stay untintable, settled at the 06.5 gate). Scan-failure rows stay surfacing-only. Removing a marker touches a file in the user's repo, not the vault — the confirmation copy says exactly that.

Decision 8 — Vault creation git-inits; identity falls back locally

The wizard's finish step always runs Conduit.initializeRepository() after VaultCore.createVault — sync is the commit boundary and needs the repo. If git has no usable identity (fresh Mac, no global user.name/user.email), finish sets a local one via the existing Conduit.setIdentity ("Sharibako", sharibako@localhost) so the non-expert's first sync cannot die on a git identity error. Detection is execution-level (Discovery below).

Discovery (deferred to execution)


Phase 2 — Execute

Branch first-run off main. Three agent tasks — the seams are real: the wizard (new window content + Keychain-write bootstrap) is one bounded surface verified by model/bootstrap tests; the ingest flow (sheet + model intents over the existing Core ingest API) is another; orphan remediation is a third that reuses the second. Each is its own conversation with its own acceptance surface.

Testing and iteration approach

Per change, the standing rhythm: swift package clean && swift test directly (never clean→build→test — the SwiftPM incremental-link bug after SharibakoCore changes) → swift build -Xswiftc -warnings-as-errors → swift-format lint --strict --recursive Sources Tests → swiftlint lint --strict → coverage ≥90%. Branching logic — root detection, key-file , wizard page progression, ingest decision state, orphan-verb enablement — lives tested in WorkshopModel extensions and the Support types; declarative View bodies (FirstRunWizard, IngestSheet) join ci.yml's named EXCLUDED regex with justification comments. Tests never touch live user state — injected home / roots / temp vaults only; Keychain writes are seam-injected so no test touches the real Keychain; bootstrap tests that shell out need age-keygen on PATH (same contract as VaultCoreEncryptionTests).

Done means

Verification and the gate

  1. The rhythm above, green, on the first-run branch.

  2. Signed-install gate — the committed Xcode project is the only real build path (the 06.4/06.5 lesson: not swift run, not scripts/install.sh):

    xcodebuild -project xcode/Sharibako.xcodeproj -scheme Sharibako \
      -configuration Release -derivedDataPath .build/xcode build
    

    Replace /Applications/Sharibako.app and launch from /Applications explicitly (Spotlight resurfaces stale DerivedData builds). The wizard path needs a no-vault environment: run with SHARIBAKO_VAULT pointed at a temp path (and a scratch HOME-shaped config if needed) so the real vault is never touched while dogfooding first-run.

    • Wizard end-to-end: prereq page (hide age from PATH to see the block), generate, backup save + verify refusal on a wrong file, root suggestion, remote skip and set, finish → ingest invite.
    • Import path: an existing identity file round-trips (import → reveal a secret encrypted to it).
    • Ingest from the panel against a real .env project; decisions, Touch ID, marker drop, sidebar glyphs.
    • Orphan remediation with a stray .sharibako — which also closes the K6's standing unlinked-markers UI-unverified item at this gate.
    • Touch ID and the eyeball are the practitioner's — the gate is not called passed by the build session.
  3. Checkpoint 2 (the ho's criterion): the 15-minute install-to- materialized-.env test with a real non-Andrew user, scheduled by the practitioner once the gate passes. First-run weakness found there inserts a polish ho (next free decimal), .


Phase 3 — Reflect

Filled at close (2026-07-25, after the practitioner's dogfood run)

The pieces work; the flow is wrong. Every surface this ho set out to build exists and functions: the wizard runs to a created vault with a git repo and a persisted scan root, the scan finds markers, the ingest chooser lists candidates, ingest commits keys behind Touch ID and drops a marker, and orphan remediation's Create Scope from Marker imported a project's two keys and made a real scope in one click. The practitioner's verdict at the gate was explicit: "it's working as it is, but how it is is wrong." This ho closes on function. The flow problem it exposed becomes its own — forward only, not a reopening.

The gate was exercised partially, and that is recorded honestly. Verified on the signed /Applications build: wizard → vault created at the configured path (.git present, "No remote" when skipped), scan (Scan found 1 marker in 1 root), the unlinked-marker row, and Create Scope from Marker end to end (Imported 2, linked 0, moved 0, left alone 0, skipped 0). Not exercised: the prereq page's block-and-recheck with age hidden from PATH, the backup page's verify-refusal on a wrong file, the identity-file import round-trip, a full panel-driven ingest with per-key decisions, and Remove Stray Marker. The flow findings arrived before those legs and superseded them as the thing worth acting on; re-running them belongs to the UX arc's own gate, against a surface that will have changed .

The deepest finding is Decision 6. Making ingest a sheet over the main window is the structural cause of everything that felt mushy. A project on disk occupies one of six states relative to the vault — outside the roots; seen-but-not-imported; imported and present; marker without scope; scope without marker; imported but drifted. The model knows all six. The UI gives homes to three (scopes and their glyphs, the unlinked-markers section), and seen-but-not-imported has no persistent home at all — it exists only inside a modal sheet the operator has to already know to open. A sheet is a transaction; the candidate set is a state. That mismatch is why the practitioner could not tell where he was.

The panel groups by noun, and the journey is a verb sequence. 06.5's SCOPE / VAULT / ADD grouping scatters one task across four locations: Rescan (VAULT) discovers, Ingest Project… (ADD) imports, the sidebar rows repair, Materialize (SCOPE) writes out, Check Drift / Materialize All Stale (VAULT) reconcile. The practitioner — who designed the system — could not find Ingest Project…, the primary import verb, sitting as the last row beneath three smaller "Add" actions.

Vocabulary leaks implementation. marker is a filename, materialize is a pipeline word, ingest is ETL jargon. scope and drift are load-bearing and earn their keep. Whether the GUI renames while the CLI stays terse is a real decision with a cost — the man pages describe the app in the CLI's words — and it belongs in a Think, not in a view file.

No bulk import, against the project's own precedent. CLI init offers "Import all N as scope-local? [Y/n]" — one keystroke for the dominant case. The GUI did not inherit it, so fourteen projects means fourteen passes through a decision sheet. The key handle is already reused across a sweep in Check Drift, so one Touch ID could cover a batch.

Decision review. D1 (paged wizard ending in an ingest hand-off) held; the hand-off earned its place mechanically, though the invite lands in the same flat surface. D2 (guided save + verify) — unexercised at the gate. D3 (fixed ~/.sharibako/vault, no chooser) held cleanly. D4 (detect and suggest a scan root) is the one that failed in practice, and not for the reason anticipated: the rule is right for a real machine, but scan_roots lives in one global file (~/Library/Application Support/Sharibako/config.yaml), not per vault — so switching SHARIBAKO_VAULT switches the vault and keeps the roots, and the wizard cheerfully added the practitioner's real code root to a sandbox vault's list. Per-vault vs global roots is now ho-06.6's problem and was never decided. D5 (GUI bootstrap mirrors the CLI) — unverified; the Keychain legs were not run. D6 (sheet over the main window) — see above; superseded in substance. D7 (two orphan verbs) — verb one verified, verb two not. D8 (git-init with local identity fallback) held.

What the tests didn't catch — all of it, by design. Every finding here is information architecture, wording, and state legibility: none of it is expressible as an assertion, and the suite was green throughout. This stretch also hardened the suite substantially — 873 → 953 tests (+80), lifting line coverage 94.92% → 96.67%, functions 90.77% → 93.33%, regions 89.56% → 92.94%, with -warnings-as-errors and both linters strict-clean. The residue is structural: roughly 90 regions are Keychain-entitlement, exit(), TTY, or execve bound and unreachable from a test process. Whether to name those files in ci.yml's exclusion list — as KeychainAgeKeyProvider already is — was deliberately left undecided rather than folded in here.

The app icon is not what the system draws. The committed .icns was an upscale from a single raster master and genuinely soft; it was re-rendered from the SVG at native resolution for every size (scripts/make-app-icon.swift, with xcode/Sharibako/Sharibako.svg now committed as the vector master). That fixed a real defect and did not fix the complaint: macOS serves the app an embossed, shadowed icon at sizes absent from the .icns entirely, because a legacy CFBundleIconFile gets the system's own treatment. Shipping the icon as an asset-catalog AppIcon (or an Icon Composer .icon) is owed — the project has no .xcassets at all.

Smaller findings worth carrying. Choose Other… ingests a directory without adding it as a scan root, so its marker is spliced into the session cache and then lost to the next launch's scan. Rescan now re-reads config.yaml on every invocation (a gate fix in this ho), so hand edits land without a relaunch. The 07-19 Keychain-probe anomaly — the GUI reporting not-found while the CLI sees the key — remains unverified, and security(1) cannot settle it because data-protection keychain items are invisible to it.

Checkpoint 2 stays open, and is expected to fail as things stand. No non-Andrew user has run the fifteen minutes. The practitioner's own run is the strongest evidence available that it would not pass — not on defects, but on flow. The checkpoint is not satisfied by this close.

Followups, in dependency order. (1) A Think naming the six project states and what each is called in the GUI — possibly a K2 amendment, since it touches the domain language. (2) A projects surface plus panel information architecture, superseding D6's sheet-only home for candidates. (3) Bulk import, defaults-then-review, one Touch ID. (4) ho-06.6 multi-root scan management, now also carrying the per-vault-vs-global roots decision. (5) The asset-catalog app icon. (6) The coverage exclusion-list decision.

Closing this ho

Closing = fill this Reflect + flip status: complete + write the to the project's K6 (/kamae-6-sharibako-state-memory.md, refreshing the block at the top, verbatim labels and order) + append a build-record entry to K4 (ho-process/kamae-4-sharibako-ho-overview.md, , append-only) + refresh the derived work-state record (.keisaku.json) if the project's generator is in reach. The block:

STATE-SUMMARY

Note for the close: this ho's criterion (Checkpoint 2) outlives its build session. The build closes at a passed signed-install gate + "ready for the usability test"; the K6 NEXT carries the test itself as the practitioner's scheduled action, and the K4 checkpoint entry stays open until a real non-Andrew user has run the 15 minutes.


Authored 2026-07-16 (Think batched and ratified same day: Decisions 1–4 by the practitioner, 5–8 logged from established codebase patterns). The Phase-4 tail's front-door ho — the one the 15-minute first-version criterion actually tests.

Rendered from the corpus, verbatim · source on GitHub →

ingested: sharibako @ 157d1da960c6 · ho-system @ 0f93b7fa32f7 · the glossary · the colophon