docks-kit 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -35,12 +35,14 @@ launcher can fall back to Bun source.
35
35
  | `cli/src/commands/omp.ts` | `docks-kit omp` session launcher: renders the free-model run overlay and forwards args to omp |
36
36
  | `cli/src/engine-native/ompOverlay.ts` | Free-model overlay render plus catalog parse and thinking-ceiling helpers |
37
37
  | `cli/src/engine-native/<axis>.ts` | One change axis per file: `codexConfig`/`codexHooks`/`codexPlugins`/`codexStatus`, `claudeSettings`/`claudeRemovals`/`claudePluginPasses`/`claudeOptionalPlugins`/`claudeLsp`, `parseModifiers`/`parseHelp`, `ompFileDeploy`/`ompMarketplace`/`ompPlugins`, `skillsManifest`/`skillsLinks`/`skillsInstall`/`skillsPrune`, `engineCtx`/`syncConcurrency`/`syncDispatch`; orchestrators keep prior exports |
38
- | `cli/src/engine-native/sharedTypes.d.ts` | Single definition per shared shape (manifest records, settings edits, omp session model, scalar modifier flags) |
38
+ | `cli/src/engine-native/sharedTypes.d.ts` | Single definition per shared shape (manifest records, settings edits, omp session model, scalar modifier flags, resolved model catalog) |
39
+ | `cli/src/engine-native/kitDb.ts` | Only opener of the SQLite store `~/.docks-kit/kit.db`: append-only migrations, one-time `state.json` import, TTL cache |
40
+ | `cli/src/engine-native/liveModels.ts` | Live model list per enabled harness (Anthropic API via Claude Code login, Codex model cache, `omp models --json`) merged over `SoT/models.json` |
39
41
  | `.oxlintrc.json` / `.oxfmtrc.json` | Mechanical lint (correctness, suspicious, eqeqeq, no-var, prefer-const, no-unused) and code format scope |
40
42
  | `cli/test/fixtures/` | Golden fixtures: `home-fresh`, `home-drift`, `home-invalid-json`, `codex-toml`, `statusline` |
41
43
  | `cli/` | Effect 4 RC CLI + bundled docs topics |
42
- | `SoT/models.json` | Kit-verified Claude and Codex model catalog |
43
- | `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative) |
44
+ | `SoT/models.json` | Curated Claude and Codex model overlay: aliases and notes always apply; id rows are the fallback when a harness live list is unavailable |
45
+ | `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative; optional `upstream` source read by `docks-kit toolchain outdated`) |
44
46
  | `SoT/.claude/bin/` | Dependency-free Bun runtime programs for Claude's statusline, SessionStart, and Notification |
45
47
  | `SoT/.omp/` | Kit-owned omp SoT: `AGENTS.md`, `config.yml`, `models.yml`, `mcp.json`, and `intercom.json` |
46
48
  | `install.sh` / `install.ps1` | POSIX and Windows global installers |
@@ -73,7 +75,7 @@ omp SoT notes:
73
75
  - `ompSync.ts syncMergedYaml` deep-merges `config.yml` through `ompYaml.ts mergeOmpConfig` and `models.yml` through `mergeOmpModels`. Both wrap one generic mapping merge; only the config wrapper prunes stale `retry.fallbackChains` wildcards.
74
76
  - `ompSync.ts` runs `ompRemovals.ts syncOmpRemovals, retired-key inventory` right after the config merge, and that pass force-prunes retired kit-owned keys from `~/.omp/agent/config.yml` on every sync, without `--reconcile`. The pass is required because `mergeOmpConfig` is additive, so removing a key from the SoT alone never removes it from a deployed file. A key retired with a recorded value is pruned only while the deployed value still matches that value, so a user edit survives; a key retired outright, such as `providers.webSearchOrder`, is pruned at any value.
75
77
  - `cycleOrder` ends with `astra` as its fifth stop. `modelRoles.astra` is `openai-codex/gpt-6-astra:xhigh`, and `modelTags.astra` is visible. Astra and Fable fall back to each other through concrete selectors. `modelRoles.fable` remains `anthropic/claude-fable-5-1:medium`, with visible `modelTags.fable`. The hidden `switch_fable` role uses the same Fable selector and keeps an empty fallback chain.
76
- - `modelRoles.task` is `openai-codex/gpt-6-sol:high`, and `advisor` is `openai-codex/gpt-6-sol:medium`. `smol`, `commit`, and `tiny` use `openai-codex/gpt-6-luna`. The four reviewer entries in `task.agentModelOverrides` inherit `task` through `@task`. Only bundled `reviewer` and `security-reviewer` are discoverable OMP agents; `code-reviewer` and `plan-reviewer` stay dormant. omp 18.2.9 does not list `gpt-6-sol` or `gpt-6-luna` in its `openai-codex` catalog, so it fuzzy-matches both selectors to the GPT-5.6 models without a warning until the catalog lists them. The owner deployed them anyway. `cli/docs/omp-models.md` records the checks.
78
+ - `modelRoles.task` is `openai-codex/gpt-6-sol:high`. `advisor` is `anthropic/claude-opus-5-5:medium` with an empty fallback chain, because a GPT-6 Sol advisor looped on repeated reads under an Opus 5.5 session; never put GPT-6 Sol in the advisor role or chain. `smol`, `commit`, and `tiny` use `openai-codex/gpt-6-luna`. The four reviewer entries in `task.agentModelOverrides` inherit `task` through `@task`. Only bundled `reviewer` and `security-reviewer` are discoverable OMP agents; `code-reviewer` and `plan-reviewer` stay dormant. omp 18.2.9 does not list `gpt-6-sol` or `gpt-6-luna` in its `openai-codex` catalog, so it fuzzy-matches both selectors to the GPT-5.6 models without a warning until the catalog lists them. The owner deployed them anyway. `cli/docs/omp-models.md` records the checks.
77
79
  - The five Anthropic roles (`default`, `slow`, `plan`, `designer`, `vision`) and the five Anthropic retry chains use `anthropic/claude-opus-5-5` at the levels the role map records. `cli/docs/omp-models.md` carries the Artificial Analysis capture behind the role map, read 2026-09-22 at Intelligence Index v4.3.2 and Coding Agent Index v1.5 from the AA comparison-page metric tables. Opus 5.5 max has the highest index in that topic. AA has not measured speed or latency for Opus 5.5 max, GPT-6 Sol, or GPT-6 Luna.
78
80
  - `modelRoles.web` is `web/firecrawl` and `retry.fallbackChains.web` carries the explicit 20-entry provider order. The kit declares both keys because the legacy `providers.webSearchOrder` key is retired: omp expands it in memory into these two keys and then drops it, and never writes that expansion back to disk. An explicit chain replaces omp's built-in web order wholesale, so every entry left out is a provider omp never tries. The owner removed the seven entries that named older models (Gemini 2.5 Flash, Claude Haiku 4.5, GPT-5.6, GPT-5.5, and Grok 4.5); keep every other provider.
79
81
  - `SoT/.omp/models.yml` declares Astra's full `low, medium, high, xhigh, max` ladder with `defaultLevel: xhigh` as the worked provider ladder-override example. It also carries a temporary `anthropic.modelOverrides.claude-opus-5-5` block with that model's limits, ladder, and prices, because the shared catalog still serves the id as a stub with null limits and zero cost; remove the block once the catalog publishes the row. `ompYaml.ts mergeOmpModels` preserves deployed-only keys in `~/.omp/agent/models.yml`, because a user file may carry provider credentials. Whole-file replacement is wrong.
@@ -84,7 +86,7 @@ omp SoT notes:
84
86
  - Sync installs `pi-intercom` at the verified version from `SoT/toolchain.json`.
85
87
  - The omp CLI is upstream-owned and self-updating through `omp update`. Sync never installs or upgrades the CLI.
86
88
  - `docks-kit omp [--model <selector>|--pick] [args...]` starts one interactive omp session with every model role set to one free model and every retry chain empty. Higher-precedence model selection can replace those overlay values. It renders the overlay to `~/.cache/docks-kit/omp-free-<model>-<digest>.yml` (mode 0600) and passes it through omp's repeatable `--config` flag. `ompOverlay.ts overlayFileName` gives each model its own file, because omp can re-read the overlay during a live session and a second launcher on another model must not rewrite it. Deployed `~/.omp/agent/` files stay untouched, so the next plain `omp` run uses the paid configuration again. Remaining arguments forward verbatim to omp.
87
- - The session model persists per machine in `~/.docks-kit/state.json` under `ompSession`, next to `harnesses`. The default is `opencode-zen/muse-spark-1.3-contributor-free` at `xhigh`. `ompOverlay.ts ladderCeiling, advisorLevelFor` derive both levels from the ladder of the chosen model for the `--model` path, and `ompOverlay.ts planEffortChoice` drives the `--pick` wizard, which asks whether every role shares one level and otherwise takes one level for the main roles and one for the advisor from that model's own ladder. Levels never come from a fixed list: free ladders are not uniform, three free models publish no `medium`, and two publish no ladder, which the overlay renders as bare selectors. The picker lists only zero-cost catalog models.
89
+ - The session model persists per machine in `~/.docks-kit/kit.db` (table `omp_session`), next to the harness selection. The default is `opencode-zen/muse-spark-1.3-contributor-free` at `xhigh`. `ompOverlay.ts ladderCeiling, advisorLevelFor` derive both levels from the ladder of the chosen model for the `--model` path, and `ompOverlay.ts planEffortChoice` drives the `--pick` wizard, which asks whether every role shares one level and otherwise takes one level for the main roles and one for the advisor from that model's own ladder. Levels never come from a fixed list: free ladders are not uniform, three free models publish no `medium`, and two publish no ladder, which the overlay renders as bare selectors. The picker lists only zero-cost catalog models.
88
90
 
89
91
  For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the matching SoT directory.
90
92
 
@@ -96,7 +98,7 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the m
96
98
  - **Effect 4 CLI stack.** The CLI pins `effect@4.0.0-rc.115` (including `effect/unstable/cli`), `@effect/platform-bun@4.0.0-rc.115` (`BunServices.layer`, `BunRuntime.runMain`), `@effect/vitest@4.0.0-rc.115`, and `vitest@5.0.0` (inside the `@effect/vitest` peer range `>=5.0.0 <6.0.0`; 5.0.0 is past the 5.x patch point for GHSA-82fw-gwwq-j7x9, fixed in 5.0.0-rc.2). `@effect/cli` and `@effect/platform` are removed and must not be reintroduced.
97
99
  - **Effect skill routing.** Effect work in this checkout must verify migration and API call shapes against the installed declarations under `node_modules/effect/dist/unstable/cli/`, never from memory or a mutable dist-tag. The `effect-ts-setup`, `effect-ts-port`, and `effect-ts-specialist` skills target Effect 3.x and do not apply.
98
100
  - **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, `agents`, and `omp`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync omp` for omp-only config edits); targets can be combined with `--dry-run`, `--skip-bubblewrap` (skip optional bubblewrap bootstrap for the Codex Linux sandbox), `--skip-plugin-refresh` (install missing plugins without refreshing existing caches), `--reconcile`, `--prune`, and the deploy-time modifiers `--claude-compact-window=<tokens>` / `--claude-permissive` / `--claude-model=<m>` / `--claude-effort=<level>` / `--claude-advisor=<on|off|default>` / `--codex-model=<m>` / `--codex-effort=<level>` (see `CLAUDE.md` § Deploy-time modifiers).
99
- - **Per-machine harness selection.** `~/.docks-kit/state.json` drives a flag-less sync. A missing file selects `claude`, `codex`, and `agents`; it never selects `omp` implicitly. `sync` never prompts and never writes the selection file. `docks-kit harnesses` is the only command that writes the `harnesses` selection. The same state file also carries the independent `ompSession` key, which only `docks-kit omp` writes; every writer merges over the stored record, so neither key can drop the other.
101
+ - **Per-machine harness selection.** The SQLite store `~/.docks-kit/kit.db` drives a flag-less sync. A missing selection selects `claude`, `codex`, and `agents`; it never selects `omp` implicitly. `sync` never prompts and never writes the selection. `docks-kit harnesses` is the only command that writes the `harness_selection` table. The same store holds the independent `omp_session` table, which only `docks-kit omp` writes, and a TTL cache (`cache_entry`) for the live Anthropic model list and `toolchain outdated` lookups. `kitDb.ts withKitDb` is the only opener: it runs the append-only `MIGRATIONS`, imports a legacy `state.json` once and renames it to `state.json.migrated`, and refuses a store from a newer schema. Cache rows carry the kit version as fingerprint, so a new release refetches them.
100
102
  - **Additive by default.** Keys present in deployed config but absent from SoT are preserved on default sync. This protects user-only additions, but means drift accumulates — neither flag-less reset can clean it up. The one exception is the Claude `removed` manifest (`claudeSync.ts REMOVED_MANIFEST, baseline removal inventory`), a curated list of unambiguous kit-owned artifacts that `claudeSync.ts syncRemovals, baseline artifact prune` force-prunes on every sync, including the home-relative `~/.local/bin/session-relay` artifact installed outside `~/.claude`; see `CLAUDE.md` § Pruning stale artifacts.
101
103
  - **`--reconcile` / `--prune` are the kit-owned reconcile flags.** Orthogonal — `--reconcile` reconciles the settings layer (SoT-declared keys/tables/arrays win; user-only keys and nested objects are preserved; permissions arrays are replaced wholesale by SoT). `--prune` uninstalls kit-managed installations not in the SoT (plugins, marketplaces, and `~/.agents/skills/*` entries tracked in `~/.agents/.kit-managed-skills`). Combine for a full reset to SoT's kit-managed scope. User-only additions outside the kit's scope (custom env vars, mcpServers, manually-installed skills, third-party plugins not declared in SoT) are always preserved. Each tool's per-tool file documents the specific paths and diff recipes.
102
104
  - **SOLID-aligned modules.** Axis modules own one concern each (see layout); orchestrators (`claudeSync.ts`, `codexSync.ts`, `skillsSync.ts`, `ompSync.ts`, `index.ts`, `argv.ts`, `parseArgs.ts`) keep prior exports. New shared shapes go in `sharedTypes.d.ts`, never a second local copy. The public CLI seam is `cli/src/engine.ts`.
package/README.md CHANGED
@@ -47,9 +47,9 @@ docks-kit sync [claude] [codex] [agents] [omp] deploy explicit targets or the m
47
47
  docks-kit harnesses view or change this machine's selection
48
48
  docks-kit update [--no-sync] self-update the kit (autodetects checkout vs global install), then sync
49
49
  docks-kit model <claude|codex> [value] get/set the DEPLOYED model (TTY picker)
50
- docks-kit models [claude|codex] model catalogs (`--json`)
50
+ docks-kit models [claude|codex|omp] [--refresh] live catalogs for enabled harnesses (`--json`)
51
51
  docks-kit omp [--model <m>|--pick] [args...] one omp session on a free model, nothing deployed changes
52
- docks-kit toolchain [check|ensure <tool>] verified-version floors for external tools
52
+ docks-kit toolchain [check|ensure <tool>|outdated] verified-version floors for external tools
53
53
  docks-kit status [--json] deployed-vs-SoT drift + toolchain + counts
54
54
  docks-kit plugins list [--json] enabledPlugins tri-state vs installed
55
55
  docks-kit skills list [--json] universal skills vs manifest
@@ -91,16 +91,16 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
91
91
  - **Additive by default** — user-only settings keys, plugins, and skills
92
92
  survive a plain sync. Reconciliation toward the SoT is explicit
93
93
  (`--reconcile` / `--prune`).
94
- - **Per-machine selection** — `~/.docks-kit/state.json` drives a flag-less sync.
95
- A missing file selects Claude Code, Codex, and universal skills. It does not
94
+ - **Per-machine selection** — `~/.docks-kit/kit.db` drives a flag-less sync.
95
+ A missing selection selects Claude Code, Codex, and universal skills. It does not
96
96
  select omp. Use `docks-kit harnesses` to view or change the selection.
97
97
  - **Idempotent** — every step is safe to re-run; no-change syncs are no-ops.
98
98
  - **Toolchain floors** — `SoT/toolchain.json` records the kit-verified version
99
99
  floors for external tools (bun, bwrap, …). `docks-kit toolchain check` prints
100
100
  the full doctor table. Bun is the one managed install and is pinned to its
101
101
  verified version.
102
- - **Model catalog** — `SoT/models.json` is the research-verified source for
103
- model validation, listings, and pickers.
102
+ - **Model catalog** — enabled harnesses provide live Claude, Codex, or omp
103
+ model IDs; `SoT/models.json` supplies aliases, notes, and the offline fallback.
104
104
  - **Claude runtime** — sync materializes three dependency-free Bun `.mjs`
105
105
  programs for statusline, SessionStart, and Notification. Quota display uses
106
106
  Claude's native `rate_limits`; there is no OAuth fetch, shared usage cache,
@@ -115,7 +115,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
115
115
  | `SoT/.codex/` | Codex SoT (config.toml, rules, AGENTS.md, marketplace) |
116
116
  | `SoT/.omp/` | omp SoT (AGENTS.md, config.yml, models.yml, mcp.json, intercom.json) |
117
117
  | `SoT/.agents/` | Universal-skill manifest |
118
- | `SoT/models.json` | Kit-verified Claude and Codex model catalog |
118
+ | `SoT/models.json` | Curated aliases, notes, and offline Claude/Codex fallback |
119
119
  | `SoT/toolchain.json` | Verified-version floors |
120
120
  | `cli/src/engine-native/` | EngineNative sync/model/toolchain implementation |
121
121
  | `cli/src/generated/sotPayload.ts` | Generated in-memory payload used by standalone and npm installs |
package/cli/docs/flags.md CHANGED
@@ -14,7 +14,7 @@ docks-kit sync omp # opt-in harness
14
14
 
15
15
  Positional targets are `claude`, `codex`, `agents`, and `omp`. A flag-less
16
16
  `sync` deploys the per-machine harness selection stored in
17
- `~/.docks-kit/state.json`. A missing or invalid state file selects `claude`,
17
+ `~/.docks-kit/kit.db`. A missing selection or an unreadable store selects `claude`,
18
18
  `codex`, and `agents`, and never selects `omp`. Choose the stored selection
19
19
  with `docks-kit harnesses`.
20
20
 
@@ -56,7 +56,7 @@ ignored with a warning; Claude modifiers never touch Codex config and vice versa
56
56
 
57
57
  | Flag | Effect |
58
58
  |------|--------|
59
- | `--model <selector>` | Session model for this run and later runs; rejected unless the live catalog reports zero input and output cost; recorded in `~/.docks-kit/state.json` under `ompSession` |
59
+ | `--model <selector>` | Session model for this run and later runs; rejected unless the live catalog reports zero input and output cost; recorded in `~/.docks-kit/kit.db` (table `omp_session`) |
60
60
  | `--pick` | Interactive picker over free catalog models; records the choice the same way |
61
61
 
62
62
  Remaining arguments forward verbatim to omp after the launcher flags.
@@ -1,9 +1,17 @@
1
1
  # Models
2
2
 
3
- `SoT/models.json` is the kit-verified model catalog — the single source for
4
- the engine validators, `docks-kit models`, the interactive picker, and the
5
- bare-flag helper output. Each tool section carries a `verified` date; update
6
- the entry and date when a model ships or retires.
3
+ `SoT/models.json` supplies aliases and notes for Claude and Codex. For each
4
+ enabled harness, `docks-kit models` and the picker list live model IDs when
5
+ available. Claude uses the Claude Code login to query the Anthropic models API
6
+ and caches the result for six hours in `~/.docks-kit/kit.db`; Codex reads
7
+ `~/.codex/models_cache.json`; omp runs `omp models --json`. The kit never needs
8
+ omp to list Claude models.
9
+
10
+ Live lists retain the curated aliases and notes, but omit curated IDs absent
11
+ from the live source. A disabled harness, missing login/cache, or unavailable
12
+ source falls back to the curated IDs. omp has no curated fallback models.
13
+ The `verified` field is the curated section date, not the live fetch date.
14
+ Bare model flags without a value print the curated list without a network call.
7
15
 
8
16
  ## Validation rules
9
17
 
@@ -33,9 +41,10 @@ the entry and date when a model ships or retires.
33
41
  ## Commands
34
42
 
35
43
  ```
36
- docks-kit models # both catalogs
37
- docks-kit models claude --json # machine-readable
38
- docks-kit model claude # current deployed + SoT + picker (TTY)
44
+ docks-kit models # enabled Claude, Codex, and omp catalogs
45
+ docks-kit models claude --json # source, fetch date, and fallback reason
46
+ docks-kit models claude --refresh # bypass the six-hour Anthropic cache
47
+ docks-kit model claude # current deployed + live list + picker (TTY)
39
48
  docks-kit model claude opus # per-machine override from the Opus SoT
40
49
  docks-kit sync claude --claude-model=opus # same, as part of a sync
41
50
  ```
@@ -12,7 +12,7 @@ against.
12
12
  | `slow` | `anthropic/claude-opus-5-5` | xhigh | 56 | $3.46 | 165.20 s |
13
13
  | `plan` | `anthropic/claude-opus-5-5` | xhigh | 56 | $3.46 | 165.20 s |
14
14
  | `task` | `openai-codex/gpt-6-sol` | high | 43 | $0.37 | n/a |
15
- | `advisor` | `openai-codex/gpt-6-sol` | medium | 40 | $0.25 | n/a |
15
+ | `advisor` | `anthropic/claude-opus-5-5` | medium | 51 | $1.34 | 22.17 s |
16
16
  | `designer` | `anthropic/claude-opus-5-5` | high | 54 | $1.82 | 12.49 s |
17
17
  | `vision` | `anthropic/claude-opus-5-5` | medium | 51 | $1.34 | 22.17 s |
18
18
  | `smol` / `commit` | `openai-codex/gpt-6-luna` | medium | 29 | $0.02 | n/a |
@@ -83,6 +83,19 @@ chains, `retry.fallbackChains.default` would send either stop to
83
83
  Chain entries are concrete selectors, not role aliases, so this pair cannot
84
84
  recurse. The hidden `switch_fable` chain stays empty.
85
85
 
86
+ ### Why `advisor` runs Opus 5.5 medium
87
+
88
+ `advisor` runs `anthropic/claude-opus-5-5:medium` with an empty fallback
89
+ chain. On 2026-09-24, with omp 18.3.0, an Opus 5.5 session with the advisor
90
+ on `openai-codex/gpt-6-sol:medium` looped. In one session the advisor made
91
+ 1,313 requests for 140 main-agent requests. It issued 1,272 `read` calls on
92
+ 248 distinct paths, read one 3-line slice 249 times, and never called
93
+ `advise`. The advisor prompt averaged about 317k tokens, above the 272K
94
+ window omp lists for GPT-6 Sol. With the advisor on Opus 5.5 medium, the
95
+ same kind of session made about one advisor request per main-agent request
96
+ and called `advise` normally. The owner excluded GPT-6 Sol from the advisor
97
+ role and its fallback chain.
98
+
86
99
  `modelRoles.web` is `web/firecrawl`, and `retry.fallbackChains.web` lists the
87
100
  explicit 20-entry provider order that follows it. The two keys replace the
88
101
  retired `providers.webSearchOrder` key, which omp no longer carries in its
@@ -405,8 +418,8 @@ distinct ladders: 21 include `medium`, 3 stop at `low, high, max` or
405
418
  bare selectors with no `:level` suffix, because an invented level makes omp
406
419
  fail when the session starts.
407
420
 
408
- The choice persists per machine in `~/.docks-kit/state.json` under
409
- `ompSession`, next to `harnesses`. It survives across sessions. It is never
421
+ The choice persists per machine in `~/.docks-kit/kit.db` (table
422
+ `omp_session`), next to the harness selection. It survives across sessions. It is never
410
423
  committed. `--model <selector>` records one selector and derives both levels
411
424
  from the catalog row. `--pick` opens an interactive wizard.
412
425
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  `docks-kit sync [claude] [codex] [agents] [omp]` — targets are positional
4
4
  words; no target means this machine's harness selection in
5
- `~/.docks-kit/state.json`. The default is `claude codex agents`, and
5
+ `~/.docks-kit/kit.db`. The default is `claude codex agents`, and
6
6
  `docks-kit harnesses` changes it.
7
7
 
8
8
  ## claude (→ ~/.claude, ~/.claude.json, shell rc)
@@ -85,4 +85,14 @@ manifest's `verified` version.
85
85
  ```text
86
86
  docks-kit toolchain check # doctor table (also inside docks-kit status)
87
87
  docks-kit toolchain ensure bun # ensure the only managed tool
88
+ docks-kit toolchain outdated [--refresh] # verified pins vs newest upstream release
88
89
  ```
90
+
91
+ `docks-kit toolchain outdated` reads each tool's optional `upstream` entry in
92
+ `SoT/toolchain.json`. An `npm` entry reads the registry `latest` tag. With a
93
+ `line`, it reads the newest release on that major line instead, so `tsc` stays
94
+ on 6.x. A `github` entry reads the latest release tag and strips `tagPrefix`.
95
+ Set `GITHUB_TOKEN` or `GH_TOKEN` to raise the GitHub rate limit. Successful
96
+ lookups cache for 24 hours in `~/.docks-kit/kit.db`; `--refresh` bypasses the
97
+ cache. The report never installs anything and never edits a pin. A failed
98
+ lookup prints `lookup failed: <reason>` and the command still exits 0.
@@ -4,12 +4,12 @@ import { bail } from "../engine";
4
4
  import {
5
5
  engineHome,
6
6
  HARNESSES,
7
- harnessStateFile,
8
7
  LEGACY_SELECTION,
9
8
  readHarnessSelection,
10
9
  writeHarnessSelection,
11
10
  type Harness,
12
11
  } from "../engine-native/harnesses";
12
+ import { kitDbFile } from "../engine-native/kitDb";
13
13
 
14
14
  const DESCRIPTIONS: Record<Harness, string> = {
15
15
  claude: "Claude Code user configuration",
@@ -50,7 +50,7 @@ export const harnessesCommand = Command.make("harnesses", {}, () =>
50
50
  }
51
51
 
52
52
  yield* Effect.sync(() => writeHarnessSelection(home, answer));
53
- yield* Console.log(`Saved harness selection: ${answer.join(", ")} (${harnessStateFile(home)})`);
53
+ yield* Console.log(`Saved harness selection: ${answer.join(", ")} (${kitDbFile(home)})`);
54
54
  }),
55
55
  ).pipe(
56
56
  Command.withDescription("Choose the harness selection that drives a flag-less docks-kit sync."),
@@ -1,7 +1,8 @@
1
1
  import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
2
2
  import { Effect, Option } from "effect";
3
3
  import { bail, engine } from "../engine";
4
- import { modelCatalog } from "../engine-native/models";
4
+ import { engineHome } from "../engine-native/harnesses";
5
+ import { defaultLiveInputs, resolveCatalog } from "../engine-native/liveModels";
5
6
  import type { Tool } from "../manifests";
6
7
 
7
8
  const tool = Argument.String("tool").pipe(Argument.withDescription("Which tool: claude | codex"));
@@ -42,7 +43,9 @@ export const modelCommand = Command.make("model", { tool, value, dryRun, verbose
42
43
  // … and offer an interactive picker when attached to a terminal.
43
44
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
44
45
 
45
- const catalog = modelCatalog(t);
46
+ const catalog = yield* Effect.promise(() =>
47
+ resolveCatalog(t, defaultLiveInputs(engineHome(process.env))),
48
+ );
46
49
  const chosen = yield* Prompt.Select({
47
50
  message: `Set the deployed ${t} model (deployed config only; a flag-less sync reverts to SoT)`,
48
51
  choices: [
@@ -1,48 +1,75 @@
1
1
  import { Argument, Command, Flag } from "effect/unstable/cli";
2
2
  import { Console, Effect, Option } from "effect";
3
3
  import { bail } from "../engine";
4
- import { modelCatalog } from "../engine-native/models";
5
- import type { Tool } from "../manifests";
4
+ import { engineHome, LEGACY_SELECTION, readHarnessSelection } from "../engine-native/harnesses";
5
+ import { defaultLiveInputs, resolveCatalog } from "../engine-native/liveModels";
6
+ import type { CatalogTool, ResolvedCatalog } from "../engine-native/sharedTypes";
6
7
 
7
8
  const tool = Argument.String("tool").pipe(
8
- Argument.withDescription("claude | codex (omit for both tool catalogs)"),
9
+ Argument.withDescription("claude | codex | omp (omit for enabled harnesses)"),
9
10
  Argument.optional,
10
11
  );
11
12
  const json = Flag.Boolean("json").pipe(
12
13
  Flag.withDescription("Machine-readable output"),
13
14
  Flag.withDefault(false),
14
15
  );
16
+ const refresh = Flag.Boolean("refresh").pipe(
17
+ Flag.withDescription("Bypass the kit cache (~/.docks-kit/kit.db) and fetch live lists again"),
18
+ Flag.withDefault(false),
19
+ );
15
20
 
16
- const renderTool = (t: Tool) =>
21
+ function isCatalogTool(value: string): value is CatalogTool {
22
+ return value === "claude" || value === "codex" || value === "omp";
23
+ }
24
+
25
+ const renderTool = (t: CatalogTool, catalog: ResolvedCatalog) =>
17
26
  Effect.gen(function* () {
18
- const catalog = modelCatalog(t);
19
- yield* Console.log(`${t} models (kit-verified ${catalog.verified}):`);
27
+ yield* Console.log(
28
+ catalog.source === "curated"
29
+ ? `${t} models (kit-verified ${catalog.verified}):`
30
+ : `${t} models (live — ${catalog.source}, fetched ${catalog.fetchedAt ?? "?"}):`,
31
+ );
32
+ if (catalog.fallbackReason !== undefined) {
33
+ yield* Console.log(` (live list unavailable: ${catalog.fallbackReason})`);
34
+ }
20
35
  for (const m of catalog.models) {
21
36
  yield* Console.log(` ${m.id.padEnd(28)} ${m.kind.padEnd(6)} ${m.note ?? ""}`);
22
37
  }
23
- yield* Console.log(
24
- t === "claude"
25
- ? " (full claude-* model IDs outside the catalog are accepted with a warning)"
26
- : " (well-formed IDs outside the catalog are accepted with a warning)",
27
- );
38
+ if (t === "claude") {
39
+ yield* Console.log(
40
+ " (full claude-* model IDs outside the catalog are accepted with a warning)",
41
+ );
42
+ } else if (t === "codex") {
43
+ yield* Console.log(" (well-formed IDs outside the catalog are accepted with a warning)");
44
+ }
28
45
  yield* Console.log("");
29
46
  });
30
47
 
31
- export const modelsCommand = Command.make("models", { tool, json }, (config) =>
48
+ export const modelsCommand = Command.make("models", { tool, json, refresh }, (config) =>
32
49
  Effect.gen(function* () {
33
50
  const requested = Option.getOrUndefined(config.tool);
34
- if (requested !== undefined && requested !== "claude" && requested !== "codex") {
35
- return yield* bail(`Unknown tool '${requested}' (valid: claude, codex)`);
36
- }
37
- const tools: Array<Tool> = requested !== undefined ? [requested as Tool] : ["claude", "codex"];
38
-
39
- if (config.json) {
40
- const out = Object.fromEntries(tools.map((t) => [t, modelCatalog(t)]));
41
- return yield* Console.log(JSON.stringify(out, null, 2));
51
+ if (requested !== undefined && !isCatalogTool(requested)) {
52
+ return yield* bail(`Unknown tool '${requested}' (valid: claude, codex, omp)`);
42
53
  }
43
-
54
+ const home = engineHome(process.env);
55
+ const tools: ReadonlyArray<CatalogTool> =
56
+ requested !== undefined
57
+ ? [requested]
58
+ : (readHarnessSelection(home) ?? LEGACY_SELECTION).filter(isCatalogTool);
59
+ const inputs = defaultLiveInputs(home, config.refresh);
60
+ const out: Partial<Record<CatalogTool, ResolvedCatalog>> = {};
44
61
  for (const t of tools) {
45
- yield* renderTool(t);
62
+ const catalog = yield* Effect.promise(() => resolveCatalog(t, inputs));
63
+ if (config.json) {
64
+ out[t] = catalog;
65
+ } else {
66
+ yield* renderTool(t, catalog);
67
+ }
46
68
  }
69
+ if (config.json) yield* Console.log(JSON.stringify(out, null, 2));
47
70
  }),
48
- ).pipe(Command.withDescription("List kit-verified Claude and Codex models (SoT/models.json)."));
71
+ ).pipe(
72
+ Command.withDescription(
73
+ "List models per enabled harness: live from each harness's login or cache, with SoT/models.json aliases and notes as overlay and offline fallback.",
74
+ ),
75
+ );
@@ -5,7 +5,7 @@ import { bail, engine } from "../engine";
5
5
  const MANAGED = ["bun"];
6
6
 
7
7
  const op = Argument.String("op").pipe(
8
- Argument.withDescription("check (default) | ensure <tool>"),
8
+ Argument.withDescription("check (default) | ensure <tool> | outdated"),
9
9
  Argument.optional,
10
10
  );
11
11
  const tool = Argument.String("tool").pipe(
@@ -17,28 +17,39 @@ const verbose = Flag.Boolean("verbose").pipe(
17
17
  Flag.withDescription("Also print no-op confirmations (present, up to date)"),
18
18
  Flag.withDefault(false),
19
19
  );
20
+ const refresh = Flag.Boolean("refresh").pipe(
21
+ Flag.withDescription("outdated: bypass the 24 h cache"),
22
+ Flag.withDefault(false),
23
+ );
20
24
 
21
- export const toolchainCommand = Command.make("toolchain", { op, tool, verbose }, (config) =>
22
- Effect.gen(function* () {
23
- const operation = Option.getOrElse(config.op, () => "check");
24
- const flags = config.verbose ? ["--verbose"] : [];
25
+ export const toolchainCommand = Command.make(
26
+ "toolchain",
27
+ { op, tool, verbose, refresh },
28
+ (config) =>
29
+ Effect.gen(function* () {
30
+ const operation = Option.getOrElse(config.op, () => "check");
31
+ const flags = config.verbose ? ["--verbose"] : [];
25
32
 
26
- switch (operation) {
27
- case "check":
28
- return yield* engine(["toolchain", "check", ...(config.verbose ? ["--verbose"] : [])]);
29
- case "ensure": {
30
- const t = Option.getOrUndefined(config.tool);
31
- if (t === undefined || !MANAGED.includes(t)) {
32
- return yield* bail(`toolchain ensure needs a managed tool: ${MANAGED.join(", ")}`);
33
+ switch (operation) {
34
+ case "check":
35
+ return yield* engine(["toolchain", "check", ...(config.verbose ? ["--verbose"] : [])]);
36
+ case "ensure": {
37
+ const t = Option.getOrUndefined(config.tool);
38
+ if (t === undefined || !MANAGED.includes(t)) {
39
+ return yield* bail(`toolchain ensure needs a managed tool: ${MANAGED.join(", ")}`);
40
+ }
41
+ return yield* engine(["toolchain", "ensure", t, ...flags]);
33
42
  }
34
- return yield* engine(["toolchain", "ensure", t, ...flags]);
43
+ case "outdated":
44
+ return yield* engine(["toolchain", "outdated", ...(config.refresh ? ["--refresh"] : [])]);
45
+ default:
46
+ return yield* bail(
47
+ `Unknown toolchain op '${operation}' (valid: check, ensure, outdated)`,
48
+ );
35
49
  }
36
- default:
37
- return yield* bail(`Unknown toolchain op '${operation}' (valid: check, ensure)`);
38
- }
39
- }),
50
+ }),
40
51
  ).pipe(
41
52
  Command.withDescription(
42
- "Verified-version floors for external tools (SoT/toolchain.json): check prints the doctor table; ensure installs one managed tool when it is missing.",
53
+ "Verified-version floors for external tools (SoT/toolchain.json): check prints the doctor table; ensure installs one managed tool when it is missing; outdated compares verified pins with the newest upstream release (network, report only).",
43
54
  ),
44
55
  );
@@ -1,13 +1,11 @@
1
1
  /**
2
- * Per-machine harness selection at ~/.docks-kit/state.json. The selection keeps
3
- * the omp harness opt-in. A missing or unreadable state file is represented by
4
- * undefined so callers resolve it to LEGACY_SELECTION and existing machines
5
- * keep today's behavior.
2
+ * Per-machine harness selection and omp session model, stored in ~/.docks-kit/kit.db (kitDb.ts).
3
+ * The selection keeps the omp harness opt-in. A missing or unreadable store is
4
+ * represented by undefined so callers resolve it to LEGACY_SELECTION.
6
5
  */
7
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
6
  import { homedir } from "node:os";
9
7
 
10
- import { p } from "./exec";
8
+ import { inTransaction, isNonBlankString, nonBlankOrNull, withKitDb } from "./kitDb";
11
9
  import type { OmpSessionModel } from "./sharedTypes";
12
10
 
13
11
  export type Harness = "claude" | "codex" | "agents" | "omp";
@@ -41,49 +39,21 @@ export function engineHome(env: NodeJS.ProcessEnv = process.env): string {
41
39
  return home !== undefined && home !== "" ? home : homedir();
42
40
  }
43
41
 
44
- export function harnessStateFile(home: string): string {
45
- return p(home, ".docks-kit", "state.json");
46
- }
47
-
48
- // Read the whole state record so one key writer keeps sibling keys intact.
49
- // A corrupt file degrades to undefined so callers fall back to defaults.
50
- function readWholeState(home: string): Record<string, unknown> | undefined {
51
- let parsed: unknown;
42
+ /** Read the stored selection; corruption, a too-new schema, or I/O errors yield undefined. */
43
+ export function readHarnessSelection(home: string): ReadonlyArray<Harness> | undefined {
52
44
  try {
53
- parsed = JSON.parse(readFileSync(harnessStateFile(home), "utf8")) as unknown;
45
+ const selection = withKitDb(home, "read", (db) =>
46
+ normalizeHarnesses(
47
+ db
48
+ .prepare("SELECT harness FROM harness_selection")
49
+ .all()
50
+ .map((row) => row["harness"]),
51
+ ),
52
+ );
53
+ return selection !== undefined && selection.length > 0 ? selection : undefined;
54
54
  } catch {
55
55
  return undefined;
56
56
  }
57
-
58
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
59
- const state = parsed as Record<string, unknown>;
60
- if (state["version"] !== 1) return undefined;
61
- return state;
62
- }
63
-
64
- // Merge the patch over the stored record so independent keys never erase
65
- // each other when only one writer runs.
66
- function writeWholeState(home: string, patch: Record<string, unknown>): void {
67
- const existing = readWholeState(home) ?? {};
68
- const next = { ...existing, ...patch, version: 1 };
69
- const directory = p(home, ".docks-kit");
70
- const file = harnessStateFile(home);
71
- const text = `${JSON.stringify(next, null, 2)}\n`;
72
- // `mode` applies only when mkdir creates the path, so an existing permissive
73
- // ~/.docks-kit would keep its mode.
74
- mkdirSync(directory, { recursive: true, mode: 0o700 });
75
- chmodSync(directory, 0o700);
76
- writeFileSync(file, text, { mode: 0o600 });
77
- chmodSync(file, 0o600);
78
- }
79
-
80
- /** Read valid local state without allowing corruption to make sync unusable. */
81
- export function readHarnessSelection(home: string): ReadonlyArray<Harness> | undefined {
82
- const state = readWholeState(home);
83
- if (state === undefined || !Array.isArray(state["harnesses"])) return undefined;
84
-
85
- const selection = normalizeHarnesses(state["harnesses"]);
86
- return selection.length > 0 ? selection : undefined;
87
57
  }
88
58
 
89
59
  export function writeHarnessSelection(home: string, selection: ReadonlyArray<Harness>): void {
@@ -96,34 +66,37 @@ export function writeHarnessSelection(home: string, selection: ReadonlyArray<Har
96
66
  throw new Error("Harness selection must contain at least one known harness name");
97
67
  }
98
68
 
99
- writeWholeState(home, { harnesses });
100
- }
101
-
102
- function isNonBlankString(value: unknown): value is string {
103
- return typeof value === "string" && value.trim() !== "";
69
+ withKitDb(home, "write", (db) =>
70
+ inTransaction(db, () => {
71
+ db.exec("DELETE FROM harness_selection");
72
+ const insert = db.prepare("INSERT INTO harness_selection (harness) VALUES (?)");
73
+ for (const harness of harnesses) insert.run(harness);
74
+ }),
75
+ );
104
76
  }
105
77
 
106
- // Read the stored session model without throwing so a corrupt entry falls
78
+ // Read the stored session model without throwing so a corrupt store falls
107
79
  // back to the default instead of breaking sync.
108
80
  export function readOmpSessionModel(home: string): OmpSessionModel | undefined {
109
- const state = readWholeState(home);
110
- if (state === undefined) return undefined;
111
- const entry = state["ompSession"];
112
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return undefined;
113
- const record = entry as Record<string, unknown>;
114
- if (!isNonBlankString(record["selector"])) {
81
+ try {
82
+ return withKitDb(home, "read", (db) => {
83
+ const row = db
84
+ .prepare("SELECT selector, thinking, advisor_thinking FROM omp_session WHERE id = 1")
85
+ .get();
86
+ const selector = row?.["selector"];
87
+ if (!isNonBlankString(selector)) return undefined;
88
+ const thinking = row?.["thinking"];
89
+ const advisorThinking = row?.["advisor_thinking"];
90
+ const model: OmpSessionModel = {
91
+ selector,
92
+ ...(isNonBlankString(thinking) ? { thinking } : {}),
93
+ ...(isNonBlankString(advisorThinking) ? { advisorThinking } : {}),
94
+ };
95
+ return model;
96
+ });
97
+ } catch {
115
98
  return undefined;
116
99
  }
117
- // Each level stands alone, so a level-free model reads back with no
118
- // levels while a half-corrupt entry keeps the valid level.
119
- const thinking = record["thinking"];
120
- const advisorThinking = record["advisorThinking"];
121
- const model: OmpSessionModel = {
122
- selector: record["selector"],
123
- ...(isNonBlankString(thinking) ? { thinking } : {}),
124
- ...(isNonBlankString(advisorThinking) ? { advisorThinking } : {}),
125
- };
126
- return model;
127
100
  }
128
101
 
129
102
  export function writeOmpSessionModel(home: string, model: OmpSessionModel): void {
@@ -132,15 +105,16 @@ export function writeOmpSessionModel(home: string, model: OmpSessionModel): void
132
105
  if (!isNonBlankString(model.selector)) {
133
106
  throw new Error("Omp session model selector must be a non-empty string");
134
107
  }
135
- // Persist only non-blank levels so a switch to a level-free model leaves
136
- // no stale level behind in the stored record.
137
- const thinking = model.thinking;
138
- const advisorThinking = model.advisorThinking;
139
- const entry: OmpSessionModel = {
140
- selector: model.selector,
141
- ...(isNonBlankString(thinking) ? { thinking } : {}),
142
- ...(isNonBlankString(advisorThinking) ? { advisorThinking } : {}),
143
- };
144
-
145
- writeWholeState(home, { ompSession: entry });
108
+ // Store blank or absent levels as NULL so a switch to a level-free model
109
+ // leaves no stale level behind.
110
+ const thinking = nonBlankOrNull(model.thinking);
111
+ const advisorThinking = nonBlankOrNull(model.advisorThinking);
112
+ withKitDb(home, "write", (db) =>
113
+ db
114
+ .prepare(
115
+ `INSERT INTO omp_session (id, selector, thinking, advisor_thinking) VALUES (1, ?, ?, ?)
116
+ ON CONFLICT(id) DO UPDATE SET selector = excluded.selector, thinking = excluded.thinking, advisor_thinking = excluded.advisor_thinking`,
117
+ )
118
+ .run(model.selector, thinking, advisorThinking),
119
+ );
146
120
  }
@@ -48,7 +48,7 @@ export async function runEngineNative(
48
48
  ctx = makeCtx(runServices);
49
49
  switch (argv[0]) {
50
50
  case "model":
51
- return modeModel(ctx, argv.slice(1));
51
+ return await modeModel(ctx, argv.slice(1));
52
52
  case "toolchain":
53
53
  return await modeToolchain(ctx, argv.slice(1));
54
54
  case "sync":