hosystem Engagements

ho-04.2 — Interactive sharibako init

created 2026-07-02
status complete
type ho-document
project sharibako
ho 04.2
kamae 5
shape ha
agent-tasks
  • 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:

Resolves deferred decisions (from ho-04's Followups block for this ho):


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:

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:

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 generateage-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


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


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

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 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


Followups tracked for future hos


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 →

ingested: sharibako @ a97b22af9b61 · ho-system @ 79e96b801a13 · the glossary · the colophon