ho-04.2 — Interactive sharibako init
- Ho-04.2-AT-01.md
- Ho-04.2-AT-02.md
The command that turns a project directory into a sharibako scope. The practitioner stands in a directory holding a .env, runs sharibako init, and walks a per-key decision for each secret sharibako finds — import it as a scope-local secret, link it to a shared entry, move it into a new shared entry, or leave it alone. When the walk finishes, the vault holds the chosen secrets and a .sharibako marker binds the directory to its scope. This is the first-run experience: the moment a scattered .env becomes vault-managed.
Every other ho-04 verb is a thin wrapper over SharibakoCore. init is not — it's the one real terminal-UX problem in the CLI, which is why it split out of ho-04 (Decision 1 there). The four-way ingest matrix from kamae-2.2 lands here as an interactive prompt, and the Materializer.ingest → acceptIngest cycle from ho-03 gets its first surface.
Out of scope:
- A scriptable non-interactive path (
init --scope <id> --import-all --yes) — deferred to a followup (Decision 6). v1initis interactive-only. The decision-collection interface is built so the scriptable path drops in later without a rewrite. sharibako runand SECURITY.md polish — ho-04.5.- The GUI ingest matrix — ho-06 (kamae-2.2 assigns the SwiftUI four-choice flow there).
- Any change to
Materializer.ingest/acceptIngestor theKeyDecisiontype — the library surface from ho-03 is complete and this consumes it as-is. .envdeletion or rewrite after ingest — the source.envis left untouched (Decision 4).
Resolves deferred decisions (from ho-04's Followups block for this ho):
- The four-way ingest matrix as a terminal interaction (Decision 1)
- Touch ID interleaving during init (Decision 2)
- Scope-ID collision UX (Decision 3)
- Source-
.envhandling after ingest (Decision 4) - First-run behavior when no key exists (Decision 5)
- Non-interactive mode (Decision 6 — deferred)
- Re-init behavior (Decision 7)
Phase 1 —
Decision 1 — The ingest matrix as a per-key arrow-key prompt
Each DetectedKey gets one interactive selection: a highlighted five-choice list — import as scope-local / link to shared / move to shared / leave alone / skip — navigated with ↑/↓ and confirmed with Enter. Not a full-screen table application; a per-key prompt that renders, takes one choice, and moves to the next key. That's the "smooth, not overboard" line.
Inline affordances:
- When
DetectedKey.nameMatchedSharedIDis set, the row pre-highlights link to shared and names the match ("OPENAI_API_KEYmatches shared entryopenai-personal"). The Materializer already computes this; the surface honors it as the default cursor position. - link to shared and move to shared need a second input — which shared entry, or the ID for the new one. Link presents the existing
vaultCore.listShared()set as a second arrow-select; move prompts for a new slug (with the same sanitize/collision discipline the scope ID uses). - A header before the walk states the scope ID and key count so the practitioner knows the before committing to per-key choices.
The decision logic sits behind an interface, IngestDecisionSource — given a ProposedScope, produce [KeyDecision]. v1 ships one implementation, the interactive TTY prompt. This is the seam that makes the deferred non-interactive path (Decision 6) a second implementation rather than a rewrite, and it's what makes the flow testable: a scripted IngestDecisionSource returns canned decisions in tests, exercising every branch without a live terminal. Same shape as ho-04's AgeKeyProvider protocol and CleanCommand's injected lineReader.
Non-TTY input (piped stdin, CI) is detected and refused with a clear message — "init needs an interactive terminal; scriptable flags are a followup" — not left to hang waiting on raw-mode reads that will never come.
Raw-mode rendering. A -rolled InteractiveSelect helper: termios raw mode, arrow-escape-sequence parsing, ANSI redraw of the choice list, SIGINT-safe terminal restore. No new package dependency — consistent with sharibako shelling out to age/git rather than linking libraries. The dependency-vs-hand-roll call stays an execution detail: if a small, well-maintained select-prompt package clearly beats ~150 lines of termios, the agent takes it and names the tradeoff. The renderer is thin and coverage-excluded with a comment (the languages-swift.md view-exclusion pattern); the logic behind the interface carries the coverage.
Decision 2 — No Touch ID during ingest; one prompt at first-run key generation
Ingest never needs the private key. Every decision the matrix collects encrypts with the public key or writes a pointer:
- import as scope-local →
vaultCore.addSecret— public-key encrypt. - move to shared →
vaultCore.addSharedEntry+link— public-key encrypt, then a plaintext pointer. - link to shared →
vaultCore.link— writes a.linkpointer, no crypto. - leave alone / skip → nothing written.
So acceptIngest runs with zero biometry prompts. On a machine that already holds a key, init prompts Touch ID not at all. The one moment biometry can fire is first-run key generation (Decision 5), because writing the Keychain item under .userPresence triggers the gate. init reads the age recipient (public key) it needs for encryption from the same provider path the other write verbs use; reading the public recipient does not prompt (confirmed in ho-04.3: key export --public retrieves the recipient with no Touch ID).
Decision 3 — Scope ID: suggestion as prompt default, idempotent-reuse warning on collision
ProposedScope.suggestedScopeID already carries collision avoidance — suggestScopeID sanitizes the directory basename and walks around vault collisions with -dev, -dev-2, and so on. The surface presents it as a prompt default: Scope ID [kanyo-dev]:. Enter accepts; typing overrides. Scope type defaults to .projectDev (ingest by nature means a project directory) and is offered as an arrow-select if the practitioner wants to change it.
If the practitioner types an ID that collides with an existing vault scope, that's not an error — acceptIngest reuses the scope idempotently (the existing scope's type wins, no rewrite). The surface prints a one-line warning ("scope kanyo-dev already exists — its keys will be added to") and asks for confirmation before proceeding, so the reuse is chosen rather than stumbled into.
Decision 4 — Leave the source .env untouched
acceptIngest writes the vault and the marker; it does not touch .env. After init, the .env that was ingested still sits in the directory with its plaintext values. This is correct, not a loose end: .env is the materialized runtime artifact — the file the practitioner's application actually reads (kamae-2.2's merge model). For imported keys, its plaintext already equals the vault value; it is the output side of the materialize contract, not a duplicate to clean up. Deleting or rewriting it would break the app on next run.
So init writes nothing to .env and prompts nothing about it. The ho-04 Followup floated deletion as a possible refinement; on inspection there is nothing to refine — the file belongs where it is. sharibako clean already exists for the practitioner who deliberately wants owned lines retracted later.
Decision 5 — First run with no key: offer inline generation
When init finds no age key (no Keychain item on macOS, no key file on Linux, no --age-key override), it prompts: No age key found. Generate one now? [Y/n]. On yes, it runs the same path as sharibako key generate — age-keygen, store in Keychain under .userPresence (one Touch ID prompt on the signed bundle), print the recipient for backup — then continues into ingest. A --no-generate flag refuses the offer and exits with the instruction to run key generate first, for the practitioner who wants key creation to stay a separate deliberate act.
This is the smoothest first-run: one command takes the practitioner from nothing to a populated vault with a bound marker. It depends on the ho-04.3 signing work — inline generation only reaches the Keychain because install.sh produces the signed .app bundle with the embedded provisioning profile. On an unsigned binary the generation step hits errSecMissingEntitlement; the practitioner runs against the installed signed binary, which is the documented path.
Decision 6 — Non-interactive mode: deferred
v1 init is interactive-only. init is inherently a first-time human setup act; scripted/CI workflows reach for materialize and run, not init. Building the flag surface now (--scope, --import-all, --leave-rest, --yes) doubles the test surface for a path that isn't yet needed. The IngestDecisionSource interface (Decision 1) means the scriptable path lands later as a second implementation feeding the same acceptIngest — no rewrite. Tracked as a followup below.
Decision 7 — Re-init: detect, reconcile, never silently rebind
Running init in a directory that already holds a .sharibako marker is a reconcile, not a fresh start. init reads the marker directly at the target directory, announces the bound scope, and offers to re-ingest — presenting only the keys in .env that the scope does not already own, plus any shared entries newly available to link. Already-decided keys are not re-presented. The scope binding is never changed silently; if the practitioner wants to bind the directory to a different scope, that takes an explicit confirmation.
This uses direct marker detection at the init directory (fileExists + loadMarker on <directory>/.sharibako), not the walk-up resolveMarker(startingFrom:). That sidesteps the home-directory-boundary error ho-04's flagged for the walker — init never walks up, so the walker bug does not reach this path. (The walker fix remains a separate small followup for the verbs that do walk; it is out of scope here.)
Deferred to execution
- Whether
InteractiveSelectis hand-rolled termios or a small vetted package (Decision 1 prior: hand-rolled; agent vetoes with reasoning if a package clearly wins). - Exact redraw behavior of the select widget (clear-and-repaint vs cursor-addressed in place) — an execution-time feel decision, tuned against a real terminal.
- Whether the re-ingest reconcile (Decision 7) reuses the full
ingest → prompt → acceptIngestpath with a pre-filteredProposedScope, or a narrower "new keys only" view — the agent picks the cleaner construction against the realProposedScopeshape. - How the scope-type override (Decision 3) is surfaced — inline in the scope-ID prompt or as a separate arrow-select — a small UX call for the agent.
Phase 2 — Execute
Two agent tasks with a clean seam between the reusable terminal-select machinery and the init command that drives it — the ho-01/02/03/04 two- rhythm.
Ho-04.2-AT-01 — Interactive select widget + decision-source interface
The InteractiveSelect raw-mode helper (arrow-key single-select, ANSI redraw, SIGINT-safe restore, non-TTY detection) and the IngestDecisionSource protocol with its interactive TTY implementation and a scripted test double. Pure terminal-UX infrastructure with its own acceptance criteria — arrow-escape parsing, non-TTY refusal, terminal restore on interrupt — testable without init existing yet.
→ /agent-tasks/Ho-04.2-AT-01.md
Ho-04.2-AT-02 — InitCommand
sharibako init [<directory>]. Wires the whole flow: resolve vault + recipient, detect an existing marker for reconcile (Decision 7), offer first-run key generation (Decision 5), run Materializer.ingest, confirm scope ID with idempotent-reuse warning (Decision 3), drive the arrow prompt through IngestDecisionSource (Decision 1), call acceptIngest, report the written marker and populated scope. Leaves .env untouched (Decision 4). Depends on AT-01's widget and interface.
→ ho-process/agent-tasks/Ho-04.2-AT-02.md
Testing and iteration approach
AT-01 lands and tests green before AT-02 opens — the select widget and decision interface are verifiable in isolation against the scripted double, no init command required. AT-02 builds on that foundation and is tested end-to-end through a scripted IngestDecisionSource that returns a mix of all five decisions, asserting the vault and marker states acceptIngest produces. The interactive TTY renderer is coverage-excluded with a comment; every decision branch is covered through the interface. Coverage floor stays consistent with ho-04 — ≥ 85% on the new CLI code with the raw-mode renderer honestly excluded.
One manual dogfood pass after AT-02: init in a scratch directory with a real .env, walking the arrow prompt against a real terminal, first-run key generation included on the signed binary. This is where the "smooth, not overboard" feel gets its only real test — the automated suite verifies logic, not feel.
Done means
sharibako initin a directory with a.envwalks each detected key through an arrow-key prompt and writes the chosen secrets to the vault plus a.sharibakomarkernameMatchedSharedIDkeys default the cursor to "link to shared" and name the match- First run with no key offers inline generation (one Touch ID prompt on the signed bundle) and continues into ingest;
--no-generaterefuses cleanly - Re-running
initon an initialized directory reconciles — offers only not-yet-owned keys, never rebinds silently - Scope ID prompts with the suggested default; a collision warns and reuses idempotently on confirmation
- The source
.envis byte-for-byte untouched byinit - Non-TTY invocation refuses with an actionable message rather than hanging
- Every decision branch is covered through the scripted
IngestDecisionSource; CI clean, both linters clean, coverage ≥ 85% with the raw-mode renderer excluded by comment
Phase 3 — Reflect
Both agent tasks landed clean (89b66e1, c1bfaf1), then a same-session correction (32ebc2f) and a full dogfood against a real .env. What follows is what the run surfaced.
What held
- The foundation works end-to-end on real data.
initwalked the twelve keys inedelmore/apps/diary/.env, imported all twelve as scope-local, wrote twelve<KEY>.agefiles plusscope.yamlunder~/.sharibako/vault/scopes/diary/, and dropped the marker. The.agefiles are real age ciphertext; a headless PTY run confirmed the value round-trip (getreturns the plaintext) and that the source.envis left byte-for-byte untouched. - Decisions 2, 4, 5, 7 verified. No Touch ID fired during ingest — the writes use the public recipient (Decision 2). The source
.envis untouched (Decision 4). First-run key generation prompted Touch ID on the signed bundle and continued into ingest (Decision 5) — the ho-04.3 signing work held under a real first run. Re-init reconcile presented only not-yet-owned keys and never rebound the scope (Decision 7). TheIngestDecisionSourceseam paid off exactly as intended for testing — every branch is driven through the scripted double. - Link-choice gating is correct. "Link to shared" is absent when no shared entries exist; the arrow menu shows import / move / leave / skip, and the cursor state machine tracks arrow navigation accurately.
What changed mid-session — empty .env refuses
The Execute shipped an empty-.env case that wrote a zero-secret marker (an empty scope you could add to later). Dogfood judgment reversed it: a truly-empty directory now refuses with CLIError.nothingToInitialize (exit 2) and writes nothing — no scope, no marker, no key generated. An empty scope in the vault is chaff, not a legitimate binding. _run was reordered to ingest up front on a keyless VaultCore so the empty check short-circuits before any write or key generation (32ebc2f).
Where the design did not hold — Decision 1 at scale
The per-key arrow prompt was specified for "smooth, not overboard." At three keys (the smoketest) it read fine. At twelve keys (real data) it does not hold, and the failure is structural, not cosmetic:
- The widget never cleans up its frame on exit.
InteractiveSelect.renderFramewrites the last choice row with no trailing newline andrun()returns with the cursor parked at the end of it. So each answered key's full menu stays on screen, and the walk accumulates roughly four lines per key — an unreadable stack by the twelfth. move-to-sharedrecords a corrupted slug. Same root cause. After the select returns,resolveMoveprints its "New shared entry ID" prompt onto the leftover "skip" line (visible asskipNew shared entry ID…), then reads the slug with a plainreadLine(). Stray arrow-key presses from the just-used select echo into that line as literal^[[Bbytes, andsanitizeSharedIDmaps them to dashes — the shared ID comes out mangled.move-to-sharedwas not dogfooded green; it is a known correctness bug inmain. Workaround for now: use import / leave-alone / skip, which round-trip correctly.- No bulk operation. Twelve keys is twelve prompts; a real
.envthis size wants an "apply to all remaining" action. The Think phase named batch ops as a possible followup — the dogfood makes it required, not optional.
The honest read: this is the evidence that flips Decision 1 from the per-key sequential walk (Q1 option we picked) to the one-page batch view (Q1 option we set aside). Every symptom above — the stacking, the glue, the arrow corruption, the missing ALL, the reverse-video block reading as heavy — is the same render-in-place per-key model failing at scale. The practitioner's direction from the dogfood: an active key highlighted clearly, the choice shown as a color change plus a check rather than a reverse-video block, spacing between rows, and an ALL to bulk-set.
What this hands forward
- ho-04.4 (new) — the ingest dashboard. Replace the per-key sequential walk with a one-page view: every detected key as a row showing its current choice (color + ✓), navigate and set each, an ALL to bulk-apply, commit once. This Decision 1's per-key model and, by replacing the prompt wholesale, retires the
move-to-sharedcorruption and the frame-stacking rather than patching them. TheIngestDecisionSourceseam holds — the dashboard is a third implementation of it;acceptIngestis unchanged. - The
move-to-sharedbug stays documented here and is fixed as a byproduct of ho-04.4. It is not patched in place — patching a prompt slated for replacement is throwaway work.
Followups tracked for future hos
- ho-04.4 — the ingest dashboard. The Reflect-driven redesign: one-page batch view (key highlighted, choice as color + ✓, spacing, ALL to bulk-set) replacing the per-key walk. Supersedes Decision 1 and retires the
move-to-sharedcorruption and frame-stacking. Highest-priority followup — the current prompt is functional but doesn't scale past a handful of keys, andmove-to-sharedis broken until it lands. - Non-interactive
init(--scope <id> --import-all --leave-rest --yes) as anotherIngestDecisionSourceimplementation. Deferred from Decision 6; lands when a scripted init workflow is actually needed. - Marker-walker home-boundary fix. ho-04's Reflect flagged
resolveMarker(startingFrom:)surfacing a filesystem error instead of a clean "no marker" at the~/.sharibakoboundary.initsidesteps it via direct detection, but the walk-up verbs still carry it — a small standalone fix. - ho-06 GUI ingest matrix consumes the same
Materializer.ingest/acceptIngestcycle and the sameKeyDecisionset; the four-choice SwiftUI flow is that surface's version of the ingest prompt — and the dashboard model from ho-04.4 is the closer analog than the per-key walk was.
Authored: 2026-07-02 (Think phase).
Execution: complete (AT-01 89b66e1, AT-02 c1bfaf1, empty-.env correction 32ebc2f).
Reflect: complete 2026-07-02.
Rendered from the corpus, verbatim · source on GitHub →