From e9a5ecb4fa254f682b72b44ac58e18e0e5a163ac Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Sat, 23 May 2026 02:41:59 +0000 Subject: [PATCH 1/2] docs(openspec): draft init/config UX overhaul (3 changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three sequential OpenSpec changes for the netclaw init/config UX overhaul planned in /home/petabridge/.claude/plans/so-a-big-ux-mossy-newt.md: 1. section-editor-abstraction — introduces ISectionEditor, registry, merge-on-save, single-step orchestrator mode, and refactors Provider/Identity/Posture to be reentrant. Lays the contract every future editable section must honor. Closes #455. 2. netclaw-config-command — new menu-driven `netclaw config` TUI command composing 10 section editors (Search, 3x chat channels, Exposure Mode, Security Posture, Audience Profiles, Outbound + Inbound Webhooks, External Skills, Skill Feeds, Browser Automation), plus a generic ListEditor framework, 4 new doctor checks, 12 smoke tapes + audit. Closes #1150. 3. simplify-netclaw-init — trims `netclaw init` to provider + identity + posture, adds existing-config refusal + --force backup-and-reset paths, post-flight nudge pointing operators at `netclaw config`. All three validated with `openspec validate --type change`. Sequential dependencies: A enables the abstraction, B introduces the command and editors, C cuts the long wizard. PR review can sequence the implementation accordingly. No production code changes in this PR; planning artifacts only. --- .../netclaw-config-command/.openspec.yaml | 2 + .../changes/netclaw-config-command/design.md | 259 ++++++++ .../netclaw-config-command/proposal.md | 190 ++++++ .../specs/feature-selection-wizard/spec.md | 59 ++ .../specs/netclaw-cli/spec.md | 33 + .../specs/netclaw-config-command/spec.md | 578 ++++++++++++++++++ .../changes/netclaw-config-command/tasks.md | 263 ++++++++ .../section-editor-abstraction/.openspec.yaml | 2 + .../section-editor-abstraction/design.md | 213 +++++++ .../section-editor-abstraction/proposal.md | 117 ++++ .../specs/netclaw-onboarding/spec.md | 112 ++++ .../specs/section-editor-abstraction/spec.md | 326 ++++++++++ .../section-editor-abstraction/tasks.md | 163 +++++ .../simplify-netclaw-init/.openspec.yaml | 2 + .../changes/simplify-netclaw-init/design.md | 208 +++++++ .../changes/simplify-netclaw-init/proposal.md | 152 +++++ .../specs/netclaw-onboarding/spec.md | 177 ++++++ .../changes/simplify-netclaw-init/tasks.md | 183 ++++++ 18 files changed, 3039 insertions(+) create mode 100644 openspec/changes/netclaw-config-command/.openspec.yaml create mode 100644 openspec/changes/netclaw-config-command/design.md create mode 100644 openspec/changes/netclaw-config-command/proposal.md create mode 100644 openspec/changes/netclaw-config-command/specs/feature-selection-wizard/spec.md create mode 100644 openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md create mode 100644 openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md create mode 100644 openspec/changes/netclaw-config-command/tasks.md create mode 100644 openspec/changes/section-editor-abstraction/.openspec.yaml create mode 100644 openspec/changes/section-editor-abstraction/design.md create mode 100644 openspec/changes/section-editor-abstraction/proposal.md create mode 100644 openspec/changes/section-editor-abstraction/specs/netclaw-onboarding/spec.md create mode 100644 openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md create mode 100644 openspec/changes/section-editor-abstraction/tasks.md create mode 100644 openspec/changes/simplify-netclaw-init/.openspec.yaml create mode 100644 openspec/changes/simplify-netclaw-init/design.md create mode 100644 openspec/changes/simplify-netclaw-init/proposal.md create mode 100644 openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md create mode 100644 openspec/changes/simplify-netclaw-init/tasks.md diff --git a/openspec/changes/netclaw-config-command/.openspec.yaml b/openspec/changes/netclaw-config-command/.openspec.yaml new file mode 100644 index 000000000..0f0616986 --- /dev/null +++ b/openspec/changes/netclaw-config-command/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-23 diff --git a/openspec/changes/netclaw-config-command/design.md b/openspec/changes/netclaw-config-command/design.md new file mode 100644 index 000000000..524c72339 --- /dev/null +++ b/openspec/changes/netclaw-config-command/design.md @@ -0,0 +1,259 @@ +## Context + +The `section-editor-abstraction` change (predecessor) introduced the +`ISectionEditor` contract, the `SectionEditorRegistry`, the merge-on-save +plumbing, and the single-step `WizardOrchestrator` mode. It refactored +Provider, Identity, and Posture into reentrant section editors but did +not introduce any new user-facing command. The linear `netclaw init` +wizard still owns the only path to configuration changes today, +including for sections that operators routinely tweak post-install +(search provider, channels, exposure mode, webhooks, skill feeds, +external skill directories, audience profiles, browser automation). + +This change introduces `netclaw config` as the canonical menu-driven +editor for those sections, composes ten new `ISectionEditor` +implementations (plus reuses the three from Change A indirectly through +the dashboard), introduces the multi-value `ListEditor` component, +and hardens the menu registry audit so the menu and its editors cannot +drift apart in subsequent work. The buggy feature-selection step from +#1150 is removed and its responsibility moves to the new +`AudienceProfilesSectionEditor`, with a smoke tape that exercises +arrow-nav and toggle keystrokes. + +## Goals / Non-Goals + +**Goals:** + +- Ship a menu-driven, reentrant TUI editor for the ten sections + operators actually want to change post-install, with doctor-blessed + saves and merge-on-save preserving every unrelated section. +- Reuse the Change A abstraction without forking: every editor in this + change is an `ISectionEditor` instance and runs inside the existing + `WizardOrchestrator` (now in single-step mode). +- Establish the generic list editor + item editor pattern so future + multi-value sections inherit add/edit/remove UX without re-inventing + it. +- Close #1150 by replacing the broken feature-selection step with the + Audience Profiles editor, exercised by a tape that drives the + failing keystrokes from the bug report. +- Activate the menu registry audit's full contract: every editor in + the registry must have a smoke tape and a round-trip xUnit test, + enforced at CI time. + +**Non-Goals:** + +- Simplifying the init wizard (third change). +- Hot-reloading the running daemon on config change. +- Editing inbound webhook route files from the TUI. +- Refactoring `netclaw provider`/`model`/`mcp` CLI subcommands. +- Identity changes post-install (renaming the agent stays a file-edit + task for MVP). +- Editing telemetry, logging, memory tuning, session timeouts, + sub-agent timeouts, shell hard-deny patterns, or scheduling on/off + from the TUI (file-edit only). +- Export/import config bundle or factory reset commands. +- Installing Playwright from the TUI (instructions sub-page only). + +## Decisions + +### D1. Dashboard is a single Termina page with a flat registry + +`ConfigDashboardPage` walks `SectionEditorRegistry.All()` once and +renders the editors in registration order, grouped by `Category` only +for visual presentation. The registry stays flat; the audit, the +round-trip test base class, and the smoke-tape lookup all key off +`SectionId`. Twelve editors render comfortably in a standard 80×24 +terminal without scrolling. + +Alternative considered: a tree-structured registry with first-class +parent/child nodes. Rejected because every "tree" need today is +satisfied by a `Category` string tag and a heavier structure would +complicate the audit, the registry resolution, and the round-trip +tests for no current benefit. + +### D2. Sub-page items via modal sub-orchestrators, not nested pages + +When the `ListEditor` opens a sub-page (e.g. Outbound Webhooks edit +form), the host invokes a fresh `WizardOrchestrator` in single-step mode +on the sub-page's viewmodel. The sub-orchestrator's Save returns its +result to the parent list; the parent list updates in-memory state and +re-renders. This keeps step lifecycle uniform across the whole config +command and avoids a separate "nested page" lifecycle. + +Alternative considered: a stack-of-pages model in Termina layout +where the sub-page is part of the same rendering pass. Rejected +because the sub-orchestrator model already exists from Change A and +adding a parallel stack would split the lifecycle. + +### D3. Doctor blessing is per-editor on save, never inline-per-field + +When a section editor saves, the host builds the candidate merged +config in memory, resolves only that editor's `RelevantDoctorChecks`, +and runs them against the candidate. The dashboard's "Run full doctor" +item is the only entry point for cross-section checks. Per-field +validation lives in the editor's own form (e.g. URL parsing) and is +distinct from doctor blessing. + +Alternative considered: per-field inline validation backed by doctor. +Rejected because doctor checks are designed to operate over complete +config sections, not single fields; running them on every keystroke +would produce confusing transient errors as the operator fills in +related fields. + +### D4. List editor `+ Add` row as a list member, not a separate action bar + +`ListEditor` renders `+ Add ` as the last row of the list +itself. Navigation is uniform (arrow keys move through items, Enter +activates) and there is no modal handoff between "list section" and +"action section." The `+ Add` row is visually distinct (different +glyph, no status) so operators do not mistake it for a data row. + +Alternative considered: a fixed action bar at the list bottom with +explicit `[ Add ]`, `[ Edit ]`, `[ Remove ]` buttons. Rejected +because every TUI list editor we model on (lazygit, k9s, git rebase +interactive) uses inline rows for adds, and the modal handoff +between list and action bar adds keystrokes for no benefit. + +### D5. Inline `d`/`y` confirm for list deletes; modal confirm for credential removal + +List deletes (`d` on a focused item) get a single-key inline +`Remove? [y/N]` prompt because the cost of an accidental delete is low +(operator re-adds the item from memory). Credential removal uses a +default-Cancel modal confirm because the cost is higher (operator +must re-enter or rotate the credential externally). Both confirm +patterns are inherited from Change A's secret-handling contract. + +### D6. New schema section for `BrowserAutomation` + +The schema gains `BrowserAutomation { Enabled: bool, +PlaywrightVersion?: string }` as a top-level section with `Enabled` +defaulting to `false` so existing configs validate without a fix +pass. A matching `BrowserAutomationConfig.cs` lives in +`Netclaw.Configuration`. The browser-automation step today writes +its state into `McpServers` indirectly; this change formalizes the +section so the editor and doctor check have a stable home. + +Alternative considered: keep using `McpServers` as the implicit +home. Rejected because conflating browser-automation with MCP +server config makes both harder to reason about; the doctor check +needs to look in one place. + +### D7. Audit promotion from soft-warn to hard-fail in this change + +In Change A the menu-registry audit allowed missing tape files +without failing (the `netclaw config` command did not exist yet). In +this change the command exists, so the audit's tape-existence check +flips to hard-fail. New section editors added in future PRs cannot +ship without a tape and a round-trip test. + +Alternative considered: keep tape-existence as soft-warn. Rejected +because the contract is only as strong as its weakest enforced rule; +soft-warn drifts into "we'll get to it" which is exactly the failure +mode the audit exists to prevent. + +### D8. Daemon-restart nudge is a stderr line, not a screen + +After a save-and-quit, Termina tears down and the operator returns to +the shell. The nudge prints to stderr after Termina exits so it +remains on screen even after the TUI clears. It is suppressed when +no writes occurred or when the daemon is not running, to avoid +nagging. + +Alternative considered: render the nudge as a final post-flight screen +inside Termina. Rejected because the operator may dismiss the screen +without reading it; a stderr line persists in the scroll buffer. + +### D9. `config-no-init.tape` covers the refusal path + +The refuse-when-no-config behavior is exercised by its own tape and +assertion. This avoids overloading any single section-editor tape with +the refusal scenario and keeps the audit's "tape per registered +editor" rule clean (the refusal tape is not associated with any +registry entry). + +### D10. Editor file layout under `Tui/Sections/
/` + +Each section editor lives in its own folder under +`src/Netclaw.Cli/Tui/Sections/`. Chat-channel editors get a +`Channels/` parent folder, webhooks get a `Webhooks/` parent. The +folder layout mirrors the menu's visual grouping for discoverability +while keeping the registry flat. + +## Risks / Trade-offs + +- [CI runtime increase] Twelve new smoke tapes plus the no-init refusal + tape add roughly 5–10 minutes to PR-gating smoke runs. → Mitigation: + smoke tapes are inherently parallelizable; if the wall-clock cost + becomes a problem, parallelize tape execution before reducing + coverage. + +- [Audit false positives during partial PRs] During implementation a + contributor may add a section editor before its tape lands. + → Mitigation: the audit's failure message names the missing artifact + explicitly. The convention is "tape and round-trip test land in the + same commit as the editor." PR review enforces it. + +- [Schema migration ergonomics] Adding `BrowserAutomation` as a new + top-level section is one of the few schema additions in this work. + → Mitigation: `"Enabled": false` default lets existing configs + validate; `SchemaFixResolver` auto-inserts the missing key on + next `netclaw doctor --fix` run. Per CLAUDE.md schema sync rule, + the schema and `BrowserAutomationConfig.cs` ship in the same PR. + +- [Reuse of existing channel-audience UX] The new chat-channel + section editors host the existing `channel-audience-tui` + cycling behavior, which is established and tested. → Mitigation: + the section editors compose the existing TUI components rather + than re-implementing them; the channel-audience-tui requirements + remain authoritative. + +- [Doctor checks that probe network endpoints] `SkillFeedsDoctorCheck` + and Slack/Discord/Mattermost `Test Connection` actions reach out + to remote services. → Mitigation: probing is warn-only or + user-initiated. Doctor errors that block save are local-only + (schema validity, key/backend pairing, etc.). + +- [Audience Profiles editor's keystroke contract] If Termina's + `SelectionListNode` has a latent bug (which #1150 implies), arrow + nav and Space toggle may misbehave. → Mitigation: the + `config-audience.tape` smoke tape drives exactly those keystrokes + and the assertion verifies the resulting state. If the underlying + component is broken at the Termina level, this tape will fail and + the bug must be fixed before merge. + +- [Removed feature-selection step on re-run] Operators who currently + rely on the feature-selection step in `netclaw init` lose it. + → Mitigation: PRD-004 and the `feature-selection-wizard` spec + delta document the relocation. The new Audience Profiles editor + is reachable from one menu entry away. Migration text in the PR + description points operators at the new path. + +## Migration Plan + +This change ships net-new behavior (`netclaw config`) plus a single +behavior removal (the feature-selection step in init). Migration +considerations: + +1. Land the change. `netclaw init` no longer shows the + feature-selection step on re-run; existing `netclaw.json` keeps + its feature-flag values untouched. +2. Operators who want to change feature flags post-install run + `netclaw config → Audience Profiles → `. +3. The new `BrowserAutomation` schema section is auto-inserted by + `netclaw doctor --fix` on existing installs (or appears + automatically when `netclaw config` runs over an existing + config that lacks it — the merge writer creates the section + with `Enabled: false` when the operator opens the editor). +4. Daemon restart is required for live config changes to take + effect; the stderr nudge instructs operators to restart when + relevant. + +Rollback: revert the change. `netclaw config` disappears from the +CLI surface. The feature-selection step returns to `netclaw init` +on re-run. The audit returns to Change A's soft-warn tape-existence +behavior. `netclaw.json` values written by `netclaw config` remain +valid against the schema and continue to be respected at runtime. + +## Open Questions + +None at execution time. All architectural decisions are locked above. diff --git a/openspec/changes/netclaw-config-command/proposal.md b/openspec/changes/netclaw-config-command/proposal.md new file mode 100644 index 000000000..41f172405 --- /dev/null +++ b/openspec/changes/netclaw-config-command/proposal.md @@ -0,0 +1,190 @@ +## Why + +After the `section-editor-abstraction` change lands, Netclaw has the +machinery to share editable sections between the init wizard and any new +command — but no command actually consumes it. Operators still have no way +to change live configuration (search provider, exposure mode, channels, +webhooks, skill feeds, external skill directories, Playwright, audience +profiles, security posture) without hand-editing `netclaw.json`. This +change introduces `netclaw config`, a menu-driven TUI editor that composes +the abstraction's section editors into a single dashboard with reentrant +section-by-section editing, doctor-blessed save, and a CI-enforced audit +that prevents the menu and the editors from drifting apart over time. + +This change also retires the buggy team/public feature-toggle screen in +the existing init wizard (#1150) by replacing it with the new Audience +Profiles section editor, which exercises arrow navigation and toggle +keystrokes under a smoke tape rather than relying on undertested +hand-coded input handling. + +Source PRDs: `PRD-004-cli-onboarding-and-config.md`, +`PRD-001-netclaw-mvp.md`, `PRD-002-gateway-security-envelope.md`. + +## What Changes + +- Add a new `netclaw config` top-level CLI command that launches Termina + with a `ConfigDashboardPage` rendering every entry in + `SectionEditorRegistry`. The dashboard computes per-section status + (`✓` configured / `⚠` warning / `✗` error / `–` default) by running + each editor's `RelevantDoctorChecks` on entry. Selecting a section + opens its editor in single-step orchestrator mode; on save the + section's checks run inline and either block (on errors), render a + "Save anyway" affordance (on warnings), or accept the write (on + clean). Returning from an editor refreshes the affected section's + status. +- Add a "Run full doctor" item at the dashboard's tail that invokes the + existing `DoctorRunner` with the same exit-code semantics as + `netclaw doctor`, plus a "Quit" item. +- Add the dashboard's existing-config refusal: if `netclaw.json` is + absent, `netclaw config` prints "No configuration found. Run + `netclaw init` first." and exits non-zero. The dashboard does not + render against a default skeleton. +- Add a generic `ListEditor` Termina component and a per-shape + `IItemEditor` contract. Day-one item-editor implementations: + `PathItemEditor` (External Skill Directories), `WebhookItemEditor` + (Outbound Webhooks — sub-page form with name + URL + auth header), + `SkillFeedItemEditor` (Skill Feeds — sub-page form with name + URL + + Bearer token), and `IdentifierItemEditor` (channel IDs, user IDs, + trusted-proxy CIDRs). Simple items edit inline; complex items open + sub-pages. Multi-value sections gain a uniform Add / Edit / Remove + affordance with default-Cancel destructive confirms. +- Add ten new `ISectionEditor` implementations registered in the menu: + Search Provider, Slack Channels, Discord Channels, Mattermost + Channels, Exposure Mode (covering Daemon host/port, trusted proxies, + and per-mode sub-forms for Reverse Proxy / Tailscale / Cloudflare), + Security Posture, Audience Profiles, Outbound Webhooks, Inbound + Webhooks, External Skill Directories, Skill Feeds, Browser + Automation. Slack/Discord/Mattermost share a `"Chat Channels"` + category for menu grouping; the registry treats them as three + independent editors. +- Add the Audience Profiles section editor as the replacement for the + init wizard's broken feature-selection step. The editor SHALL exercise + `↑/↓` navigation between audience tiers, `Space` to toggle individual + per-audience feature flags, and explicit `Reset to posture default` + affordance. A dedicated smoke tape (`config-audience.tape`) drives + these keystrokes and asserts the resulting `Tools.AudienceProfiles` + state. +- Add the Exposure Mode section editor with mode-conditional sub-forms. + Trusted Proxies multi-value list, Reverse Proxy external base URL, + Tailscale auth key (secret), and Cloudflare Tunnel token (secret) are + all reachable from one editor. The editor migrates the responsibility + previously covered by `init-wizard-reverse-proxy.tape` from init into + the config command. +- Add four new doctor checks invoked by the new editors: + `SearchBackendDoctorCheck` (backend-key pairing), + `ExternalSkillSourcesDoctorCheck` (each path is a readable + directory), `SkillFeedsDoctorCheck` (reachability, warn-only — remote + endpoints are allowed to be transiently down), and + `BrowserAutomationDoctorCheck` (Playwright binary present when + feature is enabled). +- Add a new top-level schema section + `BrowserAutomation { Enabled: bool, PlaywrightVersion?: string }` and + the matching `BrowserAutomationConfig.cs`. Schema sync per CLAUDE.md + rule. `"Enabled"` defaults to `false` so `SchemaFixResolver` can + auto-insert on upgrade. +- Add twelve new smoke tapes (`config-search.tape`, + `config-slack.tape`, `config-discord.tape`, `config-mattermost.tape`, + `config-exposure-mode.tape`, `config-posture.tape`, + `config-audience.tape`, `config-outbound-webhooks.tape`, + `config-inbound-webhooks.tape`, `config-external-skills.tape`, + `config-skill-feeds.tape`, `config-browser-automation.tape`) and a + `config-no-init.tape` that asserts the refusal path. Each tape has a + matching assertion script that checks the modified field changed and + unrelated sections are byte-identical to the pre-stage fixture. +- Add round-trip xUnit test classes for all ten new section editors, + derived from `SectionEditorTestBase` introduced in the prior + change. The Change A test pattern carries forward unchanged. +- Activate the `MenuRegistryAuditTests` smoke-tape existence check + (gated as soft-warn in Change A) into a hard fail: any registered + editor without `tests/smoke/tapes/config-.tape` + fails the audit. +- Closes #1150 (feature toggles broken for team/public dispositions — + the buggy screen is removed and its responsibility moves to Audience + Profiles). + +**In scope (MVP):** the `netclaw config` command, the dashboard, +single-step editor hosting, ten new section editors, four new doctor +checks, the new `BrowserAutomation` schema section, generic list and +item editors, twelve new smoke tapes + the no-init refusal tape, ten +new round-trip xUnit test classes, the hardened audit, and a stderr +"daemon restart required to apply changes" nudge when the daemon is +running at config-command exit. + +**Out of scope:** simplification of `netclaw init` (third change), +hot-reload of the running daemon on config change, export/import config +bundle, factory reset, route-file editing for inbound webhooks, +identity beyond what init sets (renaming the agent post-install remains +a file-edit task), telemetry/logging/memory/session/sub-agent/scheduling +config knobs (file-edit only), shell hard-deny patterns (file-edit +only), Playwright installation from within the TUI (instructions +sub-page only), and refactor of `netclaw provider`/`model`/`mcp` CLI +subcommands. + +## Capabilities + +### New Capabilities + +- `netclaw-config-command`: contract for the `netclaw config` command — + command-level lifecycle, dashboard rendering, per-section status + computation, single-step editor hosting, doctor blessing on save, + refusal when no config exists, daemon-restart nudge at exit, + list/item editor framework, and the ten section editors' shared + obligations. + +### Modified Capabilities + +- `netclaw-cli`: add `netclaw config` to the operator CLI surface; add + the `Quit` and `Run full doctor` dashboard items as standard + affordances. +- `feature-selection-wizard`: remove the feature-selection step from + `netclaw init`. The deployment-wide feature toggles previously written + by that step move to the Audience Profiles section editor in + `netclaw config`, exposed per audience and per feature with the + keystroke contract required by #1150. +- `channel-audience-tui`: re-host the existing channel-audience + cycling behavior as the per-channel-editor sub-screen, retaining + the requirement that audience defaults derive from posture but + letting the operator override per-channel from the config command. + +## Impact + +**Affected systems:** + +- CLI command surface (`Netclaw.Cli.Program` routing, + `Netclaw.Cli.Config.ConfigCommand` new class). +- Termina TUI (`Netclaw.Cli.Tui.Sections.ConfigDashboardPage`, + `ConfigDashboardViewModel`, `ListEditor`, four item editors). +- Ten new section editors under + `src/Netclaw.Cli/Tui/Sections/{Search,Channels/{Slack,Discord,Mattermost},ExposureMode,SecurityPosture,AudienceProfiles,Webhooks/{Outbound,Inbound},ExternalSkills,SkillFeeds,BrowserAutomation}/`. +- Doctor system gains four checks under + `src/Netclaw.Cli/Doctor/Checks/`. +- Schema (`netclaw-config.v1.schema.json`) gains the `BrowserAutomation` + top-level section. +- Configuration types (`src/Netclaw.Configuration/BrowserAutomationConfig.cs`). +- Test surface gains twelve smoke tapes, ten round-trip test classes, + and a hardened menu registry audit. + +**Security and operational impact:** + +- Secret-handling contract from Change A applies to every secret-bearing + field across the ten new editors. No new secret display surface is + introduced; "Remove credential" is the only path that deletes a + secret value. +- Doctor checks scoped to each editor run inline on save; cross-section + checks remain gated to the dashboard's "Run full doctor" action. No + network-probing check blocks save by default (`SkillFeedsDoctorCheck` + is warn-only) so transient outages do not lock operators out of + editing. +- The hardened audit prevents the menu and editors from drifting: + adding a new menu entry without its tape or round-trip test fails + CI immediately. +- Existing daemon does not hot-reload. A stderr nudge at config-command + exit instructs operators to restart the daemon to apply changes when + the daemon is detected as running; otherwise the nudge is omitted. +- The feature-selection step's removal is a behavioral change for + operators on non-Personal postures who re-run `netclaw init` over + existing config: they no longer see the step. Its responsibility + moves to `netclaw config → Audience Profiles`. PRD-004 is updated + in this change to reflect the new shape. +- No persistence schema changes. No new actor or session contract + changes. No external network dependencies introduced. diff --git a/openspec/changes/netclaw-config-command/specs/feature-selection-wizard/spec.md b/openspec/changes/netclaw-config-command/specs/feature-selection-wizard/spec.md new file mode 100644 index 000000000..3f1706e8e --- /dev/null +++ b/openspec/changes/netclaw-config-command/specs/feature-selection-wizard/spec.md @@ -0,0 +1,59 @@ +## REMOVED Requirements + +### Requirement: Feature selection wizard step + +**Reason**: The init-wizard feature-selection step (issue #1150) had broken +keystroke handling for Team and Public audience toggles. Its responsibility +moves to the new `AudienceProfilesSectionEditor` in `netclaw config`, +which renders per-audience feature toggles with documented arrow-nav and +Space-toggle semantics under a CI-gated smoke tape +(`config-audience.tape`). + +**Migration**: Operators previously walked this step at the end of +`netclaw init` for non-Personal postures. After this change, the init +wizard skips the feature-selection step entirely; deployment-wide +defaults are derived from the selected security posture +(per `Requirement: Audience defaults from posture` in the +`channel-audience-tui` capability) and per-audience feature toggles are +edited via `netclaw config → Audience Profiles`. Existing +`netclaw.json` files retain whatever feature-flag values they hold; +the new Audience Profiles editor preserves customizations. + +## MODIFIED Requirements + +### Requirement: Feature config Enabled flags + +The configuration schema SHALL include `Enabled` boolean properties for +Memory, Search, SkillSync, SubAgents, and Webhooks sections, plus a +top-level `Scheduling` section whose only property is `Enabled`. These +flags SHALL be written by either the init wizard's posture-default +cascade or the `AudienceProfilesSectionEditor` in `netclaw config`. +Both writers SHALL emit byte-identical output for equivalent input. + +#### Scenario: Disabled memory writes Enabled false + +- **GIVEN** the operator disabled memory in the Audience Profiles + editor (under any audience) and saved +- **WHEN** the editor's merge writer completes +- **THEN** `Memory.Enabled` is `false` in `netclaw.json` + +#### Scenario: Disabled search writes Enabled false + +- **GIVEN** the operator disabled search in the Audience Profiles + editor and saved +- **WHEN** the editor's merge writer completes +- **THEN** `Search.Enabled` is `false` in `netclaw.json` + +#### Scenario: Disabled scheduling writes top-level Scheduling.Enabled false + +- **GIVEN** the operator disabled scheduling in the Audience Profiles + editor and saved +- **WHEN** the editor's merge writer completes +- **THEN** `Scheduling.Enabled` is `false` in `netclaw.json` +- **AND** `Scheduling` contains no other properties in this change + +#### Scenario: Default Personal config has all features enabled + +- **GIVEN** the operator selected Personal posture at init +- **WHEN** the init wizard's merge writer completes +- **THEN** all `Enabled` flags default to `true` diff --git a/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md b/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md new file mode 100644 index 000000000..2c38340ce --- /dev/null +++ b/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: Config command surface + +The CLI SHALL expose `netclaw config` as a top-level command. The +command SHALL be offline (no daemon connection), SHALL operate on +local config files only, and SHALL behave per the +`netclaw-config-command` capability. `netclaw config --help` SHALL +print a one-paragraph description and exit zero. Invocations with any +positional argument SHALL print usage and exit non-zero in this change +(subcommands such as `netclaw config show|validate` remain reserved +for future work and SHALL NOT execute as a side effect). + +#### Scenario: Help text describes the command + +- **WHEN** the operator runs `netclaw config --help` +- **THEN** the command exits with status 0 +- **AND** stdout contains a one-paragraph description naming + "interactive configuration editor" +- **AND** stdout references the `netclaw init` companion command + +#### Scenario: Unknown subcommand rejected + +- **WHEN** the operator runs `netclaw config foo` +- **THEN** the command exits with non-zero status +- **AND** stderr contains usage text + +#### Scenario: No-args invocation launches dashboard + +- **WHEN** the operator runs `netclaw config` with no arguments +- **AND** `netclaw.json` exists +- **THEN** the dashboard launches per the + `netclaw-config-command` capability diff --git a/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md b/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md new file mode 100644 index 000000000..6067f06bb --- /dev/null +++ b/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md @@ -0,0 +1,578 @@ +## ADDED Requirements + +### Requirement: Config command launches dashboard + +`netclaw config` SHALL launch Termina with a dashboard page rendering every +registered `ISectionEditor` from `SectionEditorRegistry`, plus a "Run full +doctor" item and a "Quit" item at the dashboard tail. The command SHALL +operate offline (no daemon connection required) and SHALL read/write +local config files only. + +#### Scenario: Dashboard renders all registered editors + +- **GIVEN** the CLI is configured with the day-one editor registry + (Search, Slack, Discord, Mattermost, ExposureMode, SecurityPosture, + AudienceProfiles, OutboundWebhooks, InboundWebhooks, ExternalSkills, + SkillFeeds, BrowserAutomation) +- **WHEN** the operator runs `netclaw config` +- **THEN** Termina opens with a dashboard listing every editor, with + status badges computed per editor +- **AND** the tail shows a "Run full doctor" item and a "Quit" item + +#### Scenario: Config command does not require daemon + +- **GIVEN** the Netclaw daemon is not running +- **WHEN** the operator runs `netclaw config` +- **THEN** the command starts and renders the dashboard normally +- **AND** no daemon RPC or HTTP call is made + +### Requirement: Refuse when no config exists + +`netclaw config` SHALL detect a missing `netclaw.json` at startup and +refuse to render the dashboard. The command SHALL print +`No configuration found. Run \`netclaw init\` first.` to stderr and exit +with a non-zero exit code. + +#### Scenario: No config refusal exits non-zero + +- **GIVEN** `~/.netclaw/config/netclaw.json` does not exist +- **WHEN** the operator runs `netclaw config` +- **THEN** the command prints `No configuration found. Run \`netclaw init\` first.` + to stderr +- **AND** exits with a non-zero exit code +- **AND** does not render any Termina UI + +### Requirement: Dashboard status badges + +The dashboard SHALL render a status badge for every section editor by +computing `GetStatus(currentConfig)` and running the editor's +`RelevantDoctorChecks` against the on-disk config at dashboard entry. +The badge vocabulary SHALL be: `✓` configured (all checks pass), +`⚠` configured but at least one check warns, `✗` configured but at +least one check errors, and `–` not set / default. Badges SHALL be +recomputed on return from a section editor save. + +#### Scenario: Configured-and-passing section shows checkmark + +- **GIVEN** the Search section is configured with backend `duckduckgo` +- **AND** `ConfigSchemaDoctorCheck` and `SearchBackendDoctorCheck` + both pass +- **WHEN** the dashboard renders +- **THEN** the Search row shows `✓` + +#### Scenario: Configured-and-warning section shows warning glyph + +- **GIVEN** the Search section is configured with backend `brave` and a + rate-limited API key +- **AND** `SearchBackendDoctorCheck` returns WARN +- **WHEN** the dashboard renders +- **THEN** the Search row shows `⚠` + +#### Scenario: Unset section shows dash + +- **GIVEN** the Outbound Webhooks section has no configured webhooks +- **WHEN** the dashboard renders +- **THEN** the Outbound Webhooks row shows `–` + +### Requirement: Sub-grouping by category + +Section editors that declare the same `Category` value SHALL be grouped +visually in the dashboard under that category label. The label itself +SHALL be unselectable; only the editor rows underneath it accept focus. +Grouping SHALL NOT affect the registry's flat enumeration or the audit's +per-editor checks. + +#### Scenario: Chat-channels group renders three siblings + +- **GIVEN** the Slack, Discord, and Mattermost editors declare + `Category = "Chat Channels"` +- **WHEN** the dashboard renders +- **THEN** the three rows render under a "Chat Channels" group label +- **AND** the group label cannot be selected or activated +- **AND** the dashboard registry audit still treats the three as + independent registered editors + +### Requirement: Section editor hosting + +Opening a section from the dashboard SHALL launch the editor's +`IWizardStepViewModel` (produced by `CreateEditor(context)`) inside a +single-step `WizardOrchestrator`. The orchestrator SHALL drive save and +cancel semantics exactly as in the linear wizard, then return control +to the dashboard. The dashboard SHALL refresh the affected section's +status before re-rendering. + +#### Scenario: Open editor, save, return + +- **GIVEN** the dashboard is displayed with the Search row focused +- **WHEN** the operator presses Enter +- **THEN** the Search section editor opens in single-step mode +- **AND** the editor's UI matches the section editor contract (pre-filled + non-secret fields, masked empty secret fields) +- **AND** on Save the orchestrator writes via the merge layer and returns + to the dashboard +- **AND** the dashboard re-renders with the updated Search status badge + +#### Scenario: Open editor, cancel, return without write + +- **GIVEN** the dashboard is displayed with the Search row focused +- **WHEN** the operator opens the editor, changes the backend selector, + and presses Esc +- **THEN** the editor shows the unsaved-changes discard confirm dialog +- **AND** on confirm-discard, control returns to the dashboard +- **AND** no `netclaw.json` write occurred +- **AND** the dashboard re-renders with the unchanged Search status badge + +### Requirement: Doctor blessing on section save + +When a section editor saves, the host SHALL build a candidate merged +config in memory, resolve the editor's `RelevantDoctorChecks`, and run +each check against the candidate. If any check returns ERROR, the +host SHALL block the save, surface an inline error banner, and keep +focus inside the editor. If any check returns WARN (and no ERROR), the +host SHALL render an inline warning banner with a `Save anyway` +affordance and a `Cancel` affordance. If all checks pass, the host +SHALL write the merged candidate to disk and return to the dashboard. + +#### Scenario: Error-level check blocks save + +- **GIVEN** the Search editor is open with backend `brave` selected and + the API key field left blank (no stored key) +- **WHEN** the operator saves +- **THEN** `SearchBackendDoctorCheck` returns ERROR +- **AND** the inline error banner displays the check's message +- **AND** the Save button is disabled until the error condition is + cleared + +#### Scenario: Warn-level check surfaces banner with override + +- **GIVEN** the Skill Feeds editor is open with a feed whose URL is + currently unreachable +- **WHEN** the operator saves +- **THEN** `SkillFeedsDoctorCheck` returns WARN +- **AND** the inline warning banner displays the check's message +- **AND** the host renders `[ Save anyway ]` and `[ Cancel ]` +- **AND** activating Save anyway writes the merged candidate to disk + +#### Scenario: Clean checks write to disk + +- **GIVEN** the Search editor is open with backend `duckduckgo` and no + required API key +- **WHEN** the operator saves +- **THEN** all relevant checks pass +- **AND** the merge writer produces a new `netclaw.json` with only the + Search section changed +- **AND** control returns to the dashboard + +### Requirement: Run full doctor item + +The dashboard SHALL include a "Run full doctor" item at the tail that +invokes `DoctorRunner` against the on-disk config and renders results +on a doctor results page. The results page SHALL list each check's +status (PASS/WARN/ERROR/SKIPPED) with summary text. Pressing Esc or +activating the page's "Back to dashboard" action SHALL return to the +dashboard with no config write performed. + +#### Scenario: Full doctor lists every check + +- **GIVEN** the dashboard is displayed and the daemon-restart status + is irrelevant +- **WHEN** the operator selects "Run full doctor" +- **THEN** `DoctorRunner` runs every registered check against on-disk + config +- **AND** the results page renders one row per check with PASS/WARN/ERROR + status and check name + +#### Scenario: Full doctor does not modify config + +- **GIVEN** the dashboard's "Run full doctor" item runs +- **WHEN** results render and the operator returns to the dashboard +- **THEN** no config file write has occurred +- **AND** the dashboard's per-section status badges reflect the same + on-disk state as before + +### Requirement: Daemon-restart nudge at exit + +`netclaw config` SHALL print a stderr nudge at exit instructing the +operator to restart the daemon for changes to take effect, when (a) at +least one config or secrets write occurred during the session AND (b) +the daemon is currently running. If either condition is false, the +nudge SHALL be omitted. + +#### Scenario: Daemon running plus config change emits nudge + +- **GIVEN** the daemon is running +- **AND** the operator saved at least one section during the session +- **WHEN** the operator quits the dashboard +- **THEN** the stderr nudge `Config saved. Restart the daemon to apply + changes: netclaw daemon stop && netclaw daemon start` is printed +- **AND** the command exits with status 0 + +#### Scenario: Daemon not running suppresses nudge + +- **GIVEN** the daemon is not running +- **AND** the operator saved at least one section during the session +- **WHEN** the operator quits the dashboard +- **THEN** no nudge is printed +- **AND** the command exits with status 0 + +#### Scenario: No writes suppresses nudge regardless of daemon state + +- **GIVEN** the operator opened the dashboard, browsed editors, but + saved nothing +- **WHEN** the operator quits +- **THEN** no nudge is printed regardless of daemon state + +### Requirement: Generic list editor component + +The CLI SHALL provide a generic `ListEditor` Termina component +parameterized by an `IItemEditor` describing the item shape. The +component SHALL render an Add row at the bottom (`+ Add `), an +inline-or-sub-page edit affordance per item depending on +`IItemEditor.RequiresSubPage`, an inline delete affordance keyed to +`d` with single-key confirmation for low-stakes deletes, and overall +Save / Cancel affordances. The list editor SHALL preserve item +identity across edit by consulting `IItemEditor.KeyOf(item)` so that +in-place renames (rather than delete + add) round-trip correctly. + +#### Scenario: Inline edit for simple items + +- **GIVEN** an `ExternalSkills.Sources` list with three path entries +- **WHEN** the operator presses Enter on a focused row +- **THEN** an inline single-line input overlay replaces the row +- **AND** Enter saves the edit to in-memory list state +- **AND** Esc cancels without modifying state + +#### Scenario: Sub-page edit for complex items + +- **GIVEN** an `Notifications.Webhooks` list with two configured + webhooks +- **WHEN** the operator presses Enter on a focused row +- **THEN** a sub-page form opens showing every webhook field +- **AND** Save on the sub-page returns to the list with the in-memory + webhook updated +- **AND** Cancel on the sub-page returns to the list with no change + +#### Scenario: Delete confirmation prevents accidental removal + +- **GIVEN** a focused list item +- **WHEN** the operator presses `d` +- **THEN** an inline `Remove? [y/N]` prompt replaces the row's display +- **AND** pressing `y` removes the item from in-memory state +- **AND** any other key cancels the deletion + +#### Scenario: Item identity preserved on in-place rename + +- **GIVEN** a webhook list with an entry whose `KeyOf` returns + `"critical-pager"` +- **WHEN** the operator edits the entry and changes its name to + `pagerduty-prod` +- **THEN** the list save records a single update (not a delete + add) +- **AND** the underlying `Notifications.Webhooks` array contains exactly + one entry with the new name and the preserved auth header + (per the secret-handling contract) + +### Requirement: Search Provider editor + +The dashboard SHALL include a `SearchSectionEditor` +(`SectionId = "Search"`) for editing the search backend and its +credentials. The editor SHALL present a single-selection list among +`Brave`, `DuckDuckGo`, `SearXng (self-hosted)`. Backend-dependent +fields SHALL render: Brave shows an API key input (secret-handling +contract); SearXng shows an instance URL input; DuckDuckGo shows no +additional fields. The editor SHALL declare `RelevantDoctorChecks` = +`{ConfigSchemaDoctorCheck, SearchBackendDoctorCheck}`. + +#### Scenario: Switching to DuckDuckGo preserves stored Brave key + +- **GIVEN** the Search section is configured with backend `brave` and a + stored Brave API key +- **WHEN** the operator switches the backend to `duckduckgo` and saves +- **THEN** `netclaw.json` records `Search.Backend = "duckduckgo"` +- **AND** `secrets.json` retains the Brave API key encrypted at its + original location + +#### Scenario: Brave without key blocks save + +- **GIVEN** the Search section is unconfigured +- **WHEN** the operator selects `brave`, leaves the key empty, and saves +- **THEN** `SearchBackendDoctorCheck` returns ERROR +- **AND** the save is blocked + +### Requirement: Chat channel editors + +The dashboard SHALL include three independently-registered chat-channel +section editors: `SlackSectionEditor` (`SectionId = "Slack"`), +`DiscordSectionEditor` (`SectionId = "Discord"`), and +`MattermostSectionEditor` (`SectionId = "Mattermost"`). Each editor +SHALL declare `Category = "Chat Channels"` for menu grouping. Each +editor SHALL surface its platform's authentication tokens +(per-platform secret-handling contract), an allowed-channels list, +an allowed-users list, the DMs-enabled toggle, the channel audience +profile selector, and a Test Connection affordance that runs the +existing per-platform probe and renders results in an inline banner. + +#### Scenario: Slack editor exposes both bot and app tokens with leave-blank-to-keep + +- **GIVEN** the Slack section has both bot and app tokens stored +- **WHEN** the operator opens the Slack section editor +- **THEN** both token fields render empty with "configured — leave blank + to keep" hint +- **AND** saving with both fields blank preserves both stored tokens + +#### Scenario: Discord editor exposes single token + +- **GIVEN** the Discord section is unconfigured +- **WHEN** the operator opens the Discord section editor +- **THEN** one token field is displayed with "(not set)" hint +- **AND** no app-token field exists (Discord uses a single bot token) + +#### Scenario: Mattermost editor exposes server URL plus token + +- **GIVEN** the Mattermost section is unconfigured +- **WHEN** the operator opens the Mattermost section editor +- **THEN** a Server URL text field is displayed in addition to the token + field + +#### Scenario: Test Connection renders inline banner + +- **GIVEN** the Slack editor is open with valid tokens entered +- **WHEN** the operator activates Test Connection +- **THEN** the existing Slack probe runs in-process +- **AND** results render in an inline banner with workspace name and + channel-access summary + +### Requirement: Exposure Mode editor + +The dashboard SHALL include an `ExposureModeSectionEditor` +(`SectionId = "Daemon.ExposureMode"`) that lets the operator select +among `Local`, `Reverse Proxy`, `Tailscale`, `Cloudflare Tunnel`. The +editor SHALL surface mode-conditional sub-forms: Reverse Proxy +requires an external base URL plus a trusted-proxy CIDR list; Tailscale +requires an auth-key secret plus hostname; Cloudflare Tunnel requires a +tunnel-token secret plus optional access-policy email domain. The +editor SHALL also surface daemon host and port. `RelevantDoctorChecks` +SHALL include `ConfigSchemaDoctorCheck` and the existing +`ExposureModeDoctorCheck`. + +#### Scenario: Local mode requires no sub-form + +- **GIVEN** the Exposure Mode editor is open with `Local` selected +- **WHEN** the operator saves +- **THEN** `Daemon.ExposureMode = "Local"` is written +- **AND** no trusted-proxy or tunnel configuration is required + +#### Scenario: Reverse Proxy without trusted proxies blocks save + +- **GIVEN** the Exposure Mode editor is open with `Reverse Proxy` + selected +- **AND** the trusted-proxy list is empty +- **WHEN** the operator saves +- **THEN** `ExposureModeDoctorCheck` returns ERROR +- **AND** the save is blocked + +### Requirement: Security Posture editor + +The dashboard SHALL include a `SecurityPostureSectionEditor` +(`SectionId = "Security.Posture"`) presenting `Personal`, `Team`, +`Enterprise` posture choices with descriptive subtitles. When the +operator changes posture and the existing `Tools.AudienceProfiles` +section has been customized away from the prior posture's defaults, +the editor SHALL surface a three-option cascade dialog: cancel, +apply posture with overwrite, or apply posture preserving custom +profiles. + +#### Scenario: Cascade dialog presents three options + +- **GIVEN** the current posture is `Personal` and the Team audience + profile has been customized in `Tools.AudienceProfiles` +- **WHEN** the operator selects `Team` and saves +- **THEN** the cascade dialog opens with default focus on `Cancel` +- **AND** options are: `Cancel — keep current posture`, + `Apply new posture, overwrite profiles`, + `Apply new posture, keep custom profiles` + +#### Scenario: Default focus prevents accidental overwrite + +- **GIVEN** the cascade dialog is open +- **WHEN** the operator presses Enter or Esc +- **THEN** the dialog cancels the posture change +- **AND** `Tools.AudienceProfiles` is unchanged + +### Requirement: Audience Profiles editor + +The dashboard SHALL include an `AudienceProfilesSectionEditor` +(`SectionId = "Tools.AudienceProfiles"`) replacing the init wizard's +feature-selection step. The editor SHALL render an audience picker for +`Personal`, `Team`, `Public`. Opening an audience SHALL display a +per-audience editor with one toggleable row per feature +(`memory`, `search`, `skills`, `scheduling`, `sub-agents`, +`webhooks`), a shell-mode selector for that audience, an approval +policy selector, and a `Reset to posture default` affordance. Arrow +keys SHALL navigate rows; `Space` SHALL toggle the focused checkbox; +`Enter` on a checkbox row SHALL also toggle (alternative to Space). +`RelevantDoctorChecks` SHALL include `ConfigSchemaDoctorCheck` and +`ToolAudienceProfilesDoctorCheck`. + +#### Scenario: Down-arrow then Space toggles second row + +- **GIVEN** the Team audience editor is open +- **AND** initial focus is on the first feature row (`memory`, + currently enabled) +- **WHEN** the operator presses `↓` then `Space` +- **THEN** focus moves to the second row (`search`) +- **AND** the `search` toggle flips (off if it was on, on if it was + off) +- **AND** the change is reflected in `Tools.AudienceProfiles.Team` + when the editor saves + +#### Scenario: Reset to posture default replaces all toggles + +- **GIVEN** the Team audience editor is open with several custom + toggle states +- **WHEN** the operator activates `Reset to posture default` +- **THEN** every toggle and the shell-mode selector revert to the + current posture's default mapping for the Team audience + +### Requirement: Outbound Webhooks editor + +The dashboard SHALL include an `OutboundWebhooksSectionEditor` +(`SectionId = "Notifications.Webhooks"`) presenting the existing +multi-value array via the generic `ListEditor` with the +`WebhookItemEditor` sub-page form. Each webhook SHALL be editable +with name, URL, optional auth-header value (secret-handling contract), +and optional event filter. Add/edit/remove SHALL produce a correctly +merged `Notifications.Webhooks` array. + +#### Scenario: Add second webhook preserves first + +- **GIVEN** `Notifications.Webhooks` contains one entry `ops-alerts` +- **WHEN** the operator opens the editor, adds a new webhook + `critical-pager`, and saves +- **THEN** `Notifications.Webhooks` is a two-entry array +- **AND** the first entry is byte-identical to its pre-save state + +### Requirement: Inbound Webhooks editor + +The dashboard SHALL include an `InboundWebhooksSectionEditor` +(`SectionId = "Webhooks"`) presenting the feature-flag toggle plus +the request-timeout integer field. Route file editing SHALL remain +file-based and out of this editor's scope. `RelevantDoctorChecks` +SHALL include `ConfigSchemaDoctorCheck` and the existing +`InboundWebhookRoutesDoctorCheck`. + +#### Scenario: Enabling inbound webhooks with no routes surfaces warning + +- **GIVEN** `~/.netclaw/config/webhooks/` contains zero route files +- **WHEN** the operator enables inbound webhooks and saves +- **THEN** `InboundWebhookRoutesDoctorCheck` returns WARN +- **AND** the inline warning banner explains routes must be added via + files +- **AND** Save anyway writes `Webhooks.Enabled = true` + +### Requirement: External Skill Directories editor + +The dashboard SHALL include an `ExternalSkillsSectionEditor` +(`SectionId = "ExternalSkills"`) presenting the existing path array +via the generic `ListEditor` with the `PathItemEditor` inline-edit +shape. The editor SHALL validate each path on save: existence, +directory-ness, readability. Errors SHALL render inline below the +relevant row. `RelevantDoctorChecks` SHALL include +`ConfigSchemaDoctorCheck` and the new +`ExternalSkillSourcesDoctorCheck`. + +#### Scenario: Non-existent path blocks save + +- **GIVEN** the External Skills editor is open with a newly-added path + pointing at a non-existent directory +- **WHEN** the operator saves +- **THEN** `ExternalSkillSourcesDoctorCheck` returns ERROR +- **AND** the row renders the error inline +- **AND** the save is blocked + +### Requirement: Skill Feeds editor + +The dashboard SHALL include a `SkillFeedsSectionEditor` +(`SectionId = "SkillFeeds"`) presenting the existing feed array via +the generic `ListEditor` with the `SkillFeedItemEditor` sub-page +form. Each feed SHALL expose name, URL, optional Bearer API key +(secret-handling contract), and a Test Connection affordance. +`RelevantDoctorChecks` SHALL include `ConfigSchemaDoctorCheck` and +the new `SkillFeedsDoctorCheck` (WARN-only on reachability so transient +remote outages do not lock operators out of editing). + +#### Scenario: Unreachable feed surfaces warning but allows save + +- **GIVEN** the Skill Feeds editor is open with a feed pointing at an + unreachable URL +- **WHEN** the operator saves +- **THEN** `SkillFeedsDoctorCheck` returns WARN +- **AND** the inline warning banner displays "feed unreachable" +- **AND** activating Save anyway writes the merged config + +### Requirement: Browser Automation editor + +The dashboard SHALL include a `BrowserAutomationSectionEditor` +(`SectionId = "BrowserAutomation"`) presenting the feature-flag toggle +and a status indicator showing whether Playwright is installed and at +which version. If Playwright is not installed, the toggle SHALL be +disabled and an "Install instructions" sub-page SHALL be reachable +from the editor footer. The installation itself SHALL NOT be invoked +from inside the TUI; the sub-page SHALL print platform-appropriate +shell commands and instruct the operator to re-open the editor after +installing. `RelevantDoctorChecks` SHALL include +`ConfigSchemaDoctorCheck` and the new +`BrowserAutomationDoctorCheck`. + +#### Scenario: Toggle disabled when Playwright absent + +- **GIVEN** the Browser Automation editor is open +- **AND** Playwright is not installed on the host +- **WHEN** the editor renders +- **THEN** the `Browser automation enabled` toggle is disabled +- **AND** the editor footer shows `[ Install instructions → ]` + +#### Scenario: Enabling without Playwright blocks save + +- **GIVEN** the Browser Automation editor is open +- **AND** Playwright is not installed +- **AND** the editor is somehow holding `Enabled = true` (e.g. from a + hand-edited file) +- **WHEN** the operator saves +- **THEN** `BrowserAutomationDoctorCheck` returns ERROR +- **AND** the save is blocked with remediation guidance + +### Requirement: Smoke tape per editor and the no-init refusal + +The smoke-test harness SHALL include a tape per registered section +editor at `tests/smoke/tapes/config-.tape` plus a +matching assertion script at +`tests/smoke/assertions/config-.sh`. The harness +SHALL also include `config-no-init.tape` and its assertion exercising +the refuse-when-no-config path. Each section-editor tape SHALL +pre-stage existing `netclaw.json` and `secrets.json` fixtures, +exercise at least one save round-trip, and the assertion SHALL verify +the modified field changed and all other top-level sections are +byte-identical. + +#### Scenario: Audit fails when an editor lacks a tape + +- **GIVEN** a newly-added `ISectionEditor` registered in the menu +- **AND** no tape file at `tests/smoke/tapes/config-.tape` +- **WHEN** `MenuRegistryAuditTests` runs +- **THEN** the test fails with a message naming the missing tape path + +#### Scenario: Audience tape exercises arrow nav and toggle + +- **WHEN** `config-audience.tape` runs +- **THEN** the tape sends `↓`, `Space`, `↑`, `Space` keystrokes within + the Team audience editor +- **AND** the assertion verifies the per-feature toggle state in + `Tools.AudienceProfiles.Team` + +#### Scenario: No-config refusal exits non-zero + +- **GIVEN** the smoke test harness stages a `NETCLAW_HOME` containing + no `config/netclaw.json` +- **WHEN** `config-no-init.tape` runs `netclaw config` +- **THEN** the command exits with non-zero status +- **AND** the assertion observes the refusal message on stderr diff --git a/openspec/changes/netclaw-config-command/tasks.md b/openspec/changes/netclaw-config-command/tasks.md new file mode 100644 index 000000000..4ff3b3978 --- /dev/null +++ b/openspec/changes/netclaw-config-command/tasks.md @@ -0,0 +1,263 @@ +## 1. OpenSpec planning artifacts and traceability + +- [ ] 1.1 Confirm proposal, design, and spec deltas cover the + `netclaw config` command, the dashboard, ten section editors, the + generic list/item editor framework, the four new doctor checks, the + schema addition for `BrowserAutomation`, twelve smoke tapes plus the + no-init refusal tape, ten round-trip xUnit test classes, and the + hardened menu registry audit. +- [ ] 1.2 Verify traceability references to `PRD-004`, `PRD-001`, and + `PRD-002` across change artifacts. +- [ ] 1.3 Run `openspec validate netclaw-config-command --type change` + and resolve all issues. + +## 2. Schema and configuration types + +- [ ] 2.1 Add a `BrowserAutomation` top-level section to + `src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json` + with `Enabled` (bool, default `false`) and `PlaywrightVersion` + (string, optional). Use `additionalProperties: false`. +- [ ] 2.2 Add `src/Netclaw.Configuration/BrowserAutomationConfig.cs` + matching the schema. +- [ ] 2.3 Update existing exemption list / schema-fix entries as needed + so `SchemaFixResolver` can auto-insert `BrowserAutomation` on + upgrade. + +## 3. Dashboard scaffolding + +- [ ] 3.1 Add `src/Netclaw.Cli/Config/ConfigCommand.cs` as the + top-level command class wired into `Netclaw.Cli.Program` routing. +- [ ] 3.2 Add `src/Netclaw.Cli/Tui/Sections/ConfigDashboardPage.cs` and + `ConfigDashboardViewModel.cs` rendering each `ISectionEditor` from + the registry, plus "Run full doctor" and "Quit" items. +- [ ] 3.3 Implement per-section status badge computation at dashboard + entry (runs each editor's `RelevantDoctorChecks` against on-disk + config and caches results until the editor saves). +- [ ] 3.4 Implement category grouping (siblings sharing `Category` + render under a single unselectable label). +- [ ] 3.5 Implement no-config refusal path: detect missing + `netclaw.json` at startup, print refusal to stderr, exit non-zero. +- [ ] 3.6 Implement daemon-restart nudge: detect running daemon at + exit; print stderr line only when (a) at least one section saved + during the session AND (b) the daemon is running. + +## 4. Generic list/item editor framework + +- [ ] 4.1 Add `src/Netclaw.Cli/Tui/Sections/Components/IItemEditor.cs` + with `DisplayRow`, `KeyOf`, `RequiresSubPage`, + `CreateSubPageEditor`, `EditInline`, `AddInline`. +- [ ] 4.2 Add `src/Netclaw.Cli/Tui/Sections/Components/ListEditor.cs` + implementing add (inline `+ Add` row), edit (inline or sub-page + depending on item editor), remove (single-key `d` then `[y/N]` + prompt), Save / Cancel, in-place rename via `KeyOf` semantics. +- [ ] 4.3 Add `PathItemEditor` (inline string edit; validates path + existence/readability lazily on parent save). +- [ ] 4.4 Add `IdentifierItemEditor` (inline string edit; used by + channel-ID lists, user-ID lists, trusted-proxy CIDR list). +- [ ] 4.5 Add `WebhookItemEditor` (sub-page form: name, URL, optional + auth-header secret-handling, optional event filter). +- [ ] 4.6 Add `SkillFeedItemEditor` (sub-page form: name, URL, + optional Bearer API key secret-handling, Test Connection + affordance). + +## 5. Shared editor components + +- [ ] 5.1 Add `ValidationBanner` component for the inline + errors-and-warnings band above the action row. +- [ ] 5.2 Add `DiscardChangesPrompt` (used on Esc-with-dirty-state in + any editor). +- [ ] 5.3 Add `RemoveCredentialPrompt` (default-Cancel modal confirm + for any secret removal). + +## 6. Section editors — single-value + +- [ ] 6.1 `SearchSectionEditor` (`SectionId = "Search"`): backend + selector + conditional API key / SearXng URL fields. Honor + `ExistingConfig`. `RelevantDoctorChecks`: + `{ConfigSchemaDoctorCheck, SearchBackendDoctorCheck}`. +- [ ] 6.2 `SecurityPostureSectionEditor` + (`SectionId = "Security.Posture"`): three-choice posture list with + cascade dialog (Cancel | Overwrite | Keep custom) when changing + posture over customized `Tools.AudienceProfiles`. +- [ ] 6.3 `AudienceProfilesSectionEditor` + (`SectionId = "Tools.AudienceProfiles"`): audience picker + (Personal | Team | Public) opening per-audience editor with + toggleable feature rows, shell-mode selector, approval policy + selector, and "Reset to posture default" affordance. MUST exercise + arrow nav + Space toggle (#1150 contract). +- [ ] 6.4 `InboundWebhooksSectionEditor` (`SectionId = "Webhooks"`): + feature-flag toggle + request timeout integer. +- [ ] 6.5 `BrowserAutomationSectionEditor` + (`SectionId = "BrowserAutomation"`): feature-flag toggle with + Playwright detection at entry; install-instructions sub-page when + Playwright absent. + +## 7. Section editors — multi-value (compose ListEditor) + +- [ ] 7.1 `OutboundWebhooksSectionEditor` + (`SectionId = "Notifications.Webhooks"`) using + `WebhookItemEditor`. +- [ ] 7.2 `ExternalSkillsSectionEditor` + (`SectionId = "ExternalSkills"`) using `PathItemEditor`. +- [ ] 7.3 `SkillFeedsSectionEditor` (`SectionId = "SkillFeeds"`) using + `SkillFeedItemEditor`. + +## 8. Section editors — chat channels (composite) + +- [ ] 8.1 `SlackSectionEditor` (`SectionId = "Slack"`, + `Category = "Chat Channels"`): bot token + app token, allowed + channels list, allowed users list, DMs toggle, audience profile + selector, Test Connection. Reuses + `channel-audience-tui` cycling component for the channel list. +- [ ] 8.2 `DiscordSectionEditor` (`SectionId = "Discord"`, + `Category = "Chat Channels"`): single bot token, same affordances + otherwise. +- [ ] 8.3 `MattermostSectionEditor` (`SectionId = "Mattermost"`, + `Category = "Chat Channels"`): server URL + bot token, same + affordances otherwise. + +## 9. Section editor — exposure mode (composite) + +- [ ] 9.1 `ExposureModeSectionEditor` + (`SectionId = "Daemon.ExposureMode"`): mode selector (Local | + Reverse Proxy | Tailscale | Cloudflare Tunnel), daemon host/port + fields, mode-conditional sub-forms. +- [ ] 9.2 Reverse Proxy sub-form: external base URL + trusted + proxies list (via `ListEditor` + `IdentifierItemEditor`). +- [ ] 9.3 Tailscale sub-form: auth key (secret) + hostname. +- [ ] 9.4 Cloudflare Tunnel sub-form: tunnel token (secret) + + optional access-policy email domain. + +## 10. New doctor checks + +- [ ] 10.1 `SearchBackendDoctorCheck` (validates backend ↔ required + credential pairing; ERROR when Brave/SearXng configured without + required field). +- [ ] 10.2 `ExternalSkillSourcesDoctorCheck` (validates each path is + an existing readable directory). +- [ ] 10.3 `SkillFeedsDoctorCheck` (validates URL reachability; + WARN-only — transient outages don't block saves). +- [ ] 10.4 `BrowserAutomationDoctorCheck` (ERROR when + `BrowserAutomation.Enabled = true` and Playwright binary not + resolvable from PATH). +- [ ] 10.5 Register each new check via the existing doctor + registration extensions so they participate in + `netclaw doctor` runs. + +## 11. DI wiring + +- [ ] 11.1 Register all ten new editors via + `services.AddSectionEditor()` in the CLI DI composition + root. +- [ ] 11.2 Confirm registry construction fails fast on any duplicate + `SectionId`. +- [ ] 11.3 Wire `ConfigCommand` into the CLI top-level command + dispatch. + +## 12. Round-trip xUnit tests (Layer 2) + +- [ ] 12.1 `SearchSectionEditorTests` covering single-value path and + the DuckDuckGo ↔ Brave backend switch preserves Brave key + scenario. +- [ ] 12.2 `SlackSectionEditorTests` covering reentrancy across + channel-list + user-list + secret-handling for both tokens. +- [ ] 12.3 `DiscordSectionEditorTests`. +- [ ] 12.4 `MattermostSectionEditorTests` (incl. server URL field). +- [ ] 12.5 `ExposureModeSectionEditorTests` covering all four mode + sub-forms. +- [ ] 12.6 `SecurityPostureSectionEditorTests` covering all three + cascade options. +- [ ] 12.7 `AudienceProfilesSectionEditorTests` covering toggle + rount-trip and posture-default reset. +- [ ] 12.8 `OutboundWebhooksSectionEditorTests` covering add / + edit / remove / in-place rename preserves item identity. +- [ ] 12.9 `InboundWebhooksSectionEditorTests`. +- [ ] 12.10 `ExternalSkillsSectionEditorTests` (incl. invalid-path + inline validation). +- [ ] 12.11 `SkillFeedsSectionEditorTests` (incl. WARN-only reachability + behavior). +- [ ] 12.12 `BrowserAutomationSectionEditorTests` (incl. + toggle-disabled-when-absent behavior). + +## 13. Smoke tapes (Layer 1) + +- [ ] 13.1 `config-search.tape` + assertion: pre-stage Brave + key, + switch to DuckDuckGo, save, assert backend=duckduckgo and Brave + key preserved. +- [ ] 13.2 `config-slack.tape` + assertion: pre-stage tokens + 2 + channels, add 1 channel, save, assert 3 channels and tokens + unchanged. +- [ ] 13.3 `config-discord.tape` + assertion. +- [ ] 13.4 `config-mattermost.tape` + assertion (incl. URL + token + + channel). +- [ ] 13.5 `config-exposure-mode.tape` + assertion: pre-stage Local, + switch to Reverse Proxy, add CIDR, save, assert mode and CIDR + changes plus byte-equal unrelated sections. Migrates coverage + from former `init-wizard-reverse-proxy.tape`. +- [ ] 13.6 `config-posture.tape` + assertion: change Personal → + Team, accept cascade, save, assert posture and audience-default + changes. +- [ ] 13.7 `config-audience.tape` + assertion: exercise `↓`, + `Space`, `↑`, `Space` keystrokes on Team audience editor, save, + assert `Tools.AudienceProfiles.Team` toggle state. This tape is + the #1150 regression guard. +- [ ] 13.8 `config-outbound-webhooks.tape` + assertion: pre-stage 1 + webhook, add 2nd via sub-page, save, assert array length 2 and + first byte-identical. +- [ ] 13.9 `config-inbound-webhooks.tape` + assertion. +- [ ] 13.10 `config-external-skills.tape` + assertion: pre-stage 1 + path, add 1 + remove the original via `d`, save, assert single + remaining new entry. +- [ ] 13.11 `config-skill-feeds.tape` + assertion: pre-stage empty, + add 1 feed with Bearer key via sub-page, save, assert feed in + config + key in secrets. +- [ ] 13.12 `config-browser-automation.tape` + assertion: pre-stage + Playwright absent, open install instructions, exit without save, + assert no config write. +- [ ] 13.13 `config-no-init.tape` + assertion: stage empty + `NETCLAW_HOME`, run `netclaw config`, assert non-zero exit and + stderr refusal message. + +## 14. Menu registry audit promotion + +- [ ] 14.1 In `MenuRegistryAuditTests`, flip the smoke-tape + existence check from soft-warn to hard-fail. The test asserts a + matching tape file at `tests/smoke/tapes/config-.tape` + for every registered editor. +- [ ] 14.2 Update the audit's failure-message text to name (a) the + editor's `SectionId`, (b) the missing artifact path, (c) the + remediation step ("add a tape" / "add a test class" / "declare + `RelevantDoctorChecks` or `[NoDoctorChecks]`"). + +## 15. PRD-004 update + +- [ ] 15.1 Update `docs/prd/PRD-004-cli-onboarding-and-config.md`: + replace the "reentrant init dashboard" wording with the + simplified-init + `netclaw config` split. List the ten section + editors as the menu surface. +- [ ] 15.2 Cross-reference issues #455 (closed in Change A) and + #1150 (closed in this change). + +## 16. Quality gates + +- [ ] 16.1 `dotnet build` clean. +- [ ] 16.2 `dotnet test` clean: all round-trip tests pass; audit + passes (every registered editor has tape + test class + doctor + checks); existing tests remain green. +- [ ] 16.3 `./scripts/smoke/run-smoke.sh light` clean (all 12 new + config tapes plus the no-init refusal tape pass). +- [ ] 16.4 `dotnet slopwatch analyze` reports no new violations. +- [ ] 16.5 `./scripts/Add-FileHeaders.ps1 -Verify` reports clean. +- [ ] 16.6 `openspec validate netclaw-config-command --type change` + passes. + +## 17. Documentation + +- [ ] 17.1 Update CLI `--help` text for `netclaw config` so the + command is discoverable from `netclaw --help`. +- [ ] 17.2 Update `feeds/skills/.system/files/netclaw-operations/SKILL.md` + per CLAUDE.md system-skills sync rule, adding a section that + describes `netclaw config` and the ten editable sections. Bump + `metadata.version`. +- [ ] 17.3 PR description closes #1150 and references this OpenSpec + change ID. diff --git a/openspec/changes/section-editor-abstraction/.openspec.yaml b/openspec/changes/section-editor-abstraction/.openspec.yaml new file mode 100644 index 000000000..0f0616986 --- /dev/null +++ b/openspec/changes/section-editor-abstraction/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-23 diff --git a/openspec/changes/section-editor-abstraction/design.md b/openspec/changes/section-editor-abstraction/design.md new file mode 100644 index 000000000..8a48d0dde --- /dev/null +++ b/openspec/changes/section-editor-abstraction/design.md @@ -0,0 +1,213 @@ +## Context + +The `netclaw init` wizard composed of `WizardOrchestrator` + a fixed list of +`IWizardStepViewModel`s produces a runnable Netclaw configuration but treats +the on-disk state as a write-once target. There is no shared abstraction for +"the editable surface of one configuration section," so every section's input +collection, validation, and persistence logic lives inline in its step +viewmodel. Three foundations from PR #432 partially anticipate the shared +abstraction: + +- `WizardContext.ExistingConfig` is declared on the context object but + never populated. +- `ConfigFileHelper` and `ProviderCredentialWriter` already implement the + load-merge-write pattern, used today by `netclaw provider`/`model`/`mcp` + CLI subcommands. +- Each `IWizardStepViewModel.OnEnter(context, direction)` already receives a + direction marker, but no step uses it. + +This change formalizes the shared abstraction so the next change can compose +existing step viewmodels into the new `netclaw config` command without +forking their logic. It also closes the long-standing reentrancy gap (#455): +re-running `netclaw init` over an existing install now produces a sensible +pre-filled wizard with merge-on-save semantics, rather than the prior +undefined behavior. + +## Goals / Non-Goals + +**Goals:** + +- Define `ISectionEditor` such that any step viewmodel implementing it can + be hosted either by the linear init wizard or by a single-step + orchestrator that the next change introduces, with no per-host behavior + difference visible to the user. +- Lock in three operational contracts that future section editors must + honor: reentrancy (pre-fill from `ExistingConfig`), secret handling + (never rehydrate; "leave blank to keep"), and merge-on-save + (byte-equality of every other top-level section). +- Establish the audit + test harness up-front so the contracts are enforced + from the first registered editor, not retrofitted later when drift has + already begun. +- Refactor Provider, Identity, and Posture step viewmodels to implement + `ISectionEditor`. Behavior inside today's linear init wizard remains + observable-equivalent for first-run. +- Close #455 (reentrant init) as a byproduct of populating `ExistingConfig` + at entry and switching `WizardConfigBuilder` to merge-on-save. + +**Non-Goals:** + +- Introducing the `netclaw config` command (next change). +- Adding the remaining seven section editors (next change). +- Simplifying the init wizard's step list to provider + identity + + posture only (third change). +- Hot-reload of the running daemon on config change (out of scope; remains + a documented manual-restart limitation). +- Section editor UI for sections that today are file-edited only + (`Persistence`, `Logging`, `Telemetry`, etc.) — these stay on the + exemption list. +- Reworking `netclaw provider`/`model`/`mcp` CLI subcommands to share + backing logic with the new abstraction. Their existing behavior is + unchanged; future work may unify them. + +## Decisions + +### D1. `ISectionEditor` as a viewmodel factory, not a viewmodel base class + +The interface returns an `IWizardStepViewModel` from `CreateEditor(context)` +rather than extending the existing viewmodel base. This keeps the +orchestrator's lifecycle contract authoritative and avoids multiple +inheritance / diamond issues for step viewmodels that already extend a +shared base. It also lets a single `ISectionEditor` produce different +viewmodels for different contexts in the future (e.g. a future +"compact" view) without changing the interface. + +Alternative considered: make `ISectionEditor` itself extend +`IWizardStepViewModel`. Rejected because it conflates "this thing is a +runnable step" with "this thing describes an editable section in the +registry"; the dashboard and audit code want the metadata without +constructing a runnable step. + +### D2. Merge-on-save via existing `ConfigFileHelper` primitives + +`WizardConfigBuilder` is refactored to call `ConfigFileHelper.LoadConfigFiles` +and `GetOrCreateSection` rather than building a fresh dictionary. The +existing primitives have already been proven by `ProviderCredentialWriter` +and the CLI subcommands; no new merge code is introduced. Each editor +contributes via an explicit `SectionContribution` record carrying +`Dictionary` for non-secrets and +`Dictionary` for secrets. The merge writer applies +the actions deterministically; "blank means X" is the editor's job to +interpret, not the merge layer's. + +Alternative considered: introduce a fresh JSON-patch-style operation log. +Rejected because the existing dictionary-based pattern is already in +production use and a parallel mechanism would introduce a forking point. + +### D3. Secret-presence lookup as a first-class API + +`ConfigFileHelper.SecretPresent(paths, sectionId, key)` is added to satisfy +the "configured / not set" hint without exposing the decrypted value. This +keeps the secret-handling contract enforceable at the type level: editors +that need to show the hint cannot accidentally hold the decrypted value +because the API does not return one. + +Alternative considered: have editors call the secrets protector and discard +the decrypted value after a length check. Rejected because the decrypted +value would still transit through process memory; a presence-only API +guarantees the value is never decrypted at all. + +### D4. Audit walks the menu registry, not the full schema + +`MenuRegistryAuditTests` walks `SectionEditorRegistry.All()`. Schema +sections without a registered editor are not audited unless they appear +in the exemption list. The audit's purpose is to enforce contracts on +editors we ship, not to demand editors for every schema knob; the +exemption list is the explicit "we know about this section and choose +not to expose it" record. + +Alternative considered: walk the schema and require every top-level +section to either have an editor or an exemption. Rejected per planning +discussion: forcing editors for every schema knob produces shallow, +unhelpful UIs for sections nobody edits via TUI. The menu-driven audit +prevents drift on the surfaces we promise to users, which is the failure +mode that actually matters. + +### D5. Refactor exactly three editors in this change + +Provider, Identity, and Posture are the three steps that survive in the +simplified init wizard (third change). Refactoring them here lets us +verify the abstraction end-to-end against real editors without entangling +this change with the larger config-command surface. The remaining seven +editors are introduced as new `ISectionEditor` implementations in the +next change, alongside the dashboard that hosts them. + +Alternative considered: refactor all ten existing init steps at once. +Rejected because it bloats this PR and ties the abstraction's correctness +to behavioral equivalence across far more surface area than necessary to +prove the contract. + +### D6. `ExistingConfig` is `Dictionary`, not strongly typed + +Reuses the type already declared on `WizardContext`. Strongly-typed access +would require introducing a parallel typed view of `netclaw.json`, which +defeats the schema-as-source-of-truth principle. The dictionary form is +also forgiving across schema versions: an unknown key simply doesn't +surface in any editor's slice. + +Alternative considered: bind to typed `*Config` records via +`IConfiguration`. Rejected because the merge step would then need to +re-emit the typed records as JSON, multiplying the round-trip surface +area and introducing per-property null/default ambiguity. + +### D7. `WizardOrchestrator` gets a single-step constructor, not a new class + +Existing orchestration logic (back/forward, dirty tracking, save flow) +already covers the single-step case; we add a constructor and a mode +flag rather than a parallel orchestrator type. This keeps the +orchestrator the single authority on step lifecycle. + +Alternative considered: introduce `SectionEditorRunner` as a separate +host. Rejected because behavior would inevitably drift between two +orchestrators over time. + +## Risks / Trade-offs + +- [Refactor risk] Touching three existing step viewmodels could regress + first-run init behavior. → Mitigation: existing `init-wizard.tape` + smoke test continues to gate every PR. Round-trip xUnit tests added in + this change provide finer-grained protection than the tape alone. + +- [Merge-on-save regressions] If the merge logic loses precision on edge + shapes (`JsonElement` value kinds, nested arrays), unrelated sections + could silently change. → Mitigation: round-trip tests assert + byte-equality of unmodified sections. The existing `ConfigFileHelper` + already handles the JsonElement coercion path; we extend its coverage, + not rewrite it. + +- [Vacuous audit] At the end of this change, the registry contains only + three editors and the audit asserts a small surface. The audit's value + scales with the next change. → Mitigation: the audit is wired now so + that adding any editor in the next change automatically tightens the + enforcement; no follow-up wiring step is required. + +- [Secrets in `ExistingConfig`] The parsed `netclaw.json` may include + schema fields that are themselves sensitive (e.g. allowed user IDs, + email domains). → Mitigation: only `secrets.json` is exempted from + context loading; non-secret PII present in `netclaw.json` is no more + exposed than today. Section editors that render lists of IDs already + display them in clear; this is unchanged. + +- [Schema sections added without registry update] Future schema additions + not in the exemption list and not bound to an editor would fail the + audit immediately on their first PR. → Mitigation: this is the intended + behavior. The exemption list is updated in the same PR that adds the + schema section. + +## Migration Plan + +This change is internal-only and observable behavior is preserved for +first-run init. No data migration is required. The deploy story: + +1. Land this change. `netclaw init` continues to behave identically for + first-run installs; re-runs over existing config now pre-populate + fields and merge on save (previously undefined). +2. The next change introduces `netclaw config`. No further migration + needed. + +Rollback: revert the change. `WizardContext.ExistingConfig` returns to its +declared-but-unused state. `WizardConfigBuilder` returns to overwrite. +First-run behavior is unaffected. + +## Open Questions + +None at execution time. All architectural decisions are locked above. diff --git a/openspec/changes/section-editor-abstraction/proposal.md b/openspec/changes/section-editor-abstraction/proposal.md new file mode 100644 index 000000000..c5c746cea --- /dev/null +++ b/openspec/changes/section-editor-abstraction/proposal.md @@ -0,0 +1,117 @@ +## Why + +Netclaw's `netclaw init` wizard is a linear forward-pass over a hardcoded step +sequence with no reentrancy: re-running it over an existing install is +undefined, and changing one configuration knob requires editing +`netclaw.json` by hand. Existing single-section CLI editors +(`netclaw provider`, `netclaw model`, `netclaw mcp`) prove the load-merge-write +pattern works, but they duplicate logic with the wizard rather than sharing it. +This change introduces the shared abstraction that both the init wizard and a +forthcoming `netclaw config` command (next change) will compose, completes the +long-deferred reentrancy of `netclaw init` (#455), and makes future config +knobs reentrant by construction. + +Source PRDs: `PRD-004-cli-onboarding-and-config.md`, `PRD-001-netclaw-mvp.md`. + +## What Changes + +- Add a `ISectionEditor` interface in `Netclaw.Cli.Tui.Sections`. Each instance + describes one editable configuration section: schema-keyed identity, + dashboard summary, status badge computation, relevant doctor checks, and a + factory that returns a `IWizardStepViewModel` runnable either by the wizard + orchestrator or standalone. +- Add `SectionEditorRegistry`, `SectionStatus`, `SectionContribution` + (carrying explicit `FieldAction` and `SecretAction` per field), and + `SectionEditorExemptions` (documented opt-outs for schema sections that + intentionally have no TUI editor). +- Add a single-step constructor to `WizardOrchestrator` so a section editor can + be run outside the linear wizard with the same lifecycle, save, and cancel + semantics. +- Populate `WizardContext.ExistingConfig` at `netclaw init` entry when an + existing `netclaw.json` is present. Each refactored section editor's + `OnEnter()` pre-fills non-secret fields from its slice. +- Switch `WizardConfigBuilder.WriteConfigFile()` from "build fresh + + overwrite" to "load existing + merge + write," matching the pattern already + used by `ProviderCredentialWriter`. Apply the same load-merge-write rule to + the secrets writer. +- Refactor three existing init step viewmodels — Provider, Identity, + SecurityPosture — to implement `ISectionEditor`. Behavior inside the linear + init wizard is unchanged for first-run; reentrant pre-population is gained + for the next change's config command. +- Establish day-one reentrancy contracts in code: secrets never rehydrate + to screen (masked input with "leave blank to keep" semantics), and + section saves preserve every other top-level section in `netclaw.json` and + `secrets.json` byte-for-byte. +- Add a `MenuRegistryAuditTests` xUnit test that walks the registry and + asserts each registered editor declares non-empty `RelevantDoctorChecks` + (or carries an explicit `[NoDoctorChecks]` justification attribute), has a + registered round-trip test class, and — once the config command lands in + the next change — has a matching smoke tape. In this change the audit runs + vacuously over a registry containing the three refactored editors. +- Add a `SectionEditorTestBase` xUnit harness with shared round-trip + scenarios: `RoundTrip_NoOpEdit_PreservesConfig`, + `RoundTrip_SingleFieldEdit_UpdatesOnlyThatField`, + `Secrets_BlankSubmit_PreservesExistingSecret`, + `Secrets_NonBlankSubmit_ReplacesSecret`, + `Secrets_RemoveAction_DeletesSecret`. Concrete subclasses for the three + refactored editors are included. +- Add `ConfigFileHelper.SecretPresent(paths, section, key)` so editors can + render "configured — leave blank to keep" hints without decrypting the + secret value (#455 contract: never rehydrate secrets to the screen). +- Closes #455 (`netclaw init` reentrancy gap). + +**In scope (MVP):** the abstraction, registry, exemption list, audit and +round-trip test harnesses, single-step orchestrator mode, merge-on-save for +both `netclaw.json` and `secrets.json`, `ExistingConfig` population at init +entry, and refactor of Provider/Identity/Posture to implement the contract. + +**Out of scope:** the new `netclaw config` command itself (next change), the +remaining nine section editors (next change), simplification of the init +wizard step list (third change), and hot-reload of the running daemon on +config changes. + +## Capabilities + +### New Capabilities + +- `section-editor-abstraction`: contract requirements for the reusable + editable-section abstraction — `ISectionEditor`, registry semantics, + reentrancy contract, secret-handling contract, merge-on-save semantics, + and audit obligations for every registered editor. + +### Modified Capabilities + +- `netclaw-onboarding`: `netclaw init` SHALL populate `WizardContext.ExistingConfig` + at entry from on-disk config, and section editors SHALL pre-fill non-secret + fields from it in `OnEnter()` while leaving secret fields empty with the + documented "configured" hint. The wizard's terminal write SHALL be a merge + over existing config, not an overwrite. + +## Impact + +**Affected systems:** + +- CLI init wizard wiring (`Netclaw.Cli.Program`, + `Netclaw.Cli.Tui.Wizard.WizardOrchestrator`, + `Netclaw.Cli.Tui.Wizard.WizardConfigBuilder`, + `Netclaw.Cli.Tui.Wizard.WizardContext`). +- Three init step viewmodels (`ProviderStepViewModel`, `IdentityStepViewModel`, + `SecurityPostureStepViewModel`) gain `ISectionEditor` implementations. +- Config merge helper (`Netclaw.Cli.Config.ConfigFileHelper`) gains + `SecretPresent(...)`. +- New test surface under `tests/Netclaw.Cli.Tests/Tui/Sections/` covering the + abstraction and the three refactored editors. + +**Security and operational impact:** + +- Secrets are never re-rendered to the TUI; the new `SecretPresent` lookup + returns existence only, never the decrypted value. This preserves the + default-deny posture for credential display. +- Merge-on-save replaces overwrite-on-save. The contract guarantee is + byte-equality of all other top-level sections in `netclaw.json` and + `secrets.json`. Round-trip tests enforce the guarantee. +- Re-running `netclaw init` over an existing config is no longer undefined; + in this change the wizard pre-fills fields and merges on save. Explicit + "existing-config refusal" UX lands in the third change. +- No new network surface, no new persistence schema, no new daemon + contract changes. diff --git a/openspec/changes/section-editor-abstraction/specs/netclaw-onboarding/spec.md b/openspec/changes/section-editor-abstraction/specs/netclaw-onboarding/spec.md new file mode 100644 index 000000000..a2b358f09 --- /dev/null +++ b/openspec/changes/section-editor-abstraction/specs/netclaw-onboarding/spec.md @@ -0,0 +1,112 @@ +## ADDED Requirements + +### Requirement: Reentrant init pre-population + +`netclaw init` SHALL load existing `netclaw.json` and `secrets.json` at +entry and assign the parsed top-level dictionary to +`WizardContext.ExistingConfig`. When the wizard runs over an existing +install, every step viewmodel implementing `ISectionEditor` SHALL pre-fill +non-secret UI fields from its slice in `ExistingConfig` and SHALL render +secret-bearing fields empty with the documented hint text indicating +whether the underlying secret is present. Steps that do not implement +`ISectionEditor` SHALL preserve their first-run behavior in this change. + +#### Scenario: Provider step pre-fills from existing config + +- **GIVEN** `netclaw.json` contains a configured `Providers.anthropic` + entry +- **WHEN** `netclaw init` enters the Provider step +- **THEN** the provider list opens with `anthropic` as the focused + selection +- **AND** any API key input renders empty with "configured — leave blank + to keep" hint text +- **AND** the OAuth token expiry date displays as previously stored + +#### Scenario: Identity step pre-fills from existing config + +- **GIVEN** `netclaw.json` contains a previously-set agent name, user + name, and timezone +- **WHEN** `netclaw init` enters the Identity step +- **THEN** each text field opens with the previously-set value as the + default + +#### Scenario: Security Posture step pre-fills from existing config + +- **GIVEN** `netclaw.json` contains a previously-set deployment posture +- **WHEN** `netclaw init` enters the Security Posture step +- **THEN** the posture list opens with the previously-set posture as the + focused selection + +#### Scenario: Fresh install leaves ExistingConfig null + +- **GIVEN** no `netclaw.json` exists on disk +- **WHEN** `netclaw init` enters the wizard +- **THEN** `WizardContext.ExistingConfig` is `null` +- **AND** every step renders its first-run defaults + +### Requirement: Merge-on-save for init wizard + +`netclaw init` SHALL produce its terminal `netclaw.json` write as a merge +of the wizard's accumulated contributions over the existing on-disk file +(or a fresh skeleton when no file exists). For every top-level section +the wizard did not contribute to, the resulting file SHALL be +byte-identical to its pre-write state. The same merge rule SHALL apply +to `secrets.json`. + +#### Scenario: Re-running init preserves unrelated sections + +- **GIVEN** `netclaw.json` contains configured `Slack`, `Discord`, and + `Search` sections +- **AND** `netclaw init` is re-run and only the Provider step is + modified +- **WHEN** the wizard completes and writes +- **THEN** the resulting `netclaw.json` contains the updated `Providers` + section +- **AND** `Slack`, `Discord`, and `Search` are byte-identical to their + pre-write state + +#### Scenario: Re-running init preserves unrelated secrets + +- **GIVEN** `secrets.json` contains a Brave API key and Slack bot/app + tokens +- **AND** `netclaw init` is re-run and only the Provider step's API key + is changed +- **WHEN** the wizard completes and writes +- **THEN** the resulting `secrets.json` contains the new provider API key +- **AND** the Brave API key and Slack tokens are byte-identical to their + pre-write state + +#### Scenario: First-run write produces a complete file + +- **GIVEN** no `netclaw.json` exists on disk +- **WHEN** the wizard completes and writes +- **THEN** the resulting `netclaw.json` contains every section the + wizard contributed to +- **AND** validates against `netclaw-config.v1.schema.json` + +### Requirement: Secrets never rehydrate to the wizard UI + +No step in `netclaw init` SHALL display the decrypted value of any +secret stored in `secrets.json`. Secret-bearing inputs SHALL render +empty masked fields whose hint text indicates whether a value exists, +following the secret-handling contract defined in the +`section-editor-abstraction` capability. + +#### Scenario: Re-run shows stored API key as configured-not-displayed + +- **GIVEN** `secrets.json` contains a stored Brave API key +- **WHEN** `netclaw init` is re-run and reaches a step that would render + the API key field +- **THEN** the field renders empty +- **AND** the hint text reads "configured — leave blank to keep" +- **AND** no part of the decrypted key appears anywhere on screen + +#### Scenario: Re-run with blank submit preserves the stored secret + +- **GIVEN** `secrets.json` contains a stored Brave API key +- **WHEN** `netclaw init` is re-run and the user leaves the API key + field blank and continues +- **THEN** the wizard's terminal write does not rewrite the stored + encrypted value +- **AND** the Brave API key is byte-identical in `secrets.json` + pre-write and post-write diff --git a/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md b/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md new file mode 100644 index 000000000..73dca0abf --- /dev/null +++ b/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md @@ -0,0 +1,326 @@ +## ADDED Requirements + +### Requirement: Section editor interface + +The CLI SHALL define a `ISectionEditor` contract in +`Netclaw.Cli.Tui.Sections` that describes a single editable configuration +section. Each implementation SHALL declare a stable `SectionId` whose value +matches a schema key in `netclaw-config.v1.schema.json` (dotted-path form is +permitted for nested sections such as `Daemon.ExposureMode` and +`Tools.AudienceProfiles`), a user-facing `DisplayName`, an optional +`Category` grouping label, a `GetStatus` method returning +`SectionStatus.{Default, Configured, Warning, Error, Missing}` from current +on-disk config, a secret-redacting `Summary` for dashboard display, a +non-empty `RelevantDoctorChecks` collection (or an explicit +`[NoDoctorChecks]` justification attribute), and a `CreateEditor` +factory that returns an `IWizardStepViewModel`. + +#### Scenario: Editor declares schema-keyed identity + +- **WHEN** a class implements `ISectionEditor` +- **THEN** its `SectionId` resolves to a top-level or dotted-path key in + `netclaw-config.v1.schema.json` +- **AND** the audit (defined under "Menu registry audit") fails if the + identifier resolves to no schema key and the section is not on the + documented exemption list + +#### Scenario: Editor exposes status and summary without decrypting secrets + +- **GIVEN** an `ISectionEditor` whose section owns a secret in `secrets.json` +- **WHEN** the editor produces `GetStatus(...)` and `Summary(...)` +- **THEN** the returned status reflects on-disk configured/default/error + state +- **AND** the summary string contains no secret value or last-N characters + of any secret + +#### Scenario: Editor declares relevant doctor checks + +- **WHEN** a class implements `ISectionEditor` +- **THEN** `RelevantDoctorChecks` contains at least one doctor check type, + OR the implementing class is annotated with + `[NoDoctorChecks(justification: "")]` +- **AND** the audit fails when neither condition holds + +#### Scenario: Editor produces a step viewmodel that the orchestrator can run + +- **GIVEN** an `ISectionEditor` and a `WizardContext` +- **WHEN** `CreateEditor(context)` is invoked +- **THEN** the returned `IWizardStepViewModel` is runnable inside the + existing `WizardOrchestrator` +- **AND** it is also runnable in single-step orchestrator mode (see + "Single-step orchestrator") + +### Requirement: Section editor registry + +The CLI SHALL provide a DI-discovered `SectionEditorRegistry` holding every +registered `ISectionEditor`. Registration SHALL occur via the extension +method `services.AddSectionEditor()`. The registry SHALL expose at +minimum `IReadOnlyList All()` and +`ISectionEditor Get(string sectionId)`. Section identity SHALL be unique +within the registry. + +#### Scenario: Editors are resolved via dependency injection + +- **GIVEN** a DI container with `AddSectionEditor()` + invoked at startup +- **WHEN** the container resolves `SectionEditorRegistry` +- **THEN** `registry.All()` returns a list containing the registered editor +- **AND** `registry.Get("Providers")` returns the same instance + +#### Scenario: Duplicate section identity is rejected + +- **GIVEN** two `ISectionEditor` implementations claiming the same + `SectionId` +- **WHEN** the DI container builds the registry +- **THEN** registry construction fails fast with an exception naming the + duplicate identifier + +### Requirement: Section editor exemption list + +The CLI SHALL maintain a documented exemption list at +`Netclaw.Cli.Tui.Sections.SectionEditorExemptions` enumerating schema +sections that intentionally have no TUI editor. Each entry SHALL carry a +machine-readable category (e.g. "internal-only", "set-once-at-install", +"covered by CLI subcommand", "covered by another editor", "out of MVP +scope"). The exemption list SHALL be the only mechanism by which an +unregistered schema section avoids audit failure. + +#### Scenario: Schema section absent from registry and absent from exemptions + +- **GIVEN** the schema declares a top-level section `Foo` +- **AND** no `ISectionEditor` implementation has `SectionId = "Foo"` +- **AND** `"Foo"` is not present in `SectionEditorExemptions` +- **WHEN** the menu registry audit runs +- **THEN** the audit fails with a message naming the section + +#### Scenario: Schema section in exemption list + +- **GIVEN** the schema declares a top-level section `Persistence` +- **AND** no editor exists for it +- **AND** `"Persistence"` is present in `SectionEditorExemptions` with + category `"set-once-at-install"` +- **WHEN** the audit runs +- **THEN** the audit does not fail for `Persistence` + +### Requirement: Single-step orchestrator mode + +`WizardOrchestrator` SHALL support construction with a single +`IWizardStepViewModel` and a `WizardContext`, running that step +standalone without the linear-wizard step list. `GoNext()` from the step +SHALL invoke save-and-exit semantics; `GoBack()` or `Esc` SHALL invoke +cancel-and-exit semantics. `IsApplicable` filtering and step-to-step +navigation SHALL be skipped in this mode. + +#### Scenario: Single step runs to save + +- **GIVEN** a section editor's step viewmodel and a context +- **WHEN** a `WizardOrchestrator` is constructed in single-step mode +- **AND** the step invokes `GoNext()` +- **THEN** the orchestrator runs the save path +- **AND** returns control to the caller after disk write completes + +#### Scenario: Single step cancels without saving + +- **GIVEN** a section editor in single-step mode +- **WHEN** the step invokes `GoBack()` or the user presses Esc +- **THEN** the orchestrator returns without writing +- **AND** disk state is unchanged + +### Requirement: Reentrancy contract + +Every `ISectionEditor` SHALL honor the following reentrancy contract: +on `OnEnter(context, NavigationDirection.Forward)`, if +`context.ExistingConfig` is non-null, the editor SHALL read its slice +keyed by `SectionId` and pre-fill non-secret UI fields from that slice; +secret-bearing fields SHALL remain empty, with the documented hint text +indicating whether the underlying secret is present. + +#### Scenario: Non-secret fields pre-fill from ExistingConfig + +- **GIVEN** an editor with `SectionId = "Search"` +- **AND** `context.ExistingConfig["Search"]` contains + `{ "Backend": "brave" }` +- **WHEN** the editor's step viewmodel enters in the Forward direction +- **THEN** the backend selector renders with `brave` as the + current/selected value + +#### Scenario: Secret-bearing fields render empty regardless of disk state + +- **GIVEN** an editor with a secret-bearing field whose underlying value is + stored encrypted in `secrets.json` +- **WHEN** the editor enters in the Forward direction +- **THEN** the secret input field renders empty +- **AND** the field hint reads "configured — leave blank to keep" when the + underlying secret exists, or "(not set)" otherwise + +### Requirement: Secret-handling contract + +Section editors SHALL render every secret-bearing field as an empty masked +input. Blank-on-save SHALL preserve the existing encrypted secret value +without rewriting it. Non-blank-on-save SHALL replace the existing value +with the newly entered one. An explicit "Remove credential" action SHALL +be the only path that deletes a secret value from `secrets.json`. Under no +circumstance SHALL the decrypted value of a stored secret be displayed to +the user. + +#### Scenario: Blank submit preserves existing secret + +- **GIVEN** an editor with a secret-bearing field that has a stored value +- **WHEN** the user leaves the field empty and saves +- **THEN** the merge writer records `SecretAction.Preserve` for the field +- **AND** `secrets.json` is byte-identical for that key after the write + +#### Scenario: Non-blank submit replaces stored secret + +- **GIVEN** an editor with a secret-bearing field that has a stored value +- **WHEN** the user enters a new masked value and saves +- **THEN** the merge writer records `SecretAction.Replace(newValue)` +- **AND** `secrets.json` is rewritten with the new encrypted value at the + corresponding key + +#### Scenario: Remove credential deletes stored secret + +- **GIVEN** an editor with a secret-bearing field that has a stored value +- **WHEN** the user activates "Remove credential" and confirms (default + Cancel) +- **THEN** the merge writer records `SecretAction.Remove` +- **AND** the corresponding key is absent from the rewritten `secrets.json` + +### Requirement: Merge-on-save semantics + +Section editors SHALL produce a `SectionContribution` carrying explicit +`FieldAction.{Preserve, Replace, Remove}` per non-secret field and +`SecretAction.{Preserve, Replace, Remove}` per secret field. The merge +writer SHALL load existing `netclaw.json` and `secrets.json` as mutable +dictionaries, apply the contribution's actions to the editor's section, +and write the resulting documents. After a section save, every other +top-level section in both files SHALL be byte-identical to its pre-save +state. + +#### Scenario: Editing one section preserves all others + +- **GIVEN** `netclaw.json` contains sections `Providers`, `Slack`, `Search`, + `ExposureMode` +- **WHEN** the user opens the Search editor, modifies the `Backend` field, + and saves +- **THEN** `Providers`, `Slack`, `ExposureMode` are byte-identical in the + resulting file +- **AND** only `Search` has changed + +#### Scenario: Empty-array semantic distinct from missing key + +- **GIVEN** an editor for a section containing a multi-value list +- **WHEN** the user removes all entries and saves +- **THEN** the resulting `netclaw.json` writes the list as an empty array + `[]` +- **AND** the corresponding schema key is present and not removed + +### Requirement: Existing-config population at init entry + +When `netclaw init` launches, the entry point SHALL load +`netclaw.json` and `secrets.json` via `ConfigFileHelper.LoadConfigFiles` +and assign the parsed `netclaw.json` dictionary to +`WizardContext.ExistingConfig`. Secret values from `secrets.json` SHALL +NOT be loaded into the context; only an existence indicator (via +`ConfigFileHelper.SecretPresent(...)`) SHALL be queryable by editors. + +#### Scenario: First-run leaves ExistingConfig null + +- **GIVEN** no `netclaw.json` exists on disk +- **WHEN** `netclaw init` enters the wizard +- **THEN** `WizardContext.ExistingConfig` is `null` + +#### Scenario: Re-run populates ExistingConfig + +- **GIVEN** `netclaw.json` exists on disk +- **WHEN** `netclaw init` enters the wizard +- **THEN** `WizardContext.ExistingConfig` contains the parsed top-level + dictionary +- **AND** no decrypted secret values are present anywhere in the context + +### Requirement: Secret-presence lookup without decryption + +`ConfigFileHelper` SHALL expose a method +`bool SecretPresent(NetclawPaths paths, string sectionId, string key)` +that returns whether the specified secret key exists in `secrets.json` +without decrypting or returning its value. The method SHALL be the sole +hint source for editors deciding between "configured — leave blank to +keep" and "(not set)" placeholders. + +#### Scenario: Existing secret reports present + +- **GIVEN** `secrets.json` contains an encrypted value at + `Search.BraveApiKey` +- **WHEN** `SecretPresent(paths, "Search", "BraveApiKey")` is invoked +- **THEN** the result is `true` +- **AND** the decrypted value is never materialized in memory by this call + +#### Scenario: Missing secret reports absent + +- **GIVEN** `secrets.json` does not contain a value at + `Search.BraveApiKey` +- **WHEN** `SecretPresent(paths, "Search", "BraveApiKey")` is invoked +- **THEN** the result is `false` + +### Requirement: Round-trip test harness + +The test project SHALL provide an abstract +`SectionEditorTestBase` carrying the canonical shared +reentrancy and merge scenarios: `RoundTrip_NoOpEdit_PreservesConfig`, +`RoundTrip_SingleFieldEdit_UpdatesOnlyThatField`, +`Secrets_BlankSubmit_PreservesExistingSecret`, +`Secrets_NonBlankSubmit_ReplacesSecret`, +`Secrets_RemoveAction_DeletesSecret`. Concrete subclasses SHALL exist for +every registered `ISectionEditor`. + +#### Scenario: Base scenarios are inherited by every concrete subclass + +- **WHEN** a developer adds a new `ISectionEditor` implementation and + registers it +- **THEN** the project will not pass `dotnet test` until a corresponding + subclass of `SectionEditorTestBase` exists +- **AND** the menu registry audit fails when the subclass is missing + +#### Scenario: Round-trip no-op preserves config byte-for-byte + +- **GIVEN** a stocked existing-config fixture +- **WHEN** the editor's step viewmodel runs `OnEnter`, makes no changes, + and saves +- **THEN** the resulting `netclaw.json` and `secrets.json` are + byte-identical to the fixture + +### Requirement: Menu registry audit + +The test project SHALL include `MenuRegistryAuditTests` that walks +`SectionEditorRegistry` and asserts, for every registered editor: a +matching concrete `SectionEditorTestBase` subclass exists, the +editor's `RelevantDoctorChecks` is non-empty (or the class is annotated +with `[NoDoctorChecks]`), and — once smoke tapes ship for the editor in +the next change — a matching tape file exists at +`tests/smoke/tapes/config-.tape`. The audit SHALL +report all failures in one assertion message naming each missing +artifact. + +#### Scenario: Missing round-trip test class fails the audit + +- **GIVEN** a registered `ISectionEditor` without a matching + `SectionEditorTestBase` subclass +- **WHEN** `MenuRegistryAuditTests` runs +- **THEN** the test fails with a message naming the missing test class + +#### Scenario: Empty RelevantDoctorChecks without justification fails the audit + +- **GIVEN** a registered `ISectionEditor` whose `RelevantDoctorChecks` + returns no entries +- **AND** whose class is not annotated with `[NoDoctorChecks]` +- **WHEN** `MenuRegistryAuditTests` runs +- **THEN** the test fails with a message naming the editor + +#### Scenario: Vacuous registry passes the audit + +- **GIVEN** a registry containing only the three Change A editors + (Provider, Identity, Posture) +- **AND** each has a matching round-trip test class and non-empty + `RelevantDoctorChecks` +- **WHEN** `MenuRegistryAuditTests` runs +- **THEN** the audit passes diff --git a/openspec/changes/section-editor-abstraction/tasks.md b/openspec/changes/section-editor-abstraction/tasks.md new file mode 100644 index 000000000..f94028569 --- /dev/null +++ b/openspec/changes/section-editor-abstraction/tasks.md @@ -0,0 +1,163 @@ +## 1. OpenSpec planning artifacts and traceability + +- [ ] 1.1 Confirm proposal, design, and spec deltas cover the + `ISectionEditor` contract, registry, single-step orchestrator mode, + exemption list, secret-handling rules, merge-on-save semantics, + reentrant pre-population, and audit/test harness obligations. +- [ ] 1.2 Verify traceability references to `PRD-004` and `PRD-001` across + change artifacts. +- [ ] 1.3 Run `openspec validate section-editor-abstraction --type change` + and resolve all issues. + +## 2. Core abstraction + +- [ ] 2.1 Add `src/Netclaw.Cli/Tui/Sections/ISectionEditor.cs` with + `SectionId`, `DisplayName`, `Category?`, `GetStatus`, `Summary`, + `RelevantDoctorChecks`, `CreateEditor`. +- [ ] 2.2 Add `src/Netclaw.Cli/Tui/Sections/SectionStatus.cs` with the + `Default | Configured | Warning | Error | Missing` enum. +- [ ] 2.3 Add `src/Netclaw.Cli/Tui/Sections/SectionContribution.cs` with + `FieldAction.{Preserve, Replace, Remove}` and + `SecretAction.{Preserve, Replace, Remove}` discriminated unions plus a + contribution record carrying the per-field dictionaries. +- [ ] 2.4 Add `src/Netclaw.Cli/Tui/Sections/NoDoctorChecksAttribute.cs` + carrying a required `justification` string for editors that genuinely + have no relevant checks. + +## 3. Registry and exemption list + +- [ ] 3.1 Add `src/Netclaw.Cli/Tui/Sections/SectionEditorRegistry.cs` with + `All()` and `Get(string sectionId)` methods. Construction fails fast on + duplicate `SectionId`. +- [ ] 3.2 Add `services.AddSectionEditor()` DI extension on + `IServiceCollection` registering the editor as `ISectionEditor` + (transient) and as itself (for direct test resolution). +- [ ] 3.3 Add `src/Netclaw.Cli/Tui/Sections/SectionEditorExemptions.cs` + with the documented exemption set and per-entry category metadata. +- [ ] 3.4 Wire `SectionEditorRegistry` and the three Change A editors + (Provider, Identity, Posture) into the existing CLI DI composition root. + +## 4. Single-step orchestrator mode + +- [ ] 4.1 Add a single-step constructor to `WizardOrchestrator` accepting + one `IWizardStepViewModel` and a `WizardContext`. +- [ ] 4.2 In single-step mode, `GoNext()` triggers save-and-exit; + `GoBack()` / `Esc` triggers cancel-and-exit. Step-to-step filtering + via `IsApplicable` is skipped. +- [ ] 4.3 Add orchestrator-level unit tests covering save-and-exit and + cancel-and-exit single-step paths. + +## 5. Merge-on-save plumbing + +- [ ] 5.1 Refactor `WizardConfigBuilder.WriteConfigFile` to load existing + `netclaw.json` via `ConfigFileHelper.LoadConfigFiles`, apply each + step's `SectionContribution`, and write the merged dictionary back. + Sections not contributed to remain byte-identical. +- [ ] 5.2 Refactor the wizard's secrets writer to load existing + `secrets.json` and apply each contribution's `SecretAction`s. Blank + on a secret-bearing field maps to `Preserve`; explicit + `SecretAction.Remove` deletes the key. +- [ ] 5.3 Add `ConfigFileHelper.SecretPresent(paths, sectionId, key)` that + inspects `secrets.json` for the key's existence without invoking the + data-protection unprotect path. Unit-test against a fixture with both + present and absent values. +- [ ] 5.4 Update `WizardOrchestrator.WriteConfig` to drive the new merge + path. Existing first-run behavior remains observable-equivalent because + the empty-existing path collapses to the previous overwrite shape. + +## 6. ExistingConfig population at init entry + +- [ ] 6.1 At the `netclaw init` entry point in `Netclaw.Cli.Program`, load + `netclaw.json` via `ConfigFileHelper.LoadConfigFiles` and assign the + parsed dictionary to `WizardContext.ExistingConfig`. Leave secrets out + of the context entirely. +- [ ] 6.2 Remove the "Deferred — not implemented yet" comment block on + `WizardContext.ExistingConfig` and document the populated-at-entry + semantics. +- [ ] 6.3 Confirm the wizard's lifetime owns `ExistingConfig` for the + duration of the run; the dictionary is read-only after entry. + +## 7. Refactor three existing init step viewmodels + +- [ ] 7.1 `ProviderStepViewModel`: implement `ISectionEditor` (SectionId + `Providers`). Honor `ExistingConfig` in `OnEnter(direction)` for + provider type, endpoint, auth method, model selection, and OAuth + token expiry. API key field renders empty with "configured — leave + blank to keep" hint when `SecretPresent` returns true. +- [ ] 7.2 `IdentityStepViewModel`: implement `ISectionEditor` (SectionId + `Identity`). Honor `ExistingConfig` for agent name, user name, + timezone, comm style, workspaces directory, webhook URL. (Step is + trimmed in the third change; this change keeps existing fields.) +- [ ] 7.3 `SecurityPostureStepViewModel`: implement `ISectionEditor` + (SectionId `Security.Posture`, dotted path). Honor `ExistingConfig` + for the posture selection and posture-default cascade. +- [ ] 7.4 Each refactored editor declares non-empty + `RelevantDoctorChecks` referencing the existing checks that scope to + the editor's section. +- [ ] 7.5 Each refactored editor produces a `SectionContribution` from + its viewmodel state on save; the orchestrator collects contributions + and routes them through the new merge writer. + +## 8. Round-trip test harness + +- [ ] 8.1 Add + `tests/Netclaw.Cli.Tests/Tui/Sections/SectionEditorTestBase.cs` + abstract harness with the five canonical scenarios: + `RoundTrip_NoOpEdit_PreservesConfig`, + `RoundTrip_SingleFieldEdit_UpdatesOnlyThatField`, + `Secrets_BlankSubmit_PreservesExistingSecret`, + `Secrets_NonBlankSubmit_ReplacesSecret`, + `Secrets_RemoveAction_DeletesSecret`. +- [ ] 8.2 Concrete test class for `ProviderSectionEditor` covering + provider, endpoint, model, OAuth, and API-key paths. +- [ ] 8.3 Concrete test class for `IdentitySectionEditor`. +- [ ] 8.4 Concrete test class for `SecurityPostureSectionEditor`, + including the posture-cascade write semantics. + +## 9. Menu registry audit + +- [ ] 9.1 Add + `tests/Netclaw.Cli.Tests/Tui/Sections/MenuRegistryAuditTests.cs` with + a single test that walks `SectionEditorRegistry.All()` and asserts: + every registered editor has a `SectionEditorTestBase` + subclass; every editor has non-empty `RelevantDoctorChecks` or + `[NoDoctorChecks]`; and (gated by file existence, no error if absent + in this change) a smoke tape at + `tests/smoke/tapes/config-.tape` exists when present. +- [ ] 9.2 Audit failure message lists all missing artifacts in one + assertion message, naming each editor + missing piece. +- [ ] 9.3 Smoke-tape file existence is checked but not required at the + audit level until the next change lands; comment in the test + documents the cutover. + +## 10. Existing test suite preservation + +- [ ] 10.1 Run `./scripts/smoke/run-smoke.sh init-wizard` and confirm the + existing init-wizard tape passes unchanged. +- [ ] 10.2 Run `./scripts/smoke/run-smoke.sh init-wizard-reverse-proxy` + and confirm the existing reverse-proxy tape passes unchanged. +- [ ] 10.3 Run the full `./scripts/smoke/run-smoke.sh light` and confirm + no regressions. + +## 11. Quality gates + +- [ ] 11.1 `dotnet build` clean across the solution. +- [ ] 11.2 `dotnet test` clean: all new round-trip tests pass; audit + passes vacuously over the three registered editors; existing tests + remain green. +- [ ] 11.3 `dotnet slopwatch analyze` reports no new violations. +- [ ] 11.4 `./scripts/Add-FileHeaders.ps1 -Verify` reports clean. +- [ ] 11.5 `openspec validate section-editor-abstraction --type change` + passes. + +## 12. Documentation and traceability + +- [ ] 12.1 Update `PROJECT_CONTEXT.md` or `TOOLING.md` if the abstraction + changes the way operators or contributors are expected to add editable + sections (a one-liner pointing at `ISectionEditor` is sufficient at + this stage). +- [ ] 12.2 Update PRD-004 with a forward reference to the + `netclaw config` command landing in the next change; this change does + not yet introduce it. +- [ ] 12.3 PR description closes #455 (reentrant init) and references this + OpenSpec change ID. diff --git a/openspec/changes/simplify-netclaw-init/.openspec.yaml b/openspec/changes/simplify-netclaw-init/.openspec.yaml new file mode 100644 index 000000000..0f0616986 --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-23 diff --git a/openspec/changes/simplify-netclaw-init/design.md b/openspec/changes/simplify-netclaw-init/design.md new file mode 100644 index 000000000..31237a60d --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/design.md @@ -0,0 +1,208 @@ +## Context + +The `section-editor-abstraction` change (Change A) refactored Provider, +Identity, and Posture step viewmodels into reentrant `ISectionEditor`s +and switched the wizard's terminal write to merge-on-save. The +`netclaw-config-command` change (Change B) introduced +`netclaw config` and the ten section editors that now own the +configuration surfaces previously walked by the init wizard. With both +changes landed, `netclaw init` is the only piece left that still +treats configuration as a single big linear flow. + +This change trims the wizard to provider + identity + posture so new +operators reach `netclaw chat` after three prompts, and makes the +existing-config-on-re-run behavior explicit (refuse + offer `--force`) +instead of the prior undefined behavior. The wizard's previous +breadth — Slack/Discord/Mattermost setup, ACL, search, browser +automation, MCP servers, exposure mode, channel audience configuration, +feature toggles, external skills, skill feeds, webhook URL — moves +entirely to `netclaw config`. None of those surfaces are deleted; they +just leave the init step list. + +## Goals / Non-Goals + +**Goals:** + +- Reduce time-to-first-chat for new operators: three prompts after + provider selection (provider auth + model selection are part of the + Provider step's existing sub-flow). +- Make re-running `netclaw init` over an existing install a + well-defined operation: refuse with helpful pointers by default, and + offer `--force` for a backed-up reset. +- Preserve the existing posture-default cascade: Personal / Team / + Enterprise still drive the initial `Tools.AudienceProfiles` mapping + written at init time. +- Migrate the reverse-proxy exposure-mode init tape coverage to the + `netclaw config` smoke tape introduced in Change B. + +**Non-Goals:** + +- Deleting any `ISectionEditor` class that lived as an init step. The + classes survive as `netclaw config` editors after Change B. +- Renaming or re-architecting `netclaw config`. +- Changing posture-default mappings. +- Introducing an Identity section editor in `netclaw config`. Renaming + the agent post-install remains a file-edit (or `init --force`) task + for MVP. +- Hot-reload of the running daemon on init completion. + +## Decisions + +### D1. Step list reduced to three; classes preserved + +The init wizard's `WizardOrchestrator` step composition is reduced from +the current 12-entry list to exactly three: Provider, Identity, +Posture. The other `ISectionEditor` implementations (Search, Slack, +Discord, Mattermost, Exposure, AudienceProfiles, OutboundWebhooks, +InboundWebhooks, ExternalSkills, SkillFeeds, BrowserAutomation) remain +registered in the registry and reachable via `netclaw config` — +they're just not part of `netclaw init`'s step list. + +Alternative considered: delete the step viewmodel classes that +weren't on the init list. Rejected because they ARE the section +editors `netclaw config` runs; the same class serves both. Keeping +one class per editable section is the whole point of the +`ISectionEditor` abstraction. + +### D2. Existing-config detection refuses by default, allows `--force` + +Re-running `netclaw init` over an existing install in the current +code is undefined behavior. After Change A's merge-on-save plus +`ExistingConfig` pre-population, a naive re-run would silently +re-walk the wizard and re-write whatever the operator typed. That's +confusing — `netclaw init` is named for "initial setup," not "edit." +The right behavior is: + +- Default: refuse with a clear message pointing at `netclaw config` + for live edits. +- Force: explicit `--force` flag triggers a type-to-confirm backup + and proceeds as a fresh first-run. Backup is rename-aside + (`netclaw.json.bak.`); operators retain manual recovery. + +Alternative considered: have `netclaw init` re-running over existing +config auto-launch `netclaw config`. Rejected because it conflates +two commands; an operator typing `netclaw init` after install +expects setup behavior, not menu-edit behavior. Refusing is clearer. + +### D3. Trimmed Identity step preserves three fields, defaults the rest + +`IdentityStepViewModel`'s field set drops to agent name + user name ++ timezone. The previously-prompted fields (webhook URL, +communication style, workspaces directory) use their existing +defaults and are not exposed in init. Operators wanting to change +them post-install edit `netclaw.json` directly until a future +Identity section editor lands. + +Alternative considered: add a "Show advanced fields" affordance in +the trimmed Identity step. Rejected because it re-introduces the +"long wizard" feel; the explicit out-of-MVP file-edit path is the +right scope discipline. + +### D4. Post-flight nudge in Termina + stderr after teardown + +The post-flight screen inside Termina confirms what was set, reports +health-check pass/fail, and prints the next-step nudge ("Run +`netclaw chat` to start, or `netclaw config` to configure ..."). On +Termina teardown the same one-line nudge prints to stderr so it +remains visible after the TUI clears. This dual-path matches Change +B's daemon-restart nudge pattern. + +Alternative considered: just print the nudge to stderr after exit +without a Termina screen. Rejected because operators benefit from +seeing setup-complete confirmation while the TUI is still up; the +stderr line is a fallback for cases where the operator's terminal +emulator wipes the screen on Termina exit. + +### D5. Reverse-proxy tape migrates to config, not deleted outright + +`init-wizard-reverse-proxy.tape` exercises an exposure-mode flow +that today lives inside the init wizard. With exposure mode moved +to `netclaw config`, the equivalent flow is `config-exposure-mode.tape` +(introduced in Change B). This change deletes the init-side tape +because its coverage is fully owned by the config-side tape. Net +tape count for exposure-mode regression coverage remains 1. + +### D6. New init tapes for refuse-and-force paths + +The refuse path and the `--force` reset path need explicit smoke +coverage, otherwise a future change could regress them silently. +Two new tapes: + +- `init-existing-config-refuse.tape` — pre-stages a config and + asserts refusal text + exit zero on TTY confirm. +- `init-force-reset.tape` — pre-stages a config, runs `--force`, + types `reset` to confirm, completes the short flow, asserts the + .bak files exist and a fresh `netclaw.json` was written. + +Both are short tapes (likely <40 lines each). The new init tape +total is 3 (down from the current 2: one is revised, one is deleted, +two are added). + +### D7. PRD-004 update lands in this change + +PRD-004's "reentrant init dashboard" wording was authored before this +sequence of changes locked the simplified-init + `netclaw config` +split. The wording is updated in this change to match the shipped +shape; cross-references to issues #455 (closed in Change A) and +#1150 (closed in Change B) are added. + +## Risks / Trade-offs + +- [Behavior change for re-runs] Operators who have been + re-running `netclaw init` to tweak config (against the prior + undefined behavior) will be refused after this change. → + Mitigation: the refusal message names `netclaw config` and + `netclaw init --force` explicitly. Documentation update in + PRD-004 references the new behavior. Existing-config detection + is consistent across TTY and non-TTY contexts. + +- [Posture-default writes happen non-interactively now] Operators on + Team or Enterprise postures no longer walk a feature-selection + step at init. They see the defaults applied automatically and can + override per-audience later. → Mitigation: the Change B Audience + Profiles editor is the documented place to tune; PRD-004 names it. + +- [Identity field loss for new installs] New operators no longer + set webhook URL, communication style, or workspaces directory at + init. → Mitigation: defaults are reasonable; webhook URL belongs + in Outbound Webhooks (Change B's section editor); workspaces + directory and communication style are file-edit-only for MVP and + documented as such in PRD-004. + +- [.bak files accumulate on repeated forces] Each `--force` reset + creates a new pair of timestamped .bak files. After many forces + the directory could grow. → Mitigation: this is the operator's + responsibility; the .bak files are theirs to manage. The + type-to-confirm gate ensures forced resets are deliberate, so + accumulation is bounded by intentional operator action. + +- [CI surprise on non-TTY re-runs] Existing CI scripts that called + `netclaw init` non-interactively over a populated config would + silently re-walk previously. After this change they exit non-zero. + → Mitigation: the new behavior is the safe one. Any CI that was + relying on undefined re-run behavior was already buggy; the + non-zero exit makes the breakage visible. Migration is to call + `netclaw config` (programmatic CLI use is via the + CLI subcommands `netclaw provider/model/mcp`, not `netclaw config`). + +## Migration Plan + +1. Land Changes A and B before this change. +2. Land this change. Existing operators on Personal posture: their + re-runs now refuse cleanly. Existing operators on Team or + Enterprise: same. Operators wanting to edit anything use + `netclaw config`; operators wanting a clean slate use + `netclaw init --force`. +3. PRD-004 update is part of this change's PR. +4. The CHANGELOG / release notes call out the simplified-init + behavior change so operators are not surprised on upgrade. + +Rollback: revert this change. The wizard returns to its 12-step +linear form. Existing-config detection disappears (re-runs go back +to undefined behavior). The two new init tapes are deleted; the +init-wizard-reverse-proxy tape returns. `netclaw config` remains +available as long as Change B remains. + +## Open Questions + +None at execution time. All architectural decisions are locked above. diff --git a/openspec/changes/simplify-netclaw-init/proposal.md b/openspec/changes/simplify-netclaw-init/proposal.md new file mode 100644 index 000000000..56b095929 --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/proposal.md @@ -0,0 +1,152 @@ +## Why + +`netclaw init` is the first-impression experience for every new Netclaw +operator, and it has grown into a 12-step linear wizard that walks +through provider selection, security posture, feature selection, +channel pickers and per-channel sub-flows, search backend, browser +automation, identity, external skills, skill feeds, exposure mode, and +a final health check. This is the longest single point of abandonment +for new installs. After the `section-editor-abstraction` change +introduced reentrancy and the `netclaw-config-command` change moved +ongoing configuration to a menu-driven editor, the init wizard's +purpose is now strictly bootstrap: produce a minimum-viable config +that lets the operator reach `netclaw chat` as quickly as possible. +This change cuts the wizard down to three prompts — provider, +identity, posture — and routes operators to `netclaw config` for +everything else. It also makes the existing-config detection behavior +explicit (refuse with a helpful message; offer `--force` for a backed-up +reset) instead of leaving re-runs as undefined behavior. + +Source PRDs: `PRD-004-cli-onboarding-and-config.md`, +`PRD-001-netclaw-mvp.md`. + +## What Changes + +- Trim `netclaw init` to three steps + a terminal write/health-check: + - **Step 1: Provider** — reuse existing `ProviderStepViewModel` + (refactored to `ISectionEditor` in Change A) end-to-end. + - **Step 2: Identity** — trimmed to agent name, user name (what the + agent calls the operator), and timezone. Drop the webhook URL + prompt, the workspaces-directory prompt, and the communication-style + prompt. Defaults remain available for the dropped values. + - **Step 3: Security Posture** — reuse existing + `SecurityPostureStepViewModel` (refactored in Change A). The + posture choice applies the posture-default `Tools.AudienceProfiles` + mapping in-memory before the terminal write; operators tune + per-audience later via `netclaw config → Audience Profiles`. + - **Terminal**: write merged config and run the existing health-check. +- Remove from `netclaw init` the following step viewmodels (the + corresponding `ISectionEditor` implementations introduced in Change B + remain in `netclaw config`): `ChannelPickerStepViewModel`, + `ChannelsStepViewModel`, `FeatureSelectionStepViewModel`, + `SearchStepViewModel`, `SlackStepViewModel`, `DiscordStepViewModel`, + `MattermostStepViewModel`, `ExposureModeStepViewModel`, + `BrowserAutomationStepViewModel`, `ExternalSkillsStepViewModel`, + `SkillFeedsStepViewModel`. The classes are not deleted (they live on + as section editors); only their participation in the init step list + is removed. +- Add a post-flight screen inside Termina that confirms what was set, + reports health-check pass/fail, and points operators at + `netclaw config` for further configuration. On Termina teardown, the + same one-line nudge prints to stderr so it remains visible after the + TUI clears: `Setup complete. Run \`netclaw chat\` to start, or + \`netclaw config\` to configure channels, webhooks, search, and + more.` +- Add explicit existing-config detection at `netclaw init` entry. When + `netclaw.json` exists and `--force` was not passed, the command + renders a refusal screen (TTY) or prints to stderr (non-TTY) + pointing operators at `netclaw config` for edits or + `netclaw init --force` to reset. Exit zero in TTY-confirmed + acknowledgement; exit non-zero in non-TTY usage so CI catches the + surprise. +- Add `netclaw init --force` behavior: when an existing config is + present, the command opens a type-to-confirm backup screen. On + confirm, `netclaw.json` is renamed to `netclaw.json.bak.` + and `secrets.json` is renamed to `secrets.json.bak.`. The + wizard then proceeds as a fresh first-run. Operators must re-enter + credentials; the .bak files are preserved for manual recovery. +- Revise `tests/smoke/tapes/init-wizard.tape` and its assertion + script to exercise the three-step flow (provider + identity + + posture) plus the post-flight screen. The tape shortens from + ~150 lines to ~50. +- Delete `tests/smoke/tapes/init-wizard-reverse-proxy.tape` and its + assertion. Reverse-proxy coverage migrates to + `config-exposure-mode.tape` introduced in Change B. +- Add two new smoke tapes covering the new init UX: + - `init-existing-config-refuse.tape` — pre-stage a `netclaw.json`, + run `netclaw init`, assert refusal message + zero exit. + - `init-force-reset.tape` — pre-stage a `netclaw.json`, run + `netclaw init --force`, type "reset" to confirm, complete the + short flow, assert `.bak.*` files exist and new config is + written. +- Update PRD-004 to reflect the simplified-init + `netclaw config` + shape: the original "reentrant init dashboard" wording is replaced + with the documented two-command split. + +**In scope (MVP):** trimming the wizard to provider + identity + +posture, the post-flight screen and stderr nudge, the existing-config +refusal and `--force` reset paths, revising the existing init tape, +deleting the reverse-proxy init tape, and adding two new init tapes +covering the refuse and force paths. + +**Out of scope:** any behavioral change to `netclaw config` (it +already exists from the previous change); deleting the existing init +step viewmodel classes (they continue to back the section editors in +`netclaw config`); migrating identity-related setup that today lives +inside the trimmed Identity step (workspaces directory, communication +style — these continue to use their existing defaults silently for +MVP; operators wanting to change them edit the file directly until +a future Identity section editor lands); changes to PRD-002 or +posture defaults. + +## Capabilities + +### Modified Capabilities + +- `netclaw-onboarding`: the init wizard's collected inputs SHALL be + trimmed to provider, identity (agent name + user name + timezone), + and security posture. The wizard SHALL detect existing config at + entry and refuse (or offer `--force` reset). The wizard SHALL show + a post-flight screen pointing operators at `netclaw config`. + +## Impact + +**Affected systems:** + +- CLI entry point (`Netclaw.Cli.Program`) gains the existing-config + detection branch and the `--force` flag. +- Init wizard step list (`Netclaw.Cli.Tui.Wizard.WizardOrchestrator` + composition) is reduced to three viewmodels. +- `IdentityStepViewModel` is trimmed (no class removal; field set is + reduced). The viewmodel continues to satisfy the `ISectionEditor` + contract introduced in Change A. +- Init smoke tape (`tests/smoke/tapes/init-wizard.tape`) is rewritten; + reverse-proxy tape is deleted; two new init tapes added. +- PRD-004 is updated to match the simplified-init + `netclaw config` + shape. + +**Security and operational impact:** + +- Existing-config refusal prevents accidental re-runs from blasting + through an existing install. The `--force` path explicitly backs up + both `netclaw.json` and `secrets.json` to timestamped `.bak.*` + files; operators retain a manual recovery path. The force path + requires a type-to-confirm because the operation moves credentials + out of the active file (forcing re-entry). +- Trimming Identity drops the in-wizard webhook URL prompt. The + outbound-webhook surface was already available via `netclaw config → + Outbound Webhooks` (Change B); operators with active webhook + configurations are not affected (their existing webhook entries + remain). Operators on a fresh install no longer set up a webhook + during init; they do so in `netclaw config` post-bootstrap. +- The simplified init reduces the time-to-first-chat for new + operators. No new network surface, no new persistence schema, no + new daemon contract change. +- Posture's audience-profile cascade continues to be applied on init + (Personal posture sets all features enabled; Team and Enterprise + set audience-appropriate defaults). Operators on Team or Enterprise + who used to walk the feature-selection step now get the same + posture-default mapping written non-interactively and can tune via + `netclaw config → Audience Profiles`. +- No change to the daemon. No change to existing CLI subcommands + (`netclaw provider`, `netclaw model`, `netclaw mcp`). diff --git a/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md b/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md new file mode 100644 index 000000000..45baaf1ab --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md @@ -0,0 +1,177 @@ +## MODIFIED Requirements + +### Requirement: Guided onboarding + +`netclaw init` SHALL provide a three-step guided setup collecting LLM +provider configuration, identity (agent name, operator name, timezone), +and security posture. On completion, the wizard SHALL apply the +posture-default `Tools.AudienceProfiles` mapping in-memory, write the +merged config and secrets via the merge-on-save writer, and run the +existing health check to verify the baseline configuration is +functional. If daemon startup fails because configuration validation +rejects the resulting exposure-mode or remote-auth topology, the +wizard SHALL surface that failure as a structured setup error with +remediation guidance. The wizard SHALL NOT collect Slack credentials, +ACL inputs, search backend, browser automation, memory provider, +MCP server configuration, exposure mode, channels, audience-specific +feature flags, external skill directories, skill feeds, or webhook +URLs during this flow. Those sections SHALL be configured via +`netclaw config` after first-run setup completes. + +The wizard SHALL NOT write `AGENTS.md` to disk during identity file +generation. AGENTS.md is binary-controlled firmware loaded from +embedded resources at runtime. The wizard SHALL continue to write +`SOUL.md` and `TOOLING.md` as operator-mutable identity files. + +For non-Personal postures, the wizard SHALL apply the posture-default +feature-flag mapping non-interactively (memory, search, skills, +scheduling, sub-agents, webhooks) per the posture's documented +defaults. The wizard SHALL NOT present a separate feature-selection +step. Operators wanting to override these defaults per-audience SHALL +use `netclaw config → Audience Profiles`. + +#### Scenario: First-time setup + +- **WHEN** operator runs `netclaw init` on a fresh install +- **THEN** the wizard collects provider, identity (agent name, user + name, timezone), and security posture inputs +- **AND** writes a runnable baseline configuration via the merge-on-save + writer +- **AND** writes SOUL.md and TOOLING.md to `~/.netclaw/identity/` +- **AND** does NOT write AGENTS.md (or writes a reference-only stub) +- **AND** does NOT prompt for Slack, ACL, search, browser automation, + exposure mode, channels, audience-feature flags, external skills, + skill feeds, or webhook URLs + +#### Scenario: Identity files written on completion + +- **WHEN** the wizard completes and writes config +- **THEN** `SOUL.md` is written from the embedded SOUL template +- **AND** `TOOLING.md` is written from the embedded TOOLING template +- **AND** `AGENTS.md` is NOT written from a template + +#### Scenario: Posture cascade applied non-interactively + +- **GIVEN** the operator selected `Team` posture +- **WHEN** the wizard completes its terminal write +- **THEN** `Tools.AudienceProfiles.Team` is populated with the + posture-default mapping (memory, search, skills, scheduling, + sub-agents enabled; webhooks disabled per posture rule) +- **AND** the wizard does not show a separate feature-selection step +- **AND** the operator can edit per-audience features via + `netclaw config → Audience Profiles` + +#### Scenario: Exposure-mode startup validation failure shown cleanly + +- **GIVEN** the operator completes `netclaw init` +- **AND** the written configuration causes `ExposureModeValidationService` + to reject daemon startup +- **WHEN** the health-check step starts the daemon +- **THEN** the wizard shows a failed health-check item containing the + validation message +- **AND** the wizard includes remediation guidance for fixing the + exposure/auth configuration +- **AND** the operator is not shown a raw stack trace + +#### Scenario: Startup validation failure does not degrade to generic readiness timeout + +- **GIVEN** daemon startup fails immediately because exposure validation + rejects the configuration +- **WHEN** the health-check step polls daemon readiness +- **THEN** the wizard reports the actual startup validation failure +- **AND** it does NOT report only `Daemon did not become ready` unless + the failure reason is genuinely unavailable + +#### Scenario: Post-flight nudge points to netclaw config + +- **GIVEN** the wizard completes its terminal write successfully +- **WHEN** the health check passes +- **THEN** Termina displays a post-flight screen confirming what was + set +- **AND** Termina displays a line directing the operator at + `netclaw config` for further configuration +- **AND** after Termina teardown the same one-line nudge prints to + stderr so it remains visible after the TUI clears + +## ADDED Requirements + +### Requirement: Existing-config detection at init entry + +`netclaw init` SHALL detect the presence of a previously-written +`netclaw.json` at startup. When detected and `--force` was not passed, +the command SHALL refuse to proceed: in a TTY it renders a refusal +screen pointing operators at `netclaw config` for live edits or +`netclaw init --force` to reset; in non-TTY usage it prints the +refusal to stderr. The TTY path SHALL exit with status 0 after the +operator acknowledges; the non-TTY path SHALL exit with non-zero +status so CI catches the surprise. + +#### Scenario: TTY refusal shows actionable guidance and exits zero + +- **GIVEN** `netclaw.json` exists on disk +- **AND** `netclaw init` is run in an interactive TTY without `--force` +- **WHEN** the command starts +- **THEN** Termina renders a refusal screen that names both alternative + commands: `netclaw config` and `netclaw init --force` +- **AND** the operator presses Enter to acknowledge +- **AND** the command exits with status 0 +- **AND** `netclaw.json` and `secrets.json` are unchanged + +#### Scenario: Non-TTY refusal exits non-zero + +- **GIVEN** `netclaw.json` exists on disk +- **AND** `netclaw init` is run with stdout/stderr redirected (not a TTY) +- **AND** `--force` was not passed +- **WHEN** the command starts +- **THEN** the refusal text prints to stderr +- **AND** the command exits with non-zero status +- **AND** `netclaw.json` and `secrets.json` are unchanged + +#### Scenario: No existing config proceeds normally + +- **GIVEN** no `netclaw.json` exists on disk +- **WHEN** `netclaw init` is run +- **THEN** the wizard proceeds to Step 1 (Provider) without showing the + refusal screen + +### Requirement: Force-reset backup flow + +`netclaw init --force` SHALL detect existing config and require an +explicit type-to-confirm before proceeding. On confirm, the command +SHALL rename `~/.netclaw/config/netclaw.json` to +`netclaw.json.bak.` and +`~/.netclaw/config/secrets.json` to `secrets.json.bak.`. +The wizard SHALL then proceed as a fresh first-run. The .bak files +SHALL be preserved on disk so operators retain a manual recovery +path. The command SHALL print the .bak file paths to the post-flight +screen so operators know where the prior config went. + +#### Scenario: Force without confirm leaves config unchanged + +- **GIVEN** `netclaw.json` exists on disk +- **AND** `netclaw init --force` is run in an interactive TTY +- **WHEN** the confirm screen renders and the operator cancels +- **THEN** the command exits with status 0 +- **AND** `netclaw.json` and `secrets.json` are unchanged + +#### Scenario: Force with confirm backs up and proceeds + +- **GIVEN** `netclaw.json` and `secrets.json` exist on disk +- **AND** `netclaw init --force` is run in an interactive TTY +- **WHEN** the operator types "reset" and confirms +- **THEN** the original `netclaw.json` is renamed to + `netclaw.json.bak.` +- **AND** the original `secrets.json` is renamed to + `secrets.json.bak.` +- **AND** the wizard proceeds to Step 1 (Provider) with + `WizardContext.ExistingConfig` set to `null` +- **AND** on successful completion the post-flight screen lists the + .bak file paths + +#### Scenario: Force on a fresh install behaves as plain init + +- **GIVEN** no `netclaw.json` exists on disk +- **AND** `netclaw init --force` is run +- **WHEN** the command starts +- **THEN** no backup screen is shown (nothing to back up) +- **AND** the wizard proceeds to Step 1 (Provider) normally diff --git a/openspec/changes/simplify-netclaw-init/tasks.md b/openspec/changes/simplify-netclaw-init/tasks.md new file mode 100644 index 000000000..ada4d874f --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/tasks.md @@ -0,0 +1,183 @@ +## 1. OpenSpec planning artifacts and traceability + +- [ ] 1.1 Confirm proposal, design, and spec deltas cover the trimmed + three-step init flow, existing-config refusal, `--force` reset with + backup, post-flight nudge, and the smoke-tape revisions. +- [ ] 1.2 Verify traceability references to `PRD-004` and `PRD-001` + across change artifacts. +- [ ] 1.3 Run `openspec validate simplify-netclaw-init --type change` + and resolve all issues. + +## 2. CLI entry point + +- [ ] 2.1 Update `Netclaw.Cli.Program` `netclaw init` dispatch to + parse the new `--force` flag. Unknown flags produce usage error + and non-zero exit. +- [ ] 2.2 Add existing-config detection at init entry: if + `netclaw.json` exists and `--force` was not passed, branch to the + refusal path (TTY screen vs non-TTY stderr). +- [ ] 2.3 Implement non-TTY refusal: print + `Netclaw is already initialized at . Run \`netclaw config\` + to edit, or \`netclaw init --force\` to reset.` to stderr; exit + with non-zero status. +- [ ] 2.4 Implement TTY refusal: launch Termina with a single-screen + refusal page; default focus on `[ OK ]`; Enter or Esc exits with + status 0. + +## 3. `--force` reset path + +- [ ] 3.1 When `--force` is passed and `netclaw.json` exists, launch + Termina with the type-to-confirm backup screen. The text + acknowledges both `netclaw.json` and `secrets.json` will be moved + aside. +- [ ] 3.2 Default focus on `[ Cancel ]`; the `[ Reset and continue ]` + button is enabled only when the operator types `reset` into the + confirm input. +- [ ] 3.3 On confirm, rename `netclaw.json` → + `netclaw.json.bak.` and `secrets.json` → + `secrets.json.bak.` atomically. Generate timestamp once + per invocation so the two files share a suffix. +- [ ] 3.4 After backup, proceed into the three-step wizard as a fresh + first-run (`WizardContext.ExistingConfig = null`). +- [ ] 3.5 On successful post-flight, list the .bak file paths in the + post-flight screen so the operator knows where the prior config + went. +- [ ] 3.6 `--force` with no existing config silently behaves as plain + `netclaw init` (no backup screen). + +## 4. Wizard step list trim + +- [ ] 4.1 Reduce `WizardOrchestrator`'s init-side step list to exactly + three viewmodels: Provider, Identity, Posture. Health check remains + the terminal step. +- [ ] 4.2 Remove from the init step list (NOT delete the classes): + `ChannelPickerStepViewModel`, `ChannelsStepViewModel`, + `FeatureSelectionStepViewModel`, `SearchStepViewModel`, + `SlackStepViewModel`, `DiscordStepViewModel`, + `MattermostStepViewModel`, `ExposureModeStepViewModel`, + `BrowserAutomationStepViewModel`, `ExternalSkillsStepViewModel`, + `SkillFeedsStepViewModel`. These classes continue to back + `netclaw config` section editors per Change B. +- [ ] 4.3 Verify each removed class is still registered with the DI + container as an `ISectionEditor` so `netclaw config` continues to + resolve them. + +## 5. Identity step trim + +- [ ] 5.1 In `IdentityStepViewModel`, retain only the agent-name, + user-name, and timezone fields when running inside the init step + list. The class's `ISectionEditor` implementation may continue to + expose additional fields for future post-install editing; the init + step's view SHALL omit them. +- [ ] 5.2 Remove from the init wizard's Identity view: webhook URL + prompt, communication-style prompt, workspaces-directory prompt. + Their default values are preserved silently. +- [ ] 5.3 Validate fields per existing rules (agent name required, no + whitespace; user name required; timezone validates against + `TimeZoneInfo.FindSystemTimeZoneById`). + +## 6. Posture cascade write + +- [ ] 6.1 In the Posture step's `ContributeConfig` (or the wizard's + terminal write path), apply the posture-default + `Tools.AudienceProfiles` mapping for the selected posture + (Personal: all features on; Team: per-audience defaults per + posture rule; Enterprise: stricter defaults). +- [ ] 6.2 The cascade SHALL write only `Tools.AudienceProfiles` + entries that the operator has not explicitly customized in + `ExistingConfig`. On fresh first-run `ExistingConfig` is null, so + the full posture default applies. + +## 7. Post-flight screen + +- [ ] 7.1 Add a post-flight Termina page showing: provider summary + ("Anthropic — claude-sonnet-4-6"), identity summary ("Netclaw, + aaron, America/Los_Angeles"), posture, health-check status. +- [ ] 7.2 If health check fails, show the failure message and a + `[ Back to Posture ]` action that returns to the Posture step. +- [ ] 7.3 If health check passes, show a `[ Done ]` action and the + nudge text: + `Run \`netclaw chat\` to start, or \`netclaw config\` to configure + channels, webhooks, search, and more.` +- [ ] 7.4 On Termina teardown after a successful Done, print the same + one-line nudge to stderr so it remains visible after the TUI + clears. +- [ ] 7.5 When `--force` reset was used, append the .bak file paths + to the post-flight screen and stderr. + +## 8. Smoke tape revisions + +- [ ] 8.1 Rewrite `tests/smoke/tapes/init-wizard.tape` to exercise + the three-step flow plus post-flight. Target ≤ 60 lines. +- [ ] 8.2 Rewrite `tests/smoke/assertions/init-wizard.sh` to assert + only the bootstrap fields: provider config, models config, identity + files (`SOUL.md`, `TOOLING.md`), posture, and doctor exit code 0 + or 2. +- [ ] 8.3 Delete `tests/smoke/tapes/init-wizard-reverse-proxy.tape` + and `tests/smoke/assertions/init-wizard-reverse-proxy.sh`. + Reverse-proxy coverage is owned by `config-exposure-mode.tape` + from Change B. + +## 9. New smoke tapes + +- [ ] 9.1 Add `tests/smoke/tapes/init-existing-config-refuse.tape`: + pre-stage a `netclaw.json`, run `netclaw init`, observe the TTY + refusal screen, press Enter to acknowledge, assert exit 0. +- [ ] 9.2 Add `tests/smoke/assertions/init-existing-config-refuse.sh`: + assert the pre-staged config is byte-identical post-run. +- [ ] 9.3 Add `tests/smoke/tapes/init-force-reset.tape`: pre-stage a + `netclaw.json`, run `netclaw init --force`, type `reset`, confirm, + complete the three-step flow, assert post-flight Done. +- [ ] 9.4 Add `tests/smoke/assertions/init-force-reset.sh`: assert + (a) a `netclaw.json.bak.*` file exists with the original content, + (b) the new `netclaw.json` reflects what the tape typed, (c) + doctor exits 0 or 2. + +## 10. Documentation + +- [ ] 10.1 Update `docs/prd/PRD-004-cli-onboarding-and-config.md` to + replace the "reentrant init dashboard" wording with the documented + simplified-init + `netclaw config` split. List the three init steps + and reference `netclaw config` for the rest. +- [ ] 10.2 Cross-reference issues #455 and #1150 in PRD-004's Cross- + References section. +- [ ] 10.3 Update `feeds/skills/.system/files/netclaw-identity/SKILL.md` + (per CLAUDE.md system-skills sync rule) so the agent knows the + trimmed identity field set and the `netclaw config` path for + per-audience editing. Bump `metadata.version`. +- [ ] 10.4 Update CLI `--help` text so `netclaw init --help` documents + the trimmed flow and the `--force` flag. + +## 11. Quality gates + +- [ ] 11.1 `dotnet build` clean. +- [ ] 11.2 `dotnet test` clean: round-trip tests for Provider, + Identity, Posture still pass against the trimmed Identity field + set; menu registry audit passes (all editors registered, tapes + exist, test classes exist). +- [ ] 11.3 `./scripts/smoke/run-smoke.sh init-wizard` passes the + rewritten tape. +- [ ] 11.4 `./scripts/smoke/run-smoke.sh light` passes (incl. the two + new init tapes and the 12 `netclaw config` tapes from Change B). +- [ ] 11.5 `dotnet slopwatch analyze` reports no new violations. +- [ ] 11.6 `./scripts/Add-FileHeaders.ps1 -Verify` reports clean. +- [ ] 11.7 `openspec validate simplify-netclaw-init --type change` + passes. + +## 12. Manual acceptance + +- [ ] 12.1 Fresh install (no `~/.netclaw/`): `netclaw init` reaches + working chat in ≤ 3 prompts after provider selection. Verified by + walking through the wizard manually. +- [ ] 12.2 Re-run init over existing config without `--force`: + refusal screen renders, Enter acknowledges, exit 0, config + unchanged. +- [ ] 12.3 Re-run init over existing config with `--force`: confirm + screen renders, type-to-confirm gate works, .bak files created + with matching timestamps, fresh three-step flow runs, new config + written. +- [ ] 12.4 Non-TTY refusal: `netclaw init > /dev/null 2>&1` over an + existing config exits non-zero. +- [ ] 12.5 PR description references this OpenSpec change ID and + cross-references #455 (closed in Change A) and #1150 (closed in + Change B) as already-closed precedents. From 29593d1906bc71b0250209fb0c5b31e8f7f6599b Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Sat, 23 May 2026 16:22:45 +0000 Subject: [PATCH 2/2] docs: address self-scrutiny review of openspec changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review pass on the three init/config UX changes surfaced real issues. Fixes: UI mockups landed in the repo (were stuck in ~/.claude/plans/): - docs/ui/TUI-002-netclaw-config-wireframes.md (dashboard + 12 editors + 8 page templates + doctor results + nudge) - docs/ui/TUI-003-simplified-init-wireframes.md (3 init steps + post-flight + refusal + force-reset confirm) - Each change's design.md references its corresponding wireframe document as the authoritative visual contract ISectionEditor vs menu split (Change A): - ISectionEditor gains `bool ShowInMenu` flag (default true) - MenuRegistryAuditTests waives tape-existence check for ShowInMenu == false editors (e.g. Provider, Identity covered by init-wizard.tape and the netclaw provider CLI) - Round-trip test + RelevantDoctorChecks contract still applies to every registered editor regardless of ShowInMenu Schema/SectionId mismatches: - Identity is NOT a top-level schema key; added to exemption list with category "synthetic-spans-multiple-sections" and ShowInMenu = false in Change A's tasks - Top-level Security, Daemon, Tools added to exemption list in Change B's tasks with category "covered by another editor's dotted-path SectionId" naming the covering editor - Exemption-list spec scenarios cover both top-level and dotted-path coverage netclaw config show|validate reserved (Change B): - Reserved subcommands now print an explicit "not yet implemented; PRD-004" notice and exit non-zero, preserving the documented future surface (previously rejected as unknown) Important items tightened across the changes: - Change B section editors explicitly REFACTOR existing init step viewmodels (not create duplicates) where the section already has an init step - Daemon-restart nudge now specifies the PID-file + TCP probe with a 250 ms bound; timeout suppresses the nudge (conservative) - In-place rename for list items now specifies originalKey/newKey tracking, secrets-store rekey, and array-position preservation - BrowserAutomation schema-migration scenarios cover both the editor opening over a pre-existing config and doctor --fix auto-insert - --force non-TTY refusal scenario added in Change C - .bak filename collision handled via -1/-2 suffix; timestamp moves from unix-seconds to unix-millis - Multi-instance editing and Test Connection partial-failure shape documented in Change B's design Risks section All three changes re-validated: openspec validate section-editor-abstraction --type change ✓ openspec validate netclaw-config-command --type change ✓ openspec validate simplify-netclaw-init --type change ✓ --- docs/ui/TUI-002-netclaw-config-wireframes.md | 1150 +++++++++++++++++ docs/ui/TUI-003-simplified-init-wireframes.md | 328 +++++ .../changes/netclaw-config-command/design.md | 24 + .../specs/netclaw-cli/spec.md | 34 +- .../specs/netclaw-config-command/spec.md | 61 +- .../changes/netclaw-config-command/tasks.md | 83 +- .../section-editor-abstraction/design.md | 32 + .../specs/section-editor-abstraction/spec.md | 78 +- .../section-editor-abstraction/tasks.md | 30 +- .../changes/simplify-netclaw-init/design.md | 8 + .../specs/netclaw-onboarding/spec.md | 46 +- .../changes/simplify-netclaw-init/tasks.md | 11 +- 12 files changed, 1814 insertions(+), 71 deletions(-) create mode 100644 docs/ui/TUI-002-netclaw-config-wireframes.md create mode 100644 docs/ui/TUI-003-simplified-init-wireframes.md diff --git a/docs/ui/TUI-002-netclaw-config-wireframes.md b/docs/ui/TUI-002-netclaw-config-wireframes.md new file mode 100644 index 000000000..ed3e4854a --- /dev/null +++ b/docs/ui/TUI-002-netclaw-config-wireframes.md @@ -0,0 +1,1150 @@ +# TUI-002: `netclaw config` Wireframes + +Source PRDs: `PRD-004`, `PRD-001`, `PRD-002` + +Backing OpenSpec change: `openspec/changes/netclaw-config-command/` + +Companion: `TUI-001-command-wireframes.md` (init wizard + chat + plain CLI), +`TUI-003-simplified-init-wireframes.md` (the trimmed init flow that ships +alongside `netclaw config`). + +## Overview + +`netclaw config` is a menu-driven Termina TUI command for live configuration +editing. Operators reach every editable section without leaving the terminal, +without re-entering existing secrets, and without hand-editing +`netclaw.json`. Each section editor is reentrant by construction (pre-fills +non-secret fields from on-disk state) and doctor-blessed on save (relevant +checks run against the candidate config before write). + +Twelve editors ship day one: + +| Editor | SectionId | Category | Multi-value | +|-------------------------|------------------------------|-----------------|-------------| +| Search Provider | `Search` | — | no | +| Slack Channels | `Slack` | Chat Channels | partial | +| Discord Channels | `Discord` | Chat Channels | partial | +| Mattermost Channels | `Mattermost` | Chat Channels | partial | +| Exposure Mode | `Daemon.ExposureMode` | — | partial | +| Security Posture | `Security.Posture` | — | no | +| Audience Profiles | `Tools.AudienceProfiles` | — | partial | +| Outbound Webhooks | `Notifications.Webhooks` | — | yes | +| Inbound Webhooks | `Webhooks` | — | no | +| External Skill Dirs | `ExternalSkills` | — | yes | +| Skill Feeds | `SkillFeeds` | — | yes | +| Browser Automation | `BrowserAutomation` | — | no | + +## Termina Component Vocabulary + +All wireframes reference Termina 0.5.1 components (same as TUI-001): + +- **PanelNode** — bordered container with optional title +- **TextInputNode** — single or multi-line text input (masked variant for secrets) +- **SelectionListNode** — keyboard-navigable option list (single or multi-select) +- **TextNode** — static or dynamic text block +- **SpinnerNode** — animated progress indicator (used for Test Connection actions) + +## Conventions + +### Status glyph vocabulary + +| Glyph | Meaning | +|-------|---------| +| `✓` | Section configured, all relevant doctor checks pass | +| `⚠` | Section configured, at least one check returns WARN | +| `✗` | Section configured, at least one check returns ERROR (blocks save) | +| `–` | Section unset / default / disabled | +| `▸` | Currently focused row | + +A footer hint on the dashboard reads: +`✓ ok · ⚠ warning · ✗ error · – not set` + +### Keystroke conventions + +| Key | Effect | +|-----------------|-----------------------------------------------------------------------| +| `↑` / `↓` | Move focus within list | +| `←` / `→` | Move focus across action row (Save / Cancel / etc.) | +| `Tab` / `Shift+Tab` | Move focus across fields in a form | +| `Enter` | Activate focused element (open editor, submit, toggle) | +| `Esc` | Cancel / go back. Confirms discard if section has unsaved changes. | +| `d` | In list editors: delete focused item (with inline `[y/N]` confirm) | +| `q` | Dashboard quit only | +| `Space` | Toggle focused checkbox | + +### Footer hint style + +Every page renders a single-line footer at the bottom listing the relevant +keystrokes for that page. Page-specific. Common combinations defined in the +page templates below. + +### Title bar conventions + +Every page has a single-line title bar at top, framed by the panel border: + +``` +╭─ ───────────────────────────────... +``` + +Sub-pages use a breadcrumb form: + +``` +╭─ Outbound Webhooks › Edit "critical-pager" ──... +``` + +--- + +## Navigation tree + +``` +netclaw config + └── Config.0 Dashboard ◀─ all editors return here on Save/Cancel + ├── Config.1 Search Provider + ├── Config.2 Slack Channels + ├── Config.3 Discord Channels + ├── Config.4 Mattermost Channels + ├── Config.5 Exposure Mode + ├── Config.6 Security Posture + ├── Config.7 Audience Profiles ← addresses #1150 + ├── Config.8 Outbound Webhooks + ├── Config.9 Inbound Webhooks + ├── Config.10 External Skill Directories + ├── Config.11 Skill Feeds + ├── Config.12 Browser Automation + ├── Config.D Run full doctor + └── Quit + +netclaw config (when no netclaw.json exists) + └── Config.E0 Refuse with `netclaw init` pointer ─── exit non-zero +``` + +--- + +## Page templates + +Reusable patterns referenced by the per-editor sections below. + +### T1. Single-value editor (no secret, no sub-pages) + +``` +╭─
───────────────────────────────────────────╮ +│ │ +│ : │ +│ │ +│ │ +│ : │ +│ │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Transitions: +- `Tab` cycles fields. +- `Enter` on Save → run blessing → write or block. +- `Enter` or `Esc` on Cancel → discard-confirm (T7) if dirty → return to dashboard. + +### T2. Multi-value list with inline edits + +``` +╭─
───────────────────────────────────────────╮ +│ │ +│ ▸ │ +│ │ +│ │ +│ │ +│ + Add │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit · d remove · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Transitions: +- `Enter` on an item → inline edit overlay (single-line input). +- `Enter` on `+ Add` → inline empty input overlay. +- `d` on an item → inline `Remove? [y/N]` prompt; `y` removes, anything else cancels. +- `Enter` on Save → write list to schema array → return to dashboard. +- `Esc` on Cancel → discard-confirm if dirty. + +### T3. Multi-value list with sub-page items + +Same as T2 visually. `Enter` on item or `+ Add` opens a sub-page (T4) +instead of inline edit. + +``` +╭─
───────────────────────────────────────────╮ +│ │ +│ ▸ │ +│ │ +│ │ +│ + Add │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit · d remove · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### T4. Item sub-page (form) + +``` +╭─ ──────────────────────────────╮ +│ │ +│ : │ +│ │ +│ │ +│ : │ +│ │ +│ │ +│ [ Save ] [ Cancel ] [ Delete ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +`Delete` button shown only on Edit mode, not Add. Activating it → T5 with +destructive copy. + +Transitions: +- `Save` returns to the parent list with the new/updated item applied to + in-memory state. Disk write happens on the parent's outer `Save`. +- `Cancel` returns to parent list without applying. +- `Delete` opens T5; on confirm, removes from in-memory list, returns to + parent. + +### T5. Confirmation dialog (default-Cancel) + +``` +╭─ ──────────────────────────────────────────╮ +│ │ +│ │ +│ │ +│ ▸ [ Cancel ] [ Yes, ] │ +│ │ +│ Default: Cancel (Esc or Enter) │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Default focus on Cancel. `Enter` or `Esc` cancels. `Tab` + `Enter` on +"Yes" confirms. + +### T6. Inline validation banner + +Rendered above the action row of any editor while doctor blessing finds +issues. ERROR variant: + +``` +│ ╭─ Issues ───────────────────────────────────────────────╮ │ +│ │ ✗ Brave backend requires an API key │ │ +│ │ ⚠ Endpoint TLS certificate expires in 14 days │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ [ Save ] (disabled) [ Cancel ] │ +``` + +WARN-only variant: + +``` +│ ╭─ Warnings ─────────────────────────────────────────────╮ │ +│ │ ⚠ Endpoint TLS certificate expires in 14 days │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ [ Save anyway ] [ Cancel ] │ +``` + +### T7. Unsaved-changes discard confirm + +``` +╭─ Discard changes? ──────────────────────────────────────────╮ +│ │ +│ You have unsaved changes in this section. │ +│ Closing now will lose them. │ +│ │ +│ ▸ [ Keep editing ] [ Discard ] │ +│ │ +│ Default: Keep editing (Esc or Enter) │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Shown when user hits Esc on a section editor with dirty state. + +### T8. Empty list placeholder + +``` +╭─
───────────────────────────────────────────╮ +│ │ +│ (no configured) │ +│ │ +│ ▸ + Add │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ Enter add · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Shown when a list editor opens with zero items. + +--- + +## Config.0 — Dashboard + +``` +╭─ Netclaw Configuration ─────────────────────────────────────╮ +│ │ +│ ▸ Search Provider ✓ Brave │ +│ Chat Channels │ +│ Slack ✓ 3 channels, 2 users │ +│ Discord – not configured │ +│ Mattermost – not configured │ +│ Exposure Mode ✓ Local │ +│ Security Posture ✓ Personal │ +│ Audience Profiles ✓ default │ +│ Outbound Webhooks ⚠ 2 configured, 1 unreachable │ +│ Inbound Webhooks – disabled │ +│ External Skill Dirs ✓ 2 directories │ +│ Skill Feeds – none │ +│ Browser Automation – disabled │ +│ │ +│ ────────── │ +│ Run full doctor │ +│ Quit │ +│ │ +│ ↑/↓ navigate · Enter open · q quit · ✓ ok · ⚠ warn · ✗ err │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Status computation:** on dashboard entry, each editor's +`GetStatus(currentConfig)` runs (with `RelevantDoctorChecks` against +on-disk state). Results cached for the dashboard session; re-computed +when returning from a saved editor. + +**Sub-grouping indentation:** chat-channel rows render at +2 indent under +the "Chat Channels" label. The label itself is unselectable. + +**No "Save dashboard" action:** the dashboard is purely a navigation +layer. All saves are at section granularity. + +### Layout structure + +``` +PanelNode (outer: "Netclaw Configuration") +├── SelectionListNode (single-select; entries from SectionEditorRegistry +│ grouped by Category, plus "Run full doctor" and +│ "Quit" tail items) +└── TextNode (footer hint line) +``` + +--- + +## Config.E0 — No-config refusal + +Rendered when `~/.netclaw/config/netclaw.json` is missing at launch. + +``` +╭─ No Netclaw configuration found ────────────────────────────╮ +│ │ +│ No configuration file at: │ +│ ~/.netclaw/config/netclaw.json │ +│ │ +│ Run `netclaw init` to create one. │ +│ │ +│ [ OK ] │ +│ │ +│ Enter exit │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Non-interactive (when stdout is not a TTY, e.g. CI): prints +`No configuration found. Run \`netclaw init\` first.` to stderr and exits +non-zero. The interactive variant exits zero after acknowledgement. + +--- + +## Config.1 — Search Provider + +### 1.1 Main editor + +``` +╭─ Search Provider ───────────────────────────────────────────╮ +│ │ +│ Backend: │ +│ ▸ Brave (current) │ +│ DuckDuckGo │ +│ SearXng (self-hosted) │ +│ │ +│ Brave API key: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ SearXng instance URL: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ (not applicable — only required for SearXng) │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ [ Save ] [ Cancel ] [ Remove credential ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Field conditionality:** Brave API key disabled when backend ≠ Brave; +SearXng URL disabled when backend ≠ SearXng; DuckDuckGo has no fields. + +**Reentrancy:** Backend selector pre-fills from current config. API key +field is empty regardless; hint indicates "configured" or "not set" +based on `ConfigFileHelper.SecretPresent(...)`. + +### 1.2 Remove credential confirm (T5) + +``` +╭─ Remove Brave API key? ─────────────────────────────────────╮ +│ │ +│ This deletes your Brave API key from secrets.json. │ +│ Search will fall back to DuckDuckGo unless you set a new │ +│ key. You can re-enter at any time. │ +│ │ +│ ▸ [ Cancel ] [ Yes, remove ] │ +│ │ +│ Default: Cancel (Esc or Enter) │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Doctor checks** (`RelevantDoctorChecks`): `ConfigSchemaDoctorCheck`, +`SearchBackendDoctorCheck`. + +--- + +## Config.2 — Slack Channels + +### 2.1 Main editor + +``` +╭─ Slack Channels ────────────────────────────────────────────╮ +│ │ +│ Bot token: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ App token (Socket Mode): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ Allowed channels: 3 configured → │ +│ Allowed users: 2 configured → │ +│ DMs enabled: [ X ] yes │ +│ Audience profile: Personal │ +│ │ +│ [ Save ] [ Cancel ] [ Test connection ] │ +│ [ Remove credentials ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Sub-pages: +- "Allowed channels" → 2.2 list editor. +- "Allowed users" → 2.3 list editor. + +### 2.2 Allowed channels list (T2) + +``` +╭─ Slack Channels › Allowed channel IDs ──────────────────────╮ +│ │ +│ ▸ C01ABCDE │ +│ C01FGHIJ │ +│ C01KLMNO │ +│ │ +│ + Add channel ID │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit · d remove · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +`Save` here is "apply to in-memory state and return to 2.1." Disk write +happens when 2.1 itself saves. + +### 2.3 Allowed users list + +Same shape as 2.2 with user IDs. Uses `IdentifierItemEditor`. + +### 2.4 Test connection (inline banner) + +Runs the existing Slack probe logic from `SlackStepViewModel`; result +rendered in an inline banner above the action row: + +``` +│ ╭─ Connection test ──────────────────────────────────────╮ │ +│ │ ✓ Bot token valid (workspace: petabridge) │ │ +│ │ ✓ Socket Mode app token valid │ │ +│ │ ✓ Bot has access to 3 of 3 configured channels │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +``` + +Failure shape: + +``` +│ ╭─ Connection test ──────────────────────────────────────╮ │ +│ │ ✗ Bot token invalid: 401 invalid_auth │ │ +│ │ Check `xoxb-` token in the Slack app config │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +``` + +Test results never modify config; they're advisory before Save. + +### 2.5 Remove credentials confirm (T5) + +``` +╭─ Remove Slack credentials? ─────────────────────────────────╮ +│ │ +│ This deletes both the Slack bot token and the Socket │ +│ Mode app token from secrets.json. Slack will be │ +│ disconnected until you re-enter both. Allowed channels │ +│ and users are preserved in netclaw.json. │ +│ │ +│ ▸ [ Cancel ] [ Yes, remove ] │ +│ │ +│ Default: Cancel (Esc or Enter) │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `SlackAuthDoctorCheck`, +`SlackAclDoctorCheck`. + +--- + +## Config.3 — Discord Channels + +Structurally identical to 2.x except: +- Single token field (bot token only; no app token). +- Otherwise: allowed channels list, allowed users list, DMs toggle, + audience profile, test connection, remove credentials. + +(Layouts identical to 2.1–2.5 with the App token row removed.) + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `DiscordAuthDoctorCheck`. + +--- + +## Config.4 — Mattermost Channels + +Structurally identical to 2.x plus: +- `Server URL` text field at the top. +- Same token, channels, users, DMs, audience profile, test connection, + remove credentials. + +``` +╭─ Mattermost Channels ───────────────────────────────────────╮ +│ │ +│ Server URL: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ https://chat.example.com │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ Bot token: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ Allowed channels: 5 configured → │ +│ Allowed users: 3 configured → │ +│ DMs enabled: [ X ] yes │ +│ Audience profile: Team │ +│ │ +│ [ Save ] [ Cancel ] [ Test connection ] │ +│ [ Remove credentials ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `MattermostAuthDoctorCheck`. + +--- + +## Config.5 — Exposure Mode + +### 5.1 Mode selection + +``` +╭─ Exposure Mode ─────────────────────────────────────────────╮ +│ │ +│ How is Netclaw reachable from outside the host? │ +│ │ +│ ▸ Local │ +│ 127.0.0.1 only. No external exposure. │ +│ │ +│ Reverse Proxy │ +│ Behind nginx/Caddy/etc. Trusted proxies required. │ +│ │ +│ Tailscale │ +│ Auth via Tailscale identity. Mesh network required. │ +│ │ +│ Cloudflare Tunnel │ +│ Cloudflare access-protected. Tunnel credentials needed. │ +│ │ +│ ────── │ +│ Daemon host: 127.0.0.1 │ +│ Daemon port: 5199 │ +│ │ +│ [ Configure mode → ] [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Tab to buttons · Enter activate │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Conditionality:** "Configure mode →" button is enabled only when +selected mode requires sub-config (Reverse Proxy, Tailscale, Cloudflare). +Local has no sub-config. + +### 5.2 Reverse Proxy sub-form (T1-shaped) + +``` +╭─ Exposure Mode › Reverse Proxy ─────────────────────────────╮ +│ │ +│ External base URL (must be HTTPS): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ https://netclaw.example.com │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ Trusted proxies (CIDR list): 2 configured → │ +│ │ +│ [ Apply ] [ Cancel ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Trusted proxies row → 5.5 list editor. + +### 5.3 Tailscale sub-form + +``` +╭─ Exposure Mode › Tailscale ─────────────────────────────────╮ +│ │ +│ Tailscale auth key: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ Hostname on tailnet: netclaw │ +│ │ +│ [ Apply ] [ Cancel ] [ Remove auth key ] │ +│ │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 5.4 Cloudflare Tunnel sub-form + +``` +╭─ Exposure Mode › Cloudflare Tunnel ─────────────────────────╮ +│ │ +│ Tunnel token: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ Access policy email domain (optional): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ [ Apply ] [ Cancel ] [ Remove tunnel token ] │ +│ │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 5.5 Trusted proxies list (T2 with `IdentifierItemEditor`) + +``` +╭─ Exposure Mode › Trusted Proxies ───────────────────────────╮ +│ │ +│ ▸ 10.0.0.0/8 │ +│ 192.168.1.0/24 │ +│ │ +│ + Add CIDR │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit · d remove · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `ExposureModeDoctorCheck`. + +--- + +## Config.6 — Security Posture + +### 6.1 Posture selection (T1-shaped) + +``` +╭─ Security Posture ──────────────────────────────────────────╮ +│ │ +│ Current posture: Personal │ +│ │ +│ ▸ Personal │ +│ Just me. Local-only by default. Tools have wide access. │ +│ │ +│ Team │ +│ Small team via Slack/Discord. Audience-restricted tools. │ +│ │ +│ Enterprise │ +│ Production deployment. Strict audience profiles, audit. │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Tab to buttons · Enter activate │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 6.2 Cascade warning (T5 variant — three options) + +Shown only when changing posture AND `Tools.AudienceProfiles` has been +customized away from the prior posture's defaults. + +``` +╭─ Posture change affects Audience Profiles ──────────────────╮ +│ │ +│ You have customized Audience Profiles. Changing posture │ +│ will overwrite them with the new posture's defaults. │ +│ │ +│ ▸ [ Cancel — keep current posture ] │ +│ [ Apply new posture, overwrite profiles ] │ +│ [ Apply new posture, keep custom profiles ] │ +│ │ +│ Default: Cancel (Esc or Enter) │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `SecurityPolicyDoctorCheck`. + +--- + +## Config.7 — Audience Profiles *(addresses #1150)* + +### 7.1 Audience selection + +``` +╭─ Audience Profiles ─────────────────────────────────────────╮ +│ │ +│ Configure tool access per audience tier. │ +│ │ +│ ▸ Personal ✓ Default for posture: Personal │ +│ Team ✓ Default for posture: Personal │ +│ Public ✓ Default for posture: Personal │ +│ │ +│ ────── │ +│ │ +│ Shell mode (global): HostAllowed │ +│ │ +│ [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit audience · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 7.2 Per-audience editor + +``` +╭─ Audience Profiles › Team ──────────────────────────────────╮ +│ │ +│ Tools enabled for the Team audience: │ +│ │ +│ [ X ] memory │ +│ [ X ] search │ +│ [ X ] skills │ +│ [ ] scheduling │ +│ [ X ] sub-agents │ +│ [ ] webhooks │ +│ │ +│ Shell mode for Team: SandboxOnly │ +│ Approval policy: Required │ +│ │ +│ [ Save ] [ Cancel ] [ Reset to posture default ] │ +│ │ +│ ↑/↓ navigate · Space toggle · Tab to buttons · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Key bindings critical to #1150:** + +- `↑` / `↓` MUST move focus between toggle rows. +- `Space` MUST toggle the focused checkbox. +- `Enter` on a checkbox row also toggles (alternative to Space). +- `Tab` moves to the action row. +- `Reset to posture default` replaces all toggles + shell mode with the + posture-default mapping. + +The `config-audience.tape` smoke tape explicitly exercises `↓`, `Space`, +`↑`, `Space` to lock in the keystroke contract. Regression in arrow +nav OR toggle is caught. + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `ToolAudienceProfilesDoctorCheck`. + +--- + +## Config.8 — Outbound Webhooks + +### 8.1 List page (T3) + +``` +╭─ Outbound Webhooks ─────────────────────────────────────────╮ +│ │ +│ ▸ ops-alerts ✓ healthy │ +│ critical-pager ⚠ unreachable last 3 attempts │ +│ │ +│ + Add webhook │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit · d remove · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Empty-state (T8): + +``` +╭─ Outbound Webhooks ─────────────────────────────────────────╮ +│ │ +│ (no webhooks configured) │ +│ │ +│ ▸ + Add webhook │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ Enter add · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 8.2 Add/edit form (T4) + +``` +╭─ Outbound Webhooks › Edit "critical-pager" ─────────────────╮ +│ │ +│ Name: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ critical-pager │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ URL: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ https://events.pagerduty.com/v2/enqueue │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ Auth header (e.g. "Authorization: Bearer ..."): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ Event filter (optional, comma-separated): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ session.error,session.compaction │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ [ Save ] [ Cancel ] [ Delete webhook ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 8.3 Delete confirm (T5) + +``` +╭─ Remove webhook "critical-pager"? ──────────────────────────╮ +│ │ +│ This webhook will be removed from Notifications.Webhooks. │ +│ Any stored auth header for it will be deleted. │ +│ │ +│ ▸ [ Cancel ] [ Yes, remove ] │ +│ │ +│ Default: Cancel (Esc or Enter) │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `WebhookFormatDoctorCheck`. + +--- + +## Config.9 — Inbound Webhooks + +``` +╭─ Inbound Webhooks ──────────────────────────────────────────╮ +│ │ +│ Inbound webhooks let external systems trigger Netclaw │ +│ via signed HTTP requests. Routes are defined per webhook │ +│ under ~/.netclaw/config/webhooks/*.json (file-edited). │ +│ │ +│ [ X ] Inbound webhooks enabled │ +│ │ +│ Request timeout (seconds): 30 │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ Tab next · Space toggle · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Note:** route file editing remains file-based; this editor only +toggles the feature and sets the timeout. If user enables this flag +but no routes exist, `InboundWebhookRoutesDoctorCheck` (existing) +surfaces the empty-routes condition — per CLAUDE.md "fail loudly," +we do NOT silently default to dummy routes. + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `InboundWebhookRoutesDoctorCheck`. + +--- + +## Config.10 — External Skill Directories + +### 10.1 List page (T2 with `PathItemEditor`) + +``` +╭─ External Skill Directories ────────────────────────────────╮ +│ │ +│ ▸ ~/.claude/skills │ +│ ~/work/team-skills │ +│ ~/personal-skills │ +│ │ +│ + Add directory │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit · d remove · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Empty state per T8. + +### 10.2 Inline add/edit overlay + +``` +│ ~/work/team-skills │ +│ ╭─ Edit directory ───────────────────────────────────────╮ │ +│ │ ~/personal-skills_ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ [Enter] save · [Esc] cancel │ +``` + +Renders as an overlay row replacing the focused item. Validates: path +exists, is a directory, is readable. Errors render inline below the +input row. + +### 10.3 Inline delete confirm + +When `d` pressed on a focused item: + +``` +│ ▸ ~/.claude/skills Remove? [y/N] │ +``` + +Single-keypress. `y` removes; anything else cancels. No modal. + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `ExternalSkillSourcesDoctorCheck`. + +--- + +## Config.11 — Skill Feeds + +### 11.1 List page (T3 with `SkillFeedItemEditor`) + +``` +╭─ Skill Feeds ───────────────────────────────────────────────╮ +│ │ +│ ▸ corp-internal-feed ✓ reachable │ +│ legacy-feed ✗ 403 forbidden │ +│ │ +│ + Add feed │ +│ │ +│ [ Save ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Enter edit · d remove · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 11.2 Add/edit form (T4) + +``` +╭─ Skill Feeds › Edit "corp-internal-feed" ───────────────────╮ +│ │ +│ Name: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ corp-internal-feed │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ Feed URL: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ https://skills.internal.corp/manifest.json │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ API key (Bearer token, optional): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ (configured — leave blank to keep) │ +│ │ +│ [ Save ] [ Cancel ] [ Test connection ] │ +│ [ Delete feed ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 11.3 Delete confirm (T5) + +``` +╭─ Remove feed "legacy-feed"? ────────────────────────────────╮ +│ │ +│ This feed will be removed from SkillFeeds.Feeds. Any │ +│ stored Bearer token for it will be deleted. │ +│ │ +│ ▸ [ Cancel ] [ Yes, remove ] │ +│ │ +│ Default: Cancel (Esc or Enter) │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `SkillFeedsDoctorCheck` +(WARN-only — transient outages don't block saves). + +--- + +## Config.12 — Browser Automation + +### 12.1 Status & toggle (Playwright not installed) + +``` +╭─ Browser Automation ────────────────────────────────────────╮ +│ │ +│ Headless browser support via Playwright. Used by the │ +│ `browser` tool for web scraping and form interaction. │ +│ │ +│ Status: Playwright not installed │ +│ │ +│ [ ] Browser automation enabled │ +│ (cannot enable until Playwright is installed) │ +│ │ +│ [ Install instructions → ] [ Cancel ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 12.2 Status & toggle (Playwright installed) + +``` +╭─ Browser Automation ────────────────────────────────────────╮ +│ │ +│ Status: Playwright installed (v1.42.0) │ +│ │ +│ [ X ] Browser automation enabled │ +│ │ +│ [ Save ] [ Cancel ] [ Uninstall instructions → ] │ +│ │ +│ Tab next · Space toggle · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +### 12.3 Install instructions sub-page + +``` +╭─ Browser Automation › Install Playwright ───────────────────╮ +│ │ +│ Playwright is not currently installed. To install: │ +│ │ +│ 1. Run: │ +│ dotnet tool install --global Microsoft.Playwright.CLI│ +│ │ +│ 2. Then: │ +│ playwright install chromium │ +│ │ +│ After installation, return to this editor and re-open to │ +│ detect the installation. │ +│ │ +│ [ OK ] │ +│ │ +│ Enter exit │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Why not shell out to install:** installing global tooling from a TUI +is too magical and platform-fragile. Print instructions; let the user +run them in their shell. Detection on re-open is automatic +(`BrowserAutomationDoctorCheck` resolves `playwright` from PATH at +editor entry). + +**Doctor checks:** `ConfigSchemaDoctorCheck`, `BrowserAutomationDoctorCheck`. + +--- + +## Config.D — Run full doctor + +``` +╭─ Doctor — full configuration check ─────────────────────────╮ +│ │ +│ ✓ ConfigSchema OK │ +│ ✓ Providers OK │ +│ ✓ Models OK │ +│ ⚠ Search Brave API key valid but rate- │ +│ limited per recent probes │ +│ ✓ Slack OK │ +│ – Discord Not configured │ +│ – Mattermost Not configured │ +│ ✓ Exposure OK (Local) │ +│ ✓ AudienceProfiles OK │ +│ ✗ Notifications.Webhooks critical-pager unreachable │ +│ ✓ ExternalSkills OK │ +│ – SkillFeeds None configured │ +│ – BrowserAutomation Disabled │ +│ │ +│ Summary: 8 pass · 1 warning · 1 error · 4 skipped │ +│ │ +│ Exit code on close: 1 (errors present) │ +│ │ +│ [ Back to dashboard ] │ +│ │ +│ Enter back · Esc back │ +╰─────────────────────────────────────────────────────────────╯ +``` + +Invokes the same `DoctorRunner` used by `netclaw doctor`. Results page +renders status per check. + +--- + +## Daemon-restart nudge at exit + +Printed to stderr after Termina teardown when (a) at least one section +saved during the session AND (b) the daemon is currently running. + +``` +Config saved. Restart the daemon to apply changes: + netclaw daemon stop && netclaw daemon start +``` + +When the daemon is not running OR no saves occurred, the nudge is +omitted. + +**Daemon detection:** `netclaw config` uses the same lightweight probe +as `netclaw daemon status` (PID file lookup at the documented path, +falling back to a port-open check on the configured daemon port). The +probe is bounded to 250 ms; if the probe times out, the nudge is +omitted (conservative — better to miss the nudge than to falsely +suggest a restart). diff --git a/docs/ui/TUI-003-simplified-init-wireframes.md b/docs/ui/TUI-003-simplified-init-wireframes.md new file mode 100644 index 000000000..47acd44d7 --- /dev/null +++ b/docs/ui/TUI-003-simplified-init-wireframes.md @@ -0,0 +1,328 @@ +# TUI-003: Simplified `netclaw init` Wireframes + +Source PRDs: `PRD-004`, `PRD-001` + +Backing OpenSpec change: `openspec/changes/simplify-netclaw-init/` + +Companion: `TUI-001-command-wireframes.md` (prior 6-step init wizard, +superseded by this document), `TUI-002-netclaw-config-wireframes.md` +(the `netclaw config` command that owns post-bootstrap edits). + +## Overview + +`netclaw init` is trimmed from 12 steps to three: LLM provider, +identity, security posture. The goal is time-to-first-chat. Everything +else (channels, search, webhooks, exposure mode, audience profiles, +skill feeds, external skill directories, browser automation, MCP +servers) moves to `netclaw config` (see TUI-002). + +Existing-config detection is now explicit: re-running over an existing +install refuses with helpful pointers, or accepts `--force` to back +up and reset. + +## Termina Component Vocabulary + +Same as TUI-001 / TUI-002: + +- **PanelNode** — bordered container with optional title +- **TextInputNode** — single or multi-line text input (masked variant for secrets) +- **SelectionListNode** — keyboard-navigable option list +- **TextNode** — static or dynamic text block +- **SpinnerNode** — animated progress indicator (post-flight health check) + +## Conventions + +Glyphs and keystrokes follow TUI-002 conventions. Init-specific: + +- Title bar shows step indicator `Step of 3: `. +- Step navigation: Tab cycles fields; Enter on Next advances; Enter or + Esc on Back returns; Esc on a step with dirty state triggers discard + confirm (see TUI-002 T7). + +--- + +## Navigation tree + +``` +netclaw init (fresh install — no existing config) + ├── Init.1 Provider selection (+ existing auth sub-flow) + ├── Init.2 Identity (agent name, user name, timezone) + ├── Init.3 Security Posture + └── Init.4 Post-flight (health-check, summary) ─── exit + stderr nudge + +netclaw init (existing config detected, no --force) + └── Init.E1 Refuse + suggest `netclaw config` or `netclaw init --force` + +netclaw init --force (existing config detected) + └── Init.E2 Backup confirm ──→ Init.1 (proceeds as fresh) + +netclaw init --force (no existing config) + └── Init.1 (proceeds as fresh; no backup screen) +``` + +--- + +## Init.1 — Provider selection + +Reuses existing `ProviderStepViewModel` (refactored to `ISectionEditor` +in `section-editor-abstraction` change). After the provider type is +picked, the existing auth sub-flow runs (auth method → endpoint → API +key or OAuth device flow → model selection). Behavior unchanged from +prior versions. + +``` +╭─ Netclaw Setup — Step 1 of 3: LLM Provider ─────────────────╮ +│ │ +│ Choose your LLM provider: │ +│ │ +│ ▸ Anthropic │ +│ OpenAI │ +│ OpenRouter │ +│ GitHub Copilot │ +│ Ollama (local, no API key) │ +│ OpenAI-compatible (custom endpoint) │ +│ │ +│ ↑/↓ navigate · Enter select · Esc quit │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Transitions:** + +- `Enter` → existing auth sub-flow (TUI-001 covers the sub-flow shapes). +- `Esc` → quit setup (with discard confirm if anything was entered). + +**Reentrancy:** in the rare case `netclaw init` runs over existing +config (only via `--force` reset; otherwise the command refuses +at Init.E1), the provider selector pre-fills the existing provider +type. API key field renders empty per the secret-handling contract +(`configured — leave blank to keep`). + +--- + +## Init.2 — Identity + +Trimmed `IdentityStepViewModel` (see Change C tasks 5.x). Drops the +prior webhook URL prompt, the workspaces-directory prompt, and the +communication-style prompt. Keeps agent name, user name, timezone. + +``` +╭─ Netclaw Setup — Step 2 of 3: Identity ─────────────────────╮ +│ │ +│ Your provider is configured. Now let's set up the agent. │ +│ │ +│ Agent name: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ Netclaw │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ Your name (what the agent calls you): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ Timezone (IANA name): │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ America/Los_Angeles │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ [ Next ] [ Back ] [ Cancel ] │ +│ │ +│ Tab next · Enter activate · Esc cancel │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Transitions:** + +- `Next` → Init.3. +- `Back` → Init.1. +- `Cancel` → discard confirm → exit. + +**Validation:** Agent name required, no whitespace. User name required. +Timezone validates against `TimeZoneInfo.FindSystemTimeZoneById`. + +**Dropped fields' defaults:** webhook URL is left unset (operators add +operational webhooks via `netclaw config → Outbound Webhooks`). +Workspaces directory defaults to `~/.netclaw/workspaces`. Communication +style defaults to neutral. These remain editable via file edit for now +(future Identity section editor in `netclaw config` is out of MVP +scope). + +--- + +## Init.3 — Security Posture + +Reuses existing `SecurityPostureStepViewModel`. + +``` +╭─ Netclaw Setup — Step 3 of 3: Security Posture ─────────────╮ +│ │ +│ How will Netclaw be used? │ +│ │ +│ ▸ Personal │ +│ Just me. Local-only by default. Tools have wide access. │ +│ │ +│ Team │ +│ Small team via Slack/Discord. Audience-restricted tools. │ +│ │ +│ Enterprise │ +│ Production deployment. Strict audience profiles, audit. │ +│ │ +│ [ Next ] [ Back ] [ Cancel ] │ +│ │ +│ ↑/↓ navigate · Tab to buttons · Enter activate │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Transitions:** + +- `Next` (Enter on Next button OR Enter on a posture row) → applies + posture-default `Tools.AudienceProfiles` mapping in-memory → + proceeds to Init.4 (terminal write + health check). +- `Back` → Init.2. + +**Posture cascade applied non-interactively (no separate feature +selection step):** + +| Posture | Audience.Personal | Audience.Team | Audience.Public | Shell mode | +|------------|-------------------|-----------------------------|----------------------------|---------------| +| Personal | all features on | n/a (Personal-only) | n/a | HostAllowed | +| Team | all features on | search+memory+skills on; webhooks off | webhooks off; memory off | SandboxOnly | +| Enterprise | search+memory on | search+memory on | nothing on | SandboxOnly | + +Operators override per-audience post-install via `netclaw config → +Audience Profiles`. + +--- + +## Init.4 — Post-flight + +After Init.3 applies posture, the wizard writes merged config + secrets ++ runs the existing health check + shows results. + +``` +╭─ Netclaw Setup — Setup Complete ────────────────────────────╮ +│ │ +│ ✓ Provider configured: Anthropic (claude-sonnet-4-6) │ +│ ✓ Identity set: Netclaw (aaron, America/Los_Angeles) │ +│ ✓ Posture: Personal │ +│ ✓ Configuration written to ~/.netclaw/config/netclaw.json │ +│ ✓ Health check passed │ +│ │ +│ ────── │ +│ │ +│ Run `netclaw chat` to start talking to your agent. │ +│ Run `netclaw config` to set up channels, search, webhooks, │ +│ external skills, browser automation, and more. │ +│ │ +│ [ Done ] │ +│ │ +│ Enter exit │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Transitions:** + +- `Enter` → Termina tears down. The same two-line nudge is also printed + to stderr after exit so users see it even after the TUI clears. + +**Failure path:** if health check fails (doctor errors), the page shows +the errors and a `[ Back to Posture ]` action instead of `[ Done ]`. +Operator returns to Init.3 to fix. + +### Post-flight when `--force` was used + +When `netclaw init --force` triggered a backup, the post-flight screen +appends a `.bak` file disclosure section so operators know where the +prior config went: + +``` +│ ────── │ +│ Previous configuration backed up to: │ +│ ~/.netclaw/config/netclaw.json.bak.1716508800 │ +│ ~/.netclaw/config/secrets.json.bak.1716508800 │ +│ │ +│ Restore manually if needed. │ +``` + +The same paths are printed to stderr after Termina teardown. + +--- + +## Init.E1 — Existing config refusal + +Rendered when `netclaw init` is invoked, `~/.netclaw/config/netclaw.json` +exists, and `--force` was not passed. + +``` +╭─ Netclaw is already initialized ────────────────────────────╮ +│ │ +│ Found existing configuration: │ +│ ~/.netclaw/config/netclaw.json │ +│ │ +│ To edit your configuration interactively, run: │ +│ netclaw config │ +│ │ +│ To start over from scratch (existing config backed up): │ +│ netclaw init --force │ +│ │ +│ [ OK ] │ +│ │ +│ Enter exit │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Non-interactive variant** (when stdout is not a TTY, e.g. CI): +prints the same text to stderr and exits non-zero. The interactive +variant exits zero on acknowledgement. + +--- + +## Init.E2 — Force-reset backup confirm + +Rendered when `netclaw init --force` runs and existing config is +detected. + +``` +╭─ Reset Netclaw configuration? ──────────────────────────────╮ +│ │ +│ This will: │ +│ • Move netclaw.json → netclaw.json.bak.<timestamp> │ +│ • Move secrets.json → secrets.json.bak.<timestamp> │ +│ • Start setup from scratch │ +│ │ +│ Your old config is preserved as a .bak file; you can │ +│ restore it manually if needed. │ +│ │ +│ Type "reset" to confirm: │ +│ ╭────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ ╰────────────────────────────────────────────────────────╯ │ +│ │ +│ ▸ [ Cancel ] [ Reset and continue ] │ +│ │ +╰─────────────────────────────────────────────────────────────╯ +``` + +**Type-to-confirm here because this is genuinely destructive** (running +config + secrets get moved aside, fresh setup writes new ones). +Single-Y/N is insufficient. + +**Transitions:** + +- `Cancel` → exit zero. Config unchanged. +- `Reset and continue` (enabled only when "reset" typed) → backup + performed (rename atomically; timestamp generated once per + invocation so both files share a suffix) → proceed to Init.1. + +**Non-TTY refusal:** `netclaw init --force > /dev/null 2>&1` cannot +prompt for the type-to-confirm. The command SHALL refuse in non-TTY +contexts with `--force` requires interactive confirm and exit non-zero. + +**`--force` over no existing config:** silently behaves as plain +`netclaw init` (no backup screen, no extra prompt). + +**Backup timestamp collision avoidance:** the timestamp suffix uses +unix-milliseconds (`netclaw.json.bak.<millis>`). On the extremely +unlikely event of a collision (two `--force` invocations in the same +millisecond), an auto-increment suffix is appended +(`netclaw.json.bak.<millis>-1`). diff --git a/openspec/changes/netclaw-config-command/design.md b/openspec/changes/netclaw-config-command/design.md index 524c72339..203c7532c 100644 --- a/openspec/changes/netclaw-config-command/design.md +++ b/openspec/changes/netclaw-config-command/design.md @@ -1,5 +1,11 @@ ## Context +**UI wireframes:** every page introduced by this change is mocked in +`docs/ui/TUI-002-netclaw-config-wireframes.md` (dashboard, all 12 section +editors, list editor templates T1–T8, doctor results page, daemon +restart nudge). Implementors SHALL treat TUI-002 as the visual contract; +this design document explains decisions and trade-offs around it. + The `section-editor-abstraction` change (predecessor) introduced the `ISectionEditor` contract, the `SectionEditorRegistry`, the merge-on-save plumbing, and the single-step `WizardOrchestrator` mode. It refactored @@ -228,6 +234,24 @@ while keeping the registry flat. is reachable from one menu entry away. Migration text in the PR description points operators at the new path. +- [Multi-instance editing] Two concurrent `netclaw config` processes + on the same install would both load → merge → write to the same + `netclaw.json` and `secrets.json`. → Mitigation: out of MVP scope; + semantics are last-write-wins per the file's atomic tmp-rename + write. Documented as a known limitation. File locks are deferred + until there is concrete evidence of operators running multiple + TUI editors simultaneously. + +- [Test Connection partial failure shape] Slack/Discord/Mattermost + Test Connection actions probe several capabilities (auth, channel + access, DM access). Some sub-probes may succeed while others + fail. → Mitigation: the result banner SHALL render one line per + sub-probe with its own status glyph (`✓ Bot token valid`, + `✗ Channel C01ABCDE not in workspace`). Network timeouts SHALL + render as `⚠ probe timed out` rather than a fatal failure, since + the operator may have a transient network issue. Test Connection + is advisory only; it never blocks the editor's Save. + ## Migration Plan This change ships net-new behavior (`netclaw config`) plus a single diff --git a/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md b/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md index 2c38340ce..7f4f7bf93 100644 --- a/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md +++ b/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md @@ -6,10 +6,11 @@ The CLI SHALL expose `netclaw config` as a top-level command. The command SHALL be offline (no daemon connection), SHALL operate on local config files only, and SHALL behave per the `netclaw-config-command` capability. `netclaw config --help` SHALL -print a one-paragraph description and exit zero. Invocations with any -positional argument SHALL print usage and exit non-zero in this change -(subcommands such as `netclaw config show|validate` remain reserved -for future work and SHALL NOT execute as a side effect). +print a one-paragraph description and exit zero. `netclaw config show` +and `netclaw config validate` are RESERVED subcommands (PRD-004) and +SHALL print a not-yet-implemented notice and exit non-zero in this +change, preserving the documented future surface. Unknown subcommands +SHALL print usage and exit non-zero. #### Scenario: Help text describes the command @@ -18,12 +19,33 @@ for future work and SHALL NOT execute as a side effect). - **AND** stdout contains a one-paragraph description naming "interactive configuration editor" - **AND** stdout references the `netclaw init` companion command +- **AND** stdout lists the reserved `show` and `validate` subcommands + with a "not yet implemented; see PRD-004" note -#### Scenario: Unknown subcommand rejected +#### Scenario: Reserved subcommand show exits non-zero with reservation notice + +- **WHEN** the operator runs `netclaw config show` +- **THEN** stderr contains + `\`netclaw config show\` is reserved for future use (PRD-004) and is + not yet implemented.` +- **AND** the command exits with non-zero status +- **AND** no `netclaw.json` write occurs + +#### Scenario: Reserved subcommand validate exits non-zero with reservation notice + +- **WHEN** the operator runs `netclaw config validate` +- **THEN** stderr contains + `\`netclaw config validate\` is reserved for future use (PRD-004) + and is not yet implemented.` +- **AND** the command exits with non-zero status +- **AND** no `netclaw.json` write occurs + +#### Scenario: Unknown subcommand rejected with usage - **WHEN** the operator runs `netclaw config foo` - **THEN** the command exits with non-zero status -- **AND** stderr contains usage text +- **AND** stderr contains usage text naming the dashboard launch + (`netclaw config` with no args) and the reserved subcommands #### Scenario: No-args invocation launches dashboard diff --git a/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md b/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md index 6067f06bb..d24231490 100644 --- a/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md +++ b/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md @@ -195,8 +195,14 @@ dashboard with no config write performed. `netclaw config` SHALL print a stderr nudge at exit instructing the operator to restart the daemon for changes to take effect, when (a) at least one config or secrets write occurred during the session AND (b) -the daemon is currently running. If either condition is false, the -nudge SHALL be omitted. +the daemon is currently running. Daemon-running detection SHALL reuse +the same probe used by `netclaw daemon status` (PID-file check at the +documented daemon path, falling back to a TCP-open check on the +configured daemon port). The probe SHALL be bounded by a 250 ms +timeout; on timeout the nudge SHALL be omitted (conservative — missing +a true-positive nudge is preferable to a false-positive nudge after a +network hiccup). If either condition is false, the nudge SHALL be +omitted. #### Scenario: Daemon running plus config change emits nudge @@ -222,6 +228,16 @@ nudge SHALL be omitted. - **WHEN** the operator quits - **THEN** no nudge is printed regardless of daemon state +#### Scenario: Daemon-detection probe timeout suppresses nudge + +- **GIVEN** the operator saved at least one section during the session +- **AND** the PID-file lookup fails (file absent or unreadable) +- **AND** the TCP-open check on the daemon port exceeds the 250 ms + bound +- **WHEN** the operator quits the dashboard +- **THEN** no nudge is printed +- **AND** the command exits with status 0 + ### Requirement: Generic list editor component The CLI SHALL provide a generic `ListEditor<T>` Termina component @@ -264,12 +280,21 @@ in-place renames (rather than delete + add) round-trip correctly. - **GIVEN** a webhook list with an entry whose `KeyOf` returns `"critical-pager"` +- **AND** the entry's auth header is stored under that key in + `secrets.json` (e.g. `Notifications.Webhooks.critical-pager.AuthHeader`) - **WHEN** the operator edits the entry and changes its name to `pagerduty-prod` -- **THEN** the list save records a single update (not a delete + add) -- **AND** the underlying `Notifications.Webhooks` array contains exactly - one entry with the new name and the preserved auth header - (per the secret-handling contract) +- **THEN** the list editor tracks the rename via the `(originalKey, + newKey)` pair across the edit lifecycle +- **AND** the merge writer locates the underlying schema-array entry + by `originalKey` (not by array index), replaces the name and other + fields, and writes the updated entry at the same array position +- **AND** the corresponding secrets-store key is renamed from + `originalKey` to `newKey` atomically; the stored encrypted value + for `originalKey` is unchanged in encrypted form and re-keyed +- **AND** the resulting `Notifications.Webhooks` array contains + exactly one entry, named `pagerduty-prod`, with the previously + stored auth header still configured ### Requirement: Search Provider editor @@ -541,6 +566,30 @@ installing. `RelevantDoctorChecks` SHALL include - **THEN** `BrowserAutomationDoctorCheck` returns ERROR - **AND** the save is blocked with remediation guidance +#### Scenario: Existing config without BrowserAutomation section opens cleanly + +- **GIVEN** an existing `netclaw.json` written prior to this change + that lacks a top-level `BrowserAutomation` section +- **WHEN** the operator opens the Browser Automation editor +- **THEN** the editor renders with the toggle reflecting + `Enabled = false` (schema default) +- **AND** no schema-validation error is surfaced for the missing + section +- **AND** the merge writer treats a no-op exit as a true no-op (no + speculative `BrowserAutomation` section is written until the + operator explicitly saves a non-default state) + +#### Scenario: SchemaFixResolver auto-insert tolerates missing section on doctor --fix + +- **GIVEN** an existing `netclaw.json` written prior to this change + that lacks the `BrowserAutomation` section +- **WHEN** the operator runs `netclaw doctor --fix` +- **THEN** `SchemaFixResolver` inserts + `BrowserAutomation: { Enabled: false }` using the schema's default + value +- **AND** subsequent `ConfigSchemaDoctorCheck` runs pass without + warning + ### Requirement: Smoke tape per editor and the no-init refusal The smoke-test harness SHALL include a tape per registered section diff --git a/openspec/changes/netclaw-config-command/tasks.md b/openspec/changes/netclaw-config-command/tasks.md index 4ff3b3978..7b434a9cd 100644 --- a/openspec/changes/netclaw-config-command/tasks.md +++ b/openspec/changes/netclaw-config-command/tasks.md @@ -71,55 +71,75 @@ ## 6. Section editors — single-value -- [ ] 6.1 `SearchSectionEditor` (`SectionId = "Search"`): backend - selector + conditional API key / SearXng URL fields. Honor +These editors REUSE existing step viewmodels where possible. Each +existing step viewmodel is REFACTORED to implement `ISectionEditor` +(per Change A's contract) and is moved into the new folder structure +under `src/Netclaw.Cli/Tui/Sections/<Section>/`. No new duplicate +classes are created for sections that today have an init step +viewmodel; the same class serves both init (when in the trimmed step +list, post Change C) and `netclaw config` (single-step mode). + +- [ ] 6.1 `SearchSectionEditor` (`SectionId = "Search"`, + `ShowInMenu = true`): refactor of existing `SearchStepViewModel`. + Backend selector + conditional API key / SearXng URL fields. Honor `ExistingConfig`. `RelevantDoctorChecks`: `{ConfigSchemaDoctorCheck, SearchBackendDoctorCheck}`. - [ ] 6.2 `SecurityPostureSectionEditor` - (`SectionId = "Security.Posture"`): three-choice posture list with - cascade dialog (Cancel | Overwrite | Keep custom) when changing - posture over customized `Tools.AudienceProfiles`. + (`SectionId = "Security.Posture"`, `ShowInMenu = true`): refactored + to `ISectionEditor` in Change A; this change adds the cascade dialog + (Cancel | Overwrite | Keep custom) when changing posture over + customized `Tools.AudienceProfiles`. - [ ] 6.3 `AudienceProfilesSectionEditor` - (`SectionId = "Tools.AudienceProfiles"`): audience picker - (Personal | Team | Public) opening per-audience editor with - toggleable feature rows, shell-mode selector, approval policy - selector, and "Reset to posture default" affordance. MUST exercise - arrow nav + Space toggle (#1150 contract). -- [ ] 6.4 `InboundWebhooksSectionEditor` (`SectionId = "Webhooks"`): - feature-flag toggle + request timeout integer. + (`SectionId = "Tools.AudienceProfiles"`, `ShowInMenu = true`): NEW + editor (no init-step equivalent — the buggy `FeatureSelectionStepViewModel` + is replaced by this editor). Audience picker (Personal | Team | Public) + opening per-audience editor with toggleable feature rows, + shell-mode selector, approval policy selector, and "Reset to + posture default" affordance. MUST exercise arrow nav + Space toggle + (#1150 contract). +- [ ] 6.4 `InboundWebhooksSectionEditor` (`SectionId = "Webhooks"`, + `ShowInMenu = true`): NEW editor. Feature-flag toggle + request + timeout integer. - [ ] 6.5 `BrowserAutomationSectionEditor` - (`SectionId = "BrowserAutomation"`): feature-flag toggle with - Playwright detection at entry; install-instructions sub-page when - Playwright absent. + (`SectionId = "BrowserAutomation"`, `ShowInMenu = true`): refactor + of existing `BrowserAutomationStepViewModel`. Feature-flag toggle + with Playwright detection at entry; install-instructions sub-page + when Playwright absent. ## 7. Section editors — multi-value (compose ListEditor) - [ ] 7.1 `OutboundWebhooksSectionEditor` - (`SectionId = "Notifications.Webhooks"`) using - `WebhookItemEditor`. + (`SectionId = "Notifications.Webhooks"`, `ShowInMenu = true`): NEW + editor. Uses `WebhookItemEditor`. - [ ] 7.2 `ExternalSkillsSectionEditor` - (`SectionId = "ExternalSkills"`) using `PathItemEditor`. -- [ ] 7.3 `SkillFeedsSectionEditor` (`SectionId = "SkillFeeds"`) using - `SkillFeedItemEditor`. + (`SectionId = "ExternalSkills"`, `ShowInMenu = true`): refactor of + existing `ExternalSkillsStepViewModel`. Uses `PathItemEditor`. +- [ ] 7.3 `SkillFeedsSectionEditor` (`SectionId = "SkillFeeds"`, + `ShowInMenu = true`): refactor of existing `SkillFeedsStepViewModel`. + Uses `SkillFeedItemEditor`. ## 8. Section editors — chat channels (composite) - [ ] 8.1 `SlackSectionEditor` (`SectionId = "Slack"`, - `Category = "Chat Channels"`): bot token + app token, allowed + `Category = "Chat Channels"`, `ShowInMenu = true`): refactor of + existing `SlackStepViewModel`. Bot token + app token, allowed channels list, allowed users list, DMs toggle, audience profile - selector, Test Connection. Reuses - `channel-audience-tui` cycling component for the channel list. + selector, Test Connection. Reuses `channel-audience-tui` cycling + component for the channel list. - [ ] 8.2 `DiscordSectionEditor` (`SectionId = "Discord"`, - `Category = "Chat Channels"`): single bot token, same affordances + `Category = "Chat Channels"`, `ShowInMenu = true`): refactor of + existing `DiscordStepViewModel`. Single bot token, same affordances otherwise. - [ ] 8.3 `MattermostSectionEditor` (`SectionId = "Mattermost"`, - `Category = "Chat Channels"`): server URL + bot token, same + `Category = "Chat Channels"`, `ShowInMenu = true`): refactor of + existing `MattermostStepViewModel`. Server URL + bot token, same affordances otherwise. ## 9. Section editor — exposure mode (composite) - [ ] 9.1 `ExposureModeSectionEditor` - (`SectionId = "Daemon.ExposureMode"`): mode selector (Local | + (`SectionId = "Daemon.ExposureMode"`, `ShowInMenu = true`): refactor + of existing `ExposureModeStepViewModel`. Mode selector (Local | Reverse Proxy | Tailscale | Cloudflare Tunnel), daemon host/port fields, mode-conditional sub-forms. - [ ] 9.2 Reverse Proxy sub-form: external base URL + trusted @@ -127,6 +147,17 @@ - [ ] 9.3 Tailscale sub-form: auth key (secret) + hostname. - [ ] 9.4 Cloudflare Tunnel sub-form: tunnel token (secret) + optional access-policy email domain. +- [ ] 9.5 Add `Daemon` to `SectionEditorExemptions` with category + `"covered by another editor's dotted-path SectionId"` naming + `Daemon.ExposureMode` as the owner. The non-exposure parts of + `Daemon` (host, port, trusted proxies) are part of the + ExposureModeSectionEditor's surface. +- [ ] 9.6 Add `Security` to `SectionEditorExemptions` with category + `"covered by another editor's dotted-path SectionId"` naming + `Security.Posture`. +- [ ] 9.7 Add `Tools` to `SectionEditorExemptions` with category + `"covered by another editor's dotted-path SectionId"` naming + `Tools.AudienceProfiles`. ## 10. New doctor checks diff --git a/openspec/changes/section-editor-abstraction/design.md b/openspec/changes/section-editor-abstraction/design.md index 8a48d0dde..d7c2de416 100644 --- a/openspec/changes/section-editor-abstraction/design.md +++ b/openspec/changes/section-editor-abstraction/design.md @@ -1,5 +1,12 @@ ## Context +**UI wireframes:** SecurityPosture's appearance inside `netclaw config` +is in `docs/ui/TUI-002-netclaw-config-wireframes.md` (§ Config.6). +Provider and Identity remain init-only and their wireframes are in +`docs/ui/TUI-003-simplified-init-wireframes.md` (§ Init.1, Init.2) +once Change C lands; for this change they continue to use the prior +init wizard wireframes documented in `docs/ui/TUI-001-command-wireframes.md`. + The `netclaw init` wizard composed of `WizardOrchestrator` + a fixed list of `IWizardStepViewModel`s produces a runnable Netclaw configuration but treats the on-disk state as a write-once target. There is no shared abstraction for @@ -115,6 +122,31 @@ editors we ship, not to demand editors for every schema knob; the exemption list is the explicit "we know about this section and choose not to expose it" record. +The audit distinguishes three kinds of editor: + +- **`ShowInMenu == true` editors with a top-level `SectionId`** (e.g. + `Search`, `Slack`). Require: round-trip test class, non-empty + `RelevantDoctorChecks` (or `[NoDoctorChecks]`), AND a smoke tape at + `tests/smoke/tapes/config-<sectionid-lower>.tape` (once the + `netclaw config` dashboard exists from the next change). +- **`ShowInMenu == true` editors with a dotted-path `SectionId`** (e.g. + `Security.Posture`, `Daemon.ExposureMode`, `Tools.AudienceProfiles`). + Same requirements as above. The top-level parent section (e.g. + `Security`) must appear in `SectionEditorExemptions` with a + "covered by another editor" entry naming the dotted-path editor as + the canonical owner. +- **`ShowInMenu == false` editors** (e.g. `Providers`, `Identity`). + Require: round-trip test class and `RelevantDoctorChecks`. Smoke-tape + existence is NOT required — these editors run inside the init wizard + (covered by `init-wizard.tape`) or via dedicated CLI subcommands + (covered by their respective tapes). + +The synthetic-identifier case (e.g. `Identity`, which spans several +schema sections rather than owning one) is treated as `ShowInMenu == +false` and must appear in the exemption list with category +`"synthetic-spans-multiple-sections"` so reviewers can see it's not a +real schema key. + Alternative considered: walk the schema and require every top-level section to either have an editor or an exemption. Rejected per planning discussion: forcing editors for every schema knob produces shallow, diff --git a/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md b/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md index 73dca0abf..51ecf3d56 100644 --- a/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md +++ b/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md @@ -7,8 +7,12 @@ The CLI SHALL define a `ISectionEditor` contract in section. Each implementation SHALL declare a stable `SectionId` whose value matches a schema key in `netclaw-config.v1.schema.json` (dotted-path form is permitted for nested sections such as `Daemon.ExposureMode` and -`Tools.AudienceProfiles`), a user-facing `DisplayName`, an optional -`Category` grouping label, a `GetStatus` method returning +`Tools.AudienceProfiles`; a synthetic-identifier form is permitted ONLY for +editors whose data spans multiple schema sections, in which case the editor +MUST appear in the documented exemption list), a user-facing `DisplayName`, +an optional `Category` grouping label, a `bool ShowInMenu` flag (default +`true`; editors that participate in init but are not exposed in the +`netclaw config` menu SHALL return `false`), a `GetStatus` method returning `SectionStatus.{Default, Configured, Warning, Error, Missing}` from current on-disk config, a secret-redacting `Summary` for dashboard display, a non-empty `RelevantDoctorChecks` collection (or an explicit @@ -50,6 +54,18 @@ factory that returns an `IWizardStepViewModel`. - **AND** it is also runnable in single-step orchestrator mode (see "Single-step orchestrator") +#### Scenario: Editor opts out of the netclaw config menu + +- **GIVEN** an `ISectionEditor` whose section is owned by the init + wizard or a CLI subcommand and is not exposed for ad-hoc editing + via `netclaw config` +- **WHEN** the editor declares `ShowInMenu => false` +- **THEN** the dashboard SHALL NOT render the editor as a menu entry +- **AND** the menu registry audit's smoke-tape existence check + SHALL NOT require a `config-<sectionid>.tape` for that editor +- **AND** the round-trip test contract SHALL still apply (the editor + must have a `SectionEditorTestBase<TEditor>` subclass) + ### Requirement: Section editor registry The CLI SHALL provide a DI-discovered `SectionEditorRegistry` holding every @@ -79,11 +95,17 @@ within the registry. The CLI SHALL maintain a documented exemption list at `Netclaw.Cli.Tui.Sections.SectionEditorExemptions` enumerating schema -sections that intentionally have no TUI editor. Each entry SHALL carry a -machine-readable category (e.g. "internal-only", "set-once-at-install", -"covered by CLI subcommand", "covered by another editor", "out of MVP -scope"). The exemption list SHALL be the only mechanism by which an -unregistered schema section avoids audit failure. +sections that intentionally have no top-level TUI editor. Each entry +SHALL carry a machine-readable category (e.g. "internal-only", +"set-once-at-install", "covered by CLI subcommand", "covered by +another editor's dotted-path SectionId", "synthetic-spans-multiple-sections", +"out of MVP scope"). The exemption list SHALL be the only mechanism +by which an unregistered schema section avoids audit failure. The +audit SHALL consider a top-level schema section "covered" when ANY +registered editor's `SectionId` starts with `<section>.` (dotted-path +ownership); such top-level sections still require an exemption-list +entry naming the covering editor to make the relationship explicit +and reviewable. #### Scenario: Schema section absent from registry and absent from exemptions @@ -102,6 +124,19 @@ unregistered schema section avoids audit failure. - **WHEN** the audit runs - **THEN** the audit does not fail for `Persistence` +#### Scenario: Top-level schema section covered by a dotted-path editor + +- **GIVEN** the schema declares a top-level section `Security` +- **AND** an editor with `SectionId = "Security.Posture"` is + registered +- **AND** `"Security"` is present in `SectionEditorExemptions` with + category `"covered by another editor's dotted-path SectionId"` + naming `Security.Posture` +- **WHEN** the audit runs +- **THEN** the audit does not fail for `Security` +- **AND** the audit's failure-message vocabulary treats the + exemption's "covering editor" reference as the canonical owner + ### Requirement: Single-step orchestrator mode `WizardOrchestrator` SHALL support construction with a single @@ -293,13 +328,15 @@ every registered `ISectionEditor`. The test project SHALL include `MenuRegistryAuditTests` that walks `SectionEditorRegistry` and asserts, for every registered editor: a -matching concrete `SectionEditorTestBase<TEditor>` subclass exists, the +matching concrete `SectionEditorTestBase<TEditor>` subclass exists; the editor's `RelevantDoctorChecks` is non-empty (or the class is annotated -with `[NoDoctorChecks]`), and — once smoke tapes ship for the editor in -the next change — a matching tape file exists at -`tests/smoke/tapes/config-<section-lowercase>.tape`. The audit SHALL -report all failures in one assertion message naming each missing -artifact. +with `[NoDoctorChecks]`); and, for editors with `ShowInMenu == true`, +once smoke tapes ship for the editor in the next change, a matching +tape file exists at `tests/smoke/tapes/config-<section-lowercase>.tape`. +Editors with `ShowInMenu == false` are exempt from the tape-existence +check (they participate in init or in CLI subcommands; init-side +coverage is provided by `init-wizard.tape`). The audit SHALL report +all failures in one assertion message naming each missing artifact. #### Scenario: Missing round-trip test class fails the audit @@ -322,5 +359,20 @@ artifact. (Provider, Identity, Posture) - **AND** each has a matching round-trip test class and non-empty `RelevantDoctorChecks` +- **AND** Provider and Identity declare `ShowInMenu == false` while + Posture declares `ShowInMenu == true` - **WHEN** `MenuRegistryAuditTests` runs - **THEN** the audit passes +- **AND** the audit does not require a `config-providers.tape`, + `config-identity.tape`, or `config-security.posture.tape` for the + `ShowInMenu == false` editors + +#### Scenario: ShowInMenu editor missing its smoke tape fails the audit + +- **GIVEN** a registered editor with `ShowInMenu == true` +- **AND** no file at + `tests/smoke/tapes/config-<sectionid-lower>.tape` +- **AND** the `netclaw config` command exists (tape requirement is + active per the change that introduces the dashboard) +- **WHEN** `MenuRegistryAuditTests` runs +- **THEN** the audit fails with a message naming the missing tape diff --git a/openspec/changes/section-editor-abstraction/tasks.md b/openspec/changes/section-editor-abstraction/tasks.md index f94028569..379fb52e5 100644 --- a/openspec/changes/section-editor-abstraction/tasks.md +++ b/openspec/changes/section-editor-abstraction/tasks.md @@ -79,18 +79,26 @@ ## 7. Refactor three existing init step viewmodels -- [ ] 7.1 `ProviderStepViewModel`: implement `ISectionEditor` (SectionId - `Providers`). Honor `ExistingConfig` in `OnEnter(direction)` for - provider type, endpoint, auth method, model selection, and OAuth - token expiry. API key field renders empty with "configured — leave - blank to keep" hint when `SecretPresent` returns true. -- [ ] 7.2 `IdentityStepViewModel`: implement `ISectionEditor` (SectionId - `Identity`). Honor `ExistingConfig` for agent name, user name, - timezone, comm style, workspaces directory, webhook URL. (Step is - trimmed in the third change; this change keeps existing fields.) +- [ ] 7.1 `ProviderStepViewModel`: implement `ISectionEditor` + (SectionId `Providers`, `ShowInMenu = false` — covered by the + existing `netclaw provider` CLI per D3 of the planning doc). Honor + `ExistingConfig` in `OnEnter(direction)` for provider type, endpoint, + auth method, model selection, and OAuth token expiry. API key field + renders empty with "configured — leave blank to keep" hint when + `SecretPresent` returns true. +- [ ] 7.2 `IdentityStepViewModel`: implement `ISectionEditor` + (SectionId `Identity` as a synthetic identifier — Identity is NOT a + top-level schema key; identity data spans `Workspaces`, + `Notifications`, and identity files like `SOUL.md`. Add the + synthetic ID `Identity` to `SectionEditorExemptions` with category + `"synthetic-spans-multiple-sections"`. `ShowInMenu = false` — set + once at init in MVP). Honor `ExistingConfig` for agent name, user + name, timezone, comm style, workspaces directory, webhook URL. (Step + is trimmed in the third change; this change keeps existing fields.) - [ ] 7.3 `SecurityPostureStepViewModel`: implement `ISectionEditor` - (SectionId `Security.Posture`, dotted path). Honor `ExistingConfig` - for the posture selection and posture-default cascade. + (SectionId `Security.Posture`, dotted path; `ShowInMenu = true` — + surfaces in the dashboard in Change B). Honor `ExistingConfig` for + the posture selection and posture-default cascade. - [ ] 7.4 Each refactored editor declares non-empty `RelevantDoctorChecks` referencing the existing checks that scope to the editor's section. diff --git a/openspec/changes/simplify-netclaw-init/design.md b/openspec/changes/simplify-netclaw-init/design.md index 31237a60d..7749bc45b 100644 --- a/openspec/changes/simplify-netclaw-init/design.md +++ b/openspec/changes/simplify-netclaw-init/design.md @@ -1,5 +1,13 @@ ## Context +**UI wireframes:** every page introduced by this change — the three +init steps, the post-flight screen, the existing-config refusal +(Init.E1), and the force-reset backup confirm (Init.E2) — is mocked +in `docs/ui/TUI-003-simplified-init-wireframes.md`. Implementors SHALL +treat TUI-003 as the visual contract for this change. The companion +TUI-002 mocks `netclaw config`, which is the destination operators are +nudged toward at post-flight. + The `section-editor-abstraction` change (Change A) refactored Provider, Identity, and Posture step viewmodels into reentrant `ISectionEditor`s and switched the wizard's terminal write to merge-on-save. The diff --git a/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md b/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md index 45baaf1ab..35c4a9603 100644 --- a/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md +++ b/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md @@ -139,12 +139,20 @@ status so CI catches the surprise. `netclaw init --force` SHALL detect existing config and require an explicit type-to-confirm before proceeding. On confirm, the command SHALL rename `~/.netclaw/config/netclaw.json` to -`netclaw.json.bak.<unix-timestamp>` and -`~/.netclaw/config/secrets.json` to `secrets.json.bak.<unix-timestamp>`. -The wizard SHALL then proceed as a fresh first-run. The .bak files -SHALL be preserved on disk so operators retain a manual recovery -path. The command SHALL print the .bak file paths to the post-flight -screen so operators know where the prior config went. +`netclaw.json.bak.<unix-millis>` and +`~/.netclaw/config/secrets.json` to `secrets.json.bak.<unix-millis>`. +A single timestamp SHALL be generated per invocation so both files +share a suffix. On the extremely unlikely event of a collision (an +existing file at the chosen suffix), an auto-incrementing dash +suffix SHALL be appended (`.bak.<unix-millis>-1`, `-2`, ...) until a +free filename is found. The wizard SHALL then proceed as a fresh +first-run. The .bak files SHALL be preserved on disk so operators +retain a manual recovery path. The command SHALL print the .bak file +paths to the post-flight screen so operators know where the prior +config went. `netclaw init --force` SHALL refuse to run in non-TTY +contexts (no stdin or no terminal-controlled stdout) because the +type-to-confirm prompt cannot be rendered safely; the command SHALL +print a non-TTY refusal message to stderr and exit non-zero. #### Scenario: Force without confirm leaves config unchanged @@ -175,3 +183,29 @@ screen so operators know where the prior config went. - **WHEN** the command starts - **THEN** no backup screen is shown (nothing to back up) - **AND** the wizard proceeds to Step 1 (Provider) normally + +#### Scenario: Force in non-TTY context refuses + +- **GIVEN** `netclaw.json` exists on disk +- **AND** `netclaw init --force` is run with stdout or stdin not a TTY + (e.g. piped, redirected, or in CI) +- **WHEN** the command starts +- **THEN** stderr contains + `\`netclaw init --force\` requires an interactive terminal for the + reset confirmation. Run it from a TTY.` +- **AND** the command exits with non-zero status +- **AND** the existing `netclaw.json` and `secrets.json` are + unchanged +- **AND** no .bak files are created + +#### Scenario: Force handles existing .bak filename collision + +- **GIVEN** `netclaw.json` exists on disk +- **AND** a previously-created backup at + `~/.netclaw/config/netclaw.json.bak.<expected-millis>` already + exists (e.g. from a prior force run within the same millisecond) +- **WHEN** the operator types "reset" and confirms +- **THEN** the backup uses + `netclaw.json.bak.<expected-millis>-1` (and the corresponding + `secrets.json.bak.<expected-millis>-1`) +- **AND** the existing backup file is not overwritten diff --git a/openspec/changes/simplify-netclaw-init/tasks.md b/openspec/changes/simplify-netclaw-init/tasks.md index ada4d874f..5e41b17c5 100644 --- a/openspec/changes/simplify-netclaw-init/tasks.md +++ b/openspec/changes/simplify-netclaw-init/tasks.md @@ -34,9 +34,11 @@ button is enabled only when the operator types `reset` into the confirm input. - [ ] 3.3 On confirm, rename `netclaw.json` → - `netclaw.json.bak.<unix-ts>` and `secrets.json` → - `secrets.json.bak.<unix-ts>` atomically. Generate timestamp once - per invocation so the two files share a suffix. + `netclaw.json.bak.<unix-millis>` and `secrets.json` → + `secrets.json.bak.<unix-millis>` atomically. Generate the + millisecond timestamp once per invocation so the two files share a + suffix. If a file already exists at the chosen suffix, append a + dash-counter (`-1`, `-2`, …) until a free name is found. - [ ] 3.4 After backup, proceed into the three-step wizard as a fresh first-run (`WizardContext.ExistingConfig = null`). - [ ] 3.5 On successful post-flight, list the .bak file paths in the @@ -44,6 +46,9 @@ went. - [ ] 3.6 `--force` with no existing config silently behaves as plain `netclaw init` (no backup screen). +- [ ] 3.7 `--force` in a non-TTY context (stdin or stdout not a + terminal) SHALL refuse with the documented stderr message and + exit non-zero before any file mutation. ## 4. Wizard step list trim