docks-kit 0.4.0 → 0.5.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
@@ -32,12 +32,13 @@ Tool-specific instructions live alongside this file:
32
32
 
33
33
  Codex SoT notes:
34
34
  - `SoT/.codex/AGENTS.md` deploys to `~/.codex/AGENTS.md` as global Codex instructions.
35
- - `SoT/.codex/config.toml` pins Codex to `model = "gpt-5.5"`, sets reasoning effort/summaries (`xhigh` + `detailed`), `model_verbosity = "medium"` (Codex ships gpt-5.5 at `low`; medium restores the API default), `personality`, live top-level `web_search`, workspace-write sandboxing with sandboxed command network access, cross-session `memories` (+ dedicated note tools), `[agents]` subagent limits (`max_threads = 12`, `max_depth = 2` — intentionally above Codex defaults for broad parallel kit work; deeper recursion increases cost and predictability risk), a 128 KiB `project_doc_max_bytes` budget for the repo-side AGENTS.md chain (the global `~/.codex/AGENTS.md` is uncapped and not counted), and enables the Docks plugins as `docks@docks` + `session-relay@docks`.
35
+ - `SoT/.codex/config.toml` pins Codex to `model = "gpt-5.6-sol"`, sets reasoning effort/summaries (`xhigh` + `detailed`), `model_verbosity = "medium"`, `personality`, live top-level `web_search`, workspace-write sandboxing with sandboxed command network access, cross-session `memories` (+ dedicated note tools), `[agents]` subagent limits (`max_threads = 12`, `max_depth = 2` — intentionally above Codex defaults for broad parallel kit work; deeper recursion increases cost and predictability risk), a 128 KiB `project_doc_max_bytes` budget for the repo-side AGENTS.md chain (the global `~/.codex/AGENTS.md` is uncapped and not counted), and enables the Docks plugins as `docks@docks` + `session-relay@docks`.
36
36
  - `SoT/.codex/rules/*.rules` deploys to `~/.codex/rules/` as kit-managed Codex command policy. This is Codex's equivalent of permission allow/prompt/block rules; user-learned approvals in `~/.codex/rules/default.rules` are preserved.
37
37
  - `SoT/.codex/plugins/marketplace.json` deploys to Codex's personal marketplace path at `~/.agents/plugins/marketplace.json`; when the `codex` CLI is available, sync reruns `codex plugin add <plugin@marketplace>` for enabled SoT plugins so stale cached installs are refreshed.
38
38
  - The `codex` CLI binary is upstream-owned, not kit-owned. The official standalone installer keeps package metadata under `$CODEX_HOME/packages/standalone` and places the `codex` symlink in `~/.local/bin` by default; sync only warns with a download-then-run installer command when the CLI is missing. Existing installs can self-update with `codex update`; npm and Homebrew remain upstream alternatives.
39
39
  - `SoT/.codex/AGENTS.md` deliberately does not import `@RTK.md`: RTK's published Codex integration is prompt-file based rather than hook based, so importing it leaks implementation detail into agent-visible context. Use Codex hooks for RTK only after the kit installs a hook-backed Codex integration.
40
40
  - Claude runtime settings are an authoring template with sentinels. `claudeRuntime.ts` materializes absolute Bun/script paths only after the shared `bun.ts` bootstrap is ready; `claudeSync.ts` writes all runtime assets before atomically committing settings, then prunes the legacy shell scripts and Stop hook. Native `rate_limits` is the sole quota source, so jq/curl/OAuth caches are not runtime dependencies. A missing Bun defers only this cutover and preserves legacy pointers/files.
41
+ - Claude's deployed SoT defaults are `model: fable` and `effortLevel: high`; `advisorModel` is deliberately absent/off. `--claude-advisor=on` is the per-machine opt-in and writes `advisorModel: fable` after the settings merge.
41
42
 
42
43
  For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`), see the matching SoT directory.
43
44
 
@@ -45,7 +46,7 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`), see the matching SoT d
45
46
 
46
47
  - **Idempotent operations.** Every EngineNative sync step must be safe to re-run. Settings merges, plugin installs, and marketplace adds are all idempotent — re-running with no SoT changes is a no-op.
47
48
  - **Removed bash engine.** The bash engine was removed after the `bash-engine-final` tag. `DOCKS_KIT_ENGINE=bash` must fail with the removed-engine message; engine bugs are fixed forward in EngineNative.
48
- - **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, and `agents`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync codex` for Codex-only config edits); targets can be combined with `--dry-run`, `--skip-rtk`, `--reconcile`, `--prune`, `--yes` (auto-accept toolchain prompts), and the deploy-time modifiers `--claude-compact-window=<tokens>` / `--claude-permissive` / `--claude-model=<m>` / `--codex-model=<m>` (see `CLAUDE.md` § Deploy-time modifiers).
49
+ - **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, and `agents`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync codex` for Codex-only config edits); targets can be combined with `--dry-run`, `--skip-rtk`, `--reconcile`, `--prune`, `--yes` (auto-accept toolchain prompts), 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).
49
50
  - **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 (`claude::_removed_manifest`), a curated list of unambiguous kit-owned artifacts that `claude::sync_removals` force-prunes on every sync; see `CLAUDE.md` § Pruning stale artifacts.
50
51
  - **`--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.
51
52
  - **SOLID-aligned modules.** `cli/src/engine-native/parseArgs.ts` owns flag parsing/validation. `toolchain.ts` owns the verified-version gate over `SoT/toolchain.json`; `bun.ts` owns the shared, memoized Bun bootstrap; `claudeRuntime.ts` owns Claude settings materialization. `claudeSync.ts`, `codexSync.ts`, and `skillsSync.ts` own tool-specific sync logic. `index.ts` is the thin orchestrator. The public CLI seam is `cli/src/engine.ts`.
package/cli/docs/flags.md CHANGED
@@ -27,12 +27,17 @@ docks-kit sync claude agents # two
27
27
  | Flag | Effect |
28
28
  |------|--------|
29
29
  | `--claude-model=<m>` | Deploy-time modifier: deployed model (aliases or full claude-* IDs; `default` unsets) |
30
+ | `--claude-effort=<level>` | Deploy-time modifier: `effortLevel`; valid `low`, `medium`, `high`, `xhigh`, or `default` (Claude SoT: `high`) |
31
+ | `--claude-advisor=<state>` | Deploy-time modifier: advisor `on`, `off`, or `default` (SoT off/unset) |
30
32
  | `--claude-compact-window=<n>` | Deploy-time modifier: autocompact window in tokens (`680000` or `680k`) |
31
33
  | `--claude-permissive` | Deploy-time modifier: empty permissions.ask/deny (sandboxes) |
32
34
  | `--claude-plugin=<name>` | Sticky opt-in plugin (known: supabase, n8n); comma-separate for several |
33
35
  | `--codex-model=<m>` | Deploy-time modifier: deployed Codex model |
36
+ | `--codex-effort=<level>` | Deploy-time modifier: `model_reasoning_effort`; valid `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `ultra`, or `default` (Codex SoT: `xhigh`; model-dependent) |
34
37
 
35
- Bare `--claude-model` / `--codex-model` (no value) prints the model catalog.
38
+ Bare model, effort, or advisor modifiers print the relevant valid-value catalog
39
+ and exit 2. A modifier for a target not selected by the positional arguments is
40
+ ignored with a warning; Claude modifiers never touch Codex config and vice versa.
36
41
 
37
42
  ## Renamed legacy flags (pre-CLI sync.sh)
38
43
 
@@ -26,6 +26,30 @@ are versioned config snapshots without publishing the authoring `SoT/` tree.
26
26
  Kit-home resolution remains available for checkout/package update behavior and
27
27
  display paths, but sync reads do not depend on it.
28
28
 
29
+ ### Bun 1.3.14 blocked-postinstall notice
30
+
31
+ A supported global install may finish successfully with this exact notice:
32
+
33
+ ```
34
+ Blocked 1 postinstall. Run `bun pm -g untrusted` for details.
35
+ ```
36
+
37
+ For the pinned production dependency graph, the diagnostic names only:
38
+
39
+ ```
40
+ ./node_modules/@parcel/watcher @2.5.6
41
+ » [install]: node scripts/build-from-source.js
42
+ ```
43
+
44
+ `@parcel/watcher` is a transitive of the Effect Bun runtime. Supported default
45
+ installs already carry its platform prebuilt package, and that install script
46
+ only attempts a source build when `npm_config_build_from_source=true` was
47
+ explicitly requested. `esbuild` is not in the consumer production graph. The
48
+ blocked notice therefore needs no trust action for the supported default
49
+ install; `docks-kit --version`, model catalogs, toolchain checks, and real sync
50
+ remain functional with the script blocked. CI pins the one-package/one-command
51
+ identity above and will fail if the script-bearing set changes.
52
+
29
53
  ## 3. curl installer (Unix-only)
30
54
 
31
55
  ```
@@ -18,8 +18,8 @@ the entry and date when a model ships or retires.
18
18
  ## The `best` alias and `default` pseudo-value
19
19
 
20
20
  - `best` resolves to Fable 5 where the org has access, latest Opus otherwise.
21
- Needs Claude Code >= 2.1.170. (The kit SoT default is `opus`, paired with
22
- the `fable` advisor see the advisor note below.)
21
+ Needs Claude Code >= 2.1.170. The kit SoT pins `fable` directly, so lack of
22
+ Fable access is surfaced instead of silently changing the configured model.
23
23
  - `default` is an engine pseudo-value: it DELETES the deployed `model` key so
24
24
  the account default applies. It never reaches the settings file as a value.
25
25
 
@@ -29,14 +29,14 @@ the entry and date when a model ships or retires.
29
29
  docks-kit models # both catalogs
30
30
  docks-kit models claude --json # machine-readable
31
31
  docks-kit model claude # current deployed + SoT + picker (TTY)
32
- docks-kit model claude fable # set deployed model
32
+ docks-kit model claude opus # per-machine override from the Fable SoT
33
33
  docks-kit sync claude --claude-model=opus # same, as part of a sync
34
34
  ```
35
35
 
36
36
  ## Advisor pairing note (Claude)
37
37
 
38
- The advisor model must be at least as capable as the main model; Fable
39
- outranks Opus, so an Opus main + Fable advisor works while Fable main +
40
- Opus advisor is rejected. The SoT ships `model: opus` + `advisorModel:
41
- fable` as the deployed-everywhere default; use settings.local.json only
42
- for a deliberate per-machine deviation.
38
+ The SoT ships `model: fable` with advisor off (`advisorModel` unset).
39
+ Advisor is a per-machine opt-in: `docks-kit sync claude --claude-advisor=on`
40
+ writes `advisorModel: fable`; `off` and `default` delete the key. Fable-main +
41
+ Fable-advisor is an accepted pairing. The advisor needs Fable org access and
42
+ Claude Code >= 2.1.170.
@@ -5,15 +5,32 @@ the SoT is never touched. They all share one contract:
5
5
 
6
6
  > A later flag-less sync reverts the modifier: the settings merge (Claude)
7
7
  > and the config.toml merge (Codex) re-assert SoT values for kit-owned keys.
8
- > Re-pass the flag on machines that should keep the override, or make it
9
- > persistent in `~/.claude/settings.local.json` (which sync never touches).
8
+ > Re-pass the flag on machines that should keep the override. Claude-only
9
+ > profiles can instead use `~/.claude/settings.local.json`, which sync never
10
+ > touches.
11
+
12
+ Claude's embedded SoT is `model: fable`, `effortLevel: high`, with advisor
13
+ off (`advisorModel` unset). Codex's embedded reasoning effort is `xhigh`.
10
14
 
11
15
  | Modifier | Deployed change | Typical use |
12
16
  |----------|-----------------|-------------|
13
- | `--claude-model=<m>` | `.model` in ~/.claude/settings.json (`default` deletes the key) | Drop one machine to opus while the SoT stays on `best` |
17
+ | `--claude-model=<m>` | `.model` in ~/.claude/settings.json (`default` deletes the key) | Override one machine while the SoT retains `fable` |
18
+ | `--claude-effort=<level>` | `.effortLevel` in ~/.claude/settings.json (`default` writes `high`) | Tune persisted Claude effort per machine; valid `low`, `medium`, `high`, `xhigh` |
19
+ | `--claude-advisor=<state>` | `on` sets `.advisorModel = "fable"`; `off`/`default` remove it | Enable Claude advisor only on machines that need it |
14
20
  | `--claude-compact-window=<n>` | `env.CLAUDE_CODE_AUTO_COMPACT_WINDOW` | Disposable containers running long autonomous work (e.g. `680k`) — not host machines |
15
21
  | `--claude-permissive` | `permissions.ask = []`, `permissions.deny = []` | Sandboxes/containers where prompts stall unattended work. Never on a host — the deny list is the safety floor |
16
22
  | `--codex-model=<m>` | top-level `model = "…"` in ~/.codex/config.toml | Same as claude-model, for Codex |
23
+ | `--codex-effort=<level>` | top-level `model_reasoning_effort = "…"` (`default` writes `xhigh`) | Tune Codex effort per machine; valid `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `ultra` (model-dependent) |
24
+
25
+ Bare effort/advisor flags print their verified catalog and exit 2. Invalid
26
+ values do the same with a clear error. Passing a tool-specific modifier without
27
+ selecting that positional target warns and ignores it.
28
+
29
+ A flag-less Claude sync also removes the formerly kit-owned `advisorModel`
30
+ from machines synced before advisor became opt-in. Any explicit advisor state
31
+ owns that key for the run: `on` writes `fable`; `off` and `default` delete it.
32
+ Codex has no advisor modifier because its documented config has no advisor
33
+ setting; `review_model` applies only to `/review`.
17
34
 
18
35
  ## Standalone get/set (no full sync)
19
36
 
@@ -15,18 +15,21 @@ Order matters — runtime readiness and settings form one transaction:
15
15
  unavailable, omit only the new runtime pointers and preserve legacy ones.
16
16
  3. When ready, write `bin/statusline.mjs`, `bin/session-start.mjs`,
17
17
  `bin/notify.mjs`, and `notification.mp3`; deploy CLAUDE.md; atomically commit
18
- settings; then prune the old shell assets and Stop hook.
18
+ settings.
19
19
  4. **settings.json merge semantics** — additive: SoT keys win, permissions arrays are
20
20
  unioned, user-only keys survive. `--reconcile` replaces permissions arrays
21
21
  wholesale instead.
22
- 5. **Deploy-time modifiers** (`--claude-compact-window`, `--claude-permissive`,
23
- `--claude-model`) deployed file only.
24
- 6. ~/.claude.json (showTurnDuration, user-scoped MCP servers), connector env
25
- export, removed-artifact pruning.
26
- 7. **Plugins**seven idempotent passes via the `claude plugin` CLI
22
+ 5. **Removed-artifact pruning** prune old shell assets, the Stop hook, and
23
+ stale kit-owned settings. A flag-less sync removes `advisorModel`; an
24
+ explicit advisor state excludes only that key so the modifier owns it.
25
+ 6. **Deploy-time modifiers** (`--claude-compact-window`, `--claude-permissive`,
26
+ `--claude-model`, `--claude-effort`, `--claude-advisor`) deployed file only.
27
+ 7. ~/.claude.json (showTurnDuration, user-scoped MCP servers) and connector env
28
+ export.
29
+ 8. **Plugins** — seven idempotent passes via the `claude plugin` CLI
27
30
  (marketplaces → install → update → [--prune: uninstall/remove] → re-assert
28
31
  SoT enabled-state). Optional opt-ins via `--claude-plugin=<name>`.
29
- 8. LSP server binaries (npm globals).
32
+ 9. LSP server binaries (npm globals).
30
33
 
31
34
  The statusline reads Claude's native `rate_limits`. There is no OAuth request,
32
35
  usage cache, jq/curl runtime dependency, or Stop fetch hook.
@@ -35,8 +38,8 @@ usage cache, jq/curl runtime dependency, or Stop fetch hook.
35
38
 
36
39
  bubblewrap check (Linux), config.toml merge (top-level keys replaced
37
40
  per-key, [table] blocks replaced wholesale, user-only keys/tables preserved),
38
- `--codex-model` modifier, rules, AGENTS.md, personal marketplace file,
39
- `codex plugin add` refresh.
41
+ `--codex-model` then `--codex-effort` modifiers, rules, AGENTS.md, personal
42
+ marketplace file, `codex plugin add` refresh.
40
43
 
41
44
  ## agents (→ ~/.agents/skills, ~/.claude/skills symlinks)
42
45
 
@@ -5,6 +5,12 @@ import { existsSync } from "node:fs"
5
5
  import { join } from "node:path"
6
6
  import { bail, engine } from "../engine"
7
7
  import type { Logger } from "../engine-native/logger"
8
+ import {
9
+ advisorCatalog,
10
+ advisorFlagGrammar,
11
+ effortCatalog,
12
+ effortFlagGrammar
13
+ } from "../efforts"
8
14
  import { kitHome } from "../kitHome"
9
15
  import { modelCatalog, type Tool } from "../manifests"
10
16
  import { LoggerService } from "../services"
@@ -49,7 +55,7 @@ const LEGACY_HINTS: Record<string, string> = {
49
55
  "--agents": "--agents was renamed: pass the target as a word, e.g. 'sync agents'"
50
56
  }
51
57
 
52
- const catalogHint = (t: Tool): string => {
58
+ const modelCatalogHint = (t: Tool): string => {
53
59
  const c = modelCatalog(t)
54
60
  const list = c.models
55
61
  .map((m) => ` ${m.id}${m.note !== undefined ? ` — ${m.note}` : ""}`)
@@ -85,6 +91,14 @@ const claudeModel = Options.text("claude-model").pipe(
85
91
  Options.withDescription("Deploy-time modifier: set deployed Claude model (see `docks-kit models claude`)"),
86
92
  Options.optional
87
93
  )
94
+ const claudeEffort = Options.text("claude-effort").pipe(
95
+ Options.withDescription("Deploy-time modifier: set Claude effortLevel (bare flag shows valid levels)"),
96
+ Options.optional
97
+ )
98
+ const claudeAdvisor = Options.text("claude-advisor").pipe(
99
+ Options.withDescription("Deploy-time modifier: set Claude advisor on/off/default"),
100
+ Options.optional
101
+ )
88
102
  const claudeCompactWindow = Options.text("claude-compact-window").pipe(
89
103
  Options.withDescription("Deploy-time modifier: set deployed autocompact window in tokens (e.g. 680000 or 680k)"),
90
104
  Options.optional
@@ -102,6 +116,10 @@ const codexModel = Options.text("codex-model").pipe(
102
116
  Options.withDescription("Deploy-time modifier: set deployed Codex model (see `docks-kit models codex`)"),
103
117
  Options.optional
104
118
  )
119
+ const codexEffort = Options.text("codex-effort").pipe(
120
+ Options.withDescription("Deploy-time modifier: set Codex model_reasoning_effort (bare flag shows valid levels)"),
121
+ Options.optional
122
+ )
105
123
 
106
124
  export const syncCommand = Command.make(
107
125
  "sync",
@@ -114,10 +132,13 @@ export const syncCommand = Command.make(
114
132
  yes,
115
133
  verbose,
116
134
  claudeModel,
135
+ claudeEffort,
136
+ claudeAdvisor,
117
137
  claudeCompactWindow,
118
138
  claudePermissive,
119
139
  claudePlugin,
120
- codexModel
140
+ codexModel,
141
+ codexEffort
121
142
  },
122
143
  (config) =>
123
144
  Effect.gen(function* () {
@@ -125,7 +146,14 @@ export const syncCommand = Command.make(
125
146
  if (VALID_TARGETS.includes(t)) continue
126
147
  if (t === "--claude-model" || t === "--codex-model") {
127
148
  const tool: Tool = t === "--claude-model" ? "claude" : "codex"
128
- return yield* bail(`${catalogHint(tool)}\n${t} requires a value: ${t}=<model>`)
149
+ return yield* bail(`${modelCatalogHint(tool)}\n${t} requires a value: ${t}=<model>`)
150
+ }
151
+ if (t === "--claude-effort" || t === "--codex-effort") {
152
+ const tool: Tool = t === "--claude-effort" ? "claude" : "codex"
153
+ return yield* bail(`${effortCatalog(tool)}\n${t} requires a value: ${effortFlagGrammar(tool)}`)
154
+ }
155
+ if (t === "--claude-advisor") {
156
+ return yield* bail(`${advisorCatalog()}\n${t} requires a value: ${advisorFlagGrammar()}`)
129
157
  }
130
158
  const hint = LEGACY_HINTS[t]
131
159
  if (hint !== undefined) {
@@ -148,8 +176,11 @@ export const syncCommand = Command.make(
148
176
  if (config.verbose) args.push("--verbose")
149
177
  if (config.claudePermissive) args.push("--claude-permissive")
150
178
  Option.map(config.claudeModel, (m) => args.push(`--claude-model=${m}`))
179
+ Option.map(config.claudeEffort, (level) => args.push(`--claude-effort=${level}`))
180
+ Option.map(config.claudeAdvisor, (state) => args.push(`--claude-advisor=${state}`))
151
181
  Option.map(config.claudeCompactWindow, (w) => args.push(`--claude-compact-window=${w}`))
152
182
  Option.map(config.codexModel, (m) => args.push(`--codex-model=${m}`))
183
+ Option.map(config.codexEffort, (level) => args.push(`--codex-effort=${level}`))
153
184
  for (const occurrence of config.claudePlugin) {
154
185
  occurrence
155
186
  .split(",")
@@ -0,0 +1,91 @@
1
+ import { sotClaudeSettings, type Tool } from "./manifests"
2
+ import { payloadText } from "./payload"
3
+
4
+ export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"] as const
5
+ export const CODEX_REASONING_EFFORTS = [
6
+ "none",
7
+ "minimal",
8
+ "low",
9
+ "medium",
10
+ "high",
11
+ "xhigh",
12
+ "max",
13
+ "ultra"
14
+ ] as const
15
+ export const CLAUDE_ADVISOR_STATES = ["on", "off", "default"] as const
16
+
17
+ const VERIFIED = "2026-07-10"
18
+ const DEFAULT = "default"
19
+
20
+ export type ClaudeEffortLevel = typeof CLAUDE_EFFORT_LEVELS[number]
21
+ export type CodexReasoningEffort = typeof CODEX_REASONING_EFFORTS[number]
22
+ export type ClaudeAdvisorState = typeof CLAUDE_ADVISOR_STATES[number]
23
+
24
+ const upstreamEfforts = (tool: Tool): ReadonlyArray<string> =>
25
+ tool === "claude" ? CLAUDE_EFFORT_LEVELS : CODEX_REASONING_EFFORTS
26
+
27
+ export const effortModifierValues = (tool: Tool): ReadonlyArray<string> => [
28
+ ...upstreamEfforts(tool),
29
+ DEFAULT
30
+ ]
31
+
32
+ export const effortValueGrammar = (tool: Tool): string => effortModifierValues(tool).join("|")
33
+
34
+ export const effortFlagGrammar = (tool: Tool): string =>
35
+ `--${tool}-effort=<${effortValueGrammar(tool)}>`
36
+
37
+ export const advisorValueGrammar = (): string => CLAUDE_ADVISOR_STATES.join("|")
38
+
39
+ export const advisorFlagGrammar = (): string => `--claude-advisor=<${advisorValueGrammar()}>`
40
+
41
+ export const isEffortModifierValue = (tool: Tool, value: string): boolean =>
42
+ effortModifierValues(tool).includes(value)
43
+
44
+ export function validateEffortDefault(tool: Tool, value: unknown): string {
45
+ const toolName = tool === "claude" ? "Claude" : "Codex"
46
+ const setting = tool === "claude" ? "effortLevel" : "model_reasoning_effort"
47
+ if (typeof value !== "string" || value === "") {
48
+ throw new Error(`Embedded SoT ${toolName} ${setting} is missing`)
49
+ }
50
+ if (!upstreamEfforts(tool).includes(value)) {
51
+ throw new Error(`Embedded SoT ${toolName} ${setting} '${value}' is outside the verified catalog`)
52
+ }
53
+ return value
54
+ }
55
+
56
+ function codexSotEffort(): string | undefined {
57
+ return payloadText("SoT/.codex/config.toml").match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1]
58
+ }
59
+
60
+ export function sotEffort(tool: Tool): string {
61
+ const value = tool === "claude" ? sotClaudeSettings().effortLevel : codexSotEffort()
62
+ return validateEffortDefault(tool, value)
63
+ }
64
+
65
+ export function resolveEffort(tool: Tool, value: string): string {
66
+ if (value === DEFAULT) return sotEffort(tool)
67
+ if (!upstreamEfforts(tool).includes(value)) {
68
+ throw new Error(`Invalid ${tool} effort '${value}'`)
69
+ }
70
+ return value
71
+ }
72
+
73
+ export function effortCatalog(tool: Tool): string {
74
+ const setting = tool === "claude" ? "effortLevel" : "model_reasoning_effort"
75
+ const lines = [
76
+ `Available ${tool} effort levels (${setting}; verified ${VERIFIED}):`,
77
+ ...upstreamEfforts(tool).map((value) => ` ${value}`),
78
+ ` default — SoT: ${sotEffort(tool)}`
79
+ ]
80
+ if (tool === "codex") lines.push(" (support is model-dependent)")
81
+ return lines.join("\n")
82
+ }
83
+
84
+ export function advisorCatalog(): string {
85
+ return [
86
+ `Available claude advisor states (advisorModel; verified ${VERIFIED}):`,
87
+ " on — set advisorModel: fable",
88
+ " off — unset advisorModel",
89
+ " default — SoT: off (unset)"
90
+ ].join("\n")
91
+ }
@@ -0,0 +1,101 @@
1
+ /** Atomic top-level Claude settings modifiers, including direct model mode. */
2
+ import { p } from "./exec"
3
+ import { readFileSync, renameSync, writeFileSync } from "node:fs"
4
+
5
+ import { resolveEffort } from "../efforts"
6
+ import type { Ctx } from "./index"
7
+ import { isObject, jqStringify, parseJson } from "./jq"
8
+
9
+ interface ClaudeSettingEdit {
10
+ readonly tag: string
11
+ readonly key: "model" | "effortLevel" | "advisorModel"
12
+ readonly value: string | undefined
13
+ readonly dryRun: string
14
+ readonly changed: string
15
+ readonly unchanged: string
16
+ }
17
+
18
+ function syncClaudeSetting(ctx: Ctx, edit: ClaudeSettingEdit): void {
19
+ const { change, echo, err, verbose, warn } = ctx.services.logger
20
+ const userSettings = p(ctx.home, ".claude", "settings.json")
21
+
22
+ if (ctx.dryRun) {
23
+ echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`)
24
+ return
25
+ }
26
+
27
+ let text: string
28
+ try {
29
+ text = readFileSync(userSettings, "utf8")
30
+ } catch {
31
+ warn(`(${edit.tag}) ${userSettings} missing — skipped`)
32
+ return
33
+ }
34
+ const doc = parseJson(text)
35
+ if (doc === undefined) {
36
+ err(`(${edit.tag}) ${userSettings} is not valid JSON — skipped`)
37
+ return
38
+ }
39
+ if (isObject(doc)) {
40
+ if (edit.value === undefined) delete doc[edit.key]
41
+ else doc[edit.key] = edit.value
42
+ }
43
+ const out = jqStringify(doc)
44
+ if (out === text) {
45
+ verbose(edit.unchanged)
46
+ return
47
+ }
48
+ writeFileSync(`${userSettings}.tmp`, out)
49
+ renameSync(`${userSettings}.tmp`, userSettings)
50
+ change(edit.changed)
51
+ ctx.nextStepTriggers.claudeRestart = true
52
+ }
53
+
54
+ export function syncClaudeModel(ctx: Ctx, model: string): void {
55
+ if (model === "") return
56
+ const unset = model === "default"
57
+ syncClaudeSetting(ctx, {
58
+ tag: "--claude-model",
59
+ key: "model",
60
+ value: unset ? undefined : model,
61
+ dryRun: unset ? "delete .model (account default applies)" : `set .model=${model}`,
62
+ changed: `Model: deployed settings model set to ${model} (SoT unchanged; flag-less sync reverts)`,
63
+ unchanged: `Model: deployed settings model already ${unset ? "unset (account default)" : model}`
64
+ })
65
+ }
66
+
67
+ export function syncClaudeEffort(ctx: Ctx, effort: string): void {
68
+ if (effort === "") return
69
+ const resolved = resolveEffort("claude", effort)
70
+ const useDefault = effort === "default"
71
+ syncClaudeSetting(ctx, {
72
+ tag: "--claude-effort",
73
+ key: "effortLevel",
74
+ value: resolved,
75
+ dryRun: `set .effortLevel=${resolved}`,
76
+ changed: useDefault
77
+ ? `Effort: deployed settings effortLevel set to ${resolved} (SoT default)`
78
+ : `Effort: deployed settings effortLevel set to ${resolved} (SoT unchanged; flag-less sync reverts)`,
79
+ unchanged: `Effort: deployed settings effortLevel already ${resolved}`
80
+ })
81
+ }
82
+
83
+ export function syncClaudeAdvisor(ctx: Ctx, state: string): void {
84
+ if (state === "") return
85
+ const enabled = state === "on"
86
+ const useDefault = state === "default"
87
+ syncClaudeSetting(ctx, {
88
+ tag: "--claude-advisor",
89
+ key: "advisorModel",
90
+ value: enabled ? "fable" : undefined,
91
+ dryRun: enabled ? "set .advisorModel=fable" : "delete .advisorModel (advisor disabled)",
92
+ changed: enabled
93
+ ? "Advisor: deployed settings advisorModel set to fable (SoT unchanged; flag-less sync reverts)"
94
+ : useDefault
95
+ ? "Advisor: deployed settings advisorModel unset (SoT default: off)"
96
+ : "Advisor: deployed settings advisorModel unset (--claude-advisor=off; SoT unchanged)",
97
+ unchanged: enabled
98
+ ? "Advisor: deployed settings advisorModel already fable"
99
+ : `Advisor: deployed settings advisorModel already unset (${useDefault ? "SoT default: off" : "advisor off"})`
100
+ })
101
+ }
@@ -17,7 +17,11 @@ import {
17
17
  } from "node:fs"
18
18
  import { tmpdir } from "node:os"
19
19
  import { bunBootstrap } from "./bun"
20
- import { syncClaudeModel } from "./claudeModel"
20
+ import {
21
+ syncClaudeAdvisor,
22
+ syncClaudeEffort,
23
+ syncClaudeModel
24
+ } from "./claudeSettingsModifiers"
21
25
  import { claudeRuntimePaths, materializeClaudeSettings, type ClaudeRuntimePaths } from "./claudeRuntime"
22
26
  import { p, writeBytesIfChanged, writeFileIfChanged, writeTextIfChanged } from "./exec"
23
27
  import type { Ctx } from "./index"
@@ -73,6 +77,8 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
73
77
  syncCompactWindow(ctx, claudeDir)
74
78
  syncPermissive(ctx, claudeDir)
75
79
  syncClaudeModel(ctx, ctx.claudeModel)
80
+ syncClaudeEffort(ctx, ctx.claudeEffort)
81
+ syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
76
82
  syncClaudeJson(ctx)
77
83
  syncConnectorEnv(ctx)
78
84
  syncPlugins(ctx, claudeDir)
@@ -489,6 +495,7 @@ const REMOVED_MANIFEST = {
489
495
  files: ["alert_bubble.mp3"],
490
496
  settingsKeys: [
491
497
  "showTurnDuration",
498
+ "advisorModel",
492
499
  "env.CLAUDE_CODE_SUBAGENT_MODEL",
493
500
  "env.ANTHROPIC_DEFAULT_OPUS_MODEL",
494
501
  "env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
@@ -550,7 +557,7 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
550
557
  ...(runtime.kind === "ready" ? REMOVED_MANIFEST.runtimeReady.files : [])
551
558
  ]
552
559
  const settingsKeys = [
553
- ...REMOVED_MANIFEST.settingsKeys,
560
+ ...REMOVED_MANIFEST.settingsKeys.filter((key) => key !== "advisorModel" || ctx.claudeAdvisor === ""),
554
561
  ...(runtime.kind === "ready" ? REMOVED_MANIFEST.runtimeReady.settingsKeys : [])
555
562
  ]
556
563
 
@@ -6,7 +6,7 @@
6
6
  import { spawnSync } from "node:child_process"
7
7
  import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
8
8
 
9
- import { syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
9
+ import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
10
10
  import { p } from "./exec"
11
11
  import type { Ctx } from "./index"
12
12
  import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
@@ -21,6 +21,7 @@ export function codexSync(ctx: Ctx): void {
21
21
  if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
22
22
  syncConfig(ctx, sotConfig, userConfig)
23
23
  syncCodexModel(ctx, ctx.codexModel)
24
+ syncCodexEffort(ctx, ctx.codexEffort)
24
25
  syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
25
26
  syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
26
27
  syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
@@ -7,6 +7,7 @@
7
7
  import { p } from "./exec"
8
8
  import { readFileSync, renameSync, writeFileSync } from "node:fs"
9
9
 
10
+ import { resolveEffort } from "../efforts"
10
11
  import type { Ctx } from "./index"
11
12
 
12
13
  export function replaceTopLevelSetting(content: string, key: string, replacement: string): string {
@@ -48,27 +49,59 @@ export function replaceTopLevelSettingInFile(file: string, key: string, replacem
48
49
  return true
49
50
  }
50
51
 
51
- export function syncCodexModel(ctx: Ctx, model: string): void {
52
+ interface CodexSettingEdit {
53
+ readonly tag: string
54
+ readonly key: "model" | "model_reasoning_effort"
55
+ readonly value: string
56
+ readonly changed: string
57
+ readonly unchanged: string
58
+ }
59
+
60
+ function syncCodexSetting(ctx: Ctx, edit: CodexSettingEdit): void {
52
61
  const { change, echo, verbose, warn } = ctx.services.logger
53
62
  const userCodexSettings = p(ctx.home, ".codex", "config.toml")
54
63
 
55
- if (model === "") return
56
-
57
64
  if (ctx.dryRun) {
58
- echo(`[dry-run] (--codex-model) set model = "${model}" in ${userCodexSettings}`)
65
+ echo(`[dry-run] (${edit.tag}) set ${edit.key} = "${edit.value}" in ${userCodexSettings}`)
59
66
  return
60
67
  }
61
68
 
62
69
  try {
63
70
  readFileSync(userCodexSettings)
64
71
  } catch {
65
- warn(`(--codex-model) ${userCodexSettings} missing — skipped`)
72
+ warn(`(${edit.tag}) ${userCodexSettings} missing — skipped`)
66
73
  return
67
74
  }
68
- if (replaceTopLevelSettingInFile(userCodexSettings, "model", `model = "${model}"`)) {
69
- change(`Model: deployed Codex model set to ${model} (SoT unchanged; flag-less sync reverts)`)
75
+ if (replaceTopLevelSettingInFile(userCodexSettings, edit.key, `${edit.key} = "${edit.value}"`)) {
76
+ change(edit.changed)
70
77
  ctx.nextStepTriggers.codexRestart = true
71
78
  } else {
72
- verbose(`Model: deployed Codex model already ${model}`)
79
+ verbose(edit.unchanged)
73
80
  }
74
81
  }
82
+
83
+ export function syncCodexModel(ctx: Ctx, model: string): void {
84
+ if (model === "") return
85
+ syncCodexSetting(ctx, {
86
+ tag: "--codex-model",
87
+ key: "model",
88
+ value: model,
89
+ changed: `Model: deployed Codex model set to ${model} (SoT unchanged; flag-less sync reverts)`,
90
+ unchanged: `Model: deployed Codex model already ${model}`
91
+ })
92
+ }
93
+
94
+ export function syncCodexEffort(ctx: Ctx, effort: string): void {
95
+ if (effort === "") return
96
+ const resolved = resolveEffort("codex", effort)
97
+ const useDefault = effort === "default"
98
+ syncCodexSetting(ctx, {
99
+ tag: "--codex-effort",
100
+ key: "model_reasoning_effort",
101
+ value: resolved,
102
+ changed: useDefault
103
+ ? `Effort: deployed Codex model_reasoning_effort set to ${resolved} (SoT default)`
104
+ : `Effort: deployed Codex model_reasoning_effort set to ${resolved} (SoT unchanged; flag-less sync reverts)`,
105
+ unchanged: `Effort: deployed Codex model_reasoning_effort already ${resolved}`
106
+ })
107
+ }
@@ -15,7 +15,14 @@ import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
15
15
  import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
16
16
  import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
17
17
  import { modeModel, modeToolchain } from "./modes"
18
- import { ExitError, parseArgs, validateModelFlags } from "./parseArgs"
18
+ import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
19
+
20
+ export type ModifierFlag =
21
+ | "--claude-model"
22
+ | "--claude-effort"
23
+ | "--claude-advisor"
24
+ | "--codex-model"
25
+ | "--codex-effort"
19
26
 
20
27
  export interface Ctx {
21
28
  readonly repoDir: string
@@ -31,7 +38,12 @@ export interface Ctx {
31
38
  claudePermissive: boolean
32
39
  claudePlugins: Array<string>
33
40
  claudeModel: string
41
+ claudeEffort: string
42
+ claudeAdvisor: string
34
43
  codexModel: string
44
+ codexEffort: string
45
+ /** Distinguishes an explicitly empty modifier from an option that was not supplied. */
46
+ modifierFlags?: Set<ModifierFlag>
35
47
  /** Injected capability seam (logger/deps/platform) — see services.ts. */
36
48
  readonly services: EngineServices
37
49
  bunRuntime?: BunRuntimeState
@@ -66,7 +78,11 @@ function makeCtx(services: EngineServices): Ctx {
66
78
  claudePermissive: env["CLAUDE_PERMISSIVE"] === "1",
67
79
  claudePlugins: (env["CLAUDE_PLUGINS"] ?? "").split(" ").filter((s) => s !== ""),
68
80
  claudeModel: env["CLAUDE_MODEL"] ?? "",
81
+ claudeEffort: "",
82
+ claudeAdvisor: "",
69
83
  codexModel: env["CODEX_MODEL"] ?? "",
84
+ codexEffort: "",
85
+ modifierFlags: new Set(),
70
86
  services,
71
87
  targetFilterSet: false,
72
88
  syncClaude: false,
@@ -79,7 +95,7 @@ function makeCtx(services: EngineServices): Ctx {
79
95
  function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
80
96
  const { echo } = ctx.services.logger
81
97
  parseArgs(ctx, args)
82
- validateModelFlags(ctx)
98
+ validateModifierFlags(ctx)
83
99
 
84
100
  const claudeRan = ctx.syncClaude
85
101
  const claudeRuntime = claudeRan ? claudeSync(ctx) : undefined
@@ -6,7 +6,7 @@ import { p } from "./exec"
6
6
  import { readFileSync } from "node:fs"
7
7
  import { payloadText } from "../payload"
8
8
 
9
- import { syncClaudeModel } from "./claudeModel"
9
+ import { syncClaudeModel } from "./claudeSettingsModifiers"
10
10
  import { syncCodexModel } from "./codexToml"
11
11
  import type { Ctx } from "./index"
12
12
  import { isObject, parseJson, type Json } from "./jq"
@@ -4,7 +4,17 @@
4
4
  * early parser exit and is caught once in runEngineNative.
5
5
  */
6
6
 
7
- import type { Ctx } from "./index"
7
+ import type { Ctx, ModifierFlag } from "./index"
8
+ import {
9
+ CLAUDE_ADVISOR_STATES,
10
+ advisorCatalog,
11
+ advisorFlagGrammar,
12
+ advisorValueGrammar,
13
+ effortCatalog,
14
+ effortFlagGrammar,
15
+ effortValueGrammar,
16
+ isEffortModifierValue
17
+ } from "../efforts"
8
18
  import { printModels, validateClaudeModel, validateCodexModel } from "./models"
9
19
 
10
20
  export class ExitError extends Error {
@@ -14,6 +24,13 @@ export class ExitError extends Error {
14
24
  }
15
25
 
16
26
  const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
27
+ const MODIFIER_FLAGS = new Set<ModifierFlag>([
28
+ "--claude-model",
29
+ "--claude-effort",
30
+ "--claude-advisor",
31
+ "--codex-model",
32
+ "--codex-effort"
33
+ ])
17
34
 
18
35
  function usage(ctx: Ctx): void {
19
36
  const { echo } = ctx.services.logger
@@ -41,6 +58,8 @@ function usage(ctx: Ctx): void {
41
58
  echo(
42
59
  " --claude-model=<m> set deployed ~/.claude/settings.json model (aliases: best|opus|fable|sonnet|haiku, full claude-* IDs, or 'default' to unset)"
43
60
  )
61
+ echo(` ${effortFlagGrammar("claude").padEnd(39)} set deployed effortLevel`)
62
+ echo(` ${advisorFlagGrammar().padEnd(39)} set deployed advisor state`)
44
63
  echo(
45
64
  " --claude-compact-window=<n> set deployed autocompact window in tokens (e.g. 680000 or 680k) for disposable sessions"
46
65
  )
@@ -48,6 +67,7 @@ function usage(ctx: Ctx): void {
48
67
  " --claude-permissive empty permissions.ask/deny in deployed settings (sandboxes/containers; unattended commits + pushes)"
49
68
  )
50
69
  echo(" --codex-model=<m> set deployed ~/.codex/config.toml model (e.g. gpt-5.5)")
70
+ echo(` ${effortFlagGrammar("codex").padEnd(39)} set deployed model_reasoning_effort`)
51
71
  echo("")
52
72
  echo("Sticky opt-ins (installed + enabled until --prune)")
53
73
  echo(
@@ -81,9 +101,42 @@ function selectTarget(ctx: Ctx, target: string): void {
81
101
  ctx.targetFilterSet = true
82
102
  }
83
103
 
104
+ function setModifier(ctx: Ctx, flag: ModifierFlag, value: string): void {
105
+ switch (flag) {
106
+ case "--claude-model":
107
+ ctx.claudeModel = value
108
+ break
109
+ case "--claude-effort":
110
+ ctx.claudeEffort = value
111
+ break
112
+ case "--claude-advisor":
113
+ ctx.claudeAdvisor = value
114
+ break
115
+ case "--codex-model":
116
+ ctx.codexModel = value
117
+ break
118
+ case "--codex-effort":
119
+ ctx.codexEffort = value
120
+ break
121
+ }
122
+ const flags = ctx.modifierFlags ?? new Set<ModifierFlag>()
123
+ flags.add(flag)
124
+ ctx.modifierFlags = flags
125
+ }
126
+
127
+ function isModifierFlag(value: string): value is ModifierFlag {
128
+ return MODIFIER_FLAGS.has(value as ModifierFlag)
129
+ }
130
+
84
131
  export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
85
132
  const { err } = ctx.services.logger
86
- for (const arg of args) {
133
+ for (let index = 0; index < args.length; index += 1) {
134
+ const arg = args[index] ?? ""
135
+ if (isModifierFlag(arg) && args[index + 1] === "") {
136
+ setModifier(ctx, arg, "")
137
+ index += 1
138
+ continue
139
+ }
87
140
  switch (arg) {
88
141
  case "claude":
89
142
  case "codex":
@@ -116,6 +169,18 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
116
169
  printModels(ctx, "codex")
117
170
  err("--codex-model requires a value: --codex-model=<model>")
118
171
  throw new ExitError(2)
172
+ case "--claude-effort":
173
+ printCatalog(ctx, effortCatalog("claude"))
174
+ err(`--claude-effort requires a value: ${effortFlagGrammar("claude")}`)
175
+ throw new ExitError(2)
176
+ case "--codex-effort":
177
+ printCatalog(ctx, effortCatalog("codex"))
178
+ err(`--codex-effort requires a value: ${effortFlagGrammar("codex")}`)
179
+ throw new ExitError(2)
180
+ case "--claude-advisor":
181
+ printCatalog(ctx, advisorCatalog())
182
+ err(`--claude-advisor requires a value: ${advisorFlagGrammar()}`)
183
+ throw new ExitError(2)
119
184
  case "--claude-compact-window":
120
185
  err("--claude-compact-window requires a value: --claude-compact-window=<tokens> (e.g. 680k)")
121
186
  throw new ExitError(2)
@@ -159,9 +224,15 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
159
224
  break
160
225
  }
161
226
  if (arg.startsWith("--claude-model=")) {
162
- ctx.claudeModel = arg.slice("--claude-model=".length)
227
+ setModifier(ctx, "--claude-model", arg.slice("--claude-model=".length))
163
228
  } else if (arg.startsWith("--codex-model=")) {
164
- ctx.codexModel = arg.slice("--codex-model=".length)
229
+ setModifier(ctx, "--codex-model", arg.slice("--codex-model=".length))
230
+ } else if (arg.startsWith("--claude-effort=")) {
231
+ setModifier(ctx, "--claude-effort", arg.slice("--claude-effort=".length))
232
+ } else if (arg.startsWith("--codex-effort=")) {
233
+ setModifier(ctx, "--codex-effort", arg.slice("--codex-effort=".length))
234
+ } else if (arg.startsWith("--claude-advisor=")) {
235
+ setModifier(ctx, "--claude-advisor", arg.slice("--claude-advisor=".length))
165
236
  } else if (arg.startsWith("--claude-compact-window=")) {
166
237
  const parsed = parseCompactWindow(arg.slice("--claude-compact-window=".length))
167
238
  if (parsed === undefined) {
@@ -184,9 +255,15 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
184
255
  }
185
256
  }
186
257
 
187
- export function validateModelFlags(ctx: Ctx): void {
258
+ function printCatalog(ctx: Ctx, catalog: string): void {
259
+ for (const line of catalog.split("\n")) ctx.services.logger.echo(line)
260
+ }
261
+
262
+ export function validateModifierFlags(ctx: Ctx): void {
188
263
  const { err, warn } = ctx.services.logger
189
- if (ctx.claudeModel !== "") {
264
+ const supplied = (flag: ModifierFlag, value: string): boolean =>
265
+ value !== "" || ctx.modifierFlags?.has(flag) === true
266
+ if (supplied("--claude-model", ctx.claudeModel)) {
190
267
  if (!ctx.syncClaude) {
191
268
  warn("--claude-model ignored: claude target not selected")
192
269
  ctx.claudeModel = ""
@@ -196,7 +273,27 @@ export function validateModelFlags(ctx: Ctx): void {
196
273
  throw new ExitError(2)
197
274
  }
198
275
  }
199
- if (ctx.codexModel !== "") {
276
+ if (supplied("--claude-effort", ctx.claudeEffort)) {
277
+ if (!ctx.syncClaude) {
278
+ warn("--claude-effort ignored: claude target not selected")
279
+ ctx.claudeEffort = ""
280
+ } else if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
281
+ printCatalog(ctx, effortCatalog("claude"))
282
+ err(`Invalid Claude effort '${ctx.claudeEffort}' — valid: ${effortValueGrammar("claude")}`)
283
+ throw new ExitError(2)
284
+ }
285
+ }
286
+ if (supplied("--claude-advisor", ctx.claudeAdvisor)) {
287
+ if (!ctx.syncClaude) {
288
+ warn("--claude-advisor ignored: claude target not selected")
289
+ ctx.claudeAdvisor = ""
290
+ } else if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
291
+ printCatalog(ctx, advisorCatalog())
292
+ err(`Invalid Claude advisor state '${ctx.claudeAdvisor}' — valid: ${advisorValueGrammar()}`)
293
+ throw new ExitError(2)
294
+ }
295
+ }
296
+ if (supplied("--codex-model", ctx.codexModel)) {
200
297
  if (!ctx.syncCodex) {
201
298
  warn("--codex-model ignored: codex target not selected")
202
299
  ctx.codexModel = ""
@@ -206,4 +303,14 @@ export function validateModelFlags(ctx: Ctx): void {
206
303
  throw new ExitError(2)
207
304
  }
208
305
  }
306
+ if (supplied("--codex-effort", ctx.codexEffort)) {
307
+ if (!ctx.syncCodex) {
308
+ warn("--codex-effort ignored: codex target not selected")
309
+ ctx.codexEffort = ""
310
+ } else if (!isEffortModifierValue("codex", ctx.codexEffort)) {
311
+ printCatalog(ctx, effortCatalog("codex"))
312
+ err(`Invalid Codex effort '${ctx.codexEffort}' — valid: ${effortValueGrammar("codex")}`)
313
+ throw new ExitError(2)
314
+ }
315
+ }
209
316
  }
@@ -1,13 +1,15 @@
1
1
  // Generated by cli/scripts/generate-sot-payload.ts. DO NOT EDIT.
2
- // Edit SoT/ or notification.mp3, then run: bun cli/scripts/generate-sot-payload.ts
2
+ // Edit SoT/, notification.mp3, or package.json, then run: bun cli/scripts/generate-sot-payload.ts
3
+
4
+ export const GENERATED_PACKAGE_VERSION = "0.5.0"
3
5
 
4
6
  export const GENERATED_PAYLOAD_TEXT = {
5
7
  "SoT/.agents/skills.txt": "# Universal AI-agent skills (agentskills.io standard).\n# Bootstrapped to ~/.agents/skills/ by cli/src/engine-native/skillsSync.ts during ./docks-kit sync.\n# One slug per line: <owner>/<repo>. Lines starting with # are comments.\n# Each skill's canonical SKILL.md lands in ~/.agents/skills/<name>/ — Codex\n# reads that path natively; Claude Code gets a ~/.claude/skills/<name>\n# symlink to it. The skills sync names both agents the kit supports\n# (-a claude-code codex) so the CLI keeps the shared canonical copy.\n\n# Browser automation CLI — reaches JS-rendered, auth-walled, login-gated pages\n# (x.com, LinkedIn, Confluence) that built-in WebFetch can't. The skills sync\n# auto-installs the `agent-browser` npm package + downloads Chrome for Testing\n# (~175 MB) on first sync; Linux runs `agent-browser install --with-deps` which\n# may prompt for sudo to install system libs (libnss3, libatk, ...).\nvercel-labs/agent-browser\n",
6
- "SoT/models.json": "{\n \"$comment\": \"Kit-verified model catalog — single source for EngineNative validators, the docks-kit CLI (models/model commands, pickers, bare-flag help), and docs. Entries are research-proofed: update an entry and its tool-level `verified` date when a model ships or retires. The catalog informs, it doesn't imprison: well-formed IDs outside it (claude-* / codex charset) apply with a warning.\",\n \"claude\": {\n \"verified\": \"2026-07-08\",\n \"models\": [\n { \"id\": \"best\", \"kind\": \"alias\", \"note\": \"Fable 5 where the org has access, latest Opus otherwise (Claude Code >=2.1.170)\" },\n { \"id\": \"opus\", \"kind\": \"alias\", \"note\": \"latest Opus (currently Opus 4.8) — the kit SoT default, paired with the fable advisor\" },\n { \"id\": \"fable\", \"kind\": \"alias\", \"note\": \"Fable 5 — needs org access + Claude Code >=2.1.170\" },\n { \"id\": \"sonnet\", \"kind\": \"alias\", \"note\": \"latest Sonnet (currently Sonnet 5)\" },\n { \"id\": \"haiku\", \"kind\": \"alias\", \"note\": \"latest Haiku (currently Haiku 4.5)\" },\n { \"id\": \"default\", \"kind\": \"alias\", \"note\": \"engine pseudo-value: deletes the deployed model key so the account default applies\" },\n { \"id\": \"claude-fable-5\", \"kind\": \"id\", \"note\": \"Fable 5\" },\n { \"id\": \"claude-opus-4-8\", \"kind\": \"id\", \"note\": \"Opus 4.8\" },\n { \"id\": \"claude-sonnet-5\", \"kind\": \"id\", \"note\": \"Sonnet 5\" },\n { \"id\": \"claude-haiku-4-5-20251001\", \"kind\": \"id\", \"note\": \"Haiku 4.5\" }\n ]\n },\n \"codex\": {\n \"verified\": \"2026-07-09\",\n \"models\": [\n { \"id\": \"gpt-5.6-sol\", \"kind\": \"id\", \"note\": \"GPT-5.6 Sol — frontier, recommended default; the kit SoT pin\" },\n { \"id\": \"gpt-5.6-terra\", \"kind\": \"id\", \"note\": \"GPT-5.6 Terra — balanced tier\" },\n { \"id\": \"gpt-5.6-luna\", \"kind\": \"id\", \"note\": \"GPT-5.6 Luna — fast/light tier\" },\n { \"id\": \"gpt-5.5\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5.5-codex\", \"kind\": \"id\", \"note\": \"codex-tuned gpt-5.5\" },\n { \"id\": \"gpt-5.1\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5-codex\", \"kind\": \"id\", \"note\": \"codex-tuned gpt-5\" }\n ]\n }\n}\n",
8
+ "SoT/models.json": "{\n \"$comment\": \"Kit-verified model catalog — single source for EngineNative validators, the docks-kit CLI (models/model commands, pickers, bare-flag help), and docs. Entries are research-proofed: update an entry and its tool-level `verified` date when a model ships or retires. The catalog informs, it doesn't imprison: well-formed IDs outside it (claude-* / codex charset) apply with a warning.\",\n \"claude\": {\n \"verified\": \"2026-07-08\",\n \"models\": [\n { \"id\": \"best\", \"kind\": \"alias\", \"note\": \"Fable 5 where the org has access, latest Opus otherwise (Claude Code >=2.1.170)\" },\n { \"id\": \"opus\", \"kind\": \"alias\", \"note\": \"latest Opus (currently Opus 4.8)\" },\n { \"id\": \"fable\", \"kind\": \"alias\", \"note\": \"Fable 5 — the kit SoT default; needs org access + Claude Code >=2.1.170\" },\n { \"id\": \"sonnet\", \"kind\": \"alias\", \"note\": \"latest Sonnet (currently Sonnet 5)\" },\n { \"id\": \"haiku\", \"kind\": \"alias\", \"note\": \"latest Haiku (currently Haiku 4.5)\" },\n { \"id\": \"default\", \"kind\": \"alias\", \"note\": \"engine pseudo-value: deletes the deployed model key so the account default applies\" },\n { \"id\": \"claude-fable-5\", \"kind\": \"id\", \"note\": \"Fable 5\" },\n { \"id\": \"claude-opus-4-8\", \"kind\": \"id\", \"note\": \"Opus 4.8\" },\n { \"id\": \"claude-sonnet-5\", \"kind\": \"id\", \"note\": \"Sonnet 5\" },\n { \"id\": \"claude-haiku-4-5-20251001\", \"kind\": \"id\", \"note\": \"Haiku 4.5\" }\n ]\n },\n \"codex\": {\n \"verified\": \"2026-07-09\",\n \"models\": [\n { \"id\": \"gpt-5.6-sol\", \"kind\": \"id\", \"note\": \"GPT-5.6 Sol — frontier, recommended default; the kit SoT pin\" },\n { \"id\": \"gpt-5.6-terra\", \"kind\": \"id\", \"note\": \"GPT-5.6 Terra — balanced tier\" },\n { \"id\": \"gpt-5.6-luna\", \"kind\": \"id\", \"note\": \"GPT-5.6 Luna — fast/light tier\" },\n { \"id\": \"gpt-5.5\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5.5-codex\", \"kind\": \"id\", \"note\": \"codex-tuned gpt-5.5\" },\n { \"id\": \"gpt-5.1\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5\", \"kind\": \"id\", \"note\": \"previous generation\" },\n { \"id\": \"gpt-5-codex\", \"kind\": \"id\", \"note\": \"codex-tuned gpt-5\" }\n ]\n }\n}\n",
7
9
  "SoT/toolchain.json": "{\n \"$comment\": \"Kit toolchain manifest — DATA only (versions, floors, policy); check/install logic lives in cli/src/engine-native/toolchain.ts with per-surface sync callbacks in cli/src/engine-native/. kind: check (doctor visibility only) | managed (kit installs/upgrades it) | pin (no binary probe — a version pin for a tool the kit invokes via npx). policy (managed only): track (upgrade toward latest, gated by `verified`) | present (install when missing, never upgrade). `verified` = last kit-tested version — anything above it prompts before install (--yes auto-accepts; non-TTY declines and falls back to the pinned `verified` when pinnable). Supply-chain stance: every kit-driven install is pinned to `verified` or gated by it — never floating @latest (npm-worm/Shai-Hulud surface). Update `verified` after testing a new release.\",\n \"tools\": {\n \"jq\": { \"kind\": \"check\", \"note\": \"optional operator CLI; EngineNative JSON and Claude runtime do not invoke it\" },\n \"curl\": { \"kind\": \"check\", \"note\": \"contextual POSIX installer transport for RTK/Bun bootstrap\" },\n \"git\": { \"kind\": \"check\", \"note\": \"plugin marketplaces (claude/codex clone them) + kit checkout updates\" },\n \"node\": { \"kind\": \"check\", \"note\": \"hosts npm globals (agent-browser, LSP servers)\" },\n \"npm\": { \"kind\": \"check\", \"note\": \"npm-global installer\" },\n \"claude\": { \"kind\": \"check\", \"floor\": \"2.1.170\", \"note\": \"kit floor — `best` alias + Fable 5 need >=2.1.170 (mirrors settings minimumVersion)\" },\n \"codex\": { \"kind\": \"check\", \"note\": \"upstream-owned; standalone installer prints when missing\" },\n \"ffplay\": { \"kind\": \"check\", \"note\": \"Notification hook sound (previously unchecked)\" },\n \"bwrap\": { \"kind\": \"check\", \"os\": \"linux\", \"note\": \"Codex Linux sandbox runtime\" },\n \"intelephense\": { \"kind\": \"check\", \"verified\": \"1.18.5\", \"note\": \"php-lsp server binary; `verified` pins claude::sync_lsp_servers' npm install\" },\n \"typescript-language-server\": { \"kind\": \"check\", \"verified\": \"5.3.0\", \"note\": \"typescript-lsp server binary; `verified` pins claude::sync_lsp_servers' npm install\" },\n \"tsc\": { \"kind\": \"check\", \"verified\": \"6.0.3\", \"note\": \"typescript-lsp dependency (npm package `typescript`); `verified` pins claude::sync_lsp_servers' npm install. Deliberately on the 6.x line: typescript-language-server embeds TypeScript's programmatic API, which TS7 (native) doesn't yet expose — the repo's own devDependency runs TS7 for tsc --noEmit\" },\n \"rtk\": { \"kind\": \"managed\", \"policy\": \"track\", \"floor\": \"0.43.0\", \"verified\": \"0.43.0\", \"pinnable\": true,\n \"note\": \"PreToolUse hook — supply-chain review before unverified upgrades; installer honors RTK_VERSION=vX.Y.Z pin\" },\n \"bun\": { \"kind\": \"managed\", \"policy\": \"present\", \"verified\": \"1.3.14\", \"pinnable\": true,\n \"note\": \"runtime for effect-solutions + the docks-kit CLI; bootstrap installs the verified release (installer takes bun-vX.Y.Z); self-updates via `bun upgrade` when wanted\" },\n \"effect-solutions\": { \"kind\": \"managed\", \"policy\": \"track\", \"verified\": \"0.5.3\", \"pinnable\": true,\n \"note\": \"Effect docs CLI (bun global) — track keeps it self-upgrading, gated by the verified pin\" },\n \"agent-browser\": { \"kind\": \"managed\", \"policy\": \"track\", \"verified\": \"0.31.1\", \"pinnable\": true,\n \"note\": \"browser-automation CLI (npm global), gated by the verified pin; first install also downloads Chrome for Testing\" },\n \"skills-cli\": { \"kind\": \"pin\", \"verified\": \"1.5.15\",\n \"note\": \"the `skills` npm package the kit runs via `npx skills@<verified>` on every agents sync (universal-skill install/remove) — pinned, never @latest\" }\n }\n}\n",
8
- "SoT/.claude/CLAUDE.md": "@RTK.md\n\n## Research Before Implementation\n\nBefore writing or modifying code that uses an API, hook, method, or config surface you have not verified in this session, research current documentation first.\n\n**Research workflow:**\n1. Use `resolve-library-id` → `query-docs` (context7) to fetch up-to-date docs for the specific library/framework\n2. If context7 doesn't cover it, read the official docs with `agent-browser` (it reaches JS-rendered, auth-walled, and login-gated pages); fall back to `WebFetch` for a simple static page\n3. Only then proceed to implementation\n\n**When to research:**\n- Installing or configuring a dependency\n- Using an API, hook, method, or pattern you haven't verified in this session\n- Upgrading or migrating between versions\n- Any task where you'd otherwise rely on training data for syntax/behavior\n\n**Do NOT:**\n- Assume API signatures, method names, or config options from memory\n- Generate framework code without checking current docs first\n- Skip research because the library \"seems familiar\"\n\n<constraint>\nResearch the codebase before editing. Never change code you haven't read.\n</constraint>\n\n## Agentic Harness Heuristics\n\n**1. Persistence.** Keep going until the user's query is completely resolved. Only yield when sure the problem is solved. Before ending a turn, check the last paragraph: if it is a plan, a question you can answer yourself, or a promise of work not done (\"I'll…\"), do that work now.\n\n**2. Default to parallel.** Whenever you have multiple independent operations (reads, greps, web fetches, independent edits), invoke them in a single response with multiple tool-use blocks. Sequential calls only when output of one operation is required as input to the next.\n\n**3. Multi-pass search.** First-pass search often misses — vary the wording (colleague-questions over keywords) before concluding something doesn't exist.\n\n**4. Trace symbols.** Before modifying a symbol, trace it to its definitions and all usages. Don't assume a function's behavior or a type's shape from the call site alone.\n\n**5. Linter-loop 3-strike rule.** Don't loop more than 3 times fixing linter errors on the same file. On the third attempt, stop and ask the user — repeated failure usually means the diagnosis is wrong, not the code.\n\n**6. Read-before-Edit TTL.** If you haven't read a file with the Read tool in the last ~5 messages, re-read it before editing. Cached file content goes stale silently when the user edits between turns.\n\n**7. Big-file rule.** For files >1000 lines, prefer Grep + scoped Read (`offset` + `limit`) over reading the entire file. Whole-file reads bloat context; targeted reads keep the working set small.\n\n**8. Todo hygiene.** Use TaskCreate for items with meaningful outcome (≥5 min, distinct deliverable). Never include operational sub-actions (linting, testing, searching, examining the codebase) as their own todos — those are sub-steps in service of higher-level tasks. Mark complete immediately when done, never in batches.\n\n**9. Literal-instruction rule.** Current frontier models follow instructions literally — they do not silently generalize from intent. Phrase requirements as explicit checklists with success criteria, not narrative.\n\n**10. Context hygiene.** Prefer `/clear` at task boundaries and `/rewind` for wrong-path detours over carrying rot forward (corrections accumulate noise; rewinds preserve the prefix and discard the bad branch). On a continuing task, run `/compact` with steering before context quality degrades. Never stop, summarize, or suggest a new session on account of context limits.\n\n**11. Autonomy calibration.** For minor choices (naming, formatting, default values, which of two equivalent approaches), pick a reasonable option and note it — don't ask. Ask first only for scope changes, destructive actions, or decisions that change the deliverable. When the user is describing a problem or asking a question rather than requesting a change, the deliverable is your assessment — report findings and stop; don't apply fixes until asked. Don't close a finished task with \"Want me to also…?\" — run the obvious verification, then stop cleanly.\n\n**12. Capability triggering.** When the answer depends on current or version-specific information, search or fetch before answering — never answer from memory. When work fans out across independent items (many files to read, many tests to run, many candidates to check), delegate to parallel subagents; never spawn one for work you can complete directly. For verification, prefer a fresh-context subagent over self-critique. On tasks longer than a few turns, keep a running notes file and re-read it before each phase.\n\n<constraint>\nTreat the 12 heuristics above as protocol, not preference. If a turn ends without honoring an applicable one (e.g., lint-loop guard not respected, edit without re-read), self-correct in the next turn before continuing.\n</constraint>\n\n## Project Skills\n\nProjects may have a `.claude/skills/` directory with Tool Wrapper skills managed by `/docs`. Claude Code auto-discovers these at session start — only descriptions are loaded, full content loads on demand via the Skill tool.\n\nSkills follow the [agentskills.io](https://agentskills.io) open standard:\n- **SKILL.md**: frontmatter (`name`, `description`, `user-invocable: false`, `metadata`) + body (≤500 lines)\n- **references/**: on-demand detail files (30-150 lines each), loaded when the skill instructs Claude to read them\n- **Discovery**: Claude Code scans `.claude/skills/*/SKILL.md` at session start, loads only `name` + `description` (~100 tokens per skill)\n- **Triggering**: Claude semantically matches descriptions against user tasks, invokes via `Skill` tool — no `@import` or pointer tables needed\n- **CSO (Claude Search Optimization)**: descriptions MUST start with \"Use when...\" and describe trigger conditions, not capabilities\n- **Third-party / vendored skills**: add an `upstream:` frontmatter block (`source`, `license`, `vendored_at: \"YYYY-MM-DD\"`) when vendoring a skill from an external repo. The block marks the skill as vendored so kit-specific checks (CSO start-prefix, `user-invocable`, `metadata.updated`) are relaxed and the skill's body is preserved verbatim from upstream. Universal structural checks (fenced frontmatter, name matches directory, description length, 500-line body cap) still apply.\n\n<constraint>\nAfter any code change affecting documented patterns, update the relevant skill in `.claude/skills/` and its `metadata.updated` frontmatter field. When introducing something new, create a skill or add a `references/` file to an existing skill.\n</constraint>\n\n## Project Agents\n\nProjects and the global kit may have a `.claude/agents/` directory containing subagent definitions. Each agent file declares its `model` (`sonnet`/`opus`/`haiku`/`inherit`/full model ID), `tools`, and system prompt. Claude Code auto-discovers them at session start and delegates when a `subagent_type` matches or when a slash command explicitly invokes them.\n\nAgent files follow this structure:\n- **Frontmatter**: `name` (kebab-case, matches filename), `description` (CSO — starts \"Use when…\" with a \"Not for…\" exclusion clause), `tools`, `model`\n- **Body** (≤500 lines): `<constraint>` blocks for non-negotiable rules, `## Workflow` with context-acknowledgment as step 1, `## Output Format`, `## Anti-Hallucination Checks`, `## Success Criteria`\n- **Model-selection resolution** (per Claude Code docs): `CLAUDE_CODE_SUBAGENT_MODEL` env var → per-invocation `model` param → agent frontmatter `model:` → parent conversation. The env var is NOT set in this kit, so per-agent frontmatter controls selection.\n\n<constraint>\nWhen adding a new agent: use kebab-case name matching filename, CSO-compliant description (starts \"Use when…\", contains a \"Not\" exclusion clause), explicit `model` and `tools`.\n</constraint>\n\n## Picking the right models for workflows and subagents\n\nRankings, higher = better. Intelligence is how hard a problem the model can be handed unsupervised. Taste covers UI/UX, code quality, API design, and copy. Cost is relative spend — a tie-breaker only.\n\n| model | cost | intelligence | taste |\n|----------|------|--------------|-------|\n| gpt-5.5 | 9 | 8 | 5 |\n| opus-4.8 | 4 | 7 | 8 |\n| sonnet-5 | 5 | 5 | 7 |\n\nFable 5 outranks all three on intelligence and taste, but its limits are spent for now — treat opus-4.8 as the ceiling until Fable access returns, then Fable takes the top intelligence+taste slot.\n\nHow to apply:\n- These are defaults, not limits. Standing permission to override: if a cheaper model's output misses the bar, rerun with a smarter one without asking. Judge the output, not the price tag — escalating costs less than shipping mediocre work.\n- Cost is a tie-breaker only; when axes conflict for anything that ships, intelligence > taste > cost.\n- Bulk/mechanical work (clear-spec implementation, data analysis, migrations): gpt-5.5.\n- Anything user-facing (UI, copy, API design) needs taste ≥ 7 → opus-4.8 (sonnet-5 when Opus is saturated).\n- Reviews of plans/implementations: opus-4.8, optionally gpt-5.5 as a second independent perspective.\n- Never use Haiku.\n- gpt-5.5 is reachable only through the Codex CLI (`~/.codex/config.toml` defaults to gpt-5.5): `codex exec` for headless implementation/analysis, `codex review` for diff review, `codex exec -s read-only` with a self-contained prompt for ad-hoc investigation, UI verification, or data analysis. Codex is more efficient than Claude on well-specced execution and stronger at computer-use and UI/UX verification — offload those and report results back.\n- Claude models run via the Agent/Workflow `model` parameter (`opus`, `sonnet`).\n\nUsing gpt-5.5 inside workflows and subagents (the `model` parameter takes only Claude models, so wrap it):\n- Spawn a thin Claude wrapper agent with `model: 'sonnet', effort: 'low'` whose prompt tells it to write a self-contained Codex prompt, run `codex exec` via Bash, and return Codex's output verbatim. The wrapper only shuttles the prompt and result — gpt-5.5 does the work.\n\nReaching gpt-5.5 (or a full-context worker in another project) as a persistent session — the `session-relay` skill (shared bus + `relay` CLI, Claude ⇄ Codex):\n- `codex exec` is one-shot and stateless. When the offload must be resumable, span several turns, or run in another project with that project's own config, spawn a real session instead: `relay spawn <dir> --tool codex --model gpt-5.5 --effort xhigh` (or `--tool claude --model opus` for a Claude worker), then continue it with `send` / `wake`.\n- Two independent perspectives on a plan = the red-team pair spawn: a gpt-5.5 worker and an opus worker debate over the bus, orchestrator writes the verdict — the concrete form of the \"second independent perspective\" review above.\n- Pin `--model`/`--effort` on every spawn/wake; never leave an unattended relay child on a top interactive default (e.g. Fable). Each spawn/wake bills the target's subscription — spawn deliberately, never in loops.\n\n## Agentic Engineering Discipline\n\n1. **State assumptions; push back when warranted.** If a requirement is ambiguous in a way that changes the deliverable, surface the ambiguity and propose 1–2 concrete interpretations in your first message — do not silently pick one and run with it. Surface inconsistencies and confusion instead of guessing past them; present tradeoffs when approaches genuinely differ; push back when the request looks wrong. Agreeable-but-wrong is the failure mode, not disagreement.\n2. **Minimum code that solves the stated problem.** Each named pattern below is a defect — catch it during generation, not after:\n\n **Code slop**\n - **Defensive guards** around internally-trusted calls (`try`, `if x != null`). Validate at system boundaries only.\n - **Speculative abstraction.** No helper for one caller; no interface for one implementer. Three similar lines beats premature DRY.\n - **Backwards-compat shims** without a caller — re-exports, deprecation aliases, untoggled feature flags. Just change the code.\n - **Half-finished stubs.** `TODO handle later`, `throw new Error('not implemented')`. Implement or remove the path.\n - **Underscore-rename of unused vars.** Delete the var.\n - **Dead code left behind.** After a refactor, delete the paths, helpers, and imports the change made unreachable.\n\n **Comment slop**\n - **Restate-the-code.** `// increment i`. The identifier already says it.\n - **Provenance.** `// added for ticket X`, `// used by Y`. Belongs in the PR description; rots in code.\n - **Tombstones.** `// removed Z`, `// previously did W`. Git remembers.\n - **Docstring bloat** on self-evident functions. One line, only when the WHY isn't obvious from the name.\n\n **Output slop**\n - **End-of-turn diff-restatement.** One or two sentences: what changed, what's next. Don't recap what's in the diff.\n - **Narration tics.** \"Now I'll…\", \"Let me check…\", play-by-play between tool calls. Terse working shorthand between tool calls is fine; play-by-play is not — write a sentence when something load-bearing happens (a finding, a direction change, a blocker).\n - **Compressed final summaries.** The final message is for a reader who didn't watch the work: outcome first, complete sentences. Shorten by dropping detail, never by compressing into fragments or arrow chains.\n3. **Surgical changes only.** Do not modify code, comments, or formatting outside the explicit scope of the request. Surface unrelated issues as follow-ups — do not fix inline.\n4. **State how success will be verified before implementing.** Name the test, build, smoke check, or diff inspection that will prove the change works. Prefer executable criteria — a test that fails before and passes after, a command with expected output — over judgment calls, and keep each change small enough that its diff is reviewable in one sitting.\n5. **Review scope follows the pipeline.** In pipeline reviews with a downstream filter (multi-agent scans, verification phases), report every issue found with confidence and severity — filtering happens downstream. In ad-hoc reviews, flag only gaps that affect correctness or the stated requirements; treat the rest as optional.\n6. **Ground every progress claim in evidence.** Before reporting progress or completion, audit each claim against a tool result from this session — show the test output, the command and what it returned. If something is unverified, say so explicitly; if tests fail, say so with the output.\n\n<constraint>\nTreat the six rules above as preventive (during generation), not remedial (after the fact). Self-correct if a turn drifts.\n</constraint>\n",
10
+ "SoT/.claude/CLAUDE.md": "@RTK.md\n\n## Research Before Implementation\n\nBefore writing or modifying code that uses an API, hook, method, or config surface you have not verified in this session, research current documentation first.\n\n**Research workflow:**\n1. Use `resolve-library-id` → `query-docs` (context7) to fetch up-to-date docs for the specific library/framework\n2. If context7 doesn't cover it, read the official docs with `agent-browser` (it reaches JS-rendered, auth-walled, and login-gated pages); fall back to `WebFetch` for a simple static page\n3. Only then proceed to implementation\n\n**When to research:**\n- Installing or configuring a dependency\n- Using an API, hook, method, or pattern you haven't verified in this session\n- Upgrading or migrating between versions\n- Any task where you'd otherwise rely on training data for syntax/behavior\n\n**Do NOT:**\n- Assume API signatures, method names, or config options from memory\n- Generate framework code without checking current docs first\n- Skip research because the library \"seems familiar\"\n\n<constraint>\nResearch the codebase before editing. Never change code you haven't read.\n</constraint>\n\n## Agentic Harness Heuristics\n\n**1. Persistence.** Keep going until the user's query is completely resolved. Only yield when sure the problem is solved. Before ending a turn, check the last paragraph: if it is a plan, a question you can answer yourself, or a promise of work not done (\"I'll…\"), do that work now.\n\n**2. Default to parallel.** Whenever you have multiple independent operations (reads, greps, web fetches, independent edits), invoke them in a single response with multiple tool-use blocks. Sequential calls only when output of one operation is required as input to the next.\n\n**3. Multi-pass search.** First-pass search often misses — vary the wording (colleague-questions over keywords) before concluding something doesn't exist.\n\n**4. Trace symbols.** Before modifying a symbol, trace it to its definitions and all usages. Don't assume a function's behavior or a type's shape from the call site alone.\n\n**5. Linter-loop 3-strike rule.** Don't loop more than 3 times fixing linter errors on the same file. On the third attempt, stop and ask the user — repeated failure usually means the diagnosis is wrong, not the code.\n\n**6. Read-before-Edit TTL.** If you haven't read a file with the Read tool in the last ~5 messages, re-read it before editing. Cached file content goes stale silently when the user edits between turns.\n\n**7. Big-file rule.** For files >1000 lines, prefer Grep + scoped Read (`offset` + `limit`) over reading the entire file. Whole-file reads bloat context; targeted reads keep the working set small.\n\n**8. Todo hygiene.** Use TaskCreate for items with meaningful outcome (≥5 min, distinct deliverable). Never include operational sub-actions (linting, testing, searching, examining the codebase) as their own todos — those are sub-steps in service of higher-level tasks. Mark complete immediately when done, never in batches.\n\n**9. Literal-instruction rule.** Current frontier models follow instructions literally — they do not silently generalize from intent. Phrase requirements as explicit checklists with success criteria, not narrative.\n\n**10. Context hygiene.** Prefer `/clear` at task boundaries and `/rewind` for wrong-path detours over carrying rot forward (corrections accumulate noise; rewinds preserve the prefix and discard the bad branch). On a continuing task, run `/compact` with steering before context quality degrades. Never stop, summarize, or suggest a new session on account of context limits.\n\n**11. Autonomy calibration.** For minor choices (naming, formatting, default values, which of two equivalent approaches), pick a reasonable option and note it — don't ask. Ask first only for scope changes, destructive actions, or decisions that change the deliverable. When the user is describing a problem or asking a question rather than requesting a change, the deliverable is your assessment — report findings and stop; don't apply fixes until asked. Don't close a finished task with \"Want me to also…?\" — run the obvious verification, then stop cleanly.\n\n**12. Capability triggering.** When the answer depends on current or version-specific information, search or fetch before answering — never answer from memory. When work fans out across independent items (many files to read, many tests to run, many candidates to check), delegate to parallel subagents; never spawn one for work you can complete directly. For verification, prefer a fresh-context subagent over self-critique. On tasks longer than a few turns, keep a running notes file and re-read it before each phase.\n\n<constraint>\nTreat the 12 heuristics above as protocol, not preference. If a turn ends without honoring an applicable one (e.g., lint-loop guard not respected, edit without re-read), self-correct in the next turn before continuing.\n</constraint>\n\n## Project Skills\n\nProjects may have a `.claude/skills/` directory with Tool Wrapper skills managed by `/docs`. Claude Code auto-discovers these at session start — only descriptions are loaded, full content loads on demand via the Skill tool.\n\nSkills follow the [agentskills.io](https://agentskills.io) open standard:\n- **SKILL.md**: frontmatter (`name`, `description`, `user-invocable: false`, `metadata`) + body (≤500 lines)\n- **references/**: on-demand detail files (30-150 lines each), loaded when the skill instructs Claude to read them\n- **Discovery**: Claude Code scans `.claude/skills/*/SKILL.md` at session start, loads only `name` + `description` (~100 tokens per skill)\n- **Triggering**: Claude semantically matches descriptions against user tasks, invokes via `Skill` tool — no `@import` or pointer tables needed\n- **CSO (Claude Search Optimization)**: descriptions MUST start with \"Use when...\" and describe trigger conditions, not capabilities\n- **Third-party / vendored skills**: add an `upstream:` frontmatter block (`source`, `license`, `vendored_at: \"YYYY-MM-DD\"`) when vendoring a skill from an external repo. The block marks the skill as vendored so kit-specific checks (CSO start-prefix, `user-invocable`, `metadata.updated`) are relaxed and the skill's body is preserved verbatim from upstream. Universal structural checks (fenced frontmatter, name matches directory, description length, 500-line body cap) still apply.\n\n<constraint>\nAfter any code change affecting documented patterns, update the relevant skill in `.claude/skills/` and its `metadata.updated` frontmatter field. When introducing something new, create a skill or add a `references/` file to an existing skill.\n</constraint>\n\n## Project Agents\n\nProjects and the global kit may have a `.claude/agents/` directory containing subagent definitions. Each agent file declares its `model` (`sonnet`/`opus`/`haiku`/`inherit`/full model ID), `tools`, and system prompt. Claude Code auto-discovers them at session start and delegates when a `subagent_type` matches or when a slash command explicitly invokes them.\n\nAgent files follow this structure:\n- **Frontmatter**: `name` (kebab-case, matches filename), `description` (CSO — starts \"Use when…\" with a \"Not for…\" exclusion clause), `tools`, `model`\n- **Body** (≤500 lines): `<constraint>` blocks for non-negotiable rules, `## Workflow` with context-acknowledgment as step 1, `## Output Format`, `## Anti-Hallucination Checks`, `## Success Criteria`\n- **Model-selection resolution** (per Claude Code docs): `CLAUDE_CODE_SUBAGENT_MODEL` env var → per-invocation `model` param → agent frontmatter `model:` → parent conversation. The env var is NOT set in this kit, so per-agent frontmatter controls selection.\n\n<constraint>\nWhen adding a new agent: use kebab-case name matching filename, CSO-compliant description (starts \"Use when…\", contains a \"Not\" exclusion clause), explicit `model` and `tools`.\n</constraint>\n\n## Picking the right models for workflows and subagents\n\nRankings, all scores 10 = best. Intelligence is how hard a problem the model can be handed unsupervised. Taste covers UI/UX, code quality, API design, and copy. Budget fit is marginal spend plus quota headroom — a tie-breaker only.\n\n| model | budget fit | intelligence | taste |\n|-------------|------------|--------------|-------|\n| gpt-5.6-sol | 9 | 10 | 6 |\n| fable-5 | 3 | 9 | 9 |\n| opus-4.8 | 5 | 7 | 8 |\n| sonnet-5 | 7 | 5 | 7 |\n\nCapacity: Sol has abundant subscription headroom; Fable is scarce; Opus is moderate; Sonnet is economical. gpt-5.6-sol holds the top intelligence slot — it is the default implementer for plan-sized work. Fable 5 (available again; the kit's Claude default) holds the top taste slot and is the orchestrator/interactive tier, not a bulk executor.\n\nHow to apply:\n- These are defaults, not limits. Standing permission to override: if a cheaper model's output misses the bar, rerun with a smarter one without asking. Judge the output, not the price tag — escalating costs less than shipping mediocre work.\n- Budget fit is a tie-breaker only; when axes conflict for anything that ships, intelligence > taste > budget fit.\n- Implementation of plans, and bulk/mechanical work (clear-spec implementation, data analysis, migrations): gpt-5.6-sol.\n- Anything user-facing (UI, copy, API design) needs taste ≥ 7 → fable-5 or opus-4.8 (sonnet-5 when both are saturated).\n- Reviews of plans/implementations: gpt-5.6-sol, optionally the best available Claude — fable-5 when access allows, opus-4.8 otherwise — as a second independent perspective.\n- Never use Haiku.\n- Claude models run via the Agent/Workflow `model` parameter (`opus`, `sonnet`, `fable` where org access allows).\n\nReaching gpt-5.6-sol — always through the `session-relay` skill (shared bus + `relay` CLI, Claude ⇄ Codex), even for one-shots, so every exchange stays resumable:\n- `relay spawn <dir> --tool codex --model gpt-5.6-sol --effort xhigh` (or `--tool claude --model opus` for a Claude worker in another project), then continue it with `send` / `wake`. Codex runs on its own CLI (`~/.codex/config.toml` defaults to gpt-5.6-sol) and is more efficient than Claude on well-specced execution and stronger at computer-use and UI/UX verification — offload those and report results back.\n- An independent fresh-context review = a NEW spawn (fresh spawn is fresh context). Two independent perspectives on a plan = the red-team pair spawn: a gpt-5.6-sol worker and an opus worker debate over the bus, orchestrator writes the verdict — the concrete form of the \"second independent perspective\" review above.\n- Pin `--model`/`--effort` on every spawn/wake; never leave an unattended relay child on a top interactive default (e.g. Fable). Each spawn/wake bills the target's subscription — spawn deliberately, never in loops.\n\n## Agentic Engineering Discipline\n\n1. **State assumptions; push back when warranted.** If a requirement is ambiguous in a way that changes the deliverable, surface the ambiguity and propose 1–2 concrete interpretations in your first message — do not silently pick one and run with it. Surface inconsistencies and confusion instead of guessing past them; present tradeoffs when approaches genuinely differ; push back when the request looks wrong. Agreeable-but-wrong is the failure mode, not disagreement.\n2. **Minimum code that solves the stated problem.** Each named pattern below is a defect — catch it during generation, not after:\n\n **Code slop**\n - **Defensive guards** around internally-trusted calls (`try`, `if x != null`). Validate at system boundaries only.\n - **Speculative abstraction.** No helper for one caller; no interface for one implementer. Three similar lines beats premature DRY.\n - **Backwards-compat shims** without a caller — re-exports, deprecation aliases, untoggled feature flags. Just change the code.\n - **Half-finished stubs.** `TODO handle later`, `throw new Error('not implemented')`. Implement or remove the path.\n - **Underscore-rename of unused vars.** Delete the var.\n - **Dead code left behind.** After a refactor, delete the paths, helpers, and imports the change made unreachable.\n\n **Comment slop**\n - **Restate-the-code.** `// increment i`. The identifier already says it.\n - **Provenance.** `// added for ticket X`, `// used by Y`. Belongs in the PR description; rots in code.\n - **Tombstones.** `// removed Z`, `// previously did W`. Git remembers.\n - **Docstring bloat** on self-evident functions. One line, only when the WHY isn't obvious from the name.\n\n **Output slop**\n - **End-of-turn diff-restatement.** One or two sentences: what changed, what's next. Don't recap what's in the diff.\n - **Narration tics.** \"Now I'll…\", \"Let me check…\", play-by-play between tool calls. Terse working shorthand between tool calls is fine; play-by-play is not — write a sentence when something load-bearing happens (a finding, a direction change, a blocker).\n - **Compressed final summaries.** The final message is for a reader who didn't watch the work: outcome first, complete sentences. Shorten by dropping detail, never by compressing into fragments or arrow chains.\n3. **Surgical changes only.** Do not modify code, comments, or formatting outside the explicit scope of the request. Surface unrelated issues as follow-ups — do not fix inline.\n4. **State how success will be verified before implementing.** Name the test, build, smoke check, or diff inspection that will prove the change works. Prefer executable criteria — a test that fails before and passes after, a command with expected output — over judgment calls, and keep each change small enough that its diff is reviewable in one sitting.\n5. **Review scope follows the pipeline.** In pipeline reviews with a downstream filter (multi-agent scans, verification phases), report every issue found with confidence and severity — filtering happens downstream. In ad-hoc reviews, flag only gaps that affect correctness or the stated requirements; treat the rest as optional.\n6. **Ground every progress claim in evidence.** Before reporting progress or completion, audit each claim against a tool result from this session — show the test output, the command and what it returned. If something is unverified, say so explicitly; if tests fail, say so with the output.\n\n<constraint>\nTreat the six rules above as preventive (during generation), not remedial (after the fact). Self-correct if a turn drifts.\n</constraint>\n",
9
11
  "SoT/.claude/mcp-servers.json": "{\n \"mcpServers\": {\n \"chrome-devtools\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"-y\", \"chrome-devtools-mcp@1.5.0\"],\n \"env\": {}\n }\n }\n}\n",
10
- "SoT/.claude/settings.json": "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"minimumVersion\": \"2.1.170\",\n \"model\": \"opus\",\n \"advisorModel\": \"fable\",\n \"effortLevel\": \"xhigh\",\n \"autoMemoryEnabled\": true,\n \"skillListingMaxDescChars\": 2048,\n \"respectGitignore\": true,\n \"cleanupPeriodDays\": 14,\n \"skillListingBudgetFraction\": 0.05,\n \"env\": {\n \"CLAUDE_CODE_MAX_OUTPUT_TOKENS\": \"64000\",\n \"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR\": \"1\",\n \"CLAUDE_CODE_AUTO_COMPACT_WINDOW\": \"468000\",\n \"CLAUDE_CODE_NO_FLICKER\": \"1\"\n },\n \"permissions\": {\n \"defaultMode\": \"auto\",\n \"allow\": [\n \"Read\",\n \"Glob\",\n \"Grep\",\n \"WebFetch\",\n \"WebSearch\",\n \"Edit(./)\",\n \"Write(./)\",\n \"Bash(git *)\",\n \"Bash(git add *)\",\n \"Bash(git commit *)\",\n \"Bash(git status *)\",\n \"Bash(git diff *)\",\n \"Bash(git log *)\",\n \"Bash(git branch *)\",\n \"Bash(git checkout *)\",\n \"Bash(git switch *)\",\n \"Bash(git stash *)\",\n \"Bash(git fetch *)\",\n \"Bash(git pull *)\",\n \"Bash(git tag *)\",\n \"Bash(git show *)\",\n \"Bash(git blame *)\",\n \"Bash(git worktree *)\",\n \"Bash(gh *)\",\n \"Bash(pnpm *)\",\n \"Bash(npm *)\",\n \"Bash(npx *)\",\n \"Bash(node *)\",\n \"Bash(docker *)\",\n \"Bash(docker-compose *)\",\n \"Bash(rtk *)\",\n \"Bash(ls *)\",\n \"Bash(cat *)\",\n \"Bash(find *)\",\n \"Bash(grep *)\",\n \"Bash(head *)\",\n \"Bash(tail *)\",\n \"Bash(wc *)\",\n \"Bash(sort *)\",\n \"Bash(uniq *)\",\n \"Bash(diff *)\",\n \"Bash(which *)\",\n \"Bash(pwd *)\",\n \"Bash(date *)\",\n \"Bash(mkdir *)\",\n \"Bash(basename *)\",\n \"Bash(dirname *)\",\n \"Bash(realpath *)\",\n \"Bash(jq *)\",\n \"Bash(curl *)\",\n \"Bash(tree *)\",\n \"Bash(sed *)\",\n \"Bash(awk *)\",\n \"Bash(cut *)\",\n \"Bash(tr *)\",\n \"Bash(tee *)\",\n \"Bash(echo *)\",\n \"Bash(printf *)\",\n \"Bash(env *)\",\n \"Bash(printenv *)\",\n \"Bash(uname *)\",\n \"Bash(file *)\",\n \"Bash(stat *)\",\n \"Bash(du *)\",\n \"Bash(id *)\",\n \"Bash(whoami *)\",\n \"Bash(php *)\",\n \"Bash(composer *)\",\n \"Bash(python3 *)\",\n \"Bash(python *)\",\n \"Bash(pip *)\",\n \"Bash(pip3 *)\"\n ],\n \"deny\": [\n \"Read(**/.env)\",\n \"Read(**/.env.local)\",\n \"Read(**/secrets/**)\",\n \"Read(**/*.key)\",\n \"Read(**/*.pem)\",\n \"Read(**/*.p12)\",\n \"Read(**/.credentials*)\",\n \"Edit(**/.env)\",\n \"Edit(**/.env.local)\",\n \"Edit(**/secrets/**)\",\n \"Write(**/.env)\",\n \"Write(**/.env.local)\",\n \"Write(**/secrets/**)\",\n \"Bash(sudo *)\",\n \"Bash(rm -rf /)\",\n \"Bash(rm -rf / *)\",\n \"Bash(rm -rf ~)\",\n \"Bash(rm -rf ~ *)\",\n \"Bash(rm -rf $HOME)\",\n \"Bash(rm -rf $HOME *)\",\n \"Bash(> /dev *)\",\n \"Bash(dd if= *)\",\n \"Bash(mkfs *)\",\n \"Bash(eval *)\",\n \"Bash(chmod 777 *)\",\n \"Bash(chmod -R 777 *)\",\n \"Bash(git push --force origin main *)\",\n \"Bash(git push --force origin master *)\",\n \"Bash(git push -f origin main *)\",\n \"Bash(git push -f origin master *)\"\n ],\n \"ask\": [\n \"Bash(git clean *)\",\n \"Bash(docker volume rm *)\",\n \"Bash(docker system prune *)\"\n ]\n },\n \"hooks\": {\n \"SessionStart\": [\n {\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_BUN__\",\n \"args\": [\"__DOCKS_KIT_SESSION_START__\"],\n \"timeout\": 5\n }\n ]\n }\n ],\n \"Notification\": [\n {\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_BUN__\",\n \"args\": [\"__DOCKS_KIT_NOTIFY__\"],\n \"timeout\": 10,\n \"async\": true\n }\n ]\n }\n ],\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"rtk hook claude\"\n }\n ]\n }\n ],\n \"PostToolUseFailure\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"echo '{\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PostToolUseFailure\\\",\\\"additionalContext\\\":\\\"Last bash command failed. Repository / file state may have shifted \\u2014 re-read affected files before retrying. If the failure is a missing dependency or env mismatch, surface it to the user rather than retrying blindly.\\\"}}'\",\n \"timeout\": 5\n }\n ]\n }\n ],\n \"SubagentStop\": [\n {\n \"hooks\": [\n {\n \"type\": \"prompt\",\n \"prompt\": \"You are a quality gate for subagent outputs in a multi-agent code-analysis pipeline.\\n\\nEvaluate the subagent's `last_assistant_message` field (in the JSON below) against these requirements:\\n\\n1. ALLOW (return `{}`): Mode-selection or no-issues responses. Examples: \\\"Which mode do you prefer\\\", \\\"select an option\\\", \\\"no issues / problems / violations / blockers found\\\".\\n\\n2. ALLOW (return `{}`): Output contains at least one concrete file:line citation \\u2014 e.g. `src/auth.ts:42`, `lib/db.ts:100-115`, or path references that include line numbers.\\n\\n3. BLOCK (return `{\\\"decision\\\":\\\"block\\\",\\\"reason\\\":\\\"<one-line explanation>\\\"}`): Output claims about code or findings WITHOUT concrete file:line citations. Vague references like \\\"the auth handler\\\" or \\\"near the database code\\\" are not acceptable as the only evidence.\\n\\nSubagent invocation JSON:\\n$ARGUMENTS\\n\\nReturn ONLY the JSON decision (no commentary, no markdown fences).\",\n \"timeout\": 30\n }\n ]\n }\n ]\n },\n \"statusLine\": {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_STATUSLINE__\",\n \"refreshInterval\": 5\n },\n \"enabledPlugins\": {\n \"context7@claude-plugins-official\": true,\n \"frontend-design@claude-plugins-official\": true,\n \"php-lsp@claude-plugins-official\": true,\n \"typescript-lsp@claude-plugins-official\": true,\n \"docks@docks\": true,\n \"session-relay@docks\": true,\n \"effect-kit@docks\": true\n },\n \"extraKnownMarketplaces\": {\n \"docks\": {\n \"source\": {\n \"source\": \"github\",\n \"repo\": \"DocksDocks/docks\"\n }\n }\n },\n \"alwaysThinkingEnabled\": true,\n \"showThinkingSummaries\": true,\n \"viewMode\": \"default\",\n \"theme\": \"dark-daltonized\",\n \"skipDangerousModePermissionPrompt\": true\n}\n",
12
+ "SoT/.claude/settings.json": "{\n \"$schema\": \"https://json.schemastore.org/claude-code-settings.json\",\n \"minimumVersion\": \"2.1.170\",\n \"model\": \"fable\",\n \"effortLevel\": \"high\",\n \"autoMemoryEnabled\": true,\n \"skillListingMaxDescChars\": 2048,\n \"respectGitignore\": true,\n \"cleanupPeriodDays\": 14,\n \"skillListingBudgetFraction\": 0.05,\n \"env\": {\n \"CLAUDE_CODE_MAX_OUTPUT_TOKENS\": \"64000\",\n \"CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR\": \"1\",\n \"CLAUDE_CODE_AUTO_COMPACT_WINDOW\": \"468000\",\n \"CLAUDE_CODE_NO_FLICKER\": \"1\"\n },\n \"permissions\": {\n \"defaultMode\": \"auto\",\n \"allow\": [\n \"Read\",\n \"Glob\",\n \"Grep\",\n \"WebFetch\",\n \"WebSearch\",\n \"Edit(./)\",\n \"Write(./)\",\n \"Bash(git *)\",\n \"Bash(git add *)\",\n \"Bash(git commit *)\",\n \"Bash(git status *)\",\n \"Bash(git diff *)\",\n \"Bash(git log *)\",\n \"Bash(git branch *)\",\n \"Bash(git checkout *)\",\n \"Bash(git switch *)\",\n \"Bash(git stash *)\",\n \"Bash(git fetch *)\",\n \"Bash(git pull *)\",\n \"Bash(git tag *)\",\n \"Bash(git show *)\",\n \"Bash(git blame *)\",\n \"Bash(git worktree *)\",\n \"Bash(gh *)\",\n \"Bash(pnpm *)\",\n \"Bash(npm *)\",\n \"Bash(npx *)\",\n \"Bash(node *)\",\n \"Bash(docker *)\",\n \"Bash(docker-compose *)\",\n \"Bash(rtk *)\",\n \"Bash(ls *)\",\n \"Bash(cat *)\",\n \"Bash(find *)\",\n \"Bash(grep *)\",\n \"Bash(head *)\",\n \"Bash(tail *)\",\n \"Bash(wc *)\",\n \"Bash(sort *)\",\n \"Bash(uniq *)\",\n \"Bash(diff *)\",\n \"Bash(which *)\",\n \"Bash(pwd *)\",\n \"Bash(date *)\",\n \"Bash(mkdir *)\",\n \"Bash(basename *)\",\n \"Bash(dirname *)\",\n \"Bash(realpath *)\",\n \"Bash(jq *)\",\n \"Bash(curl *)\",\n \"Bash(tree *)\",\n \"Bash(sed *)\",\n \"Bash(awk *)\",\n \"Bash(cut *)\",\n \"Bash(tr *)\",\n \"Bash(tee *)\",\n \"Bash(echo *)\",\n \"Bash(printf *)\",\n \"Bash(env *)\",\n \"Bash(printenv *)\",\n \"Bash(uname *)\",\n \"Bash(file *)\",\n \"Bash(stat *)\",\n \"Bash(du *)\",\n \"Bash(id *)\",\n \"Bash(whoami *)\",\n \"Bash(php *)\",\n \"Bash(composer *)\",\n \"Bash(python3 *)\",\n \"Bash(python *)\",\n \"Bash(pip *)\",\n \"Bash(pip3 *)\"\n ],\n \"deny\": [\n \"Read(**/.env)\",\n \"Read(**/.env.local)\",\n \"Read(**/secrets/**)\",\n \"Read(**/*.key)\",\n \"Read(**/*.pem)\",\n \"Read(**/*.p12)\",\n \"Read(**/.credentials*)\",\n \"Edit(**/.env)\",\n \"Edit(**/.env.local)\",\n \"Edit(**/secrets/**)\",\n \"Write(**/.env)\",\n \"Write(**/.env.local)\",\n \"Write(**/secrets/**)\",\n \"Bash(sudo *)\",\n \"Bash(rm -rf /)\",\n \"Bash(rm -rf / *)\",\n \"Bash(rm -rf ~)\",\n \"Bash(rm -rf ~ *)\",\n \"Bash(rm -rf $HOME)\",\n \"Bash(rm -rf $HOME *)\",\n \"Bash(> /dev *)\",\n \"Bash(dd if= *)\",\n \"Bash(mkfs *)\",\n \"Bash(eval *)\",\n \"Bash(chmod 777 *)\",\n \"Bash(chmod -R 777 *)\",\n \"Bash(git push --force origin main *)\",\n \"Bash(git push --force origin master *)\",\n \"Bash(git push -f origin main *)\",\n \"Bash(git push -f origin master *)\"\n ],\n \"ask\": [\n \"Bash(git clean *)\",\n \"Bash(docker volume rm *)\",\n \"Bash(docker system prune *)\"\n ]\n },\n \"hooks\": {\n \"SessionStart\": [\n {\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_BUN__\",\n \"args\": [\"__DOCKS_KIT_SESSION_START__\"],\n \"timeout\": 5\n }\n ]\n }\n ],\n \"Notification\": [\n {\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_BUN__\",\n \"args\": [\"__DOCKS_KIT_NOTIFY__\"],\n \"timeout\": 10,\n \"async\": true\n }\n ]\n }\n ],\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"rtk hook claude\"\n }\n ]\n }\n ],\n \"PostToolUseFailure\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [\n {\n \"type\": \"command\",\n \"command\": \"echo '{\\\"hookSpecificOutput\\\":{\\\"hookEventName\\\":\\\"PostToolUseFailure\\\",\\\"additionalContext\\\":\\\"Last bash command failed. Repository / file state may have shifted \\u2014 re-read affected files before retrying. If the failure is a missing dependency or env mismatch, surface it to the user rather than retrying blindly.\\\"}}'\",\n \"timeout\": 5\n }\n ]\n }\n ],\n \"SubagentStop\": [\n {\n \"hooks\": [\n {\n \"type\": \"prompt\",\n \"prompt\": \"You are a quality gate for subagent outputs in a multi-agent code-analysis pipeline.\\n\\nEvaluate the subagent's `last_assistant_message` field (in the JSON below) against these requirements:\\n\\n1. ALLOW (return `{}`): Mode-selection or no-issues responses. Examples: \\\"Which mode do you prefer\\\", \\\"select an option\\\", \\\"no issues / problems / violations / blockers found\\\".\\n\\n2. ALLOW (return `{}`): Output contains at least one concrete file:line citation \\u2014 e.g. `src/auth.ts:42`, `lib/db.ts:100-115`, or path references that include line numbers.\\n\\n3. BLOCK (return `{\\\"decision\\\":\\\"block\\\",\\\"reason\\\":\\\"<one-line explanation>\\\"}`): Output claims about code or findings WITHOUT concrete file:line citations. Vague references like \\\"the auth handler\\\" or \\\"near the database code\\\" are not acceptable as the only evidence.\\n\\nSubagent invocation JSON:\\n$ARGUMENTS\\n\\nReturn ONLY the JSON decision (no commentary, no markdown fences).\",\n \"timeout\": 30\n }\n ]\n }\n ]\n },\n \"statusLine\": {\n \"type\": \"command\",\n \"command\": \"__DOCKS_KIT_STATUSLINE__\",\n \"refreshInterval\": 5\n },\n \"enabledPlugins\": {\n \"context7@claude-plugins-official\": true,\n \"frontend-design@claude-plugins-official\": true,\n \"php-lsp@claude-plugins-official\": true,\n \"typescript-lsp@claude-plugins-official\": true,\n \"docks@docks\": true,\n \"session-relay@docks\": true,\n \"effect-kit@docks\": true\n },\n \"extraKnownMarketplaces\": {\n \"docks\": {\n \"source\": {\n \"source\": \"github\",\n \"repo\": \"DocksDocks/docks\"\n }\n }\n },\n \"alwaysThinkingEnabled\": true,\n \"showThinkingSummaries\": true,\n \"viewMode\": \"default\",\n \"theme\": \"dark-daltonized\",\n \"skipDangerousModePermissionPrompt\": true\n}\n",
11
13
  "SoT/.claude/bin/statusline.mjs": "const ESC = \"\\x1b[\"\nconst PIPE = `${ESC}90m | ${ESC}0m`\nconst DOT = `${ESC}90m • ${ESC}0m`\nconst DIM = `${ESC}2m${ESC}38;2;156;162;175m`\n\nfunction isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction finitePercentage(value) {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0 && value <= 100\n ? value\n : undefined\n}\n\nfunction roundHalfEven(value) {\n const lower = Math.floor(value)\n const fraction = value - lower\n if (fraction < 0.5) return lower\n if (fraction > 0.5) return lower + 1\n return lower % 2 === 0 ? lower : lower + 1\n}\n\nfunction pathBasename(path) {\n const parts = path.split(/[\\\\/]+/).filter((part) => part !== \"\")\n return parts.at(-1) ?? \"\"\n}\n\nfunction modelName(input) {\n const model = isRecord(input.model) && typeof input.model.display_name === \"string\"\n ? input.model.display_name\n : \"\"\n const suffix = model.indexOf(\" (\")\n return suffix === -1 ? model : model.slice(0, suffix)\n}\n\nfunction workingDirectory(input, cwd) {\n if (isRecord(input.workspace) && typeof input.workspace.current_dir === \"string\" && input.workspace.current_dir !== \"\") {\n return input.workspace.current_dir\n }\n if (typeof input.cwd === \"string\" && input.cwd !== \"\") return input.cwd\n return cwd\n}\n\nfunction compactWindow(env, total) {\n const raw = env.CLAUDE_CODE_AUTO_COMPACT_WINDOW\n if (typeof raw !== \"string\" || !/^[0-9]+$/.test(raw)) return total\n const parsed = Number(raw)\n return Number.isSafeInteger(parsed) && parsed >= 1000 && parsed < total ? parsed : total\n}\n\nfunction formatTokensK(value) {\n if (value < 1000) return `${value}k`\n if (value % 1000 === 0) return `${value / 1000}M`\n return `${(roundHalfEven(value / 100) / 10).toFixed(1)}M`\n}\n\nfunction contextSegment(input, env) {\n if (!isRecord(input.context_window)) return \"\"\n const used = finitePercentage(input.context_window.used_percentage)\n if (used === undefined) return \"\"\n\n const total = input.context_window.context_window_size\n if (typeof total !== \"number\" || !Number.isFinite(total) || total <= 0) {\n return `${ESC}38;2;130;160;230mctx ${roundHalfEven(used)}%${ESC}0m`\n }\n\n const usedK = roundHalfEven((used / 100) * (total / 1000))\n const effectiveK = Math.trunc(compactWindow(env, total) / 1000)\n if (effectiveK <= 0) return `${ESC}38;2;130;160;230mctx ${roundHalfEven(used)}%${ESC}0m`\n const effectivePercentage = roundHalfEven((usedK / effectiveK) * 100)\n return `${ESC}38;2;130;160;230mctx ${effectivePercentage}%${ESC}0m ${DIM}(${formatTokensK(usedK)}/${formatTokensK(effectiveK)})${ESC}0m`\n}\n\nfunction resetDelta(value, nowMs) {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) return \"\"\n const seconds = Math.trunc(value) - Math.floor(nowMs / 1000)\n if (seconds <= 0) return \"now\"\n const days = Math.trunc(seconds / 86_400)\n if (days > 0) return `${days}d`\n const hours = Math.trunc((seconds % 86_400) / 3_600)\n if (hours > 0) return `${hours}h`\n return `${Math.trunc((seconds % 3_600) / 60)}m`\n}\n\nfunction quotaWindow(value, label, color, nowMs) {\n if (!isRecord(value)) return \"\"\n const used = finitePercentage(value.used_percentage)\n if (used === undefined) return \"\"\n const delta = resetDelta(value.resets_at, nowMs)\n const reset = delta === \"\" ? \"\" : ` ${DIM}(${delta})${ESC}0m`\n return `${ESC}38;2;${color}m${label} ${roundHalfEven(used)}%${ESC}0m${reset}`\n}\n\nfunction decodeStdout(result) {\n const stdout = result?.stdout\n if (stdout === undefined || stdout === null) return \"\"\n return typeof stdout === \"string\" ? stdout : stdout.toString()\n}\n\nfunction resolveBranch(directory, which, spawnSync) {\n const git = which(\"git\")\n if (typeof git !== \"string\" || git === \"\") return \"\"\n const commands = [\n [git, \"-C\", directory, \"symbolic-ref\", \"--short\", \"HEAD\"],\n [git, \"-C\", directory, \"rev-parse\", \"--short\", \"HEAD\"]\n ]\n for (const command of commands) {\n try {\n const result = spawnSync(command, { stdin: \"ignore\", stdout: \"pipe\", stderr: \"ignore\" })\n if (result?.success === true) return decodeStdout(result).trim()\n } catch {\n return \"\"\n }\n }\n return \"\"\n}\n\nexport function formatStatusline(input, options = {}) {\n if (!isRecord(input)) return \"\"\n const env = isRecord(options.env) ? options.env : process.env\n const nowMs = typeof options.nowMs === \"number\" ? options.nowMs : Date.now()\n const cwd = typeof options.cwd === \"string\" ? options.cwd : process.cwd()\n const branch = typeof options.branch === \"string\" ? options.branch : \"\"\n const directory = workingDirectory(input, cwd)\n\n const model = `${ESC}38;5;208m${ESC}1m${modelName(input)}${ESC}22m${ESC}0m`\n const folder = `${ESC}1m${ESC}38;2;76;208;222m${pathBasename(directory)}${ESC}22m${ESC}0m`\n const branchSegment = branch === \"\" ? \"\" : `${DOT}${ESC}1m${ESC}38;2;192;103;222m${branch}${ESC}22m${ESC}0m`\n const context = contextSegment(input, env)\n\n const rateLimits = isRecord(input.rate_limits) ? input.rate_limits : {}\n const fiveHour = quotaWindow(rateLimits.five_hour, \"5h\", \"100;200;200\", nowMs)\n const sevenDay = quotaWindow(rateLimits.seven_day, \"7d\", \"230;180;90\", nowMs)\n const quota = fiveHour === \"\" && sevenDay === \"\"\n ? \"\"\n : `${PIPE}${fiveHour}${fiveHour !== \"\" && sevenDay !== \"\" ? DOT : \"\"}${sevenDay}`\n\n return `${model}${PIPE}${folder}${branchSegment}${context === \"\" ? \"\" : `${PIPE}${context}`}${quota}`\n}\n\nexport async function main(options = {}) {\n const readStdin = options.readStdin ?? (() => Bun.stdin.text())\n const writeStdout = options.writeStdout ?? ((value) => process.stdout.write(value))\n let raw\n try {\n raw = await readStdin()\n } catch {\n return 0\n }\n\n let input\n try {\n input = JSON.parse(raw)\n } catch {\n return 0\n }\n if (!isRecord(input)) return 0\n\n const cwd = typeof options.cwd === \"string\" ? options.cwd : process.cwd()\n const directory = workingDirectory(input, cwd)\n const branch = resolveBranch(\n directory,\n options.which ?? ((name) => Bun.which(name)),\n options.spawnSync ?? ((argv, spawnOptions) => Bun.spawnSync(argv, spawnOptions))\n )\n const output = formatStatusline(input, {\n env: options.env ?? process.env,\n nowMs: options.nowMs ?? Date.now(),\n cwd,\n branch\n })\n if (output !== \"\") writeStdout(`${output}\\n`)\n return 0\n}\n\nif (import.meta.main) process.exit(await main())\n",
12
14
  "SoT/.claude/bin/session-start.mjs": "import { readFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\n\nfunction isRecord(value) {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction nonEmpty(value, fallback) {\n return typeof value === \"string\" && value !== \"\" ? value : fallback\n}\n\nfunction pad(value) {\n return String(value).padStart(2, \"0\")\n}\n\nfunction configuredEffort(home, readText) {\n try {\n const parsed = JSON.parse(readText(`${home}/.claude/settings.json`))\n return isRecord(parsed) ? nonEmpty(parsed.effortLevel, \"default\") : \"default\"\n } catch {\n return \"default\"\n }\n}\n\nfunction localZone(now) {\n const part = new Intl.DateTimeFormat(\"en-US\", { timeZoneName: \"short\" })\n .formatToParts(now)\n .find((value) => value.type === \"timeZoneName\")\n return part?.value ?? \"\"\n}\n\nexport function sessionStartLines(options = {}) {\n const env = isRecord(options.env) ? options.env : process.env\n const now = options.now instanceof Date ? options.now : new Date()\n const home = typeof options.home === \"string\" ? options.home : homedir()\n const readText = options.readText ?? ((path) => readFileSync(path, \"utf8\"))\n const weekday = new Intl.DateTimeFormat(\"en-US\", { weekday: \"long\" }).format(now)\n const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`\n const time = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`\n const effort = nonEmpty(env.CLAUDE_CODE_EFFORT_LEVEL, configuredEffort(home, readText))\n const context = env.CLAUDE_CODE_DISABLE_1M_CONTEXT === \"1\" ? \"200K\" : \"1M\"\n const compactWindow = nonEmpty(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, \"full\")\n const subagent = nonEmpty(env.CLAUDE_CODE_SUBAGENT_MODEL, \"default\")\n return [\n `[CONTEXT] Current date: ${weekday}, ${date} ${time} ${localZone(now)}`,\n `[CONFIG] Context: ${context} | Compact-window: ${compactWindow} | Effort: ${effort} | Thinking: adaptive | Subagent: ${subagent}`\n ]\n}\n\nexport async function main(options = {}) {\n const writeStdout = options.writeStdout ?? ((value) => process.stdout.write(value))\n writeStdout(`${sessionStartLines(options).join(\"\\n\")}\\n`)\n return 0\n}\n\nif (import.meta.main) process.exit(await main())\n",
13
15
  "SoT/.claude/bin/notify.mjs": "const DEFAULT_SOUND = `${import.meta.dir}/../notification.mp3`\n\nexport function selectPlayer(options = {}) {\n const platform = options.platform ?? process.platform\n const sound = options.sound ?? DEFAULT_SOUND\n const which = options.which ?? ((name) => Bun.which(name))\n if (platform === \"darwin\") {\n const afplay = which(\"afplay\")\n if (typeof afplay === \"string\" && afplay !== \"\") return [afplay, sound]\n }\n const ffplay = which(\"ffplay\")\n if (typeof ffplay === \"string\" && ffplay !== \"\") {\n return [ffplay, \"-nodisp\", \"-autoexit\", \"-loglevel\", \"quiet\", sound]\n }\n const paplay = which(\"paplay\")\n if (typeof paplay === \"string\" && paplay !== \"\") return [paplay, sound]\n const aplay = which(\"aplay\")\n if (typeof aplay === \"string\" && aplay !== \"\") return [aplay, \"-q\", sound]\n return undefined\n}\n\nexport async function main(options = {}) {\n const sound = options.sound ?? DEFAULT_SOUND\n const fileExists = options.fileExists ?? ((path) => Bun.file(path).exists())\n if (!await fileExists(sound)) return 0\n const command = selectPlayer({ ...options, sound })\n if (command === undefined) return 0\n const spawnSync = options.spawnSync ?? ((argv, spawnOptions) => Bun.spawnSync(argv, spawnOptions))\n const result = spawnSync(command, { stdin: \"ignore\", stdout: \"ignore\", stderr: \"ignore\" })\n return typeof result.exitCode === \"number\" ? result.exitCode : 1\n}\n\nif (import.meta.main) process.exit(await main())\n",
@@ -38,4 +40,4 @@ export const GENERATED_PAYLOAD_PATHS = [
38
40
  "notification.mp3"
39
41
  ] as const
40
42
 
41
- export const GENERATED_PAYLOAD_HASH = "4b7580dd66aeaabf4a11c62a01290837398c9ced7790d2dcb2ef056c9f2151e7"
43
+ export const GENERATED_PAYLOAD_HASH = "ee72d5dbc640f90fbacba29a6486e668adbbf7632e958f55952d8eb94cc8286e"
package/cli/src/main.ts CHANGED
@@ -12,6 +12,7 @@ import { statusCommand } from "./commands/status"
12
12
  import { syncCommand } from "./commands/sync"
13
13
  import { toolchainCommand } from "./commands/toolchain"
14
14
  import { updateCommand } from "./commands/update"
15
+ import { GENERATED_PACKAGE_VERSION } from "./generated/sotPayload"
15
16
 
16
17
  const root = Command.make("docks-kit", {}, () =>
17
18
  Effect.gen(function* () {
@@ -59,16 +60,25 @@ if (process.env["DOCKS_KIT_ENGINE"] === "native-raw") {
59
60
 
60
61
  const cli = Command.run(root, {
61
62
  name: "docks-kit",
62
- version: "0.1.0"
63
+ version: GENERATED_PACKAGE_VERSION
63
64
  })
64
65
 
65
- // @effect/cli's Options.repeated does not accept `--flag=value` syntax (only
66
- // `--flag value`), but the documented form for the repeatable opt-in is
67
- // `--claude-plugin=<name>` normalize it here so both spellings work.
68
- const argv = process.argv.flatMap((a) =>
69
- a.startsWith("--claude-plugin=")
70
- ? ["--claude-plugin", a.slice("--claude-plugin=".length)]
71
- : [a]
72
- )
66
+ // Normalize the repeatable plugin's documented equals form and exact empty
67
+ // text-option assignments that @effect/cli otherwise routes into positional
68
+ // targets. EngineNative owns the resulting shared empty-value validation.
69
+ const emptyTextOptions = new Set([
70
+ "--claude-model=",
71
+ "--claude-effort=",
72
+ "--claude-advisor=",
73
+ "--codex-model=",
74
+ "--codex-effort="
75
+ ])
76
+ const argv = process.argv.flatMap((a) => {
77
+ if (a.startsWith("--claude-plugin=")) {
78
+ return ["--claude-plugin", a.slice("--claude-plugin=".length)]
79
+ }
80
+ if (emptyTextOptions.has(a)) return [a.slice(0, -1), ""]
81
+ return [a]
82
+ })
73
83
 
74
84
  cli(argv).pipe(Effect.provide(Layer.mergeAll(BunContext.layer, EngineServicesLive)), BunRuntime.runMain)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Portable AI coding agent config kit — SoT sync engine + typed CLI for Claude Code, Codex, and universal agent skills",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,54 +0,0 @@
1
- /**
2
- * --claude-model deploy-time modifier, shared with the `model claude <value>`
3
- * direct mode.
4
- */
5
- import { p } from "./exec"
6
- import { readFileSync, renameSync, writeFileSync } from "node:fs"
7
-
8
- import type { Ctx } from "./index"
9
- import { isObject, jqStringify, parseJson } from "./jq"
10
-
11
- export function syncClaudeModel(ctx: Ctx, model: string): void {
12
- const { change, echo, err, verbose, warn } = ctx.services.logger
13
- const userSettings = p(ctx.home, ".claude", "settings.json")
14
-
15
- if (model === "") return
16
-
17
- if (ctx.dryRun) {
18
- if (model === "default") {
19
- echo(`[dry-run] (--claude-model) delete .model in ${userSettings} (account default applies)`)
20
- } else {
21
- echo(`[dry-run] (--claude-model) set .model=${model} in ${userSettings}`)
22
- }
23
- return
24
- }
25
-
26
- let text: string
27
- try {
28
- text = readFileSync(userSettings, "utf8")
29
- } catch {
30
- warn(`(--claude-model) ${userSettings} missing — skipped`)
31
- return
32
- }
33
- const doc = parseJson(text)
34
- if (doc === undefined) {
35
- err(`(--claude-model) ${userSettings} is not valid JSON — skipped`)
36
- return
37
- }
38
- if (isObject(doc)) {
39
- if (model === "default") {
40
- delete doc["model"]
41
- } else {
42
- doc["model"] = model
43
- }
44
- }
45
- const out = jqStringify(doc)
46
- if (out === text) {
47
- verbose(`Model: deployed settings model already ${model === "default" ? "unset (account default)" : model}`)
48
- return
49
- }
50
- writeFileSync(`${userSettings}.tmp`, out)
51
- renameSync(`${userSettings}.tmp`, userSettings)
52
- change(`Model: deployed settings model set to ${model} (SoT unchanged; flag-less sync reverts)`)
53
- ctx.nextStepTriggers.claudeRestart = true
54
- }