Skip to content

feat(tui): workspace manager TUI (PR 3 of 3)#171

Merged
donbeave merged 93 commits into
mainfrom
feature/workspace-manager-tui-secrets
Apr 26, 2026
Merged

feat(tui): workspace manager TUI (PR 3 of 3)#171
donbeave merged 93 commits into
mainfrom
feature/workspace-manager-tui-secrets

Conversation

@donbeave

@donbeave donbeave commented Apr 25, 2026

Copy link
Copy Markdown
Member

A substantial redesign of the operator console's workspace manager. The merged code is the source of truth; this description summarizes what landed across ~90 commits.

Series: PR 1 of 3 — toml_edit migration (#162) · PR 2 of 3 — workspace manager TUI (#166) · PR 3 of 3 — this PR

What's in this PR

Environments tab (renamed from "Secrets")

Per-workspace env-var management directly inside jackin console. Two scopes:

  • Workspace env — applies to every agent launch in this workspace.
  • Per-agent overrides — applies only when launching a specific agent class.

Operator interactions:

  • Add via scope-first flow: + Add environment variable → ScopePicker (All agents / Specific agent) → EnvKey → SourcePicker (Plain text / 1Password) → value modal or 1Password picker.
  • Edit a plain value via Enter on the row. Op:// references are not text-editable — operator deletes (D) and re-adds via the picker for changes.
  • Delete via D + confirm.
  • Per-row masking: M toggles JUST the focused row. Op:// rows never mask (they're paths, not credentials).
  • Live duplicate-key validation: red border + inline warning + Enter blocked while invalid.
  • Op:// rows render as breadcrumb: <email> / <vault> / <item> → <field> (multi-account) or <vault> / <item> → <field> (single).
  • [op] text marker on op:// keys for visual distinction; alphabetical sort across rows.
  • Empty section omitted entirely (no placeholder noise).
  • Default agent moves to the Agents tab; managed there with * (mark), Space (allow/disallow auto-clears default).

1Password picker

Multi-account-aware drill-down (Account → Vault → Item → Field). Single-account operators skip the Account pane.

  • Filter-as-you-type per pane; arrow keys navigate; Enter advances; Esc goes back one pane.
  • Field pane sorts concealed fields first; auto-cursors on the first concealed field.
  • Item rows show additional_information subtitle for disambiguation.
  • Session-scoped metadata cache; r refreshes any pane. Field values never enter the cacheRawOpField serde shape omits the value JSON key by design.
  • Four failure states surfaced inline: not installed, not signed in, zero vaults, mid-drill error.
  • Startup availability probe uses a 3-second timeout (OpCli::new_probe()) so a blocked op binary cannot freeze jackin console startup.
  • P key and footer hint are suppressed when op is unavailable.

Inline agent disambiguation

The legacy full-screen ConsoleStage::Agent picker is deleted (~700 LOC removed). Replaced by Modal::AgentPicker, an overlay on the manager list.

Three-branch launch logic (Enter on a workspace):

  1. default_agent set → immediate launch.
  2. Single eligible agent → immediate launch.
  3. Multiple eligible → compact picker overlay (filter, ↑↓, Enter to launch).

Main-screen preview pane

Right-pane workspace summary: General → Mounts → Environments → Agents. Environments preview shows a flat alphabetical list with workspace-level and per-agent rows. Block omitted entirely when the workspace has no env at any scope.

UX rules codified in RULES.md

  • TUI Keybindings. Plain letters/numbers/Enter/Esc/Tab/arrows only — no Ctrl/Alt/Cmd modifiers.
  • TUI List Modals. Standard layout: persistent Filter: row → list body → standard footer.
  • Contextual key absorption. When a focused row owns a key, that row absorbs the keypress.

Polish & bug fixes

  • Caps Lock parity: letter shortcuts accept KeyModifiers::SHIFT.
  • Q exit confirmation dialog outside the main screen.
  • Non-blocking event loop: event::poll(50ms) + per-tick worker-channel drain. Spinner advances without keypress.
  • TextInput modal: 1-cell padding + subtle dim background band.
  • After save → exit, cursor lands on the just-saved workspace.
  • ConfirmSave required_height off-by-one fixed (last diff line was always clipped).
  • Footer hints correctly suppressed for rows where the action is a no-op.

Trust model

  • RawOpField deliberately omits the value JSON key — a unit test exhaustively destructures OpField to fail compilation if a value field is ever added.
  • Session cache stores only metadata (accounts/vaults/items/field labels), never raw field values.
  • op:// references stored verbatim; resolution happens at launch, never in the editor.

Tests

  • 1047 tests pass (843 baseline + 204 new across the series).
  • Unit: TextInput validation, picker state machines, JSON parsing, render-buffer assertions.
  • Integration: end-to-end TUI flows in tests/manager_flow.rs — Environments-tab edit/delete/add, scope-first flow, picker open/cancel/commit, agent picker disambiguation, multi-account picker, save/exit cursor positioning.

Out of scope (deferred)

  • Editor-side validation of op:// path syntax.
  • Secret rotation, audit log, bulk import.
  • Copy-to-clipboard shortcut.
  • Global or Agent (non-workspace) env scopes in the TUI — CLI-only per feat(cli): env-var management verbs for all four scopes #170.
  • CHANGELOG.md (operator curates manually).
  • secrets_flat_rows hot-path allocation cache (documented in docs/superpowers/plans/pr171-review-deferred.md).

Test plan

  • cargo build --all-targets — clean
  • cargo nextest run -p jackin — 1047/1047
  • cargo clippy -- -D warnings — clean
  • cargo fmt --check — clean
  • Manual smoke: Environments tab — add (scope picker → key → source → value), edit plain, delete, mask toggle, per-agent override flow.
  • Manual smoke: 1Password picker — drill all 4 panes, refresh (r), Esc back-navigation, failure states.
  • Manual smoke: agent disambiguation — single-agent direct launch, default-agent direct launch, multi-agent popup, Esc cancel.
  • Manual smoke: Q exit dialog — main screen exits silently, anywhere else opens confirm.

🤖 Generated with Claude Code

donbeave and others added 14 commits April 25, 2026 02:12
Add secrets_masked / secrets_expanded / pending_env_key fields to
EditorState, SecretsScopeTag type, EnvKey/EnvValue TextInputTarget variants,
DeleteEnvVar ConfirmTarget variant. Extend change_count with env diff
logic. Wire commit_editor_save to compute env diffs and call
ConfigEditor::set_env_var / remove_env_var before save(). Extend
build_confirm_save_lines with an "Env vars:" diff section.

The new TextInputTarget / ConfirmTarget variants are unreachable in this
commit; their handlers land in commit 2 (UI). Chose Option A: placeholder
unreachable!() arm in apply_text_input_to_pending so all type definitions
are established in commit 1. ConfirmTarget has no exhaustive-match site
so it needed no placeholder (Modal::Confirm uses target: _).

Removing Copy from TextInputTarget (EnvValue now carries a String) forced
minor follow-on changes: pass-by-reference at the apply_text_input_to_pending
callsite and `&Variant`-style comparisons in existing test assertions in
prelude.rs and editor.rs.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Replace render_secrets_stub with a sectioned, hierarchical render:
"Workspace env" header always visible, key rows with masked/unmasked
display, collapsible per-agent override sections, "+ Add" sentinels, dirty
markers. Wire input handlers: Enter (edit/expand/add per row kind), D
(delete confirm), A (add), Ctrl+M (mask toggle), Left on expanded headers
(collapse). Tab-cycle resets masking and collapses sections on leave.
EnvKey/EnvValue handlers in handle_editor_modal route the two-step add
flow; DeleteEnvVar handler removes the key from pending. Tab strip drops
"Secrets ⏳" placeholder label and dim/italic style.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Add unit tests for change_count env-diff variants on EditorState and
render-buffer tests for masking and section expand/collapse. Add seven
integration scenarios in tests/manager_flow.rs covering edit, delete,
add, masking toggle, section expand/collapse, dirty detection, and the
two-step add flow. Append a "Workspace editor — Secrets tab" section
to docs/src/content/docs/commands/console.mdx documenting the keybindings
and scope semantics.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
The Secrets tab key-map advertises `→` as an alias for `Enter` on a
collapsed agent override header (symmetric with `←` for collapse), and
the docs landed in commit 3 say so. Commit 2 left this unwired — Right
fell through to the tab-advance handler before the Secrets dispatch
could see it. Add a guard at the top of the `Tab | Right` arm that, on
the Secrets tab, expands the section when Right is pressed on a
collapsed `AgentHeader`. Tab and Right-on-non-header still advance.
Update the integration test to exercise `→` instead of `Enter` so a
future regression of the guard is caught.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…/item/field listing

Add OpStructRunner trait with account_list, vault_list, item_list, item_get
methods. Implement on OpCli using the same spawn-and-channel timeout pattern
as OpRunner::read. Add serde shapes (RawOpVault, RawOpItem, RawOpItemDetail,
RawOpField) that explicitly omit field values — the picker is a metadata
browser that must not deserialize secret values into memory. Includes unit
tests for JSON parsing, value-suppression, signed-out detection, and the
concealed flag derivation.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…lue input)

Add OpPickerState, OpPickerStage, OpLoadState types in
src/console/widgets/op_picker/. Add render function with three-pane
drill-down, filter-as-you-type, Braille spinner, and four failure-state
instructional displays. Wire Modal::OpPicker variant into modal_outer_rect,
render_modal, and handle_editor_modal. Intercept Ctrl+O in the EnvValue
TextInput arm of handle_editor_modal; restore original text on Esc-cancel.
Add Ctrl+O footer hint to contextual_row_items Secrets key-row arm. Calls
OpStructRunner from commit 5; field values are never deserialized.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Unit tests on the op picker state machine: filter narrowing, pane
advancement clearing the filter, Esc back-navigation through Vault → Item →
Field, concealed-first field sort, commit returning the op:// path.
Integration tests in tests/manager_flow.rs: Ctrl+O from EnvValue modal opens
OpPicker; Esc-cancel restores the EnvValue modal with original text intact.
Docs: Ctrl+O row in Secrets key-map, plus a new "1Password picker" sub-
section documenting requirements and failure-state messages.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
No Ctrl/Alt/Cmd modifiers in TUI keybindings; actions on rows rather than
inside text modals. Applies project-wide starting with the Secrets tab
cleanup that follows in subsequent commits.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Replace the Ctrl-modified mask binding with plain M, consistent with the
new RULES.md TUI Keybindings rule. Update footer hints, docs, and tests.
The ctrl_key test helper stays for now; it goes away in the next commit
once the Ctrl+O picker redesign also no longer needs it.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
The TUI Keybindings rule now also forbids Shift modifiers, not just
Ctrl/Alt/Cmd. Shift+Tab is a chord; rule is "single-key inputs only".
Drop the standalone BackTab arm in the editor's tab-cycle handler
(KeyCode::Left already implements the previous-tab fallthrough). Drop
BackTab from confirm/save_discard/mount_dst_choice/confirm_save/git_prompt
alternations — Left and h still cover prev-direction. Replace BackTab
with Left in test drivers. Update inline comments.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Remove the Ctrl+O interception inside the EnvValue text modal. P on a
Secrets-tab key row opens the picker directly; on commit, the chosen
op:// reference is written straight to that key's pending value (no
intermediate text modal). P on the +Add sentinel opens the picker first;
the EnvKey modal then collects the key name with the path pre-stashed
on EditorState. Drop saved_target/saved_label/original_value from
OpPickerState — the picker no longer reconstructs the EnvValue modal.
Update footer hints, docs, and tests; remove the ctrl_key test helper
now that no caller remains.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Replace the full-screen ConsoleStage::Agent picker with a Modal::AgentPicker
overlay on the manager list. Delete draw_agent_screen, the legacy
handle_event Agent branch, ConsoleStage::Agent variant, ConsoleState's
selected_agent / agent_query fields, and filtered_agents method. Three-
branch launch logic: default_agent set → direct launch; single eligible
agent → direct launch; multiple eligible → popup. Add AgentPickerState
widget anchored on ManagerState.list_modal. New InputOutcome::
LaunchWithAgent variant. Tests updated: 5 legacy state/input tests
deleted, 5 new manager_flow integration tests for the popup, 3 unit
tests for the picker widget.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
`op account list --format json` against op CLI v2.x reports each account
with an `account_uuid` key, not `id`. The picker probe was failing in the
field with "missing field `id` at line 7 column 3" when an operator with
multiple accounts pressed P → 1Password. Add a serde alias on RawOpAccount
so either shape parses. Add a unit test using a real-shape JSON fixture.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Append a dated amendment block to the spec capturing how the Stage 3
implementation diverged from the 2026-04-23 design: manager-as-default
landing (not an excursion), single-variant ConsoleStage with
Modal::AgentPicker replacing the full-screen Agent stage, three-branch
launch logic, the no-Ctrl/no-Shift keybinding philosophy, and the
1Password picker moving from a Ctrl+O modal-internal action to a P
row-level action on the Secrets tab. Preserves the original record;
amendments captured separately.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
@donbeave donbeave changed the title feat(tui): Secrets tab — per-workspace env-var management feat(tui): Secrets tab, 1Password picker, inline agent disambiguation Apr 25, 2026
donbeave and others added 15 commits April 25, 2026 15:26
Add an Account pane as the first stage of the 1Password picker, visible
only when the operator has 2+ accounts signed in to the op CLI. Single-
account setups skip the pane entirely (no UX regression). All downstream
op calls (vault_list, item_list, item_get) thread the chosen account_uuid
via --account <id> so cross-account drilling works correctly.

OpAccount gains email + url fields for human-readable display. RawOpAccount
serde shape uses #[serde(default)] on optional fields for tolerance against
older op versions. Esc from Vault returns to Account when multi-account;
otherwise closes the picker as before.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…indents

The "Workspace env" section label was incorrectly focusable, so the
cursor landed on it when the tab opened. The label is now rendered as a
non-focusable section title above the list. All actionable rows (key
rows, "+ Add" sentinel) render at the same indent column. Agent override
sections still have focusable headers because they expand/collapse;
their content rows align with workspace-level rows for visual consistency.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…neral

The General tab no longer shows the Default agent or Last used rows —
both were read-only and added clutter. Default agent is now set on the
Agents tab via the * key: cursor on an allowed agent and press * to set
it as default, or press * on the current default to clear it. Disallowing
the current default agent (Space) also clears the default. The default
agent is marked visibly in the Agents-tab list. Footer hints updated.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Cache parsed Vec<OpAccount>/<OpVault>/<OpItem>/<OpField> results on
ConsoleState so repeated picker drilling within one `jackin console`
session is instant after the first call. Cache hits skip the op subprocess
entirely. Manual refresh via `r` keypress in any picker pane re-fires the
op call and updates the cache.

The cache stores only structural metadata. Field values from `op item get`
are never deserialized (RawOpField omits the JSON `value` key by design),
so credentials never enter the cache.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Two picker UX fixes:

1. Scrolling now works in all four panes. The list widget's selection state
   tracks viewport offset; navigating below the visible window scrolls the
   list down. Repro: a vault with 20+ items.

2. Breadcrumbs drop the redundant trailing pane-name suffix. The pane
   content makes the type self-evident — "Items" / "Vaults" / "Fields"
   added noise. Titles now read "<email> → <vault>" instead of
   "<email> → <vault> → Items".

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
1Password lets multiple items share a title (two "Google" entries with
different usernames, for example), and only the additional_information
subtitle distinguishes them. Capture op item list's additional_information
field on OpItem.subtitle, then render Item-pane rows as `<title> (<subtitle>)`
with the parenthetical in PHOSPHOR_DIM so the title still reads as primary.
The subtitle also participates in filter-as-you-type so operators can search
by username when the titles collide.

Items without an additional_information value (e.g., secure notes) render
with just the title, no trailing parens.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
The Mounts and Agents tabs never rendered per-row dirty markers; the
footer's `S save workspace (N changes)` is the canonical unsaved-state
indicator. The General tab's Name/Working dir rows and the Secrets tab's
key rows were the outliers — adding visual noise that made the editor
inconsistent with itself.

Drop the markers from both render paths and the now-unused dirty
parameter from render_editor_row / render_secrets_key_line. Remove the
orphaned env_key_is_dirty helper.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
… without keystrokes

The console event loop was calling `event::read()` directly, which
blocks until a key/mouse event arrives. While blocked, the Braille
spinner stopped advancing and worker-thread results sat in the mpsc
channel undelivered — both updates only ran when the next event
unblocked the loop, making the picker feel frozen for seconds at a time
even after `op` had finished.

Switch to `event::poll(Duration::from_millis(50))` + conditional
`event::read()`. Loop runs at 20 Hz regardless of input. Add
`ManagerState::poll_picker_loads()` that drains the OpPicker channels
on every tick, so loading state advances as soon as the worker finishes
rather than waiting for keystroke pumping.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
… add

Adding a new env var via Enter on the +Add sentinel now walks through:
EnvKey modal (name) → SourcePicker modal (Plain text / 1Password) →
EnvValue modal or OpPicker, depending on the choice. The 1Password
button is disabled when the op CLI isn't on PATH; the picker still has
the existing `P`-on-key-row shortcut for direct picker access.

ConsoleState gains an `op_available` flag set once at startup. Sentinel-
add invocations always go through the source picker; existing key-row
edits and P-on-key-row picks are unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Caps Lock causes terminals to send letter keys with the SHIFT modifier
set. Existing match arms guarded by `key.modifiers == KeyModifiers::NONE`
silently rejected those, so operators with Caps Lock on saw `M`/`D`/`A`/
`P`/etc. no-op everywhere — D delete, A add, M mask/unmask, P 1Password,
and so on.

Replace the strict NONE check with a SHIFT-tolerant guard
(`(modifiers - SHIFT).is_empty()`) on every letter shortcut in the
editor, manager list, and widget handlers. Ctrl/Alt/Cmd modifiers are
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…onment" wording

Three small consistency fixes to the Secrets tab:

1. Console header reads "edit workspace · <name>" (Edit) and
   "create workspace" (Create) instead of "edit · <name>" / "new
   workspace" — the missing "workspace" suffix was ambiguous.

2. Empty Secrets tab no longer renders "Workspace env" label or
   "(no env vars)" placeholder — both add no information and aren't
   present on Mounts/Agents tabs. The empty state is now just the
   "+ Add environment variable" sentinel.

3. User-facing TUI text uses "environment" instead of "env" — the
   abbreviation makes sense for CLI subcommand names but reads as
   noise in the TUI. Sentinel text, modal titles, and footer hints
   updated. CLI verbs and Rust identifiers (EnvScope, set_env_var,
   WorkspaceConfig.env) are unchanged — the TUI is the only surface
   where the full word matters.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…t stage is loading

The picker title was emitting a full breadcrumb only after the load
completed, so during the 1-3s op subprocess the modal said just
"1Password" with no context — operator couldn't tell whether they were
waiting for vaults, items, or fields. Stage transitions advance at
request-time now (not result-time), so the title reflects the target of
the in-flight load: `<email> → <vault>` while items load, `<email> →
<vault> → <item>` while fields load, etc. Loading body messages adapt
the same way: "loading vaults from <email>", "loading items from
<vault>", "loading fields from <item>".

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…creen

Pressing Q anywhere outside the main manager list now opens an "Exit
jackin'?" Y/N confirmation dialog. The operator can press N or Esc to
return to exactly where they were — useful when Q is accidentally hit
mid-flow. Q on the main screen still exits silently (today's behavior).

Q in a text input modal (EnvKey, EnvValue, picker filter) is treated as
input and never triggers the confirm. Routing is gated by
is_on_main_screen + consumes_letter_input checks at the top of the event
loop, so the dialog is the single chokepoint.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…g body

While loading an item's fields, the picker title now shows the parent
context (`<email> → <vault>`) and the loading body names the item being
descended into (`loading ChainArgos Redshift (donbeave)…`). Previously
the title carried the item too, which made it ambiguous what was loading
versus already loaded. Principle: title = where you are, body = what
you're descending into. The same rule already applies to vault and item
loads (which were unaffected by this change).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
The EnvKey TextInput modal now carries the current scope's existing
environment-variable keys as a "forbidden" list. While the operator
types, an inline red warning shows when the name collides — and Enter
is blocked until the name is changed or cleared. Workspace-scope and
agent-scope sentinels each populate the forbidden list from their own
env map, so a key named "DB_URL" can coexist on workspace and agent
overrides without false positives.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…t, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
… corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
Resolves 9 conflicts to integrate PR #173 (link-check infrastructure),
PR #171 (workspace manager TUI rewrite), and minor doc updates with the
per-mount-isolation work.

Conflict resolutions of note:
- src/console/state.rs, src/console/preview.rs: take main's slimmer
  versions — main's #171 moved the agent-preview rendering into the new
  manager TUI components, where the iso badge work from this branch
  already lives (render/list.rs, render/editor.rs).
- src/console/manager/state.rs: combine main's `DeleteEnvVar` +
  `SecretsScopeTag` with this branch's `DeleteIsolatedAndSave` /
  source-drift Confirm modal extension.
- src/console/manager/input/editor.rs: route through main's
  `apply_editor_confirm` helper for simple variants while handling
  `DeleteIsolatedAndSave` inline (consumes `plan` and re-stashes as
  `EditorSaveFlow::PendingCommit`).
- docs/.github/workflows/docs.yml: take main's verbatim — main has
  post-#173 refinements (lychee-action SHA pin, cache@v5 bump).
- docs/.../per-mount-isolation.mdx: keep this branch's `<RepoFile>`
  conversion of `src/cli/cd.rs`.

Drops one orphan test (`preselects_saved_workspace_for_nested_directory_under_mount_root`)
that #171 removed from console/state.rs — the same coverage exists in
workspace/resolve.rs:215 (`saved_workspace_match_depth_still_matches_nested_path_under_mount_root`).

Adds `isolation: MountIsolation::Shared` to MountConfig literals that
arrived via main's tests (state.rs, render/list.rs, manager_flow.rs ×6).

All checks pass locally:
- cargo fmt --check
- cargo clippy -- -D warnings
- cargo nextest run (1153/1153)
- bun run check:repo-links (in docs/)
- bun run build (in docs/)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…it, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
… line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…lit proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…t/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…able, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…t, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
… corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…it, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
… line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…lit proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…t/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…able, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…t, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
… corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
donbeave added a commit that referenced this pull request Apr 26, 2026
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 6, 2026
* feat(tui): extend EditorState and save path for Secrets tab

Add secrets_masked / secrets_expanded / pending_env_key fields to
EditorState, SecretsScopeTag type, EnvKey/EnvValue TextInputTarget variants,
DeleteEnvVar ConfirmTarget variant. Extend change_count with env diff
logic. Wire commit_editor_save to compute env diffs and call
ConfigEditor::set_env_var / remove_env_var before save(). Extend
build_confirm_save_lines with an "Env vars:" diff section.

The new TextInputTarget / ConfirmTarget variants are unreachable in this
commit; their handlers land in commit 2 (UI). Chose Option A: placeholder
unreachable!() arm in apply_text_input_to_pending so all type definitions
are established in commit 1. ConfirmTarget has no exhaustive-match site
so it needed no placeholder (Modal::Confirm uses target: _).

Removing Copy from TextInputTarget (EnvValue now carries a String) forced
minor follow-on changes: pass-by-reference at the apply_text_input_to_pending
callsite and `&Variant`-style comparisons in existing test assertions in
prelude.rs and editor.rs.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): implement Secrets tab render and input handlers

Replace render_secrets_stub with a sectioned, hierarchical render:
"Workspace env" header always visible, key rows with masked/unmasked
display, collapsible per-agent override sections, "+ Add" sentinels, dirty
markers. Wire input handlers: Enter (edit/expand/add per row kind), D
(delete confirm), A (add), Ctrl+M (mask toggle), Left on expanded headers
(collapse). Tab-cycle resets masking and collapses sections on leave.
EnvKey/EnvValue handlers in handle_editor_modal route the two-step add
flow; DeleteEnvVar handler removes the key from pending. Tab strip drops
"Secrets ⏳" placeholder label and dim/italic style.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(tui): Secrets tab unit and integration tests; update console docs

Add unit tests for change_count env-diff variants on EditorState and
render-buffer tests for masking and section expand/collapse. Add seven
integration scenarios in tests/manager_flow.rs covering edit, delete,
add, masking toggle, section expand/collapse, dirty detection, and the
two-step add flow. Append a "Workspace editor — Secrets tab" section
to docs/src/content/docs/commands/console.mdx documenting the keybindings
and scope semantics.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): wire Right arrow to expand collapsed Secrets sections

The Secrets tab key-map advertises `→` as an alias for `Enter` on a
collapsed agent override header (symmetric with `←` for collapse), and
the docs landed in commit 3 say so. Commit 2 left this unwired — Right
fell through to the tab-advance handler before the Secrets dispatch
could see it. Add a guard at the top of the `Tab | Right` arm that, on
the Secrets tab, expands the section when Right is pressed on a
collapsed `AgentHeader`. Tab and Right-on-non-header still advance.
Update the integration test to exercise `→` instead of `Enter` so a
future regression of the guard is caught.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(op): add OpStructRunner trait and OpCli implementation for vault/item/field listing

Add OpStructRunner trait with account_list, vault_list, item_list, item_get
methods. Implement on OpCli using the same spawn-and-channel timeout pattern
as OpRunner::read. Add serde shapes (RawOpVault, RawOpItem, RawOpItemDetail,
RawOpField) that explicitly omit field values — the picker is a metadata
browser that must not deserialize secret values into memory. Includes unit
tests for JSON parsing, value-suppression, signed-out detection, and the
concealed flag derivation.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): 1Password vault/item/field picker modal (Ctrl+O from EnvValue input)

Add OpPickerState, OpPickerStage, OpLoadState types in
src/console/widgets/op_picker/. Add render function with three-pane
drill-down, filter-as-you-type, Braille spinner, and four failure-state
instructional displays. Wire Modal::OpPicker variant into modal_outer_rect,
render_modal, and handle_editor_modal. Intercept Ctrl+O in the EnvValue
TextInput arm of handle_editor_modal; restore original text on Esc-cancel.
Add Ctrl+O footer hint to contextual_row_items Secrets key-row arm. Calls
OpStructRunner from commit 5; field values are never deserialized.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(tui): op picker unit/integration tests; update console docs

Unit tests on the op picker state machine: filter narrowing, pane
advancement clearing the filter, Esc back-navigation through Vault → Item →
Field, concealed-first field sort, commit returning the op:// path.
Integration tests in tests/manager_flow.rs: Ctrl+O from EnvValue modal opens
OpPicker; Esc-cancel restores the EnvValue modal with original text intact.
Docs: Ctrl+O row in Secrets key-map, plus a new "1Password picker" sub-
section documenting requirements and failure-state messages.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(rules): add TUI Keybindings section to RULES.md

No Ctrl/Alt/Cmd modifiers in TUI keybindings; actions on rows rather than
inside text modals. Applies project-wide starting with the Secrets tab
cleanup that follows in subsequent commits.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(tui): Ctrl+M → M for Secrets-tab mask toggle

Replace the Ctrl-modified mask binding with plain M, consistent with the
new RULES.md TUI Keybindings rule. Update footer hints, docs, and tests.
The ctrl_key test helper stays for now; it goes away in the next commit
once the Ctrl+O picker redesign also no longer needs it.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(tui): drop Shift+Tab support; tighten RULES.md rule

The TUI Keybindings rule now also forbids Shift modifiers, not just
Ctrl/Alt/Cmd. Shift+Tab is a chord; rule is "single-key inputs only".
Drop the standalone BackTab arm in the editor's tab-cycle handler
(KeyCode::Left already implements the previous-tab fallthrough). Drop
BackTab from confirm/save_discard/mount_dst_choice/confirm_save/git_prompt
alternations — Left and h still cover prev-direction. Replace BackTab
with Left in test drivers. Update inline comments.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(tui): move 1Password picker to row-level P action

Remove the Ctrl+O interception inside the EnvValue text modal. P on a
Secrets-tab key row opens the picker directly; on commit, the chosen
op:// reference is written straight to that key's pending value (no
intermediate text modal). P on the +Add sentinel opens the picker first;
the EnvKey modal then collects the key name with the path pre-stashed
on EditorState. Drop saved_target/saved_label/original_value from
OpPickerState — the picker no longer reconstructs the EnvValue modal.
Update footer hints, docs, and tests; remove the ctrl_key test helper
now that no caller remains.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): Modal::AgentPicker — inline agent disambiguation popup

Replace the full-screen ConsoleStage::Agent picker with a Modal::AgentPicker
overlay on the manager list. Delete draw_agent_screen, the legacy
handle_event Agent branch, ConsoleStage::Agent variant, ConsoleState's
selected_agent / agent_query fields, and filtered_agents method. Three-
branch launch logic: default_agent set → direct launch; single eligible
agent → direct launch; multiple eligible → popup. Add AgentPickerState
widget anchored on ManagerState.list_modal. New InputOutcome::
LaunchWithAgent variant. Tests updated: 5 legacy state/input tests
deleted, 5 new manager_flow integration tests for the popup, 3 unit
tests for the picker widget.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(op): accept account_uuid alongside id in `op account list` output

`op account list --format json` against op CLI v2.x reports each account
with an `account_uuid` key, not `id`. The picker probe was failing in the
field with "missing field `id` at line 7 column 3" when an operator with
multiple accounts pressed P → 1Password. Add a serde alias on RawOpAccount
so either shape parses. Add a unit test using a real-shape JSON fixture.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(spec): amend workspace-manager-tui-design for 2026-04-25 changes

Append a dated amendment block to the spec capturing how the Stage 3
implementation diverged from the 2026-04-23 design: manager-as-default
landing (not an excursion), single-variant ConsoleStage with
Modal::AgentPicker replacing the full-screen Agent stage, three-branch
launch logic, the no-Ctrl/no-Shift keybinding philosophy, and the
1Password picker moving from a Ctrl+O modal-internal action to a P
row-level action on the Secrets tab. Preserves the original record;
amendments captured separately.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(op): multi-account 1Password picker

Add an Account pane as the first stage of the 1Password picker, visible
only when the operator has 2+ accounts signed in to the op CLI. Single-
account setups skip the pane entirely (no UX regression). All downstream
op calls (vault_list, item_list, item_get) thread the chosen account_uuid
via --account <id> so cross-account drilling works correctly.

OpAccount gains email + url fields for human-readable display. RawOpAccount
serde shape uses #[serde(default)] on optional fields for tolerance against
older op versions. Esc from Vault returns to Account when multi-account;
otherwise closes the picker as before.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): Secrets tab — skip Workspace label as focusable row, align indents

The "Workspace env" section label was incorrectly focusable, so the
cursor landed on it when the tab opened. The label is now rendered as a
non-focusable section title above the list. All actionable rows (key
rows, "+ Add" sentinel) render at the same indent column. Agent override
sections still have focusable headers because they expand/collapse;
their content rows align with workspace-level rows for visual consistency.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): move default_agent to Agents tab; remove Last used from General

The General tab no longer shows the Default agent or Last used rows —
both were read-only and added clutter. Default agent is now set on the
Agents tab via the * key: cursor on an allowed agent and press * to set
it as default, or press * on the current default to clear it. Disallowing
the current default agent (Space) also clears the default. The default
agent is marked visibly in the Agents-tab list. Footer hints updated.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(op): session-scoped cache for picker structural metadata

Cache parsed Vec<OpAccount>/<OpVault>/<OpItem>/<OpField> results on
ConsoleState so repeated picker drilling within one `jackin console`
session is instant after the first call. Cache hits skip the op subprocess
entirely. Manual refresh via `r` keypress in any picker pane re-fires the
op call and updates the cache.

The cache stores only structural metadata. Field values from `op item get`
are never deserialized (RawOpField omits the JSON `value` key by design),
so credentials never enter the cache.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): picker scrolling and breadcrumb cleanup

Two picker UX fixes:

1. Scrolling now works in all four panes. The list widget's selection state
   tracks viewport offset; navigating below the visible window scrolls the
   list down. Repro: a vault with 20+ items.

2. Breadcrumbs drop the redundant trailing pane-name suffix. The pane
   content makes the type self-evident — "Items" / "Vaults" / "Fields"
   added noise. Titles now read "<email> → <vault>" instead of
   "<email> → <vault> → Items".

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(op): show item subtitle (additional_information) in picker

1Password lets multiple items share a title (two "Google" entries with
different usernames, for example), and only the additional_information
subtitle distinguishes them. Capture op item list's additional_information
field on OpItem.subtitle, then render Item-pane rows as `<title> (<subtitle>)`
with the parenthetical in PHOSPHOR_DIM so the title still reads as primary.
The subtitle also participates in filter-as-you-type so operators can search
by username when the titles collide.

Items without an additional_information value (e.g., secure notes) render
with just the title, no trailing parens.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): drop per-row "● unsaved" markers on General and Secrets tabs

The Mounts and Agents tabs never rendered per-row dirty markers; the
footer's `S save workspace (N changes)` is the canonical unsaved-state
indicator. The General tab's Name/Working dir rows and the Secrets tab's
key rows were the outliers — adding visual noise that made the editor
inconsistent with itself.

Drop the markers from both render paths and the now-unused dirty
parameter from render_editor_row / render_secrets_key_line. Remove the
orphaned env_key_is_dirty helper.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): non-blocking event loop — spinner ticks and op results land without keystrokes

The console event loop was calling `event::read()` directly, which
blocks until a key/mouse event arrives. While blocked, the Braille
spinner stopped advancing and worker-thread results sat in the mpsc
channel undelivered — both updates only ran when the next event
unblocked the loop, making the picker feel frozen for seconds at a time
even after `op` had finished.

Switch to `event::poll(Duration::from_millis(50))` + conditional
`event::read()`. Loop runs at 20 Hz regardless of input. Add
`ManagerState::poll_picker_loads()` that drains the OpPicker channels
on every tick, so loading state advances as soon as the worker finishes
rather than waiting for keystroke pumping.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): source-picker modal — Plain text / 1Password choice on env add

Adding a new env var via Enter on the +Add sentinel now walks through:
EnvKey modal (name) → SourcePicker modal (Plain text / 1Password) →
EnvValue modal or OpPicker, depending on the choice. The 1Password
button is disabled when the op CLI isn't on PATH; the picker still has
the existing `P`-on-key-row shortcut for direct picker access.

ConsoleState gains an `op_available` flag set once at startup. Sentinel-
add invocations always go through the source picker; existing key-row
edits and P-on-key-row picks are unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): accept Shift modifier on letter shortcuts (caps-lock parity)

Caps Lock causes terminals to send letter keys with the SHIFT modifier
set. Existing match arms guarded by `key.modifiers == KeyModifiers::NONE`
silently rejected those, so operators with Caps Lock on saw `M`/`D`/`A`/
`P`/etc. no-op everywhere — D delete, A add, M mask/unmask, P 1Password,
and so on.

Replace the strict NONE check with a SHIFT-tolerant guard
(`(modifiers - SHIFT).is_empty()`) on every letter shortcut in the
editor, manager list, and widget handlers. Ctrl/Alt/Cmd modifiers are
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): Secrets tab consistency — header label, empty state, "environment" wording

Three small consistency fixes to the Secrets tab:

1. Console header reads "edit workspace · <name>" (Edit) and
   "create workspace" (Create) instead of "edit · <name>" / "new
   workspace" — the missing "workspace" suffix was ambiguous.

2. Empty Secrets tab no longer renders "Workspace env" label or
   "(no env vars)" placeholder — both add no information and aren't
   present on Mounts/Agents tabs. The empty state is now just the
   "+ Add environment variable" sentinel.

3. User-facing TUI text uses "environment" instead of "env" — the
   abbreviation makes sense for CLI subcommand names but reads as
   noise in the TUI. Sentinel text, modal titles, and footer hints
   updated. CLI verbs and Rust identifiers (EnvScope, set_env_var,
   WorkspaceConfig.env) are unchanged — the TUI is the only surface
   where the full word matters.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): picker loading panel shows breadcrumb so operator knows what stage is loading

The picker title was emitting a full breadcrumb only after the load
completed, so during the 1-3s op subprocess the modal said just
"1Password" with no context — operator couldn't tell whether they were
waiting for vaults, items, or fields. Stage transitions advance at
request-time now (not result-time), so the title reflects the target of
the in-flight load: `<email> → <vault>` while items load, `<email> →
<vault> → <item>` while fields load, etc. Loading body messages adapt
the same way: "loading vaults from <email>", "loading items from
<vault>", "loading fields from <item>".

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): Q always opens an exit confirmation when not on the main screen

Pressing Q anywhere outside the main manager list now opens an "Exit
jackin'?" Y/N confirmation dialog. The operator can press N or Esc to
return to exactly where they were — useful when Q is accidentally hit
mid-flow. Q on the main screen still exits silently (today's behavior).

Q in a text input modal (EnvKey, EnvValue, picker filter) is treated as
input and never triggers the confirm. Routing is gated by
is_on_main_screen + consumes_letter_input checks at the top of the event
loop, so the dialog is the single chokepoint.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): field-load title shows parent context, item moved to loading body

While loading an item's fields, the picker title now shows the parent
context (`<email> → <vault>`) and the loading body names the item being
descended into (`loading ChainArgos Redshift (donbeave)…`). Previously
the title carried the item too, which made it ambiguous what was loading
versus already loaded. Principle: title = where you are, body = what
you're descending into. The same rule already applies to vault and item
loads (which were unaffected by this change).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): block duplicate env keys live in the create-env-name modal

The EnvKey TextInput modal now carries the current scope's existing
environment-variable keys as a "forbidden" list. While the operator
types, an inline red warning shows when the name collides — and Enter
is blocked until the name is changed or cleared. Workspace-scope and
agent-scope sentinels each populate the forbidden list from their own
env map, so a key named "DB_URL" can coexist on workspace and agent
overrides without false positives.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): rename Secrets tab to Environments

The values managed in this tab aren't always secrets (DEBUG=1 is just a
config flag), so "Environments" is the more accurate label. Tab strip,
modal titles, and docs prose updated. Internal identifiers (EditorTab::
Secrets, SecretsRow, secrets_*) are unchanged — they describe the
codepath, not the user-facing label.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): op:// rows render as breadcrumb, never mask, ignore Enter

1Password references are paths, not credentials. Render them as a
breadcrumb (`<email>/<vault>/<item>→<field>` for multi-account,
`<vault>/<item>→<field>` for single-account) styled with dim/white/green
hierarchy so the field name reads as the addressable thing. Drop masking
on op:// rows entirely — the breadcrumb isn't a secret, no point masking
it. Block Enter from opening a text-edit modal on op:// rows; operators
delete and re-add via the source picker for changes. Plain-text rows
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): per-row masking — M toggles the focused row only

Replace the global secrets_masked bool with an unmasked_rows BTreeSet
keyed by (scope, key). M toggles membership for the focused row;
everything else stays masked by default. Tab leave clears the set so
re-entering the tab returns to the all-masked baseline. Op:// rows
ignore the set entirely — they render as breadcrumbs (commit 31), not
masked values, so M is a no-op on them.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): mark op:// rows with ⚿ glyph; sort env keys alphabetically

Visual differentiation between plain-text and 1Password environment
variables: a left-edge ⚿ (lock-and-key) glyph appears on op:// rows,
plain rows render two blank spaces in the same column for alignment.
Both row types share one alphabetically-sorted list — no section break
— so the operator can scan keys in order and see the source at a
glance.

BTreeMap iteration already produces sorted keys, so the alphabetical
ordering is a confirmation pass with a regression-pinning test rather
than a code change to the iteration order.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): add per-agent override entry point on Environments tab

Operators can now add overrides for agents that don't yet have any —
previously the override section only appeared once `pending.agents`
already had an entry, so a fresh agent had no UI path to its first
override key.

A new sentinel row at the bottom of the Environments tab — `+ Add
per-agent override for ...` — opens an agent picker filtered to allowed
agents without existing overrides. Picking an agent creates the override
entry, auto-expands the new section, and lands the cursor on the
section's `+ Add <agent> environment variable` sentinel so the operator
can immediately add the first key. The sentinel hides itself when no
eligible agents remain.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): per-agent override picker drops into normal add flow

The picker for per-agent overrides was creating an empty section with a
"(0 vars)" header and a redundant per-agent "+ Add" sentinel inside as
soon as the operator chose an agent — looking messy if the operator
canceled out, and visually inconsistent with how workspace-level entries
are added. Now picking an agent opens the EnvKey modal directly with
Agent scope, and the rest of the chain (SourcePicker → value) is the
same as the workspace-level path. The agent's section materializes
organically when the first key/value commits; canceling at any modal
step leaves pending.agents untouched, so no orphan sections appear.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): TextInput shows red border on duplicate, hides Enter hint when invalid

Three small polish tweaks to the EnvKey TextInput modal so the visual
state always matches what Enter will actually do:

- Duplicate value → red border (DANGER_RED), inline ⚠ warning, footer
  hint shows only `Esc cancel`.
- Empty value → default border (no error styling — empty isn't "wrong",
  it's "not ready"), no inline warning, footer hint shows only
  `Esc cancel`.
- Valid value → default border, footer hint shows `Enter confirm ·
  Esc cancel`.

Enter blocks on both empty and duplicate. The widget gains an
`is_valid()` predicate that gates both the commit and the hint.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): scope-first add flow — pick All agents / Specific agent at start

Replaces the bottom-of-tab "+ Add per-agent override for ..." sentinel
landed in commit 34 with a scope question at the start of the add
flow. Press Enter on the workspace-level "+ Add environment variable"
sentinel; the new ScopePicker modal asks "All agents" or "Specific
agent". All-agents drops into the workspace add flow as before.
Specific-agent opens the agent picker (now showing every allowed agent
— no longer filtered by "doesn't yet have overrides", since operators
legitimately want to add more keys to agents that already have some),
then the EnvKey modal with Agent scope. The fast-path
"+ Add <agent> environment variable" inside an expanded agent section
is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): AgentPicker layout follows the canonical list-modal pattern

The AgentPicker drifted stylistically from the OpPicker — different
title format (filter inlined), missing persistent filter row, "no
matches" placeholder, hard-coded "Enter launch" footer text. Operators
saw two visually distinct pickers for the same conceptual "pick from a
list with filter-as-you-type" interaction.

Refactor to match the OpPicker's layout: persistent Filter: row at
top, list body below, standardized footer. AgentPicker gains a
configurable confirm_label so the Enter-action word reads "launch" in
the launch-disambiguation path and "select" in the override-scope path
— same widget, two contexts, no truncation or wrong wording.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(rules): document the canonical TUI list-modal layout

Codify the list-modal layout pattern (filter row, list body, footer)
that the OpPicker, SourcePicker, and AgentPicker now share. Future
picker widgets follow the same shape so the TUI stays visually
consistent across modals. Sits alongside the existing TUI Keybindings
rule in RULES.md.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): Environments tab layout — blank-line between sections, [op] marker

Three small layout fixes for the Environments tab:

1. Add a blank-line spacer between the workspace section and each
   agent override section. Cursor skips spacers on ↑/↓.
2. Replace the ⚿ glyph marker on op:// rows with a clearer `[op]` text
   marker. Better terminal compat and immediately legible as "1Password".
3. Standardize the row prefix at 7 characters (2-char cursor column +
   5-char marker column) so plain and op:// rows align across the tab.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): agent-section headers absorb ←/→ regardless of expanded state

Pressing → on an already-expanded section header (or ← on a collapsed
one) was falling through to tab cycling — operators saw the active
editor tab change unexpectedly when arrowing on `▼ Agent: <name>` or
`▶ Agent: <name>` rows. The header now absorbs ←/→ in both states:
acts on the row when relevant, no-ops otherwise, but never lets the
keypress reach the tab handler.

Codifies the convention as "Contextual key absorption" in
RULES.md § TUI Keybindings so future row types follow the same default.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): show environments on the main screen — workspace + per-agent

The main-screen workspace-manager preview gains a read-only
Environments block listing the highlighted workspace's env-variable
keys, sorted alphabetically with `[op]` markers on op:// references.
The Agents block now lists each agent's env overrides nested under the
agent name, with a "(no overrides)" placeholder for agents that don't
have any. Block order is General → Mounts → Environments → Agents so
the operator scans config-shaped data first and agent-specific things
last.

The preview stays read-only — `E` enters the editor for changes. Only
key names appear; values (plain or op://) never render.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): consolidate env preview — workspace + per-agent in one block

The main-screen Environments block now holds all env-related content:
workspace-level keys under an "All agents:" sub-header first, then each
agent's overrides nested under per-agent sub-headers. Agents without
overrides are omitted from this block — their absence is the signal.
The Agents block reverts to a lean summary (default + allowed agent
list) with no env nesting; per-agent env detail lives only in the
Environments block now.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): main-screen env preview is one flat alphabetical list with right-side agent label

Replace the workspace+per-agent sub-section layout with a flat,
alphabetical list. Each row shows the [op] marker, the env key name,
and (when scoped to a specific agent) the agent name on the right.
Workspace-level rows have an empty right column. Same env name
appearing at both workspace and agent level renders as two distinct
rows; sort puts workspace rows before agent rows when names tie.

The Agents block stays lean (default + allowed-list, no env nesting).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): omit Environments preview block when workspace has no env vars

The main-screen Environments preview block was rendering an empty
"(no environment variables)" placeholder for workspaces with zero env
entries — visually noisy and unhelpful when most workspaces don't have
any. Now the block is omitted entirely when both workspace-level env
and per-agent overrides are empty; the Agents block fills the freed
space. Block appears as soon as any env entry exists at any scope.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): add 1-cell right padding on Environments preview agent label

Agent labels on per-agent env rows were rendering flush with the right
border. Subtract one cell from the right-padding budget so the label
ends one column before the border, matching the visual breathing room
of other right-aligned content.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): TextInput modal — 1-cell padding + subtle dim background on input field

Two small visual polish tweaks to the TextInput modal:

1. Input text now has a 1-cell left and right padding so it doesn't
   sit flush against the modal border.
2. The input field gets a very subtle dim background tint
   (Color::Rgb(20, 24, 22)) so the input region is visually distinct
   even when empty — hinting "this is where you type".

Applies to every TextInput-modal use across the TUI (EnvKey, EnvValue,
workspace name, mount paths, etc.) since they all share TextInputState.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): TextInput dim background respects 1-cell padding on each side

Commit 47 painted the dim INPUT_BG_DIM background across the full input
row, including the 1-cell pads on each side. The pads should stay the
panel color so the dim band reads as the input region, not the whole
row. Move the bg block from `input_row` (full inner width) to
`textarea_area` (inset by 1 each side); the band now spans only where
input actually lives.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(op): strip trailing newline from `op read` output

`op read` appends a trailing newline as CLI convention. The resolved
env value was carrying that newline through to the container — when the
shell exported the var, `env` showed a blank line after the value.
Strip exactly one trailing `\n` (and an optional preceding `\r` for
Windows line endings) so internal newlines in legitimately-multi-line
secrets survive while the CLI's terminal newline is removed.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(reviews): land Claude and Codex detailed reviews of PR #171

Two independent reviews of the workspace-manager-tui PR's 50-commit
scope, captured side-by-side under docs/superpowers/reviews/. Used to
guide the final pre-merge sweep — keeping them tracked so the audit
trail of what was checked, by whom, and what was flagged stays visible
in git history alongside the work itself.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(config): validate candidate before rename + reject reserved/unknown-agent in env setters

ConfigEditor::save was renaming the temp file over the real config
BEFORE running the validation that AppConfig::load_or_init performs.
Two operator-reproducible bricking cases:

1. `jackin config env set FOO bar --agent ghost` wrote `[agents.ghost]`
   without the required `git` field. Subsequent CLI commands failed at
   load with "missing field `git`" before they could unset the value.
2. `jackin config env set DOCKER_HOST tcp://bad` succeeded silently,
   then ALL future `config env *` commands failed at load with the
   reserved-runtime-name validation.

Two-layer fix:

- Setter-level pre-flight: reject reserved env names (DOCKER_HOST and
  the rest of the runtime list) and unknown agents at set_env_var
  time, before any document mutation. Faster failure, clearer error.
- Save-level safety net: parse the candidate file via the same
  validation load uses, BEFORE renaming over the real config. If
  validation fails, remove the temp file and propagate the error;
  the real config stays untouched.

Adds five integration tests covering the two reproduction paths plus
the save-level rollback invariant.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(console): refresh workspace list after manager save so launch routing isn't stale

ConsoleState.workspaces was a snapshot captured at console startup and
never refreshed. Manager edits (create/rename/edit/delete) rebuilt
ManagerState.workspaces but the launch dispatcher kept reading the
stale snapshot — so a renamed workspace appeared correctly in the list
but failed to resolve at launch, and edits to default_agent /
allowed_agents were ignored by launch routing until the next console
restart.

Chose Option B (derive launch choices from current AppConfig per-call)
because the call sites were tractable: only `dispatch_launch_for_workspace`
and the `LaunchWithAgent` arm in `run_console` consumed the snapshot,
and `WorkspaceChoice` is fully derivable from `(name, &AppConfig, &cwd)`.
Option B eliminates the duplication outright rather than papering over
it with refresh-after-save plumbing — there is no second source of truth
to keep in sync. `ConsoleState.workspaces: Vec<WorkspaceChoice>` and
`selected_workspace: usize` are dropped; a new
`pending_launch: Option<LoadWorkspaceInput>` pins the picker target
across the AgentPicker open→commit gap. `build_workspace_choice` is
exported from `console::state` and called per dispatch from current
config.

Adds four integration regression tests in tests/manager_flow.rs covering
create/rename/edit-default_agent/delete paths, plus two unit tests on
`build_workspace_choice` itself in src/console/state.rs (returns None for
unknown saved name; picks up default_agent from current config).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(op): use op-provided `reference` field; parse 4-segment as vault/item/section/field

Two correlated correctness fixes for 1Password reference handling:

1. Use the `reference` field that `op item get --format json` returns
   instead of synthesizing `op://Vault/Item/Field` from display names.
   The synthesized form was wrong for fields living inside sections,
   for items whose names contained `/` or whitespace, and for any case
   where 1Password's authoritative serializer differs from naive
   display-name concatenation. The new path always uses the string `op`
   itself produces — no synthesis, no escaping bugs.

2. Fix `parse_op_reference` to interpret 4-segment references per the
   official syntax (https://developer.1password.com/docs/cli/secret-reference-syntax/):
   - 3 segments: `op://<vault>/<item>/<field>`
   - 4 segments: `op://<vault>/<item>/<section>/<field>`
   The previous "4 segments = account/vault/item/field" interpretation
   was a jackin-specific extension that conflicted with op CLI itself.
   Account scope stays separate state on the picker (`selected_account`
   in OpPickerState) and is not encoded in the path.

The trust model is preserved: `RawOpField` continues to omit the
`value` field, and the safety test exhaustively destructures `OpField`
to fail compilation if anyone ever adds it. The destructure now also
covers the new `reference` field.

Cross-account references where the chosen vault lives outside the
op CLI's default account remain a documented limitation for this PR
— operator must ensure the relevant account is signed in.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(op): timeout op CLI probe; async account_list in picker constructor

OpCli::probe was using plain Command::output() with no timeout — a
wedged op (network stall, biometric held open, etc.) would freeze the
caller indefinitely. Route probe through the same channel-and-thread
timeout pattern that read/vault_list/item_list/item_get use; extract
spawn_op_with_timeout so probe (no JSON parse) and run_op_json (JSON
parse) share the spawn-and-wait primitive.

The picker constructor was calling account_list synchronously, which
blocked the TUI render loop before the spinner could paint. Move the
initial probe onto a worker thread (sharing a single Arc<dyn
OpStructRunner + Send + Sync> with the picker so test stubs are
captured instead of replaced by a fresh OpCli); the picker renders
the spinner immediately, account_list completes asynchronously, and
poll_load routes 0/1/multi to the right state via a new
handle_accounts_loaded helper.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(op): truncate_stderr respects UTF-8 char boundaries

truncate_stderr was slicing a String at byte index 4096. If that byte
fell inside a multi-byte UTF-8 character (e.g., op CLI emitting an
error in a non-ASCII locale, or any future op output containing
multi-byte glyphs), the slice would panic on an error path. Round the
truncation point down to the nearest char boundary at or before MAX.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): EnvValue modal allows empty values; target-specific input validity

TextInputState::is_valid was globally rejecting empty input, which is
correct for EnvKey/Name/Workdir but wrong for EnvValue — POSIX env
semantics distinguish VAR="" (set to empty) from unset VAR, and some
workloads use empty values to clear inherited defaults.

Add an allow_empty flag on TextInputState (settable via
new_allow_empty constructor); is_valid honors it for the empty-string
case while still rejecting duplicates. Wire the EnvValue modal sites
to construct via new_allow_empty. Other text-input targets keep the
non-empty default.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(tui): SourcePicker cycle dedup + align Secrets letter modifier guards

Two small consistency tweaks called out by review:

1. SourcePickerState had byte-identical cycle_forward and cycle_backward
   methods (a 2-button toggle has only one transition). Collapse to a
   single cycle() matching ScopePickerState's pattern.

2. The Secrets-tab letter shortcuts were using two different modifier
   guards: m|M and p|P checked `(modifiers - SHIFT).is_empty()` (the
   convention codified in RULES.md § TUI Keybindings), while d|D and a|A
   checked `!modifiers.contains(CONTROL)` (accepted Alt/Cmd/Super
   silently). Align d|D and a|A with the canonical pattern so all four
   letter shortcuts behave the same way.

Both items are pure refactors — no operator-visible behavior change.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(op): picker async workers exercise the injected runner

Commit 55 switched the picker's runner field from Box<dyn OpStructRunner +
Send> to Arc<dyn OpStructRunner + Send + Sync> so worker threads share
the same runner instance the constructor was given. That removed the
test-vs-production gap where injected mocks were silently bypassed by
spawned threads (which were building fresh OpCli instances).

Land the regression tests that exercise this newly-reachable surface:
vault_list, item_list, and item_get all now pass through the injected
mock when called from worker threads. Future changes to the picker's
async loading paths can be caught by tests instead of only at runtime.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(reviews): mark PR #171 review findings as resolved

All findings in `pr-171-claude-detailed-review.md` and
`pr-171-codex-detailed-review.md` were addressed by commits 52–59
(workspace-list refresh, candidate-config validation, op://
reference fidelity, EnvValue empty-OK, async account_list, UTF-8
stderr truncation, async-worker test seam, SourcePicker dedup,
Secrets-tab modifier-guard alignment).

Move each finding to a "Resolved" section with the commit hash that
closed it so the markdown reflects current branch state and future
review passes don't re-flag closed concerns.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(op-picker): refresh stale comments about runner threading

Several comments in `src/console/widgets/op_picker/mod.rs` still
described the pre-commit-55 contract where worker threads built a
fresh `OpCli` regardless of the injected runner. That changed when
`OpPickerState::runner` switched from `Box<dyn OpStructRunner +
Send>` to `Arc<dyn OpStructRunner + Send + Sync>` and
`runner_clone_for_thread` became a thin `Arc::clone`. Reword:

- `OpPickerState::new` doc — "Boxes a fresh OpCli" → "Wraps a fresh
  OpCli runner in an Arc". The constructor wraps in `Arc::new`, not
  `Box::new`, and the wrapping is what enables the worker-thread
  share.
- `enter_on_account_advances_to_vault_with_account_scope` test doc —
  drop the "spawned thread runs through `runner_clone_for_thread`"
  line and the parenthetical claim that the spawned thread builds a
  fresh OpCli. Replace with an explicit pointer to the
  `vault_list_uses_injected_runner_in_async_worker` test that
  exercises the worker-thread path through the injected stub.
- "OpCache integration tests" header comment — drop the "(always
  OpCli)" phrasing and reframe the section as "tests that
  short-circuit before the worker spawn", which is what they
  actually do today.

No code or behavior change.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(tui): consolidate pending env writes through one helper

Three Secrets-tab commit paths produced a final
`(scope, key, value)` triple and inserted it into `editor.pending`:

- `OpPicker` key-row commit (already routed through a helper);
- `EnvValue` text-modal commit (open-coded match);
- `EnvKey` commit's sentinel-picker fast path (open-coded match).

The two open-coded sites repeated the agent-scope auto-create +
`secrets_expanded.insert(agent.clone())` dance with the same
`pending.env.insert(...)` / `pending.agents.entry(...).or_default()`
shape. A future change to the contract (e.g. tracking dirty rows,
emitting a save-prompt dirty bit, recording last-edited timestamps)
would have to update three places in lockstep.

Rename `apply_picker_value_to_pending` → `set_pending_env_value`
since it's no longer picker-specific, expand the docstring to spell
out all three callers and the auto-expand-section invariant, and
route the two open-coded sites through it. Net `-13 +12` LOC; no
behavior change. All 1046 nextest cases still pass.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(comments): trim verbose docs in operator_env + op_cache

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(comments): trim verbose docs in widgets

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(comments): trim verbose docs in manager state + input

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(comments): trim verbose docs in manager render

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(comments): trim verbose docs in console + config/editor

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* chore(comments): trim verbose docs in tests

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(operator_env): collapse repeated layer loops in validate + merge

`validate_reserved_names` repeated the same `for key in env.keys() { if
is_reserved(key) { offenses.push(...) } }` pattern four times, once per
EnvLayer variant. With the explanatory comments trimmed it was just four
copies of the same shape. Hoist into a local closure that takes the
layer label and the env map; the four call sites now read as a tiny
table of layer constructors.

`merge_layers` likewise had four byte-identical insertion loops; iterate
over the four layer references in priority order instead.

No behavior change. Test count unchanged at 1046.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(operator_env): extract build_attributed_layers helper

`resolve_operator_env_with` and `write_launch_diagnostic` each rebuilt
the same per-layer attributed map: global → agent → workspace →
workspace-agent, later wins. Two ~30-line copies of the same precedence
ladder, with comments now stripped, made the duplication obvious.

Hoist into `build_attributed_layers(config, agent, workspace)`. The
diagnostic site keeps the single extra step it needs (drop keys not in
`resolved`). The resolver site keeps the dispatch loop on top.

A regression in either copy of the ladder would have silently dropped
attribution for a layer; one source means errors and diagnostics are
guaranteed to agree on which layer supplied each key.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(op-picker): hoist Up/Down cursor cycle into a shared helper

Each of the four pane key handlers (Account/Vault/Item/Field) had a
byte-identical Up/Down arm that computed `(cur == 0 ? n-1 : cur-1)` /
`(cur+1 >= n ? 0 : cur+1)`. Eight near-identical bodies — readable in
isolation, painful to keep in sync if the wrap-around rule ever
changes.

Hoist into `OpPickerState::step_selection(&mut ListState, count, delta)`.
Each pane site now reads as one line per direction: compute filtered
count, call the helper.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(workspace): derive Default on WorkspaceConfig; collapse fixtures

`WorkspaceConfig` had every field defaultable (`String`, `Vec`,
`Option`, `BTreeMap`) but no `Default` derive, so 20+ fixtures across
console tests, two production builders, and the editor's `new_create`
spelled out all seven fields by hand. With the trimmed comments off,
the noise from the literals dominated the test bodies' actual intent
(which mounts? which agents? which env?).

Add `#[derive(Default)]`. Rewrite each fixture site to either
`WorkspaceConfig::default()` (when it really wants the empty
workspace) or struct-update syntax (`{ field: ..., ..Default::default() }`)
when it customizes one or two fields. Test bodies are now only as wide
as the field they care about.

Production sites updated to match: `EditorState::new_create` and
`WorkspaceCreate::build_workspace` use `WorkspaceConfig::default()` /
struct-update. No serialization or behavior change — the `Default`
impl produces the same all-empty value that every fixture was
spelling out.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(widgets): unify cursor-cycle helper across all list pickers

`agent_picker`, `github_picker`, `workdir_pick` each carried a
hand-rolled wrap-around cursor body inside their Up/Down arms — six
near-identical clones of the `(cur == 0 ? n-1 : cur-1)` /
`(cur+1 >= n ? 0 : cur+1)` pattern. `op_picker` had its own private
`Self::step_selection` helper covering the same four-pane case.

Hoist the helper to `console::widgets::cycle_select` (pub(crate)), drop
op_picker's private copy, and route all five pickers through it. The
`unwrap_or(0)` vs `map_or(0, …)` difference was non-load-bearing: every
caller already initializes `select(Some(0))` on a non-empty list and the
`count == 0` short-circuit covers the empty case identically.

Net: -45 LOC, single source of truth for wrap semantics, zero
behaviour change.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(workspace): finish WorkspaceConfig fixture sweep across remaining sites

Iteration 2 added `Default` to `WorkspaceConfig` and converted six TUI
test files that dominated the noise. Fifteen further sites still spelled
out the seven-field literal: production helpers (`resolve.rs`,
`planner.rs`, `mod.rs`), config CRUD (`config/mod.rs`,
`config/editor.rs`, `config/workspaces.rs`), state-machine fixtures
(`console/manager/state.rs` + four `input/*.rs` siblings),
operator-env reserved-name validators, app-context targeting, and two
integration test files (`manager_flow.rs`, `workspace_config_crud.rs`).

Each site now uses struct-update (`{ workdir: …, mounts: …,
..Default::default() }`) — keeping only the fields the test actually
cares about. No serialization or behaviour change: `Default` produces
the same all-empty values the literals were spelling out.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* test(manager_flow): collapse seed_config into seed_config_with_env

`seed_config` and `seed_config_with_env` were near-identical except for
one field — `env: BTreeMap::new()` vs `env: env_map`. Now that
`WorkspaceConfig::default()` lands the empty case, `seed_config`
delegates: `seed_config_with_env(paths, temp_dir, vec![])`.

Removes one duplicated workspace-skeleton fixture; both tests-paths
exercise the same code instead of two near-identical copies that could
drift.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(reviews): drop fully-resolved Claude + Codex reviews

All findings from both reviews have been addressed across commits 52–59
and subsequent polish iterations. The full audit trail (file contents,
finding-to-commit mapping, every fix discussion) lives in the git log
itself; the standalone markdown files no longer carry information that
isn't already searchable in commit messages and PR history.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor: replace production unwraps with structural matches

Four `.unwrap()` sites in non-test code each guarded an invariant
already established by the immediately preceding `match` on a
collection's length, but expressed it as `into_iter().next().unwrap()`
or `as_ref().unwrap()`. Rewriting each as a slice-pattern match,
`Option::map_or`, or `swap_remove(0)` carries the invariant in the
type rather than in the prose, so a future refactor that drops the
length guard becomes a compile error rather than a runtime panic.

No behavior change.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(tests): finish WorkspaceConfig fixture sweep in manager_flow

Three remaining `WorkspaceConfig { … last_agent: None, env: …::new(),
agents: …::new() }` literals in manager_flow's helpers and tests still
spelled out the all-default tail by hand. The previous sweep landed
the pattern across the repo but skipped these three because each had
a non-default field interleaved (`allowed_agents`, `default_agent`)
that hid the trailing `..Default::default()` opportunity from a
literal grep. Hoist the trailer everywhere; the fixture intent
becomes "set these specific fields, defaults for the rest" rather
than mechanical zero-elision.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(manager): make manager-private submodules non-pub

`agent_allow`, `create`, and `github_mounts` are referenced only from
inside `console/manager/`. Drop their `pub` to express that boundary
in types — the public surface of the manager module remains `input`,
`render`, `state`, and `mount_info` (the last is reached from the
file-browser git prompt).

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(render): drop dead op_accounts chain + merge is_op double-scan

- Remove `op_accounts: &[OpAccount]` from `render_secrets_key_line`,
  `render_secrets_tab`, and `render_editor`; the param was immediately
  discarded with `let _ = op_accounts` and carried no information.
- Replace the two-scan pattern (`is_op_reference` then `parse_op_reference`
  on the same string) with a single `parse_op_reference` call; use
  `.is_some()` to derive the `[op]` marker. Eliminates one `starts_with`
  scan per key row per frame.
- Update the two `render_secrets_tab` call sites in tests and the single
  `render_editor` call site in `render/mod.rs`.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(nav): eliminate double secrets_flat_rows allocation on cursor move

`step_secrets_cursor_down` and `step_secrets_cursor_up` each called
`secrets_flat_rows` internally, duplicating the allocation in the same
keystroke that also called `secrets_flat_row_count` (another clone of the
same Vec). On every Down keypress in the Secrets tab, two full Vec<SecretsRow>
were allocated and dropped.

Fix: compute `rows` once at the call site, pass `&[SecretsRow]` into the
step helpers. Both helpers lose their `editor`/`config` parameters and the
internal `secrets_flat_rows` call. Remove `secrets_flat_row_count` entirely
(it was just `.len()` of the Vec) and drop it from the import and the now-
unreachable `Secrets` arm of `max_row_for_tab`.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(op-picker): borrow account id for cache writes instead of cloning

Add `selected_account_id_ref() -> Option<&str>` alongside the owned
`selected_account_id() -> Option<String>`. Use the reference form in the
three `poll_load` arms that write to the op cache (vaults, items, fields) —
they only need `&str` for the cache key, not an owned `String`. The owned
form is still used by `start_*_load` callers that move the value into a
spawned thread.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* style: rustfmt pass after render/editor.rs refactor

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): correct misleading footer hints + document op_picker invariant

Footer:
- Remove `M mask/unmask` from AgentHeader and add-sentinel footer arms;
  `toggle_focused_row_mask` returns early for both row kinds (`_ => return`),
  so advertising the key was a silent lie.
- Remove `M mask/unmask` from the SectionSpacer/None fallback arm (cursor
  never lands there). Degrade to empty hint.
- Suppress "Enter pick working directory" hint on General tab row 1 when
  `pending.mounts.is_empty()`; `open_editor_field_modal` silently does
  nothing when there are no mounts to pick from.
- Remove dead `enumerate()` + `let _ = i;` in `footer_spans`.

OpPicker:
- Document the `self.accounts.len() > 1` multi-account invariant in
  `handle_accounts_loaded` and the `handle_vault_key` Esc guard; the single-
  account fast-path intentionally leaves `self.accounts` empty, making
  the len check the canonical multi-account signal.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(render): thread live config into current-dir details pane

`render_current_dir_details_pane` was constructing `AppConfig::default()`
for both the agents-block height calculation and the `render_agents_subpanel`
call, so the Agents sub-panel on the "Current directory" row always rendered
zero agents regardless of what was actually configured.

Fix: add `config: &AppConfig` parameter and thread it in from the call site
in `render_list_body` (which already had `config`).

Also: document the `TempDir` drop requirement in `seed_config_with_env` so
a future test can't accidentally drop the guard before assertions complete.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(tui): cap startup op probe at 3s; gate P key + hint on op_available

Startup probe:
- Add `OpCli::new_probe()` with a 3-second timeout. `OpCli::new()` retains
  the 30-second `OP_DEFAULT_TIMEOUT` for all operational calls (vault-list,
  item-get, etc.) where blocking is expected. The startup probe only needs
  to know whether `op` is installed and responsive; a false negative here is
  acceptable — the picker renders its own error state on first open.
- Use `new_probe()` in `ConsoleState::new` so a network-stalled or
  biometric-blocked `op` binary cannot freeze `jackin console` startup for
  up to 30 seconds before the terminal is painted.

P key guard:
- Add `&& op_available` to the `P` arm in `handle_editor_key`; when `op`
  is absent the key silently does nothing rather than opening a picker that
  immediately shows a fatal error panel.
- Thread `op_available: bool` through `render_editor` → `contextual_row_items`
  and conditionally omit the `P · 1Password` footer items on key rows and
  add-sentinel rows when `op` is unavailable.
- Set `state.op_available = true` in the five `op_picker_*` integration tests
  that exercise the `P` key path directly.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(confirm-save): off-by-one in required_height clips last diff line

The layout comment correctly counted 7 chrome rows (2 borders + top blank +
after-content blank + buttons + after-buttons blank + hint), but the
implementation returned N + 6. The modal was sized 1 row too short,
clipping the last changed-field line in every ConfirmSave dialog.

Fix: return N + 7, matching the documented layout. Update the test
assertion from 9 to 10 for a 3-line state.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): select just-saved workspace on return to list

After saving a workspace via the Esc → Save path (or after creating a new
one), the list cursor now lands on the workspace that was just written rather
than defaulting to the CWD row or the first entry.

The final on-disk name (`current_name`) is already correct in all cases —
the rename path updates it before the edit_workspace call — so it can be
looked up in the freshly-rebuilt ManagerState and used to set `selected`.

Adds a regression test with two workspaces to verify the cursor targets the
right entry, not just "index 1" by accident.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* refactor(confirm-save): move Create-mode env section into its match arm

The env-vars block for EditorMode::Create was appended after the match via
a second `if matches!(...Create)` guard, while the Edit arm correctly handled
its env section inline. This split made the Create arm appear complete when
it wasn't, risking duplication if someone added env logic inside the arm
without seeing the post-match block.

Move the env section into the Create arm directly, after the default-agent
block. Behaviour is identical; the structure now matches the Edit arm.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(op-picker): clear selected_item on Field→Item Esc back-navigation

`handle_item_key` Esc (Item → Vault) clears `selected_item = None`.
`handle_field_key` Esc (Field → Item) did not, leaving a stale item
reference on `self` while the operator was back on the Item pane.

The stale reference caused the breadcrumb to show the previous item name
during the first render frame after back-navigation. Add the clear to
match the established Esc-back pattern.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* feat(tui): s always exits to workspace list after successful save

Previously `s` used exit_on_success = false — it saved but kept the
operator inside the editor. The Esc → Save path already used
exit_on_success = true (returning to the list). This made `s` the odd
one out: the operator had to press Esc a second time to get back.

Change `s` to also use exit_on_success = true. Both save paths now
return to the workspace list after a successful write, with the cursor
positioned on the just-saved workspace (behaviour from an earlier commit).

Update two tests that were asserting the old "stays in editor" behaviour.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* fix(workdir-pick): scroll list so selected row stays visible

The picker rendered all choices as a Paragraph, which clips without
scrolling. On a deep directory tree (e.g. /home/user/Projects/repo
producing 4-6 ancestor choices) the highlighted row could disappear
off the bottom of the modal with no way to see it.

Apply an offset so the selected row is always within the visible
viewport: offset = selected.saturating_sub(visible - 1). Chain skip()
+ take() directly into the line-building iterator to avoid an
intermediate Vec allocation (clipp…
donbeave added a commit that referenced this pull request May 6, 2026
* docs(roadmap): iteration 1 — readability & modernization analysis

Full first draft of READABILITY_AND_MODERNIZATION.md covering §0–§10:
project inventory (72 files, 40k LOC), concept-to-location index (25
entries), documentation hierarchy proposal, source-code structural
diagnosis (single-crate recommendation, 7 module-shape rules, hot-spot
list), naming candidates, CI/tooling analysis, modernization candidates
(13 entries with alternatives comparisons), AI-agent workflow (§8.1–§8.3:
spec-driven development, superpowers alternatives, docs boundary), risks,
and execution sequencing.

All claims grounded in direct code reading (cited file paths and line
ranges). All crate/tool recommendations include 2–3 alternatives. PR #171
branch read for AgentPicker, OpStructRunner, RawOpField, and RULES.md
additions.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 2 — launch.rs deep analysis, TS strictness, preview workflow

Four concrete improvements over iteration 1:

- §4: Deep-read src/runtime/launch.rs (2368L) in full; mapped every
  function to exact line ranges and dependency graph; refined split
  proposal into 4 files (launch.rs ~120L, launch_pipeline.rs ~560L
  prod + 1200L tests, terminfo.rs ~110L, trust.rs ~60L); key finding:
  test module (1282L) exceeds production code (1083L).

- §7.11: Discovered docs/AGENTS.md documents both TypeScript strictness
  blockers explicitly (rainEngine indexed access + astro-og-canvas
  optional-property types); verified rainEngine.ts at 5 line locations;
  rewrote recommendation as a concrete 4-step fix plan.

- §6: Read preview.yml in full — Homebrew tap rolling-preview pipeline;
  flagged missing contributor documentation; resolved OQ3.

- §2 concepts 4 & 6: Exact PR #171 data — TICK_MS=50 at console/mod.rs:90
  with 20Hz rationale; compile-time safety test is an exhaustive struct
  destructure (not trybuild) at operator_env.rs:2055–2096.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 3 — operator_env.rs deep analysis, snapshot tests, OQ7

Three concrete improvements over iteration 2:

- §4: Deep-read src/operator_env.rs (1569L); mapped every function to
  exact line ranges; identified two distinct clusters (op CLI subprocess
  layer lines 96-364 vs env layer resolution lines 365-808) with ~90L
  connective tissue; proposed module-directory split into mod.rs (~100L),
  client.rs (~280L), layers.rs (~470L), picker.rs (~250L PR#171);
  dependency graph shows no circular imports.

- §7.5: Named first 3 concrete snapshot test targets with file:line
  citations — render_sentinel_description_pane (list.rs:306, zero state,
  ~10L), render_tab_strip (editor.rs:180, 4 enum variants, ~20L),
  render_mounts_subpanel (list.rs:408, 3 data-driven cases, ~30L).

- OQ7 resolved: docs/package.json confirms astro-og-canvas ^0.11.1;
  docs/src/pages/og/[...slug].png.ts:~35 has `logo: undefined` which
  is the exact exactOptionalPropertyTypes conflict; fix is one line
  (omit the property). §7.11 and §9 updated accordingly.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 4 — config/editor.rs deep analysis, exact rustdoc coverage

Two concrete improvements:

- §4: Deep-read config/editor.rs (1467L); key finding: production code is
  only 503L (lines 1-503), tests are 963L (lines 504-1467 — nearly 2x);
  mapped all 18 public methods with exact line ranges into 5 domain groups;
  flagged create_workspace/edit_workspace validation-first architectural
  pattern as a refactor-preserve invariant; proposed 6-file module-directory
  split; priority note: lower than launch.rs/operator_env.rs since production
  code is already a reasonable size.

- §1/§4/§7.6: Corrected rustdoc coverage from estimate "~28%" to exact
  count 37/90 files = 41%; identified cluster: console/manager/ and
  console/widgets/ (PR #171 additions) are well-covered; runtime/, app/,
  cli/ lag; updated all three occurrences consistently.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 5 — concrete brainstorm template, §10 step 4 ordering, §5 pruning

Three concrete improvements:

- §8.2: Added 17-line concrete docs/internal/agent-skills/brainstorm.md
  template with 6 fields (Purpose/When/Steps/Outputs/Done when/Overlap
  guard). The Done-when and Overlap-guard fields are the discipline gates
  missing from superpowers default brainstorming. Makes §8.2 immediately
  actionable instead of aspirational.

- §10 step 4: Refined split ordering from sketch to priority-grounded
  sequence: config/types (4a) → manifest (4b) → config/editor 503L (4c)
  → operator_env 810L (4d) → app/dispatch (4e) → runtime/launch 1083L
  (4f last). Each sub-step has architectural notes and what-could-go-wrong.
  Flags the operator_env circular-dependency check for layers.rs→client.rs.

- §5: Replaced 3 "leave as is" non-candidates (rows 10, 12, 15) with
  verified candidates from code reading: provision_claude_auth→apply_auth_forward
  (auth.rs:17), AuthProvisionOutcome→AuthForwardOutcome (instance/mod.rs),
  spawn_wait_thread→spawn_exit_watcher (operator_env.rs:202).

- OQ5 resolved: instance/auth.rs read — only 210L production code,
  585L tests (3x); no split needed; removed from hot-spot concern.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 6 — hot-spot splits, §8 cc-sdd pivot, op_cache, logging

Four improvements:

- §1: Hot-spot table rebuilt with production/test LOC split columns for
  all 22 files. Key finding: manifest/validate.rs is 145L prod/816L tests
  (exemplary testing, not a god file); app/mod.rs is 928L prod/22L tests
  (most genuine god file after launch.rs). Adds Priority column.

- §8: Operator prefers existing tools over hand-rolled skill files.
  §8.1 recommendation pivots from hand-rolled (Option C) to cc-sdd
  (Option B). §8.2 rewrites comparison table — cc-sdd covers
  spec/plan/execute; TDD/debugging/review covered by existing project
  docs (TESTING.md, open-review-findings.mdx). Removes custom brainstorm
  template from iteration 5; no docs/internal/agent-skills/ dir needed.

- §2 concept 14: Session-scoped op cache confirmed at
  src/console/op_cache.rs (252L, all production, no tests). Full detail:
  BTreeMap keyed by (account, vault_id, item_id) tuples, DEFAULT_ACCOUNT_KEY
  sentinel, invalidation methods, //! doc confirms metadata-only guarantee.

- §7.14: New modernization entry — structured logging (log vs tracing
  vs current eprintln! approach). Recommendation: defer. Research in
  _research_notes.md.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 7 — MDX specs, §9 dep graph fix, §7.13 Renovate full entry

Five concrete improvements:

- §8.1/§8.3/§3: Operator direction — specs become public Starlight MDX
  pages at docs/src/content/docs/specs/ (draft: true while in-progress),
  not in docs/internal/specs/. Living source of truth updated with each
  code change. §8.3 boundary contract rewritten for public-spec model.
  §3 target shape updated to remove internal specs/ and add public specs/.

- §9 R1: Corrected from "circular import risk" to "compilation-at-distance
  risk". Verified by grep: config imports workspace (config/mod.rs:1,5,6)
  but workspace does NOT import config. One-way dep: config → workspace.
  No circular risk; real risk is 30+ use path updates when AppConfig moves.

- §10 step 2: Rewritten to match cc-sdd + Starlight MDX approach (was
  still describing hand-rolled docs/internal/agent-skills/ from pre-
  iteration-6 recommendation). Added draft page / lychee link-check caveat.

- §7.13 Renovate: Full six-subheading entry replacing 2-sentence stub.
  Evaluated Dependabot and Renovate Cloud App; both rejected due to DCO
  sign-off constraint (RENOVATE_GIT_AUTHOR env var in renovate.yml:26
  is not replicable in either alternative). Two low-cost tunings named.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 8 — lychee draft-page constraint, concept 18 correction

Three improvements:

- §8.1: Draft-page lychee constraint verified from docs/lychee.toml (full
  read). exclude_path has only 404.html pattern — no draft exclusion.
  Starlight draft pages exist in dist/ and are scanned by PR-time lychee
  check. Two fix options added: keep specs link-free, or add exclude
  pattern to lychee.toml. Added Astro sidebar requirement: manual config
  at astro.config.ts:50-103; autogenerate{ directory: 'specs' } pattern.

- §2 concept 18: Corrected a wrong proposed move. AuthForwardMode →
  instance/auth.rs was incorrect — the type is used in 9 files, is a
  config field at config/mod.rs:89,96, and has serde Deserialize at
  line 74. Moving to instance/ would create circular dep. Corrected to:
  correctly placed, will move intra-module to config/types.rs in §10 4a.

- §4 AuthForwardMode false alarm closed: confirmed NOT a §4 Rule 3
  violation. Type is correctly in config because it IS a config value.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 9 — eprintln count, CODE_TOUR content, step 4a serde note

- §7.14: grep-verified 96 production eprintln! calls across 16 files;
  breakdown by file; no rogue debug calls found, flip condition has not triggered
- §2 concept 8: full 4-hop load chain with exact line numbers; documents
  the /__/ separator invariant in runtime_slug that prevents namespace
  collisions; CODE_TOUR column now specifies what the tour entry must explain
- §10 step 4a: confirms AuthForwardMode serde impl is a plain impl<'de>
  block (not a derive), moves with the type to config/types.rs automatically

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 10 — dispatch_value scope, Renovate automerge, //! exemplar

- §5 row 6: dispatch_value → resolve_env_value rename is 1 prod + 6 test
  call sites in a single file — lowest-cost rename in the table
- §7.13: adds minimal safe automerge pattern for lockFileMaintenance only;
  explicitly excludes patch/minor bumps and SHA-pinned Actions
- §4 Rule 7: expands //! exemplar analysis with direct quotes from
  env_model.rs:1-17 and isolates the three-element pattern

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): strip iteration annotations; reorder log chronologically

READABILITY_AND_MODERNIZATION.md now presents a clean final view:
- Remove all "(iteration N)", "(verified iteration N)", "read in full for
  iteration N", "updated iteration N" qualifiers throughout the body
- Remove the Iteration counter from §0 meta
- Resolved OQ markers simplified from "(resolved in iteration N)" to
  "(resolved)"; deferred OQ markers from "Tracked for iteration N" to
  "Deferred"

_iteration_log.md now has entries in chronological order 1–10:
- Was: 1, 3, 4, 5, 6, 7, 10, 8, 9, 2
- Now: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): research OpenSpec and IIKit; update §8 landscape

_research_notes.md:
- OpenSpec (Fission-AI/OpenSpec): brownfield-first SDD, three-phase
  propose/apply/archive, delta markers (ADDED/MODIFIED/REMOVED), living
  specs at openspec/specs/; /loop-compatible; recommended as cc-sdd
  complement for §4 module-split proposals
- IIKit (intent-integrity-chain/kit): cryptographic .feature hash-locking,
  8-phase workflow, Gherkin BDD chain, Tessl-only install; not recommended
  for jackin (no Gherkin step runner, Tessl lock-in, heavyweight for
  single-maintainer)

READABILITY_AND_MODERNIZATION.md §8.1:
- Adds Options E (OpenSpec) and F (IIKit) to the landscape and evaluation
  table with brownfield delta tracking and Rust/nextest compatibility rows
- Adds OpenSpec complement note after the cc-sdd recommendation

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 11 — dtolnay correction, ci.yml detail, snapshot targets

- §7.7 / §2 concept 25 / §10 step 3: correct false claim that
  dtolnay/rust-toolchain reads rust-toolchain.toml; action version is
  encoded in SHA rev; three sources require manual sync
- §6 ci.yml row: expand from high-level to exact job steps; document
  gaps (no MSRV job, floating tags in build-validator, no doc job,
  main jackin binary never compiled in CI)
- §7.9: grep-confirm all three snapshot target fn names; add exact
  signatures and private-fn access pattern (use super::*) with
  list.rs:720 as existing proof

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 12 — ClassSelector scope, lint table, app split

- §5 row 5: ClassSelector → AgentClass rename is 138 prod call sites
  across 17 files — highest-scope rename in the table
- §7.8: full lint table enumeration from Cargo.toml:47-75; notes that
  cast allowances are global despite TUI-scoped inline comment
- §4 4e: app/mod.rs run() read in full; refined split from 1 file to 3
  (dispatch.rs ~167L, workspace_cmd.rs ~438L, config_cmd.rs ~220L)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): remove resolved/no-action items from roadmap

Keep only forward-looking actionable proposals:

§2 concept table: removed 6 rows with "Stable"/"No change needed"
proposals (RULES.md keybindings, hardline, mount planning, XDG paths,
Docker builder, env-var ordering); renumbered 1-19

§5 naming table: removed hardline_agent row (leave-as-is, no action);
rephrased load_agent row to focus on the //! doc action not the rename;
renumbered to 1-14

§9 open questions: removed OQ3 (preview.yml, resolved — in §6),
OQ5 (auth.rs split, resolved — no split needed), OQ7 (astro-og-canvas,
resolved — fix in §7.11); renumbered to OQ1-4; merged TS strictness OQ
back into the active list

Cleanup: removed "resolved OQ5" tag from auth.rs hot-spot entry;
removed "(resolved OQ7)" qualifier from §7.11 blocker label

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): update analysis baseline — PR #171 now merged to main

Remove all "after PR #171 merges", "PR #171 branch", "pre-merge", and
"not yet on main" qualifiers throughout. Specific changes:
- §0: update baseline note from "treated as merged" to "now merged"
- §1: remove branch-only qualifiers from file tree, module map, coverage count
- §2: concept rows 1, 2, 4, 5, 6, 10, 11, 12, 14, 15 updated to reflect
  code now on main; row 12 op_cache current-state rating updated from
  requires-tribal-knowledge (pre-merge) to requires-grep (post-merge)
- OQ1 (op_picker cache): updated from "deferred" to "read from main now"
- §4, §5, §7: remove "(PR #171)" and "adds/branch" language
- §10 step 1: remove "once PR #171 merges" condition

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 6, 2026
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Co-authored-by: Claude <noreply@anthropic.com>
donbeave added a commit that referenced this pull request May 7, 2026
* feat(tui): extend EditorState and save path for Secrets tab

Add secrets_masked / secrets_expanded / pending_env_key fields to
EditorState, SecretsScopeTag type, EnvKey/EnvValue TextInputTarget variants,
DeleteEnvVar ConfirmTarget variant. Extend change_count with env diff
logic. Wire commit_editor_save to compute env diffs and call
ConfigEditor::set_env_var / remove_env_var before save(). Extend
build_confirm_save_lines with an "Env vars:" diff section.

The new TextInputTarget / ConfirmTarget variants are unreachable in this
commit; their handlers land in commit 2 (UI). Chose Option A: placeholder
unreachable!() arm in apply_text_input_to_pending so all type definitions
are established in commit 1. ConfirmTarget has no exhaustive-match site
so it needed no placeholder (Modal::Confirm uses target: _).

Removing Copy from TextInputTarget (EnvValue now carries a String) forced
minor follow-on changes: pass-by-reference at the apply_text_input_to_pending
callsite and `&Variant`-style comparisons in existing test assertions in
prelude.rs and editor.rs.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): implement Secrets tab render and input handlers

Replace render_secrets_stub with a sectioned, hierarchical render:
"Workspace env" header always visible, key rows with masked/unmasked
display, collapsible per-agent override sections, "+ Add" sentinels, dirty
markers. Wire input handlers: Enter (edit/expand/add per row kind), D
(delete confirm), A (add), Ctrl+M (mask toggle), Left on expanded headers
(collapse). Tab-cycle resets masking and collapses sections on leave.
EnvKey/EnvValue handlers in handle_editor_modal route the two-step add
flow; DeleteEnvVar handler removes the key from pending. Tab strip drops
"Secrets ⏳" placeholder label and dim/italic style.

Co-authored-by: Claude <noreply@anthropic.com>

* test(tui): Secrets tab unit and integration tests; update console docs

Add unit tests for change_count env-diff variants on EditorState and
render-buffer tests for masking and section expand/collapse. Add seven
integration scenarios in tests/manager_flow.rs covering edit, delete,
add, masking toggle, section expand/collapse, dirty detection, and the
two-step add flow. Append a "Workspace editor — Secrets tab" section
to docs/src/content/docs/commands/console.mdx documenting the keybindings
and scope semantics.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): wire Right arrow to expand collapsed Secrets sections

The Secrets tab key-map advertises `→` as an alias for `Enter` on a
collapsed agent override header (symmetric with `←` for collapse), and
the docs landed in commit 3 say so. Commit 2 left this unwired — Right
fell through to the tab-advance handler before the Secrets dispatch
could see it. Add a guard at the top of the `Tab | Right` arm that, on
the Secrets tab, expands the section when Right is pressed on a
collapsed `AgentHeader`. Tab and Right-on-non-header still advance.
Update the integration test to exercise `→` instead of `Enter` so a
future regression of the guard is caught.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(op): add OpStructRunner trait and OpCli implementation for vault/item/field listing

Add OpStructRunner trait with account_list, vault_list, item_list, item_get
methods. Implement on OpCli using the same spawn-and-channel timeout pattern
as OpRunner::read. Add serde shapes (RawOpVault, RawOpItem, RawOpItemDetail,
RawOpField) that explicitly omit field values — the picker is a metadata
browser that must not deserialize secret values into memory. Includes unit
tests for JSON parsing, value-suppression, signed-out detection, and the
concealed flag derivation.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): 1Password vault/item/field picker modal (Ctrl+O from EnvValue input)

Add OpPickerState, OpPickerStage, OpLoadState types in
src/console/widgets/op_picker/. Add render function with three-pane
drill-down, filter-as-you-type, Braille spinner, and four failure-state
instructional displays. Wire Modal::OpPicker variant into modal_outer_rect,
render_modal, and handle_editor_modal. Intercept Ctrl+O in the EnvValue
TextInput arm of handle_editor_modal; restore original text on Esc-cancel.
Add Ctrl+O footer hint to contextual_row_items Secrets key-row arm. Calls
OpStructRunner from commit 5; field values are never deserialized.

Co-authored-by: Claude <noreply@anthropic.com>

* test(tui): op picker unit/integration tests; update console docs

Unit tests on the op picker state machine: filter narrowing, pane
advancement clearing the filter, Esc back-navigation through Vault → Item →
Field, concealed-first field sort, commit returning the op:// path.
Integration tests in tests/manager_flow.rs: Ctrl+O from EnvValue modal opens
OpPicker; Esc-cancel restores the EnvValue modal with original text intact.
Docs: Ctrl+O row in Secrets key-map, plus a new "1Password picker" sub-
section documenting requirements and failure-state messages.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(rules): add TUI Keybindings section to RULES.md

No Ctrl/Alt/Cmd modifiers in TUI keybindings; actions on rows rather than
inside text modals. Applies project-wide starting with the Secrets tab
cleanup that follows in subsequent commits.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(tui): Ctrl+M → M for Secrets-tab mask toggle

Replace the Ctrl-modified mask binding with plain M, consistent with the
new RULES.md TUI Keybindings rule. Update footer hints, docs, and tests.
The ctrl_key test helper stays for now; it goes away in the next commit
once the Ctrl+O picker redesign also no longer needs it.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(tui): drop Shift+Tab support; tighten RULES.md rule

The TUI Keybindings rule now also forbids Shift modifiers, not just
Ctrl/Alt/Cmd. Shift+Tab is a chord; rule is "single-key inputs only".
Drop the standalone BackTab arm in the editor's tab-cycle handler
(KeyCode::Left already implements the previous-tab fallthrough). Drop
BackTab from confirm/save_discard/mount_dst_choice/confirm_save/git_prompt
alternations — Left and h still cover prev-direction. Replace BackTab
with Left in test drivers. Update inline comments.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(tui): move 1Password picker to row-level P action

Remove the Ctrl+O interception inside the EnvValue text modal. P on a
Secrets-tab key row opens the picker directly; on commit, the chosen
op:// reference is written straight to that key's pending value (no
intermediate text modal). P on the +Add sentinel opens the picker first;
the EnvKey modal then collects the key name with the path pre-stashed
on EditorState. Drop saved_target/saved_label/original_value from
OpPickerState — the picker no longer reconstructs the EnvValue modal.
Update footer hints, docs, and tests; remove the ctrl_key test helper
now that no caller remains.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): Modal::AgentPicker — inline agent disambiguation popup

Replace the full-screen ConsoleStage::Agent picker with a Modal::AgentPicker
overlay on the manager list. Delete draw_agent_screen, the legacy
handle_event Agent branch, ConsoleStage::Agent variant, ConsoleState's
selected_agent / agent_query fields, and filtered_agents method. Three-
branch launch logic: default_agent set → direct launch; single eligible
agent → direct launch; multiple eligible → popup. Add AgentPickerState
widget anchored on ManagerState.list_modal. New InputOutcome::
LaunchWithAgent variant. Tests updated: 5 legacy state/input tests
deleted, 5 new manager_flow integration tests for the popup, 3 unit
tests for the picker widget.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(op): accept account_uuid alongside id in `op account list` output

`op account list --format json` against op CLI v2.x reports each account
with an `account_uuid` key, not `id`. The picker probe was failing in the
field with "missing field `id` at line 7 column 3" when an operator with
multiple accounts pressed P → 1Password. Add a serde alias on RawOpAccount
so either shape parses. Add a unit test using a real-shape JSON fixture.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(spec): amend workspace-manager-tui-design for 2026-04-25 changes

Append a dated amendment block to the spec capturing how the Stage 3
implementation diverged from the 2026-04-23 design: manager-as-default
landing (not an excursion), single-variant ConsoleStage with
Modal::AgentPicker replacing the full-screen Agent stage, three-branch
launch logic, the no-Ctrl/no-Shift keybinding philosophy, and the
1Password picker moving from a Ctrl+O modal-internal action to a P
row-level action on the Secrets tab. Preserves the original record;
amendments captured separately.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(op): multi-account 1Password picker

Add an Account pane as the first stage of the 1Password picker, visible
only when the operator has 2+ accounts signed in to the op CLI. Single-
account setups skip the pane entirely (no UX regression). All downstream
op calls (vault_list, item_list, item_get) thread the chosen account_uuid
via --account <id> so cross-account drilling works correctly.

OpAccount gains email + url fields for human-readable display. RawOpAccount
serde shape uses #[serde(default)] on optional fields for tolerance against
older op versions. Esc from Vault returns to Account when multi-account;
otherwise closes the picker as before.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): Secrets tab — skip Workspace label as focusable row, align indents

The "Workspace env" section label was incorrectly focusable, so the
cursor landed on it when the tab opened. The label is now rendered as a
non-focusable section title above the list. All actionable rows (key
rows, "+ Add" sentinel) render at the same indent column. Agent override
sections still have focusable headers because they expand/collapse;
their content rows align with workspace-level rows for visual consistency.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): move default_agent to Agents tab; remove Last used from General

The General tab no longer shows the Default agent or Last used rows —
both were read-only and added clutter. Default agent is now set on the
Agents tab via the * key: cursor on an allowed agent and press * to set
it as default, or press * on the current default to clear it. Disallowing
the current default agent (Space) also clears the default. The default
agent is marked visibly in the Agents-tab list. Footer hints updated.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(op): session-scoped cache for picker structural metadata

Cache parsed Vec<OpAccount>/<OpVault>/<OpItem>/<OpField> results on
ConsoleState so repeated picker drilling within one `jackin console`
session is instant after the first call. Cache hits skip the op subprocess
entirely. Manual refresh via `r` keypress in any picker pane re-fires the
op call and updates the cache.

The cache stores only structural metadata. Field values from `op item get`
are never deserialized (RawOpField omits the JSON `value` key by design),
so credentials never enter the cache.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): picker scrolling and breadcrumb cleanup

Two picker UX fixes:

1. Scrolling now works in all four panes. The list widget's selection state
   tracks viewport offset; navigating below the visible window scrolls the
   list down. Repro: a vault with 20+ items.

2. Breadcrumbs drop the redundant trailing pane-name suffix. The pane
   content makes the type self-evident — "Items" / "Vaults" / "Fields"
   added noise. Titles now read "<email> → <vault>" instead of
   "<email> → <vault> → Items".

Co-authored-by: Claude <noreply@anthropic.com>

* feat(op): show item subtitle (additional_information) in picker

1Password lets multiple items share a title (two "Google" entries with
different usernames, for example), and only the additional_information
subtitle distinguishes them. Capture op item list's additional_information
field on OpItem.subtitle, then render Item-pane rows as `<title> (<subtitle>)`
with the parenthetical in PHOSPHOR_DIM so the title still reads as primary.
The subtitle also participates in filter-as-you-type so operators can search
by username when the titles collide.

Items without an additional_information value (e.g., secure notes) render
with just the title, no trailing parens.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): drop per-row "● unsaved" markers on General and Secrets tabs

The Mounts and Agents tabs never rendered per-row dirty markers; the
footer's `S save workspace (N changes)` is the canonical unsaved-state
indicator. The General tab's Name/Working dir rows and the Secrets tab's
key rows were the outliers — adding visual noise that made the editor
inconsistent with itself.

Drop the markers from both render paths and the now-unused dirty
parameter from render_editor_row / render_secrets_key_line. Remove the
orphaned env_key_is_dirty helper.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): non-blocking event loop — spinner ticks and op results land without keystrokes

The console event loop was calling `event::read()` directly, which
blocks until a key/mouse event arrives. While blocked, the Braille
spinner stopped advancing and worker-thread results sat in the mpsc
channel undelivered — both updates only ran when the next event
unblocked the loop, making the picker feel frozen for seconds at a time
even after `op` had finished.

Switch to `event::poll(Duration::from_millis(50))` + conditional
`event::read()`. Loop runs at 20 Hz regardless of input. Add
`ManagerState::poll_picker_loads()` that drains the OpPicker channels
on every tick, so loading state advances as soon as the worker finishes
rather than waiting for keystroke pumping.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): source-picker modal — Plain text / 1Password choice on env add

Adding a new env var via Enter on the +Add sentinel now walks through:
EnvKey modal (name) → SourcePicker modal (Plain text / 1Password) →
EnvValue modal or OpPicker, depending on the choice. The 1Password
button is disabled when the op CLI isn't on PATH; the picker still has
the existing `P`-on-key-row shortcut for direct picker access.

ConsoleState gains an `op_available` flag set once at startup. Sentinel-
add invocations always go through the source picker; existing key-row
edits and P-on-key-row picks are unchanged.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): accept Shift modifier on letter shortcuts (caps-lock parity)

Caps Lock causes terminals to send letter keys with the SHIFT modifier
set. Existing match arms guarded by `key.modifiers == KeyModifiers::NONE`
silently rejected those, so operators with Caps Lock on saw `M`/`D`/`A`/
`P`/etc. no-op everywhere — D delete, A add, M mask/unmask, P 1Password,
and so on.

Replace the strict NONE check with a SHIFT-tolerant guard
(`(modifiers - SHIFT).is_empty()`) on every letter shortcut in the
editor, manager list, and widget handlers. Ctrl/Alt/Cmd modifiers are
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): Secrets tab consistency — header label, empty state, "environment" wording

Three small consistency fixes to the Secrets tab:

1. Console header reads "edit workspace · <name>" (Edit) and
   "create workspace" (Create) instead of "edit · <name>" / "new
   workspace" — the missing "workspace" suffix was ambiguous.

2. Empty Secrets tab no longer renders "Workspace env" label or
   "(no env vars)" placeholder — both add no information and aren't
   present on Mounts/Agents tabs. The empty state is now just the
   "+ Add environment variable" sentinel.

3. User-facing TUI text uses "environment" instead of "env" — the
   abbreviation makes sense for CLI subcommand names but reads as
   noise in the TUI. Sentinel text, modal titles, and footer hints
   updated. CLI verbs and Rust identifiers (EnvScope, set_env_var,
   WorkspaceConfig.env) are unchanged — the TUI is the only surface
   where the full word matters.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): picker loading panel shows breadcrumb so operator knows what stage is loading

The picker title was emitting a full breadcrumb only after the load
completed, so during the 1-3s op subprocess the modal said just
"1Password" with no context — operator couldn't tell whether they were
waiting for vaults, items, or fields. Stage transitions advance at
request-time now (not result-time), so the title reflects the target of
the in-flight load: `<email> → <vault>` while items load, `<email> →
<vault> → <item>` while fields load, etc. Loading body messages adapt
the same way: "loading vaults from <email>", "loading items from
<vault>", "loading fields from <item>".

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): Q always opens an exit confirmation when not on the main screen

Pressing Q anywhere outside the main manager list now opens an "Exit
jackin'?" Y/N confirmation dialog. The operator can press N or Esc to
return to exactly where they were — useful when Q is accidentally hit
mid-flow. Q on the main screen still exits silently (today's behavior).

Q in a text input modal (EnvKey, EnvValue, picker filter) is treated as
input and never triggers the confirm. Routing is gated by
is_on_main_screen + consumes_letter_input checks at the top of the event
loop, so the dialog is the single chokepoint.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): field-load title shows parent context, item moved to loading body

While loading an item's fields, the picker title now shows the parent
context (`<email> → <vault>`) and the loading body names the item being
descended into (`loading ChainArgos Redshift (donbeave)…`). Previously
the title carried the item too, which made it ambiguous what was loading
versus already loaded. Principle: title = where you are, body = what
you're descending into. The same rule already applies to vault and item
loads (which were unaffected by this change).

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): block duplicate env keys live in the create-env-name modal

The EnvKey TextInput modal now carries the current scope's existing
environment-variable keys as a "forbidden" list. While the operator
types, an inline red warning shows when the name collides — and Enter
is blocked until the name is changed or cleared. Workspace-scope and
agent-scope sentinels each populate the forbidden list from their own
env map, so a key named "DB_URL" can coexist on workspace and agent
overrides without false positives.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): rename Secrets tab to Environments

The values managed in this tab aren't always secrets (DEBUG=1 is just a
config flag), so "Environments" is the more accurate label. Tab strip,
modal titles, and docs prose updated. Internal identifiers (EditorTab::
Secrets, SecretsRow, secrets_*) are unchanged — they describe the
codepath, not the user-facing label.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): op:// rows render as breadcrumb, never mask, ignore Enter

1Password references are paths, not credentials. Render them as a
breadcrumb (`<email>/<vault>/<item>→<field>` for multi-account,
`<vault>/<item>→<field>` for single-account) styled with dim/white/green
hierarchy so the field name reads as the addressable thing. Drop masking
on op:// rows entirely — the breadcrumb isn't a secret, no point masking
it. Block Enter from opening a text-edit modal on op:// rows; operators
delete and re-add via the source picker for changes. Plain-text rows
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): per-row masking — M toggles the focused row only

Replace the global secrets_masked bool with an unmasked_rows BTreeSet
keyed by (scope, key). M toggles membership for the focused row;
everything else stays masked by default. Tab leave clears the set so
re-entering the tab returns to the all-masked baseline. Op:// rows
ignore the set entirely — they render as breadcrumbs (commit 31), not
masked values, so M is a no-op on them.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): mark op:// rows with ⚿ glyph; sort env keys alphabetically

Visual differentiation between plain-text and 1Password environment
variables: a left-edge ⚿ (lock-and-key) glyph appears on op:// rows,
plain rows render two blank spaces in the same column for alignment.
Both row types share one alphabetically-sorted list — no section break
— so the operator can scan keys in order and see the source at a
glance.

BTreeMap iteration already produces sorted keys, so the alphabetical
ordering is a confirmation pass with a regression-pinning test rather
than a code change to the iteration order.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): add per-agent override entry point on Environments tab

Operators can now add overrides for agents that don't yet have any —
previously the override section only appeared once `pending.agents`
already had an entry, so a fresh agent had no UI path to its first
override key.

A new sentinel row at the bottom of the Environments tab — `+ Add
per-agent override for ...` — opens an agent picker filtered to allowed
agents without existing overrides. Picking an agent creates the override
entry, auto-expands the new section, and lands the cursor on the
section's `+ Add <agent> environment variable` sentinel so the operator
can immediately add the first key. The sentinel hides itself when no
eligible agents remain.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): per-agent override picker drops into normal add flow

The picker for per-agent overrides was creating an empty section with a
"(0 vars)" header and a redundant per-agent "+ Add" sentinel inside as
soon as the operator chose an agent — looking messy if the operator
canceled out, and visually inconsistent with how workspace-level entries
are added. Now picking an agent opens the EnvKey modal directly with
Agent scope, and the rest of the chain (SourcePicker → value) is the
same as the workspace-level path. The agent's section materializes
organically when the first key/value commits; canceling at any modal
step leaves pending.agents untouched, so no orphan sections appear.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): TextInput shows red border on duplicate, hides Enter hint when invalid

Three small polish tweaks to the EnvKey TextInput modal so the visual
state always matches what Enter will actually do:

- Duplicate value → red border (DANGER_RED), inline ⚠ warning, footer
  hint shows only `Esc cancel`.
- Empty value → default border (no error styling — empty isn't "wrong",
  it's "not ready"), no inline warning, footer hint shows only
  `Esc cancel`.
- Valid value → default border, footer hint shows `Enter confirm ·
  Esc cancel`.

Enter blocks on both empty and duplicate. The widget gains an
`is_valid()` predicate that gates both the commit and the hint.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): scope-first add flow — pick All agents / Specific agent at start

Replaces the bottom-of-tab "+ Add per-agent override for ..." sentinel
landed in commit 34 with a scope question at the start of the add
flow. Press Enter on the workspace-level "+ Add environment variable"
sentinel; the new ScopePicker modal asks "All agents" or "Specific
agent". All-agents drops into the workspace add flow as before.
Specific-agent opens the agent picker (now showing every allowed agent
— no longer filtered by "doesn't yet have overrides", since operators
legitimately want to add more keys to agents that already have some),
then the EnvKey modal with Agent scope. The fast-path
"+ Add <agent> environment variable" inside an expanded agent section
is unchanged.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): AgentPicker layout follows the canonical list-modal pattern

The AgentPicker drifted stylistically from the OpPicker — different
title format (filter inlined), missing persistent filter row, "no
matches" placeholder, hard-coded "Enter launch" footer text. Operators
saw two visually distinct pickers for the same conceptual "pick from a
list with filter-as-you-type" interaction.

Refactor to match the OpPicker's layout: persistent Filter: row at
top, list body below, standardized footer. AgentPicker gains a
configurable confirm_label so the Enter-action word reads "launch" in
the launch-disambiguation path and "select" in the override-scope path
— same widget, two contexts, no truncation or wrong wording.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(rules): document the canonical TUI list-modal layout

Codify the list-modal layout pattern (filter row, list body, footer)
that the OpPicker, SourcePicker, and AgentPicker now share. Future
picker widgets follow the same shape so the TUI stays visually
consistent across modals. Sits alongside the existing TUI Keybindings
rule in RULES.md.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): Environments tab layout — blank-line between sections, [op] marker

Three small layout fixes for the Environments tab:

1. Add a blank-line spacer between the workspace section and each
   agent override section. Cursor skips spacers on ↑/↓.
2. Replace the ⚿ glyph marker on op:// rows with a clearer `[op]` text
   marker. Better terminal compat and immediately legible as "1Password".
3. Standardize the row prefix at 7 characters (2-char cursor column +
   5-char marker column) so plain and op:// rows align across the tab.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): agent-section headers absorb ←/→ regardless of expanded state

Pressing → on an already-expanded section header (or ← on a collapsed
one) was falling through to tab cycling — operators saw the active
editor tab change unexpectedly when arrowing on `▼ Agent: <name>` or
`▶ Agent: <name>` rows. The header now absorbs ←/→ in both states:
acts on the row when relevant, no-ops otherwise, but never lets the
keypress reach the tab handler.

Codifies the convention as "Contextual key absorption" in
RULES.md § TUI Keybindings so future row types follow the same default.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): show environments on the main screen — workspace + per-agent

The main-screen workspace-manager preview gains a read-only
Environments block listing the highlighted workspace's env-variable
keys, sorted alphabetically with `[op]` markers on op:// references.
The Agents block now lists each agent's env overrides nested under the
agent name, with a "(no overrides)" placeholder for agents that don't
have any. Block order is General → Mounts → Environments → Agents so
the operator scans config-shaped data first and agent-specific things
last.

The preview stays read-only — `E` enters the editor for changes. Only
key names appear; values (plain or op://) never render.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): consolidate env preview — workspace + per-agent in one block

The main-screen Environments block now holds all env-related content:
workspace-level keys under an "All agents:" sub-header first, then each
agent's overrides nested under per-agent sub-headers. Agents without
overrides are omitted from this block — their absence is the signal.
The Agents block reverts to a lean summary (default + allowed agent
list) with no env nesting; per-agent env detail lives only in the
Environments block now.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): main-screen env preview is one flat alphabetical list with right-side agent label

Replace the workspace+per-agent sub-section layout with a flat,
alphabetical list. Each row shows the [op] marker, the env key name,
and (when scoped to a specific agent) the agent name on the right.
Workspace-level rows have an empty right column. Same env name
appearing at both workspace and agent level renders as two distinct
rows; sort puts workspace rows before agent rows when names tie.

The Agents block stays lean (default + allowed-list, no env nesting).

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): omit Environments preview block when workspace has no env vars

The main-screen Environments preview block was rendering an empty
"(no environment variables)" placeholder for workspaces with zero env
entries — visually noisy and unhelpful when most workspaces don't have
any. Now the block is omitted entirely when both workspace-level env
and per-agent overrides are empty; the Agents block fills the freed
space. Block appears as soon as any env entry exists at any scope.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): add 1-cell right padding on Environments preview agent label

Agent labels on per-agent env rows were rendering flush with the right
border. Subtract one cell from the right-padding budget so the label
ends one column before the border, matching the visual breathing room
of other right-aligned content.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): TextInput modal — 1-cell padding + subtle dim background on input field

Two small visual polish tweaks to the TextInput modal:

1. Input text now has a 1-cell left and right padding so it doesn't
   sit flush against the modal border.
2. The input field gets a very subtle dim background tint
   (Color::Rgb(20, 24, 22)) so the input region is visually distinct
   even when empty — hinting "this is where you type".

Applies to every TextInput-modal use across the TUI (EnvKey, EnvValue,
workspace name, mount paths, etc.) since they all share TextInputState.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): TextInput dim background respects 1-cell padding on each side

Commit 47 painted the dim INPUT_BG_DIM background across the full input
row, including the 1-cell pads on each side. The pads should stay the
panel color so the dim band reads as the input region, not the whole
row. Move the bg block from `input_row` (full inner width) to
`textarea_area` (inset by 1 each side); the band now spans only where
input actually lives.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(op): strip trailing newline from `op read` output

`op read` appends a trailing newline as CLI convention. The resolved
env value was carrying that newline through to the container — when the
shell exported the var, `env` showed a blank line after the value.
Strip exactly one trailing `\n` (and an optional preceding `\r` for
Windows line endings) so internal newlines in legitimately-multi-line
secrets survive while the CLI's terminal newline is removed.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(reviews): land Claude and Codex detailed reviews of PR #171

Two independent reviews of the workspace-manager-tui PR's 50-commit
scope, captured side-by-side under docs/superpowers/reviews/. Used to
guide the final pre-merge sweep — keeping them tracked so the audit
trail of what was checked, by whom, and what was flagged stays visible
in git history alongside the work itself.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(config): validate candidate before rename + reject reserved/unknown-agent in env setters

ConfigEditor::save was renaming the temp file over the real config
BEFORE running the validation that AppConfig::load_or_init performs.
Two operator-reproducible bricking cases:

1. `jackin config env set FOO bar --agent ghost` wrote `[agents.ghost]`
   without the required `git` field. Subsequent CLI commands failed at
   load with "missing field `git`" before they could unset the value.
2. `jackin config env set DOCKER_HOST tcp://bad` succeeded silently,
   then ALL future `config env *` commands failed at load with the
   reserved-runtime-name validation.

Two-layer fix:

- Setter-level pre-flight: reject reserved env names (DOCKER_HOST and
  the rest of the runtime list) and unknown agents at set_env_var
  time, before any document mutation. Faster failure, clearer error.
- Save-level safety net: parse the candidate file via the same
  validation load uses, BEFORE renaming over the real config. If
  validation fails, remove the temp file and propagate the error;
  the real config stays untouched.

Adds five integration tests covering the two reproduction paths plus
the save-level rollback invariant.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(console): refresh workspace list after manager save so launch routing isn't stale

ConsoleState.workspaces was a snapshot captured at console startup and
never refreshed. Manager edits (create/rename/edit/delete) rebuilt
ManagerState.workspaces but the launch dispatcher kept reading the
stale snapshot — so a renamed workspace appeared correctly in the list
but failed to resolve at launch, and edits to default_agent /
allowed_agents were ignored by launch routing until the next console
restart.

Chose Option B (derive launch choices from current AppConfig per-call)
because the call sites were tractable: only `dispatch_launch_for_workspace`
and the `LaunchWithAgent` arm in `run_console` consumed the snapshot,
and `WorkspaceChoice` is fully derivable from `(name, &AppConfig, &cwd)`.
Option B eliminates the duplication outright rather than papering over
it with refresh-after-save plumbing — there is no second source of truth
to keep in sync. `ConsoleState.workspaces: Vec<WorkspaceChoice>` and
`selected_workspace: usize` are dropped; a new
`pending_launch: Option<LoadWorkspaceInput>` pins the picker target
across the AgentPicker open→commit gap. `build_workspace_choice` is
exported from `console::state` and called per dispatch from current
config.

Adds four integration regression tests in tests/manager_flow.rs covering
create/rename/edit-default_agent/delete paths, plus two unit tests on
`build_workspace_choice` itself in src/console/state.rs (returns None for
unknown saved name; picks up default_agent from current config).

Co-authored-by: Claude <noreply@anthropic.com>

* fix(op): use op-provided `reference` field; parse 4-segment as vault/item/section/field

Two correlated correctness fixes for 1Password reference handling:

1. Use the `reference` field that `op item get --format json` returns
   instead of synthesizing `op://Vault/Item/Field` from display names.
   The synthesized form was wrong for fields living inside sections,
   for items whose names contained `/` or whitespace, and for any case
   where 1Password's authoritative serializer differs from naive
   display-name concatenation. The new path always uses the string `op`
   itself produces — no synthesis, no escaping bugs.

2. Fix `parse_op_reference` to interpret 4-segment references per the
   official syntax (https://developer.1password.com/docs/cli/secret-reference-syntax/):
   - 3 segments: `op://<vault>/<item>/<field>`
   - 4 segments: `op://<vault>/<item>/<section>/<field>`
   The previous "4 segments = account/vault/item/field" interpretation
   was a jackin-specific extension that conflicted with op CLI itself.
   Account scope stays separate state on the picker (`selected_account`
   in OpPickerState) and is not encoded in the path.

The trust model is preserved: `RawOpField` continues to omit the
`value` field, and the safety test exhaustively destructures `OpField`
to fail compilation if anyone ever adds it. The destructure now also
covers the new `reference` field.

Cross-account references where the chosen vault lives outside the
op CLI's default account remain a documented limitation for this PR
— operator must ensure the relevant account is signed in.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(op): timeout op CLI probe; async account_list in picker constructor

OpCli::probe was using plain Command::output() with no timeout — a
wedged op (network stall, biometric held open, etc.) would freeze the
caller indefinitely. Route probe through the same channel-and-thread
timeout pattern that read/vault_list/item_list/item_get use; extract
spawn_op_with_timeout so probe (no JSON parse) and run_op_json (JSON
parse) share the spawn-and-wait primitive.

The picker constructor was calling account_list synchronously, which
blocked the TUI render loop before the spinner could paint. Move the
initial probe onto a worker thread (sharing a single Arc<dyn
OpStructRunner + Send + Sync> with the picker so test stubs are
captured instead of replaced by a fresh OpCli); the picker renders
the spinner immediately, account_list completes asynchronously, and
poll_load routes 0/1/multi to the right state via a new
handle_accounts_loaded helper.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(op): truncate_stderr respects UTF-8 char boundaries

truncate_stderr was slicing a String at byte index 4096. If that byte
fell inside a multi-byte UTF-8 character (e.g., op CLI emitting an
error in a non-ASCII locale, or any future op output containing
multi-byte glyphs), the slice would panic on an error path. Round the
truncation point down to the nearest char boundary at or before MAX.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): EnvValue modal allows empty values; target-specific input validity

TextInputState::is_valid was globally rejecting empty input, which is
correct for EnvKey/Name/Workdir but wrong for EnvValue — POSIX env
semantics distinguish VAR="" (set to empty) from unset VAR, and some
workloads use empty values to clear inherited defaults.

Add an allow_empty flag on TextInputState (settable via
new_allow_empty constructor); is_valid honors it for the empty-string
case while still rejecting duplicates. Wire the EnvValue modal sites
to construct via new_allow_empty. Other text-input targets keep the
non-empty default.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(tui): SourcePicker cycle dedup + align Secrets letter modifier guards

Two small consistency tweaks called out by review:

1. SourcePickerState had byte-identical cycle_forward and cycle_backward
   methods (a 2-button toggle has only one transition). Collapse to a
   single cycle() matching ScopePickerState's pattern.

2. The Secrets-tab letter shortcuts were using two different modifier
   guards: m|M and p|P checked `(modifiers - SHIFT).is_empty()` (the
   convention codified in RULES.md § TUI Keybindings), while d|D and a|A
   checked `!modifiers.contains(CONTROL)` (accepted Alt/Cmd/Super
   silently). Align d|D and a|A with the canonical pattern so all four
   letter shortcuts behave the same way.

Both items are pure refactors — no operator-visible behavior change.

Co-authored-by: Claude <noreply@anthropic.com>

* test(op): picker async workers exercise the injected runner

Commit 55 switched the picker's runner field from Box<dyn OpStructRunner +
Send> to Arc<dyn OpStructRunner + Send + Sync> so worker threads share
the same runner instance the constructor was given. That removed the
test-vs-production gap where injected mocks were silently bypassed by
spawned threads (which were building fresh OpCli instances).

Land the regression tests that exercise this newly-reachable surface:
vault_list, item_list, and item_get all now pass through the injected
mock when called from worker threads. Future changes to the picker's
async loading paths can be caught by tests instead of only at runtime.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(reviews): mark PR #171 review findings as resolved

All findings in `pr-171-claude-detailed-review.md` and
`pr-171-codex-detailed-review.md` were addressed by commits 52–59
(workspace-list refresh, candidate-config validation, op://
reference fidelity, EnvValue empty-OK, async account_list, UTF-8
stderr truncation, async-worker test seam, SourcePicker dedup,
Secrets-tab modifier-guard alignment).

Move each finding to a "Resolved" section with the commit hash that
closed it so the markdown reflects current branch state and future
review passes don't re-flag closed concerns.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(op-picker): refresh stale comments about runner threading

Several comments in `src/console/widgets/op_picker/mod.rs` still
described the pre-commit-55 contract where worker threads built a
fresh `OpCli` regardless of the injected runner. That changed when
`OpPickerState::runner` switched from `Box<dyn OpStructRunner +
Send>` to `Arc<dyn OpStructRunner + Send + Sync>` and
`runner_clone_for_thread` became a thin `Arc::clone`. Reword:

- `OpPickerState::new` doc — "Boxes a fresh OpCli" → "Wraps a fresh
  OpCli runner in an Arc". The constructor wraps in `Arc::new`, not
  `Box::new`, and the wrapping is what enables the worker-thread
  share.
- `enter_on_account_advances_to_vault_with_account_scope` test doc —
  drop the "spawned thread runs through `runner_clone_for_thread`"
  line and the parenthetical claim that the spawned thread builds a
  fresh OpCli. Replace with an explicit pointer to the
  `vault_list_uses_injected_runner_in_async_worker` test that
  exercises the worker-thread path through the injected stub.
- "OpCache integration tests" header comment — drop the "(always
  OpCli)" phrasing and reframe the section as "tests that
  short-circuit before the worker spawn", which is what they
  actually do today.

No code or behavior change.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(tui): consolidate pending env writes through one helper

Three Secrets-tab commit paths produced a final
`(scope, key, value)` triple and inserted it into `editor.pending`:

- `OpPicker` key-row commit (already routed through a helper);
- `EnvValue` text-modal commit (open-coded match);
- `EnvKey` commit's sentinel-picker fast path (open-coded match).

The two open-coded sites repeated the agent-scope auto-create +
`secrets_expanded.insert(agent.clone())` dance with the same
`pending.env.insert(...)` / `pending.agents.entry(...).or_default()`
shape. A future change to the contract (e.g. tracking dirty rows,
emitting a save-prompt dirty bit, recording last-edited timestamps)
would have to update three places in lockstep.

Rename `apply_picker_value_to_pending` → `set_pending_env_value`
since it's no longer picker-specific, expand the docstring to spell
out all three callers and the auto-expand-section invariant, and
route the two open-coded sites through it. Net `-13 +12` LOC; no
behavior change. All 1046 nextest cases still pass.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(comments): trim verbose docs in operator_env + op_cache

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(comments): trim verbose docs in widgets

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(comments): trim verbose docs in manager state + input

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(comments): trim verbose docs in manager render

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(comments): trim verbose docs in console + config/editor

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>

* chore(comments): trim verbose docs in tests

Strip comments that restate the code, narrate commits, or rephrase
function/test names. Keep only WHY-level rationale: trust-model
invariants, external-system quirks, subtle behavior preserved
intentionally.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(operator_env): collapse repeated layer loops in validate + merge

`validate_reserved_names` repeated the same `for key in env.keys() { if
is_reserved(key) { offenses.push(...) } }` pattern four times, once per
EnvLayer variant. With the explanatory comments trimmed it was just four
copies of the same shape. Hoist into a local closure that takes the
layer label and the env map; the four call sites now read as a tiny
table of layer constructors.

`merge_layers` likewise had four byte-identical insertion loops; iterate
over the four layer references in priority order instead.

No behavior change. Test count unchanged at 1046.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(operator_env): extract build_attributed_layers helper

`resolve_operator_env_with` and `write_launch_diagnostic` each rebuilt
the same per-layer attributed map: global → agent → workspace →
workspace-agent, later wins. Two ~30-line copies of the same precedence
ladder, with comments now stripped, made the duplication obvious.

Hoist into `build_attributed_layers(config, agent, workspace)`. The
diagnostic site keeps the single extra step it needs (drop keys not in
`resolved`). The resolver site keeps the dispatch loop on top.

A regression in either copy of the ladder would have silently dropped
attribution for a layer; one source means errors and diagnostics are
guaranteed to agree on which layer supplied each key.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(op-picker): hoist Up/Down cursor cycle into a shared helper

Each of the four pane key handlers (Account/Vault/Item/Field) had a
byte-identical Up/Down arm that computed `(cur == 0 ? n-1 : cur-1)` /
`(cur+1 >= n ? 0 : cur+1)`. Eight near-identical bodies — readable in
isolation, painful to keep in sync if the wrap-around rule ever
changes.

Hoist into `OpPickerState::step_selection(&mut ListState, count, delta)`.
Each pane site now reads as one line per direction: compute filtered
count, call the helper.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(workspace): derive Default on WorkspaceConfig; collapse fixtures

`WorkspaceConfig` had every field defaultable (`String`, `Vec`,
`Option`, `BTreeMap`) but no `Default` derive, so 20+ fixtures across
console tests, two production builders, and the editor's `new_create`
spelled out all seven fields by hand. With the trimmed comments off,
the noise from the literals dominated the test bodies' actual intent
(which mounts? which agents? which env?).

Add `#[derive(Default)]`. Rewrite each fixture site to either
`WorkspaceConfig::default()` (when it really wants the empty
workspace) or struct-update syntax (`{ field: ..., ..Default::default() }`)
when it customizes one or two fields. Test bodies are now only as wide
as the field they care about.

Production sites updated to match: `EditorState::new_create` and
`WorkspaceCreate::build_workspace` use `WorkspaceConfig::default()` /
struct-update. No serialization or behavior change — the `Default`
impl produces the same all-empty value that every fixture was
spelling out.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(widgets): unify cursor-cycle helper across all list pickers

`agent_picker`, `github_picker`, `workdir_pick` each carried a
hand-rolled wrap-around cursor body inside their Up/Down arms — six
near-identical clones of the `(cur == 0 ? n-1 : cur-1)` /
`(cur+1 >= n ? 0 : cur+1)` pattern. `op_picker` had its own private
`Self::step_selection` helper covering the same four-pane case.

Hoist the helper to `console::widgets::cycle_select` (pub(crate)), drop
op_picker's private copy, and route all five pickers through it. The
`unwrap_or(0)` vs `map_or(0, …)` difference was non-load-bearing: every
caller already initializes `select(Some(0))` on a non-empty list and the
`count == 0` short-circuit covers the empty case identically.

Net: -45 LOC, single source of truth for wrap semantics, zero
behaviour change.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(workspace): finish WorkspaceConfig fixture sweep across remaining sites

Iteration 2 added `Default` to `WorkspaceConfig` and converted six TUI
test files that dominated the noise. Fifteen further sites still spelled
out the seven-field literal: production helpers (`resolve.rs`,
`planner.rs`, `mod.rs`), config CRUD (`config/mod.rs`,
`config/editor.rs`, `config/workspaces.rs`), state-machine fixtures
(`console/manager/state.rs` + four `input/*.rs` siblings),
operator-env reserved-name validators, app-context targeting, and two
integration test files (`manager_flow.rs`, `workspace_config_crud.rs`).

Each site now uses struct-update (`{ workdir: …, mounts: …,
..Default::default() }`) — keeping only the fields the test actually
cares about. No serialization or behaviour change: `Default` produces
the same all-empty values the literals were spelling out.

Co-authored-by: Claude <noreply@anthropic.com>

* test(manager_flow): collapse seed_config into seed_config_with_env

`seed_config` and `seed_config_with_env` were near-identical except for
one field — `env: BTreeMap::new()` vs `env: env_map`. Now that
`WorkspaceConfig::default()` lands the empty case, `seed_config`
delegates: `seed_config_with_env(paths, temp_dir, vec![])`.

Removes one duplicated workspace-skeleton fixture; both tests-paths
exercise the same code instead of two near-identical copies that could
drift.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(reviews): drop fully-resolved Claude + Codex reviews

All findings from both reviews have been addressed across commits 52–59
and subsequent polish iterations. The full audit trail (file contents,
finding-to-commit mapping, every fix discussion) lives in the git log
itself; the standalone markdown files no longer carry information that
isn't already searchable in commit messages and PR history.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor: replace production unwraps with structural matches

Four `.unwrap()` sites in non-test code each guarded an invariant
already established by the immediately preceding `match` on a
collection's length, but expressed it as `into_iter().next().unwrap()`
or `as_ref().unwrap()`. Rewriting each as a slice-pattern match,
`Option::map_or`, or `swap_remove(0)` carries the invariant in the
type rather than in the prose, so a future refactor that drops the
length guard becomes a compile error rather than a runtime panic.

No behavior change.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(tests): finish WorkspaceConfig fixture sweep in manager_flow

Three remaining `WorkspaceConfig { … last_agent: None, env: …::new(),
agents: …::new() }` literals in manager_flow's helpers and tests still
spelled out the all-default tail by hand. The previous sweep landed
the pattern across the repo but skipped these three because each had
a non-default field interleaved (`allowed_agents`, `default_agent`)
that hid the trailing `..Default::default()` opportunity from a
literal grep. Hoist the trailer everywhere; the fixture intent
becomes "set these specific fields, defaults for the rest" rather
than mechanical zero-elision.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(manager): make manager-private submodules non-pub

`agent_allow`, `create`, and `github_mounts` are referenced only from
inside `console/manager/`. Drop their `pub` to express that boundary
in types — the public surface of the manager module remains `input`,
`render`, `state`, and `mount_info` (the last is reached from the
file-browser git prompt).

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(render): drop dead op_accounts chain + merge is_op double-scan

- Remove `op_accounts: &[OpAccount]` from `render_secrets_key_line`,
  `render_secrets_tab`, and `render_editor`; the param was immediately
  discarded with `let _ = op_accounts` and carried no information.
- Replace the two-scan pattern (`is_op_reference` then `parse_op_reference`
  on the same string) with a single `parse_op_reference` call; use
  `.is_some()` to derive the `[op]` marker. Eliminates one `starts_with`
  scan per key row per frame.
- Update the two `render_secrets_tab` call sites in tests and the single
  `render_editor` call site in `render/mod.rs`.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(nav): eliminate double secrets_flat_rows allocation on cursor move

`step_secrets_cursor_down` and `step_secrets_cursor_up` each called
`secrets_flat_rows` internally, duplicating the allocation in the same
keystroke that also called `secrets_flat_row_count` (another clone of the
same Vec). On every Down keypress in the Secrets tab, two full Vec<SecretsRow>
were allocated and dropped.

Fix: compute `rows` once at the call site, pass `&[SecretsRow]` into the
step helpers. Both helpers lose their `editor`/`config` parameters and the
internal `secrets_flat_rows` call. Remove `secrets_flat_row_count` entirely
(it was just `.len()` of the Vec) and drop it from the import and the now-
unreachable `Secrets` arm of `max_row_for_tab`.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(op-picker): borrow account id for cache writes instead of cloning

Add `selected_account_id_ref() -> Option<&str>` alongside the owned
`selected_account_id() -> Option<String>`. Use the reference form in the
three `poll_load` arms that write to the op cache (vaults, items, fields) —
they only need `&str` for the cache key, not an owned `String`. The owned
form is still used by `start_*_load` callers that move the value into a
spawned thread.

Co-authored-by: Claude <noreply@anthropic.com>

* style: rustfmt pass after render/editor.rs refactor

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): correct misleading footer hints + document op_picker invariant

Footer:
- Remove `M mask/unmask` from AgentHeader and add-sentinel footer arms;
  `toggle_focused_row_mask` returns early for both row kinds (`_ => return`),
  so advertising the key was a silent lie.
- Remove `M mask/unmask` from the SectionSpacer/None fallback arm (cursor
  never lands there). Degrade to empty hint.
- Suppress "Enter pick working directory" hint on General tab row 1 when
  `pending.mounts.is_empty()`; `open_editor_field_modal` silently does
  nothing when there are no mounts to pick from.
- Remove dead `enumerate()` + `let _ = i;` in `footer_spans`.

OpPicker:
- Document the `self.accounts.len() > 1` multi-account invariant in
  `handle_accounts_loaded` and the `handle_vault_key` Esc guard; the single-
  account fast-path intentionally leaves `self.accounts` empty, making
  the len check the canonical multi-account signal.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(render): thread live config into current-dir details pane

`render_current_dir_details_pane` was constructing `AppConfig::default()`
for both the agents-block height calculation and the `render_agents_subpanel`
call, so the Agents sub-panel on the "Current directory" row always rendered
zero agents regardless of what was actually configured.

Fix: add `config: &AppConfig` parameter and thread it in from the call site
in `render_list_body` (which already had `config`).

Also: document the `TempDir` drop requirement in `seed_config_with_env` so
a future test can't accidentally drop the guard before assertions complete.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tui): cap startup op probe at 3s; gate P key + hint on op_available

Startup probe:
- Add `OpCli::new_probe()` with a 3-second timeout. `OpCli::new()` retains
  the 30-second `OP_DEFAULT_TIMEOUT` for all operational calls (vault-list,
  item-get, etc.) where blocking is expected. The startup probe only needs
  to know whether `op` is installed and responsive; a false negative here is
  acceptable — the picker renders its own error state on first open.
- Use `new_probe()` in `ConsoleState::new` so a network-stalled or
  biometric-blocked `op` binary cannot freeze `jackin console` startup for
  up to 30 seconds before the terminal is painted.

P key guard:
- Add `&& op_available` to the `P` arm in `handle_editor_key`; when `op`
  is absent the key silently does nothing rather than opening a picker that
  immediately shows a fatal error panel.
- Thread `op_available: bool` through `render_editor` → `contextual_row_items`
  and conditionally omit the `P · 1Password` footer items on key rows and
  add-sentinel rows when `op` is unavailable.
- Set `state.op_available = true` in the five `op_picker_*` integration tests
  that exercise the `P` key path directly.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(confirm-save): off-by-one in required_height clips last diff line

The layout comment correctly counted 7 chrome rows (2 borders + top blank +
after-content blank + buttons + after-buttons blank + hint), but the
implementation returned N + 6. The modal was sized 1 row too short,
clipping the last changed-field line in every ConfirmSave dialog.

Fix: return N + 7, matching the documented layout. Update the test
assertion from 9 to 10 for a 3-line state.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): select just-saved workspace on return to list

After saving a workspace via the Esc → Save path (or after creating a new
one), the list cursor now lands on the workspace that was just written rather
than defaulting to the CWD row or the first entry.

The final on-disk name (`current_name`) is already correct in all cases —
the rename path updates it before the edit_workspace call — so it can be
looked up in the freshly-rebuilt ManagerState and used to set `selected`.

Adds a regression test with two workspaces to verify the cursor targets the
right entry, not just "index 1" by accident.

Co-authored-by: Claude <noreply@anthropic.com>

* refactor(confirm-save): move Create-mode env section into its match arm

The env-vars block for EditorMode::Create was appended after the match via
a second `if matches!(...Create)` guard, while the Edit arm correctly handled
its env section inline. This split made the Create arm appear complete when
it wasn't, risking duplication if someone added env logic inside the arm
without seeing the post-match block.

Move the env section into the Create arm directly, after the default-agent
block. Behaviour is identical; the structure now matches the Edit arm.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(op-picker): clear selected_item on Field→Item Esc back-navigation

`handle_item_key` Esc (Item → Vault) clears `selected_item = None`.
`handle_field_key` Esc (Field → Item) did not, leaving a stale item
reference on `self` while the operator was back on the Item pane.

The stale reference caused the breadcrumb to show the previous item name
during the first render frame after back-navigation. Add the clear to
match the established Esc-back pattern.

Co-authored-by: Claude <noreply@anthropic.com>

* feat(tui): s always exits to workspace list after successful save

Previously `s` used exit_on_success = false — it saved but kept the
operator inside the editor. The Esc → Save path already used
exit_on_success = true (returning to the list). This made `s` the odd
one out: the operator had to press Esc a second time to get back.

Change `s` to also use exit_on_success = true. Both save paths now
return to the workspace list after a successful write, with the cursor
positioned on the just-saved workspace (behaviour from an earlier commit).

Update two tests that were asserting the old "stays in editor" behaviour.

Co-authored-by: Claude <noreply@anthropic.com>

* fix(workdir-pick): scroll list so selected row stays visible

The picker rendered all choices as a Paragraph, which clips without
scrolling. On a deep directory tree (e.g. /home/user/Projects/repo
producing 4-6 ancestor choices) the highlighted row could disappear
off the bottom of the modal with no way to see it.

Apply an offset so the selected row is always within the visible
viewport: offset = selected.saturating_sub(visible - 1). Chain skip()
+ take() directly into the line-building iterator to avoid an
intermediate Vec allocation (clippy: avoid collect when not needed).

Co-authored-by: Claude <noreply@anthropic.com>

* refactor: drop unused _config from secrets_flat_rows and callers

`secrets_flat_rows` took a `_config: &AppConfig` parameter it never used —
the function derives the row model entirely from `editor.pending` and
`editor.secrets_expanded`. Removing it cascades cleanly through six helper
functions that passed `config` solely to forward it here:

- `toggle_focused_row_mask`
- `open_secrets_enter_modal`
- `open_secrets_delete_confirm`
- `open_secrets_add_modal`
- `open_secrets_picker_modal`
- `contextual_row_items`

Also:
- `scope_label` now returns `&str` (avoids two pointless allocations;
  clippy promotes it to `const fn`).
- Test `secrets_flat_rows_sequence_is_canonical` pins the exact row
  order for a workspace with env keys, an expanded agent, and a collapsed
  agent — cursor arithmetic in the input layer is derived directly from
  this sequence.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 7, 2026
* docs(roadmap): iteration 1 — readability & modernization analysis

Full first draft of READABILITY_AND_MODERNIZATION.md covering §0–§10:
project inventory (72 files, 40k LOC), concept-to-location index (25
entries), documentation hierarchy proposal, source-code structural
diagnosis (single-crate recommendation, 7 module-shape rules, hot-spot
list), naming candidates, CI/tooling analysis, modernization candidates
(13 entries with alternatives comparisons), AI-agent workflow (§8.1–§8.3:
spec-driven development, superpowers alternatives, docs boundary), risks,
and execution sequencing.

All claims grounded in direct code reading (cited file paths and line
ranges). All crate/tool recommendations include 2–3 alternatives. PR #171
branch read for AgentPicker, OpStructRunner, RawOpField, and RULES.md
additions.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 2 — launch.rs deep analysis, TS strictness, preview workflow

Four concrete improvements over iteration 1:

- §4: Deep-read src/runtime/launch.rs (2368L) in full; mapped every
  function to exact line ranges and dependency graph; refined split
  proposal into 4 files (launch.rs ~120L, launch_pipeline.rs ~560L
  prod + 1200L tests, terminfo.rs ~110L, trust.rs ~60L); key finding:
  test module (1282L) exceeds production code (1083L).

- §7.11: Discovered docs/AGENTS.md documents both TypeScript strictness
  blockers explicitly (rainEngine indexed access + astro-og-canvas
  optional-property types); verified rainEngine.ts at 5 line locations;
  rewrote recommendation as a concrete 4-step fix plan.

- §6: Read preview.yml in full — Homebrew tap rolling-preview pipeline;
  flagged missing contributor documentation; resolved OQ3.

- §2 concepts 4 & 6: Exact PR #171 data — TICK_MS=50 at console/mod.rs:90
  with 20Hz rationale; compile-time safety test is an exhaustive struct
  destructure (not trybuild) at operator_env.rs:2055–2096.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 3 — operator_env.rs deep analysis, snapshot tests, OQ7

Three concrete improvements over iteration 2:

- §4: Deep-read src/operator_env.rs (1569L); mapped every function to
  exact line ranges; identified two distinct clusters (op CLI subprocess
  layer lines 96-364 vs env layer resolution lines 365-808) with ~90L
  connective tissue; proposed module-directory split into mod.rs (~100L),
  client.rs (~280L), layers.rs (~470L), picker.rs (~250L PR#171);
  dependency graph shows no circular imports.

- §7.5: Named first 3 concrete snapshot test targets with file:line
  citations — render_sentinel_description_pane (list.rs:306, zero state,
  ~10L), render_tab_strip (editor.rs:180, 4 enum variants, ~20L),
  render_mounts_subpanel (list.rs:408, 3 data-driven cases, ~30L).

- OQ7 resolved: docs/package.json confirms astro-og-canvas ^0.11.1;
  docs/src/pages/og/[...slug].png.ts:~35 has `logo: undefined` which
  is the exact exactOptionalPropertyTypes conflict; fix is one line
  (omit the property). §7.11 and §9 updated accordingly.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 4 — config/editor.rs deep analysis, exact rustdoc coverage

Two concrete improvements:

- §4: Deep-read config/editor.rs (1467L); key finding: production code is
  only 503L (lines 1-503), tests are 963L (lines 504-1467 — nearly 2x);
  mapped all 18 public methods with exact line ranges into 5 domain groups;
  flagged create_workspace/edit_workspace validation-first architectural
  pattern as a refactor-preserve invariant; proposed 6-file module-directory
  split; priority note: lower than launch.rs/operator_env.rs since production
  code is already a reasonable size.

- §1/§4/§7.6: Corrected rustdoc coverage from estimate "~28%" to exact
  count 37/90 files = 41%; identified cluster: console/manager/ and
  console/widgets/ (PR #171 additions) are well-covered; runtime/, app/,
  cli/ lag; updated all three occurrences consistently.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 5 — concrete brainstorm template, §10 step 4 ordering, §5 pruning

Three concrete improvements:

- §8.2: Added 17-line concrete docs/internal/agent-skills/brainstorm.md
  template with 6 fields (Purpose/When/Steps/Outputs/Done when/Overlap
  guard). The Done-when and Overlap-guard fields are the discipline gates
  missing from superpowers default brainstorming. Makes §8.2 immediately
  actionable instead of aspirational.

- §10 step 4: Refined split ordering from sketch to priority-grounded
  sequence: config/types (4a) → manifest (4b) → config/editor 503L (4c)
  → operator_env 810L (4d) → app/dispatch (4e) → runtime/launch 1083L
  (4f last). Each sub-step has architectural notes and what-could-go-wrong.
  Flags the operator_env circular-dependency check for layers.rs→client.rs.

- §5: Replaced 3 "leave as is" non-candidates (rows 10, 12, 15) with
  verified candidates from code reading: provision_claude_auth→apply_auth_forward
  (auth.rs:17), AuthProvisionOutcome→AuthForwardOutcome (instance/mod.rs),
  spawn_wait_thread→spawn_exit_watcher (operator_env.rs:202).

- OQ5 resolved: instance/auth.rs read — only 210L production code,
  585L tests (3x); no split needed; removed from hot-spot concern.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 6 — hot-spot splits, §8 cc-sdd pivot, op_cache, logging

Four improvements:

- §1: Hot-spot table rebuilt with production/test LOC split columns for
  all 22 files. Key finding: manifest/validate.rs is 145L prod/816L tests
  (exemplary testing, not a god file); app/mod.rs is 928L prod/22L tests
  (most genuine god file after launch.rs). Adds Priority column.

- §8: Operator prefers existing tools over hand-rolled skill files.
  §8.1 recommendation pivots from hand-rolled (Option C) to cc-sdd
  (Option B). §8.2 rewrites comparison table — cc-sdd covers
  spec/plan/execute; TDD/debugging/review covered by existing project
  docs (TESTING.md, open-review-findings.mdx). Removes custom brainstorm
  template from iteration 5; no docs/internal/agent-skills/ dir needed.

- §2 concept 14: Session-scoped op cache confirmed at
  src/console/op_cache.rs (252L, all production, no tests). Full detail:
  BTreeMap keyed by (account, vault_id, item_id) tuples, DEFAULT_ACCOUNT_KEY
  sentinel, invalidation methods, //! doc confirms metadata-only guarantee.

- §7.14: New modernization entry — structured logging (log vs tracing
  vs current eprintln! approach). Recommendation: defer. Research in
  _research_notes.md.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 7 — MDX specs, §9 dep graph fix, §7.13 Renovate full entry

Five concrete improvements:

- §8.1/§8.3/§3: Operator direction — specs become public Starlight MDX
  pages at docs/src/content/docs/specs/ (draft: true while in-progress),
  not in docs/internal/specs/. Living source of truth updated with each
  code change. §8.3 boundary contract rewritten for public-spec model.
  §3 target shape updated to remove internal specs/ and add public specs/.

- §9 R1: Corrected from "circular import risk" to "compilation-at-distance
  risk". Verified by grep: config imports workspace (config/mod.rs:1,5,6)
  but workspace does NOT import config. One-way dep: config → workspace.
  No circular risk; real risk is 30+ use path updates when AppConfig moves.

- §10 step 2: Rewritten to match cc-sdd + Starlight MDX approach (was
  still describing hand-rolled docs/internal/agent-skills/ from pre-
  iteration-6 recommendation). Added draft page / lychee link-check caveat.

- §7.13 Renovate: Full six-subheading entry replacing 2-sentence stub.
  Evaluated Dependabot and Renovate Cloud App; both rejected due to DCO
  sign-off constraint (RENOVATE_GIT_AUTHOR env var in renovate.yml:26
  is not replicable in either alternative). Two low-cost tunings named.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 8 — lychee draft-page constraint, concept 18 correction

Three improvements:

- §8.1: Draft-page lychee constraint verified from docs/lychee.toml (full
  read). exclude_path has only 404.html pattern — no draft exclusion.
  Starlight draft pages exist in dist/ and are scanned by PR-time lychee
  check. Two fix options added: keep specs link-free, or add exclude
  pattern to lychee.toml. Added Astro sidebar requirement: manual config
  at astro.config.ts:50-103; autogenerate{ directory: 'specs' } pattern.

- §2 concept 18: Corrected a wrong proposed move. AuthForwardMode →
  instance/auth.rs was incorrect — the type is used in 9 files, is a
  config field at config/mod.rs:89,96, and has serde Deserialize at
  line 74. Moving to instance/ would create circular dep. Corrected to:
  correctly placed, will move intra-module to config/types.rs in §10 4a.

- §4 AuthForwardMode false alarm closed: confirmed NOT a §4 Rule 3
  violation. Type is correctly in config because it IS a config value.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 9 — eprintln count, CODE_TOUR content, step 4a serde note

- §7.14: grep-verified 96 production eprintln! calls across 16 files;
  breakdown by file; no rogue debug calls found, flip condition has not triggered
- §2 concept 8: full 4-hop load chain with exact line numbers; documents
  the /__/ separator invariant in runtime_slug that prevents namespace
  collisions; CODE_TOUR column now specifies what the tour entry must explain
- §10 step 4a: confirms AuthForwardMode serde impl is a plain impl<'de>
  block (not a derive), moves with the type to config/types.rs automatically

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 10 — dispatch_value scope, Renovate automerge, //! exemplar

- §5 row 6: dispatch_value → resolve_env_value rename is 1 prod + 6 test
  call sites in a single file — lowest-cost rename in the table
- §7.13: adds minimal safe automerge pattern for lockFileMaintenance only;
  explicitly excludes patch/minor bumps and SHA-pinned Actions
- §4 Rule 7: expands //! exemplar analysis with direct quotes from
  env_model.rs:1-17 and isolates the three-element pattern

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): strip iteration annotations; reorder log chronologically

READABILITY_AND_MODERNIZATION.md now presents a clean final view:
- Remove all "(iteration N)", "(verified iteration N)", "read in full for
  iteration N", "updated iteration N" qualifiers throughout the body
- Remove the Iteration counter from §0 meta
- Resolved OQ markers simplified from "(resolved in iteration N)" to
  "(resolved)"; deferred OQ markers from "Tracked for iteration N" to
  "Deferred"

_iteration_log.md now has entries in chronological order 1–10:
- Was: 1, 3, 4, 5, 6, 7, 10, 8, 9, 2
- Now: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): research OpenSpec and IIKit; update §8 landscape

_research_notes.md:
- OpenSpec (Fission-AI/OpenSpec): brownfield-first SDD, three-phase
  propose/apply/archive, delta markers (ADDED/MODIFIED/REMOVED), living
  specs at openspec/specs/; /loop-compatible; recommended as cc-sdd
  complement for §4 module-split proposals
- IIKit (intent-integrity-chain/kit): cryptographic .feature hash-locking,
  8-phase workflow, Gherkin BDD chain, Tessl-only install; not recommended
  for jackin (no Gherkin step runner, Tessl lock-in, heavyweight for
  single-maintainer)

READABILITY_AND_MODERNIZATION.md §8.1:
- Adds Options E (OpenSpec) and F (IIKit) to the landscape and evaluation
  table with brownfield delta tracking and Rust/nextest compatibility rows
- Adds OpenSpec complement note after the cc-sdd recommendation

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 11 — dtolnay correction, ci.yml detail, snapshot targets

- §7.7 / §2 concept 25 / §10 step 3: correct false claim that
  dtolnay/rust-toolchain reads rust-toolchain.toml; action version is
  encoded in SHA rev; three sources require manual sync
- §6 ci.yml row: expand from high-level to exact job steps; document
  gaps (no MSRV job, floating tags in build-validator, no doc job,
  main jackin binary never compiled in CI)
- §7.9: grep-confirm all three snapshot target fn names; add exact
  signatures and private-fn access pattern (use super::*) with
  list.rs:720 as existing proof

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 12 — ClassSelector scope, lint table, app split

- §5 row 5: ClassSelector → AgentClass rename is 138 prod call sites
  across 17 files — highest-scope rename in the table
- §7.8: full lint table enumeration from Cargo.toml:47-75; notes that
  cast allowances are global despite TUI-scoped inline comment
- §4 4e: app/mod.rs run() read in full; refined split from 1 file to 3
  (dispatch.rs ~167L, workspace_cmd.rs ~438L, config_cmd.rs ~220L)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): remove resolved/no-action items from roadmap

Keep only forward-looking actionable proposals:

§2 concept table: removed 6 rows with "Stable"/"No change needed"
proposals (RULES.md keybindings, hardline, mount planning, XDG paths,
Docker builder, env-var ordering); renumbered 1-19

§5 naming table: removed hardline_agent row (leave-as-is, no action);
rephrased load_agent row to focus on the //! doc action not the rename;
renumbered to 1-14

§9 open questions: removed OQ3 (preview.yml, resolved — in §6),
OQ5 (auth.rs split, resolved — no split needed), OQ7 (astro-og-canvas,
resolved — fix in §7.11); renumbered to OQ1-4; merged TS strictness OQ
back into the active list

Cleanup: removed "resolved OQ5" tag from auth.rs hot-spot entry;
removed "(resolved OQ7)" qualifier from §7.11 blocker label

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): update analysis baseline — PR #171 now merged to main

Remove all "after PR #171 merges", "PR #171 branch", "pre-merge", and
"not yet on main" qualifiers throughout. Specific changes:
- §0: update baseline note from "treated as merged" to "now merged"
- §1: remove branch-only qualifiers from file tree, module map, coverage count
- §2: concept rows 1, 2, 4, 5, 6, 10, 11, 12, 14, 15 updated to reflect
  code now on main; row 12 op_cache current-state rating updated from
  requires-tribal-knowledge (pre-merge) to requires-grep (post-merge)
- OQ1 (op_picker cache): updated from "deferred" to "read from main now"
- §4, §5, §7: remove "(PR #171)" and "adds/branch" language
- §10 step 1: remove "once PR #171 merges" condition

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
donbeave added a commit that referenced this pull request May 7, 2026
…183)

* docs(roadmap): iteration 13 — AI code verifiability framing, config/types.rs full spec

Primary goal shift: codebase must be verifiable for AI-generated code.

- §0: replace generic description with explicit verifiability rationale
  (module contracts, localised concerns, types/behaviour separation)
- §4 intro: add "Why structure matters for AI-generated code" section with
  audit-units table mapping each post-split file to one reviewable question
- §4 4a: expand config/types.rs from description to full execution spec —
  exact type list, post-split mod.rs content, zero-change submodule
  guarantee (verified: agents.rs/persist.rs/workspaces.rs use super::T
  which resolves through mod.rs re-exports unchanged), impl-extension
  pattern already in use documented

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 14 — editor method map, app helpers, //! queue

- §4 4c: config/editor.rs split is now execution-ready — complete
  6-file method-to-file table with private helper placement verified
  (validate_candidate→io.rs, table_path_mut→mod.rs pub(super),
  auth_forward_str→agent_ops.rs, create_workspace delegates to AppConfig)
- §4 4e: app/mod.rs split complete — all private helpers mapped
  (parse_auth_forward_mode_from_cli→config_cmd.rs,
  workspace_env_scope→workspace_cmd.rs, print_env_table note,
  remove_data_dir_if_exists→dispatch.rs)
- §10 step 5: add //! priority queue — 10 files with draft content,
  prioritised by cold-landing impact and AI audit risk; selector.rs
  and instance/mod.rs explicitly document the /→__ invariant

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 15 — dep graph fix, trust.rs safety, OQ1 closed

- §4 4d: correct operator_env dependency graph — layers.rs imports
  both mod.rs (OpRunner) AND client.rs (OpCli for non-injectable
  resolve_operator_env wrapper at line 797); still a valid DAG
- §4 4f: verify trust.rs split safety — FnOnce injection pattern means
  launch_pipeline.rs has zero dependency on trust.rs; import chain
  documented; trust bypass audit now requires reading only ~60L
- §9 OQ1 closed: op_cache.rs read in full — 4-level structure,
  per-level invalidation, no TTL/expiry (expiry handled at OpCli
  subprocess level), DEFAULT_ACCOUNT_KEY sentinel documented

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 16 — CommandRunner Rule 3, render/editor split, 4a/4c independence

- Fix duplicate Rule 3 section introduced by previous edit; add docker.rs
  co-location note as third edge case (three edge cases, not two)
- Add render/editor.rs as new Rule 5 violator: 1666L post-PR #171
  (was listed as 782L); propose 6-file tab-by-tab split with auditability
  note on the security-adjacent Secrets tab
- Add §10 execution-order note: 4a and 4c are independent — editor.rs
  imports AppConfig via crate::config re-exports regardless of 4a order
- Append iteration 16 log entry with confidence table and weakest sections

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 17 — instance/auth.rs audit, state.rs split, line count corrections

- Add instance/auth.rs to //! priority queue at #4: four security
  invariants (0o600 perms, symlink rejection, TOCTOU-safe writes,
  macOS Keychain) documented in draft //! content
- Add state.rs as new Rule 5 violator: 992L/628L production; 26+ types
  mixed with impl blocks; propose 5-file types/behavior split
- Correct stale line counts: render/list.rs 1122→1989 (PR #171 added
  render_environments_subpanel); state.rs 865→992; priorities upgraded
- Fix §7.9 snapshot function line refs: sentinel_description_pane 306→332,
  mounts_subpanel 408→433, render_tab_strip 180→269, test ref 720→944
- Renumber //! priority queue to 11 entries (was 10)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 18 — agent_allow OQ2 closed, render/list.rs split proposal

- Close OQ2: agent_allow.rs read in full — 55L, correct //! doc,
  design sound; serves as model for //! priority queue pattern
- Add render/list.rs as new Rule 5 violator: 668L production (PR #171
  added render_environments_subpanel); propose 3-file split (mod.rs,
  details.rs, subpanels.rs); note import-path change for agents_block_agent_count
- Update §1 module map: agent_allow.rs entry corrected with size/API

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 19 — input/editor.rs critical correction, split proposal

- Correct input/editor.rs: 2349L total (was 1304L), 1141L production
  (was 547L) — PR #171 added Secrets-tab handlers; pub(super) fn
  handle_editor_modal at line 618 was invisible to previous grep pattern;
  now the largest production file in the codebase; priority → Critical
- Correct input/save.rs: 1472L total, 661L production (was 567L)
- Add 5-file split proposal for input/editor.rs: mod.rs (two dispatchers),
  secrets.rs (~500L AI-generated Secrets-tab), agents.rs, mounts.rs, general.rs
- Update key insight paragraph naming input/editor.rs as largest production file

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 20 — console splits in §10, MSRV evidence, animation.rs verdict

- Add console/manager/ as §10 Step 4f group with 5 sub-steps in priority
  order; rename existing 4f (launch.rs) → 4g; add circular-import risk note
  for ManagerStage/EditorState split sequencing
- Analyze tui/animation.rs: 582L all-production, no split needed (banner_grid
  is a tightly-coupled rendering loop); section comments compensate for missing //!
- Partially close OQ3: u64::is_multiple_of (stabilized 1.86) found in animation.rs;
  within declared MSRV 1.94; full cargo +1.94.0 check deferred (toolchain unavailable)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 21 — input/save.rs split, //! queue fix, save.rs corrections

- Add input/save.rs split proposal: 4 pub(super) fns discovered; 3-file
  split (mod.rs + flow.rs + preview.rs); no cross-dependency between
  flow and preview groups; §10 4f-v updated from Optional to concrete plan
- Fix //! queue preamble: "first 10 files" → "first 11 files"
- Correct save.rs module map (1418→1472L, correct key exports) and
  hot-spot table note (begin_editor_save ~280L → ~118L; commit_editor_save
  is the Phase 2 partner at ~149L)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 22 — input/list.rs and mount_info.rs analysis

- Analyze input/list.rs: 214L production (tests at 215); has //! doc;
  two focused pub(super) fns; no split needed; Low priority; correct module map
- Add mount_info.rs to hot-spot table: 277L production; Low priority;
  has //! doc; correct module map with 3 public enums + inspect fn
- Fix stale §2 diagnosis note: docs/internal/roadmap/ now exists

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 23 — audit units table +5 console rows, input/mod.rs corrected

- Expand audit units table from 8 to 13 entries: add state/types.rs,
  state/editor.rs, input/editor/secrets.rs, render/list/subpanels.rs,
  input/save/preview.rs — all targeting PR #171 AI-generated console code
- Add PR #171 context note linking 5 new entries to AI-generated code concern
- Correct input/mod.rs module map: 369L, add InputOutcome enum to exports
- Verify rust-toolchain.toml absence; §7.7 and §2 concept 25 already correct

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 24 — render/mod.rs analysis, //! exemplars table, EditorTab confirmed

- Add §4 Rule 7 positive exemplars table: 7 files with //! docs graded
  1-element (render/mod.rs), 2-element (input/save.rs etc), 3-element
  (env_model.rs, agent_allow.rs); PR #171 docs-discipline pattern noted
- Correct render/mod.rs module map: 421L; FooterItem + palette constants
  + render_header + centered_rect_fixed added to key exports
- Confirm EditorTab variants: General, Mounts, Agents, Secrets (Rust enum)
  vs "Secrets / Environments" (UI label); /stub qualifier already removed

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 25 — too_many_lines recount, FooterItem PR, MountConfig caveat

- Correct too_many_lines count: 13 across 8 → 16 across 11 files
  (PR #171 added 5 suppressions in console/manager); add full breakdown
  table; update all 3 occurrences in roadmap
- Fix FooterItem PR reference: #165#166 (confirmed by git log --follow)
- Add MountConfig → MountSpec rename caveat to §7.5 snapshot test description

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 26 — console/mod.rs and op_picker/render.rs analyzed

- Add console/mod.rs to hot-spot table: 406L/307L production (Low);
  correct module map from ~200 → 406L; note missing //! doc with
  ConsoleStage design block comment worth promoting
- Add op_picker/render.rs to hot-spot table: 865L/545L production
  (Medium); PR #171 AI-generated; 14 functions in two logical groups
  (entry/helpers vs level renderers); split into levels.rs proposed
- Correct 3 stale ~200L estimates for console/mod.rs across roadmap

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 27 — op_picker/mod.rs discovery, render split, operator_env correction

- Add op_picker/mod.rs to hot-spot table: 1712L/775L production (High);
  PR #171 AI-generated; OpPickerState types+behavior split opportunity;
  has 7-line //! doc; module map split into two rows (mod.rs + render.rs)
- Add op_picker/render.rs 2-file split proposal: render.rs (coordinator)
  + render_pane.rs (pane/level renderers); no cross-dependency confirmed
- Correct operator_env.rs total: 1569→2130L (880L production); update
  4 occurrences across hot-spot table, ASCII tree, §4 analysis

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 28 — op_picker/mod.rs 3-file split, count corrections

- Add op_picker/mod.rs formal 3-file split: loading.rs (async load family
  ~120L) + keys.rs (4 level key handlers ~315L) + mod.rs (types/constructors)
- Correct "24 files" → "28+" for 500L threshold count
- Update total LOC: ~40,664 → ~43,587 (2 occurrences, with provenance note)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 29 — op_picker execution order + file_browser analysis

- §10 Step 4f: expand from 5 to 7 sub-steps; add 4f-vi (op_picker/mod.rs
  → mod.rs + loading.rs + keys.rs) and 4f-vii (op_picker/render.rs →
  render.rs + pane.rs); document impl-extension and import-path caveats
- §4 //! exemplars: add file_browser/ subsystem analysis — all 5 files
  have //! docs, no file exceeds ~350L production; classified as exemplar
  (not a split candidate); document git_prompt.rs coupling-density
  justification and input.rs as 28-file false positive (144L production)
- §1 module map: expand single file_browser/ row to 5 individual rows
  with production LOC and dominant concern per file

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 30 — challenge split-first thesis, fresh LOC corrections

- §4: Add "Alternative thesis: documentation-first verification" — challenges
  the two core assumptions behind file splitting (files-as-audit-unit and
  file-size-as-context-constraint); adds 7-criterion comparison table vs
  structure-first approach; introduces phased combined recommendation:
  Phase 1 = doc sprint (//! contracts + specs/ for 3 subsystems, 2-3 PRs,
  zero structural change); Phase 2 = splits only for >600L production files
  (reduces scope from 14+ to 4 files); Phase 3 = workspace if LOC > 150K
- Fix stale LOC: app/mod.rs 951→979, config/editor.rs 1467→1548 (7 and
  8 locations respectively; verified by fresh find|xargs wc -l scan)
- §1 module map: add agent_picker.rs (436L), scope_picker.rs (201L),
  source_picker.rs (244L) — all PR #171 additions with //! docs

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 31 — fix 600L→800L threshold error, correct LOC

- §4 alternative thesis: correct ">600L production → 4 files" claim
  introduced in iteration 30; re-verified all 9 candidate files via
  #[cfg(test)] line position; threshold must be >800L to get exactly 4
  files (9 exceed 600L); add verification table with test-start lines
- Production LOC corrections (5+ locations each):
  launch.rs 1085→~1077, operator_env.rs 810→~880,
  app/mod.rs 928→~957, config/editor.rs 503→~584
- §2 OpPicker row: replace vague "no entry yet" with confirmed gap:
  PROJECT_STRUCTURE.md line 53 still lists pre-PR#171 widget set (10
  named); omits op_picker/, agent_picker.rs, scope_picker.rs,
  source_picker.rs and pre-dates the manager/ sub-structure split

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 32 — two-tier spec arch, behavioral spec template

- §8.1: Add two-tier spec architecture table distinguishing feature specs
  (public Starlight MDX, user-facing) from behavioral specs (internal
  docs/internal/specs/, for AI code verification) — resolves contradiction
  between §4 (which said docs/internal/specs/) and §8.1 (which said
  "no longer needed; specs are public")
- §8.1: Add concrete behavioral spec template for op_picker/ with state
  machine table and 3 INV invariant entries each with a grep-executable
  "Verify by:" command; template directly usable for the 3 Phase 1 specs
- §8.1: Remove erroneous "docs/internal/specs/ no longer needed" claim
- Confirmed render/editor.rs ~736L and render/list.rs ~668L production
  (no interspersed production code — all test blocks follow consecutively)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 33 — executive summary, §0 correctness

- §0: Add executive summary (~300 words) with core problem, 3-phase
  recommendation, key counter-argument, and navigation table pointing
  to §2/§4/§7/§8/§10 by question — resolves the meta-irony of a
  readability roadmap with no entry-point orientation
- §0 item 2: "1569-line monolith" → "2130-line monolith" (operator_env.rs
  current verified size; stale reference was in the first section readers see)
- §0 item 3: Add "(selective)" qualifier and explicit note that standard
  Rust co-locates struct+impl — impl-extension pattern is justified only
  for files >800L production, not as a universal rule

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 34 — spec priority reorder, §10 Phase 1 track

- §0 + §4 Phase 1: Prioritize runtime/launch.rs behavioral spec (no //!
  doc, ~1077L production, critical path — all jackin load failures trace
  here); drop config/editor.rs from Phase 1 (its 963L test suite already
  serves as behavioral spec — tests are behavioral examples); reduce Phase
  1 from 3 specs to 2 specs; add reasoning for the priority ordering
- §10 Step 2: Split into two parallel tracks — Track A (cc-sdd tooling
  setup) + Track B (Phase 1 behavioral spec authoring); Track B includes
  specific INV invariants to capture for runtime/launch.rs grounded in
  reading the actual function structure (step comment positions); adds
  sequencing rationale: spec must precede structural splits

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 35 — verified INV entries for runtime/launch.rs

Read load_agent_with lines 553-892 in full. Replaced 3 draft INVs from
iteration 34 (inferred from step comment positions) with 5 verified INVs
citing exact line numbers:
- INV-1: trust gate (line 594) precedes image build (line 736)
- INV-2: container name claimed (line 754) between image build and network
- INV-3: token verified (line 763) before network creation (line 827)
- INV-4: render_exit called at lines 886 AND 890 (all exit paths)
- INV-5: cleanup disarm semantics — Running→disarm, clean exit→cleanup,
  crash→disarm (explains jackin hardline compatibility)
Corrected wrong line number: claim_container_name call is at 754, not 918
(918 is the function definition). Each INV has a grep-executable Verify by.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 36 — CI gate for PROJECT_STRUCTURE.md freshness

§3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
three concrete options:
- Option A: CONTRIBUTING.md rule (necessary but insufficient)
- Option B: ci.yml git-diff-scoped shell check (recommended) — only checks
  files added in the current PR so it doesn't require fixing existing stale
  entries before merging; greps for module directory name in prose
- Option C: Structured TOML module registry (over-engineered for scale)
Includes concrete YAML snippet for Option B grounded in the check:repo-links.ts
pattern already established in docs/scripts/

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iterations 36-37 — CI gate + greenfield workspace architecture

Iteration 36:
- §3: Add "Preventing future PROJECT_STRUCTURE.md staleness" subsection with
  3 options (CONTRIBUTING.md rule / ci.yml git-diff check / TOML registry);
  recommend Option B (git-diff-scoped YAML step) with concrete snippet grounded
  in existing check:repo-links.ts pattern from docs/scripts/

Iteration 37 (operator directive: greenfield Rust structure):
- §4: Add "Greenfield architecture — ideal structure for a growing project"
  section based on verified cross-module dependency graph (grep iteration 37)
- Confirms dependency tiers: workspace/manifest/docker/paths/selector = Tier 0;
  config/tui/instance = Tier 1; operator_env/runtime/repo = Tier 2; console = Tier 3
- Key finding: workspace/ is LOWER-level than config/ (config re-exports workspace
  types at lines 5-6); ideal naming inverted in greenfield (jackin-core > jackin-config)
- Documents ideal 6-crate workspace: jackin-core, jackin-config, jackin-tui,
  jackin-runtime, jackin-console, jackin-shell + thin binary
- Notes console/ has NO runtime/ import — cleanest pre-existing crate boundary
- Bridge: incremental splits (4a, 4d, 4g) are pre-work toward workspace migration

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 38 — Rust workspace standards, community evidence

Ground workspace recommendation in real-world project research:
- ripgrep (9 crates), gitui (5 crates) went workspace due to library consumers
- starship and fd-find stay single-crate at 1M+ LOC — no library use case
- jackin (43K LOC, no external consumers) maps to starship/fd pattern
  → single-crate is community-standard; "stay single-crate" recommendation confirmed

Update greenfield workspace structure to follow matklad's pattern:
- Virtual manifest at root (no [package] in root Cargo.toml)
- Flat crates/ directory (not nested); crate names match folder names
- version = "0.0.0" for unpublished internal crates
- Add inline dep comments to each crate in the ASCII structure

Add research notes: ripgrep/starship/gitui/fd-find Cargo.toml findings +
Cargo workspaces reference + matklad "Large Rust Workspaces" (2021-08-22)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): revise §7.9 + §3 — adopt per-directory README.md

§7.9: Reverse previous "reject" recommendation to "adopt" per-directory
README.md for major src/ module directories. Rationale: README.md is
AI-native — Claude Code, Copilot, Cursor load it automatically on directory
entry, giving AI agents orientation before they decide which file to open.
PROJECT_STRUCTURE.md being confirmed stale removes the main argument for
the "single root file" approach.

Add three-layer documentation model table:
- README.md: directory orientation (AI + human, on entry)
- AGENTS.md: agent workflow rules (root, session start)
- CLAUDE.md: @AGENTS.md pointer only — NEVER add content here
- //! docs: file-level contracts (when reading/editing)

Add specific README.md content targets for 7 directories
(src/, src/runtime/, src/console/, src/console/manager/,
src/console/widgets/, docs/, docs/internal/).

§3 target document shape: Add per-directory README.md to proposed
hierarchy; add docs/internal/specs/ explicitly; note CLAUDE.md
design principle (single-line @AGENTS.md — never duplicate content).

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): internal docs are browsable — unified Starlight site

Operator directive: internal docs (architecture, specs, ADRs, roadmap) should
be browsable, not hidden filesystem files. They are a different TYPE of docs
focused on implementation details and vision, published as a "Developer
Reference" section of the Starlight site.

§3 target document shape:
- docs/internal/ moves into docs/src/content/docs/internal/ (Starlight pages)
- Browsable at jackin.tailrocks.com/internal/
- Sidebar: "Developer Reference" group (collapsed by default) with sub-sections
  for architecture, code-tour, contributing, testing, decisions, specs, roadmap
- Include astro.config.ts sidebar config snippet

§8.1 two-tier spec distinction eliminated:
- Feature specs and behavioral specs both live at docs/src/content/docs/internal/specs/
- Type expressed via spec_type: behavioral | feature frontmatter, not filesystem location
- Both browsable and searchable via Starlight; AI agents can be pointed to URLs

§8.3 + §4:
- All docs/internal/specs/ paths → docs/src/content/docs/internal/specs/
- ADRs: docs/internal/decisions/ → docs/src/content/docs/internal/decisions/ (browsable)
- README.md pointer for src/runtime/ updated to URL reference

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): §11 — modern Rust docs platform (future project)

Add §11 capturing the vision for a modern docs.rs alternative with:
- rustdoc JSON ingestion → Astro Starlight presentation
- MCP server for AI agent queries (Context7 alternative for Rust)
- Rust-specific query types: rust_get_context(), rust_find_impls(),
  rust_search_types() — things Context7 cannot provide
- Comparison table vs Context7
- Architecture diagram (ingestion → processing → Starlight + MCP)
- Name candidates: rustlight, ferrodoc, cargo-starlight / starlight.rs
- Note that jackin's §7.15 gen-rust-api.ts pipeline is the intentional
  prototype for the platform's processing and presentation layers

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 39 — update §0, fix stale internal/ paths

§0 executive summary: rewrite to reflect decisions from iterations 30-38:
- browsable internal docs (jackin.tailrocks.com/internal/)
- per-directory README.md adoption (§7.9 reversed)
- CLAUDE.md = @AGENTS.md single-line pointer only
- greenfield workspace architecture (matklad's virtual manifest pattern)
- §11 future project: modern Rust docs platform / Context7-for-Rust
- document size 1800+ → 2200+

Fix stale docs/internal/ bare paths not caught by iteration 38 sweep:
- Mermaid diagram: INTERNAL_ROADMAP, INTERNAL_CODE_TOUR → Starlight paths
- §7.10 ADRs: docs/internal/decisions/NNN-title.md → .mdx Starlight path
- §10 Track B item 2: op-picker spec path → Starlight MDX

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): iteration 40 — §7.15 pipeline + Rule 4 pub audit

§7.15 (new): rustdoc JSON → Astro Starlight API documentation pipeline
- Three options: rustdoc HTML publish / rustdoc JSON + bun script (recommended)
  / rustdoc-json crate as Rust binary
- Option B recommended: matches existing docs/scripts/ pattern, nightly
  isolated to separate CI step, zero effect on stable build
- Key design: URL at /internal/api/, cross-links to behavioral specs,
  Starlight unified search, prototype for §11 future project
- Pub(crate) note: gen-rust-api.ts can feed Rule 4 visibility audit
- Recommend: adopt after Phase 1 //! sprint (value ∝ coverage)

§4 Rule 4 pub discipline: replace estimated "50-100 items" guess with
verified numbers from iteration 40 grep:
- 257 bare pub items, 21 pub(crate), 61 pub(super) across 94 files
- 0 uses of unreachable_pub lint — no enforcement gate
- Top violators: operator_env.rs (17), tui/output.rs (13), planner.rs (8)
- Add concrete Cargo.toml [lints.rust] snippet: unreachable_pub = "warn"
- Revised scope: ~150-200 mechanical conversions (excludes entry points)

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): split research into 19 actionable items

Delete _research_notes.md (no longer needed).

Replace 2343L READABILITY_AND_MODERNIZATION.md with:
- README.md: index of all 19 items with phase, ordering notes, links
- READABILITY_AND_MODERNIZATION.md: lightweight research summary (63L)
- items/ITEM-001 through ITEM-019: individual actionable items

Items by phase:
  Phase 1 (low risk, no confirmation): ITEM-001..004, 006..011
  Phase 1 (needs confirmation): ITEM-005, 016, 018
  Phase 2 (structural splits, confirmation required): ITEM-012..015
  Phase 3 (deferred): ITEM-017 (rustdoc pipeline), ITEM-019 (workspace)

Each item has: summary, key files with line numbers, steps, what
needs confirmation, and relevant research backing from the 40-iteration
analysis loop.

Co-authored-by: Claude <noreply@anthropic.com>

* docs(roadmap): migrate 19 items to Starlight reference/roadmap section

Move all codebase health roadmap items from docs/internal/roadmap/items/
(plain Markdown, not browsable) to docs/src/content/docs/reference/roadmap/
(MDX pages, browsable at jackin.tailrocks.com/reference/roadmap/).

Adds a new "Codebase health" sidebar group (Phase 1 → Phase 3) to
astro.config.ts. Deletes the old items/ directory. Updates the internal
README to redirect to the new location.

Also adds codebase-readability.mdx — a new overview item that captures
the overall readability/restructuring program with a recommended execution
order: file splits first, then greenfield workspace, then per-directory
README+AGENTS.md, then docs and specs.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>

* docs(roadmap): remove premature internal/roadmap/README.md

The internal/ structure doesn't exist yet — it will be created as part
of the roadmap items themselves. No need for a redirect stub now.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>

* docs(roadmap): remove READABILITY_AND_MODERNIZATION.md research archive

All content has been distilled into the individual Starlight roadmap pages.
The full 2343L research is preserved in git history at commit b7e9fc2.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>

* docs(roadmap): fix check:repo-links errors + remove iteration log

- Replace plain code spans with <RepoFile> for validate.rs, mise.toml,
  Cargo.toml, and op_picker/mod.rs
- Remove deleted READABILITY_AND_MODERNIZATION.md reference from
  codebase-readability.mdx
- Delete _iteration_log.md (git history is the archive)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>

* docs(roadmap): fix lychee false-positive link in move-contributing-testing

The example redirect text contained a markdown hyperlink to a proposed
future file path that doesn't exist yet. Changed to a code span.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>

---------

Signed-off-by: Alexey Zhokhov <alexey@chainargos.com>
Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Co-authored-by: Codex <codex@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant