# `secret` Interactive Console Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. > Design (approved): `docs/plans/2026-07-19-secrets-interactive-cli-design.md` > Execute in worktree branch `worktree-secrets-interactive-cli` (root: `/Users/dohertj2/Desktop/scadaproj/.claude/worktrees/secrets-interactive-cli`). All paths below are relative to `ZB.MOM.WW.Secrets/` inside that worktree. Run all commands from that `ZB.MOM.WW.Secrets/` directory. **Goal:** Menu-driven Spectre.Console TUI inside the existing `secret` CLI for deployment secret-seeding and lockout recovery, launched by no-args-in-a-TTY; all headless verbs unchanged. **Architecture:** New `Interactive/` layer in `src/ZB.MOM.WW.Secrets.Cli`: UI-free services (`TargetConfigReader`, `SecretsSessionFactory`, `ReferenceAuditor`, `KekDoctor`, `BundleService`, `RecentTargetsStore`) + an `InteractiveShell` that owns `IAnsiConsole` and dispatches to `IInteractiveFlow` implementations (one file per flow, so flows parallelize). Sessions are built per target by constructing the store stack directly (SQLite or SQL-Server) — no per-target DI host. Degraded mode when the KEK is unavailable in the CLI's shell. **Tech Stack:** .NET 10, Spectre.Console (+ Spectre.Console.Testing), xunit 2.9.3, existing core seams (`ISecretStore`, `ISecretCipher`, `IMasterKeyProvider`, `KekRotationService`, `SecretLastWriterWins`). **Global rules for every task:** - TDD: write the failing test first, see it fail, implement, see it pass, commit. - Test command: `dotnet test ZB.MOM.WW.Secrets.slnx` (fully offline; must end 0-warning — the repo treats warnings as errors in CI posture). - Secret values must NEVER appear in recents JSON, bundles, logs, or non-reveal screen output. XML-doc every public member (family CommentChecker convention). - Match surrounding code style: file-scoped namespaces, `sealed` classes, records for data, `ConfigureAwait(false)` in library-ish code. --- ### Task 1: Package + project wiring **Classification:** trivial **Estimated implement time:** ~3 min **Parallelizable with:** none (everything depends on it) **Files:** - Modify: `Directory.Packages.props` - Modify: `src/ZB.MOM.WW.Secrets.Cli/ZB.MOM.WW.Secrets.Cli.csproj` - Modify: `tests/ZB.MOM.WW.Secrets.Tests/ZB.MOM.WW.Secrets.Tests.csproj` **Step 1:** Add to `Directory.Packages.props` (new `` group next to the Test group): ```xml ``` (If restore says 0.50.0 doesn't exist on the feed, use the latest stable `dotnet package search Spectre.Console` reports and keep both packages on the same version.) **Step 2:** In the Cli csproj add `` to its ItemGroup and a ProjectReference to `..\ZB.MOM.WW.Secrets.Replicator.SqlServer\ZB.MOM.WW.Secrets.Replicator.SqlServer.csproj` (SQL-hub targets like ScadaBridge central). **Step 3:** In the Tests csproj add ``. **Step 4:** Run: `dotnet build ZB.MOM.WW.Secrets.slnx` → Expected: Build succeeded, 0 warnings. **Step 5:** Commit: `git add -A && git commit -m "feat(secrets-cli): wire Spectre.Console + SqlServer store refs for interactive console"` --- ### Task 2: RecentTargetsStore **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** Task 3 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/RecentTargetsStore.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/RecentTargetsStoreTests.cs` **Step 1 (failing tests):** `RecentTargetsStoreTests` against a temp file path (constructor takes the JSON file path; production default `Path.Combine(Environment.GetFolderPath(SpecialFolder.UserProfile), ".zb-secrets", "recent-targets.json")` supplied by the caller, tests pass a temp path): - `Load_returns_empty_when_file_missing` - `Touch_then_load_roundtrips_name_and_path` - `Touch_existing_path_moves_it_first_and_updates_stamp` (inject a `TimeProvider`) - `Load_tolerates_corrupt_json_by_returning_empty` - `Touch_caps_list_at_ten_entries` - `Saved_json_contains_no_key_material_fields` (serialize, assert raw text has only `name`/`appSettingsPath`/`lastUsedUtc` properties) **Step 2:** Run `dotnet test ZB.MOM.WW.Secrets.slnx --filter "FullyQualifiedName~RecentTargetsStoreTests"` → FAIL (type missing). **Step 3 (implement):** ```csharp /// Persists the operator's recently-used store targets (names + appsettings paths ONLY — never key material). public sealed class RecentTargetsStore { public sealed record RecentTarget(string Name, string AppSettingsPath, DateTimeOffset LastUsedUtc); public RecentTargetsStore(string filePath, TimeProvider? timeProvider = null) { ... } public IReadOnlyList Load() { ... } // missing/corrupt file => [] public void Touch(string name, string appSettingsPath) { ... } // upsert by path, newest-first, cap 10, CreateDirectory, atomic write (temp+move) } ``` **Step 4:** Re-run the filter → PASS. **Step 5:** Commit `feat(secrets-cli): recent-targets store for the interactive console`. --- ### Task 3: TargetConfigReader + StoreTarget **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 2 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/StoreTarget.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/TargetConfigReader.cs` - Read for reference: `src/ZB.MOM.WW.Secrets/SecretsOptions.cs`, `src/ZB.MOM.WW.Secrets.Replicator.SqlServer/SqlServerSecretsOptions.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/TargetConfigReaderTests.cs` **Step 1 (failing tests):** temp dir fixtures writing real `appsettings.json` files: - `Reads_secrets_section_and_binds_options` (SqlitePath, MasterKey source/env-var) - `Environment_overlay_wins_over_base` (`appsettings.Production.json` overrides SqlitePath when `environment: "Production"` passed) - `Binds_sqlserver_options_when_section_present` (`Secrets:SqlServer:ConnectionString`) - `SqlitePath_is_resolved_relative_to_the_app_directory` (relative `secrets.db` → absolute under the appsettings dir — the app's CWD at runtime; this is the honest-store guarantee) - `Missing_file_throws_FileNotFoundException_with_path` - `Custom_section_path_is_honored` (e.g. `MySecrets`) **Step 2:** Run filter `TargetConfigReaderTests` → FAIL. **Step 3 (implement):** ```csharp /// A resolved deployment store target: the app's composed Secrets options + where they came from. public sealed record StoreTarget( string Name, // display name (defaults to appsettings parent dir name) string? AppSettingsPath, // null for manual-entry targets SecretsOptions Secrets, // SqlitePath already made absolute SqlServerSecretsOptions? SqlServer, IConfigurationRoot Configuration); // full composed config, consumed by ReferenceAuditor public sealed class TargetConfigReader { /// Loads the target app's config exactly the way the app would (base json + optional env overlay + env vars). public StoreTarget Read(string appSettingsPath, string? environment = null, string sectionPath = "Secrets") { ... } /// Builds a manual target from explicit db path + master-key options (no appsettings). public StoreTarget Manual(string sqlitePath, MasterKeyOptions masterKey) { ... } } ``` `Read` uses `new ConfigurationBuilder().SetBasePath(dir).AddJsonFile(fileName).AddJsonFile($"appsettings.{environment}.json", optional: true).AddEnvironmentVariables().Build()`, binds `SecretsOptions` from `sectionPath`, binds `SqlServerSecretsOptions` from `sectionPath + ":SqlServer"` only when that section has children, and absolutizes `SqlitePath` against `dir`. **Step 4:** filter → PASS. **Step 5:** Commit `feat(secrets-cli): target config reader — operate on the app's own composed Secrets config`. --- ### Task 4: SecretsSession + SecretsSessionFactory + LiteralMasterKeyProvider **Classification:** high-risk (crypto/KEK wiring; degraded-mode correctness is the lockout path) **Estimated implement time:** ~5 min **Parallelizable with:** none (Tasks 5-12 depend on it) **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/SecretsSession.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/SecretsSessionFactory.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/LiteralMasterKeyProvider.cs` - Read for reference: `src/ZB.MOM.WW.Secrets/MasterKey/MasterKeyProviderBase.cs` (KekId derivation to mirror), `src/ZB.MOM.WW.Secrets/MasterKey/MasterKeyProviderFactory.cs`, `src/ZB.MOM.WW.Secrets.Replicator.SqlServer/DependencyInjection/` (how SqlServer store + migrator are constructed), `src/ZB.MOM.WW.Secrets/DependencyInjection/SecretsServiceCollectionExtensions.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/SecretsSessionFactoryTests.cs`, `.../LiteralMasterKeyProviderTests.cs` **Step 1 (failing tests):** - `LiteralMasterKeyProvider`: `Accepts_base64_32_byte_key`, `Rejects_wrong_length`, `Derives_same_kekid_as_environment_provider_for_same_material` (set a temp env var, compare `KekId` — pins the sha256 derivation match so rewraps interoperate), `Explicit_kekid_wins`. - `SecretsSessionFactory` (temp SQLite targets): - `Opens_full_session_when_env_kek_present` (`KekAvailable == true`, cipher + store usable, schema migrated — assert a `set`-style upsert roundtrips) - `Opens_degraded_session_when_env_var_unset` (`KekAvailable == false`, `Store` still lists, `Cipher` is null) - `Override_kek_upgrades_degraded_session` (create with `LiteralMasterKeyProvider` override → full) - `SqlServer_target_with_unresolved_secret_ref_connstr_falls_back_to_sqlite_with_warning` (connstr containing `${secret:` → `session.Warnings` non-empty, store is the SQLite one) **Step 2:** filter `SecretsSessionFactoryTests|LiteralMasterKeyProviderTests` → FAIL. **Step 3 (implement):** ```csharp /// An open store session for one target. Degraded when no KEK is available (metadata ops only). public sealed class SecretsSession { public required StoreTarget Target { get; init; } public required ISecretStore Store { get; init; } public required ISecretsStoreMigrator Migrator { get; init; } public IMasterKeyProvider? MasterKey { get; init; } // null => degraded public ISecretCipher? Cipher { get; init; } // null => degraded public required string StoreKind { get; init; } // "sqlite" | "sqlserver" public IReadOnlyList Warnings { get; init; } = []; public bool KekAvailable => Cipher is not null; } public sealed class SecretsSessionFactory { /// Builds the store stack for a target. KEK resolution failure yields a DEGRADED session, never a throw. public async Task OpenAsync(StoreTarget target, IMasterKeyProvider? overrideKek, CancellationToken ct) { ... } } ``` Factory logic: pick SqlServer store when `target.SqlServer` is set AND its `ConnectionString` doesn't contain `${secret:` (else warn + SQLite fallback); construct exactly what the DI extensions construct (`SecretsSqliteConnectionFactory`/`SqliteSecretStore`/`SqliteSecretsStoreMigrator`, or the SqlServer equivalents); KEK = `overrideKek ?? try MasterKeyProviderFactory.Create(...)` — wrap creation AND a probe call (compute `KekId`) in try/catch, catching only the provider's documented failure exceptions; on failure record the reason in `Warnings` and open degraded. Always `await migrator.MigrateAsync(ct)` before returning. `LiteralMasterKeyProvider` holds the 32 raw bytes pasted by the operator, mirrors the base class's derived-KekId scheme. **Step 4:** filter → PASS. **Step 5:** Commit `feat(secrets-cli): per-target session factory with degraded-KEK mode + literal KEK provider`. --- ### Task 5: ReferenceAuditor **Classification:** standard **Estimated implement time:** ~4 min **Parallelizable with:** Task 6, Task 7 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/ReferenceAuditor.cs` - Read for reference: `src/ZB.MOM.WW.Secrets/Configuration/SecretReferenceExpander.cs` (token regex — reuse the exact pattern `\$\{secret:([^}]+)\}`) - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/ReferenceAuditorTests.cs` **Step 1 (failing tests):** in-memory `IConfigurationRoot` + temp SQLite session: - `Finds_all_distinct_secret_references_with_their_config_paths` (two keys referencing the same name → one finding, two paths) - `Classifies_present_decryptable_secret_as_Ok` - `Classifies_absent_secret_as_Missing` - `Classifies_tombstoned_secret_as_Tombstoned` - `Classifies_wrong_kek_row_as_Undecryptable` (seed row via a cipher on KEK-A, audit with session on KEK-B) - `Degraded_session_reports_PresenceOnly_status` (KEK unavailable → Ok becomes `PresentUnverified`) **Step 2:** filter → FAIL. **Step 3 (implement):** ```csharp public enum ReferenceStatus { Ok, PresentUnverified, Missing, Tombstoned, Undecryptable } public sealed record ReferenceFinding(string SecretName, ReferenceStatus Status, IReadOnlyList ConfigPaths); /// Scans a target app's composed configuration for ${secret:NAME} tokens and checks each against the store. public sealed class ReferenceAuditor { public async Task> AuditAsync(SecretsSession session, CancellationToken ct) { ... } } ``` Walk `session.Target.Configuration.AsEnumerable()` values with the regex; for each distinct name: `store.GetAsync` → null ⇒ Missing; `IsDeleted` ⇒ Tombstoned; degraded ⇒ PresentUnverified; else `cipher.Decrypt` in try/catch(`SecretDecryptionException`) ⇒ Ok/Undecryptable (discard plaintext immediately; never store it in the finding). **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): reference auditor — classify every ${secret:} the target app needs`. --- ### Task 6: KekDoctor **Classification:** high-risk (drives rewrap remediation) **Estimated implement time:** ~4 min **Parallelizable with:** Task 5, Task 7 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/KekDoctor.cs` - Read for reference: `src/ZB.MOM.WW.Secrets/Rotation/KekRotationService.cs`, `src/ZB.MOM.WW.Secrets/Rotation/RewrapReport.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/KekDoctorTests.cs` **Step 1 (failing tests):** temp store seeded under KEK-A and KEK-B: - `Diagnose_reports_ok_rows_under_session_kek` - `Diagnose_reports_wrong_kek_rows_with_their_kekid` (session on B, rows on A → `WrongKek`, foreign KekId surfaced so the operator knows which key to hunt for) - `Diagnose_reports_corrupt_row_when_kekid_matches_but_unwrap_fails` (tamper `WrapTag` byte) - `Diagnose_throws_on_degraded_session` (`InvalidOperationException` — doctor needs a KEK) - `RewrapAll_moves_wrong_kek_rows_onto_session_kek` (delegate to `KekRotationService`; assert report counts + rows now decrypt) **Step 2:** FAIL. **Step 3 (implement):** ```csharp public enum RowKekStatus { Ok, WrongKek, Corrupt } public sealed record KekDiagnosis(string SecretName, RowKekStatus Status, string RowKekId); public sealed record KekDoctorReport(string SessionKekId, IReadOnlyList Rows) { public bool Healthy => Rows.All(r => r.Status == RowKekStatus.Ok); } /// Lockout triage: verifies the session KEK actually opens every stored row and drives the rewrap remedy. public sealed class KekDoctor { public async Task DiagnoseAsync(SecretsSession session, CancellationToken ct) { ... } public Task RewrapAllAsync(SecretsSession session, IMasterKeyProvider oldKek, CancellationToken ct) => new KekRotationService(session.Store, session.Cipher!).RewrapAllAsync(oldKek, session.MasterKey!, ct); } ``` Diagnosis probe: KekId mismatch ⇒ WrongKek (no decrypt attempt); match ⇒ `cipher.Decrypt` try/catch ⇒ Ok/Corrupt. Include tombstoned rows (they still block rewrap-all). **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): KEK doctor — per-row diagnosis + guided rewrap remedy`. --- ### Task 7: SecretBundle export/import **Classification:** high-risk (data movement between stores, LWW + cross-KEK rewrap) **Estimated implement time:** ~5 min **Parallelizable with:** Task 5, Task 6 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/SecretBundle.cs` (DTOs + codec) - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/BundleService.cs` - Read for reference: `src/ZB.MOM.WW.Secrets.Abstractions/SecretLastWriterWins.cs`, `src/ZB.MOM.WW.Secrets.Abstractions/StoredSecret.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/BundleServiceTests.cs` **Step 1 (failing tests):** - `Export_then_import_into_empty_store_roundtrips_rows_byte_identical` - `Bundle_json_contains_no_plaintext` (export a known value; assert raw file text doesn't contain it) - `Import_conflict_resolved_by_last_writer_wins` (newer local row survives; newer bundle row replaces) - `Import_conflict_callback_can_force_bundle_row` (per-row override delegate) - `Cross_kek_import_rewraps_when_source_kek_supplied` (bundle from KEK-A into session on KEK-B + `oldKek` → rows land on B and decrypt) - `Cross_kek_import_without_source_kek_skips_rows_and_reports_them` **Step 2:** FAIL. **Step 3 (implement):** DTO `SecretBundle { int FormatVersion = 1; DateTimeOffset ExportedUtc; string SourceKekId; List Entries; }` where `BundleEntry` mirrors every `StoredSecret` field (byte[] as base64, `SecretName` as string). `BundleService`: ```csharp public sealed class BundleService { public async Task ExportAsync(SecretsSession session, string path, bool includeDeleted, CancellationToken ct) { ... } public async Task ImportAsync( SecretsSession session, string path, IMasterKeyProvider? sourceKek, Func? conflictOverride, CancellationToken ct) { ... } } public sealed record BundleImportReport(int Imported, int SkippedOlder, int SkippedForeignKek, int Conflicts); ``` Import per entry: rewrap via `session.Cipher.Rewrap(row, sourceKek, session.MasterKey)` when `row.KekId != session.MasterKey.KekId` and `sourceKek` matches, else count `SkippedForeignKek`; conflict when the store already has the name → default `SecretLastWriterWins`, `conflictOverride` (when supplied) decides instead; upsert winners. `ExportedUtc` from an injected `TimeProvider`. **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): ciphertext-only bundle export/import with LWW + cross-KEK rewrap`. --- ### Task 8: InteractiveShell skeleton + IInteractiveFlow + target picker **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** none (Tasks 9-12 depend on the flow seam) **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/IInteractiveFlow.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/InteractiveShell.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/InteractiveShellTests.cs` **Step 1 (failing tests):** with `Spectre.Console.Testing.TestConsole` (`Interactive()` profile, `PushKey`/`PushTextWithEnter` scripted input) and a fake flow: - `Shell_shows_target_picker_then_menu_and_quits` (recents + manual entry path; select Quit → returns 0) - `Menu_lists_registered_flows_by_title_plus_builtins` (Switch target, Quit) - `Degraded_session_flow_requiring_kek_prompts_for_key_first` (fake flow `RequiresKek = true`, degraded session → shell asks masked key/file prompt, upgrades session via factory override, then runs the flow) - `Flow_exception_renders_error_panel_and_returns_to_menu` (fake flow throws `SecretDecryptionException` → output contains remedy hint text `KEK doctor`, shell loops, next Quit exits cleanly) - `Ctrl_c_at_menu_exits_zero` **Step 2:** FAIL. **Step 3 (implement):** ```csharp /// One menu action of the interactive console. Implementations are UI-owning but store-logic-free. public interface IInteractiveFlow { string Title { get; } bool RequiresKek { get; } Task RunAsync(IAnsiConsole console, SecretsSession session, CancellationToken ct); } /// The menu loop: target selection, session lifecycle, flow dispatch, error containment. public sealed class InteractiveShell { public InteractiveShell(IAnsiConsole console, RecentTargetsStore recents, TargetConfigReader reader, SecretsSessionFactory sessions, IReadOnlyList flows) { ... } public async Task RunAsync(CancellationToken ct) { ... } } ``` Header panel per loop: target name, store kind, `KEK: OK` / `KEK: UNAVAILABLE (degraded)`. Menu = flows' titles + `Switch store target` + `Quit`. Degraded + `RequiresKek` ⇒ prompt (choice: base64 paste via `TextPrompt().Secret()`, or key-file path) → `LiteralMasterKeyProvider`/file provider → reopen session with override. Every flow call wrapped in try/catch rendering `new Panel(...)` in red with the remedy hint; loop continues. **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): interactive shell skeleton — target picker, degraded-KEK upgrade, flow dispatch`. --- ### Task 9: CRUD flows (list / set-rotate / reveal / delete) **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 10, Task 11, Task 12 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/ListSecretsFlow.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/SetSecretFlow.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/RevealSecretFlow.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/DeleteSecretFlow.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/Flows/CrudFlowTests.cs` **Step 1 (failing tests):** each flow run against a TestConsole + real temp-SQLite session: - List: `Renders_metadata_table_never_values` (name/contentType/kekId/revision/updated columns; `RequiresKek == false`) - Set: `Masked_prompt_seals_and_upserts` (typed value lands decryptable; typed value NOT in console output), `Existing_name_confirms_overwrite` - Reveal: `Confirm_then_prints_value_once`, `Decline_prints_nothing` - Delete: `Confirm_tombstones_row`, `Decline_leaves_row` **Step 2:** FAIL. **Step 3:** Implement the four flows (each ~40 lines): name via `TextPrompt` (validated through `SecretName`), value via `.Secret()`, content type via `SelectionPrompt`, description optional; seal with `session.Cipher.Encrypt` + actor `Environment.UserName` (same stamps as `SecretCommands.UpsertSealedAsync`). Reveal decrypts via `session.Cipher.Decrypt(store.GetAsync(...))` — no resolver, no cache. **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): CRUD flows for the interactive console`. --- ### Task 10: Reference-audit + seeding flow **Classification:** standard **Estimated implement time:** ~4 min **Parallelizable with:** Task 9, Task 11, Task 12 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/ReferenceAuditFlow.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/Flows/ReferenceAuditFlowTests.cs` **Step 1 (failing tests):** TestConsole + session whose target config references three secrets (one Ok, one Missing, one Tombstoned): - `Renders_status_table_for_all_references` - `Offers_to_seed_each_non_ok_reference_and_seeds_accepted_ones` (masked prompts; afterwards re-audit reports all Ok) - `Skipped_references_are_left_untouched` - `Manual_target_without_appsettings_reports_nothing_to_audit` **Step 2:** FAIL. **Step 3:** Implement: run `ReferenceAuditor.AuditAsync`, render table (status color-coded: green Ok / yellow PresentUnverified / red Missing/Tombstoned/Undecryptable + the config paths), then for each non-Ok finding `ConfirmationPrompt("Seed '' now?")` → masked value + content-type prompts → seal + upsert (tombstoned: upsert revives), finish with a re-audit summary line. `RequiresKek == true`. **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): deployment seeding flow — audit ${secret:} refs and fill the gaps`. --- ### Task 11: KEK-doctor flow **Classification:** high-risk **Estimated implement time:** ~4 min **Parallelizable with:** Task 9, Task 10, Task 12 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/KekDoctorFlow.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/Flows/KekDoctorFlowTests.cs` **Step 1 (failing tests):** - `Healthy_store_renders_all_ok_summary` - `Wrong_kek_rows_offer_rewrap_and_rewrap_succeeds` (script: choose "Rewrap from old KEK", choose env-var source pointing at KEK-A, confirm → rows decrypt under session KEK; output shows report counts) - `Rewrap_requires_explicit_confirmation` (decline → store untouched) - `Lost_kek_path_offers_reset_of_affected_secrets` (choose "Old KEK is lost" → per-row confirm+masked re-set; declined rows stay wrong-KEK) **Step 2:** FAIL. **Step 3:** Implement: `DiagnoseAsync` → summary table (session KekId, per-status counts, the foreign KekIds seen); if unhealthy, `SelectionPrompt`: *Rewrap from old KEK* (old key via env-var name / file path / masked base64 → provider; `ConfirmationPrompt` showing exactly what will happen; then `KekDoctor.RewrapAllAsync`, render `RewrapReport` counts) / *Old KEK is lost — re-set affected secrets* (per wrong-KEK row: confirm + masked new value → seal under session KEK, overwriting) / *Back*. `RequiresKek == true`. **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): lockout-recovery flow — KEK diagnosis, guided rewrap, lost-KEK reset`. --- ### Task 12: Bundle flows (export / import) **Classification:** standard **Estimated implement time:** ~4 min **Parallelizable with:** Task 9, Task 10, Task 11 **Files:** - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/BundleExportFlow.cs` - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/Flows/BundleImportFlow.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/Flows/BundleFlowTests.cs` **Step 1 (failing tests):** - `Export_prompts_for_path_and_reports_row_count` - `Import_reports_counts_and_applies_lww` - `Import_conflict_prompt_lets_operator_pick_per_row` (script both choices) - `Import_foreign_kek_prompts_for_source_key_then_rewraps` **Step 2:** FAIL. **Step 3:** Implement over `BundleService`; export path prompt defaults to `secrets-bundle--.json`; import wires the flow's per-row `ConfirmationPrompt` into `conflictOverride` and, when `SkippedForeignKek` would be non-zero (bundle `SourceKekId` ≠ session KekId), first offers the source-key prompt (env/file/masked). Render final `BundleImportReport` as a table. **Step 4:** PASS. **Step 5:** Commit `feat(secrets-cli): bundle export/import flows`. --- ### Task 13: Program.cs entry point + non-TTY pin **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** none (integrates everything) **Files:** - Modify: `src/ZB.MOM.WW.Secrets.Cli/Program.cs` (ONLY the `args.Length == 0` branch + composition) - Create: `src/ZB.MOM.WW.Secrets.Cli/Interactive/InteractiveEntry.cs` - Test: `tests/ZB.MOM.WW.Secrets.Tests/Cli/Interactive/InteractiveEntryTests.cs` **Step 1 (failing tests):** - `ShouldEnterInteractive_true_only_for_no_args_and_real_tty` (pure static: `(args, inputRedirected, outputRedirected)` truth table — pins that any redirection keeps usage/exit-2) - `CreateShell_composes_all_five_flows_plus_builtins` (flow titles present exactly once: List, Set/rotate, Reveal, Delete, Reference audit & seed, KEK doctor, Bundle export, Bundle import) **Step 2:** FAIL. **Step 3:** `InteractiveEntry` exposes `ShouldEnterInteractive(...)` and `CreateShell(IAnsiConsole console)` (news up recents store at `~/.zb-secrets/recent-targets.json`, reader, factory, all flows). `Program.cs`: replace the bare `return Usage();` with ```csharp if (args.Length == 0) { if (InteractiveEntry.ShouldEnterInteractive(args, Console.IsInputRedirected, Console.IsOutputRedirected)) return await InteractiveEntry.CreateShell(AnsiConsole.Console).RunAsync(ct); return Usage(); } ``` (The interactive path builds its own sessions per target, so move the host-based migration call so it only runs for the headless verbs — verify with the existing headless tests.) **Step 4:** `dotnet test ZB.MOM.WW.Secrets.slnx` (FULL suite — protects the headless verbs) → all pass. Manual smoke: `dotnet run --project src/ZB.MOM.WW.Secrets.Cli < /dev/null` → usage, exit 2. **Step 5:** Commit `feat(secrets-cli): launch interactive console on bare secret in a TTY`. --- ### Task 14: Version bump, docs, final green run **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** none **Files:** - Modify: `Directory.Build.props` (`` 0.2.3 → 0.3.0) - Modify: `README.md` (CLI section: interactive console paragraph + screenshot-style text sample) - Create: `docs/operations/interactive-console.md` (operator runbook: launching, deployment-seeding walk-through, lockout playbook incl. degraded mode + rewrap, bundle notes; mirror the tone of `docs/operations/kek-rotation.md`) **Step 1:** Make the edits. **Step 2:** `dotnet build ZB.MOM.WW.Secrets.slnx` → 0 warnings; `dotnet test ZB.MOM.WW.Secrets.slnx` → full suite green. **Step 3:** Commit `docs(secrets): interactive console runbook + 0.3.0 version bump`. **Step 4 (handoff, not merge):** Do NOT merge to `main` from inside this task — another agent is active on `main`. Leave the worktree branch `worktree-secrets-interactive-cli` fully committed; merging/publishing is a separate user-decision step (family convention: publish the 5 packages + app bumps only on explicit rollout). --- ## Dependency graph ``` 1 ─┬─ 2 ─┐ └─ 3 ─┴─ 4 ─┬─ 5 ─┐ ├─ 6 ─┤ ├─ 7 ─┤ └─ 8 ─┼─ 9 ──┐ ├─ 10 ─┤ ├─ 11 ─┼─ 13 ── 14 └─ 12 ─┘ ``` (9-12 also depend on their service tasks: 10→5, 11→6, 12→7.)