ho-06.3 — First-run, age-key generation, GUI ingest
- 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:
- First-run wizard — gated on
vaultState == .noVault, rendered as the main window's content: prerequisite check (age/age-keygenpresent), age key generate-or-import (Keychain), a verified backup nudge, initial scan root (detected and suggested), optional git remote. The wizard creates the vault and hands off into the first ingest. - Ingest as a SwiftUI flow — scan a directory, present per-secret
decisions (the five verdicts CLI
initwalks), commit through the existingMaterializer.ingest/acceptIngest. Reachable from the wizard's -off, from the action panel, and from orphaned-marker rows. - Orphan remediation — the 06.2 "Unlinked markers" rows stop being surfacing-only: an orphaned marker offers create scope from marker (seeded ingest) and remove stray marker (confirmed, rust-toned).
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:
- Linking UX — Phase 5 (ho-07). The ingest decision "link to shared" appears only when the shared pool already has entries; the shared-secret browser is ho-07's.
- Multi-root scan management — ho-06.6. This ho sets the initial root; adding/removing/reconfiguring roots later is the management ho.
- Import from Vaultwarden / iCloud Keychain — post-v1.
.envingest is the v1 import story. - Conflict-resolution UI — post-v1.
- Bundling
agein the app — ho-08 (distribution). This ho detects the missing binary and gives the copy-paste install line; it does not bundle. - CLI scriptable
init/ unconditionalgit init/ non-atomic ingest — the owed CLI ho. Zero files underSources/SharibakoCLI/orSources/SharibakoCore/change — every needed Core surface already exists (VaultCore.createVault(at:),Conduit.initializeRepository()/setRemote(_:)/setIdentity(name:email:),Materializer.ingest/acceptIngest,UnlinkedMarker).
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:
- Welcome + prerequisite check. Probe for
ageandage-keygenusing the same PATH-plus-fallback-directories discipline Core'sShelluses (GUI-launched apps inherit a bare PATH — the fallback list exists for exactly this). Missing → a copy-pastebrew install ageline and a Re-check button; the wizard does not advance without the binaries (even key import needsagefor every later encrypt). - Age key: generate or import. Generate →
age-keygento a temp file, store in the Keychain under.userPresence(mirroring the CLI'sAgeKeyBootstrap.generateToKeychain), scrub the temp file. Import → file picker for an existing identity file, validated (AGE-SECRET-KEY-1line 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. - Backup nudge (Decision 2).
- Where you keep code. Initial scan root, detected and suggested (Decision 4). States the vault location plainly (Decision 3) — informative line, not a control.
- Optional remote. A git URL field, validated by the existing
Conduit.setRemotegrammar; skippable. Plain copy: the remote is the vault's backup. - Finish. Creates the vault (
VaultCore.createVault+Conduit.initializeRepository+setRemotewhen 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)
- Wizard page copy. Exact wording per page — plain talk, no jargon, stakes named. Execution-level; the gate (and the usability test) judges it.
- Git-identity detection. Whether
git config user.emailresolving empty is checked viaConduit.status()plumbing or a light git call at finish; an implementation find inside the existingConduitsurface. - Candidate-scan depth for the ingest invite. How deep below the scan
root the finish step looks for
.env-bearing directories (likely the same shallow walkMaterializer.scanalready does for markers, pointed at.envfiles). Execution-level; must stay fast on a wide root. - Wizard re-entry. Whether a Settings affordance re-opens the wizard on a
machine that already has a vault (probably not this ho — the wizard is the
.noVaultpath; re-run tooling can wait for demand). Best call at execution, logged in .
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.
- Ho-06.3-AT-01 — First-run wizard +
GUI key bootstrap. No dependency.
GUIShell,GUIAgeKeyBootstrap(Keychain write, mirror constants),FirstRunWizardview,WorkshopModel+FirstRunintents (prereq probe, key generate/import, backup save + verify, root detection, vault creation + git init + optional remote), the.noVault→ wizard wiring. - Ho-06.3-AT-02 — GUI ingest flow.
Depends on -01 only at the hand-off seam (the finish-page invite).
IngestSheetview,WorkshopModel+Ingestintents (scan directory → proposal → decision state →acceptIngestbehind Touch ID), the panel's Ingest Project… verb, the wizard hand-off. - Ho-06.3-AT-03 — Orphan remediation.
Depends on AT-02 (reuses the ingest sheet). The two verbs on
.orphanedrows: seeded ingest, confirmed marker removal.
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
- A fresh launch (no vault) opens into the wizard, not the bare empty state;
the wizard blocks on missing
agewith a copy-paste install line and a working Re-check. - Generate stores a Keychain key under
.userPresencewith the shared service/account/access-group; import validates and stores an existing identity file; an existing Keychain key is detected and never overwritten. - The backup page will not Continue until the saved file exists and matches the stored identity; the recipient is displayed.
- The scan-root page preselects a plausible existing directory on a normal
Mac and accepts any override; the chosen root persists to
config.yaml. - Finish creates
~/.sharibako/vaultwith layout + git repo (+ remote when given), flips the Workshop to.open, and — when the root has.env-bearing projects — offers the first ingest immediately. - Ingest, from any of its three entry points, scans, presents per-key
decisions with CLI-parity verdicts and defaults, commits through
acceptIngestbehind one Touch ID, drops the marker, and announces the outcome; the new scope appears in the sidebar with correct glyphs. - Orphaned-marker rows offer both verbs; Create Scope seeds ingest with the marker's directory and scope ID; Remove Stray Marker confirms (system-rendered) and deletes only the marker file.
- The full rhythm is green; coverage ≥90% with new View exclusions named and
justified in ci.yml. Zero files under
Sources/SharibakoCLI/orSources/SharibakoCore/change. - The signed-install gate below is prepped and parked — and the ho's true done-means, the 15-minute first-run test with a real non-Andrew user (Checkpoint 2), is scheduled by the practitioner after the gate passes. The build session parks at "ready for the usability test."
Verification and the gate
-
The rhythm above, green, on the
first-runbranch. -
Signed-install gate — the committed Xcode project is the only real build path (the 06.4/06.5 lesson: not
swift run, notscripts/install.sh):xcodebuild -project xcode/Sharibako.xcodeproj -scheme Sharibako \ -configuration Release -derivedDataPath .build/xcode buildReplace
/Applications/Sharibako.appand launch from/Applicationsexplicitly (Spotlight resurfaces stale DerivedData builds). The wizard path needs a no-vault environment: run withSHARIBAKO_VAULTpointed at a temp path (and a scratchHOME-shaped config if needed) so the real vault is never touched while dogfooding first-run.- Wizard end-to-end: prereq page (hide
agefrom 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
.envproject; 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.
- Wizard end-to-end: prereq page (hide
-
Checkpoint 2 (the ho's criterion): the 15-minute install-to- materialized-
.envtest 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
- COMPLETED — <what this ho finished>
- NEXT — <the single pointer to what comes next>
- ACTION ITEMS / BLOCKS — <open items; blocks loudly, or
none> - PROJECT LIFECYCLE — <kamae | dev | beta | production>
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 →