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/.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..203c7532c --- /dev/null +++ b/openspec/changes/netclaw-config-command/design.md @@ -0,0 +1,283 @@ +## 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 +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<T>` 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<T>` 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<T>` renders `+ Add <noun>` 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/<Section>/` + +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. + +- [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 +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 → <audience>`. +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<T>` Termina component and a per-shape + `IItemEditor<T>` 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<TEditor>` 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-<section-lower>.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<T>`, 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..7f4f7bf93 --- /dev/null +++ b/openspec/changes/netclaw-config-command/specs/netclaw-cli/spec.md @@ -0,0 +1,55 @@ +## 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. `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 + +- **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 +- **AND** stdout lists the reserved `show` and `validate` subcommands + with a "not yet implemented; see PRD-004" note + +#### 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 naming the dashboard launch + (`netclaw config` with no args) and the reserved subcommands + +#### 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..d24231490 --- /dev/null +++ b/openspec/changes/netclaw-config-command/specs/netclaw-config-command/spec.md @@ -0,0 +1,627 @@ +## 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. 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 + +- **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 + +#### 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 +parameterized by an `IItemEditor<T>` describing the item shape. The +component SHALL render an Add row at the bottom (`+ Add <noun>`), 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"` +- **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 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 + +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<T>` 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<T>` 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<T>` 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 + +#### 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 +editor at `tests/smoke/tapes/config-<section-lowercase>.tape` plus a +matching assertion script at +`tests/smoke/assertions/config-<section-lowercase>.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-<sectionid>.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..7b434a9cd --- /dev/null +++ b/openspec/changes/netclaw-config-command/tasks.md @@ -0,0 +1,294 @@ +## 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 + +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"`, `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"`, `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"`, `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"`, `ShowInMenu = true`): NEW + editor. Uses `WebhookItemEditor`. +- [ ] 7.2 `ExternalSkillsSectionEditor` + (`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"`, `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. +- [ ] 8.2 `DiscordSectionEditor` (`SectionId = "Discord"`, + `Category = "Chat Channels"`, `ShowInMenu = true`): refactor of + existing `DiscordStepViewModel`. Single bot token, same affordances + otherwise. +- [ ] 8.3 `MattermostSectionEditor` (`SectionId = "Mattermost"`, + `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"`, `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 + proxies list (via `ListEditor<T>` + `IdentifierItemEditor`). +- [ ] 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 + +- [ ] 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<TEditor>()` 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-<sectionid>.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..d7c2de416 --- /dev/null +++ b/openspec/changes/section-editor-abstraction/design.md @@ -0,0 +1,245 @@ +## 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 +"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<string, FieldAction>` for non-secrets and +`Dictionary<string, SecretAction>` 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. + +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, +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<string, object>`, 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<TEditor>` 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..51ecf3d56 --- /dev/null +++ b/openspec/changes/section-editor-abstraction/specs/section-editor-abstraction/spec.md @@ -0,0 +1,378 @@ +## 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 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 +`[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: "<reason>")]` +- **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") + +#### 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 +registered `ISectionEditor`. Registration SHALL occur via the extension +method `services.AddSectionEditor<TEditor>()`. The registry SHALL expose at +minimum `IReadOnlyList<ISectionEditor> 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<ProviderSectionEditor>()` + 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 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 + +- **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` + +#### 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 +`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<TEditor>` 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<TEditor>` 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<TEditor>` subclass exists; the +editor's `RelevantDoctorChecks` is non-empty (or the class is annotated +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 + +- **GIVEN** a registered `ISectionEditor` without a matching + `SectionEditorTestBase<TEditor>` 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` +- **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 new file mode 100644 index 000000000..379fb52e5 --- /dev/null +++ b/openspec/changes/section-editor-abstraction/tasks.md @@ -0,0 +1,171 @@ +## 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<TEditor>()` 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`, `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; `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. +- [ ] 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<TEditor>` + 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-<sectionId-lower>.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..7749bc45b --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/design.md @@ -0,0 +1,216 @@ +## 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 +`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.<ts>`); 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.<unix-ts>` + and `secrets.json` is renamed to `secrets.json.bak.<unix-ts>`. 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..35c4a9603 --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/specs/netclaw-onboarding/spec.md @@ -0,0 +1,211 @@ +## 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.<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 + +- **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.<unix-timestamp>` +- **AND** the original `secrets.json` is renamed to + `secrets.json.bak.<unix-timestamp>` +- **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 + +#### 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 new file mode 100644 index 000000000..5e41b17c5 --- /dev/null +++ b/openspec/changes/simplify-netclaw-init/tasks.md @@ -0,0 +1,188 @@ +## 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 <path>. 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.<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 + 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). +- [ ] 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 + +- [ ] 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.