ho-06.2 — The Workshop: three-state glyphs and the heal surface
- Ho-06.2-AT-01.md
- Ho-06.2-AT-02.md
- Ho-06.2-AT-03.md
Make the vault's state legible at a glance and its drift reconcilable. This reads the scan cache ho-06.1 built — glyphs on the sidebar rows tell you where each scope lives, a pulled-on-demand drift surface tells you which materialized files have wandered from the vault, and one action reconciles them. Two gate-born chrome items ride along: a Settings scene with an appearance override, and scan-root visibility in the footer.
The ho-06 split. K4 planned ho-06 as one polish ho and flagged it as the densest in the project. It split three ways at ho-06.1's opening:
- ho-06.1 (closed 2026-07-10, PR #12) — responsiveness + honest feedback.
- ho-06.2 (this ho) — three-state glyphs + heal/drift surface.
- ho-06.3 (provisional) — first-run wizard, age-key generation + backup nudge, ingest flow. Its premise was gate-validated at 06.1: the operator could not find the repos-to-vault path from the GUI.
K4's ho-06 section body still describes the un-split ho; the split is recorded in
K4's 2026-07-10 revision header and in K6. Reconciling the section body is a
ho-kamae-4-overview-collaborator pass, flagged from this session — not edited
here.
Out of scope:
- First-run wizard, age-key generation, backup nudge, ingest flow — ho-06.3.
- Orphan remediation (create-scope-from-orphan, remove-stray-marker) — this ho surfaces orphaned markers; fixing them is ingest-adjacent, ho-06.3.
- Multi-root scan management UI (add/remove/reconfigure roots) — owed as a near-term ho (see Decision 5). This ho shows all roots read-only.
- The right-side collapsible tool rail — deferred with criteria (Decision 1).
- The linking picker / shared-secret browser, rotation-driven staleness — ho-07.
- The per-entry "plain / not-a-secret" flag — parked schema (-2 level).
- Scriptable
init, unconditionalgit init, non-atomic ingest — the owed CLI ho. Zero files underSources/SharibakoCLI/change in this ho, same rule as ho-05 and ho-06.1. - Notarization / distribution — ho-08.
Phase 1 — Think
Five decisions, ratified 2026-07-11. Two facts about the Core surfaces drive most of them, both found by reading the code before deciding:
Materializer.status(scopeID:scanRoots:)re-walks the tree on every call (it callsscan(roots:)internally). Glyphs must not route through it — that is the exact cost ho-06.1's in-memory scan cache exists to kill.Materializer.heal(marker:)decrypts every owned key to compare vault-plaintext against file-plaintext. Drift cannot be known passively; every drift check costs the age key, a Touch ID, and aVaultWorkerhop.
Decision 1 — Chrome: native top-toolbar; the right-side rail deferred with criteria
The operator floated a right-side collapsible tool rail (icons when closed)
replacing the top toolbar. The friction behind it is real — after 06.1 the
toolbar carries Rescan, Jump, Sync, Materialize, Preview, and two Add buttons,
and this ho adds heal actions. But a right-side action rail cuts against
macOS convention (Mac puts actions in the top toolbar or a bottom bar; the right
edge is conventionally an inspector — which is exactly what the third
NavigationSplitView pane already is), and it commits every later surface
(06.3, ho-07) to a non-native idiom. The operator's actual pains — crowding and
label legibility — are solvable within the native idiom.
- This ho: Heal, Check-drift, and Materialize-all-stale get homes on the top toolbar; if that crowds the bar, secondary actions (Preview .env, Jump-to-directory) move into an overflow or contextual placement. Grouping is execution-level, not a migration.
- Rail revisit criteria: revisit the right-side rail only when a later surface needs standing chrome the top toolbar can't hold without discoverability loss — concretely, 06.3's first-run flow or ho-07's linking/shared-browser adding a mode that wants a persistent surface. Until then, native top-toolbar plus overflow.
Decision 2 — Glyphs: computed from the scan cache; orphaned markers get synthetic rows
Two parts. The first is forced by cost; the second is a data-model fork.
-
2a — Computation home. Glyph state computes in
WorkshopModelfrom the in-memoryscanReport.markersintersected withscopes— not throughMaterializer.status, which re-walks. A tested computed accessor,glyphState(forScope:), reads the cache. Synchronous, no worker, no Touch ID.live_here(vault scope with a marker in the scan roots) andlive_elsewhere(vault scope with no marker in the roots) fall straight out of the intersection. -
2b — Orphaned markers get synthetic sidebar rows.
live_hereandlive_elsewhereare properties of a scope, so they map to scope rows.orphanedis a property of a marker that references a scope the vault doesn't have — it has no scope, so it has no row. Rather than hang a third glyph on rows that can't carry it, the sidebar renders orphaned markers as their own rows in a distinct "Unlinked markers" section, carrying the orphaned glyph, selectable to show the marker's path. This honors the plan's three-state intent and puts the orphan where the user is already looking. Surfacing only — remediation (create-scope, remove-marker) is ingest-adjacent and belongs to ho-06.3.scanReport.failures(malformed markers, ho-04.11) surface in the same section, distinguished from orphans by their load-failure reason.
Decision 3 — Heal: pull-based, session-cached drift; sweep behind one Touch ID; reconcile via materialize
Because drift costs a full decrypt, a "drift indicator on materialized scopes"
cannot be ambient — rendering badges at launch would decrypt every scope at
launch, prompting Touch ID the moment the window opens, which contradicts
ho-06.1's whole achievement (window interactive immediately, no work until asked).
So drift is pulled, then cached for the session — parallel to scanReport:
- Launch: no drift info, no Touch ID. Same posture as the scan cache.
- Check drift sweeps every
live_herescope throughVaultWorkerwith visibleactivityprogress, behind one Touch ID (ho-06.1's 5-minute reuse window covers the sweep). Results land in a new session cache,driftReports: [scopeID: DriftReport]. - Rendering: sidebar drift badges and the detail pane's per-key drift render from that cache. "Drift indicator on materialized scopes" becomes true after a check, cached — never ambient-at-launch. Before a check, the badge slot is empty; the sidebar row shows only its glyph.
- Reconcile is the existing
materialize(force:)flow — it already decrypts, already has thependingDiff→ confirmation dialog.healviews drift (read-only);materializefixes it (writes). After a successful reconcile, that scope's cached drift refreshes. - Materialize all stale batch-reconciles the scopes the cached drift marks as drifted, behind a confirmation listing what will be written and one Touch ID. This is the drift-driven version the first-session vignette references. ho-07 later extends "stale" to include rotation-driven staleness, reusing the button — not a scope conflict, a shared definition growing.
Decision 4 — Settings: a native scene with an appearance override
Gate item. The operator asked for a System/Light/Dark flipper; a Settings scene
is its native home. SwiftUI's Settings {} scene wires ⌘, automatically; a
stored enum drives .preferredColorScheme; persistence is @AppStorage
(UserDefaults), the right backend for a pure UI preference.
- Content this ho: the appearance override only. No invented heal/glyph toggles — a "check drift at launch" toggle would re-introduce the launch Touch ID this ho designs away.
- Two persistence backends, kept honest: appearance lives in UserDefaults
(
@AppStorage); vault and scan roots already live inconfig.yaml(operational config). The Settings scene does not fold scan-root state into UserDefaults.
Decision 5 — Scan-root visibility: footer, read-only, all roots
Gate item. "Where is it scanning?" has no UI answer — roots live invisibly in
config.yaml. Waymarking answered the vault half (footer names vault + remote);
this answers the scan-root half.
- This ho: the sidebar footer names the configured scan root(s) beside vault
and remote, full paths in the tooltip, same pattern as the vault line. Because
scanRootsis already[URL], the footer renders all roots — a user who -editsconfig.yamlto add roots sees them reflected. Read-only; the single-root pick stays the existingNSOpenPanel-on-empty flow. - Owed, near-term: multi-root scan management UI (add / remove / reconfigure). The practitioner flagged this as a real, soon need — elevating it above K4's post-MVP Deferred Decision #4. Forward-only: a new ho, not a reopening of the deferral. Recorded in K6 and flagged for the same K4 overview-collaborator pass that reconciles the ho-06 split — likely a small dedicated scan-config ho, or folded into ho-06.3's first-run config work. Not placed here; placement is an overview call.
Discovery (deferred to execution)
- Glyph and badge symbols. SF Symbol choices for
live_here/live_elsewhere/orphanedand for clean / drifted, with distinctions carried by , not color alone (colorblind-safe) and Reduce Motion respected. Execution-level; no ratification needed. - Sweep progress granularity. Whether the Check-drift
activityreports per-scope progress or a single "Checking drift…" is an execution call; a large vault makes per-scope nicer but it is not a decision.
Phase 2 — Execute
Branch ho-06.2 off main. Three agent tasks, in order — -02 extends AT-01's
sidebar rows; AT-03 is independent.
Ho-06.2-AT-01 — Glyphs + orphan rows
glyphState(forScope:) computed over the scan cache ∩ scopes (Decision 2a,
tested in WorkshopModel); sidebar renders live_here / live_elsewhere
glyphs on scope rows and an "Unlinked markers" section for orphaned markers and
scan failures (Decision 2b, surfacing only). The scope row view gains a slot for
AT-02's drift badge. Cache-only, synchronous, no Touch ID.
Model: claude-sonnet-4-6.
→ /agent-tasks/Ho-06.2-AT-01.md
Verifiable: every scope row shows a glyph reflecting whether its marker is in
the scan roots; a .sharibako pointing at a nonexistent scope appears as an
Unlinked-markers row, not a missing entry.
Ho-06.2-AT-02 — Heal surface
The driftReports session cache; the Check-drift sweep intent (VaultWorker +
Touch ID + activity); per-key drift in the detail pane; per-scope reconcile
wired to the existing materialize(force:) flow; drift-driven Materialize-all-
stale with a confirmation (Decision 3). Carries the Decision 1 chrome fix — the
new actions get toolbar homes, overflow if crowded. Extends AT-01's row view
with the drift badge. Model: claude-opus-4-8.
→ ho-process/agent-tasks/Ho-06.2-AT-02.md
Verifiable: one Touch ID sweeps drift across all live-here scopes; a hand-edited owned line shows as drift on its row and per-key in the detail; reconcile writes it back and the drift clears; Materialize-all-stale reconciles the set behind one confirmation.
Ho-06.2-AT-03 — Settings scene + scan-root footer
Native Settings {} scene with the System/Light/Dark appearance override
(@AppStorage → .preferredColorScheme, Decision 4); scan-root footer
visibility in the waymarking extension (all roots, tooltip paths, Decision 5).
Independent of glyph/heal logic. Model: claude-sonnet-4-6.
→ ho-process/agent-tasks/Ho-06.2-AT-03.md
Verifiable: ⌘, opens Settings and the appearance flipper switches the app between System, Light, and Dark, surviving relaunch; the sidebar footer names every configured scan root with full paths in the tooltip.
Testing and iteration approach
Per task, the rhythm: swift package clean && swift test directly
(clean→build→test still trips the known 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%. New branching logic — glyphState, orphan
derivation, the drift-cache reads, Materialize-all-stale set selection — lives
tested in WorkshopModel and its extension files (the ho-06.1 file-split
precedent). New View files join ci.yml's named-EXCLUDED regex with
justification comments, the established convention.
Tests never touch live user state — injected home / roots / temp vaults only.
Drift and glyph tests build ephemeral vaults with markers whose targets have
been hand-drifted, and assert the computed state, not the rendering. Day-to-day
iteration runs unsigned against the file-based age key (SHARIBAKO_AGE_KEY); the
signed build is produced once, at the gate.
Done means
- Every scope row carries a glyph computed from the scan cache:
live_herewhen a marker for it sits in the scan roots,live_elsewherewhen none does. No glyph computation re-walks the tree. - Orphaned markers (and malformed-marker scan failures) appear as rows in an "Unlinked markers" sidebar section, selectable to show the marker path. No remediation actions.
- Check-drift sweeps every live-here scope behind one Touch ID through
VaultWorkerwith visible progress, caching aDriftReportper scope; sidebar drift badges and per-key drift in the detail pane render from that cache; no drift work runs at launch. - Per-scope reconcile routes through the existing
materialize(force:)flow and refreshes that scope's cached drift; Materialize-all-stale reconciles the drifted set behind one confirmation and one Touch ID. - ⌘, opens a native Settings scene whose appearance override switches System / Light / Dark and persists across relaunch.
- The sidebar footer names every configured scan root with full paths in the tooltip.
- The full rhythm is green; coverage ≥90% with new View exclusions named and justified in ci.yml.
- Zero files under
Sources/SharibakoCLI/change. - Dogfood gate passed (below).
Verification and the gate
- The rhythm above, green, on the
ho-06.2branch. - Dogfood gate (signed install + Touch ID) — clean release build via
xcodebuild -project xcode/Sharibako.xcodeproj -scheme Sharibako -configuration Release, installed and launched from/Applicationsexplicitly (Spotlight resurfaces stale DerivedData Debug builds — the ho-06.1 gate lesson), real vault, real Keychain:- Open the Workshop against the production scan root. Every scope row shows a
glyph; a scope that lives on another machine reads as
live_elsewhere; a stray.sharibakoappears under Unlinked markers. - Check drift with one Touch ID; a scope whose
.envyou hand-edited shows drift on its row and per-key in the detail; reconcile writes it back and the drift clears. A second check within five minutes costs no new Touch ID. - Materialize all stale reconciles the drifted set behind one confirmation.
- ⌘, opens Settings; flip to Dark, quit, relaunch — the app stays Dark.
- The footer names the production scan root(s) with full paths in the tooltip. Not done until this passes.
- Open the Workshop against the production scan root. Every scope row shows a
glyph; a scope that lives on another machine reads as
Phase 3 —
Filled at ho close, 2026-07-11, after the signed-install + Touch-ID gate.
The pull-based drift model held. One Touch ID swept every live-here scope
and a reconcile within the window cost no new prompt — Decision 3's whole bet
(drift is pulled + session-cached, never ambient-at-launch, one auth for the
sweep via ho-06.1's reuse window) survived contact with a real vault and real
Keychain. Glyphs read correctly (all scopes live-here on the dogfood vault);
Check-drift → per-key drift → reconcile → clear was the tight loop as designed.
The orphan/failure "Unlinked markers" rows were not exercised at this gate (the
dogfood vault had no orphaned markers) — surfacing-only, tested at the unit
level, but unverified in the running UI; carry to the next gate that has a
stray .sharibako.
The native-chrome bet did NOT hold — the rail revisit criterion is met. The
gate proved Decision 1 wrong in practice: the overflow » → "More" → nested
submenu holding Add Shared Secret / Preview .env / Jump to Directory reads as
bad, buried UI ("NOT an acceptable UI"). The top toolbar cannot carry the
Workshop's action set legibly, and the operator's original right-side collapsible
panel instinct is the answer. This is the standing-chrome-the-toolbar-can't-hold
trigger Decision 1 named. Forward-only: owed a dedicated chrome/right-panel ho
(not a reopening of 06.2), which also reconsiders whether appearance belongs as a
persistent control at the base of that panel rather than only in Settings (the
operator floated a top-bar light/dark/system toggle; Settings is the Mac-native
home, but a panel changes the calculus).
What the tests didn't catch — View-layer, by (that is what the gate is for):
- Reconcile blanked the detail pane. Clearing the cached drift on a
successful write reverted the pane to its "no drift check yet" empty state,
which read as a blank window right after a successful reconcile (the
silent-success-reads-as-broken failure mode again). Fixed in-ho:
markScopeInSyncrebuilds the cached report as all-matchafter a full write — in sync by construction, so no decrypt and no new Touch ID — leaving the pane on the drift view showing every key "In sync" and the badge flipped to clean. - Per-key drift needed to read at a glance. "Differs" (and the other drift states) now render red; "In sync" stays quiet secondary. Fixed in-ho.
- Drift vanished when you drilled into a key. Per-key drift lived only in
the scope-overview pane, so selecting a secret hid the drift you'd just found
("isnt bringing me to the drift check window once I'm in the scope"). Fixed
in-ho: the secret-detail pane now carries an inline drift banner for the
selected key (
keyDrift(forScope:key:)), red when drifted, with its own Reconcile — drift stays visible whether you're on the scope or a key. - Primary actions melted into the calm UI. Reveal / Reconcile / Rotate
Value / Save Notes read as flat gray and gave no "press this next" signal. A
shared
primaryActionButton()treatment (bordered-prominent + larger control size) gives them a little more space and an accent shift while secondary actions stay borderless — emphasis without breaking the zen. Fixed in-ho.
Followups (for ho-06.3's opening list / the K4 overview pass):
- The right-side chrome/panel ho — elevated from Decision 1's deferral by this gate. The operator wants it; the overflow menu is a stopgap. Likely its own ho; reconsider appearance-control placement there.
- Multi-root scan-management UI — still owed (add/remove/reconfigure roots); 06.2 shipped read-only footer visibility only.
- Verify the Unlinked-markers rows at a gate with a real orphan — unit-tested, UI-unverified.
- ho-06.3 — first-run wizard, age-key generation + backup nudge, the GUI
ingest journey (the operator confirmed the CLI-only
initingest path is the current front door; the GUI has no import affordance yet).
Closing this ho
Closing = fill this Reflect + flip status: complete + write the state-summary
block to the project's K6 (ho-process/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). 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>
Appendix — fresh-session bootstrap
To execute this ho in a new Claude Code session, load and run
prompts/ho-06.2-execution-driver.md.
Authored 2026-07-11 (Think ratified same day, five decisions, split from K4's planned ho-06 at ho-06.1's opening). Executed and closed 2026-07-11: AT-01 → AT-02 → AT-03 + two gate fixes, signed-install + Touch-ID gate passed (chrome finding owed a followup ho, ).
Rendered from the corpus, verbatim · source on GitHub →