From c3ba464db49717172f268f3a009389b2706df653 Mon Sep 17 00:00:00 2001 From: Darren Cheng Date: Wed, 24 Jun 2026 14:58:11 -0700 Subject: [PATCH] docs(openspec): backfill base specs for shipping behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `openspec/specs/` was empty after the OpenSpec policy adoption (#323), so change proposals had no baseline to diff against. This backfills base specs documenting the CURRENT, already-shipping behavior of the dots CLI. Capabilities: - cli-framework: Cobra entry/command tree, component registry + dispatch, process-exit error model, pkg/run, pkg/path, cli/link, pkg/cache, cli/is - component-install: `dots install` selection, install-all orchestration, and each per-component installer (shell, editor, fonts, packages, languages, macOS defaults, tools, pi.dev) - agent-config-install: `dots install agents` symlinks + idempotent hook and status-line registration in ~/.claude/settings.json - config-update: `dots update` orchestration, weekly auto-clean, `dots clean` - system-doctor: `dots doctor` read-only diagnostics + remediation hints - cli-utilities: the 25 standalone cmd/ binaries Documents current truth (warts included: destructive no-backup symlinks, os.Exit error model). Specs are local docs only — not wired into CI. All six pass `openspec validate --strict`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- openspec/specs/agent-config-install/spec.md | 92 +++++++++ openspec/specs/cli-framework/spec.md | 158 +++++++++++++++ openspec/specs/cli-utilities/spec.md | 130 ++++++++++++ openspec/specs/component-install/spec.md | 206 ++++++++++++++++++++ openspec/specs/config-update/spec.md | 69 +++++++ openspec/specs/system-doctor/spec.md | 39 ++++ 6 files changed, 694 insertions(+) create mode 100644 openspec/specs/agent-config-install/spec.md create mode 100644 openspec/specs/cli-framework/spec.md create mode 100644 openspec/specs/cli-utilities/spec.md create mode 100644 openspec/specs/component-install/spec.md create mode 100644 openspec/specs/config-update/spec.md create mode 100644 openspec/specs/system-doctor/spec.md diff --git a/openspec/specs/agent-config-install/spec.md b/openspec/specs/agent-config-install/spec.md new file mode 100644 index 00000000..75ad58b1 --- /dev/null +++ b/openspec/specs/agent-config-install/spec.md @@ -0,0 +1,92 @@ +# agent-config-install Specification + +## Purpose + +The `agent-config-install` capability defines `dots install agents` — how the +repository's reusable skills, custom agent types, and hooks are wired into Claude +Code and Codex by symlinking directories and mutating `~/.claude/settings.json`. + +This spec documents the behavior that ships today. + +## Requirements + +### Requirement: Skill and Agent Symlinking + +The `agents` installer SHALL ensure `~/.claude` and `~/.agents` exist, then +soft-link `agents/skills` into both `~/.claude/skills` and `~/.agents/skills`, +soft-link `agents/custom` into `~/.claude/agents`, and soft-link +`agents/AGENTS.md` into `~/.claude/CLAUDE.md`. If a required parent directory +cannot be created, the installer SHALL abort the remaining work. + +#### Scenario: Skills linked for both agents + +- **WHEN** the `agents` installer runs +- **THEN** `agents/skills` is soft-linked to both `~/.claude/skills` and `~/.agents/skills` + +#### Scenario: Custom agents and global instructions linked + +- **WHEN** the `agents` installer runs +- **THEN** `agents/custom` is linked to `~/.claude/agents` and `agents/AGENTS.md` is linked to `~/.claude/CLAUDE.md` + +### Requirement: Hook Registration + +The `agents` installer SHALL register hooks in `~/.claude/settings.json`: a +`PreToolUse` hook matching `Skill` (skill-usage tracking), a `SessionStart` hook +(Argus KB memory injection), a `SessionEnd` hook (raw inbox capture), and a +`PostToolUse` hook matching `mcp__argus.*__kb_ingest` (KB change tracking). Each +hook SHALL invoke its script under `agents/hooks/` via `bash`. + +#### Scenario: Skill tracking hook registered + +- **WHEN** the `agents` installer runs against settings without the skill-tracking hook +- **THEN** a `PreToolUse` hook with matcher `Skill` running `agents/hooks/track-skill-use.sh` is added + +#### Scenario: KB ingest matcher covers Argus server names + +- **WHEN** the KB change tracking hook is registered +- **THEN** its `PostToolUse` matcher is `mcp__argus.*__kb_ingest`, covering both legacy and current Argus MCP server names + +### Requirement: Idempotent Hook Registration + +Hook registration SHALL be idempotent. Matcher-style hooks (`PreToolUse`, +`PostToolUse`) SHALL be deduplicated by their `matcher` value; session-style +hooks (`SessionStart`, `SessionEnd`) SHALL be deduplicated by their inner command +string. A hook already present SHALL NOT be added a second time. + +#### Scenario: Re-running does not duplicate hooks + +- **WHEN** the `agents` installer runs a second time +- **THEN** no duplicate hook entries are added to `settings.json` + +### Requirement: Status Line Registration + +The `agents` installer SHALL set `statusLine` in `~/.claude/settings.json` to run +`agents/hooks/statusline.sh` via `bash`, replacing it only when the configured +command differs from the existing one. + +#### Scenario: Status line set when absent or different + +- **WHEN** the `agents` installer runs and the configured status line command is not already present +- **THEN** `settings.statusLine` is set to the statusline script command + +#### Scenario: Status line unchanged when identical + +- **WHEN** the `agents` installer runs and `settings.statusLine` already matches the configured command +- **THEN** `settings.json` is left unchanged for the status line + +### Requirement: Settings File Handling + +Settings mutation SHALL read `~/.claude/settings.json`, treat a missing file as +an empty object, preserve existing keys, and write the result back as indented +JSON. When the file does not yet exist, it SHALL be created with mode `0600`; +when it exists, its current mode SHALL be preserved. + +#### Scenario: Missing settings file treated as empty + +- **WHEN** settings mutation runs and `~/.claude/settings.json` does not exist +- **THEN** mutation proceeds against an empty object and writes a new file with mode 0600 + +#### Scenario: Existing keys preserved + +- **WHEN** settings mutation adds a hook to a file with unrelated keys +- **THEN** the unrelated keys are retained in the written file diff --git a/openspec/specs/cli-framework/spec.md b/openspec/specs/cli-framework/spec.md new file mode 100644 index 00000000..9a1446f5 --- /dev/null +++ b/openspec/specs/cli-framework/spec.md @@ -0,0 +1,158 @@ +# cli-framework Specification + +## Purpose + +The `cli-framework` capability defines how the `dots` binary is structured: the +Cobra-based entry point and command tree, how installable components are +registered and dispatched, the process-exit error model, and the shared support +packages (`pkg/run`, `pkg/path`, `pkg/cache`, `cli/is`, `cli/link`) that every +command builds on. + +This spec documents the behavior that ships today. + +## Requirements + +### Requirement: CLI Entry Point + +The `dots` binary SHALL start from `main.go`, which calls +`cli/commands.Execute()`. `Execute()` SHALL register all top-level commands on a +Cobra root command named `dots` and then run it. Invoking `dots` with no +subcommand SHALL print the help text. + +#### Scenario: Bare invocation prints help + +- **WHEN** a user runs `dots` with no arguments +- **THEN** the root command's `Run` handler invokes `cmd.Help()` and exits 0 + +#### Scenario: Root execution failure exits non-zero + +- **WHEN** `root.Execute()` returns an error +- **THEN** `Execute()` prints the error and calls `os.Exit(1)` + +### Requirement: Command Tree + +The CLI SHALL register the following top-level commands on the root: `install`, +`update` (alias `up`), `clean`, `doctor`, `spinner`, and `docker` (alias +`dock`). Each command SHALL delegate to its package handler or print help when it +is a grouping command with no direct action. + +#### Scenario: Update alias resolves + +- **WHEN** a user runs `dots up` +- **THEN** the CLI resolves the `up` alias to the `update` command and runs `update.Run()` + +#### Scenario: Docker stop-all alias + +- **WHEN** a user runs `dots docker stop` or `dots docker stop-all` +- **THEN** the CLI runs `docker stop $(docker ps -a -q)` to stop all running containers + +#### Scenario: Spinner subcommands + +- **WHEN** a user runs `dots spinner braille`, `dots spinner dots`, `dots spinner circles`, or `dots spinner console` +- **THEN** the corresponding spinner animation runs + +### Requirement: Component Registry and Dispatch + +Installable components SHALL be declared in a single ordered slice returned by +`install.Components()`, each entry holding a `Name`, `Description`, optional +`Alias`, and an installer function `Fn`. `install.Call(name)` SHALL dispatch by +performing a linear name match over that slice and invoking the matched `Fn`. + +#### Scenario: Named dispatch + +- **WHEN** `install.Call("vim")` is invoked +- **THEN** the registry resolves `"vim"` to the `Vim` installer and runs it + +#### Scenario: Unmatched dispatch is a no-op + +- **WHEN** `install.Call(name)` is invoked with a name absent from the registry +- **THEN** no installer runs and the call returns without error + +### Requirement: Process-Exit Error Model + +Command and installer code SHALL surface fatal failures by calling `os.Exit(1)` +rather than returning errors to a central handler. The `install.exec()` helper +SHALL run a command via `pkg/run.Verbose` and call `os.Exit(1)` if it fails. + +#### Scenario: Installer command failure aborts + +- **WHEN** a command run through `install.exec()` returns a non-zero exit +- **THEN** the process exits with code 1 + +### Requirement: Command Execution Helpers + +`pkg/run` SHALL execute shell commands through `zsh -c` with `fmt`-style +formatting of the command string. It SHALL provide `Verbose` (logs then runs, +streams stdout/stderr), `Silent` (runs without logging the command), `Execute` +(logs raw then runs), `Capture` (returns trimmed combined output, logs a warning +on failure), `CaptureClean` (captures with stderr suppressed and surrounding +quotes trimmed), and `OSA` (runs an `osascript -e` command and captures output). + +#### Scenario: Capture returns trimmed output + +- **WHEN** `run.Capture` runs a command that prints output +- **THEN** it returns the combined stdout/stderr with surrounding whitespace trimmed + +#### Scenario: Capture warns on failure + +- **WHEN** the captured command exits non-zero +- **THEN** `run.Capture` logs a warning containing the command and error, and still returns the captured output + +### Requirement: Path Resolution + +`pkg/path` SHALL resolve the dots directory from the `DOTS` environment variable, +defaulting to `~/.dots` when unset. It SHALL provide `Dots()`, `Home()`, +`FromDots(format, args...)`, `FromHome(format, args...)`, `Cache()` (resolving to +`~/.dots/sys/cache`), `FromCache(...)`, and `Pretty()` (which abbreviates the +dots path to `$DOTS` and the home path to `~`). + +#### Scenario: DOTS override + +- **WHEN** the `DOTS` environment variable is set to a custom directory +- **THEN** `path.Dots()` returns that directory instead of `~/.dots` + +#### Scenario: Default dots location + +- **WHEN** the `DOTS` environment variable is unset +- **THEN** `path.Dots()` returns `/.dots` + +### Requirement: Symlink Management + +`cli/link` SHALL create soft links via `link.Soft` (using `os.Symlink`) and hard +links via `link.Hard` (using `os.Link`). Before creating a link, it SHALL remove +any existing file or link at the target path. This overwrite is destructive and +makes no backup. + +#### Scenario: Existing target is replaced + +- **WHEN** `link.Soft(from, to)` is called and a file already exists at `to` +- **THEN** the existing entry is removed and a new symlink to `from` is created in its place + +### Requirement: TTL Cache + +`pkg/cache` SHALL provide file-based caching under `~/.dots/sys/cache`, keyed by +filename. `Warm(key, ttlMinutes)` SHALL return true only when the cache file +exists and its modification time is within the TTL. `Read` SHALL return the +file's contents when within TTL and empty string otherwise. `Touch` SHALL write +an empty value to refresh the key. + +#### Scenario: Warm within TTL + +- **WHEN** a cache key was written less than its TTL ago +- **THEN** `cache.Warm(key, ttl)` returns true + +#### Scenario: Cold past TTL + +- **WHEN** a cache key's file is older than the TTL or does not exist +- **THEN** `cache.Warm(key, ttl)` returns false + +### Requirement: Environment Predicates + +`cli/is` SHALL provide boolean environment checks: `File(path)` (path exists), +`Command(name)` (executable resolvable on `PATH`), `Tmux()` (running inside +tmux), and `Osx()` (operating system is darwin). + +#### Scenario: Command presence check + +- **WHEN** `is.Command("brew")` is called and `brew` is on `PATH` +- **THEN** it returns true diff --git a/openspec/specs/cli-utilities/spec.md b/openspec/specs/cli-utilities/spec.md new file mode 100644 index 00000000..99210cba --- /dev/null +++ b/openspec/specs/cli-utilities/spec.md @@ -0,0 +1,130 @@ +# cli-utilities Specification + +## Purpose + +The `cli-utilities` capability defines the standalone single-purpose binaries +under `cmd/`. Each is built by `go install ./...` and is intended for shell +integration, status lines, and developer workflows. They group into system +status probes, network/IP helpers, git workflow helpers, read-only API clients, +developer tooling, and status-line/UI renderers. + +This spec documents the behavior that ships today. + +## Requirements + +### Requirement: System Status Probes + +The system status utilities SHALL print a single status value to stdout for use +in status lines: `battery-percent` (battery charge with `%` suffix), +`battery-state` (`charging` or `battery`), `cpu` (1-minute load average as a +rounded percentage), and `ssid` (current WiFi network name, with a `--short` +flag that truncates the output). These probes SHALL source their data from macOS +system commands (`pmset`, `sysctl`, `ipconfig`). + +#### Scenario: Battery state reflects power source + +- **WHEN** `battery-state` runs while on AC power +- **THEN** it prints `charging` + +#### Scenario: Short SSID truncation + +- **WHEN** `ssid --short` runs +- **THEN** it prints the network name truncated to at most two words and twelve characters + +### Requirement: Network and IP Helpers + +`ip` SHALL print the machine's IP address in a mode selected by flag: external +(default, cascading through public IP services), `--local` (interface address), +`--router` (LAN gateway), or `--home` (home WAN via DNS lookup of `HOME_WAN`), +caching results for five minutes. `gps` SHALL print `latitude,longitude` derived +from the external IP. `router` SHALL open the LAN router's admin page in the +browser. `home-scp` SHALL copy a file home over SCP using `HOME_USER` and +`HOME_WAN`. + +#### Scenario: Local IP mode + +- **WHEN** `ip --local` runs +- **THEN** it prints the local interface address (defaulting to en0) + +#### Scenario: Router page opened + +- **WHEN** `router` runs and the LAN gateway is resolvable +- **THEN** it opens `http://` in the default browser + +### Requirement: Git Workflow Helpers + +The git helpers SHALL operate relative to a canonical remote and branch. +`git-canonical-remote` SHALL print `upstream` when present, else `origin`; +`git-canonical-branch` SHALL print the canonical branch; `git-canonical-path` +SHALL print `/`; `git-ancestor` SHALL print the nearest ancestor +remote branch of HEAD. `git-masterme` SHALL push the current branch to the +canonical branch; `git-rebase-master` SHALL rebase onto the canonical +remote/branch; `git-reset-hard-master` SHALL hard-reset to it; `git-killme` SHALL +tear down a finished branch, refusing to delete protected branches. + +#### Scenario: Canonical remote prefers upstream + +- **WHEN** `git-canonical-remote` runs in a repo with both `upstream` and `origin` remotes +- **THEN** it prints `upstream` + +#### Scenario: Protected branch not deleted + +- **WHEN** `git-killme` runs on a protected branch (e.g. master, main, dev, staging, production) +- **THEN** it does not delete that branch + +### Requirement: Read-Only API Clients + +`gmail` and `slack` SHALL be read-only clients exposing subcommands for search +and retrieval, supporting a `--json` output mode. `gmail` SHALL load OAuth +credentials from `~/.dots/sys/gmail/tokens/` and refresh expired tokens; `slack` +SHALL authenticate with bot/user tokens from the environment or `~/.dots/sys/env` +and respect rate-limit `Retry-After` headers. `spotify` SHALL control playback of +the currently-playing track (save, remove, transfer, or toggle) via the Spotify +API. + +#### Scenario: Slack history retrieval + +- **WHEN** `slack history --json` runs with a valid token +- **THEN** it prints the channel's recent messages as JSON + +#### Scenario: Spotify toggle saves or removes current track + +- **WHEN** `spotify` runs with no subcommand +- **THEN** it saves the currently-playing track if it is not saved, or removes it if it is + +### Requirement: Developer Tooling + +`skill-usage` SHALL render skill-invocation usage from +`~/.dots/sys/skill-usage/usage.jsonl` as a chart, with a `suggest` subcommand that +surfaces high-leverage and unused skills and a `--json` mode. `search-github` +SHALL open a GitHub code-search URL for a given org and term in the browser. +`version-update` SHALL bump a semantic version tag (patch by default) and create +an annotated git tag. + +#### Scenario: Version bump defaults to patch + +- **WHEN** `version-update` runs with no bump argument +- **THEN** it increments the patch component of the latest `vX.Y.Z` tag and creates the new tag + +#### Scenario: Skill usage suggestions + +- **WHEN** `skill-usage suggest` runs +- **THEN** it reports top, never-used, and rarely-used skills + +### Requirement: Status-Line and UI Renderers + +`tmux-status` SHALL render tmux status-bar segments by position +(`left`/`center`/`right`/`center-current`), scaling detail to the provided width. +`tts` SHALL speak provided text aloud, defaulting to a local Kokoro TTS engine +with an optional `--remote` OpenAI mode, and SHALL skip playback while the +microphone is active. + +#### Scenario: Width-responsive tmux status + +- **WHEN** `tmux-status left ` runs with a narrow width +- **THEN** it renders a reduced-detail status segment + +#### Scenario: TTS skips while mic active + +- **WHEN** `tts ` runs while the microphone is in use +- **THEN** it does not play audio diff --git a/openspec/specs/component-install/spec.md b/openspec/specs/component-install/spec.md new file mode 100644 index 00000000..fe8dbd7e --- /dev/null +++ b/openspec/specs/component-install/spec.md @@ -0,0 +1,206 @@ +# component-install Specification + +## Purpose + +The `component-install` capability defines `dots install` — how a user selects +components, how `install all` orchestrates a full bootstrap, and the +behavior of each individual component installer (shell, editor, fonts, packages, +languages, macOS defaults, developer tools, and the pi.dev agent). + +The `agents` component has its own behavior documented in the +`agent-config-install` capability. + +This spec documents the behavior that ships today, including its destructive, +no-backup nature. + +## Requirements + +### Requirement: Component Selection + +`dots install` with no arguments SHALL present an interactive selection menu +listing `all` followed by every registered component name. `dots install all` +SHALL install every component. `dots install ` SHALL install only the +named component. Running `dots install` with stray positional arguments SHALL +print help and exit 1. + +#### Scenario: Interactive selection of a component + +- **WHEN** a user runs `dots install` and selects a component name from the prompt +- **THEN** that component's installer runs + +#### Scenario: Interactive selection of all + +- **WHEN** a user runs `dots install` and selects `all` +- **THEN** the full install-all orchestration runs + +#### Scenario: Direct named install + +- **WHEN** a user runs `dots install vim` +- **THEN** only the `vim` installer runs + +### Requirement: Component Registry and Aliases + +The installable components SHALL be, in order: `bin`, `git`, `home`, `zsh`, +`fonts`, `homebrew` (alias `brew`), `npm`, `languages`, `vim`, `hammerspoon` +(alias `hs`), `tools`, `osx`, `agents`, and `pi`. Each registered component name +and alias SHALL be exposed as a Cobra subcommand of `install`. + +#### Scenario: Alias resolves to component + +- **WHEN** a user runs `dots install brew` +- **THEN** the `homebrew` installer runs + +#### Scenario: Hammerspoon alias + +- **WHEN** a user runs `dots install hs` +- **THEN** the `hammerspoon` installer runs + +### Requirement: Install-All Orchestration + +`dots install all` SHALL first prime `sudo` access by running a `sudo` echo +command, exiting 1 if it fails, then invoke each component installer in registry +order. + +#### Scenario: Sudo priming precedes installs + +- **WHEN** `dots install all` runs +- **THEN** it requests sudo access before running any component installer, and exits 1 if sudo access cannot be obtained + +### Requirement: Symlink-Based Config Installation + +The `bin`, `git`, `home`, and `hammerspoon` components SHALL install by creating +soft links from the dots repository into the home directory; `fonts` SHALL +install via hard links into `~/Library/Fonts`. These links overwrite existing +targets without backup. The `home` component SHALL link each entry under +`home/` to `~/.`; `hammerspoon` SHALL additionally reload Hammerspoon via +AppleScript after linking. + +#### Scenario: Home dotfiles linked with leading dot + +- **WHEN** the `home` installer runs +- **THEN** each entry under `home/` is soft-linked to `~/.` + +#### Scenario: Fonts hard-linked + +- **WHEN** the `fonts` installer runs +- **THEN** each font under `fonts/` is hard-linked into `~/Library/Fonts/` + +#### Scenario: Hammerspoon reload + +- **WHEN** the `hammerspoon` installer finishes linking `~/.hammerspoon` +- **THEN** it triggers `hs.reload()` in the Hammerspoon application via AppleScript + +### Requirement: Package Installation + +The `homebrew` component SHALL run `brew update`, `brew bundle` against the +repository `Brewfile`, start the `mysql@8.0` and `postgresql@16` services, and +ensure `~/.z` exists. The `npm` component SHALL install a fixed list of global +packages, skipping any already present in the global package list. + +#### Scenario: Homebrew bundle from Brewfile + +- **WHEN** the `homebrew` installer runs +- **THEN** it runs `brew bundle` against the repository `Brewfile` and starts the MySQL and PostgreSQL services + +#### Scenario: npm skips installed packages + +- **WHEN** the `npm` installer runs and a target package is already in the global package list +- **THEN** that package is not reinstalled + +### Requirement: Language Runtime Installation + +The `languages` component SHALL ensure `asdf` is installed, then add plugins and +install pinned versions for Ruby, Python (2 and 3), Terraform, Node.js, and Go, +setting each as the active version. It SHALL also install editor support gems and +pip packages (neovim, pynvim, flake8, wakatime) and link `flake8` into `~/bin`. + +#### Scenario: Pinned runtime versions installed + +- **WHEN** the `languages` installer runs +- **THEN** it installs and activates each language at its pinned version (e.g. Ruby 3.4.7, Go 1.24.10, Node.js 24.2.0) via asdf + +### Requirement: macOS Defaults + +The `osx` component SHALL apply a set of macOS `defaults` and finish by +restarting `Dock`, `Finder`, and `SystemUIServer`. It SHALL refuse to run on a +non-darwin machine, exiting 1. + +#### Scenario: Refuses on non-darwin + +- **WHEN** the `osx` installer runs on a non-darwin operating system +- **THEN** it logs an error and exits 1 without applying any defaults + +#### Scenario: Defaults applied on macOS + +- **WHEN** the `osx` installer runs on macOS +- **THEN** it writes the configured `defaults` and restarts Dock, Finder, and SystemUIServer + +### Requirement: Shell Environment Setup + +The `zsh` component SHALL remove `/etc/zprofile` when present, install the tmux +plugin manager (tpm) and the zinit plugin manager, cloning each idempotently. + +#### Scenario: zprofile removed when present + +- **WHEN** the `zsh` installer runs and `/etc/zprofile` exists +- **THEN** it removes `/etc/zprofile` + +#### Scenario: Plugin managers installed + +- **WHEN** the `zsh` installer runs +- **THEN** it ensures tpm and zinit are cloned and updated to their latest master + +### Requirement: Editor Setup + +The `vim` component SHALL link all `vim/` configuration into `~/.vim`, create +Neovim compatibility links (`~/.config/nvim`, `~/.nvim`, `~/.nvimrc`, +`~/.config/nvim/init.vim`), install `vim-plug` if missing, and update plugins. + +#### Scenario: Neovim compatibility links created + +- **WHEN** the `vim` installer runs +- **THEN** it links `~/.vim` to `~/.config/nvim` and `~/.nvim`, and `~/.vimrc` to the Neovim init paths + +#### Scenario: vim-plug bootstrapped when missing + +- **WHEN** the `vim` installer runs and `~/.vim/autoload/plug.vim` does not exist +- **THEN** it downloads `vim-plug` to that path + +### Requirement: Developer Tool Installation + +The `tools` component SHALL install Devbox, Claude Code, and Codex, skipping each +when the corresponding command is already resolvable on `PATH`. + +#### Scenario: Already-installed tool skipped + +- **WHEN** the `tools` installer runs and `claude` is already on `PATH` +- **THEN** Claude Code is not reinstalled + +#### Scenario: Missing tool installed + +- **WHEN** the `tools` installer runs and `devbox` is not on `PATH` +- **THEN** it installs Devbox via its install script + +### Requirement: pi.dev Agent Installation + +The `pi` component SHALL install the pi.dev CLI when `pi` is not on `PATH`, +always reconcile the `~/.pi/agent/models.json` symlink to the repository copy, +and seed `defaultProvider` and `defaultModel` into `~/.pi/agent/settings.json`. +The settings seed SHALL merge into any existing content, be idempotent when both +fields already match, and write atomically via a tempfile rename. `auth.json` and +`sessions/` SHALL be left untouched. + +#### Scenario: Config symlink reconciled even when installed + +- **WHEN** the `pi` installer runs and `pi` is already on `PATH` +- **THEN** it still re-creates the `~/.pi/agent/models.json` symlink and seeds settings + +#### Scenario: Idempotent settings seed + +- **WHEN** the `pi` installer runs and `defaultProvider` and `defaultModel` already match the seeded values +- **THEN** `settings.json` is left unchanged + +#### Scenario: Settings merge preserves other keys + +- **WHEN** the `pi` installer seeds settings into an existing `settings.json` with other keys +- **THEN** the other keys are preserved and the file is written atomically diff --git a/openspec/specs/config-update/spec.md b/openspec/specs/config-update/spec.md new file mode 100644 index 00000000..979b8fcd --- /dev/null +++ b/openspec/specs/config-update/spec.md @@ -0,0 +1,69 @@ +# config-update Specification + +## Purpose + +The `config-update` capability defines `dots update` and `dots clean` — keeping an +already-installed environment current (pulling the dots repo, updating plugin +managers, packages, runtimes, and tools) and pruning stale package and editor +artifacts. + +This spec documents the behavior that ships today. + +## Requirements + +### Requirement: Update Orchestration + +`dots update` SHALL update the environment in sequence: pull and reinstall the +dots repository (`git fetch`, `git reset --hard origin/master`, `go install +./...`), update ZSH plugins via zinit (pruning broken completion symlinks), +update tmux plugins, update Homebrew and outdated packages, update Claude Code, +Devbox, and the solargraph gem, reshim asdf, and reinstall the `vim` component. +It SHALL set the tmux window title to `update` while running and restore it +afterward. + +#### Scenario: Dots repo pulled and rebuilt + +- **WHEN** `dots update` runs +- **THEN** it fetches and hard-resets the dots repo to `origin/master` and runs `go install ./...` + +#### Scenario: Vim component reinstalled + +- **WHEN** `dots update` runs +- **THEN** it invokes the `vim` installer as part of the update sequence + +### Requirement: Weekly Auto-Clean + +`dots update` SHALL run the cleanup routine before updating only when the +`dots-clean` cache key is older than one week, refreshing that key afterward. +Within a week of the last clean, the cleanup step SHALL be skipped. + +#### Scenario: Clean skipped when recently run + +- **WHEN** `dots update` runs and the `dots-clean` cache key is less than a week old +- **THEN** the cleanup routine is skipped + +#### Scenario: Clean run when stale + +- **WHEN** `dots update` runs and the `dots-clean` cache key is older than a week or absent +- **THEN** the cleanup routine runs and the `dots-clean` key is refreshed + +### Requirement: Conditional Tool Updates + +`dots update` SHALL skip updating Claude Code and Devbox when their commands are +not resolvable on `PATH`, logging that they are not installed rather than failing. + +#### Scenario: Missing tool skipped + +- **WHEN** `dots update` runs and `devbox` is not on `PATH` +- **THEN** the Devbox update is skipped with an informational log and the update continues + +### Requirement: Cleanup Command + +`dots clean` SHALL clean legacy artifacts: `brew cleanup -s` for Homebrew and +`PlugClean!` for Neovim plugins. It SHALL set the tmux window title to `clean` +while running and restore it afterward. + +#### Scenario: Cleanup removes stale artifacts + +- **WHEN** `dots clean` runs +- **THEN** it runs `brew cleanup -s` and removes uninstalled Neovim plugins diff --git a/openspec/specs/system-doctor/spec.md b/openspec/specs/system-doctor/spec.md new file mode 100644 index 00000000..f92cb6db --- /dev/null +++ b/openspec/specs/system-doctor/spec.md @@ -0,0 +1,39 @@ +# system-doctor Specification + +## Purpose + +The `system-doctor` capability defines `dots doctor` — a read-only diagnostic that +checks key environment prerequisites and prints remediation commands for any that +fail, without changing the system. + +This spec documents the behavior that ships today. + +## Requirements + +### Requirement: Diagnostic Checks + +`dots doctor` SHALL check, in order: that the Xcode Command Line Tools are +installed, that ZSH is the default shell, that Homebrew is installed, and that +`/etc/zprofile` has been removed. Each passing check SHALL log a success message +and each failing check SHALL log an error. + +#### Scenario: Default shell check passes + +- **WHEN** `dots doctor` runs and the `SHELL` environment variable points at zsh +- **THEN** it logs that ZSH is the default shell + +#### Scenario: Homebrew check fails + +- **WHEN** `dots doctor` runs and `brew` is not installed +- **THEN** it logs an error that Homebrew is not installed + +### Requirement: Resolution Suggestions + +For each failing check, `dots doctor` SHALL print a suggested resolution as one or +more shell commands. `dots doctor` SHALL be read-only — it SHALL NOT apply any +fix itself. + +#### Scenario: Failed check suggests remediation + +- **WHEN** a diagnostic check fails +- **THEN** `dots doctor` prints the remediation commands for that check and makes no changes to the system