Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions openspec/specs/agent-config-install/spec.md
Original file line number Diff line number Diff line change
@@ -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
158 changes: 158 additions & 0 deletions openspec/specs/cli-framework/spec.md
Original file line number Diff line number Diff line change
@@ -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 `<home>/.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
130 changes: 130 additions & 0 deletions openspec/specs/cli-utilities/spec.md
Original file line number Diff line number Diff line change
@@ -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://<gateway-ip>` 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 `<remote>/<branch>`; `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 <channel> --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 <width>` runs with a narrow width
- **THEN** it renders a reduced-detail status segment

#### Scenario: TTS skips while mic active

- **WHEN** `tts <text>` runs while the microphone is in use
- **THEN** it does not play audio
Loading
Loading