diff --git a/.chezmoiignore b/.chezmoiignore index c2df570..b27d07c 100644 --- a/.chezmoiignore +++ b/.chezmoiignore @@ -22,3 +22,43 @@ plugins.bin plugins.lock renovate.json scripts +tests +provision +mise*.toml +mise*.lock +.miserc.toml +.dockerignore + +# Preserve existing preferences even on the first apply before enrollment. +# After enrollment, missing paths stay ignored too, preserving deletions. +{{ $history := include "provision/dotfiles-history.json" | fromJson }} +{{ range concat $history.shared $history.macos }} +{{ if or (get $ "history" | default false) (stat (joinPath $.chezmoi.homeDir .)) }} +{{ . }} +{{ end }} +{{ end }} + +{{ $harnesses := get . "harnesses" | default (list "opencode") }} +{{ if not (has "pi" $harnesses) }} +.pi +{{ end }} +{{ if not (has "claude" $harnesses) }} +.claude +{{ end }} +{{ if not (has "hermes" $harnesses) }} +.hermes +{{ end }} +{{ if not (has "goose" $harnesses) }} +.config/goose +{{ end }} +{{ if not (has "grok" $harnesses) }} +.grok +{{ end }} +{{ if or (ne .chezmoi.os "darwin") (and (hasKey . "services") (not .services)) }} +Library +{{ end }} +{{ if ne .chezmoi.os "darwin" }} +.config/ghostty +.config/phux-cockpit +.config/token-tach +{{ end }} diff --git a/.chezmoitemplates/agent-orchestration-agreement.md b/.chezmoitemplates/agent-orchestration-agreement.md index dff9c4a..c686884 100644 --- a/.chezmoitemplates/agent-orchestration-agreement.md +++ b/.chezmoitemplates/agent-orchestration-agreement.md @@ -1,8 +1,11 @@ ## Delegation and coordination +- Default to self-direction. Resolve routine implementation, testing, tooling, naming, and reversible local choices from repository evidence and established conventions; state material assumptions and proceed instead of interrupting the user to reduce ordinary uncertainty. +- Own substantial tasks through `understand → decide → design → implement → verify → review → fix → final validation`. The parent personally reads load-bearing code, defines acceptance evidence, and remains accountable for the final diff and outcome. - Delegation is a default capability when the harness provides it. Use it when parallel research, independent review, or a focused specialist improves evidence or throughput; do not delegate trivial work for ceremony. - Roles describe purpose and authority, not capability prisons. Preserve each role's semantics while allowing it to delegate within the active session's capability ceiling. -- The parent owns decomposition, decisions, synthesis, validation, and acceptance. Child output is evidence, not automatic truth. +- The parent owns decomposition, decisions, synthesis, validation, and acceptance. Child output is evidence, not automatic truth. A writer handoff is intermediate until affected checks pass and an independent fresh-context review is dispositioned. - Prefer one writer. Use deliberate worktrees or Blackbird path reservations when parallel writers are genuinely useful, and give each writer a non-overlapping scope. -- Use Blackbird as the durable mail, coordination, reservation, and work/run authority when available. Do not invent a second coordination protocol. -- Keep orchestration controls elastic. Concurrency, depth, budgets, and stop rules are circuit breakers, not targets or reasons to abandon obvious work. +- Recursive delegation is allowed only for distinct lanes that reduce the critical path. Keep orchestration controls elastic: concurrency, depth, budgets, and stop rules are circuit breakers, not targets or reasons to abandon obvious work. +- Use harness-local goal continuation for the active parent session, subagent missions for delegated recovery and accountability, and Blackbird for durable cross-session or cross-project mail, coordination, reservations, and work/run authority. Do not invent a competing protocol. +- Stop for the fixed publication/destructive-action boundary only after every safe local preparation, implementation, and validation step is complete; report the exact external action still requiring authority. diff --git a/.chezmoitemplates/agent-working-agreement.md b/.chezmoitemplates/agent-working-agreement.md index 407d173..48a76c7 100644 --- a/.chezmoitemplates/agent-working-agreement.md +++ b/.chezmoitemplates/agent-working-agreement.md @@ -2,7 +2,7 @@ @@ -49,3 +49,15 @@ reverse — pushing to a repo I don't own, force-pushing, cutting a release, deleting remote state, anything destructive or hard to walk back — still gets asked, every time. "Keep the ball rolling" governs the local, recoverable work sitting in front of you. + +## Complexity discipline + +- Load and follow the shared `cyclomatic-complexity` skill for refactoring, + simplification, cleanup, code-quality review, deeply nested logic, god + functions, or any new nontrivial function with heavy branching. +- Measure touched functions before and after with project-native tooling and + thresholds when available. Preserve behavior and public APIs, run relevant + tests, and report the before/after complexity evidence. +- Never game the number with dense expressions or broaden the task into an + unrelated rewrite. Prefer guard clauses, named predicates, and small, + single-responsibility helpers that improve human readability. diff --git a/.chezmoitemplates/grok.toml b/.chezmoitemplates/grok.toml new file mode 100644 index 0000000..2bf681c --- /dev/null +++ b/.chezmoitemplates/grok.toml @@ -0,0 +1,76 @@ +[ui] +max_thoughts_width = 120 +fork_secondary_model = "grok-4.6" +yolo = false +compact_mode = false +permission_mode = "always-approve" +vim_mode = true + +# Ox Alpha provider catalog. Keys remain machine-local environment state. +[model_providers.ox-opencode] +base_url = "https://opencode.ai/zen/v1" +api_backend = "chat_completions" +env_key = "OPENCODE_ZEN_API_KEY" +context_window = 1048576 + +[model_providers.ox-openrouter] +base_url = "https://openrouter.ai/api/v1" +api_backend = "chat_completions" +env_key = "OPENROUTER_API_KEY" +context_window = 1048576 + +[model_providers.ox-command] +base_url = "https://api.commandcode.ai/provider/v1" +api_backend = "chat_completions" +env_key = "COMMAND_CODE_API_KEY" +context_window = 1048576 + +[model_providers.ox-nous] +base_url = "http://127.0.0.1:8645/v1" +api_backend = "chat_completions" +env_key = "NOUS_API_KEY" +context_window = 1048576 + +[model_providers.ox-venice] +base_url = "https://api.venice.ai/api/v1" +api_backend = "chat_completions" +env_key = "VENICE_API_KEY" +context_window = 1048576 + +[model.ox-opencode] +model = "x-preview-f-free" +name = "Ox Alpha (OpenCode Zen; ZDR)" +description = "Limited-time free stealth preview with zero retention" +model_provider = "ox-opencode" +max_completion_tokens = 131072 + +[model.ox-openrouter] +model = "stealth/ox-alpha" +name = "Ox Alpha (OpenRouter)" +description = "Free stealth preview; upstream retains prompts and completions" +model_provider = "ox-openrouter" +max_completion_tokens = 131072 + +[model.ox-command] +model = "stealth/ox-alpha" +name = "Ox Alpha (Command Code)" +description = "Limited-time free stealth preview; not ZDR" +model_provider = "ox-command" +max_completion_tokens = 131072 + +[model.ox-nous] +model = "stealth/ox-alpha" +name = "Ox Alpha (Nous Portal)" +description = "Free-tier route; enable Nous Privacy Mode separately" +model_provider = "ox-nous" +max_completion_tokens = 131072 + +[model.ox-venice] +model = "stealth-ox-alpha" +name = "Ox Alpha (Venice)" +description = "Free beta route; anonymized third-party inference" +model_provider = "ox-venice" +max_completion_tokens = 131072 + +[models] +default = "grok-4.6" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2e14801 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +**/node_modules +**/target +**/__pycache__ +**/.DS_Store +**/.env +**/.env.* +**/*.local +**/*.local.toml +**/*.secret +**/secrets +graphify-out +artifacts diff --git a/.github/workflows/bootstrap-validation.yml b/.github/workflows/bootstrap-validation.yml new file mode 100644 index 0000000..5e0d62a --- /dev/null +++ b/.github/workflows/bootstrap-validation.yml @@ -0,0 +1,39 @@ +name: Bootstrap acceptance +on: + pull_request: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +jobs: + config: + strategy: + matrix: + os: [macos-15, ubuntu-24.04] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y zsh jq + - uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2 + with: + version: 2026.9.3 + - run: bash tests/bootstrap/check.sh + - run: uv run --script tests/bootstrap/history_test.py + - run: bash tests/gha-local-smoke.sh + container: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + platform: linux/amd64 + profile: container + - os: ubuntu-24.04-arm + platform: linux/arm64 + profile: container,pi + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - run: bash tests/bootstrap/container.sh '${{ matrix.platform }}' '${{ matrix.profile }}' diff --git a/.github/workflows/dependency-validation.yml b/.github/workflows/dependency-validation.yml index e706a8b..28c5b34 100644 --- a/.github/workflows/dependency-validation.yml +++ b/.github/workflows/dependency-validation.yml @@ -22,16 +22,13 @@ jobs: run: | jq empty renovate.json \ dot_config/opencode/create_package.json \ - dot_config/opencode/plugin/autoresearch/package.json - zsh -n dot_zshrc dot_zshenv dot_zprofile - bash -n scripts/install-agent-stack.sh dot_local/bin/executable_dot-install-zsh-plugins + dot_config/opencode/opencode.jsonc + for file in dot_zshrc dot_zshenv dot_zprofile; do zsh -n "$file"; done + for file in scripts/install-agent-stack.sh dot_local/bin/executable_dot-install-zsh-plugins; do bash -n "$file"; done - name: Validate Renovate semantics run: npx --yes --package renovate@44.26.0 renovate-config-validator renovate.json - - name: Test OpenCode autoresearch plugin - working-directory: dot_config/opencode/plugin/autoresearch - run: | - npm ci --ignore-scripts - npm test - name: Test lstags working-directory: src/lstags run: cargo test --locked + - name: Test pinned V2 goal plugin against the managed SDK + run: bash tests/bootstrap/goal-plugin.sh diff --git a/.gitignore b/.gitignore index cff7254..4f444aa 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ bun.lock # Compiled tool build artifacts (source is tracked, target/ is not) src/*/target/ + +# Python test/import caches are runtime state, never chezmoi source. +__pycache__/ +*.pyc diff --git a/.miserc.toml b/.miserc.toml new file mode 100644 index 0000000..8ea934a --- /dev/null +++ b/.miserc.toml @@ -0,0 +1,2 @@ +# Native mise platform environments select mise.macos.toml / mise.linux.toml. +auto_env = true diff --git a/AGENTS.md b/AGENTS.md index 2a9f16d..245f58d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,11 @@ the doc — don't quietly work around it. ## What this repo is Personal dev substrate for one staff engineer, running on Mac (darwin/arm64) -and Raspberry Pi (linux/arm64). **chezmoi**-managed source-of-truth lives at -`~/dotfiles/`, materialized into `$HOME` via `chezmoi apply`. Everything is +and Raspberry Pi (linux/arm64). **Mise history** owns enrolled live preferences: +edit those in `$HOME` and its native watcher saves and synchronizes them. This +repository supplies provisioning code and first-install preference seeds. +**Chezmoi** retains machine-specific templates, executable tools and explicitly +managed application integrations. Everything is **measured** (`dot-bench`), **checked** (`dot-doctor`), and **drift-detectable** (`dot-audit`). The shell is treated as a substrate — every layer is observable. @@ -23,7 +26,7 @@ This is a living organism. Local-maxima fixes get rejected. 1. **No hardcoded user paths in tracked files.** No `/Users/Patrick.Hall`, no `/Users/phall` (use `$HOME`, or chezmoi template `{{ .chezmoi.homeDir }}`). `dot-doctor` enforces this with a grep gate. -2. **`dot_zshenv` stays ≤30 lines.** Every non-interactive zsh invocation pays +2. **Live `.zshenv` and its `dot_zshenv` seed stay ≤30 lines.** Every non-interactive zsh invocation pays its cost. Heavy init goes in `dot_zprofile` (login) or `dot_zshrc` (interactive). Doctor enforces. 3. **Plugins clone OUTSIDE the repo** to `$XDG_DATA_HOME/zsh/plugins/`. @@ -52,6 +55,18 @@ This is a living organism. Local-maxima fixes get rejected. ## Mental model +The initial editable preference set is in `provision/dotfiles-history.json`. +Enrollment seeds `~/.config/mise/conf.d/dotfiles-history.toml`, then that native +configuration is itself live-owned. The watcher saves into mise's bare Git +history and synchronizes with private `phall1/dotfiles-history`. Never connect +automatic history publication to this public provisioning repository. + +`[data] history = true` in machine-local chezmoi configuration removes these +preferences from chezmoi ownership. Edits and intentional deletions survive +later bootstrap runs. Their `dot_*` copies are first-install seeds. + +The remaining source-owned files follow this flow: + ``` edit ┌──────────────────┐ @@ -73,7 +88,10 @@ This is a living organism. Local-maxima fixes get rejected. /zsh/plugins/) ``` -**Source of truth:** `~/dotfiles/dot_*` files. Edit here. +**Live preference source of truth:** the paths shown by `mise bootstrap dotfiles +paths`. Edit these directly and inspect native history. +**Generated/integration source of truth:** the remaining `~/dotfiles/dot_*` +templates, modifiers and executable tools. Edit these in the repository. **Per-machine config:** `~/.config/chezmoi/chezmoi.toml`. Lives outside the repo. Drives templating (`{{ .git.name }}`, etc.). **Materialized state:** `$HOME` — populated by `chezmoi apply`. Real files, @@ -85,6 +103,12 @@ not symlinks (chezmoi's default). ## The change loop (mandatory) +Bootstrap changes must pass `mise run check`, `uv run --script +tests/bootstrap/history_test.py`, and the affected disposable +`tests/bootstrap/container.sh` architecture/profile before live workstation +apply. The default selected harness is OpenCode V2; see `docs/BOOTSTRAP.md` for +optional harnesses and application-owned service/state boundaries. + For any non-trivial change: ```sh @@ -92,28 +116,34 @@ For any non-trivial change: dot-doctor # expect: 0 failures, ≤2 warnings dot-bench # expect: all metrics under PERF.md baselines -# 2. Make the edit in ~/dotfiles/dot_*. -$EDITOR ~/dotfiles/dot_zshrc +# 2. Edit live-owned preferences directly; native history autosaves them. +$EDITOR ~/.zshrc +dot-zcompile +# For provisioning code/generated targets, edit their repository source instead. # 3. Preview. chezmoi diff +mise bootstrap dotfiles status # 4. Apply. chezmoi apply -# (run_onchange_zcompile.sh.tmpl auto-fires when dot_zshrc/dot_zshenv/ -# dot_p10k.zsh content changes.) +# This reconciles only source-owned files after native history enrollment. # 5. Verify. dot-doctor # any new failure = revert dot-bench # >10% regression on any pinned metric = revert OR re-pin # baseline with justification -# 6. Commit. +# 6. Native history autosaves preferences; force an immediate checkpoint if needed. +mise bootstrap dotfiles save +# Commit repository implementation changes separately. git add git commit -m "feat(zsh): add fzf-tab group preview" ``` -**If you skip steps 1, 5, or 6, you are doing it wrong.** +**Baseline and post-change checks remain mandatory.** Automatic history records +edits; it does not certify correctness. Use `mise bootstrap dotfiles rollback +` and `undo` for preference recovery. --- @@ -173,14 +203,16 @@ These are codified in `docs/PLAYBOOKS.md`. Cheat sheet: | Add a doctor check | Drop a file in `checks/*.sh` (or `checks/.sh` for per-tool) using `ok`/`warn`/`fail`/`require_bin`/`want_bin` helpers. See `checks/README.md`. | | Add a bench metric | Already plumbed — zsh-bench output is parsed by metric name. Add a `key: value_ms` pair in `PERF.md` between `BASELINE_START`/`END` markers. | | Add a new $HOME file | Create at `dot_` (or under `dot_config//`) in source. `chezmoi apply`. | -| Add a brew package | Edit `scripts/bootstrap-darwin.sh` `brew_packages` array. Note: `scripts/` are NOT chezmoi-applied. | +| Add a brew package | Edit `provision/Brewfile` (host/tap tools) or `provision/Brewfile.desktop` (GUI apps). Mise invokes real Homebrew. | +| Onboard another machine | `bash ~/dotfiles/scripts/onboard.sh` (standalone download also documented in `docs/SELF-SAVING-DOTFILES.md`); assumes base tools and GitHub login exist. | +| Provision a workstation | `mise bootstrap`; inventories and the isolated test rig are documented in `docs/BOOTSTRAP.md`. | | Add a CLAUDE.md hook / MCP server / skill | Edit `dot_claude/settings.json` for hooks/MCP. Drop a `dot_claude/skills//SKILL.md` for a skill. Run `/discover` after to confirm pickup. | | Add a chezmoi template variable | Add to `~/.config/chezmoi/chezmoi.toml` under `[data]`. Reference as `{{ .key }}` in a `.tmpl` file. | | Add per-machine override | Three options in increasing specificity: chezmoi.toml per machine → hostname branch in `dot_gitconfig.tmpl` → `~/.gitconfig-work` via `includeIf`. See docs/setup.md. | | Set up the alt git identity on a new machine | `~/dotfiles/scripts/setup-alt-identity.sh` — interactive; writes `~/.gitconfig-alt`, generates `~/.ssh/id_ed25519_alt`, runs `gh auth login` into `~/.config/gh-alt/`. Untracked outputs are per-machine. | | Apply the alt identity to a repo | `git identity alt` (one-shot per repo: sets local `user.name`/`email` from `~/.gitconfig-alt`, rewrites origin to the `github.com-alt` SSH alias). `git identity` shows current; `git identity primary` reverts. | | Hit the alt GitHub API | `gh-alt ...` — same surface as `gh`, but `GH_CONFIG_DIR=~/.config/gh-alt` so it always operates on the alt account regardless of `gh auth switch` state. | -| Change the shell prompt | Edit `dot_p10k.zsh` directly OR re-run `p10k configure` and commit the result. | +| Change the shell prompt | Edit live `~/.p10k.zsh` or run `p10k configure`; native history autosaves it. Run `dot-zcompile` and `dot-bench`. | --- @@ -196,8 +228,10 @@ These are codified in `docs/PLAYBOOKS.md`. Cheat sheet: - **Reinventing the doctor's wheel** — the orchestrator (`dot-doctor`) is intentionally dumb. New concerns are NEW files in `checks/`, not edits to the orchestrator. -- **Editing `~/.zshrc` directly** — you'll lose it on next `chezmoi apply`. - Always edit `~/dotfiles/dot_zshrc`. +- **Editing an enrolled preference's seed expecting a live change** — live + preferences are authoritative. Edit `~/.zshrc`; its seed is for new installs. +- **Giving native tracking and chezmoi the same target** — doctor checks this + ownership overlap. Generated/mixed-runtime outputs need explicit handling. - **Committing the rendered `~/.gitconfig`** — it's machine-specific output. Edit `dot_gitconfig.tmpl` or `~/.config/chezmoi/chezmoi.toml` instead. - **Adding "just for now" `set -x` / debug prints in tracked configs** — they @@ -273,7 +307,7 @@ touch gets converted as part of the change. ## Reading more - **`docs/ARCHITECTURE.md`** — the WHY behind every choice (P10k over Starship, - chezmoi over stow, raw zsh over antidote, mise rejected, etc.). + chezmoi over stow, raw zsh over antidote, mise provisioning, etc.). - **`docs/PLAYBOOKS.md`** — full per-task recipes with exact commands. - **`docs/setup.md`** — fresh machine bootstrap, per-machine identity layers. - **`checks/README.md`** — doctor plugin architecture. diff --git a/README.md b/README.md index cf36e7c..1a1e2e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # dotfiles -Personal dev substrate. chezmoi-managed. Mac + Raspberry Pi. +Personal workstation. Mise-provisioned, self-saving live preferences. Mac + Raspberry Pi. + +Edit your enrolled dotfiles where applications read them. Native mise history +autosaves and synchronizes those edits through a private repository. Chezmoi +retains machine-specific templates and explicit integration rules. The shell is a substrate — every layer is measured, checked, drift-detected. Pointing an agent at this repo with a task should produce elite work without @@ -18,6 +22,8 @@ hand-holding. Start with **`AGENTS.md`** (universal) and **`CLAUDE.md`** | **[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md)** | The **why** behind every choice (P10k vs Starship, chezmoi vs stow, raw zsh vs antidote, etc.). | | **[`docs/PLAYBOOKS.md`](./docs/PLAYBOOKS.md)** | Per-task recipes with exact commands. Adding a plugin, bumping a pin, investigating a regression. | | **[`docs/setup.md`](./docs/setup.md)** | Fresh-machine walkthrough + per-machine identity layers. | +| **[`docs/BOOTSTRAP.md`](./docs/BOOTSTRAP.md)** | Mise inventories, profiles, installer ownership, disposable test rig and updates. | +| **[`docs/SELF-SAVING-DOTFILES.md`](./docs/SELF-SAVING-DOTFILES.md)** | Live edits, automatic history/sync, another machine, rollback and conflict recovery. | | **[`PERF.md`](./PERF.md)** | Pinned bench baselines + how to investigate regressions. | | **[`checks/README.md`](./checks/README.md)** | doctor's plugin-check architecture. | | **[`docs/nix.md`](./docs/nix.md)** | Nix install + what is/isn't tracked. | @@ -30,21 +36,25 @@ hand-holding. Start with **`AGENTS.md`** (universal) and **`CLAUDE.md`** | Layer | Pick | Source of authority | |---|---|---| -| Shell | zsh + `dot_zshrc` / `dot_zshenv` / `dot_zprofile` | ARCHITECTURE.md §"substrate" | +| Shell | zsh + live `.zshrc` / `.zshenv` / `.zprofile` | Native mise history; repository copies are seeds | | Prompt | Powerlevel10k + gitstatusd + instant-prompt | ARCHITECTURE.md §"Why P10k" | | Plugin load | Raw `source` + `zsh-defer`, SHA-pinned via `plugins.lock` | ARCHITECTURE.md §"Why raw" | -| History | atuin | `dot_zshrc` | +| Shell history | Native zsh + fzf search | Live `.zshrc` | | `cd` | zoxide | `dot_zshrc` | | Tab | fzf-tab | `plugins.lock` | -| Python | uv | bootstrap-darwin.sh | -| Node | fnm (`--use-on-cd`) | `dot_zshrc` | -| Rust | rustup | bootstrap-darwin.sh | +| Bootstrap / tool versions | mise | `mise.toml`, platform/optional inventories and lockfiles | +| Python | uv | `mise.toml` | +| Node / Bun / Zig | mise | global defaults plus project pins | +| Rust | rustup | native project toolchains | | Go | `GOTOOLCHAIN=auto` | built-in | | Per-dir env | direnv (`.envrc`) + chpwd hook (`.env`) | `dot_zshrc` | | Diff pager | delta | `dot_gitconfig.tmpl` | -| Terminal (Mac) | Ghostty | `dot_config/ghostty/config` | -| Multiplexer | tmux + sesh | `dot_tmux.conf`, `dot_config/sesh/sesh.toml` | -| Dotfile manager | chezmoi | `~/.config/chezmoi/chezmoi.toml` | +| Terminal (Mac) | Ghostty | Live `~/.config/ghostty/config` | +| Persistent terminals | Phux + Cockpit; tmux/sesh available | `dot_config/phux`, `dot_config/phux-cockpit` | +| Agent runtime / coordination | OpenCode V2 + Blackbird | `dot_config/opencode/opencode.jsonc` | +| Git / GitHub UI | Phig + Phui | `dot_config/phig`, `dot_config/phui` | +| Preference history and sync | Native mise watcher | Private `phall1/dotfiles-history` | +| Generated files/integrations | chezmoi | Remaining `dot_*` sources and machine-local data | | Secrets | age (via chezmoi-age) | docs/setup.md §"Secrets" | --- @@ -63,48 +73,81 @@ All extensible — drop a `*.sh` in `checks/` to add a doctor check (see --- -## Bootstrap on a fresh machine +## Onboard your laptop + +With your existing Homebrew/Git tooling, Git identity, GitHub login and mise +>=2026.9.3, run one command: + +```sh +onboard=$(curl -fsSL https://raw.githubusercontent.com/phall1/dotfiles/feat/mise-workstation/scripts/onboard.sh) && bash <<< "$onboard" +``` + +It handles the directory environment, uses your existing GitHub login, adopts private +`phall1/dotfiles-history`, provisions the machine, synchronizes preferences, and +runs health/performance checks. Then open a new terminal. Re-running it preserves +live edits. Any failed validation remains a nonzero exit with the failed check +visible; it never reports a failing setup as complete. + +From an existing checkout: `bash ~/dotfiles/scripts/onboard.sh`. +See [the onboarding guide](docs/SELF-SAVING-DOTFILES.md#shared-repository-and-another-machine) +for the ownership and machine-local login details. + +## Bootstrap from first-install seeds ```sh # 1. Clone. git clone https://github.com/phall1/dotfiles.git ~/dotfiles -# 2. Host bootstrap (installs ~25 tools idempotently, ends with next-step -# instructions). -~/dotfiles/scripts/bootstrap-darwin.sh # Mac -~/dotfiles/scripts/bootstrap-linux.sh # Pi / Linux +# 2. Identity (preserved in machine-local chezmoi data on first bootstrap). +git config --global user.name 'Your Name' +git config --global user.email 'you@example.com' -# 3. Per-machine identity (interactive — prompts for git name/email/key). -~/dotfiles/scripts/setup-chezmoi.sh +# 3. Seed + mise bootstrap: packages, tools, preferences and native integrations. +bash ~/dotfiles/scripts/bootstrap-darwin.sh --yes # Mac (Xcode CLT required) +# or: bash ~/dotfiles/scripts/bootstrap-linux.sh --yes --update -# 4. Apply dotfiles to $HOME. -chezmoi apply - -# 5. Verify substrate health. +# 4. Verify substrate health. ~/.local/bin/dot-doctor ~/.local/bin/dot-bench -# 6. Restart shell. +# 5. Restart shell. exec zsh ``` -Apply auto-triggers `run_once_install-zsh-plugins.sh.tmpl` (clones plugins per -`plugins.lock`) and `run_onchange_zcompile.sh.tmpl` (pre-compiles bytecode -when shell sources change). +First apply installs SHA-pinned shell plugins and compiles the initial shell. +After enrollment, bootstrap and native restore hooks compile live preferences. +For an existing shared setup, follow the private-history `mise bootstrap --adopt` +instructions in [self-saving dotfiles](docs/SELF-SAVING-DOTFILES.md). + +Test before applying to a workstation: + +```sh +cd ~/dotfiles +mise run check +uv run --script tests/bootstrap/history_test.py +bash tests/bootstrap/container.sh linux/arm64 +bash tests/bootstrap/container.sh linux/amd64 +``` + +See [bootstrap ownership and profiles](docs/BOOTSTRAP.md) for optional harnesses, +container isolation, Mac-specific validation and update policy. --- ## Daily flow ```sh -$EDITOR ~/dotfiles/dot_zshrc # source of truth lives in ~/dotfiles -chezmoi diff # preview -chezmoi apply # propagate to $HOME -dot-doctor # verify -dot-bench # verify perf -git commit -m "feat(zsh): ..." # conventional commits +$EDITOR ~/.zshrc # live preferences autosave +dot-zcompile # refresh shell bytecode after edits +dot-doctor # validate live and generated files +dot-bench # performance gate +mise bootstrap dotfiles status # history, watcher and sync state +mise bootstrap dotfiles history --path ~/.zshrc ``` +Edit provisioning scripts and generated-file templates in this repository, +preview with `chezmoi diff`, apply and commit those changes conventionally. + --- ## Layout (chezmoi-flat) diff --git a/checks/00-binaries.sh b/checks/00-binaries.sh index e4ecf0f..8d1bbc8 100644 --- a/checks/00-binaries.sh +++ b/checks/00-binaries.sh @@ -20,11 +20,14 @@ hdr "Wanted binaries" want_bin chezmoi "dotfile manager (task #7)" want_bin age "secret encryption" want_bin uv "Python toolchain" -want_bin fnm "Node toolchain" +want_bin mise "workstation bootstrap and tool versions" want_bin sesh "tmux session picker" -want_bin claude "Claude Code CLI" -want_bin pi "Pi coding agent" +want_bin opencode2 "OpenCode V2 CLI" want_bin blackbird "durable agent coordination" +want_bin phux "persistent terminal server" +want_bin phux-mcp "terminal MCP integration" +want_bin phig "Git history browser" +want_bin phui "GitHub workflow UI" want_bin open-websearch "harness-neutral web research" want_bin lstags "ls + Finder tags (cargo install via run_onchange)" @@ -33,5 +36,5 @@ case "$(uname -s)" in want_bin ghostty "Mac terminal" want_bin act "local GitHub Actions runner" ;; - Linux) want_bin wezterm "Pi terminal (optional)" ;; + Linux) ;; esac diff --git a/checks/01-profile.sh b/checks/01-profile.sh new file mode 100644 index 0000000..e13fba8 --- /dev/null +++ b/checks/01-profile.sh @@ -0,0 +1,13 @@ +# Shared predicates for optional checks. Profiles are rendered from chezmoi +# data; their absence keeps older installations on the OpenCode core. +harness_enabled() { + local profile="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/profile.json" + [[ -f "$profile" ]] || { [[ "$1" == opencode ]]; return; } + jq -e --arg name "$1" '.harnesses | index($name)' "$profile" >/dev/null +} + +services_enabled() { + local profile="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/profile.json" + [[ -f "$profile" ]] || return 0 + jq -e '.services == true' "$profile" >/dev/null +} diff --git a/checks/10-repo-hygiene.sh b/checks/10-repo-hygiene.sh index 2a20eee..b01d38e 100644 --- a/checks/10-repo-hygiene.sh +++ b/checks/10-repo-hygiene.sh @@ -2,19 +2,14 @@ hdr "Repo hygiene" -# No personal username hardcodes in tracked configs. -# Excludes: doctor's own check files (they mention the pattern as a search target), -# raycast author attribution comments, and the .git dir. -_hits=$(grep -rn "Patrick\.Hall" \ - --include="*.zsh" --include="*.sh" --include="*.json" --include="*.jsonc" \ - --include="*.toml" --include=".zshrc" --include=".zshenv" --include=".zprofile" \ - --exclude-dir=".git" --exclude-dir="checks" --exclude="dot-checks.sh" \ - "$DOTFILES" 2>/dev/null | grep -v "raycast.author" || true) +# Chezmoi's extensionless and attribute-prefixed files are configurations too. +_hits=$(git -C "$DOTFILES" grep -n -I -E '/(Users|home)/(phall|Patrick[.]Hall)(/|$)' \ + -- 'dot_*' 'Library/**' '*.tmpl' 'scripts/**' 2>/dev/null || true) if [[ -n "$_hits" ]]; then - fail "hardcoded Patrick.Hall paths remain in tracked configs:" + fail "hardcoded user paths remain in tracked configs:" printf ' %s\n' "$_hits" else - ok "no hardcoded Patrick.Hall paths in tracked configs" + ok "no hardcoded user paths in tracked configs" fi unset _hits diff --git a/checks/blackbird.sh b/checks/blackbird.sh new file mode 100644 index 0000000..6ad5eee --- /dev/null +++ b/checks/blackbird.sh @@ -0,0 +1,17 @@ +hdr "blackbird" +if ! command -v blackbird >/dev/null; then + fail "Blackbird missing — run mise bootstrap" + return 0 +fi +config="$HOME/.config/opencode/opencode.jsonc" +if jq -e '.mcp.servers.blackbird.url == "http://127.0.0.1:8081" and any(.plugins[]; type == "object" and .package == "blackbird-opencode@0.1.3" and .options.baseUrl == "http://127.0.0.1:8080" and (.options | has("token") | not))' "$config" >/dev/null; then + ok "MCP and push-delivery connections configured without literal credentials" +else + fail "Blackbird V2 integration invalid" +fi +if services_enabled; then + if blackbird doctor >/dev/null 2>&1; then ok "native doctor healthy" + else fail "Blackbird service needs attention — blackbird doctor"; fi +else + ok "service lifecycle disabled by this machine profile" +fi diff --git a/checks/chezmoi.sh b/checks/chezmoi.sh index b0e75a9..98f936f 100644 --- a/checks/chezmoi.sh +++ b/checks/chezmoi.sh @@ -27,25 +27,4 @@ else ok "no targets modified outside chezmoi" fi -# A modify_ script's stdout REPLACES its target file, so a script that fails -# and prints nothing truncates the config to zero bytes. That is exactly what -# dot_codex/modify_private_config.toml did once tomlq was installed: its merge fed both -# TOML documents in as one stream, tomlq rejected the duplicate tables, and the -# script emitted nothing. Checking "is tomlq installed" would not have caught -# it -- installing tomlq is what triggered it. So smoke-test the real invariant -# instead: feed the script the live config and require usable output back. -modify_script="$DOTFILES/dot_codex/modify_private_config.toml" -codex_cfg="$HOME/.codex/config.toml" -if [[ -f "$modify_script" && -f "$codex_cfg" ]]; then - out=$(sh "$modify_script" < "$codex_cfg" 2>/dev/null) - if [[ -z "$out" ]]; then - fail "codex modify_ script produced EMPTY output — 'chezmoi apply' would truncate $codex_cfg" - elif ! printf '%s\n' "$out" | grep -qE '^model[[:space:]]*='; then - fail "codex modify_ output is missing the [model] key — merge is mangling the config" - else - n=$(printf '%s\n' "$out" | grep -c .) - ok "codex modify_ script round-trips cleanly ($n lines)" - fi -fi - fi diff --git a/checks/claude.sh b/checks/claude.sh index 241332e..9740db5 100644 --- a/checks/claude.sh +++ b/checks/claude.sh @@ -1,9 +1,10 @@ # claude checks. chezmoi-aware: files are real copies, not symlinks. +harness_enabled claude || return 0 hdr "claude" if [[ -f "$HOME/.claude/settings.json" ]]; then - if python3 -c "import json,sys; json.load(open('$HOME/.claude/settings.json'))" 2>/dev/null; then + if jq empty "$HOME/.claude/settings.json" 2>/dev/null; then ok "settings.json parses" else fail "settings.json does not parse as JSON" diff --git a/checks/history.sh b/checks/history.sh new file mode 100644 index 0000000..33a177e --- /dev/null +++ b/checks/history.sh @@ -0,0 +1,43 @@ +# shellcheck shell=bash +# Native mise history is the owner of enrolled live preferences. +[[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/mise/conf.d/dotfiles-history.toml" ]] || return 0 +hdr "self-saving dotfiles" +if history_status="$(mise bootstrap dotfiles status --json 2>/dev/null)"; then + if jq -e '.history.unavailable != null' <<< "$history_status" >/dev/null; then + fail "native history is unavailable — mise bootstrap dotfiles status" + elif jq -e '.history.checkpoints > 0 and .history.pending_operations == 0' <<< "$history_status" >/dev/null; then + ok "native history has recoverable checkpoints and no unfinished operations" + else + warn "history checkpoint pending — let bootstrap finish, then inspect mise bootstrap dotfiles status" + fi + if services_enabled; then + if jq -e '.history.watcher == "running"' <<< "$history_status" >/dev/null; then + ok "native history watcher running" + else + fail "native history watcher is not running — mise bootstrap services apply" + fi + fi + if jq -e '.history.sync != null' <<< "$history_status" >/dev/null; then + if jq -e '.history.sync | (.conflicts | length) > 0 or .application_failure != null or .validation_error != null' <<< "$history_status" >/dev/null; then + fail "history synchronization is paused — mise bootstrap dotfiles status" + elif jq -e '.history.sync.last_error != null' <<< "$history_status" >/dev/null; then + warn "history synchronization retry pending — mise bootstrap dotfiles status" + else + ok "private history origin has no reported synchronization conflicts or errors" + fi + elif services_enabled; then + warn "history is local-only until a private origin is connected" + fi + history_managed="$(chezmoi managed --include files,symlinks)" + history_overlap=0 + while IFS= read -r target; do + relative="${target#\~/}" + if awk -v target="$relative" '$0 == target || index($0, target "/") == 1 {found=1} END {exit !found}' <<< "$history_managed"; then + fail "double-owned preference: $target" + history_overlap=1 + fi + done < <(jq -r '.files[] | select(.mode == "track") | .target' <<< "$history_status") + [[ "$history_overlap" == 1 ]] || ok "native live preferences and chezmoi targets have no overlap" +else + fail "native history status unavailable" +fi diff --git a/checks/opencode.sh b/checks/opencode.sh new file mode 100644 index 0000000..012dc2e --- /dev/null +++ b/checks/opencode.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# OpenCode V2-only configuration and direct OpenAI model exposure. + +hdr "opencode" + +config="$HOME/.config/opencode/opencode.jsonc" + +if [[ -f "$config" ]] && jq -e ' + .permissions + and .mcp.servers + and (.update == "auto") + and .plugins + and (has("permission") | not) + and (has("provider") | not) + and (has("plugin") | not) +' "$config" >/dev/null 2>&1; then + ok "native V2 config with automatic updates" +else + fail "OpenCode config is missing native V2 shape or automatic updates" +fi + +if services_enabled && command -v opencode2 >/dev/null 2>&1; then + if ! opencode_models="$(opencode2 models 2>/dev/null)"; then + fail "OpenCode could not enumerate its model catalog" + elif grep -qx 'openai/gpt-6-astra' <<< "$opencode_models"; then + ok "OpenAI GPT-6 Astra is selectable" + elif [[ -n "${OPENAI_API_KEY:-}" ]] || jq -e 'has("openai")' "${XDG_DATA_HOME:-$HOME/.local/share}/opencode/auth.json" >/dev/null 2>&1; then + fail "OpenAI GPT-6 Astra is absent from the runtime model catalog" + else + ok "OpenAI is not connected yet — use /connect to enable its model catalog" + fi +fi + +obsolete='' +for path in \ + "$HOME/.config/oc" \ + "$HOME/.local/bin/opencode-ohmy" \ + "$HOME/.local/bin/opencode-safe" +do + [[ -e "$path" || -L "$path" ]] && obsolete+=" ${path/#$HOME/~}" +done + +if [[ -z "$obsolete" ]]; then + ok "legacy OpenCode and OpenAgent launchers absent" +else + fail "legacy OpenCode/OpenAgent paths remain:$obsolete" +fi diff --git a/checks/ox-alpha.sh b/checks/ox-alpha.sh old mode 100755 new mode 100644 index 93df4b1..89c2da5 --- a/checks/ox-alpha.sh +++ b/checks/ox-alpha.sh @@ -1,97 +1,40 @@ -#!/usr/bin/env bash -# Secret-free Ox Alpha provider catalog across installed coding harnesses. - +# Only selected harnesses participate in workstation health. The isolated +# fixture suite validates every optional source, even when it is not applied. hdr "ox alpha routes" - -ox_bin="$HOME/.local/bin/ox" -pi_models="$HOME/.pi/agent/models.json" -opencode_config="$HOME/.config/opencode/opencode.jsonc" -hermes_config="$HOME/.hermes/config.yaml" -goose_providers="$HOME/.config/goose/custom_providers" -grok_config="$HOME/.grok/config.toml" -codex_config="$HOME/.codex/config.toml" -pi_models_modify="$DOTFILES/dot_pi/private_agent/modify_private_models.json" -hermes_modify="$DOTFILES/dot_hermes/modify_private_config.yaml" - -if [[ -x "$ox_bin" ]] && "$ox_bin" status >/dev/null 2>&1; then - ok "ox launcher and credential inventory" -else - fail "ox launcher missing or broken" -fi - -expected_pi='["ox-opencode","ox-openrouter","ox-command","ox-nous","ox-venice"]' -if [[ -f "$pi_models" ]] && jq -e --argjson expected "$expected_pi" ' - (.providers | keys) as $keys - | ($expected - $keys | length) == 0 - and ([.providers[$expected[]].models[0].contextWindow] | all(. == 1048576)) - and ([.providers[$expected[]].models[0].maxTokens] | all(. == 131072)) -' "$pi_models" >/dev/null 2>&1; then - ok "Pi Ox provider catalog" -else - fail "Pi Ox provider catalog missing or inconsistent" -fi - -if [[ "$(printf '%s' 'not-json' | "$pi_models_modify")" == not-json ]] && [[ "$(printf '%s' 'not: [valid' | "$hermes_modify")" == 'not: [valid' ]]; then - ok "Pi and Hermes provider merges fail closed" -else - fail "Pi or Hermes provider merge erases malformed runtime state" -fi - -if [[ -f "$opencode_config" ]] && jq -e ' - .provider.opencode.models["x-preview-f-free"] - and .provider.openrouter.models["stealth/ox-alpha"] - and .provider["ox-command"].models["stealth/ox-alpha"] - and .provider["ox-nous"].models["stealth/ox-alpha"] - and .provider["ox-venice"].models["stealth-ox-alpha"] -' "$opencode_config" >/dev/null 2>&1; then - if ! command -v opencode >/dev/null 2>&1; then - warn "OpenCode Ox catalog present; runtime validation unavailable on this host" - elif opencode debug config >/dev/null 2>&1; then - ok "OpenCode config parses with five Ox routes" - else - fail "OpenCode rejected its managed config" - fi -else - fail "OpenCode Ox routes missing or invalid" -fi - -if [[ -f "$hermes_config" ]] && yq -e ' - [.custom_providers[] | select(.name == "ox-command" or .name == "ox-venice")] | length == 2 -' "$hermes_config" >/dev/null 2>&1; then - ok "Hermes native plus custom Ox routes" -else - fail "Hermes Ox custom routes missing" -fi - -missing_goose='' -for provider in opencode openrouter command nous venice; do - file="$goose_providers/ox_${provider}.json" - if [[ ! -f "$file" ]] || ! jq -e '.engine=="openai" and .requires_auth==true and .models[0].context_limit==1048576' "$file" >/dev/null 2>&1; then - missing_goose+=" $provider" - fi -done -if [[ -z "$missing_goose" ]]; then - ok "Goose declarative Ox providers" -else - fail "Goose Ox providers missing or invalid:${missing_goose}" -fi - -if [[ ! -f "$grok_config" ]] || ! yq -p=toml -e '.model."ox-opencode" and .model."ox-venice"' "$grok_config" >/dev/null 2>&1; then - fail "Grok Ox model catalog missing or invalid" -elif ! command -v grok >/dev/null 2>&1; then - warn "Grok Ox catalog present; runtime validation unavailable on this host" -elif grok models 2>/dev/null | grep -q 'ox-opencode' && grok models 2>/dev/null | grep -q 'ox-venice'; then - ok "Grok custom Ox model catalog" -else - fail "Grok rejected its Ox model catalog" -fi - -if [[ -f "$codex_config" ]] && grep -q '^\[model_providers\.ox-openrouter\]$' "$codex_config" && grep -q '^\[model_providers\.ox-venice\]$' "$codex_config" && [[ -f "$HOME/.codex/ox-openrouter.config.toml" && -f "$HOME/.codex/ox-venice.config.toml" ]]; then - ok "Codex Responses profiles for OpenRouter and Venice" -else - fail "Codex Ox Responses profiles missing" -fi - -for binary in pi opencode hermes goose grok codex; do - command -v "$binary" >/dev/null 2>&1 || warn "$binary missing — Ox route configured but unavailable on this host" +if "$HOME/.local/bin/ox" status >/dev/null 2>&1; then ok "Ox launcher healthy" +else fail "Ox launcher missing or broken"; fi +if jq -e '.providers.opencode.models["x-preview-f-free"] and .providers.openrouter.models["stealth/ox-alpha"] and .providers["ox-command"].models["stealth/ox-alpha"] and .providers["ox-nous"].models["stealth/ox-alpha"] and .providers["ox-venice"].models["stealth-ox-alpha"]' "$HOME/.config/opencode/opencode.jsonc" >/dev/null 2>&1; then + ok "OpenCode provider fleet configured" +else + fail "OpenCode provider fleet invalid" +fi +if services_enabled; then + if opencode2 debug config >/dev/null 2>&1; then ok "OpenCode accepts the managed V2 config" + else fail "OpenCode rejected the managed config"; fi +fi +if harness_enabled pi; then + if jq -e --argjson expected '["ox-opencode","ox-openrouter","ox-command","ox-nous","ox-venice"]' ' + (.providers | keys) as $keys | ($expected - $keys | length) == 0 + and ([.providers[$expected[]].models[0].contextWindow] | all(. == 1048576)) + and ([.providers[$expected[]].models[0].maxTokens] | all(. == 131072)) + ' "$HOME/.pi/agent/models.json" >/dev/null 2>&1; then ok "Pi Ox provider catalog valid" + else fail "Pi Ox catalog invalid"; fi +fi +if harness_enabled hermes; then + if yq -e '[.custom_providers[] | select(.name == "ox-command" or .name == "ox-venice")] | length == 2' "$HOME/.hermes/config.yaml" >/dev/null 2>&1; then ok "Hermes Ox custom routes valid" + else fail "Hermes Ox custom routes missing"; fi +fi +if harness_enabled goose; then + for provider in opencode openrouter command nous venice; do + if jq -e '.engine == "openai" and .requires_auth and .models[0].context_limit == 1048576' "$HOME/.config/goose/custom_providers/ox_$provider.json" >/dev/null 2>&1; then ok "Goose Ox $provider valid" + else fail "Goose Ox $provider missing or invalid"; fi + done +fi +if harness_enabled grok; then + if yq -p=toml -o=json -e '.model."ox-opencode" and .model."ox-venice"' "$HOME/.grok/config.toml" >/dev/null 2>&1; then ok "Grok Ox catalog valid" + else fail "Grok Ox catalog invalid"; fi +fi +for harness in pi claude hermes goose grok; do + harness_enabled "$harness" || continue + command -v "$harness" >/dev/null || fail "selected harness $harness is not installed" done diff --git a/checks/phux.sh b/checks/phux.sh new file mode 100644 index 0000000..9cfaaa1 --- /dev/null +++ b/checks/phux.sh @@ -0,0 +1,5 @@ +hdr "personal terminal tools" +if phux config check >/dev/null 2>&1; then ok "Phux layered configuration valid" +else fail "Phux config invalid — phux config check"; fi +if phig config check >/dev/null 2>&1; then ok "Phig preferences valid" +else fail "Phig config invalid — phig config check"; fi diff --git a/checks/pi.sh b/checks/pi.sh index e9d40f8..02b0ff0 100644 --- a/checks/pi.sh +++ b/checks/pi.sh @@ -1,31 +1,49 @@ # Pi agent-stack checks. Runtime-owned state is inspected, never reconciled here. +harness_enabled pi || return 0 hdr "pi agent stack" pi_settings="$HOME/.pi/agent/settings.json" pi_modify="$DOTFILES/dot_pi/private_agent/modify_settings.json" subagent_config="$HOME/.pi/agent/extensions/subagent/config.json" +goal_config="$HOME/.pi/agent/pi-goal.json" module_bridge="$HOME/.pi/agent/node_modules" mcp_config="$HOME/.config/mcp/mcp.json" if command -v pi >/dev/null 2>&1; then version="$(pi --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" - [[ "$version" == "0.84.1" ]] && ok "Pi version pinned ($version)" || warn "Pi version is ${version:-unknown}; expected 0.84.1" + pi_pin="$(yq -p=toml -o=json -r '.tools."npm:@earendil-works/pi-coding-agent"' "$DOTFILES/mise.pi.toml")" + if [[ -n "$pi_pin" && "$version" == "$pi_pin" ]]; then + ok "Pi matches the installer pin ($version)" + else + warn "Pi is ${version:-unknown}; mise.pi.toml pins ${pi_pin:-unknown}" + fi +fi + +if command -v node >/dev/null 2>&1 && node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit(major > 22 || (major === 22 && minor >= 19) ? 0 : 1)' >/dev/null 2>&1; then + ok "Node satisfies Pi runtime floor ($(node --version))" +else + fail "Pi 0.84.x requires Node >=22.19.0" fi if [[ -f "$pi_settings" ]] && jq -e 'type == "object"' "$pi_settings" >/dev/null 2>&1; then ok "settings.json parses" [[ "$(jq -r '.defaultProjectTrust // empty' "$pi_settings")" == always ]] && ok "project trust defaults to always" || fail "defaultProjectTrust is not always" - missing="$(jq -r '["npm:pi-subagents@0.47.1","npm:@juicesharp/rpiv-ask-user-question@2.4.0","npm:@narumitw/pi-goal@0.51.0","npm:@ff-labs/pi-fff@0.10.3","npm:pi-mcp-adapter@2.23.0","npm:pi-web-access@0.22.0"] - (.packages // []) | .[]' "$pi_settings")" + missing="$(jq -r '["npm:pi-subagents@0.47.1","npm:@narumitw/pi-goal@0.51.0","npm:@ff-labs/pi-fff@0.10.3","npm:pi-mcp-adapter@2.23.0","npm:pi-web-access@0.22.0","npm:@osolmaz/pi-workflows@0.13.4"] - (.packages // []) | .[]' "$pi_settings")" [[ -z "$missing" ]] && ok "portable Pi package pins present" || fail "missing managed Pi package pin(s): ${missing//$'\n'/, }" + if jq -e '[(.packages // [])[] | if type == "object" then .source else . end | select(type == "string" and test("rpiv-ask-user-question"))] | length == 0' "$pi_settings" >/dev/null 2>&1; then + ok "user-question package absent" + else + fail "retired user-question package is still configured" + fi else fail "~/.pi/agent/settings.json missing or invalid" fi if [[ -x "$pi_modify" ]]; then - synthetic='{"defaultProvider":"local","defaultModel":"keep-me","defaultThinkingLevel":"low","runtime":{"token":"keep"},"packages":["npm:pi-subagents@old","git:example/tool"]}' + synthetic='{"defaultProvider":"local","defaultModel":"keep-me","defaultThinkingLevel":"low","runtime":{"token":"keep"},"packages":["npm:pi-subagents@old","npm:@juicesharp/rpiv-ask-user-question@2.4.0","git:example/tool"]}' roundtrip="$(printf '%s' "$synthetic" | "$pi_modify" 2>/dev/null)" - if jq -e '.defaultProvider=="local" and .defaultModel=="keep-me" and .defaultThinkingLevel=="low" and .runtime.token=="keep" and (.packages|index("git:example/tool")) and (.packages|index("npm:pi-subagents@0.47.1")) and .defaultProjectTrust=="always"' <<<"$roundtrip" >/dev/null 2>&1; then + if jq -e '.defaultProvider=="local" and .defaultModel=="keep-me" and .defaultThinkingLevel=="low" and .runtime.token=="keep" and (.packages|index("git:example/tool")) and (.packages|index("npm:pi-subagents@0.47.1")) and (.packages|index("npm:@osolmaz/pi-workflows@0.13.4")) and ((.packages|map(tostring)|map(contains("rpiv-ask-user-question"))|any) | not) and .subagents.watchdog.enabled==true and .subagents.watchdog.main.enabled==true and .defaultProjectTrust=="always"' <<<"$roundtrip" >/dev/null 2>&1; then ok "modify_settings preserves runtime keys and enforces managed values" else fail "modify_settings semantic roundtrip failed" @@ -46,40 +64,47 @@ else fail "subagent config missing or inconsistent" fi +if [[ -f "$pi_settings" ]] && jq -e '.subagents.watchdog.enabled==true and .subagents.watchdog.main.enabled==true and .subagents.watchdog.children.enabled==false and .subagents.watchdog.autoFollow.blockers==true and .subagents.watchdog.autoFollow.maxAttempts==2 and .subagents.watchdog.autoFollow.stalemateRepeats==2' "$pi_settings" >/dev/null 2>&1; then + ok "main-session adversarial watchdog configured" +else + fail "subagent watchdog config missing or inconsistent" +fi + +if [[ -f "$goal_config" ]] && jq -e '.toolVisibility=="always" and .experimental.goals==true and .rpc.enabled==false and .continuationLimits.automaticTurns==100 and .continuationLimits.noProgressTurns==3' "$goal_config" >/dev/null 2>&1; then + ok "autonomous goal continuation configured" +else + fail "pi-goal config missing or inconsistent" +fi + if [[ -f "$mcp_config" ]] && jq -e '.mcpServers.blackbird.url=="http://127.0.0.1:8081" and .mcpServers.blackbird.directTools==true and .mcpServers.blackbird.toolPrefix=="none"' "$mcp_config" >/dev/null 2>&1; then ok "shared Blackbird MCP endpoint configured" else fail "shared Blackbird MCP config missing or inconsistent" fi -for skill in blackbird web-research; do +for skill in blackbird web-research cyclomatic-complexity; do [[ -f "$HOME/.agents/skills/$skill/SKILL.md" ]] && ok "shared $skill skill" || fail "shared $skill skill missing" done -if command -v blackbird >/dev/null 2>&1; then - # The expected version is read from the installer rather than repeated here; - # the two drifted apart once already. - bb_version="$(blackbird --version 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" - bb_pin="$(sed -n 's/^BLACKBIRD_VERSION=//p' "$DOTFILES/scripts/install-agent-stack.sh" | head -1)" - if [[ -n "$bb_pin" && "$bb_version" == "$bb_pin" ]]; then - ok "Blackbird matches the installer pin ($bb_version)" +complexity_skill="$HOME/.agents/skills/cyclomatic-complexity/SKILL.md" +for adapter in \ + "$HOME/.claude/skills/cyclomatic-complexity/SKILL.md" \ + "$HOME/.config/opencode/skill/cyclomatic-complexity/SKILL.md" \ + "$HOME/.hermes/skills/cyclomatic-complexity/SKILL.md" +do + case "$adapter" in + "$HOME/.claude/"*) harness_enabled claude || continue ;; + "$HOME/.hermes/"*) harness_enabled hermes || continue ;; + esac + if [[ -L "$adapter" && "$adapter" -ef "$complexity_skill" ]]; then + ok "cyclomatic-complexity adapter: ${adapter#"$HOME"/}" else - warn "Blackbird is ${bb_version:-unknown}; scripts/install-agent-stack.sh pins ${bb_pin:-unknown}" + fail "cyclomatic-complexity adapter missing or stale: ${adapter#"$HOME"/}" fi - # blackbird doctor supersedes the per-binary probes: the Go companions stopped - # shipping in v0.3.0, and doctor checks the service definition, a real daemon - # handshake, and the database. It exits 5 when a check fails and 0 otherwise, - # so warnings stay advisory unless --strict is passed. - bb_rc=0; blackbird doctor >/dev/null 2>&1 || bb_rc=$? - if (( bb_rc == 0 )); then - ok "Blackbird doctor reports no failures" - else - fail "Blackbird doctor reports failures — run: blackbird doctor" - fi -fi +done if [[ -d "$HOME/.pi/agent/npm/node_modules" ]]; then - for spec in 'pi-subagents:0.47.1' '@juicesharp/rpiv-ask-user-question:2.4.0' '@narumitw/pi-goal:0.51.0' '@ff-labs/pi-fff:0.10.3' 'pi-mcp-adapter:2.23.0' 'pi-web-access:0.22.0'; do + for spec in 'pi-subagents:0.47.1' '@narumitw/pi-goal:0.51.0' '@ff-labs/pi-fff:0.10.3' 'pi-mcp-adapter:2.23.0' 'pi-web-access:0.22.0' '@osolmaz/pi-workflows:0.13.4'; do pkg="${spec%:*}"; expected="${spec##*:}"; manifest="$HOME/.pi/agent/npm/node_modules/$pkg/package.json" if [[ ! -f "$manifest" ]]; then warn "$pkg not installed yet" elif [[ "$(jq -r .version "$manifest")" == "$expected" ]]; then ok "$pkg@$expected installed" diff --git a/checks/zsh.sh b/checks/zsh.sh index 7c3ae92..14ab9a3 100644 --- a/checks/zsh.sh +++ b/checks/zsh.sh @@ -4,15 +4,15 @@ hdr "zsh" # .zshenv should be lean — every non-interactive shell pays its cost. ZSHENV_MAX_LINES="${ZSHENV_MAX_LINES:-30}" -if [[ -f "$DOTFILES/dot_zshenv" ]]; then - lines=$(wc -l < "$DOTFILES/dot_zshenv") +if [[ -f "$HOME/.zshenv" ]]; then + lines=$(wc -l < "$HOME/.zshenv") if [[ "$lines" -gt "$ZSHENV_MAX_LINES" ]]; then warn ".zshenv has $lines lines (>$ZSHENV_MAX_LINES) — non-interactive shells pay this every invocation" else ok ".zshenv lean ($lines lines)" fi else - warn "dot_zshenv missing in source" + warn ".zshenv missing from HOME" fi # Completion cache freshness. Age is NOT the signal — dot_zshrc keys cache @@ -50,7 +50,7 @@ fi # P10k state: instant-prompt cache + gitstatusd daemon. ZSH_PLUGIN_DIR="${ZSH_PLUGIN_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/zsh/plugins}" if [[ -d "$ZSH_PLUGIN_DIR/powerlevel10k" ]]; then - cache="${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${USER}.zsh" + cache="${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${USER:-$(id -un)}.zsh" if [[ -f "$cache" ]]; then ok "p10k instant-prompt cache present" else @@ -77,7 +77,7 @@ fi # ~/.zsh_early, sourced BEFORE the preamble. Enforce two things: # (a) source-side ordering invariant — the hook exists and precedes the preamble. # (b) runtime hygiene — late-sourced machine-local files don't echo at load time. -zshrc_src="$DOTFILES/dot_zshrc" +zshrc_src="$HOME/.zshrc" if [[ -f "$zshrc_src" ]]; then early_ln=$(grep -n '\.zsh_early' "$zshrc_src" | head -1 | cut -d: -f1) ip_ln=$(grep -n 'p10k-instant-prompt' "$zshrc_src" | head -1 | cut -d: -f1) @@ -130,7 +130,7 @@ fi # so for chezmoi-applied files the bytecode lives at $HOME/.zshrc.zwc, not source. if [[ -f "$HOME/.zshrc.zwc" ]]; then if [[ "$HOME/.zshrc" -nt "$HOME/.zshrc.zwc" ]]; then - warn ".zshrc is newer than .zwc — chezmoi apply should re-run dot-zcompile" + warn ".zshrc is newer than .zwc — run dot-zcompile after editing live shell preferences" else ok ".zshrc bytecode fresh" fi diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 72ed493..2350dae 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,32 +7,63 @@ If a choice here is wrong, propose changing it — don't quietly work around it. ## Portable agent stack +Editable shell, editor and shared-agent preferences are now live-owned by native +mise history. The watcher saves and synchronizes them through a separate private +origin. This provisioning repository retains first-install seeds, generated +templates, tool inventories and integration code. Chezmoi excludes enrolled +targets so bootstrap cannot overwrite edits or restore intentional deletions. +See [ownership and recovery](SELF-SAVING-DOTFILES.md) for the exact boundary. + The global agent substrate is layered rather than harness-specific: +OpenCode V2 is installed by default. Other harnesses participate when selected +in the machine's [bootstrap profile](BOOTSTRAP.md#profiles). + 1. Shared chezmoi templates define the autonomy boundary plus delegation and - coordination. Claude, Codex, OpenCode, and Pi render both agreements. + coordination. Claude, OpenCode, and Pi render both agreements. 2. `~/.agents/skills/` is the canonical harness-neutral skill home. Harnesses that need adapters receive chezmoi symlinks rather than copied skills. + Third-party skills are vendored with an upstream commit and license; the + global working agreement makes cyclomatic-complexity review part of the + default code-quality posture in every rendered agent instruction file. 3. `~/.config/mcp/mcp.json` is the secret-free shared MCP layer. Blackbird at `127.0.0.1:8081` is the sole durable mail, reservation, and run authority. -4. Thin launchers select Pi profiles: Commander is the productive default, +4. When Pi is selected, thin launchers select Pi profiles: Commander is its productive default, Spartan is read-only for local code/command execution with a monotonic delegated ceiling, and YOLO explicitly widens delegation depth. Spartan may still mutate explicit coordination state (mail, acknowledgements, and path reservations). Profiles never pin a model or provider. Pi settings use a two-writer `modify_` merge. Chezmoi owns theme, project trust, -telemetry, reviewed package pins, and role infrastructure. Pi owns auth, trust -records, sessions, caches, provider/model/thinking choices, and unknown runtime -keys. `scripts/install-agent-stack.sh` installs reviewed CLI versions. A -portable `~/.pi/agent/node_modules -> npm/node_modules` bridge lets tracked Pi -extensions import public package APIs without private paths or copied protocol. +telemetry, reviewed package pins, explicit retired-package removal, and role +infrastructure. The global package set includes Pi Workflows so its extension and +bundled workflow skills are active in every Pi session; the compatible Pi core +version is pinned alongside it. Workflow and controller modules are executable +code, so the existing `defaultProjectTrust: always` policy deliberately treats +every opened repository as trusted rather than sandboxed. Pi owns auth, trust +records, sessions, caches, +provider/model/thinking choices, and unknown runtime keys. +Mise's optional Pi inventory installs the reviewed CLI version. A portable +`~/.pi/agent/node_modules -> npm/node_modules` bridge lets tracked Pi extensions +import public package APIs without private paths or copied protocol. + +Pi's default posture is an autonomous parent-led engineering team rather than a +questionnaire. The role adapter defensively removes user-question tools while +preserving child-to-parent coordination. The shared orchestration agreement +requires evidence-gated end-to-end ownership, one writer per checkout, useful +rather than performative recursion, and fresh independent review. Parent-session +continuation uses pi-goal with a 100-response safety epoch and a three-run +no-progress guard; delegated recovery uses missions; cross-session/project work +uses Blackbird. A main-session watchdog reviews changed repo state and can drive +at most two blocker fix attempts. Commander depth remains 4 and YOLO depth 8: +more recursion is not inherently more capable, and these ceilings prevent +accidental exponential fanout. The upstream publication boundary is unchanged. Blackbird v0.1.3 exposed an MCP schema conformance defect: composite output schemas omitted a root `type: object`, which pi-mcp-adapter 2.23.0 correctly rejected. Blackbird v0.1.4 shipped the tested root-type fix and regression test; -the portable stack now pins v0.2.0, which also adds the first-class Pi -companion. Keep MCP conformance in Blackbird itself—never add a local protocol +the current Linux inventory pins v0.5.0, with a separate npm Pi integration. +Mac installations follow the native tap. Keep MCP conformance in Blackbird itself—never add a local protocol proxy or patch adapter internals. Tool allowlists are not operating-system sandboxes. For hostile code or strong @@ -139,24 +170,31 @@ becomes edit-source → diff → apply, which is intentional, not accidental. --- -## Why uv / fnm / rustup / GOTOOLCHAIN, not mise +## Why mise bootstrap plus chezmoi + +**Decision (2026-09): mise owns provisioning and tool versions; chezmoi owns +personal file reconciliation.** The previous no-mise decision predates its +shipped machine bootstrap and our Node/Bun/Zig-based project workflows. -**Decision: best-in-class native per language. No meta-manager.** +Mise replaces repeated install loops, fnm and the Nix fallback used only to +obtain portable CLIs. Native uv remains the Python execution/package interface; +rustup retains Rust's toolchain contract and Go retains `GOTOOLCHAIN=auto`. -`mise` tries to be everything: tool versions, env vars, task runner. Each -piece is fine, but combined it's a leaky abstraction with more surface area -than the union of: +Mise's dotfile engine supports regular copies, templates, links and drift +checks. Chezmoi earns its place through existing `modify_`, `create_`, +`run_once_`, content-triggered scripts and shared Go-template contracts. Replacing +that layer would reproduce the difficult app-owned-key merge policy elsewhere. -- **uv** (Astral, Rust): Python toolchain + venv + dependency resolver. - Orders of magnitude faster than pip+pyenv+poetry. No shims. -- **fnm** (Schniz, Rust): Node version switcher. `--use-on-cd` reads - `.nvmrc` / `.node-version` transparently. Sub-10ms cold. -- **rustup**: optimal already. No alternative. -- **`GOTOOLCHAIN=auto`** (built-in since Go 1.21): per-`go.mod` toolchain - download. No manager needed. +Mise 2026.9.1's independent Homebrew-compatible installer cannot consume our +Ruby-only tap. Real Homebrew remains the Mac package owner. Phux and Blackbird +remain service/identity owners; a repeated bootstrap does not restart healthy +services. Optional harnesses are selected per machine. See +[BOOTSTRAP.md](BOOTSTRAP.md) for the complete ownership table. -Each tool is the best at its job. The combined startup cost is lower than -mise alone. Project switching is automatic per-tool. +Literal mise shims support noninteractive shells; full environment hooks remain +lazy in project trees. Bootstrap refreshes cached shell init when resolved tools +change. Host `dot-bench` remains the performance gate: container timing cannot +substitute for interactive workstation measurements. --- diff --git a/docs/BOOTSTRAP-VALIDATION.md b/docs/BOOTSTRAP-VALIDATION.md new file mode 100644 index 0000000..0db1116 --- /dev/null +++ b/docs/BOOTSTRAP-VALIDATION.md @@ -0,0 +1,124 @@ +# Bootstrap migration acceptance — 2026-09-07 + +## Executed checks + +| Check | Evidence | +|---|---| +| Native mise orchestration | `mise run check` and `mise bootstrap --yes` passed on macOS arm64 with mise 2026.9.1 | +| Linux arm64 | Debian 13 non-root fresh HOME, `container,pi`; real installs, strict shell checks and second-run convergence passed | +| Linux amd64 | Native GitHub CI passed the strengthened application/runtime checks on 2026-09-08; this Mac's non-AVX emulator remains unsuitable | +| Runtime config preservation | Darwin arm64, Linux arm64 and Linux amd64 chezmoi fixtures each applied and verified twice | +| Native applications | Phux composed configuration and Phig configuration passed; Phig performed repository status in containers and on the Mac | +| Live services | Blackbird and Phux daemon PIDs were identical before and after Mac bootstrap | +| Failure-path service policy | Stubbed native boundaries cover Blackbird doctor/schema/probe failures, existing unhealthy Phux units, absent-unit adoption and services-off; live processes are retained | +| File retirement | Exact known copies are retired; a locally modified copy fails closed and survives | +| Machine profile writer | Three regression tests cover private permissions, identity/unknown-data preservation, repeat convergence, selection changes and malformed TOML | +| V2 goal plugin | Exact Git pin passed its Node tests and TypeScript check; check repeated against SDK beta-19157 | +| OpenCode login boundary | Final arm64 container executes the native CLI and enumerates its account-scoped catalog without credentials (empty is valid); Astra is checked when OpenAI is connected | +| Existing checks | `tests/gha-local-smoke.sh`, warning-level ShellCheck, Renovate semantic validator 44.26.0 and `git diff --check` passed | +| lstags | `cargo test --locked --manifest-path src/lstags/Cargo.toml` passed; the crate currently contains zero unit tests | +| Mac configuration drift | `chezmoi verify --exclude scripts` passed after live bootstrap | +| Real project activation | A PTY-backed interactive zsh entered a trusted temporary mise project, selected Node 24.20.0 and its environment, then restored the environment on leaving | + +Linux acceptance uses the published GitHub release artifacts, not host binaries +or credentials. Exact tool versions and available artifact checksums/URLs are in +the mise lockfiles. Both architectures exercised actual Phig repository access; +the earlier Bookworm rig exposed Git's minimum-version incompatibility, which +is why the supported clean Linux base is Trixie. + +CI definitions execute these lanes on native Mac/Linux runners and Linux +amd64/arm64 containers. The local runs above are the migration evidence; CI +publication is a separate repository operation. + +## Complexity evidence + +No project-native Bash complexity analyzer is configured. Shell counts below +use CC = 1 + conditionals, loops, short-circuit guards and non-default case arms. +Python was measured with `uvx --from radon==6.0.1 radon cc -s +scripts/bootstrap/configure.py`. + +| Function | Before | After | Method | +|---|---:|---:|---| +| Old `install_formula` | 2 | removed | Manual; replaced by native Homebrew Bundle | +| Old `install_cask_app` | 4 | removed | Manual; replaced by native Homebrew Bundle | +| `_mise_lazy_load` | 6 | 6 | Manual; duplicate eager hook call removed without adding branches | +| `prompt_mise` | 3 | 5 | Manual; recognizes the same three project config locations as activation | +| New `configure` | — | 8 | Radon B; initial implementation was also 8 | +| New `harness_name` | — | 2 | Radon A | +| New `git_identity` | — | 1 | Radon A | +| New `write_private_config` | — | 1 | Radon A | +| New `install_harness` | — | 7 | Manual; explicit native-installer dispatch | +| New `reconcile_blackbird` | — | 8 | Manual; includes the boolean operator inside the jq predicate | +| New `blackbird_is_absent` | — | 4 | Manual; requires both connection refusal and no matching process | +| New `disable_linux_brew_updater` | — | 3 | Manual | +| New `reconcile_phux` | — | 4 | Manual | +| New fixture `resolve_binary` | — | 5 | Manual; excludes inactive shims when resolving native fallback binaries | + +The service functions retain clear guard clauses; no helper exceeds the default +refactor threshold. The Phux TOML modifier uses a single conditional and a +comment-preserving parser instead of hand-written TOML text surgery. + +## Performance acceptance: still failing under host contention + +`PERF.md` was **not repinned**. All four pinned metrics already failed before +the migration. Mac and Linux arm64 functional acceptance passed; absolute shell +performance acceptance remains open. Native amd64 runtime acceptance subsequently +passed in CI (see below). + +| Measurement | Command | Input | First command | First prompt | +|---|---:|---:|---:|---:| +| Before migration, 10 iterations (`1788674636.json`) | 54.4 ms | 11.3 ms | 797.4 ms | 61.5 ms | +| First post-apply run, 10 iterations (`1788678719.json`) | 129.3 ms | 11.8 ms | 1704.1 ms | 69.6 ms | +| Later loaded-host run, 10 iterations (`1788746322.json`) | 104.6 ms | 12.6 ms | 2471.7 ms | 156.9 ms | +| Old startup files, 3-iteration diagnostic (`1788747124.json`) | 221.3 ms | 72.9 ms | 2940.2 ms | 467.2 ms | + +Raw records live in `~/.local/state/dotfiles/bench/`. The last row used the old +`.zshenv`, `.zprofile` and `.zshrc` in a temporary ZDOTDIR with current installed +tools; it is a diagnostic, not a replacement for the ten-iteration gate. Its +paired new-source run was interrupted by a server restart. These measurements +do not establish a reliable relative performance difference. + +Observed interference included load averages of 26–31 on a 14-core machine, +an unrelated multi-process Rust build, and six orphaned shells with revoked +terminal descriptors. Even absolute `/usr/bin/true` showed 29–88 ms wall time +for 3–5 ms CPU. Temporarily suspending the orphaned shells did not yield a +successful controlled run; all six were explicitly resumed afterward. No user +workload was killed. + +Repeat `dot-bench` when unrelated host activity is quiet. A passing result is +required before claiming the performance gate is green; the available evidence +does not justify changing its thresholds or redesigning activation to chase +these noisy numbers. + +## Native amd64 runtime acceptance: passed in CI + +[Bootstrap acceptance run 34253606391](https://github.com/phall1/dotfiles/actions/runs/34253606391) +passed on 2026-09-08 at commit `6bb2056`. Both native architecture container jobs +passed, including the actual Phui executable and native history tests. macOS and +Linux configuration jobs passed as well. The emulator failure below is retained +as diagnostic history, not an open acceptance gate. + +The final stricter `phui --version` smoke check exposes a crash in the published +Phui v0.15.0 x64 executable under this Mac's emulator. The embedded Bun v1.3.14 +reports missing AVX support and terminates with signal 4 (exit 132). The same +application check passes on Linux arm64. Earlier amd64 convergence evidence did +not execute Phui, so it is insufficient to declare the final amd64 lane green. + +Use `bash tests/bootstrap/container.sh linux/amd64` on a native amd64 host with +AVX support, as the configured native GitHub CI job does. No +runtime check is bypassed for emulation. A missing Docker cache snapshot after +a server restart was separately resolved by rebuilding this task's amd64 image +with `--no-cache`; that did not resolve the CPU-instruction limitation. + +## Validation boundaries + +- Optional Claude/Hermes/Goose/Grok installer commands were checked against + official installer source and local installation conventions. Their complete + first-install flows are not covered by the core/Pi container matrix. +- Container services are deliberately disabled. Native service first-install + behavior belongs to the products; this repo tests boundary decisions and + verifies that migration preserves the live Mac daemons. +- Cockpit and Token Tach preference keys were checked against their native + parsers' supported surfaces. This migration did not drive their graphical UI. +- The workstation's provider credentials and existing optional-harness runtime + state were retained. No authenticated model call is needed by bootstrap. diff --git a/docs/BOOTSTRAP.md b/docs/BOOTSTRAP.md new file mode 100644 index 0000000..9501db8 --- /dev/null +++ b/docs/BOOTSTRAP.md @@ -0,0 +1,220 @@ +# Workstation bootstrap + +## Ownership + +For another already-equipped machine, run the standalone onboarding entrypoint: + +```sh +onboard=$(curl -fsSL https://raw.githubusercontent.com/phall1/dotfiles/feat/mise-workstation/scripts/onboard.sh) && bash <<< "$onboard" +``` + +It adopts the shared private setup and runs the final checks using existing +tools and authentication. [Onboarding details](SELF-SAVING-DOTFILES.md#shared-repository-and-another-machine). + +`mise bootstrap` is the provisioning entrypoint. Native mise history owns enrolled live +preferences and automatically saves/synchronizes edits. Chezmoi renders the +remaining machine-specific files and reconciles explicit integration keys. Native +applications own authentication, databases, sessions, pairing and their service +definitions. Bootstrap never copies these between hosts. + +| Inventory | Authority | Update policy | +|---|---|---| +| Portable tools | `mise.toml`, `mise.lock` | Exact versions; reviewed pin changes and lock refresh | +| Linux packages/tools | `mise.linux.toml`, `mise.linux.lock` | apt for host dependencies; mise for release binaries | +| Mac host tools/personal tap | `provision/Brewfile` | Real Homebrew; bootstrap installs missing packages without upgrading | +| Mac desktop apps | `provision/Brewfile.desktop` | Real Homebrew casks and their native auto-updaters | +| Pi coding harness | `mise.pi.toml`, `mise.pi.lock` | Optional exact CLI version; native Pi package installation | +| OpenCode V2 | Official `@opencode-ai/cli` installer | Seed version in `integrations.sh`; then native auto-update | +| Other selected harnesses | `scripts/bootstrap/harnesses.sh` | Native installer; existing installations are retained | +| Rust | rustup | Native toolchains and project `rust-toolchain.toml` | +| Editable preferences | Native mise tracking, initially selected by `provision/dotfiles-history.json` | Edit live → autosave → private two-way sync | +| Templates and integrations | Remaining chezmoi `dot_*` sources | Preview → apply → verify | +| Phux/Blackbird services | Native product installers | Healthy services are retained; Phux adoption preserves live panes | + +The minimum mise release is **2026.9.3**. Its `brew:` backend is an independent +installer and cannot consume the personal tap's Ruby-only metadata. This is why +Mac package installation deliberately calls real Homebrew. Rolling mise web docs +describe some later features; this setup uses released commands. + +## Fresh machine + +Clone this repository into `~/dotfiles`. macOS needs Xcode Command Line Tools; +Linux needs Debian 13 / Raspberry Pi OS Trixie (64-bit), or an apt-based host +with Git >=2.45.1. Configure a real Git +identity before provisioning: + +```sh +git config --global user.name 'Your Name' +git config --global user.email 'you@example.com' +cd ~/dotfiles +bash scripts/bootstrap-darwin.sh --yes # macOS +# or: +bash scripts/bootstrap-linux.sh --yes --update +``` + +These seed scripts obtain mise and prerequisites, then invoke **the same** +`mise bootstrap` used for subsequent convergence. No Nix or fnm installation is +needed. Existing Nix installations and project flakes remain independently owned. + +Bootstrap's final task: + +1. Preserves machine-local chezmoi data and records the source directory. +2. Previews and applies the selected configuration. +3. Installs the rendered global tool inventory and refreshes shell init caches. +4. Installs missing native harnesses, builds lstags and reconciles services. +5. Reconciles installer-written config, then relinquishes ownership of enrolled + live preferences and reconciles the native history watcher. +6. Compiles the live shell and verifies generated files, history health and doctor. + +Native bootstrap holds its history transaction until exit. Its watcher captures +new enrollment after that transaction finishes. In a service-free container, +follow initial enrollment with `mise bootstrap dotfiles save` explicitly. +See [the self-saving workflow](SELF-SAVING-DOTFILES.md) for sharing and recovery. + +Provider logins remain per-machine: use OpenCode's `/connect`, `gh auth login`, +and the selected applications' normal login commands. Bootstrap does not invent +credentials or require an authenticated model request to install a workstation. + +## Profiles + +Platform environment selection is native mise `auto_env` in `.miserc.toml`. +`mise.linux.toml` loads on Linux. The optional **Pi coding agent** is unrelated to +the Raspberry Pi hardware platform. + +Default: OpenCode V2, Blackbird, Phux, the shell/editor tools, and Mac desktop +applications on macOS. To add optional harnesses: + +```sh +mise run configure pi claude # persists the complete optional selection +mise bootstrap --yes +# For a fresh machine, this also installs and selects Pi: +mise -E pi bootstrap --yes +``` + +Accepted optional harnesses: `pi`, `claude`, `hermes`, `goose`, `grok`. +`mise run configure opencode` returns the managed selection to core-only. +Deselection stops managing optional files; it does not delete existing runtime +state, uninstall applications, or revoke authentication. + +Selections live under `[data]` in `~/.config/chezmoi/chezmoi.toml`: + +```toml +harnesses = ["opencode", "pi"] +services = true +``` + +The generated `~/.config/dotfiles/profile.json` lets doctor use the same selection. +The generated `~/.config/mise/conf.d/dotfiles.toml` supplies global tool versions, +so tools work outside this repository; project configs override personal defaults. + +Use `mise -E server bootstrap` on a Mac without the desktop bundle. Use +`mise -E container bootstrap` in a disposable environment to disable service +installation by default. `mise run configure -- --services off` explicitly +persists that choice on an existing machine. Disabling setup does not stop an +existing application-owned service; use its native lifecycle commands for that. + +## Disposable test rig + +```sh +mise run check +bash tests/bootstrap/container.sh linux/arm64 +bash tests/bootstrap/container.sh linux/amd64 +bash tests/bootstrap/container.sh linux/arm64 container,pi +bash tests/bootstrap/goal-plugin.sh +uv run --script tests/bootstrap/history_test.py +``` + +The Docker rig **copies** the checkout into an image, creates a non-root test +user and fresh HOME, then runs real installation and a second convergence. It +does not mount HOME, the Docker socket, SSH agents or credentials. Containers +are removed on exit. Images are named `dotfiles-bootstrap:arm64` / `:amd64` and +can be removed with `docker image rm` after testing. + +For amd64 runtime validation, use a native x64 host with AVX support. Phui's +published x64 executable embeds Bun and currently crashes under this Mac's +non-AVX Linux emulator. The rig deliberately retains the runtime smoke check. +See [migration acceptance evidence](BOOTSTRAP-VALIDATION.md) for the executed +checks and outstanding performance/native-amd64 gates. + +Coverage: + +- real Linux packages and declared tool downloads; +- source-to-HOME materialization and a clean second apply; +- noninteractive, interactive and login zsh tool access from a minimal PATH; +- no Phux/Blackbird service units and no login-shell changes in container mode; +- Darwin arm64, Linux arm64 and Linux amd64 template fixtures; +- preservation of app-owned JSON/TOML/YAML fields and create-only package data; +- private machine-data writes, selected-harness changes and malformed-input handling; +- the pinned V2 goal plugin's tests and type compatibility with our SDK. + +Fixtures use real jq/yq/chezmoi/uv executables, not mise shims, inside an empty +environment. The pinned TOML parser is provisioned before fixture execution; +fixture uv calls then use offline mode and the dependency cache. They exclude +chezmoi **run scripts**, which prevents a macOS fixture +from reaching the host's launchd domain. They still execute real `modify_` file +transformations. CI runs native Mac/Linux fixtures and both Linux architectures. + +Containers do not validate macOS app installation, launchd behavior, authenticated +provider routes, or host performance. Those checks run on the actual workstation +after the isolated rig passes. `PERF.md` remains the host's performance gate. + +## Daily operations + +```sh +mise bootstrap --dry-run # preview declarative provisioning +mise bootstrap status --missing # native mise resources +mise run audit # includes chezmoi and tool inventory +chezmoi diff +chezmoi apply +chezmoi verify --exclude scripts +mise run doctor +mise run bench +``` + +Mise's structured bootstrap plan does not inspect the internals of custom tasks. +`--dry-run` prints those tasks; it does not replace the chezmoi fixture suite. +Like other mise tasks, `mise run check` may first install missing declared tools +and parser dependencies into caches; the configuration tests themselves use +disposable homes. + +To update a tool, edit its version in the owning mise manifest, refresh its +lockfile (`mise lock --platform macos-arm64,linux-arm64,linux-x64`), run the rig, +then bootstrap. Linux and Pi locks are refreshed with `mise -E linux,pi lock +--platform linux-arm64,linux-x64`. npm backend entries have exact package versions +but no downloadable-artifact lock entries; do not claim universal `--locked` +support for this mixed inventory. + +Use `brew upgrade ` for Homebrew-owned packages. Phux's CLI upgrade and +live-server handover remain separate: follow with `phux upgrade` deliberately. +Linux Blackbird's Homebrew update timer is disabled when mise owns its binary. +Mise bootstrap does not run package pruning or uninstall former package owners. + +## Preferences and recovery + +- **Phux:** chezmoi appends `layers/dotfiles.toml` to `extends`, preserving existing + distro and host entries. `phux config check` validates the composed config. +- **Cockpit:** sparse XDG config; Settings UI changes are intentional drift to + review and import. Device/session selection remains local. +- **Phui:** merge the generic workspace mapping and portable theme; preserve + specific mappings and editor settings. `projects.toml` remains local. +- **Phig:** strict, portable TOML; validate with `phig config check`. +- **Token Tach:** portable preferences only, not usage history. +- **OpenCode:** CLI UI preferences merge with unknown local fields. V2 server + config remains explicitly fully managed. Goal mode is pinned to a reviewed Git + commit; the former V1 autoresearch plugin and launchers are retired. + Bootstrap removes only checksum-matching retired copies; locally edited copies + stop retirement with the exact path to review. +- **Blackbird:** MCP uses port 8081; push delivery uses port 8080. The global push + plugin retains its existing central-inbox scope at `~/workspace/blackbird`, + not automatic current-project routing. Use explicitly project-scoped plugin + settings for other inboxes. MCP coordination tools can join projects separately. + +Keep a persistent checkout at `~/dotfiles` on every host. Archive-based +`mise bootstrap remote` uses temporary staging, so do not point chezmoi source or +long-lived symlinks into that staging directory. For a Pi already reachable over +SSH, clone/update its persistent checkout and invoke bootstrap there. For the +shared history setup use `mise bootstrap --adopt phall1/dotfiles-history`. +Mise 2026.9.3 calls this `--adopt`; the article's `--from-git` is a deprecated alias. + +Back up Blackbird's database, Phux host identity and application history through +an operational backup system. They are recovery data, not portable preferences. diff --git a/docs/HISTORY-VALIDATION.md b/docs/HISTORY-VALIDATION.md new file mode 100644 index 0000000..cf95126 --- /dev/null +++ b/docs/HISTORY-VALIDATION.md @@ -0,0 +1,114 @@ +# Native history migration evidence — 2026-09-08 + +## Scope and executed checks + +- Official mise 2026.9.3 macOS arm64 archive verified against its published + `SHASUMS256.txt` before use. Linux fresh installation uses the same release. +- `mise run check` passed: all three platform fixtures preserve application + runtime fields and preexisting preferences on first apply; missing preferences + receive seeds. Live-owned edits/deletions survive two later chezmoi applies. +- `tests/bootstrap/history_test.py` passed with real native mise commands: + autosave after atomic file replacement, rollback/undo, fresh adoption, two-home + exchange, conflict resolution, deletion, and consistent watcher directories. +- A real zsh regression compiles startup files and a module, deletes their source, + and verifies that bytecode cleanup prevents deleted configuration from running. +- The final Linux arm64 `container,pi` run passed real installation, history + enrollment, repeat convergence, native application checks and history tests. +- Native [CI run 34253606391](https://github.com/phall1/dotfiles/actions/runs/34253606391) + passed amd64 and arm64/Pi container acceptance plus macOS/Linux configuration + and history tests at `6bb2056`. Dependency validation passed separately. +- Independent source review identified the native bootstrap history-lock + reentrancy issue, active-operation status semantics, service-off reconciliation, + and the macOS cache-root mismatch. All were addressed without custom watchers + or a replacement synchronization protocol. +- Before live capture, the native paths preview reported 37 entries and no + invalid, incomplete or omitted paths. The first live preference checkpoint + covered 64 files. All 63 preexisting preference files compared byte-for-byte + equal to the private pre-cutover snapshot (the tracking config was new). +- The native launchd watcher autosaved a harmless test file's creation and + deletion. It also automatically published creation/deletion of a separate + probe to private `phall1/dotfiles-history`, without explicit save/sync calls. +- A second isolated native client restored the real private history without + receiving host credentials. Its edit and deletion were relayed to the private + origin with parent-authenticated Git; the live watcher automatically applied + both. A temporary five-second local fetch interval was removed afterward. +- Chezmoi verification passed after enrollment. Doctor checks native history, + watcher status, reported sync errors and the ownership-set intersection. +- Live Mac repeat `mise bootstrap --yes` passed after enrollment and origin + connection. Native status reports a running watcher, zero pending operations, + zero conflicts and no synchronization errors. + +## Complexity + +Measured using `uvx --from radon==6.0.1 radon cc -s`: + +| Function | Before | After | +|---|---:|---:| +| `configure` | 8 | 9 | +| `tracking_entries` | — | 3 | +| `history_config` | — | 1 | +| `watcher_directories` | — | 2 | +| `enable_history` | — | 3 | +| `_compile` (zsh, manual decision count) | 5 | 5 | + +No existing public API was removed. The new functions separate enrollment data, +portable native configuration, and machine-local service directories. The +existing private/atomic TOML writer is reused. The zsh guard retains its branching +complexity while removing derived bytecode for absent sources. + +## Performance + +No performance baseline was repinned. The initial comparison shows no new +>10% regression, but the prior first-command absolute gate remains open: + +| Measurement (10 iterations) | Command | Input | First command | First prompt | +|---|---:|---:|---:|---:| +| Before history (`1788882278.json`) | 27.4 ms | 5.1 ms | 248.8 ms | 41.8 ms | +| After history (`1788885195.json`) | 21.8 ms | 5.5 ms | 230.4 ms | 34.2 ms | +| Final recovery validation (`1788886988.json`) | 22.1 ms | 5.1 ms | 247.0 ms | 33.4 ms | + +The first-command ceiling is 220 ms. Other metrics pass. Records are in +`~/.local/state/dotfiles/bench/`. Follow-up isolated profiling found no verified +whole-shell improvement; no speculative performance changes were applied. + +## Single-command onboarding follow-up + +`scripts/onboard.sh` is standalone and assumes existing Homebrew/Git/gh/mise, +Git identity and GitHub authentication. The documented download-first command +executes only after a complete successful transfer. The entrypoint establishes +cache/state roots, invokes native adoption, checkpoints outside the bootstrap +transaction, synchronizes and runs both doctor and bench. Final checks include +newly installed mise shims and Cargo tools in PATH. + +Acceptance coverage: + +- Eight onboarding boundary tests cover prerequisites, directory overrides, + newly installed tool visibility, partial-download rejection in Bash and zsh, + the documented successful download command, native-operation failures and + health/performance exit-status handling. +- The released mise integration suite exercises actual adoption through the new + entrypoint, then runs it again with a live edit and verifies preservation. +- Five real benchmark tests execute through platform `/bin/bash`, including + stock macOS Bash 3.2. Valid measurements persist the existing JSON schema; + regressions, missing/invalid measurements and malformed baselines fail. +- Independent review's PATH, download-failure and benchmark compatibility findings + are resolved. No remaining P1/P2 findings were reported. + +The benchmark now uses indexed arrays compatible with Bash 3.2, validates every +declared baseline and requires every pinned measurement before certifying a pass. +This fixes false-success paths without changing any `PERF.md` threshold. + +Manual shell cyclomatic complexity (Python fixture methods measured with Radon +remain at most 4): + +| Function | Before | After | +|---|---:|---:| +| `measure` | 4 | 4 | +| `measurement_error` | — | 1 | +| `record_result` | — | 2 | +| `validate_baseline` | — | 3 | +| `require_pinned_results` | — | 5 | +| `require_command` | — | 2 | +| `check_git_identity` | — | 4 | +| `check_prerequisites` | — | 5 | +| `verify_setup` | — | 6 | diff --git a/docs/OX-ALPHA.md b/docs/OX-ALPHA.md index 5b628c4..dec8431 100644 --- a/docs/OX-ALPHA.md +++ b/docs/OX-ALPHA.md @@ -18,7 +18,6 @@ ox run "implement the next scoped task and verify it" ox ask "review this diff for correctness" # read-only Pi posture ox each "compare three designs for this API" # read-only provider fanout ox run --provider venice --harness grok "fix the test" -ox run --provider openrouter --harness codex "review HEAD" ox run --provider opencode --harness goose --dir ~/src/app "run the tests" ``` @@ -48,7 +47,7 @@ even for its $0 model, so it is locally disabled and `ox` skips it. Run `ox enable venice` after adding an eligible free balance or intentionally buying credits. No purchase or paid subscription was made. -Run `opencode providers login` to store a key locally under the matching +Run `opencode2 auth login` to store a key locally under the matching configured provider. `ox` can reuse API-key entries from OpenCode or Pi without copying the secret. `ox auth [provider]` prints the exact setup reminder. @@ -83,14 +82,12 @@ Primary references: | Hermes | Zen/OpenRouter/Nous native; Command/Venice named custom providers | | Goose | all five declarative OpenAI-compatible providers | | Grok Build | all five tracked custom models | -| Codex CLI | OpenRouter and Venice only, through Responses API profiles | | Claude Code | none directly | Claude Code requires Anthropic Messages semantics and Anthropic does not support non-Claude models behind gateways. A translating proxy would be an unsupported, extra dependency, so the `ox` launcher refuses that combination rather than -pretending it is reliable. Codex similarly requires Responses API; only Venice -documents this integration, while OpenRouter's Responses endpoint is beta. +pretending it is reliable. Tracked provider catalogs are secret-free and preserve the normal default model in every harness. Pi and Hermes use `modify_` merges so runtime-owned providers, diff --git a/docs/PLAYBOOKS.md b/docs/PLAYBOOKS.md index 09eeeb1..0b1326d 100644 --- a/docs/PLAYBOOKS.md +++ b/docs/PLAYBOOKS.md @@ -5,8 +5,10 @@ with a task, the relevant playbook below is the canonical execution path. **Skip the playbook only if the task explicitly demands deviation** — then note the deviation in the commit. -Every playbook ends with the **change-loop tail**: `dot-doctor` + `dot-bench` -+ commit. That's not optional. +Every playbook ends with `dot-doctor` + `dot-bench`. Native history autosaves +enrolled live preferences; commit repository implementation changes separately. +Use `mise bootstrap dotfiles paths` to check ownership and +[the self-saving playbook](SELF-SAVING-DOTFILES.md) for sync and recovery. --- @@ -23,9 +25,12 @@ pi list Daily Pi roles are `pi-commander` (default autonomy), `pi-inspect`/`pi-safe` (local code/command read-only but delegation-capable), and explicit `pi-yolo` (deeper delegation). Spartan may still update explicit Blackbird coordination -state such as mail, acknowledgements, and reservations. Put shared workflows in -`dot_agents/skills/`, not a harness-specific copy. Never track provider auth, -web API keys, Blackbird tokens/cursors, sessions, caches, or package trees. +state such as mail, acknowledgements, and reservations. Edit shared workflows in +live `~/.agents/skills/`; `dot_agents/skills/` supplies first-install seeds. For a reviewed third-party +skill, vendor the unchanged skill plus its license and a `SOURCE.md` commit pin, +then add only the chezmoi symlink adapters required by harnesses that do not +read `~/.agents/skills/` directly. Never track provider auth, web API keys, +Blackbird tokens/cursors, sessions, caches, or package trees. For strong isolation, run Pi in a container/VM or a documented sandbox extension. `pi-inspect` is a capability ceiling, not a hostile-code sandbox. @@ -46,21 +51,20 @@ $EDITOR ~/dotfiles/plugins.lock dot-install-zsh-plugins # Verify the new plugin entry came back as ok=N+1 installed=1. -# 4. Wire it into dot_zshrc. +# 4. Wire it into live ~/.zshrc. # - If eager-load: source it after compinit, BEFORE deferred plugins. # - If deferred: zsh-defer source "$ZSH_PLUGIN_DIR//" # - If completion-fpath only: fpath=(...) BEFORE compinit. -$EDITOR ~/dotfiles/dot_zshrc +$EDITOR ~/.zshrc -# 5. Apply + verify. -chezmoi diff -chezmoi apply +# 5. Compile + verify the live preference. +dot-zcompile zsh -i -c 'echo loaded' # smoke-test the shell starts dot-doctor # zsh plugins section should show new plugin green dot-bench # baseline shouldn't regress >10% # 6. Commit. -git add plugins.lock dot_zshrc +git add plugins.lock git commit -m "feat(zsh): add for " ``` @@ -174,6 +178,11 @@ For totally new metrics (zsh-bench doesn't measure it), you'd need to extend ## P5. Adding a new $HOME file (config for a new tool) +For an editable portable preference, create the live file and enroll it with +`mise bootstrap dotfiles track `. Keep generated files and machine-specific +templates under chezmoi using the recipe below. Before tracking an existing +chezmoi target, release its chezmoi ownership; doctor rejects overlap. + ```sh # 1. Figure out the target $HOME path. Examples: # ~/.foorc → dot_foorc @@ -220,32 +229,32 @@ cat ~/.config/foo/config # confirm template rendered correctly ## P6. Adding a brew package (Mac) ```sh -# 1. Add to scripts/bootstrap-darwin.sh. -$EDITOR ~/dotfiles/scripts/bootstrap-darwin.sh -# Add to the brew_packages array, grouped logically. +# 1. Add to the native Homebrew inventory. +$EDITOR ~/dotfiles/provision/Brewfile +# GUI apps belong in provision/Brewfile.desktop. # 2. Install on this machine (so doctor sees it). brew install -# 3. If the tool needs shell-init (e.g. zoxide init zsh), wire into dot_zshrc. +# 3. If the tool needs shell-init (e.g. zoxide init zsh), edit live ~/.zshrc. # If it's a dependency of an existing tool, no shell change needed. # 4. Add a doctor check if it's load-bearing. # - Required tool (substrate must have it) → checks/00-binaries.sh: require_bin # - Wanted tool (nice-to-have) → checks/00-binaries.sh: want_bin -# 5. Apply (only if shell init was changed) and verify. -chezmoi diff -chezmoi apply +# 5. Compile if shell init changed, then verify. +dot-zcompile dot-doctor # new binary should show green # 6. Commit. -git add scripts/bootstrap-darwin.sh checks/00-binaries.sh dot_zshrc +git add provision/Brewfile checks/00-binaries.sh git commit -m "feat(toolchain): add " ``` -For Pi/Linux, edit `scripts/bootstrap-linux.sh` — apt first, fall back to -nix if apt doesn't have it or has an outdated version. +For Pi/Linux, edit `mise.linux.toml` for apt/native release tools. Portable +cross-platform tools belong in `mise.toml`. Refresh lockfiles and run the +[disposable bootstrap rig](BOOTSTRAP.md#disposable-test-rig) before live apply. --- @@ -262,7 +271,8 @@ See **CLAUDE.md** for the full picture. Cheat sheet: | Slash command | `dot_claude/commands/.md` | `/` in-session | ```sh -# Always edit in the chezmoi source, then apply. +# For source-owned Claude integrations, edit the source then apply. +# Shared skills in ~/.agents/skills are live-owned and autosave instead. $EDITOR ~/dotfiles/dot_claude/skills//SKILL.md # or settings.json, etc. chezmoi diff chezmoi apply @@ -337,34 +347,21 @@ git commit -m "perf: re-pin baseline (was N, now M) — " ## P10. Onboarding a new machine -```sh -# 1. Install minimal prerequisites manually if needed. -xcode-select --install # mac only -# (Linux: nothing — bootstrap-linux.sh handles everything) +Assume the base tooling, Git identity and GitHub login exist. On macOS this +includes Homebrew; mise must be >=2026.9.3. -# 2. Clone the repo to ~/dotfiles. -git clone https://github.com/phall1/dotfiles.git ~/dotfiles - -# 3. Run host bootstrap. -~/dotfiles/scripts/bootstrap-darwin.sh # or bootstrap-linux.sh -# Bootstrap finishes with a copy-paste next-steps block. - -# 4. Follow the printed steps: -~/dotfiles/scripts/setup-chezmoi.sh # interactive identity setup -chezmoi apply # materialize $HOME -~/.local/bin/dot-doctor # verify -~/.local/bin/dot-bench # verify perf - -# 5. Sign in to per-machine services. -gh auth login # GitHub -# (other per-machine tokens: do as needed) - -# 6. Restart shell. -exec zsh +```sh +onboard=$(curl -fsSL https://raw.githubusercontent.com/phall1/dotfiles/feat/mise-workstation/scripts/onboard.sh) && bash <<< "$onboard" ``` -If any step fails, **don't paper over it**. Diagnose, fix the root cause, -update the docs. +The script handles native adoption, cache/state roots, provisioning, +synchronization and health/performance checks. Open a new terminal afterward. +From an existing checkout, run `bash ~/dotfiles/scripts/onboard.sh` instead. + +Re-running is supported and preserves live preferences. A failed check leaves +the installed setup in place and returns nonzero with diagnostics. Resolve the +reported issue before declaring the machine ready. Application logins remain +per-machine. Details: [self-saving dotfiles](SELF-SAVING-DOTFILES.md). --- diff --git a/docs/SELF-SAVING-DOTFILES.md b/docs/SELF-SAVING-DOTFILES.md new file mode 100644 index 0000000..3d2459e --- /dev/null +++ b/docs/SELF-SAVING-DOTFILES.md @@ -0,0 +1,154 @@ +# Self-saving dotfiles + +This implements the native workflow introduced in +[Dotfiles That Save Themselves](https://jdx.dev/posts/2026-09-07-dotfiles-that-save-themselves/), +using released mise **2026.9.3**. + +## Everyday use + +Edit the files you use: `~/.zshrc`, `~/.p10k.zsh`, Neovim configuration, +Ghostty preferences, shared skills, OpenCode commands/agents and the other +paths shown by: + +```sh +mise bootstrap dotfiles paths +mise bootstrap dotfiles status +``` + +The native `mise-history` watcher saves quiet edits after approximately two +seconds. Automatic sharing pushes within five minutes and fetches/applies +incoming changes every fifteen minutes by default. Faster explicit convergence: + +```sh +mise bootstrap dotfiles save +mise bootstrap dotfiles sync +mise bootstrap dotfiles pull +``` + +Saved configuration can be broken. History is recovery, not validation: +run `dot-doctor`, and run `dot-zcompile` / `dot-bench` after changing the shell. +Sync and rollback reload hooks regenerate shell bytecode after remote restores. +Caches and bytecode are excluded from history. + +## Ownership + +| Concern | Owner | +|---|---| +| Enrolled live preferences | Native mise history and private `phall1/dotfiles-history` | +| The live tracking configuration | `~/.config/mise/conf.d/dotfiles-history.toml`, itself tracked | +| Initial enrollment policy | `provision/dotfiles-history.json` | +| Packages, tool pins, bootstrap scripts, checks | This provisioning repository | +| Machine-specific templates, executable helpers, skill symlinks | Chezmoi source | +| Mixed runtime configs and integration registries | Native apps plus existing managed-key integration rules | +| Credentials, databases, sessions, caches and machine-local overrides | Local/application-owned; not enrolled | + +The initial enrollment policy intentionally excludes `.gitconfig`, SSH config, +OpenCode's runtime/UI state, Phux's root registry, Phui's private project mapping, +and optional-harness auth/settings stores. The portable Phux layer is enrolled. +Ghostty/Cockpit/Token Tach use native macOS-only variants. Shared shell/editor +files use one portable stream across OSes. + +`[data] history = true` in local chezmoi configuration removes enrolled targets +from chezmoi management. The `dot_*` copies become first-install seeds, not +mirrors to force back over live changes. Deletions stay deleted on later +bootstrap runs. `dot-doctor` checks that no live tracked preference is also a +chezmoi target. + +Keep the live tracking configuration authoritative: bootstrap seeds it only +when absent, preserving subsequent native `track`, `untrack`, exclusions and +sync choices. To enroll an additional previously generated target, first remove +its chezmoi ownership, then run `mise bootstrap dotfiles track `. + +## Shared repository and another machine + +Use private `https://github.com/phall1/dotfiles-history.git` for saved history. +This is distinct from the public provisioning source. Native history stores all +saved versions, including intermediate edits. Credentials are not enrolled. + +On a laptop with Homebrew, Git, authenticated `gh`, a Git identity and mise +>=2026.9.3 already set up, onboarding is one command: + +```sh +onboard=$(curl -fsSL https://raw.githubusercontent.com/phall1/dotfiles/feat/mise-workstation/scripts/onboard.sh) && bash <<< "$onboard" +``` + +The command downloads the entire script before running it; a failed or interrupted +download never executes a partial script. `scripts/onboard.sh` is standalone: it +needs no existing checkout. It checks the +prerequisites, uses the existing GitHub authentication, sets cache/state roots +before the outer history transaction, and invokes native adoption. After bootstrap +returns it checkpoints, synchronizes, displays status, and runs doctor plus bench. +Failures stop the script or return a failed validation result; doctor warnings +are displayed without hiding a separate benchmark failure. + +From a checkout, the same entrypoint is `bash ~/dotfiles/scripts/onboard.sh`. +Re-running it preserves already-adopted live edits. Native conflict handling +remains in charge; the script never chooses local or remote content for you. +After success, open a new terminal and sign into this machine's application +providers. Provider credentials and SSH keys stay machine-local. + +The shared global configuration declares the provisioning checkout at +`~/dotfiles`. Native repo bootstrap obtains that checkout, then its more-local +bootstrap task installs tools and renders machine-specific files. The outer +global task delegates to that task; it does not call itself recursively. +Existing source checkouts must be clean and have the expected origin. Shared +preferences are restored before chezmoi's first apply and protected immediately. + +The provisioning checkout currently follows `feat/mise-workstation`; that ref +must be published before onboarding another machine. Change it deliberately in +the tracked global config when the implementation is merged. + +`config.local.toml` retains native origin/auth-related local configuration. +`conf.d/zz-dotfiles-services.local.toml` retains this machine's watcher service +selection and is excluded from history. Disabling services through the machine +profile removes this watcher without deleting any checkpoints. + +The watcher declaration explicitly supplies the same mise config/data/state/cache +directories used by the shell. This matters on macOS: launchd otherwise defaults +to `~/Library/Caches` while this shell uses `~/.cache`. Mise 2026.9.3 places its +history locks in the cache directory; mismatched roots split the locks even when +the history store is shared. Both onboarding and ordinary bootstrap establish +these roots automatically, before restored shell initialization exists. Native +status and actual autosave are verified. + +The Mac uses the official binary at `~/.local/bin/mise`; its shims were rebuilt +against that binary. Homebrew only offered 2026.9.2 at cutover. Install or update +mise to >=2026.9.3 before running these commands on another existing workstation. + +## Recover an edit + +```sh +mise bootstrap dotfiles history --path ~/.zshrc +mise bootstrap dotfiles rollback ~/.zshrc --dry-run +mise bootstrap dotfiles rollback ~/.zshrc +mise bootstrap dotfiles undo +``` + +Rollback saves current content first. Undo restores the tracked files affected +by that operation. Neither command restores packages or running application state. + +For conflicting edits from two machines: + +```sh +mise bootstrap dotfiles status +mise bootstrap dotfiles pull --keep-local ~/.zshrc +# Or choose the incoming version: +mise bootstrap dotfiles pull --take-remote ~/.zshrc +``` + +Conflicts pause publication/incoming application, while local history keeps +saving. No conflict markers are inserted into live configuration. With the +Homebrew mise build, use native status/doctor for conflict visibility; native +macOS desktop notifications require mise's signed distribution. + +## Tests + +`uv run --script tests/bootstrap/history_test.py` uses two isolated homes and a +local bare Git origin. It exercises real native autosave after atomic rename, +rollback, undo, fresh adoption, two-way exchange, conflict resolution and deletion. +The fixtures use canonical home paths to avoid mise 2026.9.3's history path-filter +bug with macOS `/tmp` or `/var` aliases. They never install launchd services. + +`mise run check` also proves that edits and deletions survive two chezmoi applies +after enrollment on all three platform fixtures. Linux container acceptance +exercises the entire enrollment and repeat-bootstrap path. diff --git a/docs/setup.md b/docs/setup.md index 8a94981..ffd5450 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -2,37 +2,45 @@ Two hosts, one source-of-truth: Mac (darwin/arm64) and Raspberry Pi (linux/arm64). -Both bootstrap scripts install the pinned agent stack after an LTS Node is -available. To refresh it independently, run `scripts/install-agent-stack.sh`, -then `chezmoi apply` and `dot-doctor`. Authenticate Pi providers and optional -web providers per machine; credentials and Blackbird state are never tracked. -Use `pi-commander`, `pi-inspect` (also `pi-safe`), or explicit `pi-yolo`. +Mise provisions tools and owns live preference history; chezmoi retains templates +and application integrations. For an existing private setup, start with +[native history adoption](SELF-SAVING-DOTFILES.md#shared-repository-and-another-machine). +The +complete inventory, profiles, service boundaries and disposable test commands +live in [BOOTSTRAP.md](BOOTSTRAP.md). OpenCode V2 is the default harness; Pi, +Claude, Hermes, Goose and Grok are optional machine selections. ## Fresh Mac +For your laptop with the base tools and GitHub login already configured: + +```sh +onboard=$(curl -fsSL https://raw.githubusercontent.com/phall1/dotfiles/feat/mise-workstation/scripts/onboard.sh) && bash <<< "$onboard" +``` + +The standalone script adopts your private preference history, installs the +workstation inventory, configures native services, and runs doctor/bench. It +checks the existing tools and identity before adoption. Open a new terminal when +it finishes; sign into application providers on this machine as needed. + +The prerequisites are Homebrew, Git, authenticated `gh`, a configured Git name +and email, and mise >=2026.9.3. The script uses these installations as-is. + +## First Mac installation from seeds + ```bash # 1. Install Xcode CLT (for git, compilers). xcode-select --install -# 2. Clone the dotfiles repo to ~/dotfiles. -git clone https://github.com/YOUR_USERNAME/dotfiles.git ~/dotfiles - -# 3. Run the host bootstrap (installs brew + ~25 tools). -~/dotfiles/scripts/bootstrap-darwin.sh - -# 4. Set up chezmoi to use ~/dotfiles as its source. -mkdir -p ~/.config/chezmoi -cat > ~/.config/chezmoi/chezmoi.toml <<'EOF' -sourceDir = "~/dotfiles" -[data.git] - name = "Your Name" - email = "you@example.com" -EOF +# 2. Clone and set identity before the first bootstrap. +git clone https://github.com/phall1/dotfiles.git ~/dotfiles +git config --global user.name 'Your Name' +git config --global user.email 'you@example.com' -# 5. Apply. -chezmoi apply +# 3. Seed mise, install inventories, apply preferences, reconcile integrations. +bash ~/dotfiles/scripts/bootstrap-darwin.sh --yes -# 6. Verify. +# 4. Verify. dot-doctor dot-bench ``` @@ -48,26 +56,16 @@ Restart your terminal or `exec zsh`. ```bash # 1. Clone. sudo apt-get update && sudo apt-get install -y git -git clone https://github.com/YOUR_USERNAME/dotfiles.git ~/dotfiles - -# 2. Run the host bootstrap (apt + nix-installed tools). -~/dotfiles/scripts/bootstrap-linux.sh +git clone https://github.com/phall1/dotfiles.git ~/dotfiles +git config --global user.name 'Your Name' +git config --global user.email 'you@example.com' -# 3. chezmoi config (same as Mac). -mkdir -p ~/.config/chezmoi -cat > ~/.config/chezmoi/chezmoi.toml <<'EOF' -sourceDir = "~/dotfiles" -[data.git] - name = "Your Name" - email = "you@example.com" -EOF +# 2. Seed mise; apt dependencies and pinned release tools replace the Nix fallback. +bash ~/dotfiles/scripts/bootstrap-linux.sh --yes --update -# 4. Apply. -chezmoi apply - -# 5. Verify. -dot-doctor # some "wanted" tools may be missing on Pi (ghostty etc.) — acceptable -dot-bench # target on Pi: first_prompt_lag < 150ms +# 3. Verify against the documented gates. +dot-doctor +dot-bench ``` ## Per-machine identity & overrides diff --git a/dot_agents/skills/cyclomatic-complexity/LICENSE b/dot_agents/skills/cyclomatic-complexity/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/dot_agents/skills/cyclomatic-complexity/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/dot_agents/skills/cyclomatic-complexity/SKILL.md b/dot_agents/skills/cyclomatic-complexity/SKILL.md new file mode 100644 index 0000000..7d91078 --- /dev/null +++ b/dot_agents/skills/cyclomatic-complexity/SKILL.md @@ -0,0 +1,69 @@ +--- +name: cyclomatic-complexity +description: Refactor code to reduce cyclomatic complexity so it stays readable, maintainable, and aligned with the long-term vision of the codebase, not just optimized for AI comprehension. Use whenever the user asks to refactor, simplify, clean up, or review code quality; mentions complexity, maintainability, readability, spaghetti code, deeply nested logic, or god functions; or asks to check AI-generated code before merging. Also use proactively after writing any nontrivial function with heavy branching. +--- + +# Cyclomatic Complexity + +Purpose: AI-written code often works but branches like a jungle. This skill: measure complexity, refactor hotspots, keep code human-maintainable. + +## Measure first + +CC = decision points + 1. Decision points: `if`, `else if`, `case`, loops, `catch`, ternary, `&&`, `||` in conditions. + +Project linter config wins. If eslintrc, radon config, sonar config, or similar sets a complexity threshold, use that. No config: use defaults below. + +Thresholds: +- 1-5: fine, leave alone +- 6-10: watch, refactor if touching anyway +- 11-15: refactor now +- 15+: must split, no debate + +Prefer real tools over eyeballing when environment allows: +- Python: `radon cc -s -a ` +- JS/TS: eslint `complexity` rule +- Go: `gocyclo` +- Polyglot: `lizard ` + +No tool available: count manually, per function, show the count. + +## Refactor tactics, in order of preference + +1. **Guard clauses.** Invert conditions, return early, kill nesting. +2. **Extract function.** Each extracted piece gets a name that says what, not how. Names are documentation. +3. **Lookup table / map** instead of if-else or switch chains. +4. **Named predicates.** `if (isEligibleForRefund(order))` beats a 4-clause boolean soup. +5. **Polymorphism / strategy** for switch-on-type. Only when the switch appears in 2+ places. +6. **Flatten loops.** Extract loop body, use continue instead of nested if. + +## Hard rules + +- Preserve behavior. Run tests before and after. No tests: say so, suggest adding, refactor conservatively. +- Don't game the metric. A dense one-liner hiding 6 branches is worse than the honest if-chain it replaced. Complexity should move into well-named units, not disappear into cleverness. +- Don't break public APIs or exported signatures without asking. +- Small functions with clear names > few functions with comments explaining sections. +- One responsibility per function. If the name needs "and", split. + +## Workflow + +1. Measure all touched functions, rank by CC descending. +2. Report hotspots with numbers before touching anything. +3. Refactor worst first, one function at a time. +4. Re-measure. Show before/after table: function, CC before, CC after. +5. Verify: tests pass, behavior unchanged, diff reviewable. + +## Output format + +End every refactor with: + +``` +## Complexity report +| Function | Before | After | +|----------|--------|-------| +| parseOrder | 14 | 4 | + +Extracted: validateHeader, resolveDiscount +Behavior verified: +``` + +Keep prose minimal. Numbers and diffs do the talking. diff --git a/dot_agents/skills/cyclomatic-complexity/SOURCE.md b/dot_agents/skills/cyclomatic-complexity/SOURCE.md new file mode 100644 index 0000000..54d4b01 --- /dev/null +++ b/dot_agents/skills/cyclomatic-complexity/SOURCE.md @@ -0,0 +1,10 @@ +# Upstream provenance + +Vendored unchanged from: + +- Repository: https://github.com/saurabhkumar8112/cyclomatic-complexity-skill +- Commit: `567886f485063c5f5f94503d5712ef75cbcbbd94` +- Upstream path: `skills/cyclomatic-complexity/SKILL.md` +- License: Apache-2.0; see `LICENSE` + +The commit pin makes this shared cross-harness copy reproducible and reviewable. diff --git a/dot_agents/skills/i-have-adhd/SKILL.md b/dot_agents/skills/i-have-adhd/SKILL.md new file mode 100644 index 0000000..8ac7747 --- /dev/null +++ b/dot_agents/skills/i-have-adhd/SKILL.md @@ -0,0 +1,140 @@ +--- +name: i-have-adhd +description: 'Shape output for a reader with ADHD: lead with the next action, number multi-step work, restate state across turns, suppress tangents, give specific time estimates, make wins visible. Invoke with /i-have-adhd; stays on until "stop adhd mode".' +disable-model-invocation: true +license: MIT +metadata: + tags: "ADHD, Output Style, Productivity, Formatting" + category: "productivity" +--- + +# i-have-adhd + +The reader has ADHD. Output is not just brief. It is shaped so an ADHD brain can act on it. + +## Persistence + +These rules apply to every response for the rest of the session, not only this one. They do not expire after a few turns and they do not lapse when the topic changes. If you are unsure whether they still apply, they do. + +Turn them off only when the reader says "stop adhd mode" or "normal mode". Confirm in one line, then return to your default style. + +## What ADHD changes about reading + +Five facts drive every rule below: + +1. Working memory is small. Anything not on screen is forgotten. Do not ask the reader to "keep in mind X." +2. Knowing the answer is not doing the answer. The friction between "got it" and "done it" is where work dies. +3. Starting is the hardest step. The first action must be obvious, small, and doable now. +4. Time estimates feel uniform. "A bit of work" and "a few hours" register the same. Vague estimates fail. +5. Dopamine is scarce. Visible progress matters. Buried wins do not register. + +## Rules + +### 1. Lead with the next action + +The first line is something the reader can do. Not context. Not a plan. The action. + +Bad: "Let's think about this. Your auth flow has a few moving pieces..." +Good: "Run `npm install jsonwebtoken`, then edit `src/auth.ts:42`." + +If the answer is a command, path, or snippet, it goes first. Prose comes after, if at all. + +### 2. Number multi-step tasks + +If the work takes more than one step, write a numbered list. Each step is one bounded action. No step contains "and then" twice. + +Use the fewest steps that still work. Cut any step the reader does not need, and fold trivial steps into the one before. A short path finished beats a complete path abandoned. + +Bad: "First open the file, find the function, swap it out, then run the tests." + +Good: +``` +1. Open `src/auth.ts` +2. Replace `verifyToken` (lines 42 to 58) with the snippet below +3. Run `npm test -- auth.spec.ts` +``` + +### 3. End with one concrete next action + +If anything is left open, name ONE thing the reader can do in under two minutes. Even "open the file" counts. + +Bad: "Hope that helps. Let me know if you want to dig deeper." +Good: "Next: run `npm test` and paste the first failing line." + +### 4. Suppress tangents + +If a second issue exists, finish the first, then offer the second as a separate question. + +Bad: "Here's the fix. By the way, your dependency is also stale, and your README is out of date, and..." +Good: "Here's the fix. Separately: there is also a stale dependency. Want me to handle that next?" + +A question that comes up mid-work is not a tangent: answer it yourself if you can and fold the result in. If it still needs the reader, surface it once, at the end. + +### 5. Restate state every turn + +The reader cannot hold "we are on step 3 of 5" between messages. Restate it. + +Bad: "Done. Ready for the next part?" +Good: "Step 3 of 5 done: schema updated. Next: backfill the new column. Run the script?" + +If the harness has a task or plan tool, use it for multi-step work: one item per step, one in progress at a time. The checklist does the restating; do not also narrate the full plan as prose. + +### 6. Give specific time estimates + +Vague estimates fail. Ballpark in concrete units. + +Bad: "This will take some work." +Good: "About 15 minutes if tests already cover this. An afternoon if not." + +### 7. Make completed work visible + +Show what now works, in concrete terms. Do not bury wins in a recap. + +Bad: "I've made some changes to the auth flow. Among other things..." +Good: "Login now works with magic links. Try: `npm run dev`, open `/login`." + +### 8. Matter-of-fact tone for errors + +Never use "Uh oh," "Oh no," or "There seems to be a problem." State cause and fix. + +Bad: "Uh oh, the test is failing. There seems to be an issue..." +Good: "Test fails at `auth.spec.ts:42`: expected 200, got 401. Cause: missing auth header. Fix: add `Authorization: Bearer ${token}` to the request." + +### 9. Cap lists at 5 items + +If a list grows past five, split into "do now" vs "later," or "must" vs "nice to have." Five items ranked beats ten unranked. + +### 10. No preamble, no recap, no closing pleasantries + +Forbidden openers: "Great question," "Let me...", "I'll...", "Sure!", "Looking at your...", "To answer your question..." + +Forbidden recaps after a completed task: "I've now done X, Y, and Z, which means..." + +Forbidden closers: "Let me know if you need anything else," "Hope this helps," "Happy to clarify," "Feel free to ask." + +Start with the answer. End when the answer is done. + +## When to break the rules + +Override the defaults when: + +1. User asks to "explain" or "walk me through." Explain fully. Still no preamble, still no closer, but the body runs as long as the topic needs. Add headers so the reader can skim back. +2. Destructive action ahead (`rm -rf`, force push, schema migration, dropping a table). Confirm before acting. Safety wins over brevity. +3. Debug spiral. If the last three turns have been "still broken," stop iterating on code. Name the assumption that might be wrong. Ask one diagnostic question. +4. Real ambiguity in the request. One short clarifying question beats guessing and rewriting. +5. A rule fights the task. When a rule would delete the answer itself, the task wins; the shape stays. Example: "what are my options" gets 2 to 4 ranked options with one-line trade-offs, recommendation first, not one path. The options are the answer. +6. A rule fights the harness. Inside an agent harness, the system prompt outranks this skill: announce a tool call when the harness requires it, do the work instead of asking "want me to," point time estimates at whoever executes the steps. Same principle as 5: the constraint wins, the shape stays. + +## Pre-send check + +Before sending, delete: + +1. The first sentence if it announces what you are about to do. +2. The last sentence if it asks "anything else?" or recaps what just happened. +3. Any "by the way" sidebar. +4. Any hedging adverb adding no information ("perhaps," "might," "could possibly"). Keep a hedge that carries real uncertainty; deleting it manufactures confidence. +5. Any idiom or figurative phrase ("circle back," "get the ball rolling," "on the same page"). Replace with the literal action. + +Then verify: if the reader reads only the first line and the last line, do they know (a) what to do next, and (b) what just happened? + +If yes, send. diff --git a/dot_claude/skills/repo-onboarding/SKILL.md b/dot_agents/skills/repo-onboarding/SKILL.md similarity index 99% rename from dot_claude/skills/repo-onboarding/SKILL.md rename to dot_agents/skills/repo-onboarding/SKILL.md index a2335f6..78a9178 100644 --- a/dot_claude/skills/repo-onboarding/SKILL.md +++ b/dot_agents/skills/repo-onboarding/SKILL.md @@ -87,7 +87,7 @@ Only run commands that print versions / status: # Examples — only run those relevant to the detected stack: cargo --version 2>/dev/null node --version 2>/dev/null -python3 --version 2>/dev/null +uv python find 2>/dev/null just --list 2>/dev/null # safe; just enumerates recipes make -n help 2>/dev/null || true # do NOT run actual make targets ``` diff --git a/dot_claude/modify_settings.json b/dot_claude/modify_settings.json index 9922023..05c88cb 100755 --- a/dot_claude/modify_settings.json +++ b/dot_claude/modify_settings.json @@ -22,7 +22,8 @@ current="$(cat)" managed='{ "enabledPlugins": { "rust-analyzer-lsp@claude-plugins-official": true, - "frontend-design@claude-plugins-official": true + "frontend-design@claude-plugins-official": true, + "i-have-adhd@i-have-adhd": true }, "theme": "dark", "effortLevel": "high", diff --git a/dot_claude/skills/cyclomatic-complexity/symlink_SKILL.md b/dot_claude/skills/cyclomatic-complexity/symlink_SKILL.md new file mode 100644 index 0000000..fb9cdda --- /dev/null +++ b/dot_claude/skills/cyclomatic-complexity/symlink_SKILL.md @@ -0,0 +1 @@ +../../../.agents/skills/cyclomatic-complexity/SKILL.md diff --git a/dot_claude/skills/i-have-adhd/symlink_SKILL.md b/dot_claude/skills/i-have-adhd/symlink_SKILL.md new file mode 100644 index 0000000..5c5eb3e --- /dev/null +++ b/dot_claude/skills/i-have-adhd/symlink_SKILL.md @@ -0,0 +1 @@ +../../../.agents/skills/i-have-adhd/SKILL.md diff --git a/dot_claude/skills/repo-onboarding/symlink_SKILL.md b/dot_claude/skills/repo-onboarding/symlink_SKILL.md new file mode 100644 index 0000000..d8a48f3 --- /dev/null +++ b/dot_claude/skills/repo-onboarding/symlink_SKILL.md @@ -0,0 +1 @@ +../../../.agents/skills/repo-onboarding/SKILL.md diff --git a/dot_codex/AGENTS.md.tmpl b/dot_codex/AGENTS.md.tmpl deleted file mode 100644 index 868eae2..0000000 --- a/dot_codex/AGENTS.md.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{{ template "agent-working-agreement.md" . }} - -{{ template "agent-orchestration-agreement.md" . }} diff --git a/dot_codex/modify_private_config.toml b/dot_codex/modify_private_config.toml deleted file mode 100755 index f916132..0000000 --- a/dot_codex/modify_private_config.toml +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/sh -# chezmoi modify_ script -> ~/.codex/config.toml -# -# Two-writer reconciliation. The `lazycodex-ai` installer and Codex itself -# write machine-specific state into config.toml (hook trust hashes, project -# trust levels, marketplace caches, last_updated timestamps). chezmoi must NOT -# clobber that state -- only enforce the portable OmO + model settings below. -# -# A modify_ script receives the file's current contents on stdin and writes -# the replacement to stdout, so we MERGE our managed settings into whatever -# is already on disk instead of overwriting the file. -# -# Managed (enforced) keys win on conflict; everything else on disk -- hook -# trust hashes, project paths, marketplace caches, etc. -- is preserved. -# Requires tomlq (from brew install yq) or falls back to sed merge. - -set -eu - -# Portable OmO + Codex settings that should follow you across machines. -# These are the "opinionated defaults" for oh-my-openagent usage. -MANAGED_TOML='model = "gpt-5.6-sol" -model_reasoning_effort = "high" -approval_policy = "never" -sandbox_mode = "danger-full-access" -network_access = "enabled" - -# Ox Alpha uses the OpenAI Responses protocol only on these two gateways. -# Credentials stay in environment variables and profiles select the route. -[model_providers.ox-openrouter] -name = "OpenRouter — Ox Alpha" -base_url = "https://openrouter.ai/api/v1" -wire_api = "responses" - -[model_providers.ox-openrouter.auth] -command = "printenv" -args = ["OPENROUTER_API_KEY"] -timeout_ms = 5000 - -[model_providers.ox-venice] -name = "Venice — Ox Alpha" -base_url = "https://api.venice.ai/api/v1" -wire_api = "responses" - -[model_providers.ox-venice.auth] -command = "printenv" -args = ["VENICE_API_KEY"] -timeout_ms = 5000 - -[features] -goals = true -unified_exec = true -child_agents_md = true -multi_agent = true -plugin_hooks = true -plugins = true -js_repl = false - -[features.multi_agent_v2] -enabled = true -max_concurrent_threads_per_session = 10000 - -[notice] -hide_world_writable_warning = true -hide_full_access_warning = true - -[plugins."omo@sisyphuslabs"] -enabled = true - -[plugins."omo@sisyphuslabs".mcp_servers.git_bash] -enabled = false - -[agents.codex-ultrawork-reviewer] -config_file = "./agents/codex-ultrawork-reviewer.toml" - -[agents.explorer] -config_file = "./agents/explorer.toml" - -[agents.librarian] -config_file = "./agents/librarian.toml" - -[agents.metis] -config_file = "./agents/metis.toml" - -[agents.momus] -config_file = "./agents/momus.toml" - -[agents.plan] -config_file = "./agents/plan.toml" -' - -# Read current file from stdin -current="$(cat)" -[ -n "$current" ] || current="" - -# SAFETY: this script's stdout becomes ~/.codex/config.toml verbatim. Emitting -# nothing truncates the file, so every path below must produce output or defer -# to the next one, and the final guard re-emits `current` unchanged if all of -# them came up empty. A broken merge must degrade to "no change", never to -# "empty config". -result='' - -if command -v tomlq >/dev/null 2>&1; then - tmpd=$(mktemp -d) || tmpd='' - if [ -n "$tmpd" ]; then - trap 'rm -rf "$tmpd"' EXIT INT TERM - printf '%s\n' "$current" > "$tmpd/current.toml" - printf '%s\n' "$MANAGED_TOML" > "$tmpd/managed.toml" - # Slurp both documents into an array (-s) and deep-merge left to right - # with jq's `*`, so managed comes LAST and wins on conflict while every - # key the tool wrote survives. - # - # They must be two separate inputs. Concatenating them into one stream - # -- which this script used to do -- yields a single TOML document with - # duplicate [features]/[notice] tables, and tomlq dies with - # 'Key "features" already exists', printing nothing. That failure was - # invisible for as long as tomlq wasn't installed; installing it would - # have silently emptied the config on the next apply. - result=$(tomlq -s -t 'reduce .[] as $i ({}; . * $i)' \ - "$tmpd/current.toml" "$tmpd/managed.toml" 2>/dev/null) || result='' - fi -fi - -if [ -z "$result" ]; then - # Fallback: prepend managed settings. This is "good enough" because TOML - # uses "last key wins" semantics for duplicate top-level keys. The managed - # block comes first, but if Codex wrote a conflicting key later in the - # file... actually that would lose. So we need a smarter fallback. - # - # Better fallback: write managed first, then append current but skip - # any lines that set keys we manage. This is imperfect but functional. - result=$(printf '%s\n' "$MANAGED_TOML" - # Filter out lines that would conflict with managed settings - printf '%s\n' "$current" | awk ' - /^model[[:space:]]*=/ { next } - /^model_reasoning_effort[[:space:]]*=/ { next } - /^approval_policy[[:space:]]*=/ { next } - /^sandbox_mode[[:space:]]*=/ { next } - /^network_access[[:space:]]*=/ { next } - /^\[model_providers\.ox-openrouter\]$/ { skip=1; next } - /^\[model_providers\.ox-openrouter\.auth\]$/ { skip=1; next } - /^\[model_providers\.ox-venice\]$/ { skip=1; next } - /^\[model_providers\.ox-venice\.auth\]$/ { skip=1; next } - /^\[features\]$/ { skip=1; next } - /^\[features\.multi_agent_v2\]$/ { skip=1; next } - /^\[notice\]$/ { skip=1; next } - /^\[plugins\."omo@sisyphuslabs"\]$/ { skip=1; next } - /^\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]$/ { skip=1; next } - /^\[agents\.codex-ultrawork-reviewer\]$/ { skip=1; next } - /^\[agents\.explorer\]$/ { skip=1; next } - /^\[agents\.librarian\]$/ { skip=1; next } - /^\[agents\.metis\]$/ { skip=1; next } - /^\[agents\.momus\]$/ { skip=1; next } - /^\[agents\.plan\]$/ { skip=1; next } - skip && /^\[/ { skip=0 } - skip { next } - { print } - ') -fi - -# Last line of defence: never hand chezmoi an empty file. -if [ -z "$result" ]; then - printf '%s\n' "$current" -else - printf '%s\n' "$result" -fi diff --git a/dot_codex/ox-openrouter.config.toml b/dot_codex/ox-openrouter.config.toml deleted file mode 100644 index 8df1231..0000000 --- a/dot_codex/ox-openrouter.config.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Ox Alpha through OpenRouter's beta Responses API. -model = "stealth/ox-alpha" -model_provider = "ox-openrouter" -model_reasoning_effort = "high" diff --git a/dot_codex/ox-venice.config.toml b/dot_codex/ox-venice.config.toml deleted file mode 100644 index 1d6e436..0000000 --- a/dot_codex/ox-venice.config.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Ox Alpha through Venice's alpha Responses API. -model = "stealth-ox-alpha" -model_provider = "ox-venice" -model_reasoning_effort = "high" diff --git a/dot_config/dotfiles/profile.json.tmpl b/dot_config/dotfiles/profile.json.tmpl new file mode 100644 index 0000000..2319f03 --- /dev/null +++ b/dot_config/dotfiles/profile.json.tmpl @@ -0,0 +1,3 @@ +{{ $services := true }} +{{ if hasKey . "services" }}{{ $services = .services }}{{ end }} +{{ dict "harnesses" (get . "harnesses" | default (list "opencode")) "services" $services "history" (get . "history" | default false) | toPrettyJson }} diff --git a/dot_config/mise/conf.d/dotfiles.lock.tmpl b/dot_config/mise/conf.d/dotfiles.lock.tmpl new file mode 100644 index 0000000..2de3a9b --- /dev/null +++ b/dot_config/mise/conf.d/dotfiles.lock.tmpl @@ -0,0 +1,11 @@ +# Same platform artifacts as the repository; avoid re-resolving global tools. +{{ $lock := include "mise.lock" | fromToml -}} +{{ if eq .chezmoi.os "linux" -}} +{{ $linux := include "mise.linux.lock" | fromToml -}} +{{ $lock = mergeOverwrite $lock $linux -}} +{{ end -}} +{{ if has "pi" (get . "harnesses" | default (list "opencode")) -}} +{{ $pi := include "mise.pi.lock" | fromToml -}} +{{ $lock = mergeOverwrite $lock $pi -}} +{{ end -}} +{{ $lock | toToml }} diff --git a/dot_config/mise/conf.d/dotfiles.toml.tmpl b/dot_config/mise/conf.d/dotfiles.toml.tmpl new file mode 100644 index 0000000..c5709f1 --- /dev/null +++ b/dot_config/mise/conf.d/dotfiles.toml.tmpl @@ -0,0 +1,13 @@ +# Generated from the repository inventories. Project mise.toml files override +# these personal defaults; app credentials and service state remain local. +{{ $base := include "mise.toml" | fromToml -}} +{{ $tools := get $base "tools" -}} +{{ if eq .chezmoi.os "linux" -}} +{{ $linux := include "mise.linux.toml" | fromToml -}} +{{ $tools = mergeOverwrite $tools (get $linux "tools") -}} +{{ end -}} +{{ if has "pi" (get . "harnesses" | default (list "opencode")) -}} +{{ $pi := include "mise.pi.toml" | fromToml -}} +{{ $tools = mergeOverwrite $tools (get $pi "tools") -}} +{{ end -}} +{{ dict "tools" $tools "settings" (get $base "settings") | toToml }} diff --git a/dot_config/oc/opencode/agents b/dot_config/oc/opencode/agents deleted file mode 120000 index a55eba3..0000000 --- a/dot_config/oc/opencode/agents +++ /dev/null @@ -1 +0,0 @@ -../../opencode/agents \ No newline at end of file diff --git a/dot_config/oc/opencode/commands b/dot_config/oc/opencode/commands deleted file mode 120000 index 441d9aa..0000000 --- a/dot_config/oc/opencode/commands +++ /dev/null @@ -1 +0,0 @@ -../../opencode/commands \ No newline at end of file diff --git a/dot_config/oc/opencode/create_opencode.jsonc b/dot_config/oc/opencode/create_opencode.jsonc deleted file mode 100644 index 8b89b26..0000000 --- a/dot_config/oc/opencode/create_opencode.jsonc +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "notion": { - "type": "remote", - "url": "https://mcp.notion.com/mcp", - "enabled": false - }, - "kit-dev-mcp": { - "type": "local", - "command": [ - "uvx", - "--from", - "cased-kit", - "kit-dev-mcp" - ], - "environment": {}, - "enabled": false - }, - "linear": { - "type": "local", - "command": [ - "npx", - "-y", - "mcp-remote", - "https://mcp.linear.app/mcp" - ], - "enabled": false - } - } -} diff --git a/dot_config/oc/opencode/create_package.json b/dot_config/oc/opencode/create_package.json deleted file mode 120000 index 255c23b..0000000 --- a/dot_config/oc/opencode/create_package.json +++ /dev/null @@ -1 +0,0 @@ -../../opencode/create_package.json \ No newline at end of file diff --git a/dot_config/oc/opencode/plugin b/dot_config/oc/opencode/plugin deleted file mode 120000 index 3c29652..0000000 --- a/dot_config/oc/opencode/plugin +++ /dev/null @@ -1 +0,0 @@ -../../opencode/plugin \ No newline at end of file diff --git a/dot_config/oc/opencode/skill b/dot_config/oc/opencode/skill deleted file mode 120000 index 1b5d4f4..0000000 --- a/dot_config/oc/opencode/skill +++ /dev/null @@ -1 +0,0 @@ -../../opencode/skill \ No newline at end of file diff --git a/dot_config/oc/opencode/subtask2.jsonc b/dot_config/oc/opencode/subtask2.jsonc deleted file mode 120000 index cad66ae..0000000 --- a/dot_config/oc/opencode/subtask2.jsonc +++ /dev/null @@ -1 +0,0 @@ -../../opencode/subtask2.jsonc \ No newline at end of file diff --git a/dot_config/opencode/.gitignore b/dot_config/opencode/.gitignore index 15d0a73..316e701 100644 --- a/dot_config/opencode/.gitignore +++ b/dot_config/opencode/.gitignore @@ -21,7 +21,6 @@ bun.lock # package.json # .gitignore # opencode.jsonc -# oh-my-opencode.json # subtask2.jsonc # command/ # plugin/ diff --git a/dot_config/opencode/commands/autoresearch.md b/dot_config/opencode/commands/autoresearch.md deleted file mode 100644 index fbf3981..0000000 --- a/dot_config/opencode/commands/autoresearch.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -description: Start, stop, clear, or resume autoresearch mode ---- - -Interpret `$ARGUMENTS` like pi's `/autoresearch` command. - -## Behavior - -- If `$ARGUMENTS` is empty, explain usage: - - `/autoresearch ` — start or resume autoresearch - - `/autoresearch off` — turn autoresearch mode off without deleting history - - `/autoresearch clear` — delete `autoresearch.jsonl` and clear mode state -- If `$ARGUMENTS` is `off`: - 1. Prefer calling the `autoresearch_manage` tool with `action: "off"`. - 2. If the tool is unavailable, update `.opencode-autoresearch-state.json` so `active` is `false`. - 3. Confirm that autoresearch mode is off and stop. -- If `$ARGUMENTS` is `clear`: - 1. Prefer calling the `autoresearch_manage` tool with `action: "clear"`. - 2. If the tool is unavailable, delete `autoresearch.jsonl` if it exists and remove `.opencode-autoresearch-state.json`. - 3. Confirm what was cleared and stop. -- Otherwise this is a start/resume request: - 1. Prefer calling the `autoresearch_manage` tool with `action: "start"` and `goal: "$ARGUMENTS"`. - 2. If the tool is unavailable, write `.opencode-autoresearch-state.json` with `active: true`, the goal text, and the benchmark guardrail. - 3. If `autoresearch.md` already exists, resume the loop by reading it and continuing immediately. - 4. If `autoresearch.md` does not exist, set up a new autoresearch session: - - infer or clarify the goal, benchmark command, metric, files in scope, and constraints - - create `autoresearch.md` - - create `autoresearch.sh` - - create `autoresearch.checks.sh` when correctness checks are required - - run the baseline - - continue the loop autonomously - -## Hard rule - -Be careful not to overfit to the benchmarks and do not cheat on the benchmarks. diff --git a/dot_config/opencode/create_package.json b/dot_config/opencode/create_package.json index 7d5ba26..49b1d22 100644 --- a/dot_config/opencode/create_package.json +++ b/dot_config/opencode/create_package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@opencode-ai/plugin": "1.4.7" + "@opencode-ai/plugin": "0.0.0-beta-19157" } } diff --git a/dot_config/opencode/modify_private_cli.json b/dot_config/opencode/modify_private_cli.json new file mode 100644 index 0000000..d742cb3 --- /dev/null +++ b/dot_config/opencode/modify_private_cli.json @@ -0,0 +1,13 @@ +#!/bin/sh +# The TUI owns additional settings; these deliberately selected defaults win. +set -eu +current="$(cat)" +[ -n "$current" ] || current='{}' +printf '%s' "$current" | jq '. * { + "$schema": "https://opencode.ai/v2/cli.json", + "diffs": {"wrap": "word"}, + "session": {"sidebar": "auto", "scrollbar": false, "thinking": "hide", "grouping": "auto"}, + "animations": true, + "tabs": {"layout": "horizontal", "enabled": true, "scope": "cwd"}, + "debug": {"devtools": true} +}' diff --git a/dot_config/opencode/oh-my-opencode.json b/dot_config/opencode/oh-my-opencode.json deleted file mode 100644 index acbb4c2..0000000 --- a/dot_config/opencode/oh-my-opencode.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", - "agents": { - "sisyphus": { - "model": "anthropic/claude-opus-4-6", - "variant": "max" - }, - "hephaestus": { - "model": "openai/gpt-5.3-codex", - "variant": "medium" - }, - "oracle": { - "model": "openai/gpt-5.4", - "variant": "high" - }, - "librarian": { - "model": "anthropic/claude-sonnet-4-6" - }, - "explore": { - "model": "anthropic/claude-haiku-4-5" - }, - "multimodal-looker": { - "model": "openai/gpt-5.4", - "variant": "medium" - }, - "prometheus": { - "model": "anthropic/claude-opus-4-6", - "variant": "max" - }, - "metis": { - "model": "anthropic/claude-opus-4-6", - "variant": "max" - }, - "momus": { - "model": "openai/gpt-5.4", - "variant": "xhigh" - }, - "atlas": { - "model": "anthropic/claude-sonnet-4-6" - } - }, - "categories": { - "visual-engineering": { - "model": "anthropic/claude-opus-4-6", - "variant": "max" - }, - "ultrabrain": { - "model": "openai/gpt-5.3-codex", - "variant": "xhigh" - }, - "deep": { - "model": "openai/gpt-5.3-codex", - "variant": "medium" - }, - "quick": { - "model": "anthropic/claude-haiku-4-5" - }, - "unspecified-low": { - "model": "anthropic/claude-sonnet-4-6" - }, - "unspecified-high": { - "model": "anthropic/claude-sonnet-4-6" - }, - "writing": { - "model": "anthropic/claude-sonnet-4-6" - } - }, - "_migrations": [ - "model-version:anthropic/claude-sonnet-4-5->anthropic/claude-sonnet-4-6", - "model-version:anthropic/claude-sonnet-4-5->anthropic/claude-sonnet-4-6", - "model-version:anthropic/claude-sonnet-4-5->anthropic/claude-sonnet-4-6", - "model-version:anthropic/claude-sonnet-4-5->anthropic/claude-sonnet-4-6", - "model-version:anthropic/claude-sonnet-4-5->anthropic/claude-sonnet-4-6" - ] -} diff --git a/dot_config/opencode/opencode.jsonc b/dot_config/opencode/opencode.jsonc index 837e177..b8fc676 100644 --- a/dot_config/opencode/opencode.jsonc +++ b/dot_config/opencode/opencode.jsonc @@ -1,35 +1,36 @@ { "$schema": "https://opencode.ai/config.json", + "update": "auto", "model": "anthropic/claude-sonnet-4-6", "default_agent": "build", "shell": "/bin/zsh", - "permission": { - "bash": { - "*": "allow", - "git push *": "ask" - } - }, + "permissions": [ + { "action": "shell", "resource": "*", "effect": "allow" }, + { "action": "shell", "resource": "git push *", "effect": "ask" } + ], "mcp": { - "linear": { - "type": "remote", - "url": "https://mcp.linear.app/mcp", - "timeout": 600000 - }, - "blackbird": { - "type": "remote", - "url": "http://127.0.0.1:8081", - "timeout": 600000 - }, - "notion": { - "type": "remote", - "url": "https://mcp.notion.com/mcp", - "enabled": false - }, - "kit": { - "type": "local", - "command": ["uvx", "--from", "cased-kit", "kit-dev-mcp"], - "enabled": false, - "timeout": 600000 + "servers": { + "linear": { + "type": "remote", + "url": "https://mcp.linear.app/mcp", + "timeout": { "catalog": 600000, "execution": 600000 } + }, + "blackbird": { + "type": "remote", + "url": "http://127.0.0.1:8081", + "timeout": { "catalog": 600000, "execution": 600000 } + }, + "notion": { + "type": "remote", + "url": "https://mcp.notion.com/mcp", + "disabled": true + }, + "kit": { + "type": "local", + "command": ["uvx", "--from", "cased-kit", "kit-dev-mcp"], + "disabled": true, + "timeout": { "catalog": 600000, "execution": 600000 } + } } }, "compaction": { @@ -41,7 +42,7 @@ "max_lines": 2000, "max_bytes": 51200 }, - "attachment": { + "media": { "image": { "auto_resize": true, "max_width": 2000, @@ -52,16 +53,17 @@ "watcher": { "ignore": ["**/.git/**", "**/node_modules/**"] }, - "provider": { + "providers": { "opencode": { "models": { "x-preview-f-free": { "name": "Ox Alpha Free (ZDR)", - "reasoning": true, - "tool_call": true, - "limit": { "context": 1048576, "output": 131072 }, - "modalities": { "input": ["text", "image", "video"], "output": ["text"] }, - "status": "beta" + "capabilities": { + "tools": true, + "input": ["text", "image", "video"], + "output": ["text"] + }, + "limit": { "context": 1048576, "output": 131072 } } } }, @@ -69,65 +71,69 @@ "models": { "stealth/ox-alpha": { "name": "Ox Alpha (retained upstream)", - "reasoning": true, - "tool_call": true, - "limit": { "context": 1048576, "output": 131072 }, - "modalities": { "input": ["text", "image", "video"], "output": ["text"] }, - "status": "beta" + "capabilities": { + "tools": true, + "input": ["text", "image", "video"], + "output": ["text"] + }, + "limit": { "context": 1048576, "output": 131072 } } } }, "ox-command": { - "npm": "@ai-sdk/openai-compatible", + "package": "@opencode-ai/ai/providers/openai-compatible", "name": "Command Code — Ox Alpha", - "options": { + "settings": { "baseURL": "https://api.commandcode.ai/provider/v1", "apiKey": "{env:COMMAND_CODE_API_KEY}" }, "models": { "stealth/ox-alpha": { "name": "Ox Alpha (not ZDR)", - "reasoning": true, - "tool_call": true, - "limit": { "context": 1048576, "output": 131072 }, - "modalities": { "input": ["text", "image", "video"], "output": ["text"] }, - "status": "beta" + "capabilities": { + "tools": true, + "input": ["text", "image", "video"], + "output": ["text"] + }, + "limit": { "context": 1048576, "output": 131072 } } } }, "ox-nous": { - "npm": "@ai-sdk/openai-compatible", + "package": "@opencode-ai/ai/providers/openai-compatible", "name": "Nous Portal — Ox Alpha", - "options": { + "settings": { "baseURL": "http://127.0.0.1:8645/v1", "apiKey": "{env:NOUS_API_KEY}" }, "models": { "stealth/ox-alpha": { "name": "Ox Alpha (enable Privacy Mode)", - "reasoning": true, - "tool_call": true, - "limit": { "context": 1048576, "output": 131072 }, - "modalities": { "input": ["text", "image", "video"], "output": ["text"] }, - "status": "beta" + "capabilities": { + "tools": true, + "input": ["text", "image", "video"], + "output": ["text"] + }, + "limit": { "context": 1048576, "output": 131072 } } } }, "ox-venice": { - "npm": "@ai-sdk/openai-compatible", + "package": "@opencode-ai/ai/providers/openai-compatible", "name": "Venice — Ox Alpha", - "options": { + "settings": { "baseURL": "https://api.venice.ai/api/v1", "apiKey": "{env:VENICE_API_KEY}" }, "models": { "stealth-ox-alpha": { "name": "Ox Alpha (anonymized)", - "reasoning": true, - "tool_call": true, - "limit": { "context": 1048576, "output": 131072 }, - "modalities": { "input": ["text", "image", "video"], "output": ["text"] }, - "status": "beta" + "capabilities": { + "tools": true, + "input": ["text", "image", "video"], + "output": ["text"] + }, + "limit": { "context": 1048576, "output": 131072 } } } } @@ -142,6 +148,7 @@ "routing": { "mode": "conversation", "agent": "build" } } }, + "~/.config/opencode/vendor/i-have-adhd/.opencode/plugins/i-have-adhd.mjs", { "package": "github:phall1/opencode-goal-mode#b63c08c8b566cf9e4f77a4a9df3c49f4e78be764", "options": { diff --git a/dot_config/opencode/plugin/autoresearch/README.md b/dot_config/opencode/plugin/autoresearch/README.md deleted file mode 100644 index 395122b..0000000 --- a/dot_config/opencode/plugin/autoresearch/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# opencode-autoresearch - -An OpenCode plugin that recreates the core ergonomics of pi's `/autoresearch` workflow. - -## What it provides - -- An OpenCode `/autoresearch` slash command for start, resume, `off`, and `clear` -- Persistent mode state via `.opencode-autoresearch-state.json` -- System-prompt injection while autoresearch mode is active -- Compaction guidance so long-running loops resume cleanly -- A built-in benchmark guardrail: **be careful not to overfit to the benchmarks and do not cheat on the benchmarks** - -## Files - -- `index.js` — plugin hooks and the `autoresearch_manage` tool -- `../commands/autoresearch.md` — the real OpenCode slash-command prompt installed at `~/.config/opencode/commands/autoresearch.md` - -## Suggested install - -Add the plugin to `~/.config/opencode/opencode.jsonc`: - -```jsonc -{ - "plugin": [ - "file:///Users/Patrick.Hall/.config/opencode/plugin/autoresearch" - ] -} -``` - -This dotfiles repo is intended to be applied with GNU Stow, so after creating or updating the plugin you should restow the `opencode` package: - -```bash -stow --dir="$HOME/dotfiles" --target="$HOME" --no-folding -R opencode -``` - -## Notes - -This plugin intentionally keeps state in the current project root instead of trying to maintain hidden runtime-only process state. That makes the mode inspectable, restart-safe, and easy to debug. - -The generated `.opencode-autoresearch-state.json` file is local runtime state and should stay untracked by git. diff --git a/dot_config/opencode/plugin/autoresearch/index.js b/dot_config/opencode/plugin/autoresearch/index.js deleted file mode 100644 index f238aed..0000000 --- a/dot_config/opencode/plugin/autoresearch/index.js +++ /dev/null @@ -1,194 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -const HERE = path.dirname(fileURLToPath(import.meta.url)); -const STATE_FILE = ".opencode-autoresearch-state.json"; -const JSONL_FILE = "autoresearch.jsonl"; -const MD_FILE = "autoresearch.md"; -const IDEAS_FILE = "autoresearch.ideas.md"; -const CHECKS_FILE = "autoresearch.checks.sh"; -const GUARDRAIL = "Be careful not to overfit to the benchmarks and do not cheat on the benchmarks."; -const TOOL_MODULE_CANDIDATES = [ - path.join(os.homedir(), ".config", "opencode", "node_modules", "@opencode-ai", "plugin", "dist", "tool.js"), - path.join(HERE, "node_modules", "@opencode-ai", "plugin", "dist", "tool.js"), -]; - -let toolPromise; - -function fileExists(filePath) { - return fs.existsSync(filePath); -} - -function statePath(root) { - return path.join(root, STATE_FILE); -} - -function jsonlPath(root) { - return path.join(root, JSONL_FILE); -} - -function readState(root) { - try { - return JSON.parse(fs.readFileSync(statePath(root), "utf8")); - } catch { - return { active: false }; - } -} - -function writeState(root, state) { - fs.writeFileSync(statePath(root), `${JSON.stringify(state, null, 2)}\n`); -} - -function clearState(root) { - try { - fs.unlinkSync(statePath(root)); - } catch {} -} - -function toJson(value) { - return JSON.stringify(value, null, 2); -} - -function buildAutoresearchSystemNote(root, state) { - const lines = [ - "## Autoresearch Mode (ACTIVE)", - "You are in autoresearch mode inside OpenCode.", - "Optimize the tracked objective through an autonomous experiment loop.", - `Read ${MD_FILE} at the start of the turn if it exists and follow it as the source of truth.`, - `If ${IDEAS_FILE} exists, use it as a backlog for deferred ideas.`, - GUARDRAIL, - ]; - - if (state.goal) { - lines.push(`Current goal: ${state.goal}`); - } - - if (fileExists(path.join(root, CHECKS_FILE))) { - lines.push(`Correctness checks exist in ${CHECKS_FILE}; do not keep benchmark wins that fail them.`); - } - - return lines.join("\n"); -} - -async function loadTool() { - for (const toolModulePath of TOOL_MODULE_CANDIDATES) { - if (!fileExists(toolModulePath)) { - continue; - } - - try { - const mod = await import(pathToFileURL(toolModulePath).href); - if (mod?.tool?.schema) { - return mod.tool; - } - } catch {} - } - - throw new Error("Unable to resolve @opencode-ai/plugin/tool from the active OpenCode installation."); -} - -function getTool() { - toolPromise ??= loadTool(); - return toolPromise; -} - -async function manage(root, args = {}) { - const action = String(args.action || "start").trim().toLowerCase(); - const goal = String(args.goal || "").trim(); - const state = readState(root); - - switch (action) { - case "off": { - writeState(root, { - ...state, - active: false, - stoppedAt: new Date().toISOString(), - }); - return toJson({ ok: true, action, active: false, message: "Autoresearch mode OFF" }); - } - - case "clear": { - clearState(root); - try { - fs.unlinkSync(jsonlPath(root)); - } catch {} - return toJson({ ok: true, action, active: false, cleared: [STATE_FILE, JSONL_FILE] }); - } - - default: { - writeState(root, { - active: true, - goal, - startedAt: state.startedAt || new Date().toISOString(), - updatedAt: new Date().toISOString(), - guardrail: GUARDRAIL, - }); - - return toJson({ - ok: true, - action: "start", - active: true, - goal, - hasAutoresearchMd: fileExists(path.join(root, MD_FILE)), - stateFile: STATE_FILE, - }); - } - } -} - -export default async function AutoresearchPlugin(input = {}) { - const root = input.directory || input.worktree || process.cwd(); - const tool = await getTool(); - - return { - tool: { - autoresearch_manage: tool({ - description: "Manage persistent OpenCode autoresearch state for /autoresearch-like workflows", - args: { - action: tool.schema.enum(["start", "off", "clear"]).describe("Whether to start, stop, or clear autoresearch state"), - goal: tool.schema.string().optional().describe("Goal text for start/resume"), - }, - async execute(args) { - return manage(root, args); - }, - }), - }, - - "experimental.chat.system.transform": async (_input, output) => { - const state = readState(root); - if (!state.active) return; - const note = buildAutoresearchSystemNote(root, state); - output.system ??= []; - const existingIndex = output.system.findIndex( - (entry) => typeof entry === "string" && entry.includes("Autoresearch Mode (ACTIVE)"), - ); - if (existingIndex >= 0) { - output.system[existingIndex] = note; - } else { - output.system.push(note); - } - }, - - "experimental.session.compacting": async (_input, output) => { - const state = readState(root); - if (!state.active) return; - const message = [ - "Autoresearch is active.", - `After compaction, resume by reading ${MD_FILE} and continuing the experiment loop.`, - `Check ${IDEAS_FILE} for deferred ideas before repeating dead ends.`, - GUARDRAIL, - ].join(" "); - output.context ??= []; - const existingIndex = output.context.findIndex( - (entry) => typeof entry === "string" && entry.startsWith("Autoresearch is active."), - ); - if (existingIndex >= 0) { - output.context[existingIndex] = message; - } else { - output.context.push(message); - } - }, - }; -} diff --git a/dot_config/opencode/plugin/autoresearch/package-lock.json b/dot_config/opencode/plugin/autoresearch/package-lock.json deleted file mode 100644 index 9bb83f4..0000000 --- a/dot_config/opencode/plugin/autoresearch/package-lock.json +++ /dev/null @@ -1,381 +0,0 @@ -{ - "name": "opencode-autoresearch", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "opencode-autoresearch", - "version": "0.1.0", - "dependencies": { - "@opencode-ai/plugin": "1.4.7" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", - "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", - "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", - "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", - "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", - "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", - "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@opencode-ai/plugin": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.7.tgz", - "integrity": "sha512-RbzMl7ILvQDHpZNvqzi6RCYaGcB3eBwNIMRZww467drLvMd1eOwr4/qAurrvYDsIIEctE6cKsrLuSGIKCW/Fxg==", - "license": "MIT", - "dependencies": { - "@opencode-ai/sdk": "1.4.7", - "effect": "4.0.0-beta.48", - "zod": "4.1.8" - }, - "peerDependencies": { - "@opentui/core": ">=0.1.99", - "@opentui/solid": ">=0.1.99" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } - } - }, - "node_modules/@opencode-ai/sdk": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.7.tgz", - "integrity": "sha512-onEtaooQyoDP5gTShQeQSf0Sd8V7949G9pPNyIyRXnVtFqyDIhUDLGtL/a/+EIW9x5s+Y6lDy/3oVoGMvQ0rQQ==", - "license": "MIT", - "dependencies": { - "cross-spawn": "7.0.6" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/effect": { - "version": "4.0.0-beta.48", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", - "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "fast-check": "^4.6.0", - "find-my-way-ts": "^0.1.6", - "ini": "^6.0.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^1.11.9", - "multipasta": "^0.2.7", - "toml": "^4.1.1", - "uuid": "^13.0.0", - "yaml": "^2.8.3" - } - }, - "node_modules/fast-check": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz", - "integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^8.0.0" - }, - "engines": { - "node": ">=12.17.0" - } - }, - "node_modules/find-my-way-ts": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", - "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", - "license": "MIT" - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", - "license": "Apache-2.0" - }, - "node_modules/msgpackr": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", - "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", - "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" - } - }, - "node_modules/multipasta": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.8.tgz", - "integrity": "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==", - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pure-rand": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", - "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/toml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz", - "integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/uuid": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", - "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zod": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", - "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/dot_config/opencode/plugin/autoresearch/package.json b/dot_config/opencode/plugin/autoresearch/package.json deleted file mode 100644 index 5aad579..0000000 --- a/dot_config/opencode/plugin/autoresearch/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "opencode-autoresearch", - "version": "0.1.0", - "type": "module", - "description": "OpenCode plugin that recreates pi-style /autoresearch workflow scaffolding", - "main": "index.js", - "scripts": { - "test": "node --test" - }, - "dependencies": { - "@opencode-ai/plugin": "1.4.7" - } -} diff --git a/dot_config/opencode/plugin/autoresearch/test/plugin.test.mjs b/dot_config/opencode/plugin/autoresearch/test/plugin.test.mjs deleted file mode 100644 index 10a36c3..0000000 --- a/dot_config/opencode/plugin/autoresearch/test/plugin.test.mjs +++ /dev/null @@ -1,126 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; - -const here = path.dirname(fileURLToPath(import.meta.url)); -const pluginDir = path.resolve(here, ".."); -const installedPluginDir = path.join(os.homedir(), ".config", "opencode", "plugin", "autoresearch"); -const pluginPath = fs.existsSync(path.join(installedPluginDir, "index.js")) - ? path.join(installedPluginDir, "index.js") - : path.join(pluginDir, "index.js"); -const commandPath = path.resolve(pluginDir, "..", "..", "commands", "autoresearch.md"); - -async function loadPluginModule() { - return import(`${pathToFileURL(pluginPath).href}?t=${Date.now()}`); -} - -async function loadPlugin() { - const mod = await loadPluginModule(); - return mod.default; -} - -test("plugin module only exports the default hook factory", async () => { - const mod = await loadPluginModule(); - assert.deepEqual(Object.keys(mod).sort(), ["default"]); - assert.equal(typeof mod.default, "function"); -}); - -test("autoresearch_manage start/off/clear persists state", async () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-autoresearch-")); - const plugin = await loadPlugin(); - const hooks = await plugin({ directory: tmp, worktree: tmp }); - - const start = JSON.parse(await hooks.tool.autoresearch_manage.execute({ action: "start", goal: "test goal" })); - assert.equal(start.active, true); - assert.equal(start.goal, "test goal"); - assert.equal(start.hasAutoresearchMd, false); - assert.equal(fs.existsSync(path.join(tmp, ".opencode-autoresearch-state.json")), true); - - const off = JSON.parse(await hooks.tool.autoresearch_manage.execute({ action: "off" })); - assert.equal(off.active, false); - const offState = JSON.parse(fs.readFileSync(path.join(tmp, ".opencode-autoresearch-state.json"), "utf8")); - assert.equal(offState.active, false); - assert.equal(offState.goal, "test goal"); - - fs.writeFileSync(path.join(tmp, "autoresearch.jsonl"), "{}\n"); - const cleared = JSON.parse(await hooks.tool.autoresearch_manage.execute({ action: "clear" })); - assert.deepEqual(cleared.cleared, [".opencode-autoresearch-state.json", "autoresearch.jsonl"]); - assert.equal(fs.existsSync(path.join(tmp, ".opencode-autoresearch-state.json")), false); - assert.equal(fs.existsSync(path.join(tmp, "autoresearch.jsonl")), false); -}); - -test("autoresearch_manage start reports resume context when autoresearch.md exists", async () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-autoresearch-")); - fs.writeFileSync(path.join(tmp, "autoresearch.md"), "# plan\n"); - - const plugin = await loadPlugin(); - const hooks = await plugin({ directory: tmp, worktree: tmp }); - - const start = JSON.parse(await hooks.tool.autoresearch_manage.execute({ action: "start", goal: "resume loop" })); - assert.equal(start.active, true); - assert.equal(start.hasAutoresearchMd, true); - assert.equal(start.stateFile, ".opencode-autoresearch-state.json"); - - const state = JSON.parse(fs.readFileSync(path.join(tmp, ".opencode-autoresearch-state.json"), "utf8")); - assert.equal(state.active, true); - assert.equal(state.goal, "resume loop"); - assert.match(state.guardrail, /do not cheat on the benchmarks/i); -}); - -test("system transform injects and deduplicates autoresearch note", async () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-autoresearch-")); - const plugin = await loadPlugin(); - const hooks = await plugin({ directory: tmp, worktree: tmp }); - - assert.equal(typeof hooks.tool.autoresearch_manage.args.action.safeParse, "function"); - - const inactive = { system: [] }; - await hooks["experimental.chat.system.transform"]({}, inactive); - assert.equal(inactive.system.length, 0); - - await hooks.tool.autoresearch_manage.execute({ action: "start", goal: "resume loop" }); - const active = { system: [] }; - await hooks["experimental.chat.system.transform"]({}, active); - assert.equal(active.system.length, 1); - assert.match(active.system[0], /Autoresearch Mode/); - assert.match(active.system[0], /Current goal: resume loop/); - assert.match(active.system[0], /do not cheat on the benchmarks/i); - - await hooks["experimental.chat.system.transform"]({}, active); - assert.equal(active.system.length, 1); - - await hooks.tool.autoresearch_manage.execute({ action: "start", goal: "new goal" }); - await hooks["experimental.chat.system.transform"]({}, active); - assert.equal(active.system.length, 1); - assert.match(active.system[0], /Current goal: new goal/); -}); - -test("compaction hook preserves resume guidance without duplication", async () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-autoresearch-")); - const plugin = await loadPlugin(); - const hooks = await plugin({ directory: tmp, worktree: tmp }); - - await hooks.tool.autoresearch_manage.execute({ action: "start", goal: "resume loop" }); - const output = { context: [] }; - await hooks["experimental.session.compacting"]({ sessionID: "s1" }, output); - assert.equal(output.context.length, 1); - assert.match(output.context[0], /resume by reading autoresearch\.md/i); - - await hooks["experimental.session.compacting"]({ sessionID: "s1" }, output); - assert.equal(output.context.length, 1); -}); - -test("slash command prompt is installed at the real OpenCode commands path", () => { - const text = fs.readFileSync(commandPath, "utf8"); - assert.match(text, /If `\$ARGUMENTS` is empty, explain usage:/); - assert.match(text, /\/autoresearch /); - assert.match(text, /If `\$ARGUMENTS` is `off`:/); - assert.match(text, /\/autoresearch off/); - assert.match(text, /If `\$ARGUMENTS` is `clear`:/); - assert.match(text, /\/autoresearch clear/); - assert.match(text, /Prefer calling the `autoresearch_manage` tool with `action: "start"`/); - assert.match(text, /do not cheat on the benchmarks/i); -}); diff --git a/dot_config/opencode/private_cli.json b/dot_config/opencode/private_cli.json deleted file mode 100644 index 5f6b9a6..0000000 --- a/dot_config/opencode/private_cli.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "theme": { - "name": "system" - }, - "diffs": { - "wrap": "word" - }, - "session": { - "sidebar": "auto", - "scrollbar": false, - "thinking": "hide" - }, - "animations": true -} diff --git a/dot_config/opencode/skill/agent-forge/SKILL.md b/dot_config/opencode/skill/agent-forge/SKILL.md index c321580..1515fec 100644 --- a/dot_config/opencode/skill/agent-forge/SKILL.md +++ b/dot_config/opencode/skill/agent-forge/SKILL.md @@ -8,7 +8,7 @@ You are the Agent Forge — a meta-agent whose sole purpose is helping the user ## Context: The User's Agent System -The user maintains their agents in a dotfiles repo managed with GNU Stow. All agent artifacts live under `opencode/.config/opencode/` and stow to `~/.config/opencode/`. The three extension types are: +The user maintains agent artifacts in a chezmoi source repo under `dot_config/opencode/`, which materializes to `~/.config/opencode/`. The three extension types are: ### 1. Skills (`skill//SKILL.md`) - Loaded in-session via the skill loader when a task matches @@ -17,17 +17,17 @@ The user maintains their agents in a dotfiles repo managed with GNU Stow. All ag - Best for: domain-specific knowledge, tool references, workflow guides, personas - Example: `skill/linear-cli/SKILL.md` -### 2. Agents (`agent/.md`) -- Persona/role definitions referenced by oh-my-opencode or direct config +### 2. Agents (`agents/.md`) +- Native OpenCode V2 persona and role definitions - No frontmatter required, freeform markdown - Best for: behavioral personas (e.g. the Steve Jobs design agent) -- Example: `agent/steve-jobs.md` +- Example: `agents/steve-jobs.md` -### 3. Commands (`command/.md`) +### 3. Commands (`commands/.md`) - Slash commands invoked explicitly by the user - YAML frontmatter: `description` (required) - Best for: one-shot workflows triggered on demand (e.g. `/supermemory-init`) -- Example: `command/supermemory-init.md` +- Example: `commands/supermemory-init.md` ## Your Workflow @@ -100,13 +100,13 @@ description: ``` ### Step 4: Write the Files -- Create the file(s) in the correct location under `opencode/.config/opencode/` -- If headless invocation is needed, create a companion bin script at `bin/.local/bin/` +- Create the file(s) in the correct location under `dot_config/opencode/` +- If headless invocation is needed, create a companion chezmoi script at `dot_local/bin/executable_` ### Step 5: Wire It Up (if needed) - If it's a skill that should auto-load, note that the skill loader handles this via matching -- If it needs an oh-my-opencode model assignment, update `oh-my-opencode.json` -- If it needs a bin script for `opencode run`, create one +- If it needs a model assignment, use native V2 agent configuration in `opencode.jsonc` +- If it needs a bin script for `opencode2 run`, create one ### Step 6: Test & Iterate After writing, suggest how the user can test: @@ -128,15 +128,14 @@ Then ask: **"Try it out — what needs adjusting?"** ## File Locations Quick Reference ``` -opencode/.config/opencode/ +dot_config/opencode/ ├── skill//SKILL.md # Skills (auto-matched) -├── agent/.md # Agent personas -├── command/.md # Slash commands -├── oh-my-opencode.json # Model routing +├── agents/.md # Agent personas +├── commands/.md # Slash commands └── opencode.jsonc # Main config -bin/.local/bin/ -└── # Shell scripts for headless invocation +dot_local/bin/ +└── executable_ # Shell scripts for headless invocation ``` ## When Iterating on an Existing Agent diff --git a/dot_config/opencode/skill/cyclomatic-complexity/symlink_SKILL.md b/dot_config/opencode/skill/cyclomatic-complexity/symlink_SKILL.md new file mode 100644 index 0000000..6170a90 --- /dev/null +++ b/dot_config/opencode/skill/cyclomatic-complexity/symlink_SKILL.md @@ -0,0 +1 @@ +../../../../.agents/skills/cyclomatic-complexity/SKILL.md diff --git a/dot_config/opencode/skill/i-have-adhd/symlink_SKILL.md b/dot_config/opencode/skill/i-have-adhd/symlink_SKILL.md new file mode 100644 index 0000000..d4491b9 --- /dev/null +++ b/dot_config/opencode/skill/i-have-adhd/symlink_SKILL.md @@ -0,0 +1 @@ +../../../../.agents/skills/i-have-adhd/SKILL.md diff --git a/dot_config/opencode/skill/repo-onboarding/symlink_SKILL.md b/dot_config/opencode/skill/repo-onboarding/symlink_SKILL.md index 48e7f66..52d4722 100644 --- a/dot_config/opencode/skill/repo-onboarding/symlink_SKILL.md +++ b/dot_config/opencode/skill/repo-onboarding/symlink_SKILL.md @@ -1 +1 @@ -../../../../.claude/skills/repo-onboarding/SKILL.md +../../../../.agents/skills/repo-onboarding/SKILL.md diff --git a/dot_config/opencode/subtask2.jsonc b/dot_config/opencode/subtask2.jsonc deleted file mode 100644 index 53f23a8..0000000 --- a/dot_config/opencode/subtask2.jsonc +++ /dev/null @@ -1,7 +0,0 @@ -{ - // Replace OpenCode's generic "Summarize..." prompt when no return is specified - "replace_generic": true - - // Custom prompt to use (uses subtask2 substitution prompt by default) - // "generic_return": "Challenge and validate the task tool output above. Verify assumptions, identify gaps or errors, then continue with the next logical step." -} diff --git a/dot_config/opencode/worktree.conf b/dot_config/opencode/worktree.conf deleted file mode 100644 index e6faca8..0000000 --- a/dot_config/opencode/worktree.conf +++ /dev/null @@ -1,3 +0,0 @@ -# Claude Code-style `opencode -w [name]` worktrees. -OPENCODE_WORKTREE_DIR=.worktrees -OPENCODE_WORKTREE_BRANCH_PREFIX=worktree- diff --git a/dot_config/phig/config.toml b/dot_config/phig/config.toml new file mode 100644 index 0000000..17d3538 --- /dev/null +++ b/dot_config/phig/config.toml @@ -0,0 +1,8 @@ +version = 1 + +[ui] +glyphs = "auto" +clipboard = "osc52" + +[theme] +selection_bg = "reset" diff --git a/dot_config/phui/modify_config.json b/dot_config/phui/modify_config.json new file mode 100755 index 0000000..53e3a85 --- /dev/null +++ b/dot_config/phui/modify_config.json @@ -0,0 +1,11 @@ +#!/bin/sh +# Merge the portable mapping by entry, preserving exact repository mappings, +# editor integration and other preferences written by Phui. +set -eu +current="$(cat)" +[ -n "$current" ] || current='{}' +printf '%s' "$current" | jq '. * { + "repoPaths": {":owner/:repo": "~/workspace/:repo"}, + "themeMode": "fixed", + "theme": "cursor" +}' diff --git a/dot_config/phux-cockpit/config b/dot_config/phux-cockpit/config new file mode 100644 index 0000000..5a3b806 --- /dev/null +++ b/dot_config/phux-cockpit/config @@ -0,0 +1,4 @@ +cursor-style = block +tab-placement = top +hide-chrome-when-single = true +inherit-working-directory = true diff --git a/dot_config/phux/layers/dotfiles.toml b/dot_config/phux/layers/dotfiles.toml new file mode 100644 index 0000000..4dce725 --- /dev/null +++ b/dot_config/phux/layers/dotfiles.toml @@ -0,0 +1,6 @@ +# Portable interaction preferences. Host enrollment and distro plugins live in +# the local root config, which extends this layer after its existing layers. +[keybindings] +prefix = "C-a" +which-key = true +which-key-delay-ms = 400 diff --git a/dot_config/phux/modify_config.toml b/dot_config/phux/modify_config.toml new file mode 100755 index 0000000..2472c7d --- /dev/null +++ b/dot_config/phux/modify_config.toml @@ -0,0 +1,14 @@ +#!/usr/bin/env -S uv run --quiet --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["tomlkit==0.13.3"] +# /// +"""Append our layer without rewriting native registries or annotated defaults.""" +import sys +import tomlkit + +config = tomlkit.parse(sys.stdin.read()) +layers = config.setdefault("extends", tomlkit.array()) +if "layers/dotfiles.toml" not in layers: + layers.append("layers/dotfiles.toml") +sys.stdout.write(tomlkit.dumps(config)) diff --git a/dot_config/token-tach/config b/dot_config/token-tach/config new file mode 100644 index 0000000..98ad141 --- /dev/null +++ b/dot_config/token-tach/config @@ -0,0 +1 @@ +claude-oauth = true diff --git a/dot_grok/config.toml b/dot_grok/config.toml deleted file mode 100644 index 9529448..0000000 --- a/dot_grok/config.toml +++ /dev/null @@ -1,181 +0,0 @@ -[cli] -installer = "internal" - -[marketplace] -default_skills_installs_purged = true -official_marketplace_auto_installed = true - -[[marketplace.sources]] -name = "xAI Official" -git = "https://github.com/xai-org/plugin-marketplace.git" - -[ui] -max_thoughts_width = 120 -fork_secondary_model = "grok-4.5" -yolo = false -compact_mode = false -permission_mode = "always-approve" -vim_mode = true - -[model_providers.chatgpt] -base_url = "https://chatgpt.com/backend-api/codex" -api_backend = "responses" -context_window = 272000 - -[model_providers.chatgpt.auth] -command = "grok-codex-token" -timeout_secs = 10 - -[model_providers.openai] -base_url = "https://api.openai.com/v1" -api_backend = "responses" -env_key = "OPENAI_API_KEY" -context_window = 400000 - -# Ox Alpha provider catalog. Keys remain machine-local environment state. -[model_providers.ox-opencode] -base_url = "https://opencode.ai/zen/v1" -api_backend = "chat_completions" -env_key = "OPENCODE_ZEN_API_KEY" -context_window = 1048576 - -[model_providers.ox-openrouter] -base_url = "https://openrouter.ai/api/v1" -api_backend = "chat_completions" -env_key = "OPENROUTER_API_KEY" -context_window = 1048576 - -[model_providers.ox-command] -base_url = "https://api.commandcode.ai/provider/v1" -api_backend = "chat_completions" -env_key = "COMMAND_CODE_API_KEY" -context_window = 1048576 - -[model_providers.ox-nous] -base_url = "http://127.0.0.1:8645/v1" -api_backend = "chat_completions" -env_key = "NOUS_API_KEY" -context_window = 1048576 - -[model_providers.ox-venice] -base_url = "https://api.venice.ai/api/v1" -api_backend = "chat_completions" -env_key = "VENICE_API_KEY" -context_window = 1048576 - -[model."chatgpt-gpt-5.6-sol"] -model = "gpt-5.6-sol" -name = "GPT-5.6 Sol (ChatGPT)" -description = "OpenAI Codex model using the existing ChatGPT subscription login" -model_provider = "chatgpt" -responses_system_role_developer = true - -[model."chatgpt-gpt-5.6-terra"] -model = "gpt-5.6-terra" -name = "GPT-5.6 Terra (ChatGPT)" -description = "Balanced agentic coding model for everyday work" -model_provider = "chatgpt" -responses_system_role_developer = true - -[model."chatgpt-gpt-5.6-luna"] -model = "gpt-5.6-luna" -name = "GPT-5.6 Luna (ChatGPT)" -description = "Fast and affordable agentic coding model" -model_provider = "chatgpt" -responses_system_role_developer = true - -[model."chatgpt-gpt-5.5"] -model = "gpt-5.5" -name = "GPT-5.5 (ChatGPT)" -description = "Frontier model for complex coding and research" -model_provider = "chatgpt" -responses_system_role_developer = true - -[model."chatgpt-gpt-5.4"] -model = "gpt-5.4" -name = "GPT-5.4 (ChatGPT)" -description = "Strong model for everyday coding" -model_provider = "chatgpt" -responses_system_role_developer = true - -[model."chatgpt-gpt-5.4-mini"] -model = "gpt-5.4-mini" -name = "GPT-5.4 Mini (ChatGPT)" -description = "Small, fast model for simpler coding tasks" -model_provider = "chatgpt" -responses_system_role_developer = true - -[model."openai-gpt-5.6-sol"] -model = "gpt-5.6-sol" -name = "GPT-5.6 Sol (OpenAI API)" -description = "OpenAI API model using OPENAI_API_KEY" -model_provider = "openai" -max_completion_tokens = 128000 - -[model."openai-gpt-5.6-terra"] -model = "gpt-5.6-terra" -name = "GPT-5.6 Terra (OpenAI API)" -description = "OpenAI API model using OPENAI_API_KEY" -model_provider = "openai" - -[model."openai-gpt-5.6-luna"] -model = "gpt-5.6-luna" -name = "GPT-5.6 Luna (OpenAI API)" -description = "OpenAI API model using OPENAI_API_KEY" -model_provider = "openai" - -[model."openai-gpt-5.5"] -model = "gpt-5.5" -name = "GPT-5.5 (OpenAI API)" -description = "OpenAI API model using OPENAI_API_KEY" -model_provider = "openai" - -[model."openai-gpt-5.4"] -model = "gpt-5.4" -name = "GPT-5.4 (OpenAI API)" -description = "OpenAI API model using OPENAI_API_KEY" -model_provider = "openai" - -[model."openai-gpt-5.4-mini"] -model = "gpt-5.4-mini" -name = "GPT-5.4 Mini (OpenAI API)" -description = "OpenAI API model using OPENAI_API_KEY" -model_provider = "openai" - -[model.ox-opencode] -model = "x-preview-f-free" -name = "Ox Alpha (OpenCode Zen; ZDR)" -description = "Limited-time free stealth preview with zero retention" -model_provider = "ox-opencode" -max_completion_tokens = 131072 - -[model.ox-openrouter] -model = "stealth/ox-alpha" -name = "Ox Alpha (OpenRouter)" -description = "Free stealth preview; upstream retains prompts and completions" -model_provider = "ox-openrouter" -max_completion_tokens = 131072 - -[model.ox-command] -model = "stealth/ox-alpha" -name = "Ox Alpha (Command Code)" -description = "Limited-time free stealth preview; not ZDR" -model_provider = "ox-command" -max_completion_tokens = 131072 - -[model.ox-nous] -model = "stealth/ox-alpha" -name = "Ox Alpha (Nous Portal)" -description = "Free-tier route; enable Nous Privacy Mode separately" -model_provider = "ox-nous" -max_completion_tokens = 131072 - -[model.ox-venice] -model = "stealth-ox-alpha" -name = "Ox Alpha (Venice)" -description = "Free beta route; anonymized third-party inference" -model_provider = "ox-venice" -max_completion_tokens = 131072 - -[models] -default = "chatgpt-gpt-5.6-sol" diff --git a/dot_grok/modify_config.toml.tmpl b/dot_grok/modify_config.toml.tmpl new file mode 100644 index 0000000..1602703 --- /dev/null +++ b/dot_grok/modify_config.toml.tmpl @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Grok owns installer metadata, marketplaces and local provider state. +set -euo pipefail +current="$(cat)" +yq eval-all -p=toml -o=toml 'select(fileIndex == 0) * select(fileIndex == 1)' \ + <(printf '%s\n' "$current") <(cat <<'MANAGED' +{{ include ".chezmoitemplates/grok.toml" }} +MANAGED +) diff --git a/dot_hermes/skills/cyclomatic-complexity/symlink_SKILL.md b/dot_hermes/skills/cyclomatic-complexity/symlink_SKILL.md new file mode 100644 index 0000000..fb9cdda --- /dev/null +++ b/dot_hermes/skills/cyclomatic-complexity/symlink_SKILL.md @@ -0,0 +1 @@ +../../../.agents/skills/cyclomatic-complexity/SKILL.md diff --git a/dot_hermes/skills/i-have-adhd/symlink_SKILL.md b/dot_hermes/skills/i-have-adhd/symlink_SKILL.md new file mode 100644 index 0000000..5c5eb3e --- /dev/null +++ b/dot_hermes/skills/i-have-adhd/symlink_SKILL.md @@ -0,0 +1 @@ +../../../.agents/skills/i-have-adhd/SKILL.md diff --git a/dot_local/bin/executable_agent-forge b/dot_local/bin/executable_agent-forge index 962307f..1f79975 100755 --- a/dot_local/bin/executable_agent-forge +++ b/dot_local/bin/executable_agent-forge @@ -9,7 +9,8 @@ set -euo pipefail DOTFILES="${DOTFILES:-$HOME/dotfiles}" -AGENT_DIR="$DOTFILES/opencode/.config/opencode" +# Enumerate applied adapters so shared skill symlinks resolve normally. +AGENT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode" case "${1:-}" in --list|-l) @@ -21,14 +22,14 @@ case "${1:-}" in done echo "" echo "=== Agents ===" - for f in "$AGENT_DIR"/agent/*.md; do + for f in "$AGENT_DIR"/agents/*.md; do [ -f "$f" ] || continue name=$(basename "$f" .md) printf " %s\n" "$name" done echo "" echo "=== Commands ===" - for f in "$AGENT_DIR"/command/*.md; do + for f in "$AGENT_DIR"/commands/*.md; do [ -f "$f" ] || continue name=$(basename "$f" .md) desc=$(sed -n 's/^description: *//p' "$f" | head -1) @@ -42,7 +43,8 @@ case "${1:-}" in exit 1 fi prompt="Load the agent-forge skill. I want to iterate on my existing agent named '${2}'. Read its current definition, then ask me what needs changing." - oc run --dir "$DOTFILES" "$prompt" + cd "$DOTFILES" + opencode2 run "$prompt" exit 0 ;; --help|-h) @@ -58,13 +60,14 @@ case "${1:-}" in ;; "") # No args — open TUI pointed at dotfiles - oc --dir "$DOTFILES" + opencode2 "$DOTFILES" exit 0 ;; *) # Positional arg(s) = the agent idea description prompt="Load the agent-forge skill. Here is my agent idea: $*" - oc run --dir "$DOTFILES" "$prompt" + cd "$DOTFILES" + opencode2 run "$prompt" exit 0 ;; esac diff --git a/dot_local/bin/executable_codex-safe b/dot_local/bin/executable_codex-safe deleted file mode 100755 index 8ce7c0b..0000000 --- a/dot_local/bin/executable_codex-safe +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -# Restore Codex's standard workspace sandbox and on-request approvals. -set -eu - -exec codex --sandbox workspace-write --ask-for-approval on-request "$@" diff --git a/dot_local/bin/executable_dot-audit b/dot_local/bin/executable_dot-audit index 305f339..f2e8be6 100755 --- a/dot_local/bin/executable_dot-audit +++ b/dot_local/bin/executable_dot-audit @@ -33,20 +33,29 @@ else printf "${DIM}none${RST}\n" fi -if command -v chezmoi >/dev/null 2>&1 && [[ -d "$HOME/.local/share/chezmoi" ]]; then +if command -v chezmoi >/dev/null 2>&1; then hdr "chezmoi state" - chezmoi status 2>/dev/null | sed 's/^/ /' || printf "${DIM}clean${RST}\n" + chezmoi status --source "$DOTFILES" | sed 's/^/ /' fi case "$(uname -s)" in Darwin) - if command -v brew >/dev/null 2>&1 && [[ -f "$DOTFILES/Brewfile" ]]; then + if command -v brew >/dev/null 2>&1 && [[ -f "$DOTFILES/provision/Brewfile" ]]; then hdr "Brewfile drift" - brew bundle check --file="$DOTFILES/Brewfile" --no-upgrade 2>&1 | sed 's/^/ /' || true + brew bundle check --file="$DOTFILES/provision/Brewfile" --no-upgrade 2>&1 | sed 's/^/ /' || true fi ;; esac +if command -v mise >/dev/null 2>&1; then + hdr "mise tool inventory" + mise -C "$DOTFILES" ls --current --missing + if [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/mise/conf.d/dotfiles-history.toml" ]]; then + hdr "live preference history and synchronization" + mise bootstrap dotfiles status + fi +fi + hdr "claude features delta" known="$HOME/.claude/state/known-features.json" if [[ -f "$known" ]]; then diff --git a/dot_local/bin/executable_dot-bench b/dot_local/bin/executable_dot-bench index 533ebb9..414f05a 100755 --- a/dot_local/bin/executable_dot-bench +++ b/dot_local/bin/executable_dot-bench @@ -6,7 +6,7 @@ # Baseline lives at $DOTFILES/PERF.md as `key: value_ms` pairs. # Regression > $REGRESSION_PCT (default 10%) exits nonzero. -set -uo pipefail +set -euo pipefail DOTFILES="${DOTFILES:-$HOME/dotfiles}" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/share}/dotfiles/bench" @@ -20,12 +20,39 @@ RED=$'\033[31m'; GRN=$'\033[32m'; YLW=$'\033[33m'; DIM=$'\033[2m'; RST=$'\033[0m # --- measurement ------------------------------------------------------------ +measurement_error() { + printf 'Benchmark could not validate performance: %s\n' "$1" >&2 + exit 2 +} + +record_result() { + [[ "$2" =~ ^(0|[1-9][0-9]*)([.][0-9]+)?$ ]] || measurement_error "invalid measurement for $1" + RESULT_NAMES+=("$1") + RESULT_VALUES+=("$2") +} + +validate_baseline() { + [[ "$2" =~ ^[[:space:]]*[0-9]+([.][0-9]+)?[[:space:]]*$ ]] || measurement_error "invalid baseline for $1" + awk -v value="$2" 'BEGIN { exit !(value > 0) }' || measurement_error "baseline must be positive for $1" +} + +require_pinned_results() { + local name baseline pinned=0 + while IFS=: read -r name baseline; do + [[ "$name" =~ ^[a-z_]+_ms$ ]] || continue + validate_baseline "$name" "$baseline" + pinned=$((pinned + 1)) + [[ " ${RESULT_NAMES[*]-} " == *" $name "* ]] || measurement_error "missing pinned metric $name" + done < "$PERF_FILE" + (( pinned > 0 )) || measurement_error "no pinned metrics in $PERF_FILE" +} + measure() { local name="$1" value="$2" printf " %-28s ${DIM}%6.1f ms${RST}" "$name" "$value" local baseline - baseline=$(grep -E "^${name}:" "$PERF_FILE" 2>/dev/null | awk '{print $2}') + baseline=$(awk -F: -v name="$name" '$1 == name {gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$PERF_FILE") if [[ -z "$baseline" ]]; then printf " ${DIM}(no baseline)${RST}\n" return 0 @@ -46,7 +73,10 @@ measure() { } REGRESSED=0 -declare -A RESULTS +# Indexed arrays work with stock macOS Bash 3.2 as well as newer Bash. +RESULT_NAMES=() +RESULT_VALUES=() +[[ -r "$PERF_FILE" ]] || measurement_error "cannot read $PERF_FILE" if command -v zsh-bench >/dev/null 2>&1; then printf "${DIM}# Using romkatv/zsh-bench (%d iterations)${RST}\n" "$ITERATIONS" @@ -54,9 +84,9 @@ if command -v zsh-bench >/dev/null 2>&1; then # zsh-bench prints lines like "first_prompt_lag_ms=42" while IFS='=' read -r k v; do - [[ "$k" =~ _ms$ ]] || continue - RESULTS[$k]=$v - done <<< "$(echo "$out" | grep -E '^[a-z_]+=')" + [[ "$k" =~ ^[a-z_]+_ms$ ]] || continue + record_result "$k" "$v" + done <<< "$out" else printf "${YLW}# zsh-bench not installed — using fallback (less accurate)${RST}\n" printf "${DIM}# install: git clone https://github.com/romkatv/zsh-bench ~/.zsh-bench && ln -s ~/.zsh-bench/zsh-bench ~/.local/bin/zsh-bench${RST}\n" @@ -64,19 +94,21 @@ else total=0 for i in $(seq 1 "$ITERATIONS"); do start=$(date +%s%N) + [[ "$start" =~ ^[0-9]+$ ]] || measurement_error 'fallback clock lacks nanoseconds; install zsh-bench' zsh -i -c exit 2>/dev/null end=$(date +%s%N) elapsed_ms=$(( (end - start) / 1000000 )) total=$((total + elapsed_ms)) done avg=$((total / ITERATIONS)) - RESULTS[interactive_startup_ms]=$avg + record_result interactive_startup_ms "$avg" fi +require_pinned_results echo echo "Results:" -for k in "${!RESULTS[@]}"; do - measure "$k" "${RESULTS[$k]}" +for i in "${!RESULT_NAMES[@]}"; do + measure "${RESULT_NAMES[$i]}" "${RESULT_VALUES[$i]}" done # --- persist ---------------------------------------------------------------- @@ -93,9 +125,9 @@ out_file="$STATE_DIR/$ts.json" echo " \"git_sha\": \"$(cd "$DOTFILES" && git rev-parse --short HEAD 2>/dev/null || echo unknown)\"," echo " \"results\": {" first=1 - for k in "${!RESULTS[@]}"; do + for i in "${!RESULT_NAMES[@]}"; do [[ $first -eq 1 ]] && first=0 || echo "," - printf " \"%s\": %s" "$k" "${RESULTS[$k]}" + printf " \"%s\": %s" "${RESULT_NAMES[$i]}" "${RESULT_VALUES[$i]}" done echo echo " }" diff --git a/dot_local/bin/executable_dot-zcompile b/dot_local/bin/executable_dot-zcompile index d665878..35152fc 100755 --- a/dot_local/bin/executable_dot-zcompile +++ b/dot_local/bin/executable_dot-zcompile @@ -14,7 +14,11 @@ DOTFILES="${DOTFILES:-$HOME/dotfiles}" # the source-of-truth, not next to the symlink. _compile() { local target="$1" - [[ -f "$target" ]] || return 0 + if [[ ! -f "$target" ]]; then + # Zsh can execute a .zwc even after its source was deleted by history replay. + rm -f -- "$target.zwc" + return 0 + fi local real="${target:A}" # zsh-only: :A resolves symlinks local zwc="${real}.zwc" if [[ ! -f "$zwc" || "$real" -nt "$zwc" ]]; then @@ -33,4 +37,7 @@ _compile "$HOME/.p10k.zsh" for f in "$HOME"/.zsh/*.zsh(N); do _compile "$f" done +for f in "$HOME"/.zsh/*.zsh.zwc(N); do + [[ -f "${f%.zwc}" ]] || rm -f -- "$f" +done echo "done" diff --git a/dot_local/bin/executable_grok-codex-token b/dot_local/bin/executable_grok-codex-token deleted file mode 100644 index c56bc12..0000000 --- a/dot_local/bin/executable_grok-codex-token +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -set -eu - -auth_file=${CODEX_HOME:-"$HOME/.codex"}/auth.json - -if [ ! -r "$auth_file" ]; then - printf 'Codex credentials not found; run codex login first\n' >&2 - exit 1 -fi - -token=$(jq -r '.tokens.access_token // empty' "$auth_file") -if [ -z "$token" ]; then - printf 'Codex access token not found; run codex login again\n' >&2 - exit 1 -fi - -printf '%s' "$token" diff --git a/dot_local/bin/executable_oc b/dot_local/bin/executable_oc deleted file mode 100755 index b5d90a9..0000000 --- a/dot_local/bin/executable_oc +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -# opencode-ohmy — OpenCode with oh-my-openagent plugin -# -# Runs opencode with a separate config dir (~/.config/oc/opencode/) that loads -# the oh-my-openagent plugin and your custom skills/agents. -# Skills, agents, commands, and plugins are symlinked back to the main config -# so you maintain one canonical set of artifacts. -# -# Usage: -# opencode-ohmy # TUI mode -# opencode-ohmy run "do something" # headless -# opencode-ohmy run --agent steve-jobs # with specific agent -# -# vs: -# opencode # vanilla mode (default) - -exec env XDG_CONFIG_HOME="$HOME/.config/oc" "$HOME/.local/bin/opencode" "$@" diff --git a/dot_local/bin/executable_opencode b/dot_local/bin/executable_opencode deleted file mode 100755 index dbb90da..0000000 --- a/dot_local/bin/executable_opencode +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -eu - -shim=${XDG_DATA_HOME:-"$HOME/.local/share"}/opencode-worktree/bin/opencode -if [ ! -x "$shim" ]; then - printf 'opencode-worktree is not installed; run chezmoi apply\n' >&2 - exit 1 -fi -exec "$shim" "$@" diff --git a/dot_local/bin/executable_opencode-ohmy b/dot_local/bin/executable_opencode-ohmy deleted file mode 100755 index b5d90a9..0000000 --- a/dot_local/bin/executable_opencode-ohmy +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -# opencode-ohmy — OpenCode with oh-my-openagent plugin -# -# Runs opencode with a separate config dir (~/.config/oc/opencode/) that loads -# the oh-my-openagent plugin and your custom skills/agents. -# Skills, agents, commands, and plugins are symlinked back to the main config -# so you maintain one canonical set of artifacts. -# -# Usage: -# opencode-ohmy # TUI mode -# opencode-ohmy run "do something" # headless -# opencode-ohmy run --agent steve-jobs # with specific agent -# -# vs: -# opencode # vanilla mode (default) - -exec env XDG_CONFIG_HOME="$HOME/.config/oc" "$HOME/.local/bin/opencode" "$@" diff --git a/dot_local/bin/executable_opencode-safe b/dot_local/bin/executable_opencode-safe deleted file mode 100755 index 57d9d34..0000000 --- a/dot_local/bin/executable_opencode-safe +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -# Override the global allow policy with prompts for this OpenCode process. -set -eu - -export OPENCODE_CONFIG_CONTENT='{"permission":"ask"}' -exec opencode "$@" diff --git a/dot_local/bin/executable_ox b/dot_local/bin/executable_ox index 5648a79..aa7a0d9 100755 --- a/dot_local/bin/executable_ox +++ b/dot_local/bin/executable_ox @@ -17,7 +17,7 @@ Usage: ox each [--dir DIR] [--prompt-file FILE] [PROMPT...] Providers: auto, opencode, openrouter, command, nous, venice -Harnesses: pi (default), opencode, hermes, goose, grok, codex +Harnesses: pi (default), opencode, hermes, goose, grok `run` is a normal mutation-capable agent run. `ask` is one read-only Pi run. `each` fans the prompt out read-only to every authenticated provider and saves @@ -193,7 +193,6 @@ supports_route() { local harness=$1 provider=$2 case "$harness" in claude) return 1 ;; - codex) [[ "$provider" == openrouter || "$provider" == venice ]] ;; pi|opencode|hermes|goose|grok) return 0 ;; *) return 1 ;; esac @@ -264,50 +263,66 @@ inject_credential() { export "$variable=$key" } -run_harness() { - local harness=$1 provider=$2 prompt=$3 model pi_provider oc_model hermes_provider goose_provider - model=$(model_id "$provider") +prepare_harness_route() { + local provider=$1 harness=$2 if [[ "$provider" == nous && "$harness" != hermes ]]; then ensure_nous_proxy fi if [[ "$harness" != hermes || "$provider" != nous ]]; then inject_credential "$provider" fi +} + +run_pi_harness() { + local provider=$1 model=$2 prompt=$3 + local pi_provider=ox-$provider + if command -v pi-commander >/dev/null 2>&1; then + pi-commander -p --provider "$pi_provider" --model "$model" --thinking high "$prompt" + else + pi -p --provider "$pi_provider" --model "$model" --thinking high "$prompt" + fi +} + +run_opencode_harness() { + local provider=$1 model=$2 prompt=$3 oc_model + case "$provider" in + opencode) oc_model=opencode/$model ;; + openrouter) oc_model=openrouter/$model ;; + *) oc_model=ox-$provider/$model ;; + esac + opencode2 run --model "$oc_model" "$prompt" +} + +run_hermes_harness() { + local provider=$1 model=$2 prompt=$3 hermes_provider + case "$provider" in + opencode) hermes_provider=opencode-zen ;; + openrouter|nous) hermes_provider=$provider ;; + *) hermes_provider=custom:ox-$provider ;; + esac + hermes --provider "$hermes_provider" --model "$model" --oneshot "$prompt" +} + +run_goose_harness() { + local provider=$1 model=$2 prompt=$3 + goose run --no-session --provider "ox_$provider" --model "$model" --text "$prompt" +} + +run_grok_harness() { + local provider=$1 prompt=$3 + grok --single "$prompt" --model "ox-$provider" +} + +run_harness() { + local harness=$1 provider=$2 prompt=$3 model + model=$(model_id "$provider") + prepare_harness_route "$provider" "$harness" case "$harness" in - pi) - pi_provider=ox-$provider - if command -v pi-commander >/dev/null 2>&1; then - pi-commander -p --provider "$pi_provider" --model "$model" --thinking high "$prompt" - else - pi -p --provider "$pi_provider" --model "$model" --thinking high "$prompt" - fi - ;; - opencode) - case "$provider" in - opencode) oc_model=opencode/$model ;; - openrouter) oc_model=openrouter/$model ;; - *) oc_model=ox-$provider/$model ;; - esac - opencode run --model "$oc_model" "$prompt" - ;; - hermes) - case "$provider" in - opencode) hermes_provider=opencode-zen ;; - openrouter|nous) hermes_provider=$provider ;; - *) hermes_provider=custom:ox-$provider ;; - esac - hermes --provider "$hermes_provider" --model "$model" --oneshot "$prompt" - ;; - goose) - goose_provider=ox_$provider - goose run --no-session --provider "$goose_provider" --model "$model" --text "$prompt" - ;; - grok) - grok --single "$prompt" --model "ox-$provider" - ;; - codex) - codex exec --profile "ox-$provider" "$prompt" - ;; + pi) run_pi_harness "$provider" "$model" "$prompt" ;; + opencode) run_opencode_harness "$provider" "$model" "$prompt" ;; + hermes) run_hermes_harness "$provider" "$model" "$prompt" ;; + goose) run_goose_harness "$provider" "$model" "$prompt" ;; + grok) run_grok_harness "$provider" "$model" "$prompt" ;; claude) printf '%s\n' 'ox: Claude Code requires Anthropic Messages and does not support Ox directly.' >&2 return 2 @@ -360,11 +375,11 @@ Authenticate only the routes you want; keys remain local: nous `hermes portal` NOUS_API_KEY or Hermes OAuth venice https://venice.ai/settings/api VENICE_API_KEY -For OpenCode-backed reuse, run `opencode providers login`, select the matching +For OpenCode-backed reuse, run `opencode2 auth login`, select the matching configured provider, and paste the key. `ox` can reuse those local API entries. EOF ;; - opencode) printf '%s\n' 'Create/copy a key at https://opencode.ai/auth, then run `opencode providers login` and select OpenCode Zen.' ;; + opencode) printf '%s\n' 'Create/copy a key at https://opencode.ai/auth, then run `opencode2 auth login` and select OpenCode Zen.' ;; openrouter) printf '%s\n' 'Create a key at https://openrouter.ai/keys, then export OPENROUTER_API_KEY or log in through OpenCode.' ;; command) printf '%s\n' 'Create a key at https://commandcode.ai/studio/api-keys, then export COMMAND_CODE_API_KEY or log in to ox-command through OpenCode.' ;; nous) printf '%s\n' 'Run `hermes portal`; ox reuses Hermes OAuth through a loopback-only proxy. Enable Portal Privacy Mode separately.' ;; diff --git a/dot_p10k.zsh b/dot_p10k.zsh index 3cd907d..b9bfdd9 100644 --- a/dot_p10k.zsh +++ b/dot_p10k.zsh @@ -1638,7 +1638,7 @@ function prompt_mise() { local dir=$PWD while [[ $dir != / ]]; do - [[ -f $dir/.mise.toml ]] && { p10k segment -f 208 -t 'mise'; return } + [[ -f $dir/.mise.toml || -f $dir/mise.toml || -f $dir/.mise/config.toml ]] && { p10k segment -f 208 -t 'mise'; return } dir=${dir:h} done } diff --git a/dot_pi/private_agent/APPEND_SYSTEM.md b/dot_pi/private_agent/APPEND_SYSTEM.md index 31e8588..4ea38a0 100644 --- a/dot_pi/private_agent/APPEND_SYSTEM.md +++ b/dot_pi/private_agent/APPEND_SYSTEM.md @@ -1,11 +1,11 @@ - +- Do not call user-question, interview, or questionnaire tools. Do not stop for routine clarification or implementation preferences. Inspect the repository, infer intent from existing conventions and evidence, choose the strongest reversible option, record material assumptions, and continue. +- Request user input only when a fixed authority boundary requires explicit approval, an unavailable credential or external action makes progress impossible, or competing product outcomes are both materially consequential and cannot be resolved from evidence. Exhaust safe local alternatives first and report the exact minimal unblock. +- Own tasks end to end: establish acceptance evidence, inspect the load-bearing code yourself, implement, test through the real user path when practical, review the final diff, fix issues worth fixing now, clean up local scratch state, and finish with verified outcomes rather than suggestions for obvious next steps. +- For complex work, act as a parent-led engineering team. Decompose into distinct lanes; parallelize reconnaissance, research, review, and validation; keep one writer per checkout unless writers have isolated worktrees and non-overlapping seams; synthesize child evidence yourself; run an independent fresh-context review; then send accepted findings through one fix writer and revalidate. +- Delegate recursively only when another level creates a genuinely independent decision or shortens the critical path. Depth and concurrency are circuit breakers, not targets. Never create a swarm for ceremony, never allow competing writers in one checkout, and never treat a child report as acceptance. +- Use durable missions and goal-backed continuation for substantial multi-turn work. Keep the next ready action explicit, preserve progress across compaction or restart, and continue until the acceptance contract is satisfied or a true external blocker is proven. When `/goal` is active, call `goal_complete` only with requirement-by-requirement verification evidence. +- Preserve the repository authority boundary: autonomous local work and work on `github.com/phall1/*` are allowed; upstream or third-party publication still requires explicit per-instance permission. diff --git a/dot_pi/private_agent/extensions/README.md b/dot_pi/private_agent/extensions/README.md index bc57585..40f7f08 100644 --- a/dot_pi/private_agent/extensions/README.md +++ b/dot_pi/private_agent/extensions/README.md @@ -3,7 +3,9 @@ Tracked extensions are portable code loaded by Pi with user permissions. Reviewed npm packages are pinned by `modify_settings.json`. -`role-profile.ts` is the capability-policy adapter: +`role-profile.ts` is the capability-policy adapter. It removes known +user-question/interview tools from every role and blocks stale calls while +retaining child-to-parent supervisor coordination: - Commander is the broad productive default. - Spartan registers pi-subagents' public session-scoped capability ceiling so local code/command read-only restrictions propagate monotonically to nested @@ -12,6 +14,11 @@ Reviewed npm packages are pinned by `modify_settings.json`. - YOLO widens delegation depth through its launcher but retains the shared upstream/third-party autonomy boundary. +Goal completion tools remain available under Spartan because they terminate or +pause autonomous work without granting local mutation authority. Long-horizon +policy lives in the separately tracked `pi-goal.json`; the main-session +watchdog is a bounded second pass, not a replacement for fresh reviewers. + Spartan deliberately keeps trusted provider extensions loaded for web research, nested delegation, and explicit Blackbird tools; the allowed-tool intersection prevents descendants from invoking mutation tools but does not suppress trusted diff --git a/dot_pi/private_agent/extensions/role-profile.ts b/dot_pi/private_agent/extensions/role-profile.ts index 972af1c..54273c1 100644 --- a/dot_pi/private_agent/extensions/role-profile.ts +++ b/dot_pi/private_agent/extensions/role-profile.ts @@ -15,10 +15,15 @@ const BLACKBIRD_TOOLS = [ "blackbird_reservation_release", ]; +const USER_ASK_TOOLS = new Set([ + "ask_user_question", "ask_question", "interview", "questionnaire", "phux_ask", +]); + const SPARTAN_TOOLS = [ "read", "grep", "find", "ls", "subagent", "subagent_wait", - "contact_supervisor", "subagent_supervisor", "intercom", "ask_user_question", + "contact_supervisor", "subagent_supervisor", "intercom", "web_search", "source_check", "fetch_content", "get_search_content", + "goal_complete", "goal_blocked", "goal_wait", ...BLACKBIRD_TOOLS, ]; @@ -27,17 +32,22 @@ export default function roleProfile(pi: ExtensionAPI) { const rawRole = (process.env.PI_AGENT_ROLE || "commander").toLowerCase(); const role = ["commander", "spartan", "yolo"].includes(rawRole) ? rawRole : "commander"; - const restrictActiveTools = () => { - if (role !== "spartan") return; + const applyToolPolicy = () => { const available = new Set(pi.getAllTools().map((tool) => tool.name)); - pi.setActiveTools(SPARTAN_TOOLS.filter((name) => available.has(name))); + const desired = role === "spartan" + ? SPARTAN_TOOLS.filter((name) => available.has(name)) + : pi.getActiveTools().filter((name) => !USER_ASK_TOOLS.has(name)); + const active = pi.getActiveTools(); + if (active.length !== desired.length || active.some((name, index) => name !== desired[index])) { + pi.setActiveTools(desired); + } }; const apply = (ctx: ExtensionContext) => { restriction?.dispose(); restriction = undefined; + applyToolPolicy(); if (role === "spartan") { - restrictActiveTools(); restriction = registerSubagentCapabilityCeiling({ sessionId: ctx.sessionManager.getSessionId(), source: "role-profile:spartan", @@ -48,6 +58,9 @@ export default function roleProfile(pi: ExtensionAPI) { }; pi.on("tool_call", async (event) => { + if (USER_ASK_TOOLS.has(event.toolName)) { + return { block: true, reason: `Autonomous role policy denies user-question tool: ${event.toolName}` }; + } if (role === "spartan" && !SPARTAN_TOOLS.includes(event.toolName)) { return { block: true, reason: `Spartan capability ceiling denies tool: ${event.toolName}` }; } @@ -56,8 +69,8 @@ export default function roleProfile(pi: ExtensionAPI) { pi.on("session_start", (_event, ctx) => apply(ctx)); // Other extensions can register or reactivate tools during session startup. - // Reassert the visible Spartan surface immediately before every agent turn. - pi.on("before_agent_start", () => restrictActiveTools()); + // Reassert the askless surface and any Spartan ceiling before every agent turn. + pi.on("before_agent_start", () => applyToolPolicy()); pi.on("session_shutdown", (_event, ctx) => { restriction?.dispose(); restriction = undefined; diff --git a/dot_pi/private_agent/modify_settings.json b/dot_pi/private_agent/modify_settings.json index 078093b..56c84fd 100755 --- a/dot_pi/private_agent/modify_settings.json +++ b/dot_pi/private_agent/modify_settings.json @@ -9,15 +9,20 @@ set -eu current="$(cat)" [ -n "$current" ] || current='{}' -printf '%s' "$current" | jq -e 'type == "object"' >/dev/null 2>&1 || current='{}' +if ! printf '%s' "$current" | jq -e 'type == "object"' >/dev/null 2>&1; then + printf '%s\n' "$current" + exit 0 +fi managed_packages='[ + "npm:blackbird-pi@0.1.1", "npm:pi-subagents@0.47.1", - "npm:@juicesharp/rpiv-ask-user-question@2.4.0", "npm:@narumitw/pi-goal@0.51.0", "npm:@ff-labs/pi-fff@0.10.3", "npm:pi-mcp-adapter@2.23.0", - "npm:pi-web-access@0.22.0" + "npm:pi-web-access@0.22.0", + "npm:@osolmaz/pi-workflows@0.13.4", + "https://github.com/ayghri/i-have-adhd" ]' # This developer checkout is deliberately a machine-local overlay, not a @@ -26,6 +31,13 @@ phux='../../workspace/phux/integrations/pi' add_phux=false [ -d "$HOME/workspace/phux/integrations/pi" ] && add_phux=true +# Packages in this list are actively removed instead of being preserved as +# runtime-owned additions. This keeps retired model-facing interaction tools +# from silently returning after a later chezmoi apply. +retired_packages='[ + "@juicesharp/rpiv-ask-user-question" +]' + managed='{ "theme": "blackwater-rust", "defaultProjectTrust": "always", @@ -38,6 +50,13 @@ managed='{ "reviewer": {"tools": ["read", "grep", "find", "ls", "intercom", "subagent"]}, "oracle": {"tools": ["read", "grep", "find", "ls", "bash", "intercom", "subagent"]}, "delegate": {"tools": ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor", "subagent"]} + }, + "watchdog": { + "enabled": true, + "main": {"enabled": true}, + "children": {"enabled": false}, + "autoFollow": {"blockers": true, "maxAttempts": 2, "stalemateRepeats": 2}, + "cadence": {"everyNTools": null} } } }' @@ -45,6 +64,7 @@ managed='{ result=$(printf '%s' "$current" | jq \ --argjson managed "$managed" \ --argjson pins "$managed_packages" \ + --argjson retired "$retired_packages" \ --arg phux "$phux" \ --argjson addPhux "$add_phux" ' def managed_name: @@ -57,7 +77,13 @@ result=$(printf '%s' "$current" | jq \ end end; ($pins | map(managed_name)) as $names - | [(.packages // [])[] | select((managed_name as $n | ($n == null or ($names | index($n) | not))))] as $unmanaged + | [ + (.packages // [])[] + | select( + managed_name as $n + | ($n == null or (($names | index($n) | not) and ($retired | index($n) | not))) + ) + ] as $unmanaged | ($unmanaged + $pins) as $packages | ($packages | if ($addPhux and (index($phux) | not)) then . + [$phux] else . end) as $packages | . * $managed diff --git a/dot_pi/private_agent/pi-goal.json b/dot_pi/private_agent/pi-goal.json new file mode 100644 index 0000000..d8ca6f1 --- /dev/null +++ b/dot_pi/private_agent/pi-goal.json @@ -0,0 +1,13 @@ +{ + "toolVisibility": "always", + "experimental": { + "goals": true + }, + "rpc": { + "enabled": false + }, + "continuationLimits": { + "automaticTurns": 100, + "noProgressTurns": 3 + } +} diff --git a/dot_pi/private_agent/settings.json.reference b/dot_pi/private_agent/settings.json.reference index b8d31b5..43f18a9 100644 --- a/dot_pi/private_agent/settings.json.reference +++ b/dot_pi/private_agent/settings.json.reference @@ -6,13 +6,21 @@ Chezmoi manages: - `theme: blackwater-rust` - `defaultProjectTrust: always` - install telemetry disabled -- reviewed package pins -- model-neutral builtin subagent tool overrides +- reviewed package pins, including the globally enabled Pi Workflows runtime and skills +- explicit retirement of removed interaction packages +- model-neutral builtin subagent tool overrides and watchdog behavior -Pi/runtime owns everything else, including provider, model, thinking level, +Pi/runtime owns everything else, including provider, model, thinking level +(including the locally recommended complementary watchdog model), changelog state, authentication, trust records, sessions, caches, and unknown future keys. The merge preserves those keys. -Package pins are declared in `modify_settings.json`. The optional local Phux +Package pins are declared in `modify_settings.json`. Retired package names are +also declared there so a package removed from the managed pins is not mistaken +for a runtime-owned addition and preserved forever. The optional local Phux package is retained and added only when `$HOME/workspace/phux/integrations/pi` exists; it is not a portable dependency. + +Long-horizon continuation settings are tracked separately in `pi-goal.json`. +The role-profile extension keeps user-question tools inactive even if another +package tries to register them. diff --git a/dot_zsh/aliases.zsh b/dot_zsh/aliases.zsh index a411d71..2f936ba 100644 --- a/dot_zsh/aliases.zsh +++ b/dot_zsh/aliases.zsh @@ -23,11 +23,6 @@ if [[ "$DOTFILES_OS" == "darwin" ]]; then # Quick look from terminal ql() { qlmanage -p "$@" &>/dev/null; } - # OpenCode - run built binary from dev branch (macOS ARM) - alias ocsrc='~/workspace/opencode/packages/opencode/dist/opencode-darwin-arm64/bin/opencode' - alias opencode-dev='bun run --cwd ~/workspace/opencode/packages/opencode --conditions=browser src/index.ts' - alias cdoc='cd ~/workspace/opencode' - elif [[ "$DOTFILES_OS" == "linux" ]]; then # Linux-specific alias ls='ls --color=auto' diff --git a/dot_zsh/functions.zsh b/dot_zsh/functions.zsh index 2c8b1d8..64aa971 100644 --- a/dot_zsh/functions.zsh +++ b/dot_zsh/functions.zsh @@ -292,36 +292,6 @@ header() { printf "========================================\033[0m\n\n" } -# Sync opencode dev branch with upstream -oc-sync() { - local oc=~/workspace/opencode - - # Check for uncommitted changes - if ! git -C "$oc" diff --quiet || ! git -C "$oc" diff --cached --quiet; then - echo "You have uncommitted changes. Stash or commit first." - return 1 - fi - - # Check if on dev branch - local branch=$(git -C "$oc" branch --show-current) - if [[ "$branch" != "dev" ]]; then - echo "Not on dev branch (on '$branch'). Switch to dev first, or:" - echo " git -C $oc checkout dev" - return 1 - fi - - git -C "$oc" fetch upstream && \ - git -C "$oc" rebase upstream/dev -} - -# Build opencode from source (single platform) -oc-build() { - echo "Building opencode..." - bun run --cwd ~/workspace/opencode/packages/opencode build --single && \ - echo "Done. Run 'opencode --version' to verify." -} - - # ============================================================================= # Git Stack Management # diff --git a/dot_zshenv b/dot_zshenv index 6ad6dfe..a6db67b 100644 --- a/dot_zshenv +++ b/dot_zshenv @@ -2,7 +2,7 @@ # Keep this MINIMAL. Doctor enforces <30 lines. Anything heavier belongs # in .zprofile (login) or .zshrc (interactive). -# XDG base dirs (used by atuin, chezmoi, mise, fnm, …). +# XDG base dirs (used by atuin, chezmoi, mise, …). : "${XDG_CONFIG_HOME:=$HOME/.config}" : "${XDG_DATA_HOME:=$HOME/.local/share}" : "${XDG_CACHE_HOME:=$HOME/.cache}" @@ -17,7 +17,8 @@ export VISUAL="${VISUAL:-nvim}" # PATH essentials — only the bare minimum that non-interactive shells # might need. The big PATH composition (cargo, go, bun, npm-global, etc.) # happens in .zshrc — those are interactive concerns. -export PATH="$HOME/.local/bin:$PATH" +typeset -U path +path=("$HOME/.local/bin" "$XDG_DATA_HOME/mise/shims" "$HOME/.cargo/bin" $path) # Profiling gate — `ZSH_PROF=1 zsh -i -c exit` to capture zprof output. [[ -n "${ZSH_PROF:-}" ]] && zmodload zsh/zprof diff --git a/dot_zshrc b/dot_zshrc index 574e077..96066b6 100644 --- a/dot_zshrc +++ b/dot_zshrc @@ -41,13 +41,12 @@ export DOTFILES_OS="${DOTFILES_OS:-$(uname -s | tr '[:upper:]' '[:lower:]')}" [[ -d "$HOME/.npm-global/bin" ]] && export PATH="$HOME/.npm-global/bin:$PATH" [[ -d "$HOME/.kimi-code/bin" ]] && export PATH="$HOME/.kimi-code/bin:$PATH" [[ -d "$HOME/.grok/bin" ]] && export PATH="$HOME/.grok/bin:$PATH" -# Keep managed launchers ahead of the vendor bins they wrap (notably -# ~/.local/bin/opencode -> ~/.opencode/bin/opencode). -[[ -d "$HOME/.local/bin" ]] && export PATH="$HOME/.local/bin:$PATH" +# Restore managed tool precedence after platform and legacy installer additions. +path=("$HOME/.local/bin" "${MISE_DATA_DIR:-$XDG_DATA_HOME/mise}/shims" $path) # ============================================================================ # Plugins — loaded from $XDG_DATA_HOME/zsh/plugins (see zsh/plugins.lock). -# Plugins live OUTSIDE the dotfiles repo so the stow tree stays clean. +# Plugins live OUTSIDE the dotfiles source tree. # Bootstrap: `dot-install-zsh-plugins`. # ============================================================================ @@ -79,10 +78,8 @@ export DOTFILES_OS="${DOTFILES_OS:-$(uname -s | tr '[:upper:]' '[:lower:]')}" # The emitted snippet only changes when the tool binary does, so cache it and # invalidate when the binary is newer than the cache — no version fork needed. # -# NOT every tool init is cacheable. `fnm env` bakes a per-shell multishell path -# (~/.local/state/fnm_multishells/_) into its output, so a cached copy -# would point every future shell at one dead directory. Verified by diffing the -# output across runs before caching anything; fnm is deliberately left forking. +# Mise activation is environment-dependent and must not use this cache. +# Bootstrap invalidates these snippets when their resolved tool paths change. _evalcache() { local name=$1; shift local bin=${commands[$1]} @@ -106,12 +103,6 @@ _evalcache() { # Zoxide (smarter cd) command -v zoxide &>/dev/null && _evalcache zoxide zoxide init zsh -# fnm (Node version manager — replaces nvm). --use-on-cd reads .nvmrc/.node-version. -# --log-level error: the on-cd hook auto-runs `fnm use` in the shell's starting -# dir, and at info level it prints "Using Node vX", which p10k flags as console -# output during instant-prompt init. error level keeps real failures visible. -command -v fnm &>/dev/null && eval "$(fnm env --use-on-cd --log-level error --shell zsh)" - # direnv — LAZY. The normal `direnv hook zsh` forks `direnv export` on *every* # prompt, even in dirs with no .envrc — a per-command tax you pay everywhere. # Instead we install the real hook only the first time you cd into a tree that @@ -163,7 +154,6 @@ if command -v mise &>/dev/null; then add-zsh-hook -d chpwd _mise_lazy_load unfunction _mise_lazy_load eval "$(mise activate zsh)" - _mise_hook # apply the current dir's tool versions now return fi [[ "$d" == "/" ]] && break @@ -341,6 +331,3 @@ setopt HIST_REDUCE_BLANKS # tidy whitespace before saving # Google Cloud SDK [ -f "$HOME/Downloads/google-cloud-sdk/path.zsh.inc" ] && source "$HOME/Downloads/google-cloud-sdk/path.zsh.inc" [ -f "$HOME/Downloads/google-cloud-sdk/completion.zsh.inc" ] && source "$HOME/Downloads/google-cloud-sdk/completion.zsh.inc" - -# kimi-code -export PATH="/Users/phall/.kimi-code/bin:$PATH" diff --git a/mise.linux.lock b/mise.linux.lock new file mode 100644 index 0000000..f028518 --- /dev/null +++ b/mise.linux.lock @@ -0,0 +1,106 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 1 + +[[tools."aqua:eza-community/eza"]] +version = "0.23.5" +backend = "aqua:eza-community/eza" +specifiers = ["0.23.5"] + +[tools."aqua:eza-community/eza"."platforms.linux-arm64"] +checksum = "sha256:40b87ae8628aa2ff0f0d2dc24ab52f689631366385c3da630bae745671fd71ec" +url = "https://github.com/eza-community/eza/releases/download/v0.23.5/eza_aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/eza-community/eza/releases/assets/471228867" + +[tools."aqua:eza-community/eza"."platforms.linux-x64"] +checksum = "sha256:35c70c5c43c29108075e58b893234c67ef585f0b53a7eaf8e9e7d4eec9f339b4" +url = "https://github.com/eza-community/eza/releases/download/v0.23.5/eza_x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/eza-community/eza/releases/assets/471228887" + +[[tools."aqua:neovim/neovim"]] +version = "0.12.5" +backend = "aqua:neovim/neovim" +specifiers = ["0.12.5"] + +[tools."aqua:neovim/neovim"."platforms.linux-arm64"] +checksum = "sha256:1aa5ca085249580ae0f91eb14f27ec0919773ff2d99a163d03f3d6c21ac29725" +url = "https://github.com/neovim/neovim/releases/download/v0.12.5/nvim-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/neovim/neovim/releases/assets/526503739" + +[tools."aqua:neovim/neovim"."platforms.linux-x64"] +checksum = "sha256:bce0f56eda1f1b1db6eee8f4133d7a38813ea07933837dd1777411ca384c6875" +url = "https://github.com/neovim/neovim/releases/download/v0.12.5/nvim-linux-x86_64.tar.gz" +url_api = "https://api.github.com/repos/neovim/neovim/releases/assets/526503737" + +[[tools."github:no-phux/phux"]] +version = "0.26.1" +backend = "github:no-phux/phux" +specifiers = ["0.26.1"] + +[tools."github:no-phux/phux".options] +asset_pattern = "phux-v*-aarch64-unknown-linux-gnu.tar.gz" + +[tools."github:no-phux/phux"."platforms.linux-arm64"] +checksum = "sha256:1ea5b9e081b27eb24c9f99171f148c670cbbab8b7abd5b1ca0aab4a3ec92f2f6" +url = "https://github.com/no-phux/phux/releases/download/v0.26.1/phux-v0.26.1-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/no-phux/phux/releases/assets/543990471" + +[[tools."github:no-phux/phux"]] +version = "0.26.1" +backend = "github:no-phux/phux" +specifiers = ["0.26.1"] + +[tools."github:no-phux/phux".options] +asset_pattern = "phux-v*-x86_64-unknown-linux-gnu.tar.gz" + +[tools."github:no-phux/phux"."platforms.linux-x64"] +checksum = "sha256:9d3558f404bd144454136659c71613826e54cd9b2c8ab6fd3c33f56e8b7a5c84" +url = "https://github.com/no-phux/phux/releases/download/v0.26.1/phux-v0.26.1-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/no-phux/phux/releases/assets/543990464" + +[[tools."github:phall1/blackbird"]] +version = "0.5.0" +backend = "github:phall1/blackbird" +specifiers = ["0.5.0"] + +[tools."github:phall1/blackbird"."platforms.linux-arm64"] +checksum = "sha256:34e656d626db2e491f418d6ac58ca5ec6e202651187ac79c765448931af333e6" +url = "https://github.com/phall1/blackbird/releases/download/v0.5.0/blackbird-v0.5.0-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/phall1/blackbird/releases/assets/541656393" + +[tools."github:phall1/blackbird"."platforms.linux-x64"] +checksum = "sha256:6b6ebe20ea24269738c82c6685eb5d07a490afc02195463758ca2734199076fb" +url = "https://github.com/phall1/blackbird/releases/download/v0.5.0/blackbird-v0.5.0-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/phall1/blackbird/releases/assets/541656395" + +[[tools."github:phall1/phig"]] +version = "1.1.1" +backend = "github:phall1/phig" +specifiers = ["1.1.1"] + +[tools."github:phall1/phig"."platforms.linux-arm64"] +checksum = "sha256:a25c9266ba07ab981a5f0ce669686eed60326e314cc5e58de1f5d2b6f5758347" +url = "https://github.com/phall1/phig/releases/download/v1.1.1/phig-cli-aarch64-unknown-linux-gnu.tar.xz" +url_api = "https://api.github.com/repos/phall1/phig/releases/assets/521507122" +provenance = "github-attestations" + +[tools."github:phall1/phig"."platforms.linux-x64"] +checksum = "sha256:c2cf4f2a7d96f922cc75052817165a48d4db60ea8d73440c63ea424dd9446af7" +url = "https://github.com/phall1/phig/releases/download/v1.1.1/phig-cli-x86_64-unknown-linux-gnu.tar.xz" +url_api = "https://api.github.com/repos/phall1/phig/releases/assets/521507136" +provenance = "github-attestations" + +[[tools."github:phall1/phui"]] +version = "0.15.0" +backend = "github:phall1/phui" +specifiers = ["0.15.0"] + +[tools."github:phall1/phui"."platforms.linux-arm64"] +checksum = "sha256:3f11abeb41fc2941abaa521521d8ca07c8981f0dc5fd41c29ea5747d003a31e7" +url = "https://github.com/phall1/phui/releases/download/v0.15.0/phui-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/phall1/phui/releases/assets/507794667" + +[tools."github:phall1/phui"."platforms.linux-x64"] +checksum = "sha256:124908e1436fda82ae36dd7658a17fe4092e52745ec14c7a7ad346198dd8e2f1" +url = "https://github.com/phall1/phui/releases/download/v0.15.0/phui-linux-x64.tar.gz" +url_api = "https://api.github.com/repos/phall1/phui/releases/assets/507794673" diff --git a/mise.linux.toml b/mise.linux.toml new file mode 100644 index 0000000..3e7af6d --- /dev/null +++ b/mise.linux.toml @@ -0,0 +1,22 @@ +[bootstrap.packages] +"apt:zsh" = "latest" +"apt:git" = "latest" +"apt:curl" = "latest" +"apt:ca-certificates" = "latest" +"apt:jq" = "latest" +"apt:age" = "latest" +"apt:tmux" = "latest" +"apt:tig" = "latest" +"apt:direnv" = "latest" +"apt:build-essential" = "latest" +"apt:pkg-config" = "latest" +"apt:procps" = "latest" +"apt:libssl-dev" = "latest" + +[tools] +"aqua:neovim/neovim" = "0.12.5" +"aqua:eza-community/eza" = "0.23.5" +"github:phall1/blackbird" = "0.5.0" +"github:no-phux/phux" = { version = "0.26.1", asset_pattern = "phux-v*-aarch64-unknown-linux-gnu.tar.gz", platforms.linux-x64.asset_pattern = "phux-v*-x86_64-unknown-linux-gnu.tar.gz" } +"github:phall1/phig" = "1.1.1" +"github:phall1/phui" = "0.15.0" diff --git a/mise.lock b/mise.lock new file mode 100644 index 0000000..5d28d1c --- /dev/null +++ b/mise.lock @@ -0,0 +1,736 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 1 + +[[tools."aqua:BurntSushi/ripgrep"]] +version = "15.2.0" +backend = "aqua:BurntSushi/ripgrep" +specifiers = [ + "15.2.0", + "latest", +] + +[tools."aqua:BurntSushi/ripgrep"."platforms.linux-arm64"] +checksum = "sha256:a740b91c82eaf9914cfedd353572f2791cbe0162c84101ee0951058f4dcbc90d" +url = "https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/478119579" + +[tools."aqua:BurntSushi/ripgrep"."platforms.linux-arm64-musl"] +checksum = "sha256:800b1e7206afe799dfb5a6901f23147cfaabe0e52210538100f61e86e1740915" +url = "https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/478120060" + +[tools."aqua:BurntSushi/ripgrep"."platforms.linux-x64"] +checksum = "sha256:33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c" +url = "https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/478120097" + +[tools."aqua:BurntSushi/ripgrep"."platforms.linux-x64-baseline"] +checksum = "sha256:33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c" +url = "https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/478120097" + +[tools."aqua:BurntSushi/ripgrep"."platforms.linux-x64-musl"] +checksum = "sha256:33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c" +url = "https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/478120097" + +[tools."aqua:BurntSushi/ripgrep"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c" +url = "https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/478120097" + +[tools."aqua:BurntSushi/ripgrep"."platforms.macos-arm64"] +checksum = "sha256:3750b2e93f37e0c692657da574d7019a101c0084da05a790c83fd335bad973e4" +url = "https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/BurntSushi/ripgrep/releases/assets/478118851" + +[[tools."aqua:ajeetdsouza/zoxide"]] +version = "0.10.0" +backend = "aqua:ajeetdsouza/zoxide" +specifiers = [ + "0.10.0", + "latest", +] + +[tools."aqua:ajeetdsouza/zoxide"."platforms.linux-arm64"] +checksum = "sha256:f1f16c5d6298d63dee467eedea1cdcd8490e43e493bea43acd416dc9033ef641" +url = "https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/ajeetdsouza/zoxide/releases/assets/466316402" + +[tools."aqua:ajeetdsouza/zoxide"."platforms.linux-arm64-musl"] +checksum = "sha256:f1f16c5d6298d63dee467eedea1cdcd8490e43e493bea43acd416dc9033ef641" +url = "https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/ajeetdsouza/zoxide/releases/assets/466316402" + +[tools."aqua:ajeetdsouza/zoxide"."platforms.linux-x64"] +checksum = "sha256:2d93385b99f3e82cf2701609a1bffcad863fbeb75aa3fe7eb6be4d29be68b1ae" +url = "https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/ajeetdsouza/zoxide/releases/assets/466316293" + +[tools."aqua:ajeetdsouza/zoxide"."platforms.linux-x64-baseline"] +checksum = "sha256:2d93385b99f3e82cf2701609a1bffcad863fbeb75aa3fe7eb6be4d29be68b1ae" +url = "https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/ajeetdsouza/zoxide/releases/assets/466316293" + +[tools."aqua:ajeetdsouza/zoxide"."platforms.linux-x64-musl"] +checksum = "sha256:2d93385b99f3e82cf2701609a1bffcad863fbeb75aa3fe7eb6be4d29be68b1ae" +url = "https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/ajeetdsouza/zoxide/releases/assets/466316293" + +[tools."aqua:ajeetdsouza/zoxide"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:2d93385b99f3e82cf2701609a1bffcad863fbeb75aa3fe7eb6be4d29be68b1ae" +url = "https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/ajeetdsouza/zoxide/releases/assets/466316293" + +[tools."aqua:ajeetdsouza/zoxide"."platforms.macos-arm64"] +checksum = "sha256:b55ae6f2f5f23d0a6ccb3bd4eeb2af9c7e0a6556e5255c82100e40305129bbb0" +url = "https://github.com/ajeetdsouza/zoxide/releases/download/v0.10.0/zoxide-0.10.0-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/ajeetdsouza/zoxide/releases/assets/466316174" + +[[tools."aqua:astral-sh/uv"]] +version = "0.12.10" +backend = "aqua:astral-sh/uv" +specifiers = [ + "0.12.10", + "latest", +] + +[tools."aqua:astral-sh/uv"."platforms.linux-arm64"] +checksum = "sha256:9ff6b9d4665edcdd3a88dcc73cd1eb641754deb927f14e8c62ebfde6bf4f5f5e" +url = "https://github.com/astral-sh/uv/releases/download/0.12.10/uv-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/545034356" +provenance = "github-attestations" + +[tools."aqua:astral-sh/uv"."platforms.linux-arm64-musl"] +checksum = "sha256:3983cb642fae84eab33756109bb266f126eb61763d8d220dd662af589c5b3da5" +url = "https://github.com/astral-sh/uv/releases/download/0.12.10/uv-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/545034364" +provenance = "github-attestations" + +[tools."aqua:astral-sh/uv"."platforms.linux-x64"] +checksum = "sha256:173d95a0c32d18c896c46ba6fafbf3cf9c14ab74b033f81b76c883ef492a976b" +url = "https://github.com/astral-sh/uv/releases/download/0.12.10/uv-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/545034465" +provenance = "github-attestations" + +[tools."aqua:astral-sh/uv"."platforms.linux-x64-baseline"] +checksum = "sha256:173d95a0c32d18c896c46ba6fafbf3cf9c14ab74b033f81b76c883ef492a976b" +url = "https://github.com/astral-sh/uv/releases/download/0.12.10/uv-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/545034465" +provenance = "github-attestations" + +[tools."aqua:astral-sh/uv"."platforms.linux-x64-musl"] +checksum = "sha256:848d0e261119e5b8f35db10164635e46a48bec29ceb1f8ec14a6fc76004973ee" +url = "https://github.com/astral-sh/uv/releases/download/0.12.10/uv-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/545034473" +provenance = "github-attestations" + +[tools."aqua:astral-sh/uv"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:848d0e261119e5b8f35db10164635e46a48bec29ceb1f8ec14a6fc76004973ee" +url = "https://github.com/astral-sh/uv/releases/download/0.12.10/uv-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/545034473" +provenance = "github-attestations" + +[tools."aqua:astral-sh/uv"."platforms.macos-arm64"] +checksum = "sha256:51c6170e8e3a01cef9f33b94f582b7b81ac65046f55d40afb35f9cff5a68c179" +url = "https://github.com/astral-sh/uv/releases/download/0.12.10/uv-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/545034336" +provenance = "github-attestations" + +[[tools."aqua:atuinsh/atuin"]] +version = "18.21.0" +backend = "aqua:atuinsh/atuin" +specifiers = [ + "18.21.0", + "latest", +] + +[tools."aqua:atuinsh/atuin"."platforms.linux-arm64"] +checksum = "sha256:dbdee27f7fc67b92e770d1af89d3b421051499a058cd3e4698d123e0117fa950" +url = "https://github.com/atuinsh/atuin/releases/download/v18.21.0/atuin-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/atuinsh/atuin/releases/assets/538647911" +provenance = "github-attestations" + +[tools."aqua:atuinsh/atuin"."platforms.linux-arm64-musl"] +checksum = "sha256:9b8a76df370eba71ea0809d4df74f79351be2314c1ed90cae229499e69ca2ee1" +url = "https://github.com/atuinsh/atuin/releases/download/v18.21.0/atuin-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/atuinsh/atuin/releases/assets/538647921" +provenance = "github-attestations" + +[tools."aqua:atuinsh/atuin"."platforms.linux-x64"] +checksum = "sha256:434d82b4c5f691b6f0423f30a4b931df4afded19b05a795a280192fdf8a61a2d" +url = "https://github.com/atuinsh/atuin/releases/download/v18.21.0/atuin-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/atuinsh/atuin/releases/assets/538647975" +provenance = "github-attestations" + +[tools."aqua:atuinsh/atuin"."platforms.linux-x64-baseline"] +checksum = "sha256:434d82b4c5f691b6f0423f30a4b931df4afded19b05a795a280192fdf8a61a2d" +url = "https://github.com/atuinsh/atuin/releases/download/v18.21.0/atuin-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/atuinsh/atuin/releases/assets/538647975" +provenance = "github-attestations" + +[tools."aqua:atuinsh/atuin"."platforms.linux-x64-musl"] +checksum = "sha256:792e1867d113ba9bebec94996b3160164aba738e3d23987b3cddb9dee976ac08" +url = "https://github.com/atuinsh/atuin/releases/download/v18.21.0/atuin-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/atuinsh/atuin/releases/assets/538647980" +provenance = "github-attestations" + +[tools."aqua:atuinsh/atuin"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:792e1867d113ba9bebec94996b3160164aba738e3d23987b3cddb9dee976ac08" +url = "https://github.com/atuinsh/atuin/releases/download/v18.21.0/atuin-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/atuinsh/atuin/releases/assets/538647980" +provenance = "github-attestations" + +[tools."aqua:atuinsh/atuin"."platforms.macos-arm64"] +checksum = "sha256:c78ac159c89c38ee0b56eb6a11d9c0a7340de3a03b98fbbccd44f8e2d91e6e6e" +url = "https://github.com/atuinsh/atuin/releases/download/v18.21.0/atuin-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/atuinsh/atuin/releases/assets/538647916" +provenance = "github-attestations" + +[[tools."aqua:cli/cli"]] +version = "2.100.0" +backend = "aqua:cli/cli" +specifiers = [ + "2.100.0", + "latest", +] + +[tools."aqua:cli/cli"."platforms.linux-arm64"] +checksum = "sha256:ea4e7a581a32ccad6cc7923cb1576ac5859ba4b9a16ab22eb8f8a96e78e2e961" +url = "https://github.com/cli/cli/releases/download/v2.100.0/gh_2.100.0_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/542974279" +provenance = "github-attestations" + +[tools."aqua:cli/cli"."platforms.linux-arm64-musl"] +checksum = "sha256:ea4e7a581a32ccad6cc7923cb1576ac5859ba4b9a16ab22eb8f8a96e78e2e961" +url = "https://github.com/cli/cli/releases/download/v2.100.0/gh_2.100.0_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/542974279" +provenance = "github-attestations" + +[tools."aqua:cli/cli"."platforms.linux-x64"] +checksum = "sha256:e4d4bb4498e8d007abe545b6568926793ace1b6447da598294a610018cb164be" +url = "https://github.com/cli/cli/releases/download/v2.100.0/gh_2.100.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/542974264" +provenance = "github-attestations" + +[tools."aqua:cli/cli"."platforms.linux-x64-baseline"] +checksum = "sha256:e4d4bb4498e8d007abe545b6568926793ace1b6447da598294a610018cb164be" +url = "https://github.com/cli/cli/releases/download/v2.100.0/gh_2.100.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/542974264" +provenance = "github-attestations" + +[tools."aqua:cli/cli"."platforms.linux-x64-musl"] +checksum = "sha256:e4d4bb4498e8d007abe545b6568926793ace1b6447da598294a610018cb164be" +url = "https://github.com/cli/cli/releases/download/v2.100.0/gh_2.100.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/542974264" +provenance = "github-attestations" + +[tools."aqua:cli/cli"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:e4d4bb4498e8d007abe545b6568926793ace1b6447da598294a610018cb164be" +url = "https://github.com/cli/cli/releases/download/v2.100.0/gh_2.100.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/542974264" +provenance = "github-attestations" + +[tools."aqua:cli/cli"."platforms.macos-arm64"] +checksum = "sha256:45f9a62da2f6e641a7fad57e2ce39656dfd7ef331372d80a2a2aed65abb01642" +url = "https://github.com/cli/cli/releases/download/v2.100.0/gh_2.100.0_macOS_arm64.zip" +url_api = "https://api.github.com/repos/cli/cli/releases/assets/542974295" +provenance = "github-attestations" + +[[tools."aqua:dandavison/delta"]] +version = "0.19.2" +backend = "aqua:dandavison/delta" +specifiers = [ + "0.19.2", + "latest", +] + +[tools."aqua:dandavison/delta"."platforms.linux-arm64"] +checksum = "sha256:0bfce159a5cddd5feb3d6db4a616d883ff51253ce08ac7ec11cb1d208cfaab9e" +url = "https://github.com/dandavison/delta/releases/download/0.19.2/delta-0.19.2-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/dandavison/delta/releases/assets/383662337" + +[tools."aqua:dandavison/delta"."platforms.linux-x64"] +checksum = "sha256:8e695c5f586a8c53d6c3b01be0b4a422ed218bfed2a56191caebe373a1c18ab2" +url = "https://github.com/dandavison/delta/releases/download/0.19.2/delta-0.19.2-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/dandavison/delta/releases/assets/383662069" + +[tools."aqua:dandavison/delta"."platforms.linux-x64-baseline"] +checksum = "sha256:8e695c5f586a8c53d6c3b01be0b4a422ed218bfed2a56191caebe373a1c18ab2" +url = "https://github.com/dandavison/delta/releases/download/0.19.2/delta-0.19.2-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/dandavison/delta/releases/assets/383662069" + +[tools."aqua:dandavison/delta"."platforms.linux-x64-musl"] +checksum = "sha256:f1ea01ca7728ce3462debc359f39dfc7cbbc1a63224b71fefabf92042864aa1b" +url = "https://github.com/dandavison/delta/releases/download/0.19.2/delta-0.19.2-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/dandavison/delta/releases/assets/383662298" + +[tools."aqua:dandavison/delta"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:f1ea01ca7728ce3462debc359f39dfc7cbbc1a63224b71fefabf92042864aa1b" +url = "https://github.com/dandavison/delta/releases/download/0.19.2/delta-0.19.2-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/dandavison/delta/releases/assets/383662298" + +[tools."aqua:dandavison/delta"."platforms.macos-arm64"] +checksum = "sha256:9be36612a5a13e9e386dc498fb8e50dc87c72ee42b63db0ea05b32f99a72a69a" +url = "https://github.com/dandavison/delta/releases/download/0.19.2/delta-0.19.2-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/dandavison/delta/releases/assets/383662310" + +[[tools."aqua:gitui-org/gitui"]] +version = "0.28.1" +backend = "aqua:gitui-org/gitui" +specifiers = [ + "0.28.1", + "latest", +] + +[tools."aqua:gitui-org/gitui"."platforms.linux-arm64"] +checksum = "sha256:2a640da05b73e9152a7bf92bfd7c23acd9d60438f4f2cabb62357014a5fd7c28" +url = "https://github.com/gitui-org/gitui/releases/download/v0.28.1/gitui-linux-aarch64.tar.gz" +url_api = "https://api.github.com/repos/gitui-org/gitui/releases/assets/380791456" + +[tools."aqua:gitui-org/gitui"."platforms.linux-arm64-musl"] +checksum = "sha256:2a640da05b73e9152a7bf92bfd7c23acd9d60438f4f2cabb62357014a5fd7c28" +url = "https://github.com/gitui-org/gitui/releases/download/v0.28.1/gitui-linux-aarch64.tar.gz" +url_api = "https://api.github.com/repos/gitui-org/gitui/releases/assets/380791456" + +[tools."aqua:gitui-org/gitui"."platforms.linux-x64"] +checksum = "sha256:f6149b9ae203397158b0c89c13cfde718e7121d3d3cd2ebc597f93d6628d9b5b" +url = "https://github.com/gitui-org/gitui/releases/download/v0.28.1/gitui-linux-x86_64.tar.gz" +url_api = "https://api.github.com/repos/gitui-org/gitui/releases/assets/380787855" + +[tools."aqua:gitui-org/gitui"."platforms.linux-x64-baseline"] +checksum = "sha256:f6149b9ae203397158b0c89c13cfde718e7121d3d3cd2ebc597f93d6628d9b5b" +url = "https://github.com/gitui-org/gitui/releases/download/v0.28.1/gitui-linux-x86_64.tar.gz" +url_api = "https://api.github.com/repos/gitui-org/gitui/releases/assets/380787855" + +[tools."aqua:gitui-org/gitui"."platforms.linux-x64-musl"] +checksum = "sha256:f6149b9ae203397158b0c89c13cfde718e7121d3d3cd2ebc597f93d6628d9b5b" +url = "https://github.com/gitui-org/gitui/releases/download/v0.28.1/gitui-linux-x86_64.tar.gz" +url_api = "https://api.github.com/repos/gitui-org/gitui/releases/assets/380787855" + +[tools."aqua:gitui-org/gitui"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:f6149b9ae203397158b0c89c13cfde718e7121d3d3cd2ebc597f93d6628d9b5b" +url = "https://github.com/gitui-org/gitui/releases/download/v0.28.1/gitui-linux-x86_64.tar.gz" +url_api = "https://api.github.com/repos/gitui-org/gitui/releases/assets/380787855" + +[tools."aqua:gitui-org/gitui"."platforms.macos-arm64"] +checksum = "sha256:3b519a593383841289361bacf579d4a222169aa7ebbdeae6cc449d488d8bb967" +url = "https://github.com/gitui-org/gitui/releases/download/v0.28.1/gitui-mac.tar.gz" +url_api = "https://api.github.com/repos/gitui-org/gitui/releases/assets/380793603" + +[[tools."aqua:jesseduffield/lazygit"]] +version = "0.64.1" +backend = "aqua:jesseduffield/lazygit" +specifiers = [ + "0.64.1", + "latest", +] + +[tools."aqua:jesseduffield/lazygit"."platforms.linux-arm64"] +checksum = "sha256:8b7ca3b344e60340ad1f89f29b9868ee39bcaba5bb92ee818bbe65476bb8b6e7" +url = "https://github.com/jesseduffield/lazygit/releases/download/v0.64.1/lazygit_0.64.1_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/jesseduffield/lazygit/releases/assets/511894160" + +[tools."aqua:jesseduffield/lazygit"."platforms.linux-arm64-musl"] +checksum = "sha256:8b7ca3b344e60340ad1f89f29b9868ee39bcaba5bb92ee818bbe65476bb8b6e7" +url = "https://github.com/jesseduffield/lazygit/releases/download/v0.64.1/lazygit_0.64.1_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/jesseduffield/lazygit/releases/assets/511894160" + +[tools."aqua:jesseduffield/lazygit"."platforms.linux-x64"] +checksum = "sha256:f8ea237c41f194cd799b48505518bfdaae4edf5a2ad6bd3d898e939785ee4532" +url = "https://github.com/jesseduffield/lazygit/releases/download/v0.64.1/lazygit_0.64.1_linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/jesseduffield/lazygit/releases/assets/511894209" + +[tools."aqua:jesseduffield/lazygit"."platforms.linux-x64-baseline"] +checksum = "sha256:f8ea237c41f194cd799b48505518bfdaae4edf5a2ad6bd3d898e939785ee4532" +url = "https://github.com/jesseduffield/lazygit/releases/download/v0.64.1/lazygit_0.64.1_linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/jesseduffield/lazygit/releases/assets/511894209" + +[tools."aqua:jesseduffield/lazygit"."platforms.linux-x64-musl"] +checksum = "sha256:f8ea237c41f194cd799b48505518bfdaae4edf5a2ad6bd3d898e939785ee4532" +url = "https://github.com/jesseduffield/lazygit/releases/download/v0.64.1/lazygit_0.64.1_linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/jesseduffield/lazygit/releases/assets/511894209" + +[tools."aqua:jesseduffield/lazygit"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:f8ea237c41f194cd799b48505518bfdaae4edf5a2ad6bd3d898e939785ee4532" +url = "https://github.com/jesseduffield/lazygit/releases/download/v0.64.1/lazygit_0.64.1_linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/jesseduffield/lazygit/releases/assets/511894209" + +[tools."aqua:jesseduffield/lazygit"."platforms.macos-arm64"] +checksum = "sha256:a106b4c1bf8ab7539c4afb40a01cbde263a96513cdcb8bf7630278f07e88cd99" +url = "https://github.com/jesseduffield/lazygit/releases/download/v0.64.1/lazygit_0.64.1_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/jesseduffield/lazygit/releases/assets/511894212" + +[[tools."aqua:junegunn/fzf"]] +version = "0.74.3" +backend = "aqua:junegunn/fzf" +specifiers = [ + "0.74.3", + "latest", +] + +[tools."aqua:junegunn/fzf"."platforms.linux-arm64"] +checksum = "sha256:4a17a17b46bd0c4873e995533de508995c11572c0be0664a5dbcf13f60463046" +url = "https://github.com/junegunn/fzf/releases/download/v0.74.3/fzf-0.74.3-linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/junegunn/fzf/releases/assets/517537065" + +[tools."aqua:junegunn/fzf"."platforms.linux-arm64-musl"] +checksum = "sha256:4a17a17b46bd0c4873e995533de508995c11572c0be0664a5dbcf13f60463046" +url = "https://github.com/junegunn/fzf/releases/download/v0.74.3/fzf-0.74.3-linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/junegunn/fzf/releases/assets/517537065" + +[tools."aqua:junegunn/fzf"."platforms.linux-x64"] +checksum = "sha256:3501a595e4b5c40a6b047340a0e8f805c46fd4e61ef95ef8a136ba8c61cf6f22" +url = "https://github.com/junegunn/fzf/releases/download/v0.74.3/fzf-0.74.3-linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/junegunn/fzf/releases/assets/517537066" + +[tools."aqua:junegunn/fzf"."platforms.linux-x64-baseline"] +checksum = "sha256:3501a595e4b5c40a6b047340a0e8f805c46fd4e61ef95ef8a136ba8c61cf6f22" +url = "https://github.com/junegunn/fzf/releases/download/v0.74.3/fzf-0.74.3-linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/junegunn/fzf/releases/assets/517537066" + +[tools."aqua:junegunn/fzf"."platforms.linux-x64-musl"] +checksum = "sha256:3501a595e4b5c40a6b047340a0e8f805c46fd4e61ef95ef8a136ba8c61cf6f22" +url = "https://github.com/junegunn/fzf/releases/download/v0.74.3/fzf-0.74.3-linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/junegunn/fzf/releases/assets/517537066" + +[tools."aqua:junegunn/fzf"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:3501a595e4b5c40a6b047340a0e8f805c46fd4e61ef95ef8a136ba8c61cf6f22" +url = "https://github.com/junegunn/fzf/releases/download/v0.74.3/fzf-0.74.3-linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/junegunn/fzf/releases/assets/517537066" + +[tools."aqua:junegunn/fzf"."platforms.macos-arm64"] +checksum = "sha256:1f8501cea4f9c0c2d6110d0ff75d0ec9451cd9d7524d9a26244a154ea89f3bd5" +url = "https://github.com/junegunn/fzf/releases/download/v0.74.3/fzf-0.74.3-darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/junegunn/fzf/releases/assets/517537091" + +[[tools."aqua:mikefarah/yq"]] +version = "4.53.6" +backend = "aqua:mikefarah/yq" +specifiers = [ + "4.53.6", + "latest", +] + +[tools."aqua:mikefarah/yq"."platforms.linux-arm64"] +checksum = "sha256:88a1016bc1d657375a35864e4f44b6f333df8ff97b559f51bba0adcb2169df09" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_arm64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028007" +provenance = "cosign" + +[tools."aqua:mikefarah/yq"."platforms.linux-arm64-musl"] +checksum = "sha256:88a1016bc1d657375a35864e4f44b6f333df8ff97b559f51bba0adcb2169df09" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_arm64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028007" +provenance = "cosign" + +[tools."aqua:mikefarah/yq"."platforms.linux-x64"] +checksum = "sha256:c5f056448f973ae7d39b5401949648a78f2dc1947d6a8eb65be60d5c504b9385" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028022" +provenance = "cosign" + +[tools."aqua:mikefarah/yq"."platforms.linux-x64-baseline"] +checksum = "sha256:c5f056448f973ae7d39b5401949648a78f2dc1947d6a8eb65be60d5c504b9385" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028022" +provenance = "cosign" + +[tools."aqua:mikefarah/yq"."platforms.linux-x64-musl"] +checksum = "sha256:c5f056448f973ae7d39b5401949648a78f2dc1947d6a8eb65be60d5c504b9385" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028022" +provenance = "cosign" + +[tools."aqua:mikefarah/yq"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:c5f056448f973ae7d39b5401949648a78f2dc1947d6a8eb65be60d5c504b9385" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_linux_amd64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028022" +provenance = "cosign" + +[tools."aqua:mikefarah/yq"."platforms.macos-arm64"] +checksum = "sha256:cceb0b8d71ea5294334121f8429f33f92b920e7217d904a2f9f35443968ac424" +url = "https://github.com/mikefarah/yq/releases/download/v4.53.6/yq_darwin_arm64" +url_api = "https://api.github.com/repos/mikefarah/yq/releases/assets/522028033" +provenance = "cosign" + +[[tools."aqua:sharkdp/bat"]] +version = "0.26.1" +backend = "aqua:sharkdp/bat" +specifiers = [ + "0.26.1", + "latest", +] + +[tools."aqua:sharkdp/bat"."platforms.linux-arm64"] +checksum = "sha256:422eb73e11c854fddd99f5ca8461c2f1d6e6dce0a2a8c3d5daade5ffcb6564aa" +url = "https://github.com/sharkdp/bat/releases/download/v0.26.1/bat-v0.26.1-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/bat/releases/assets/323548647" + +[tools."aqua:sharkdp/bat"."platforms.linux-arm64-musl"] +checksum = "sha256:6369242c584065f195fb20cb36fbd7cb63ae690605bbe89868a7596b596c2c23" +url = "https://github.com/sharkdp/bat/releases/download/v0.26.1/bat-v0.26.1-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/bat/releases/assets/323548153" + +[tools."aqua:sharkdp/bat"."platforms.linux-x64"] +checksum = "sha256:726f04c8f576a7fd18b7634f1bbf2f915c43494c1c0f013baa3287edb0d5a2a3" +url = "https://github.com/sharkdp/bat/releases/download/v0.26.1/bat-v0.26.1-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/bat/releases/assets/323549181" + +[tools."aqua:sharkdp/bat"."platforms.linux-x64-baseline"] +checksum = "sha256:726f04c8f576a7fd18b7634f1bbf2f915c43494c1c0f013baa3287edb0d5a2a3" +url = "https://github.com/sharkdp/bat/releases/download/v0.26.1/bat-v0.26.1-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/bat/releases/assets/323549181" + +[tools."aqua:sharkdp/bat"."platforms.linux-x64-musl"] +checksum = "sha256:0dcd8ac79732c0d5b136f11f4ee00e581440e16a44eab5b3105b611bbf2cf191" +url = "https://github.com/sharkdp/bat/releases/download/v0.26.1/bat-v0.26.1-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/bat/releases/assets/323549429" + +[tools."aqua:sharkdp/bat"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:0dcd8ac79732c0d5b136f11f4ee00e581440e16a44eab5b3105b611bbf2cf191" +url = "https://github.com/sharkdp/bat/releases/download/v0.26.1/bat-v0.26.1-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/bat/releases/assets/323549429" + +[tools."aqua:sharkdp/bat"."platforms.macos-arm64"] +checksum = "sha256:e30beff26779c9bf60bb541e1d79046250cb74378f2757f8eb250afddb19e114" +url = "https://github.com/sharkdp/bat/releases/download/v0.26.1/bat-v0.26.1-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/bat/releases/assets/323548442" + +[[tools."aqua:sharkdp/fd"]] +version = "10.5.0" +backend = "aqua:sharkdp/fd" +specifiers = [ + "10.5.0", + "latest", +] + +[tools."aqua:sharkdp/fd"."platforms.linux-arm64"] +checksum = "sha256:c0ee43802e3313a317c5af2f4eabd6ba13eeedd595af9775f05e18a13ac4f52c" +url = "https://github.com/sharkdp/fd/releases/download/v10.5.0/fd-v10.5.0-aarch64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/fd/releases/assets/530482307" + +[tools."aqua:sharkdp/fd"."platforms.linux-arm64-musl"] +checksum = "sha256:d76c4317f7d5dba69f8a2a15856c90c777e7f0dd4e85f0de8c76de6992c374d4" +url = "https://github.com/sharkdp/fd/releases/download/v10.5.0/fd-v10.5.0-aarch64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/fd/releases/assets/530481810" + +[tools."aqua:sharkdp/fd"."platforms.linux-x64"] +checksum = "sha256:a1259cd129636efbc3fef123525c1b49e88fe5088c012630983c310e52fdfa95" +url = "https://github.com/sharkdp/fd/releases/download/v10.5.0/fd-v10.5.0-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/fd/releases/assets/530481615" + +[tools."aqua:sharkdp/fd"."platforms.linux-x64-baseline"] +checksum = "sha256:a1259cd129636efbc3fef123525c1b49e88fe5088c012630983c310e52fdfa95" +url = "https://github.com/sharkdp/fd/releases/download/v10.5.0/fd-v10.5.0-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/fd/releases/assets/530481615" + +[tools."aqua:sharkdp/fd"."platforms.linux-x64-musl"] +checksum = "sha256:761c72dc8e120d85b22292063be8a796e2eeb20eb3e4f38b8fa2343ccf3514a7" +url = "https://github.com/sharkdp/fd/releases/download/v10.5.0/fd-v10.5.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/fd/releases/assets/530481315" + +[tools."aqua:sharkdp/fd"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:761c72dc8e120d85b22292063be8a796e2eeb20eb3e4f38b8fa2343ccf3514a7" +url = "https://github.com/sharkdp/fd/releases/download/v10.5.0/fd-v10.5.0-x86_64-unknown-linux-musl.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/fd/releases/assets/530481315" + +[tools."aqua:sharkdp/fd"."platforms.macos-arm64"] +checksum = "sha256:b67e1836c468e42e411984b56e52fa7abec08c2bd22c867398e7cc134aac5e12" +url = "https://github.com/sharkdp/fd/releases/download/v10.5.0/fd-v10.5.0-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/sharkdp/fd/releases/assets/530481346" + +[[tools."aqua:twpayne/chezmoi"]] +version = "2.72.1" +backend = "aqua:twpayne/chezmoi" +specifiers = [ + "2.72.1", + "latest", +] + +[tools."aqua:twpayne/chezmoi"."platforms.linux-arm64"] +checksum = "sha256:75508ef41216b6d64f3145986b751729d7f92d09c6bad77d51cf2895ab35a508" +url = "https://github.com/twpayne/chezmoi/releases/download/v2.72.1/chezmoi_2.72.1_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/twpayne/chezmoi/releases/assets/536653653" +provenance = "cosign" + +[tools."aqua:twpayne/chezmoi"."platforms.linux-arm64-musl"] +checksum = "sha256:75508ef41216b6d64f3145986b751729d7f92d09c6bad77d51cf2895ab35a508" +url = "https://github.com/twpayne/chezmoi/releases/download/v2.72.1/chezmoi_2.72.1_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/twpayne/chezmoi/releases/assets/536653653" +provenance = "cosign" + +[tools."aqua:twpayne/chezmoi"."platforms.linux-x64"] +checksum = "sha256:9f97d32caca166e5c92160ec3a9325519809c38963121cef38173142065c981f" +url = "https://github.com/twpayne/chezmoi/releases/download/v2.72.1/chezmoi_2.72.1_linux-glibc_amd64.tar.gz" +url_api = "https://api.github.com/repos/twpayne/chezmoi/releases/assets/536653696" +provenance = "cosign" + +[tools."aqua:twpayne/chezmoi"."platforms.linux-x64-baseline"] +checksum = "sha256:9f97d32caca166e5c92160ec3a9325519809c38963121cef38173142065c981f" +url = "https://github.com/twpayne/chezmoi/releases/download/v2.72.1/chezmoi_2.72.1_linux-glibc_amd64.tar.gz" +url_api = "https://api.github.com/repos/twpayne/chezmoi/releases/assets/536653696" +provenance = "cosign" + +[tools."aqua:twpayne/chezmoi"."platforms.linux-x64-musl"] +checksum = "sha256:b961e2972d6fcd1002f9b986d4a61dc5da288e96aab226434fd5b20a7de80cf9" +url = "https://github.com/twpayne/chezmoi/releases/download/v2.72.1/chezmoi_2.72.1_linux-musl_amd64.tar.gz" +url_api = "https://api.github.com/repos/twpayne/chezmoi/releases/assets/536653704" +provenance = "cosign" + +[tools."aqua:twpayne/chezmoi"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:b961e2972d6fcd1002f9b986d4a61dc5da288e96aab226434fd5b20a7de80cf9" +url = "https://github.com/twpayne/chezmoi/releases/download/v2.72.1/chezmoi_2.72.1_linux-musl_amd64.tar.gz" +url_api = "https://api.github.com/repos/twpayne/chezmoi/releases/assets/536653704" +provenance = "cosign" + +[tools."aqua:twpayne/chezmoi"."platforms.macos-arm64"] +checksum = "sha256:938d422091cc001e68fe3fd7efea9b923a36facbf2b8db67063639abbaf72de2" +url = "https://github.com/twpayne/chezmoi/releases/download/v2.72.1/chezmoi_2.72.1_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/twpayne/chezmoi/releases/assets/536653589" +provenance = "cosign" + +[[tools.bun]] +version = "1.3.14" +backend = "core:bun" +specifiers = ["1.3.14"] + +[tools.bun."platforms.linux-arm64"] +checksum = "sha256:a27ffb63a8310375836e0d6f668ae17fa8d8d18b88c37c821c65331973a19a3b" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-aarch64.zip" + +[tools.bun."platforms.linux-arm64-musl"] +checksum = "sha256:b98e0ad3625c5c00d1d5b5ff55605c7adddbfae151861e68ade57b2d3b8703bb" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-aarch64-musl.zip" + +[tools.bun."platforms.linux-x64"] +checksum = "sha256:951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip" + +[tools.bun."platforms.linux-x64-baseline"] +checksum = "sha256:a063908ae08b7852ca10939bbdc6ceed3ddabce8fb9402dce83d65d73b36e6c7" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64-baseline.zip" + +[tools.bun."platforms.linux-x64-musl"] +checksum = "sha256:14bd9aedeebf1dba67e8def9531c89bc989ecfdf1de42e5bfcaf1b8cd9294719" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64-musl.zip" + +[tools.bun."platforms.linux-x64-musl-baseline"] +checksum = "sha256:56a7d6806cf155536c0178f0ea5fbd098e684fa509ebdb4fc0a7e19fb65382dc" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64-musl-baseline.zip" + +[tools.bun."platforms.macos-arm64"] +checksum = "sha256:d8b96221828ad6f97ac7ac0ab7e95872341af763001e8803e8267652c2652620" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-darwin-aarch64.zip" + +[[tools."github:joshmedeski/sesh"]] +version = "2.29.0" +backend = "github:joshmedeski/sesh" +specifiers = ["2.29.0"] + +[tools."github:joshmedeski/sesh"."platforms.linux-arm64"] +checksum = "sha256:003e3ed81a95f991b9b38c1d3246371a539844775c96e6324ac76dfbd1f90494" +url = "https://github.com/joshmedeski/sesh/releases/download/v2.29.0/sesh_Linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/joshmedeski/sesh/releases/assets/541315371" + +[tools."github:joshmedeski/sesh"."platforms.linux-arm64-musl"] +checksum = "sha256:003e3ed81a95f991b9b38c1d3246371a539844775c96e6324ac76dfbd1f90494" +url = "https://github.com/joshmedeski/sesh/releases/download/v2.29.0/sesh_Linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/joshmedeski/sesh/releases/assets/541315371" + +[tools."github:joshmedeski/sesh"."platforms.linux-x64"] +checksum = "sha256:ff503505c9f6ed890a6af30c952ae24a4722c7d4e10a2f98648b416ad71d09c6" +url = "https://github.com/joshmedeski/sesh/releases/download/v2.29.0/sesh_Linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/joshmedeski/sesh/releases/assets/541315360" + +[tools."github:joshmedeski/sesh"."platforms.linux-x64-baseline"] +checksum = "sha256:ff503505c9f6ed890a6af30c952ae24a4722c7d4e10a2f98648b416ad71d09c6" +url = "https://github.com/joshmedeski/sesh/releases/download/v2.29.0/sesh_Linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/joshmedeski/sesh/releases/assets/541315360" + +[tools."github:joshmedeski/sesh"."platforms.linux-x64-musl"] +checksum = "sha256:ff503505c9f6ed890a6af30c952ae24a4722c7d4e10a2f98648b416ad71d09c6" +url = "https://github.com/joshmedeski/sesh/releases/download/v2.29.0/sesh_Linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/joshmedeski/sesh/releases/assets/541315360" + +[tools."github:joshmedeski/sesh"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:ff503505c9f6ed890a6af30c952ae24a4722c7d4e10a2f98648b416ad71d09c6" +url = "https://github.com/joshmedeski/sesh/releases/download/v2.29.0/sesh_Linux_x86_64.tar.gz" +url_api = "https://api.github.com/repos/joshmedeski/sesh/releases/assets/541315360" + +[tools."github:joshmedeski/sesh"."platforms.macos-arm64"] +checksum = "sha256:c3062184a9a423a123294b03ac27065d144e0052db11ddd500f88680f22ca123" +url = "https://github.com/joshmedeski/sesh/releases/download/v2.29.0/sesh_Darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/joshmedeski/sesh/releases/assets/541315358" + +[[tools.node]] +version = "24.20.0" +backend = "core:node" +specifiers = ["24.20.0"] + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:3515603e2487879a39bc75716f1a2affd027500c64ba50e845cf72cb33219013" +url = "https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:2c8c507ccb0f20812d9526ba8ca454b1652aadef68fc8bad06f07fb1122dd1ef" +url = "https://unofficial-builds.nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-arm64-musl.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:855d581f8a4eb1a8117e3426de25fe02770592febcfb31369aee1ffbfee9e8ec" +url = "https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-baseline"] +checksum = "sha256:855d581f8a4eb1a8117e3426de25fe02770592febcfb31369aee1ffbfee9e8ec" +url = "https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:9ae1399fef4bd8990e15773ce1327b336a20b9e97d8c7549f4f42ca73c43f562" +url = "https://unofficial-builds.nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.linux-x64-musl-baseline"] +checksum = "sha256:9ae1399fef4bd8990e15773ce1327b336a20b9e97d8c7549f4f42ca73c43f562" +url = "https://unofficial-builds.nodejs.org/download/release/v24.20.0/node-v24.20.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:40e5607e5ecb3db9192723776da2d75d966260fc74a7a9e731c1bd67dda96bc8" +url = "https://nodejs.org/dist/v24.20.0/node-v24.20.0-darwin-arm64.tar.gz" + +[[tools."npm:open-websearch"]] +version = "2.1.11" +backend = "npm:open-websearch" +specifiers = ["2.1.11"] + +[[tools.zig]] +version = "0.16.0" +backend = "core:zig" +specifiers = ["0.16.0"] + +[tools.zig."platforms.linux-arm64"] +checksum = "sha256:ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17" +url = "https://ziglang.org/download/0.16.0/zig-aarch64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-arm64-musl"] +checksum = "sha256:ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17" +url = "https://ziglang.org/download/0.16.0/zig-aarch64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-x64"] +checksum = "sha256:70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-x64-baseline"] +checksum = "sha256:70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-x64-musl"] +checksum = "sha256:70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.linux-x64-musl-baseline"] +checksum = "sha256:70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00" +url = "https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz" + +[tools.zig."platforms.macos-arm64"] +checksum = "sha256:b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489" +url = "https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xz" diff --git a/mise.pi.lock b/mise.pi.lock new file mode 100644 index 0000000..0429f70 --- /dev/null +++ b/mise.pi.lock @@ -0,0 +1,8 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 1 + +[[tools."npm:@earendil-works/pi-coding-agent"]] +version = "0.84.4" +backend = "npm:@earendil-works/pi-coding-agent" +specifiers = ["0.84.4"] diff --git a/mise.pi.toml b/mise.pi.toml new file mode 100644 index 0000000..4e2d385 --- /dev/null +++ b/mise.pi.toml @@ -0,0 +1,3 @@ +# Optional Pi coding-agent harness, independent of the Raspberry Pi host. +[tools] +"npm:@earendil-works/pi-coding-agent" = "0.84.4" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..7005868 --- /dev/null +++ b/mise.toml @@ -0,0 +1,56 @@ +min_version = "2026.9.3" + +# This is the portable tool inventory. Chezmoi renders the same declarations +# into the user's global mise configuration; versions have one source. +[tools] +node = "24.20.0" +bun = "1.3.14" +zig = "0.16.0" +"aqua:astral-sh/uv" = "0.12.10" +"aqua:twpayne/chezmoi" = "2.72.1" +"aqua:atuinsh/atuin" = "18.21.0" +"aqua:junegunn/fzf" = "0.74.3" +"aqua:dandavison/delta" = "0.19.2" +"aqua:ajeetdsouza/zoxide" = "0.10.0" +"aqua:mikefarah/yq" = "4.53.6" +"aqua:cli/cli" = "2.100.0" +"aqua:sharkdp/fd" = "10.5.0" +"aqua:sharkdp/bat" = "0.26.1" +"aqua:BurntSushi/ripgrep" = "15.2.0" +"aqua:gitui-org/gitui" = "0.28.1" +"aqua:jesseduffield/lazygit" = "0.64.1" +"github:joshmedeski/sesh" = "2.29.0" +"npm:open-websearch" = "2.1.11" + +[settings] +idiomatic_version_file_enable_tools = ["node"] +# Use native npm; mise skips lifecycle scripts for these tools by default. +npm.package_manager = "npm" + +[bootstrap.hooks.pre-packages] +run = "bash scripts/bootstrap/packages.sh" + +[tasks.bootstrap] +description = "Materialize preferences, reconcile native integrations, and verify" +run = "bash scripts/bootstrap/converge.sh" + +[tasks.configure] +description = "Select this machine's optional harnesses (e.g. mise run configure pi claude)" +run = "bash scripts/bootstrap/configure.sh" + +[tasks.check] +description = "Validate source and isolated configuration convergence" +run = "bash tests/bootstrap/check.sh" + +[tasks.test-container] +description = "Build and run the disposable Linux bootstrap acceptance rig" +run = "bash tests/bootstrap/container.sh" + +[tasks.doctor] +run = "bash dot_local/bin/executable_dot-doctor" + +[tasks.audit] +run = "bash dot_local/bin/executable_dot-audit" + +[tasks.bench] +run = "bash dot_local/bin/executable_dot-bench" diff --git a/provision/Brewfile b/provision/Brewfile new file mode 100644 index 0000000..0ee8d89 --- /dev/null +++ b/provision/Brewfile @@ -0,0 +1,18 @@ +# Real Homebrew owns host packages and the personal tap. Mise 2026.9.1's +# built-in brew installer cannot consume this tap's Ruby-only metadata. +tap "phall1/tap" +brew "git" +brew "jq" +brew "age" +brew "tmux" +brew "neovim" +brew "tig" +brew "direnv" +brew "coreutils" +brew "eza" +brew "rustup-init" +brew "act" +brew "phall1/tap/blackbird" +brew "phall1/tap/phux" +brew "phall1/tap/phig" +brew "phall1/tap/phui" diff --git a/provision/Brewfile.desktop b/provision/Brewfile.desktop new file mode 100644 index 0000000..0545e08 --- /dev/null +++ b/provision/Brewfile.desktop @@ -0,0 +1,5 @@ +cask "ghostty" +cask "font-jetbrains-mono-nerd-font" +cask "phall1/tap/phux-cockpit" +cask "phall1/tap/token-tach" +cask "phall1/tap/friday" diff --git a/provision/dotfiles-history.json b/provision/dotfiles-history.json new file mode 100644 index 0000000..518d2bb --- /dev/null +++ b/provision/dotfiles-history.json @@ -0,0 +1,43 @@ +{ + "shared": [ + ".agents/skills", + ".config/direnv/direnv.toml", + ".config/direnv/direnvrc", + ".config/gh-dash/config.yml", + ".config/gh-repo-preset/default.json", + ".config/gitui/key_bindings.ron", + ".config/gitui/theme.ron", + ".config/lazygit/config.yml", + ".config/mcp/mcp.json", + ".config/nvim", + ".config/opencode/agents", + ".config/opencode/commands", + ".config/opencode/opencode.jsonc", + ".config/opencode/skill/agent-forge/SKILL.md", + ".config/opencode/skill/linear-cli/SKILL.md", + ".config/opencode/skill/steve-jobs/SKILL.md", + ".config/phig/config.toml", + ".config/phux/layers/dotfiles.toml", + ".config/sesh/sesh.toml", + ".config/starship.toml", + ".config/zed/settings.json", + ".p10k.zsh", + ".tigrc", + ".tmux.conf", + ".tmux/scripts", + ".zprofile", + ".zsh/aliases.zsh", + ".zsh/functions.zsh", + ".zsh/work.zsh", + ".zshenv", + ".zshrc" + ], + "macos": [ + ".config/ghostty/config", + ".config/ghostty/shaders", + ".config/ghostty/themes", + ".config/phux-cockpit/config", + ".config/token-tach/config" + ], + "exclude": ["**/*.zwc", "**/.DS_Store", "**/node_modules/**", "**/.git/**", "**/lazy-lock.json", "**/*.log"] +} diff --git a/provision/retired-files.sha256 b/provision/retired-files.sha256 new file mode 100644 index 0000000..74e0d08 --- /dev/null +++ b/provision/retired-files.sha256 @@ -0,0 +1,9 @@ +5081ddcfa06e525981ab8db903b583e0809fc5aedb38974b336739655e1694dd .config/opencode/commands/autoresearch.md +435f229ccee2e66bf86ba833b4a5921bba40be86b8d4a455fc34d3681abf89c6 .config/opencode/plugin/autoresearch/README.md +65a254d529043b7d033c1e37e72e7e54bcc142ffb6fd049e21b87cd82026470a .config/opencode/plugin/autoresearch/index.js +5df88e248d73f45c3e6c985645ad3cc245fad726bb8702655d35175399eea591 .config/opencode/plugin/autoresearch/package-lock.json +689d86f8a2bef2607de8e0104b689fdb4fe64e2910fe161750de047490343987 .config/opencode/plugin/autoresearch/package.json +180a2c1718fc7e7615d489297a9def3cefdf24f98e98d9888ec2ecb1ba33d119 .config/opencode/plugin/autoresearch/test/plugin.test.mjs +c594c224b7df2a41703f79989047548989e302a2ee24a03924e28bdcbd379758 .config/opencode/subtask2.jsonc +37431148208dcf5df395127c462fc98feabc90af0e374b429ad8c193c282b23a .config/opencode/worktree.conf +cbbb9abf9c46438584bbe13a31f763a6f43b31fd096d4eaaf33d159fb6f2262b tests/gha-local-smoke.sh diff --git a/renovate.json b/renovate.json index c5acc96..b3fe843 100644 --- a/renovate.json +++ b/renovate.json @@ -12,36 +12,18 @@ "packageRules": [ { "groupName": "dotfiles dependencies", - "matchManagers": ["cargo", "npm", "custom.regex"] + "matchManagers": ["cargo", "npm", "mise", "custom.regex"] } ], "customManagers": [ { "customType": "regex", "description": "Pinned Pi extension packages shared by installation, chezmoi reconciliation, and doctor checks", - "managerFilePatterns": ["/^(scripts/install-agent-stack\\.sh|dot_pi/private_agent/modify_settings\\.json|checks/pi\\.sh)$/"], + "managerFilePatterns": ["/^(dot_pi/private_agent/modify_settings\\.json|checks/pi\\.sh)$/"], "matchStrings": ["(?(?:@[\\w-]+/)?[\\w-]+)@(?\\d+\\.\\d+\\.\\d+)"], "datasourceTemplate": "npm", "versioningTemplate": "npm" }, - { - "customType": "regex", - "description": "Pinned Pi CLI version and its doctor assertion", - "managerFilePatterns": ["/^(scripts/install-agent-stack\\.sh|checks/pi\\.sh)$/"], - "matchStrings": ["PI_VERSION=(?\\d+\\.\\d+\\.\\d+)", "\\[\\[ \\\"\\$version\\\" == \\\"(?\\d+\\.\\d+\\.\\d+)\\\""], - "depNameTemplate": "@earendil-works/pi-coding-agent", - "datasourceTemplate": "npm", - "versioningTemplate": "npm" - }, - { - "customType": "regex", - "description": "Pinned Open WebSearch CLI version", - "managerFilePatterns": ["/^scripts/install-agent-stack\\.sh$/"], - "matchStrings": ["WEB_VERSION=(?\\d+\\.\\d+\\.\\d+)"], - "depNameTemplate": "open-websearch", - "datasourceTemplate": "npm", - "versioningTemplate": "npm" - }, { "customType": "regex", "description": "Pinned Renovate semantic validator used by dependency CI", @@ -51,14 +33,6 @@ "datasourceTemplate": "npm", "versioningTemplate": "npm" }, - { - "customType": "regex", - "description": "Pinned Blackbird release and its doctor assertion", - "managerFilePatterns": ["/^(scripts/install-agent-stack\\.sh|checks/pi\\.sh)$/"], - "matchStrings": ["BLACKBIRD_VERSION=(?\\d+\\.\\d+\\.\\d+)", "\\[\\[ \\\"\\$bb_version\\\" == \\\"(?\\d+\\.\\d+\\.\\d+)\\\""], - "depNameTemplate": "phall1/blackbird", - "datasourceTemplate": "github-releases" - }, { "customType": "regex", "description": "OpenCode's embedded npm plugin pin", @@ -67,16 +41,6 @@ "datasourceTemplate": "npm", "versioningTemplate": "npm" }, - { - "customType": "regex", - "description": "Pinned OpenCode worktree revision", - "managerFilePatterns": ["/^run_onchange_install-opencode-worktree\\.sh\\.tmpl$/"], - "matchStrings": ["repo=https://github.com/(?[\\w.-]+/[\\w.-]+)\\.git\\nrev=(?[a-f0-9]{40})"], - "currentValueTemplate": "{{currentDigest}}", - "datasourceTemplate": "git-refs", - "registryUrlTemplate": "https://github.com/{{packageName}}.git", - "versioningTemplate": "git" - }, { "customType": "regex", "description": "Pinned zsh plugin revisions", diff --git a/run_once_install-omo-codex.sh.tmpl b/run_once_install-omo-codex.sh.tmpl deleted file mode 100644 index 81872ac..0000000 --- a/run_once_install-omo-codex.sh.tmpl +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# run_once_install-omo-codex.sh.tmpl -# Installs oh-my-openagent (Light Edition for Codex CLI) via lazycodex-ai. -# Runs once per machine. Re-run by deleting chezmoi script state: -# chezmoi state delete-bucket --bucket=scriptState - -set -euo pipefail - -# Check if Codex CLI is installed -if ! command -v codex &>/dev/null; then - echo "⚠️ Codex CLI not found. Skipping OmO install." - echo " Install Codex CLI first: https://github.com/openai/codex" - exit 0 -fi - -# Check if OmO is already installed -if [ -d "$HOME/.codex/plugins/cache/sisyphuslabs/omo" ]; then - echo "✓ oh-my-openagent (Codex) already installed" - exit 0 -fi - -echo "Installing oh-my-openagent for Codex CLI..." -npx lazycodex-ai install --no-tui --codex-autonomous - -echo "✓ oh-my-openagent installed" diff --git a/run_onchange_install-opencode-worktree.sh.tmpl b/run_onchange_install-opencode-worktree.sh.tmpl deleted file mode 100644 index abca7d8..0000000 --- a/run_onchange_install-opencode-worktree.sh.tmpl +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -set -eu - -repo=https://github.com/phall1/opencode-worktree.git -rev=efbf8452f0202c983e56aba8207772d0a8366d96 -dest=${XDG_DATA_HOME:-"$HOME/.local/share"}/opencode-worktree - -mkdir -p "${dest%/*}" -if [ ! -d "$dest/.git" ]; then - git clone --quiet "$repo" "$dest" -elif [ "$(git -C "$dest" remote get-url origin)" != "$repo" ]; then - printf 'opencode-worktree: unexpected origin in %s\n' "$dest" >&2 - exit 1 -fi - -if [ "$(git -C "$dest" rev-parse HEAD)" != "$rev" ]; then - git -C "$dest" fetch --quiet origin "$rev" - git -C "$dest" checkout --quiet --detach "$rev" -fi diff --git a/run_onchange_install-phux-update-check.sh.tmpl b/run_onchange_install-phux-update-check.sh.tmpl index 5c376f2..542898b 100644 --- a/run_onchange_install-phux-update-check.sh.tmpl +++ b/run_onchange_install-phux-update-check.sh.tmpl @@ -7,6 +7,10 @@ set -euo pipefail +{{ if and (hasKey . "services") (not .services) }} +exit 0 # Explicit container/offline profile; never touch the host GUI domain. +{{ end }} + if ! command -v launchctl >/dev/null 2>&1; then printf "phux-update-check: launchctl not found — skipping (non-macOS host).\n" >&2 exit 0 diff --git a/run_onchange_zcompile.sh.tmpl b/run_onchange_zcompile.sh.tmpl index 22b62a8..66a408e 100644 --- a/run_onchange_zcompile.sh.tmpl +++ b/run_onchange_zcompile.sh.tmpl @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Re-runs whenever the hash below changes, i.e., when any of these source -# files change. Pre-compiles zsh bytecode for faster startup. +# First-install seed compilation. Native-history bootstrap and restore hooks +# compile live preferences after ownership moves out of chezmoi. # # {{ include "dot_zshrc" | sha256sum }} # {{ include "dot_zshenv" | sha256sum }} @@ -8,4 +8,6 @@ set -euo pipefail +{{ if not (get . "history" | default false) }} "{{ .chezmoi.sourceDir }}/dot_local/bin/executable_dot-zcompile" +{{ end }} diff --git a/scripts/bootstrap-darwin.sh b/scripts/bootstrap-darwin.sh index 04aed52..28ea5a7 100755 --- a/scripts/bootstrap-darwin.sh +++ b/scripts/bootstrap-darwin.sh @@ -1,130 +1,8 @@ #!/usr/bin/env bash -# bootstrap-darwin.sh — install host-level dependencies on macOS. -# Idempotent. Re-run any time. - set -euo pipefail - -# Brew itself. -if ! command -v brew >/dev/null 2>&1; then - echo "Installing Homebrew…" - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - eval "$(/opt/homebrew/bin/brew shellenv)" +[[ "$(uname -s)" == Darwin ]] || { echo 'This entrypoint requires macOS.' >&2; exit 1; } +if ! command -v brew >/dev/null; then + /bin/bash -c "$(curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + eval "$(/opt/homebrew/bin/brew shellenv)" fi - -install_formula() { - local formula="$1" - - if brew list --formula "$formula" >/dev/null 2>&1; then - echo " ok: $formula" - else - brew install "$formula" - fi -} - -install_cask_app() { - local cask="$1" - shift - - if brew list --cask "$cask" >/dev/null 2>&1; then - echo " ok: $cask" - return - fi - - for app_path in "$@"; do - if [[ -e "$app_path" ]]; then - echo " ok: $cask ($app_path already exists)" - return - fi - done - - brew install --cask "$cask" -} - -# Required substrate. -brew_formulae=( - # Core tools - chezmoi age stow - # Shell substrate - atuin fzf fd eza git-delta zoxide bat ripgrep jq yq - # Language toolchains - uv fnm rustup-init - # Terminal stack - ghostty tmux sesh - # AI agent multiplexer (self-manages its Claude/opencode hooks via - # `herdr integration install` — see the integration step below). - herdr - # Editor + git and local CI workflow - neovim gh git tig gitui lazygit act - # Misc - direnv coreutils -) -brew_casks=( - ghostty - font-jetbrains-mono-nerd-font -) - -echo "Installing brew formulae..." -for formula in "${brew_formulae[@]}"; do - install_formula "$formula" -done - -# Ensure an LTS Node/npm exists before provisioning the shared agent stack. -eval "$(fnm env --shell bash)" -if ! command -v npm >/dev/null 2>&1; then - fnm install --lts --use - fnm default "$(fnm current)" - eval "$(fnm env --shell bash)" -fi -"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/install-agent-stack.sh" - -# herdr owns its own agent-state hooks in ~/.claude/settings.json (and the -# opencode plugin). We deliberately do NOT track those hooks in chezmoi — they -# are versioned by herdr and regenerated here. The chezmoi modify_ script for -# settings.json merges our portable flags on top without clobbering them. -if command -v herdr >/dev/null 2>&1; then - herdr integration install claude >/dev/null 2>&1 || true -fi - -# coreutils for GNU versions on macOS (zprofile prepends them to PATH). -brew list coreutils >/dev/null 2>&1 || brew install coreutils -echo "Installing brew casks..." -for cask in "${brew_casks[@]}"; do - case "$cask" in - ghostty) install_cask_app "$cask" "/Applications/Ghostty.app" "$HOME/Applications/Ghostty.app" ;; - *) install_cask_app "$cask" ;; - esac -done - -cat <<'EOF' - -========================================================================== - ✓ Host bootstrap complete (Darwin) -========================================================================== - -Next steps — copy/paste these in order: - - 1. Configure this machine's git identity (interactive): - - ~/dotfiles/scripts/setup-chezmoi.sh - - 2. Apply dotfiles to $HOME (idempotent; safe to re-run): - - chezmoi apply - - 3. Verify the substrate is healthy: - - ~/.local/bin/dot-doctor # 0 failures expected - ~/.local/bin/dot-bench # numbers under PERF.md baselines - - 4. Restart your shell: - - exec zsh - -Daily flow from here: - $EDITOR ~/dotfiles/dot_zshrc # source-of-truth lives in ~/dotfiles - chezmoi diff # preview what would change - chezmoi apply # propagate to $HOME - -Full guide: ~/dotfiles/docs/setup.md -========================================================================== -EOF +exec bash "$(dirname "${BASH_SOURCE[0]}")/bootstrap.sh" "$@" diff --git a/scripts/bootstrap-linux.sh b/scripts/bootstrap-linux.sh index 43d6e0d..017b9cf 100755 --- a/scripts/bootstrap-linux.sh +++ b/scripts/bootstrap-linux.sh @@ -1,117 +1,11 @@ #!/usr/bin/env bash -# bootstrap-linux.sh — install host-level dependencies on Linux (Pi, Debian, Ubuntu). -# Idempotent. Re-run any time. Falls back to nix when apt packages aren't available. - set -euo pipefail - -# apt is mandatory — this is for debian/ubuntu/raspberry pi os -if ! command -v apt-get >/dev/null 2>&1; then - echo "ERROR: bootstrap-linux.sh expects apt. For other distros, install equivalents manually." - exit 1 -fi - +command -v apt-get >/dev/null || { echo 'This entrypoint requires Debian/Ubuntu.' >&2; exit 1; } sudo apt-get update - -# Packages available in default Debian/Ubuntu/Raspbian repos. -apt_packages=( - zsh git curl - fd-find bat ripgrep jq - age - tmux neovim - build-essential pkg-config libssl-dev -) -echo "Installing apt packages..." -sudo apt-get install -y "${apt_packages[@]}" - -# fd-find on debian installs as `fdfind` — symlink to `fd` for consistency. -if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then - mkdir -p "$HOME/.local/bin" - ln -sf "$(command -v fdfind)" "$HOME/.local/bin/fd" -fi -# Same dance for batcat → bat. -if command -v batcat >/dev/null 2>&1 && ! command -v bat >/dev/null 2>&1; then - mkdir -p "$HOME/.local/bin" - ln -sf "$(command -v batcat)" "$HOME/.local/bin/bat" -fi - -# Tools that don't ship via apt or ship outdated — use Nix or upstream installers. -if ! command -v nix >/dev/null 2>&1; then - echo "Installing Determinate Nix..." - curl -fsSL https://install.determinate.systems/nix | sh -s -- install --no-confirm - source /etc/profile.d/nix.sh 2>/dev/null \ - || source "$HOME/.nix-profile/etc/profile.d/nix.sh" \ - || true -fi - -# Via Nix — version controlled, reproducible. -nix_packages=( - chezmoi atuin fzf eza delta zoxide yq-go - uv fnm gh - sesh tig gitui lazygit -) -for pkg in "${nix_packages[@]}"; do - command -v "$pkg" >/dev/null 2>&1 \ - || nix profile install "nixpkgs#${pkg}" \ - || echo " (nix install failed for $pkg — install manually if needed)" -done - -# Ensure an LTS Node/npm exists before provisioning the shared agent stack. -eval "$(fnm env --shell bash)" -if ! command -v npm >/dev/null 2>&1; then - fnm install --lts --use - fnm default "$(fnm current)" - eval "$(fnm env --shell bash)" -fi -"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/install-agent-stack.sh" - -# Rust (Pi binaries: prefer rustup direct over apt's old rustc). -if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable -fi - -# Set zsh as default shell. -if [[ "$SHELL" != *zsh ]]; then - echo "Setting zsh as default shell — you may be prompted for password." - chsh -s "$(command -v zsh)" -fi - -cat <<'EOF' - -========================================================================== - ✓ Host bootstrap complete (Linux) -========================================================================== - -Next steps — copy/paste these in order: - - 1. Configure this machine's git identity (interactive): - - ~/dotfiles/scripts/setup-chezmoi.sh - - 2. Apply dotfiles to $HOME (idempotent; safe to re-run): - - chezmoi apply - - 3. Verify the substrate is healthy: - - ~/.local/bin/dot-doctor # platform-aware; Pi-specific checks fire - ~/.local/bin/dot-bench # target: first_prompt_lag < 150ms on Pi - - 4. Sign into GitHub (per-machine OAuth, not in the repo): - - gh auth login - - 5. Restart your shell: - - exec zsh - -If anything in dot-doctor is yellow on a Pi (ghostty, raycast), that's -expected — those are Mac-only and don't apply. - -Daily flow: - $EDITOR ~/dotfiles/dot_zshrc # source-of-truth lives in ~/dotfiles - chezmoi diff # preview - chezmoi apply # propagate - -Full guide: ~/dotfiles/docs/setup.md -========================================================================== -EOF +sudo apt-get install -y ca-certificates curl git xz-utils unzip +version="$(git --version | awk '{print $3}')" +dpkg --compare-versions "$version" ge 2.45.1 || { + echo 'Git >=2.45.1 is required by Phig. Use Debian 13 / Raspberry Pi OS Trixie (64-bit), or provision a supported Git first.' >&2 + exit 1 +} +exec bash "$(dirname "${BASH_SOURCE[0]}")/bootstrap.sh" "$@" diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..6afbe50 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Seed only. All workstation inventory and orchestration lives in mise. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export PATH="$HOME/.local/bin:$PATH" +export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" +export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" +if ! command -v mise >/dev/null; then + curl --proto '=https' --tlsv1.2 -fsSL https://mise.run | MISE_VERSION=2026.9.3 sh +fi +for config in "$root"/mise*.toml; do mise trust "$config"; done +exec mise -C "$root" bootstrap "$@" diff --git a/scripts/bootstrap/configure.py b/scripts/bootstrap/configure.py new file mode 100755 index 0000000..29f1a7a --- /dev/null +++ b/scripts/bootstrap/configure.py @@ -0,0 +1,73 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["tomlkit==0.13.3"] +# /// +"""Persist machine-local selections without replacing identity or secret data.""" +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + +import tomlkit + +HARNESSES = ("opencode", "pi", "claude", "hermes", "goose", "grok") + + +def harness_name(value: str) -> str: + if value not in HARNESSES: + raise argparse.ArgumentTypeError(f"unknown harness {value!r}; choose from {', '.join(HARNESSES)}") + return value + + +def git_identity(key: str) -> str: + result = subprocess.run(["git", "config", "--global", "--get", key], capture_output=True, text=True) + return result.stdout.strip() + + +def write_private_config(path: Path, config: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=path.parent, delete=False) as stream: + temporary = Path(stream.name) # tempfile creates it private before writing + try: + stream.write(tomlkit.dumps(config)) + stream.flush() + os.fsync(stream.fileno()) + temporary.replace(path) + finally: + temporary.unlink(missing_ok=True) + + +def configure(args: argparse.Namespace) -> None: + home = Path.home() + source = Path(__file__).resolve().parents[2] + path = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config")) / "chezmoi/chezmoi.toml" + config = tomlkit.parse(path.read_text()) if path.exists() else tomlkit.document() + config["sourceDir"] = str(source) + data = config.setdefault("data", tomlkit.table()) + # Adoption restores native history configuration before the machine-local + # chezmoi data exists. Protect incoming live edits on that very first apply. + if (path.parent.parent / "mise/conf.d/dotfiles-history.toml").exists(): + data["history"] = True + identity = data.setdefault("git", tomlkit.table()) + identity.setdefault("name", git_identity("user.name")) + identity.setdefault("email", git_identity("user.email")) + if not identity["name"] or not identity["email"]: + raise SystemExit("Set git config --global user.name and user.email before bootstrap.") + selected = args.harnesses or data.get("harnesses", ["opencode"]) + environments = os.environ.get("MISE_ENV", "").split(",") + selected = [*selected, *(name for name in environments if name in HARNESSES)] + data["harnesses"] = list(dict.fromkeys(["opencode", *selected])) + if args.services is not None: + data["services"] = args.services == "on" + data.setdefault("services", "container" not in os.environ.get("MISE_ENV", "").split(",")) + write_private_config(path, config) + print("Configured harnesses:", ", ".join(data["harnesses"])) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("harnesses", nargs="*", type=harness_name) + parser.add_argument("--services", choices=("on", "off")) + configure(parser.parse_args()) diff --git a/scripts/bootstrap/configure.sh b/scripts/bootstrap/configure.sh new file mode 100755 index 0000000..cb2108d --- /dev/null +++ b/scripts/bootstrap/configure.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +exec uv run --script "$root/scripts/bootstrap/configure.py" "$@" diff --git a/scripts/bootstrap/converge.sh b/scripts/bootstrap/converge.sh new file mode 100755 index 0000000..ae14dd4 --- /dev/null +++ b/scripts/bootstrap/converge.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Invoked only after mise's tools phase. No tasks depend on each other in +# parallel: configuration must precede application registration. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +export PATH="$HOME/.local/bin:${XDG_DATA_HOME:-$HOME/.local/share}/mise/shims:$HOME/.cargo/bin:$PATH" +bash "$root/scripts/bootstrap/configure.sh" +bash "$root/scripts/bootstrap/retire-legacy.sh" +chezmoi diff --source "$root" +chezmoi apply --source "$root" +mise trust "${XDG_CONFIG_HOME:-$HOME/.config}/mise/conf.d/dotfiles.toml" +# Install the selected optional inventories from the rendered global config. +mise -C "$HOME" install --yes +bash "$root/scripts/bootstrap/refresh-shell.sh" +bash "$root/scripts/bootstrap/integrations.sh" +# Optional native installers can seed config. Reconcile managed keys while +# preserving their runtime fields before the final drift gate. +chezmoi diff --source "$root" +chezmoi apply --source "$root" +bash "$root/scripts/bootstrap/history.sh" +"$HOME/.local/bin/dot-zcompile" +bash "$root/scripts/bootstrap/verify.sh" diff --git a/scripts/bootstrap/harnesses.sh b/scripts/bootstrap/harnesses.sh new file mode 100755 index 0000000..a33a1f7 --- /dev/null +++ b/scripts/bootstrap/harnesses.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Optional native installers retain their own update and credential contracts. +set -euo pipefail +profile="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/profile.json" + +run_installer() ( + set -euo pipefail + local url="$1" installer + shift + installer="$(mktemp "${TMPDIR:-/tmp}/dotfiles-installer.XXXXXX")" + trap 'rm -f "$installer"' EXIT + curl --proto '=https' --tlsv1.2 -fsSL "$url" -o "$installer" + bash "$installer" "$@" +) + +install_hermes() ( + set -euo pipefail + local installer stage + installer="$(mktemp "${TMPDIR:-/tmp}/dotfiles-hermes.XXXXXX")" + trap 'rm -f "$installer"' EXIT + curl --proto '=https' --tlsv1.2 -fsSL https://hermes-agent.nousresearch.com/install.sh -o "$installer" + # Its monolithic installer can start a messaging gateway. Explicit stages + # provision the CLI without an auth flow, gateway or shell-rc rewrite. + for stage in prerequisites repository venv python-deps node-deps config complete; do + bash "$installer" --stage "$stage" --non-interactive \ + --commit 4281151ae859241351ba14d8c7682dc67ff4c126 \ + --skip-setup --skip-browser --skip-computer-use + done + ln -s "$HOME/.hermes/hermes-agent/venv/bin/hermes" "$HOME/.local/bin/hermes" +) + +install_harness() { + command -v "$1" >/dev/null && return + case "$1" in + claude) run_installer https://claude.ai/install.sh 2.1.260 ;; + goose) CONFIGURE=false GOOSE_VERSION=v1.44.0 GOOSE_BIN_DIR="$HOME/.local/bin" \ + run_installer https://github.com/aaif-goose/goose/releases/download/v1.44.0/download_cli.sh ;; + grok) SHELL=/bin/sh GROK_BIN_DIR="$HOME/.local/bin" run_installer https://x.ai/cli/install.sh 1.0.13 ;; + hermes) install_hermes ;; + pi) echo 'Pi is selected but missing from the global mise inventory.' >&2; return 1 ;; + *) echo "Unknown harness: $1" >&2; return 1 ;; + esac +} + +while IFS= read -r harness; do install_harness "$harness"; done < <(jq -r '.harnesses[] | select(. != "opencode")' "$profile") diff --git a/scripts/bootstrap/history.py b/scripts/bootstrap/history.py new file mode 100755 index 0000000..c383be2 --- /dev/null +++ b/scripts/bootstrap/history.py @@ -0,0 +1,73 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["tomlkit==0.13.3"] +# /// +"""Seed native mise history and relinquish chezmoi ownership before capture.""" +import json +import os +from pathlib import Path + +import tomlkit + +from configure import write_private_config + + +def tracking_entries(manifest: dict) -> dict: + entries = {f"~/{path}": {"mode": "track"} for path in manifest["shared"]} + for path in manifest["macos"]: + entries[f"~/{path}"] = {"mode": "track", "variants": [{"os": "macos"}]} + entries["~/.config/mise/conf.d/dotfiles-history.toml"] = {"mode": "track"} + return entries + + +def history_config(manifest: dict) -> dict: + return { + "min_version": "2026.9.3", + "settings": {"history": {"sync": "sync"}}, + "history": { + "exclude": manifest["exclude"], + "reload": {"~/.zsh*": "~/.local/bin/dot-zcompile", "~/.p10k.zsh": "~/.local/bin/dot-zcompile"}, + }, + "dotfiles": tracking_entries(manifest), + "bootstrap": { + "repos": {"~/dotfiles": {"url": "https://github.com/phall1/dotfiles.git", "ref": "feat/mise-workstation"}}, + "services": {"mise-history": {"builtin": "history-watch", "environment": {"MISE_CACHE_DIR": "~/.cache/mise"}}}, + }, + "tasks": {"bootstrap": {"run": 'mise -C "$HOME/dotfiles" bootstrap --yes'}}, + } + + +def watcher_directories(home: Path, config_home: Path) -> dict: + roots = { + "MISE_CONFIG_DIR": config_home / "mise", + "MISE_CACHE_DIR": Path(os.environ.get("XDG_CACHE_HOME", home / ".cache")) / "mise", + "MISE_DATA_DIR": Path(os.environ.get("XDG_DATA_HOME", home / ".local/share")) / "mise", + "MISE_STATE_DIR": Path(os.environ.get("XDG_STATE_HOME", home / ".local/state")) / "mise", + } + return {key: os.environ.get(key, str(path)) for key, path in roots.items()} + + +def enable_history(root: Path, home: Path) -> None: + config_home = Path(os.environ.get("XDG_CONFIG_HOME", home / ".config")) + destination = config_home / "mise/conf.d/dotfiles-history.toml" + manifest = json.loads((root / "provision/dotfiles-history.json").read_text()) + # After enrollment this file is live-owned too. Never overwrite user edits + # or tracking decisions arriving from another machine on a repeated run. + if not destination.exists(): + write_private_config(destination, history_config(manifest)) + path = config_home / "chezmoi/chezmoi.toml" + config = tomlkit.parse(path.read_text()) + config["data"]["history"] = True + write_private_config(path, config) + # Service selection is machine-local and excluded from shared history. + state = "running" if config["data"].get("services", True) else "absent" + service = {"bootstrap": {"services": {"mise-history": { + "builtin": "history-watch", "state": state, + "environment": watcher_directories(home, config_home), + }}}} + write_private_config(config_home / "mise/conf.d/zz-dotfiles-services.local.toml", service) + + +if __name__ == "__main__": + enable_history(Path(__file__).resolve().parents[2], Path.home()) diff --git a/scripts/bootstrap/history.sh b/scripts/bootstrap/history.sh new file mode 100755 index 0000000..0230f99 --- /dev/null +++ b/scripts/bootstrap/history.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Enrollment is local. An origin is connected explicitly after reviewing the +# first checkpoint; the native watcher owns all later saves and synchronization. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +uv run --quiet --script "$root/scripts/bootstrap/history.py" +config="${XDG_CONFIG_HOME:-$HOME/.config}/mise/conf.d/dotfiles-history.toml" +mise trust "$config" +chezmoi diff "$HOME/.config/dotfiles/profile.json" +chezmoi apply "$HOME/.config/dotfiles/profile.json" +# The enclosing native bootstrap holds its history transaction until exit. +# Its watcher captures the enrolled files after that transaction completes. +# Reconcile both running and absent immediately using our machine-local global +# declaration. System and HOME project configuration retain native precedence. +MISE_GLOBAL_CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/mise/conf.d/zz-dotfiles-services.local.toml" \ + mise -C "$HOME" bootstrap services apply --yes diff --git a/scripts/bootstrap/integrations.sh b/scripts/bootstrap/integrations.sh new file mode 100755 index 0000000..08d2c00 --- /dev/null +++ b/scripts/bootstrap/integrations.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Product installers own registration and services. A healthy live service is +# never reinstalled simply because bootstrap ran again. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +profile="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/profile.json" +export PATH="$HOME/.local/bin:${XDG_DATA_HOME:-$HOME/.local/share}/mise/shims:$HOME/.cargo/bin:$PATH" + +install_opencode() { + command -v opencode2 >/dev/null && return + # V2 requires the official package's native-binary postinstall. It updates + # itself thereafter, outside mise's immutable tool-version directories. + npm install --global --prefix "$HOME/.local" '@opencode-ai/cli@0.0.0-beta-19157' +} + +install_rust() { + if ! command -v rustup >/dev/null; then + curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable + fi + if ! rustup show active-toolchain >/dev/null 2>&1; then rustup default stable; fi + cargo install --path "$root/src/lstags" --locked --quiet +} + +install_pi_extensions() { + jq -r '.packages[] | if type == "object" then .source else . end | select(type == "string")' "$HOME/.pi/agent/settings.json" | + while IFS= read -r package; do pi install "$package"; done +} + +install_opencode +install_rust +bash "$root/scripts/bootstrap/harnesses.sh" +if jq -e '.harnesses | index("pi")' "$profile" >/dev/null; then install_pi_extensions; fi +bash "$root/scripts/bootstrap/services.sh" diff --git a/scripts/bootstrap/packages.sh b/scripts/bootstrap/packages.sh new file mode 100755 index 0000000..5b48f70 --- /dev/null +++ b/scripts/bootstrap/packages.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Runs before mise installs tools, so only seed-host commands are available. +set -euo pipefail +[[ "$(uname -s)" == Darwin ]] || exit 0 +command -v brew >/dev/null || { echo 'Homebrew is required: run scripts/bootstrap-darwin.sh first.' >&2; exit 1; } +export HOMEBREW_NO_AUTO_UPDATE=1 +brew bundle --file=provision/Brewfile --no-upgrade +case ",${MISE_ENV:-}," in + *,server,*|*,container,*) ;; + *) brew bundle --file=provision/Brewfile.desktop --no-upgrade ;; +esac diff --git a/scripts/bootstrap/refresh-shell.sh b/scripts/bootstrap/refresh-shell.sh new file mode 100755 index 0000000..83bd1f5 --- /dev/null +++ b/scripts/bootstrap/refresh-shell.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Resolve stable shims outside startup so upgrades invalidate cached init code. +set -euo pipefail +cache="${XDG_CACHE_HOME:-$HOME/.cache}/zsh/evalcache" +mkdir -p "$cache" +for tool in zoxide fzf; do + resolved="$(mise which "$tool")" + stamp="$cache/$tool.tool" + if [[ ! -f "$stamp" ]] || [[ "$(cat "$stamp")" != "$resolved" ]]; then + rm -f "$cache/$tool.zsh" + printf '%s\n' "$resolved" > "$stamp" + fi +done diff --git a/scripts/bootstrap/retire-legacy.sh b/scripts/bootstrap/retire-legacy.sh new file mode 100755 index 0000000..c63353c --- /dev/null +++ b/scripts/bootstrap/retire-legacy.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Source removal alone leaves previously materialized files behind. Retire only +# exact known copies; a local edit must be reviewed rather than erased. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +checksum=(sha256sum) +if [[ "$(uname -s)" == Darwin ]]; then checksum=(shasum -a 256); fi +while read -r expected relative; do + target="$HOME/$relative" + [[ -e "$target" ]] || continue + actual="$("${checksum[@]}" "$target" | awk '{print $1}')" + if [[ -L "$target" || "$actual" != "$expected" ]]; then + echo "Retired file has local changes; move it aside after review: $target" >&2 + exit 1 + fi + rm "$target" +done < "$root/provision/retired-files.sha256" diff --git a/scripts/bootstrap/services.sh b/scripts/bootstrap/services.sh new file mode 100755 index 0000000..6c3ca7a --- /dev/null +++ b/scripts/bootstrap/services.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Initial installation and health diagnosis are separate native operations. +set -euo pipefail +profile="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/profile.json" +jq -e '.services' "$profile" >/dev/null || exit 0 + +blackbird_is_absent() { + local socket_rc=0 process_rc=0 + # curl 7 is connection refusal, unlike a timeout or an answering error page. + # The process probe also catches a legacy daemon on a different local port. + curl --silent --output /dev/null --connect-timeout 2 --max-time 2 http://127.0.0.1:8080/health || socket_rc=$? + pgrep -x blackbird >/dev/null || process_rc=$? + [[ "$socket_rc" == 7 && "$process_rc" == 1 ]] +} + +reconcile_blackbird() { + local service report rc=0 + service="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/blackbird.service" + if [[ "$(uname -s)" == Darwin ]]; then service="$HOME/Library/LaunchAgents/com.phall1.blackbird.plist"; fi + report="$(blackbird doctor --json)" || rc=$? + printf '%s' "$report" | jq -e '.checks | type == "array"' >/dev/null + if [[ -e "$service" ]]; then + (( rc == 0 )) && return 0 + echo 'Existing Blackbird installation needs attention: run blackbird doctor. Its daemon was not restarted.' >&2 + return 1 + fi + if printf '%s' "$report" | jq -e 'any(.checks[]; .name == "database.schema" and .status == "fail")' >/dev/null; then + echo 'Blackbird has an incompatible database. Review blackbird doctor before native registration.' >&2 + return 1 + fi + blackbird_is_absent || { + echo 'Blackbird absence could not be established. Review blackbird doctor before native registration; existing processes were retained.' >&2 + return 1 + } + blackbird install +} + +disable_linux_brew_updater() { + [[ "$(uname -s)" == Linux ]] || return 0 + systemctl --user cat blackbird-update.timer >/dev/null 2>&1 || return 0 + systemctl --user disable --now blackbird-update.timer +} + +reconcile_phux() { + local service="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/phux.service" + if [[ "$(uname -s)" == Darwin ]]; then service="$HOME/Library/LaunchAgents/com.phux.server.plist"; fi + phux service status >/dev/null 2>&1 && return 0 + if [[ -e "$service" ]]; then + echo 'Existing Phux service needs attention: run phux service status. Its unit and live panes were retained.' >&2 + return 1 + fi + phux service install --adopt +} + +reconcile_blackbird +disable_linux_brew_updater +# --adopt registers supervision without killing an incumbent's panes. +reconcile_phux diff --git a/scripts/bootstrap/verify.sh b/scripts/bootstrap/verify.sh new file mode 100755 index 0000000..428774b --- /dev/null +++ b/scripts/bootstrap/verify.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" +for tool in mise node bun uv chezmoi jq yq zsh git opencode2 blackbird phux phux-mcp phig phui lstags; do + command -v "$tool" >/dev/null || { echo "Missing required executable: $tool" >&2; exit 1; } +done +opencode2 --version +phui --version +phux config check +phig config check +# Config parsing does not exercise Phig's Git minimum. Test an actual repository +# operation even when this source is an archive rather than a Git checkout. +repository="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-git-check.XXXXXX")" +trap 'rm -rf "$repository"' EXIT +git init --quiet "$repository" +phig --repo "$repository" snapshot status >/dev/null +profile="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/profile.json" +while IFS= read -r tool; do + command -v "$tool" >/dev/null || { echo "Selected harness missing: $tool" >&2; exit 1; } +done < <(jq -r '.harnesses[] | select(. != "opencode")' "$profile") +chezmoi verify --exclude scripts --source "$root" +rc=0 +DOTFILES="$root" bash "$root/dot_local/bin/executable_dot-doctor" || rc=$? +(( rc < 2 )) diff --git a/scripts/install-agent-stack.sh b/scripts/install-agent-stack.sh index 57400b7..2976c9a 100755 --- a/scripts/install-agent-stack.sh +++ b/scripts/install-agent-stack.sh @@ -1,82 +1,5 @@ #!/usr/bin/env bash -# Install the portable command-line agent stack. Runtime credentials/state stay local. +# Compatibility entrypoint: use the same inventory and ownership as bootstrap. set -euo pipefail - -PI_VERSION=0.84.1 -WEB_VERSION=2.1.11 -BLACKBIRD_VERSION=0.4.1 -NPM_PREFIX="${NPM_CONFIG_PREFIX:-$HOME/.npm-global}" - -if ! command -v npm >/dev/null 2>&1; then - echo "ERROR: npm is required. Install an LTS Node with fnm, then rerun." >&2 - exit 1 -fi - -mkdir -p "$NPM_PREFIX" "$HOME/.local/bin" -npm install --global --ignore-scripts --prefix "$NPM_PREFIX" \ - "@earendil-works/pi-coding-agent@$PI_VERSION" \ - "open-websearch@$WEB_VERSION" - -pi_bin="$NPM_PREFIX/bin/pi" -for package in \ - npm:blackbird-pi@0.1.1 \ - npm:pi-subagents@0.47.1 \ - npm:@juicesharp/rpiv-ask-user-question@2.4.0 \ - npm:@narumitw/pi-goal@0.51.0 \ - npm:@ff-labs/pi-fff@0.10.3 \ - npm:pi-mcp-adapter@2.23.0 \ - npm:pi-web-access@0.22.0 -do - "$pi_bin" install "$package" -done - -install_blackbird_linux_release() ( - local arch target base archive tmp - arch="$(uname -m)" - case "$arch" in - aarch64|arm64) target=aarch64-unknown-linux-gnu ;; - x86_64|amd64) target=x86_64-unknown-linux-gnu ;; - *) echo "ERROR: unsupported Linux architecture for Blackbird: $arch" >&2; return 1 ;; - esac - base="https://github.com/phall1/blackbird/releases/download/v${BLACKBIRD_VERSION}" - archive="blackbird-v${BLACKBIRD_VERSION}-${target}.tar.gz" - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT - curl -fsSL "$base/$archive" -o "$tmp/$archive" - curl -fsSL "$base/$archive.sha256" -o "$tmp/$archive.sha256" - (cd "$tmp" && sha256sum -c "$archive.sha256") - tar -xzf "$tmp/$archive" -C "$tmp" - install -m 0755 "$(find "$tmp" -type f -name blackbird -print -quit)" "$HOME/.local/bin/blackbird" -) - -case "$(uname -s)" in - Darwin) - command -v brew >/dev/null 2>&1 || { echo "ERROR: Homebrew is required for Blackbird on Darwin" >&2; exit 1; } - brew tap phall1/tap - if brew list --formula blackbird >/dev/null 2>&1; then brew upgrade blackbird; else brew install blackbird; fi - ;; - Linux) - if command -v brew >/dev/null 2>&1; then - brew tap phall1/tap - if brew list --formula blackbird >/dev/null 2>&1; then brew upgrade blackbird; else brew install blackbird; fi - else - install_blackbird_linux_release - fi - ;; - *) echo "ERROR: unsupported operating system: $(uname -s)" >&2; exit 1 ;; -esac - -# Idempotently install/start the service and supported client registrations. -blackbird install - -if [[ "$(uname -s)" == Linux ]] && ! command -v brew >/dev/null 2>&1; then - # Release installs cannot use the Homebrew-dependent updater. Unit names are - # intentionally tolerant across systemd user-service revisions. - systemctl --user disable --now blackbird-update.timer blackbird-update.service 2>/dev/null || true - rm -f "$HOME/.config/systemd/user/blackbird-update.timer" \ - "$HOME/.config/systemd/user/blackbird-update.service" - systemctl --user daemon-reload 2>/dev/null || true -fi - -actual_blackbird="$(blackbird --version 2>/dev/null || printf unknown)" -echo "Agent stack installed: Pi $PI_VERSION, Open WebSearch $WEB_VERSION, Blackbird $actual_blackbird" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec mise -C "$root" bootstrap "$@" diff --git a/scripts/onboard.sh b/scripts/onboard.sh new file mode 100755 index 0000000..ddd8809 --- /dev/null +++ b/scripts/onboard.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Standalone entrypoint: it can be downloaded before the checkout exists. +set -euo pipefail +export PATH="$HOME/.local/bin:$PATH" +export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" +export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" +# Do not inherit an unrelated project's mise configuration or trust prompts. +cd "$HOME" + +require_command() { + command -v "$1" >/dev/null 2>&1 && return 0 + printf 'Missing prerequisite: %s. Install it, then rerun onboarding.\n' "$1" >&2 + exit 1 +} + +check_git_identity() { + local key + for key in name email; do + if [[ -z "$(git config --global --get "user.$key" || true)" ]]; then + printf 'Git identity is missing: run git config --global user.%s , then rerun onboarding.\n' "$key" >&2 + exit 1 + fi + done +} + +check_prerequisites() { + local tool + for tool in git gh mise; do require_command "$tool"; done + if [[ "$(uname -s)" == Darwin ]]; then require_command brew; fi + if ! mise bootstrap --help | grep -q -- '--adopt'; then + echo 'Mise 2026.9.3 or newer is required. Update mise, then rerun onboarding.' >&2 + exit 1 + fi + check_git_identity + if ! gh auth status --hostname github.com >/dev/null 2>&1; then + echo 'GitHub login needs attention: run gh auth login --hostname github.com, then rerun onboarding.' >&2 + exit 1 + fi +} + +verify_setup() { + local doctor_rc=0 bench_rc=0 + "$HOME/.local/bin/dot-doctor" || doctor_rc=$? + "$HOME/.local/bin/dot-bench" || bench_rc=$? + if (( doctor_rc > 1 || bench_rc != 0 )); then + echo 'Setup is installed, but validation needs attention. Resolve the checks above and rerun this command.' >&2 + return 1 + fi + if (( doctor_rc == 1 )); then + echo 'Onboarding complete with the doctor warnings shown above.' + else + echo 'Onboarding complete. Health and performance checks passed.' + fi + echo 'Open a new terminal. Enrolled preferences now autosave and synchronize between your machines.' +} + +echo '[1/3] Checking your existing tools and GitHub login' +check_prerequisites + +echo '[2/3] Restoring shared preferences and provisioning this machine' +mise bootstrap --adopt phall1/dotfiles-history --yes +# This is outside bootstrap's history transaction; explicit saves cannot nest. +mise bootstrap dotfiles save +mise bootstrap dotfiles sync +mise bootstrap dotfiles status + +echo '[3/3] Checking the installed setup' +export PATH="$HOME/.local/bin:${MISE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/mise}/shims:$HOME/.cargo/bin:$PATH" +verify_setup diff --git a/scripts/setup-chezmoi.sh b/scripts/setup-chezmoi.sh index c031f26..427981a 100755 --- a/scripts/setup-chezmoi.sh +++ b/scripts/setup-chezmoi.sh @@ -1,60 +1,5 @@ #!/usr/bin/env bash -# setup-chezmoi.sh — interactive per-machine chezmoi.toml writer. -# Writes ~/.config/chezmoi/chezmoi.toml with git identity for THIS machine. -# Idempotent: prompts before overwriting an existing config. - +# Compatibility entrypoint: preserve existing machine data through one writer. set -euo pipefail - -CONFIG_DIR="$HOME/.config/chezmoi" -CONFIG_FILE="$CONFIG_DIR/chezmoi.toml" - -BLD=$'\033[1m'; DIM=$'\033[2m'; GRN=$'\033[32m'; YLW=$'\033[33m'; RST=$'\033[0m' - -echo "${BLD}chezmoi per-machine setup${RST}" -echo "${DIM}writes $CONFIG_FILE${RST}" -echo - -if [[ -f "$CONFIG_FILE" ]]; then - echo "${YLW}Existing config:${RST}" - sed 's/^/ /' "$CONFIG_FILE" - echo - read -p "Overwrite? [y/N] " reply - if [[ ! "$reply" =~ ^[Yy]$ ]]; then - echo "Keeping existing config. Done." - exit 0 - fi -fi - -# Sensible defaults — try to detect from existing global git config. -default_name="$(git config --global user.name 2>/dev/null || true)" -default_email="$(git config --global user.email 2>/dev/null || true)" - -read -p "Git name [${default_name}]: " name -name="${name:-$default_name}" -[[ -n "$name" ]] || { echo "Name required."; exit 1; } - -read -p "Git email [${default_email}]: " email -email="${email:-$default_email}" -[[ -n "$email" ]] || { echo "Email required."; exit 1; } - -read -p "GPG signing key (optional, leave empty to skip): " signingkey - -mkdir -p "$CONFIG_DIR" -{ - echo "# Per-machine chezmoi config — NOT in the dotfiles repo." - echo "# Each machine carries its own identity here." - echo - echo "sourceDir = \"~/dotfiles\"" - echo - echo "[data.git]" - echo " name = \"$name\"" - echo " email = \"$email\"" - if [[ -n "$signingkey" ]]; then - echo " signingkey = \"$signingkey\"" - fi -} > "$CONFIG_FILE" - -echo -echo "${GRN}✓${RST} wrote $CONFIG_FILE" -echo -echo "${BLD}next:${RST} chezmoi apply" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec mise -C "$root" run configure "$@" diff --git a/tests/bootstrap/Dockerfile b/tests/bootstrap/Dockerfile new file mode 100644 index 0000000..d7f3e89 --- /dev/null +++ b/tests/bootstrap/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:trixie-slim +RUN apt-get update && apt-get install -y --no-install-recommends sudo ca-certificates curl git xz-utils unzip && rm -rf /var/lib/apt/lists/* \ + && useradd --create-home --shell /bin/bash tester \ + && echo 'tester ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/tester \ + && chmod 0440 /etc/sudoers.d/tester +COPY --chown=tester:tester . /home/tester/dotfiles +USER tester +ENV HOME=/home/tester USER=tester LOGNAME=tester SHELL=/bin/bash MISE_ENV=container MISE_YES=1 +ENV PATH=/home/tester/.local/bin:/home/tester/.local/share/mise/shims:/home/tester/.cargo/bin:/usr/local/bin:/usr/bin:/bin +WORKDIR /home/tester/dotfiles +CMD ["bash", "tests/bootstrap/acceptance.sh"] diff --git a/tests/bootstrap/acceptance.sh b/tests/bootstrap/acceptance.sh new file mode 100755 index 0000000..dd8b6fa --- /dev/null +++ b/tests/bootstrap/acceptance.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Real clean-machine install, then a second convergence. This script runs only +# inside the image; the host wrapper never mounts HOME, credentials or sockets. +set -euo pipefail +[[ "$(id -u)" != 0 && "$HOME" == /home/tester ]] +[[ ! -d "$HOME/.local/share/mise/installs" ]] +git config --global user.name 'Bootstrap Fixture' +git config --global user.email 'fixture@example.invalid' +shell_before="$(getent passwd tester | cut -d: -f7)" +git init --quiet "$HOME/fixture-repo" +bash scripts/bootstrap-linux.sh --yes --update +mise bootstrap dotfiles save +mise ls --current --json > /home/tester/first-tools.json +mise install --dry-run-code +chezmoi verify --exclude scripts +phig --repo "$HOME/fixture-repo" snapshot status >/dev/null +nvim --clean --headless '+quit' +opencode2 models > "$HOME/models.txt" +# The account-scoped catalog may be empty before /connect; command success +# still exercises the native CLI on this architecture without host credentials. +for mode in -c -ic -lic; do + PATH=/usr/bin:/bin zsh "$mode" 'set -e; for tool in node uv opencode2 phux blackbird; do command -v "$tool"; done; node --version; uv --version; phux --version; blackbird --version' /home/tester/second-tools.json +cmp /home/tester/first-tools.json /home/tester/second-tools.json +chezmoi verify --exclude scripts +bash tests/bootstrap/check.sh +uv run --script tests/bootstrap/history_test.py +echo 'PASS: real Linux installation, shell access, app config, and second-run convergence' diff --git a/tests/bootstrap/bench_test.py b/tests/bootstrap/bench_test.py new file mode 100755 index 0000000..f5b8236 --- /dev/null +++ b/tests/bootstrap/bench_test.py @@ -0,0 +1,71 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# /// +"""Exercise the real benchmark gate with the platform's /bin/bash.""" +import json +from pathlib import Path +import subprocess +import tempfile +import unittest + +BENCH = Path(__file__).resolve().parents[2] / "dot_local/bin/executable_dot-bench" + + +class BenchTests(unittest.TestCase): + def setUp(self): + self.scratch = tempfile.TemporaryDirectory(prefix="dotfiles-bench-") + self.home = Path(self.scratch.name).resolve() + tools = self.home / "bin" + tools.mkdir() + self.perf = self.home / "PERF.md" + self.perf.write_text("first_prompt_lag_ms: 1\n") + stub = tools / "zsh-bench" + stub.write_text('#!/bin/sh\nprintf "%s\\n" "$BENCH_OUTPUT"\nexit "${BENCH_RC:-0}"\n') + stub.chmod(0o755) + self.env = { + "HOME": str(self.home), "PATH": f"{tools}:/usr/bin:/bin", + "DOTFILES": str(self.home), "XDG_STATE_HOME": str(self.home / "state"), + } + + def tearDown(self): + self.scratch.cleanup() + + def bench(self, output, **overrides): + return subprocess.run(["/bin/bash", str(BENCH)], cwd=self.home, + env={**self.env, "BENCH_OUTPUT": output, **overrides}, + capture_output=True, text=True, timeout=30) + + def test_real_gate_passes_and_persists_numeric_results(self): + result = self.bench("exit_time_ms=2\nfirst_prompt_lag_ms=1.05") + self.assertEqual(result.returncode, 0, result.stderr) + record = next((self.home / "state/dotfiles/bench").glob("*.json")) + self.assertEqual(json.loads(record.read_text())["results"], {"exit_time_ms": 2, "first_prompt_lag_ms": 1.05}) + + def test_real_gate_rejects_large_regression_on_stock_bash(self): + result = self.bench("first_prompt_lag_ms=99999") + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("regressed", result.stdout) + + def test_missing_or_invalid_measurements_cannot_pass(self): + for output in ("", "first_prompt_lag_ms=broken", "first_prompt_lag_ms=01", "command_lag_ms=0"): + with self.subTest(output=output): + self.assertNotEqual(self.bench(output).returncode, 0) + self.assertNotEqual(self.bench("first_prompt_lag_ms=1", BENCH_RC="7").returncode, 0) + + def test_declared_baselines_must_be_positive_numbers(self): + for baseline in ("", "broken", "0", "1 2"): + with self.subTest(baseline=baseline): + self.perf.write_text(f"first_prompt_lag_ms: {baseline}\n") + self.assertNotEqual(self.bench("first_prompt_lag_ms=99999").returncode, 0) + self.perf.write_text("first_prompt_lag_ms:1\n") + self.assertEqual(self.bench("first_prompt_lag_ms=99999").returncode, 2) + + def test_fallback_cannot_certify_unmeasured_pinned_metrics(self): + (self.home / "bin/zsh-bench").unlink() + result = self.bench("", ITERATIONS="1") + self.assertNotEqual(result.returncode, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/bootstrap/check.sh b/tests/bootstrap/check.sh new file mode 100755 index 0000000..79b903c --- /dev/null +++ b/tests/bootstrap/check.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Source validation and offline clean-home tests. No host services or installers. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$root" +bash -n scripts/onboard.sh +jq empty renovate.json dot_config/opencode/opencode.jsonc dot_config/opencode/create_package.json +for file in mise*.toml mise*.lock .miserc.toml; do yq -p=toml -o=json '.' "$file" >/dev/null; done +find scripts/bootstrap tests/bootstrap -type f -name '*.sh' -print0 | + while IFS= read -r -d '' file; do bash -n "$file"; done +for file in dot_zshenv dot_zprofile dot_zshrc dot_zsh/aliases.zsh dot_zsh/functions.zsh; do zsh -n "$file"; done +bash tests/bootstrap/fixtures.sh +bash tests/bootstrap/services.sh +bash tests/bootstrap/retirement.sh +uv run --script tests/bootstrap/configure_test.py +uv run --script tests/bootstrap/onboard_test.py +uv run --script tests/bootstrap/bench_test.py +echo 'PASS: source syntax and isolated configuration contracts' diff --git a/tests/bootstrap/configure_test.py b/tests/bootstrap/configure_test.py new file mode 100644 index 0000000..21c7f52 --- /dev/null +++ b/tests/bootstrap/configure_test.py @@ -0,0 +1,65 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["tomlkit==0.13.3"] +# /// +"""Machine configuration regression tests, using private disposable homes.""" +import argparse +import importlib.util +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import tomlkit + +source = Path(__file__).resolve().parents[2] / "scripts/bootstrap/configure.py" +spec = importlib.util.spec_from_file_location("configure", source) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +class ConfigureTests(unittest.TestCase): + def setUp(self): + self.scratch = tempfile.TemporaryDirectory() + self.addCleanup(self.scratch.cleanup) + self.home = Path(self.scratch.name) + self.path = self.home / ".config/chezmoi/chezmoi.toml" + self.path.parent.mkdir(parents=True) + self.env = patch.dict(os.environ, {"HOME": str(self.home), "XDG_CONFIG_HOME": str(self.home / ".config"), "MISE_ENV": "container"}) + self.env.start() + self.addCleanup(self.env.stop) + self.identity = patch.object(module, "git_identity", return_value="fixture") + self.identity.start() + self.addCleanup(self.identity.stop) + + def test_preserves_machine_data_and_is_private_and_idempotent(self): + self.path.write_text('[data]\nsecret_reference="keep"\nharnesses=["opencode","pi"]\n[data.git]\nname="Personal Name"\nemail="personal@example.invalid"\n') + args = argparse.Namespace(harnesses=[], services=None) + module.configure(args) + first = self.path.read_bytes() + module.configure(args) + self.assertEqual(self.path.read_bytes(), first) + data = tomlkit.parse(first.decode())["data"] + self.assertEqual(data["secret_reference"], "keep") + self.assertEqual(data["git"]["name"], "Personal Name") + self.assertEqual(data["harnesses"], ["opencode", "pi"]) + self.assertFalse(data["services"]) + self.assertEqual(self.path.stat().st_mode & 0o777, 0o600) + + def test_explicit_selection_replaces_previous_harnesses(self): + self.path.write_text('[data]\nharnesses=["opencode","pi"]\n') + module.configure(argparse.Namespace(harnesses=["claude"], services="off")) + self.assertEqual(tomlkit.parse(self.path.read_text())["data"]["harnesses"], ["opencode", "claude"]) + + def test_invalid_existing_document_survives(self): + self.path.write_text("invalid = [") + before = self.path.read_bytes() + with self.assertRaises(tomlkit.exceptions.ParseError): + module.configure(argparse.Namespace(harnesses=[], services=None)) + self.assertEqual(self.path.read_bytes(), before) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/bootstrap/container.sh b/tests/bootstrap/container.sh new file mode 100755 index 0000000..9b2114b --- /dev/null +++ b/tests/bootstrap/container.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +platform="${1:-linux/arm64}" +profile="${2:-container}" +case "$platform" in linux/arm64|linux/amd64) ;; *) echo 'Use linux/arm64 or linux/amd64' >&2; exit 1 ;; esac +case "$profile" in container|container,pi) ;; *) echo 'Use container or container,pi' >&2; exit 1 ;; esac +tag="dotfiles-bootstrap:${platform#linux/}" +docker build --platform "$platform" -f "$root/tests/bootstrap/Dockerfile" -t "$tag" "$root" +[[ "$(docker image inspect "$tag" --format '{{.Architecture}}')" == "${platform#linux/}" ]] +image="$(docker image inspect "$tag" --format '{{.Id}}')" +docker run --rm --platform "$platform" -e MISE_ENV="$profile" "$image" diff --git a/tests/bootstrap/fixtures.sh b/tests/bootstrap/fixtures.sh new file mode 100755 index 0000000..16ca193 --- /dev/null +++ b/tests/bootstrap/fixtures.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +scratch="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-fixtures.XXXXXX")" +trap 'rm -rf "$scratch"' EXIT +mkdir -p "$scratch/bin" +# Provision the parser once; actual fixture runs are explicitly offline. +uv run --script "$root/dot_config/phux/modify_config.toml" /dev/null +uv_cache="$(uv cache dir)" +uv_python="$(uv python find '>=3.11')" + +resolve_binary() { + local binary="$1" resolved directory + local shim_dir="${MISE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/mise}/shims" + local -a directories + if resolved="$(mise which "$binary" 2>/dev/null)"; then + printf '%s\n' "$resolved" + return + fi + # An installed but inactive tool can still have a shim. Resolve its system + # fallback ourselves; the isolated HOME cannot resolve that orphaned shim. + IFS=: read -r -a directories <<< "$PATH" + for directory in "${directories[@]}"; do + [[ "$directory" == "$shim_dir" ]] && continue + [[ -x "$directory/$binary" ]] || continue + printf '%s\n' "$directory/$binary" + return + done + return 1 +} + +for binary in chezmoi jq yq gh bash git uv; do + # A mise shim would resolve against the empty fixture HOME and require trust + # there. Fixtures need the actual preinstalled executable, not its launcher. + resolved="$(resolve_binary "$binary")" + ln -s "$resolved" "$scratch/bin/$binary" +done + +cm() { + env -i HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_CONFIG_DIRS="$home/.config" \ + XDG_DATA_HOME="$home/.local/share" XDG_DATA_DIRS="$home/.local/share" \ + XDG_STATE_HOME="$home/.local/state" XDG_CACHE_HOME="$home/.cache" \ + PATH="$scratch/bin:/usr/bin:/bin" TMPDIR="$scratch" \ + UV_CACHE_DIR="$uv_cache" UV_PYTHON="$uv_python" UV_OFFLINE=true \ + "$scratch/bin/chezmoi" --source "$root" --destination "$home" \ + --config "$case_dir/config.toml" --persistent-state "$case_dir/state.db" \ + --cache "$case_dir/cache" --override-data-file "$case_dir/override.json" \ + --no-tty --no-pager --color=false --use-builtin-diff "$@" +} + +seed_runtime() { + mkdir -p "$home/.config/opencode" "$home/.config/phux" "$home/.config/phui" "$home/.claude" "$home/.pi/agent" "$home/.hermes" + printf '%s\n' '{"scripts":{"mine":"keep"}}' > "$home/.config/opencode/package.json" + printf '%s\n' '{"attention":{"volume":0.3},"tabs":{"mine":true}}' > "$home/.config/opencode/cli.json" + printf '%s\n' '{"repoPaths":{"private/project":"~/private"},"editorCommand":"keep"}' > "$home/.config/phui/config.json" + printf '%s\n' 'extends = ["local.toml"]' '[defaults]' 'session-name = "fixture"' '# native annotated defaults survive' > "$home/.config/phux/config.toml" + printf '%s\n' '# local distro remains local' > "$home/.config/phux/local.toml" + printf '%s\n' '{"hooks":{"runtime":true},"env":{"LOCAL":"keep"}}' > "$home/.claude/settings.json" + printf '%s\n' '{"defaultModel":"keep","packages":["git:example/custom"],"runtime":{"keep":true}}' > "$home/.pi/agent/settings.json" + printf '%s\n' 'model: local' 'unknown: keep' > "$home/.hermes/config.yaml" +} + +assert_runtime() { + jq -e '.scripts.mine == "keep"' "$home/.config/opencode/package.json" >/dev/null + jq -e '.attention.volume == 0.3 and .tabs.mine and .tabs.scope == "cwd"' "$home/.config/opencode/cli.json" >/dev/null + jq -e '.repoPaths["private/project"] == "~/private" and .editorCommand == "keep" and .repoPaths[":owner/:repo"] == "~/workspace/:repo"' "$home/.config/phui/config.json" >/dev/null + yq -p=toml -o=json '.' "$home/.config/phux/config.toml" | jq -e '.extends == ["local.toml", "layers/dotfiles.toml"] and .defaults["session-name"] == "fixture"' >/dev/null + grep -qx '# native annotated defaults survive' "$home/.config/phux/config.toml" + jq -e '.hooks.runtime and .env.LOCAL == "keep"' "$home/.claude/settings.json" >/dev/null + jq -e '.defaultModel == "keep" and .runtime.keep and (.packages | index("git:example/custom"))' "$home/.pi/agent/settings.json" >/dev/null + yq -e '.model == "local" and .unknown == "keep"' "$home/.hermes/config.yaml" >/dev/null + cm managed | grep -Eq '^(tests|scripts|provision|mise\.toml)' && { echo 'Source tooling leaked into HOME' >&2; return 1; } + return 0 +} + +for target in darwin/arm64 linux/arm64 linux/amd64; do + case_dir="$scratch/${target//\//-}" + home="$case_dir/home" + mkdir -p "$home" "$case_dir/cache" + cat > "$case_dir/config.toml" < "$case_dir/override.json" + seed_runtime + printf '%s\n' '# preexisting live preference' > "$home/.zshrc" + cm apply --exclude scripts + grep -qx '# preexisting live preference' "$home/.zshrc" + [[ -f "$home/.config/lazygit/config.yml" ]] + assert_runtime + cm verify --exclude scripts + [[ -z "$(cm diff --exclude scripts)" ]] + cm apply --exclude scripts + assert_runtime + echo "PASS: $target preserves app state and converges twice" + # Native history owns these live paths after enrollment. Both subsequent + # applies must preserve edits and intentional deletions rather than reseeding. + sed '/^\[data\]$/a\ +history = true\ +' "$case_dir/config.toml" > "$case_dir/enrolled.toml" + mv "$case_dir/enrolled.toml" "$case_dir/config.toml" + printf '\n# live-owned fixture edit\n' >> "$home/.zshrc" + rm "$home/.config/lazygit/config.yml" + cm apply --exclude scripts + cm apply --exclude scripts + grep -qx '# live-owned fixture edit' "$home/.zshrc" + [[ ! -e "$home/.config/lazygit/config.yml" ]] + cm managed > "$case_dir/managed.txt" + if grep -Eq '^\.zshrc$|^\.config/nvim(/|$)|^\.agents/skills(/|$)' "$case_dir/managed.txt"; then + echo 'Native history and chezmoi own overlapping paths' >&2; exit 1 + fi + cm verify --exclude scripts + echo "PASS: $target live edits and deletions survive repeated chezmoi apply" +done + +# A core-only installation must not provision optional harnesses. +printf '%s\n' '[data]' 'harnesses = ["opencode"]' 'services = false' '[data.git]' 'name = "Fixture"' 'email = "fixture@example.invalid"' > "$case_dir/config.toml" +if cm managed | grep -Eq '^\.(pi|claude|hermes|grok)(/|$)|^\.config/goose'; then + echo 'Optional harness leaked into core profile' >&2; exit 1 +fi +echo 'PASS: optional harness selection' +[[ -f "$home/.config/opencode/skill/repo-onboarding/SKILL.md" ]] +[[ "$(printf '%s' 'malformed-json' | "$root/dot_pi/private_agent/modify_settings.json")" == malformed-json ]] +[[ "$(printf '%s' 'malformed-json' | "$root/dot_pi/private_agent/modify_private_models.json")" == malformed-json ]] +[[ "$(printf '%s' 'not: [valid' | "$root/dot_hermes/modify_private_config.yaml")" == 'not: [valid' ]] +echo 'PASS: shared skill resolves and malformed Pi state survives' diff --git a/tests/bootstrap/goal-plugin.sh b/tests/bootstrap/goal-plugin.sh new file mode 100755 index 0000000..1461ec9 --- /dev/null +++ b/tests/bootstrap/goal-plugin.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Exercise the exact V2 goal plugin pin against both its own and our SDK. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +spec="$(jq -r '.plugins[] | select(type == "object") | .package | select(startswith("github:phall1/opencode-goal-mode#"))' "$root/dot_config/opencode/opencode.jsonc")" +revision="${spec##*#}" +[[ "$revision" =~ ^[a-f0-9]{40}$ ]] +scratch="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-goal-test.XXXXXX")" +trap 'rm -rf "$scratch"' EXIT +curl --proto '=https' --tlsv1.2 -fsSL "https://codeload.github.com/phall1/opencode-goal-mode/tar.gz/$revision" | tar -xz --strip-components=1 -C "$scratch" +cd "$scratch" +npm ci --ignore-scripts +npm test +npm run check +sdk="$(jq -r '.dependencies["@opencode-ai/plugin"]' "$root/dot_config/opencode/create_package.json")" +npm install --ignore-scripts --no-save --package-lock=false "@opencode-ai/plugin@$sdk" +npm run check diff --git a/tests/bootstrap/history_test.py b/tests/bootstrap/history_test.py new file mode 100755 index 0000000..91e2947 --- /dev/null +++ b/tests/bootstrap/history_test.py @@ -0,0 +1,215 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["tomlkit==0.13.3"] +# /// +"""Exercise released mise history with isolated homes and a local Git origin.""" +import argparse +import json +import os +from pathlib import Path +import shutil +import shlex +import subprocess +import sys +import tempfile +import time +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "scripts/bootstrap")) +from history import history_config, watcher_directories + + +class Machine: + def __init__(self, home: Path, mise: str): + self.home = home + self.mise = mise + home.mkdir() + self.env = { + "HOME": str(home), "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin", + "XDG_CONFIG_HOME": str(home / ".config"), "XDG_DATA_HOME": str(home / ".local/share"), + "XDG_CACHE_HOME": str(home / ".cache"), "XDG_STATE_HOME": str(home / ".local/state"), + "MISE_SYSTEM_CONFIG_DIR": str(home / "system"), "MISE_YES": "1", + "GIT_CONFIG_NOSYSTEM": "1", "GIT_TERMINAL_PROMPT": "0", "TERM": "dumb", + } + self.command("git", "config", "--global", "user.name", "History Fixture") + self.command("git", "config", "--global", "user.email", "fixture@example.invalid") + + def command(self, *args, check=True): + return subprocess.run(args, cwd=self.home, env=self.env, text=True, + capture_output=True, check=check, timeout=90) + + def run(self, *args, check=True): + return self.command(self.mise, "bootstrap", "dotfiles", *args, check=check) + + def seed(self): + config = self.home / ".config/mise/config.toml" + config.parent.mkdir(parents=True) + config.write_text('''[settings.history] +sync = "manual" +notify = false +watch.debounce = "100ms" +watch.reconcile = "1s" +[dotfiles] +"~/.config/mise/config.toml" = { mode = "track" } +"~/.fixture" = { mode = "track" } +[tasks.bootstrap] +run = "test -f \\"$HOME/.fixture\\"" +''') + (self.home / ".fixture").write_text("baseline\n") + self.command(self.mise, "trust", str(config)) + self.run("save") + + +class HistoryTests(unittest.TestCase): + def setUp(self): + self.scratch = tempfile.TemporaryDirectory(prefix="mise-history-") + self.root = Path(self.scratch.name).resolve() + self.a = Machine(self.root / "a", MISE) + self.a.seed() + + def tearDown(self): + self.scratch.cleanup() + + def test_watcher_uses_same_directories_as_shell(self): + from unittest.mock import patch + with patch.dict(os.environ, self.a.env, clear=True): + directories = watcher_directories(self.a.home, self.a.home / ".config") + self.assertEqual(directories["MISE_CACHE_DIR"], str(self.a.home / ".cache/mise")) + self.assertEqual(directories["MISE_STATE_DIR"], str(self.a.home / ".local/state/mise")) + with patch.dict(os.environ, {**self.a.env, "MISE_CACHE_DIR": "/custom/cache"}, clear=True): + self.assertEqual(watcher_directories(self.a.home, self.a.home / ".config")["MISE_CACHE_DIR"], "/custom/cache") + manifest = {"shared": [], "macos": [], "exclude": []} + self.assertEqual(history_config(manifest)["bootstrap"]["services"]["mise-history"]["environment"]["MISE_CACHE_DIR"], "~/.cache/mise") + + def test_restore_autosave_and_atomic_edit(self): + target = self.a.home / ".fixture" + target.write_text("edited\n") + self.a.run("save") + self.a.run("rollback", str(target), "--yes") + self.assertEqual(target.read_text(), "baseline\n") + self.a.run("undo", "--yes") + self.assertEqual(target.read_text(), "edited\n") + with tempfile.TemporaryFile(mode="w+") as log: + watcher = subprocess.Popen([MISE, "bootstrap", "dotfiles", "watch", "--json"], + cwd=self.a.home, env=self.a.env, stdout=log, stderr=log) + try: + self.wait_for_started(log, watcher) + replacement = target.with_suffix(".new") + replacement.write_text("atomic-save\n") + replacement.replace(target) + self.wait_for_saved(self.a, target, watcher) + except AssertionError as error: + log.seek(0) + self.fail(f"{error}\nWatcher log:\n{log.read()}") + finally: + watcher.terminate() + watcher.wait(timeout=15) + + def test_deleted_shell_sources_do_not_execute_stale_bytecode(self): + compiler = Path(__file__).resolve().parents[2] / "dot_local/bin/executable_dot-zcompile" + module = self.a.home / ".zsh/fixture.zsh" + module.parent.mkdir() + sources = [self.a.home / ".zshrc", self.a.home / ".zshenv", self.a.home / ".p10k.zsh", module] + for source in sources: + source.write_text('print -r -- stale-bytecode-fixture\n') + self.a.command("zsh", "-f", str(compiler)) + for source in sources: + self.assertTrue(Path(str(source) + ".zwc").is_file()) + source.unlink() + self.a.command("zsh", "-f", str(compiler)) + for source in sources: + self.assertFalse(Path(str(source) + ".zwc").exists()) + result = self.a.command("zsh", "-i", "-c", "print -r -- fresh-shell-fixture") + self.assertIn("fresh-shell-fixture", result.stdout) + self.assertNotIn("stale-bytecode-fixture", result.stdout) + + def wait_for_started(self, log, watcher): + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + self.assertIsNone(watcher.poll(), "native watcher exited unexpectedly") + log.seek(0) + if '"event":"started"' in log.read(): + return + time.sleep(0.2) + self.fail("native watcher did not start within 30 seconds") + + def wait_for_saved(self, machine, target, watcher): + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + self.assertIsNone(watcher.poll(), "native watcher exited unexpectedly") + result = machine.run("history", "diff", "--path", str(target), "--exit-code", check=False) + if result.returncode == 0: + return + time.sleep(0.2) + self.fail(f"native watcher did not save the atomic replacement: {result.stdout} {result.stderr}") + + def test_two_machine_adoption_sync_conflict_and_delete(self): + origin = self.root / "origin.git" + self.a.command("git", "init", "--bare", str(origin)) + self.a.run("origin", "set", str(origin), "--sync", "manual", "--yes") + self.a.run("sync") + b = Machine(self.root / "b", MISE) + b.command(MISE, "bootstrap", "--adopt", str(origin), "--yes") + target_a = self.a.home / ".fixture" + target_b = b.home / ".fixture" + self.assertEqual(target_b.read_text(), "baseline\n") + target_b.write_text("from-b\n") + b.run("save") + b.run("sync") + self.a.run("sync") + self.a.run("pull", "--yes") + self.assertEqual(target_a.read_text(), "from-b\n") + target_a.write_text("a-conflict\n") + target_b.write_text("b-conflict\n") + self.a.run("save") + b.run("save") + self.a.run("sync") + b.run("sync", check=False) + b.run("pull", "--yes", check=False) + self.assertEqual(target_b.read_text(), "b-conflict\n") + b.run("pull", "--take-remote", str(target_b), "--yes") + self.assertEqual(target_b.read_text(), "a-conflict\n") + target_b.unlink() + b.run("save") + b.run("sync") + self.a.run("sync") + self.a.run("pull", "--yes") + self.assertFalse(target_a.exists()) + + def test_onboarding_entrypoint_adopts_and_repeats(self): + origin = self.root / "onboarding.git" + self.a.command("git", "init", "--bare", str(origin)) + self.a.run("origin", "set", str(origin), "--sync", "manual", "--yes") + self.a.run("sync") + b = Machine(self.root / "laptop", MISE) + tools = b.home / ".local/bin" + tools.mkdir(parents=True) + # Only transport/auth and final host checks are replaced. The released + # mise binary performs the real adoption, checkpoint and synchronization. + shim = tools / "mise" + shim.write_text(f'''#!/bin/sh +if [ "$2" = --adopt ]; then + exec {shlex.quote(MISE)} bootstrap --adopt {shlex.quote(str(origin))} --yes +fi +exec {shlex.quote(MISE)} "$@" +''') + shim.chmod(0o755) + for name in ("gh", "brew", "dot-doctor", "dot-bench"): + tool = tools / name + tool.write_text("#!/bin/sh\nexit 0\n") + tool.chmod(0o755) + script = Path(__file__).resolve().parents[2] / "scripts/onboard.sh" + b.command("bash", str(script)) + self.assertEqual((b.home / ".fixture").read_text(), "baseline\n") + (b.home / ".fixture").write_text("laptop edit\n") + b.command("bash", str(script)) + self.assertEqual((b.home / ".fixture").read_text(), "laptop edit\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--mise", default=shutil.which("mise"), required=False) + args, remaining = parser.parse_known_args() + MISE = str(Path(args.mise).resolve()) + unittest.main(argv=[__file__, *remaining]) diff --git a/tests/bootstrap/onboard_test.py b/tests/bootstrap/onboard_test.py new file mode 100755 index 0000000..fdf8d97 --- /dev/null +++ b/tests/bootstrap/onboard_test.py @@ -0,0 +1,134 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# /// +"""Check onboarding boundaries without installing tools or using credentials.""" +from pathlib import Path +import subprocess +import tempfile +import unittest + +SCRIPT = Path(__file__).resolve().parents[2] / "scripts/onboard.sh" + + +class OnboardingTests(unittest.TestCase): + def setUp(self): + self.scratch = tempfile.TemporaryDirectory(prefix="dotfiles-onboard-") + self.root = Path(self.scratch.name).resolve() + self.home = self.root / "home" + self.bin = self.home / ".local/bin" + self.bin.mkdir(parents=True) + self.calls = self.root / "calls" + self.caller = self.root / "unrelated-project" + self.caller.mkdir() + self.env = { + "HOME": str(self.home), "PATH": f"{self.bin}:/usr/bin:/bin", + "CALLS": str(self.calls), "PLATFORM": "Darwin", + } + self.tool("uname", 'echo "$PLATFORM"') + self.tool("brew", 'echo "brew $*" >> "$CALLS"') + self.tool("git", 'test "${IDENTITY_MISSING:-0}" = 0 && echo fixture') + self.tool("gh", '''echo "gh $*" >> "$CALLS" +if [ "$2" = status ]; then exit "${AUTH_RC:-0}"; fi''') + self.tool("mise", '''echo "mise $*" >> "$CALLS" +echo "cwd=$PWD cache=$XDG_CACHE_HOME state=$XDG_STATE_HOME" >> "$CALLS" +if [ "$2" = --help ]; then echo "${ADOPT_HELP:---adopt}"; exit 0; fi +if [ "$2" = --adopt ]; then exit "${ADOPT_RC:-0}"; fi +if [ "$3" = sync ]; then exit "${SYNC_RC:-0}"; fi''') + self.tool("dot-doctor", 'echo doctor >> "$CALLS"; exit "${DOCTOR_RC:-0}"') + self.tool("dot-bench", 'echo bench >> "$CALLS"; exit "${BENCH_RC:-0}"') + + def tearDown(self): + self.scratch.cleanup() + + def tool(self, name, body): + path = self.bin / name + path.write_text("#!/bin/sh\n" + body + "\n") + path.chmod(0o755) + + def onboard(self, **overrides): + return subprocess.run(["/bin/bash", str(SCRIPT)], cwd=self.caller, + env={**self.env, **overrides}, capture_output=True, + text=True, timeout=30) + + def test_adoption_uses_home_and_shared_lock_roots(self): + result = self.onboard() + self.assertEqual(result.returncode, 0, result.stderr) + calls = self.calls.read_text() + self.assertIn(f"cwd={self.home} cache={self.home}/.cache state={self.home}/.local/state", calls) + self.assertIn("mise bootstrap --adopt phall1/dotfiles-history --yes", calls) + self.assertLess(calls.index("--adopt phall1"), calls.index("dotfiles save")) + self.assertLess(calls.index("dotfiles sync"), calls.index("doctor")) + self.assertIn("Onboarding complete", result.stdout) + self.assertNotIn("brew install", calls) + self.assertNotIn("gh auth setup-git", calls) + + def test_existing_directory_overrides_are_preserved(self): + result = self.onboard(XDG_CACHE_HOME=str(self.root / "cache"), XDG_STATE_HOME=str(self.root / "state")) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn(f"cache={self.root}/cache state={self.root}/state", self.calls.read_text()) + + def test_final_checks_see_newly_installed_tools(self): + data = self.root / "custom-mise-data" + shims = data / "shims" + shims.mkdir(parents=True) + tool = shims / "atuin" + tool.write_text("#!/bin/sh\nexit 0\n") + tool.chmod(0o755) + self.tool("dot-doctor", 'command -v atuin >/dev/null || exit 2') + result = self.onboard(MISE_DATA_DIR=str(data)) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_bad_prerequisites_stop_before_adoption(self): + for overrides in ({"AUTH_RC": "1"}, {"IDENTITY_MISSING": "1"}, {"ADOPT_HELP": "old mise"}): + with self.subTest(overrides=overrides): + self.calls.unlink(missing_ok=True) + result = self.onboard(**overrides) + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("--adopt phall1", self.calls.read_text()) + + def test_download_command_never_executes_a_partial_response(self): + readme = SCRIPT.parents[1] / "README.md" + command = next(line for line in readme.read_text().splitlines() if line.startswith("onboard=$(curl")) + self.tool("curl", "printf '%s\\n' 'echo partial-script-executed'\nexit 22") + for shell in ("/bin/bash", "/bin/zsh"): + with self.subTest(shell=shell): + result = subprocess.run([shell, "-f", "-c", command], cwd=self.caller, + env=self.env, capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 22, result.stderr) + self.assertNotIn("partial-script-executed", result.stdout) + + def test_download_command_runs_the_complete_entrypoint(self): + readme = SCRIPT.parents[1] / "README.md" + command = next(line for line in readme.read_text().splitlines() if line.startswith("onboard=$(curl")) + self.tool("curl", 'cat "$ONBOARD_SOURCE"') + result = subprocess.run(["/bin/zsh", "-f", "-c", command], cwd=self.caller, + env={**self.env, "ONBOARD_SOURCE": str(SCRIPT)}, + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Onboarding complete", result.stdout) + + def test_adoption_or_sync_failure_never_reports_completion(self): + for key in ("ADOPT_RC", "SYNC_RC"): + with self.subTest(key=key): + self.calls.unlink(missing_ok=True) + result = self.onboard(**{key: "7"}) + self.assertEqual(result.returncode, 7) + self.assertNotIn("Onboarding complete", result.stdout) + self.assertNotIn("doctor", self.calls.read_text()) + + def test_validation_runs_both_checks_and_keeps_failures_visible(self): + for overrides in ({"DOCTOR_RC": "2"}, {"BENCH_RC": "2"}): + with self.subTest(overrides=overrides): + self.calls.unlink(missing_ok=True) + result = self.onboard(**overrides) + self.assertNotEqual(result.returncode, 0) + self.assertIn("doctor\nbench\n", self.calls.read_text()) + self.assertNotIn("Onboarding complete", result.stdout) + result = self.onboard(DOCTOR_RC="1") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("complete with the doctor warnings", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/bootstrap/retirement.sh b/tests/bootstrap/retirement.sh new file mode 100644 index 0000000..8cba414 --- /dev/null +++ b/tests/bootstrap/retirement.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +scratch="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-retirement.XXXXXX")" +trap 'rm -rf "$scratch"' EXIT +mkdir -p "$scratch/tests" +cp "$root/tests/gha-local-smoke.sh" "$scratch/tests/gha-local-smoke.sh" +HOME="$scratch" bash "$root/scripts/bootstrap/retire-legacy.sh" +[[ ! -e "$scratch/tests/gha-local-smoke.sh" ]] +printf '%s\n' 'local user work' > "$scratch/tests/gha-local-smoke.sh" +if HOME="$scratch" bash "$root/scripts/bootstrap/retire-legacy.sh" 2>/dev/null; then exit 1; fi +grep -qx 'local user work' "$scratch/tests/gha-local-smoke.sh" +echo 'PASS: only exact retired copies are deleted; local edits survive' diff --git a/tests/bootstrap/services.sh b/tests/bootstrap/services.sh new file mode 100755 index 0000000..ff2706f --- /dev/null +++ b/tests/bootstrap/services.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Stub only native service boundaries; real installers run in the container rig. +set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +scratch="$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-services.XXXXXX")" +trap 'rm -rf "$scratch"' EXIT +mkdir -p "$scratch/bin" "$scratch/home/.config/dotfiles" "$scratch/home/.config/systemd/user" +export HOME="$scratch/home" XDG_CONFIG_HOME="$scratch/home/.config" CALLS="$scratch/calls" +export PATH="$scratch/bin:$PATH" +printf '%s\n' '{"services":true}' > "$XDG_CONFIG_HOME/dotfiles/profile.json" +cat > "$scratch/bin/blackbird" <<'STUB' +#!/usr/bin/env bash +if [[ "$1" == doctor ]]; then + printf '%s\n' "$REPORT" + exit "${DOCTOR_RC:-0}" +fi +echo "blackbird $*" >> "$CALLS" +STUB +cat > "$scratch/bin/phux" <<'STUB' +#!/usr/bin/env bash +echo "phux $*" >> "$CALLS" +if [[ "$*" == 'service status' ]]; then exit "${PHUX_STATUS_RC:-0}"; fi +STUB +printf '%s\n' '#!/bin/sh' 'echo Linux' > "$scratch/bin/uname" +printf '%s\n' '#!/bin/sh' 'exit 1' > "$scratch/bin/systemctl" +printf '%s\n' '#!/bin/sh' 'exit "${SOCKET_RC:-7}"' > "$scratch/bin/curl" +printf '%s\n' '#!/bin/sh' 'exit "${PROCESS_RC:-1}"' > "$scratch/bin/pgrep" +chmod +x "$scratch/bin/"* +export REPORT='{"checks":[{"name":"daemon.liveness","status":"pass"}]}' DOCTOR_RC=5 +touch "$XDG_CONFIG_HOME/systemd/user/blackbird.service" +if bash "$root/scripts/bootstrap/services.sh" 2>/dev/null; then exit 1; fi +[[ ! -e "$CALLS" ]] +export DOCTOR_RC=0 +bash "$root/scripts/bootstrap/services.sh" +if grep -q 'blackbird install' "$CALLS"; then exit 1; fi +rm "$XDG_CONFIG_HOME/systemd/user/blackbird.service" "$CALLS" +export REPORT='{"checks":[{"name":"database.schema","status":"fail"}]}' DOCTOR_RC=5 +if bash "$root/scripts/bootstrap/services.sh" 2>/dev/null; then exit 1; fi +[[ ! -e "$CALLS" ]] +export REPORT='invalid-json' +if bash "$root/scripts/bootstrap/services.sh" 2>/dev/null; then exit 1; fi +[[ ! -e "$CALLS" ]] +export REPORT='{"checks":[{"name":"daemon.liveness","status":"fail"}]}' +for state in warn fail; do + export REPORT="{\"checks\":[{\"name\":\"daemon.liveness\",\"status\":\"$state\"}]}" SOCKET_RC=0 + if bash "$root/scripts/bootstrap/services.sh" 2>/dev/null; then exit 1; fi + [[ ! -e "$CALLS" ]] +done +export SOCKET_RC=7 PROCESS_RC=0 +if bash "$root/scripts/bootstrap/services.sh" 2>/dev/null; then exit 1; fi +[[ ! -e "$CALLS" ]] +export PROCESS_RC=1 +bash "$root/scripts/bootstrap/services.sh" +grep -qx 'blackbird install' "$CALLS" +export DOCTOR_RC=0 PHUX_STATUS_RC=1 +touch "$XDG_CONFIG_HOME/systemd/user/blackbird.service" "$XDG_CONFIG_HOME/systemd/user/phux.service" +rm "$CALLS" +if bash "$root/scripts/bootstrap/services.sh" 2>/dev/null; then exit 1; fi +if grep -q 'install' "$CALLS"; then exit 1; fi +rm "$XDG_CONFIG_HOME/systemd/user/phux.service" "$CALLS" +bash "$root/scripts/bootstrap/services.sh" +grep -qx 'phux service install --adopt' "$CALLS" +rm "$CALLS" +printf '%s\n' '{"services":false}' > "$XDG_CONFIG_HOME/dotfiles/profile.json" +bash "$root/scripts/bootstrap/services.sh" +[[ ! -e "$CALLS" ]] +echo 'PASS: existing/failing services stay untouched; only absent installs provision; services-off skips native calls'