docks-kit 0.17.0 → 0.17.1

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
@@ -75,7 +75,7 @@ omp SoT notes:
75
75
  - Sync installs `pi-intercom` at the verified version from `SoT/toolchain.json`.
76
76
  - The omp CLI is upstream-owned and self-updating through `omp update`. Sync never installs or upgrades the CLI.
77
77
  - `docks-kit omp [--model <selector>|--pick] [args...]` starts one interactive omp session on a single free model. It renders a run overlay to `~/.cache/docks-kit/omp-free-<model>-<digest>.yml` (mode 0600) and passes it through omp's repeatable `--config` flag. `ompOverlay.ts overlayFileName` gives each model its own file, because omp can re-read the overlay during a live session and a second launcher on another model must not rewrite it. Deployed `~/.omp/agent/` files stay untouched, so the next plain `omp` run uses the paid configuration again. Remaining arguments forward verbatim to omp.
78
- - The session model persists per machine in `~/.docks-kit/state.json` under `ompSession`, next to `harnesses`. The default is `opencode-zen/muse-spark-1.3-contributor-free` at `xhigh`. `ompOverlay.ts ladderCeiling, advisorLevelFor` derive both the session level and the advisor level from the ladder of the chosen model, never from a fixed list: free ladders are not uniform, three free models publish no `medium`, and two publish no ladder, which the overlay renders as bare selectors. The picker lists only zero-cost catalog models.
78
+ - The session model persists per machine in `~/.docks-kit/state.json` under `ompSession`, next to `harnesses`. The default is `opencode-zen/muse-spark-1.3-contributor-free` at `xhigh`. `ompOverlay.ts ladderCeiling, advisorLevelFor` derive both levels from the ladder of the chosen model for the `--model` path, and `ompOverlay.ts planEffortChoice` drives the `--pick` wizard, which asks whether every role shares one level and otherwise takes one level for the main roles and one for the advisor from that model's own ladder. Levels never come from a fixed list: free ladders are not uniform, three free models publish no `medium`, and two publish no ladder, which the overlay renders as bare selectors. The picker lists only zero-cost catalog models.
79
79
 
80
80
  For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the matching SoT directory.
81
81
 
package/README.md CHANGED
@@ -48,6 +48,7 @@ docks-kit harnesses view or change this machine's se
48
48
  docks-kit update [--no-sync] self-update the kit (autodetects checkout vs global install), then sync
49
49
  docks-kit model <claude|codex> [value] get/set the DEPLOYED model (TTY picker)
50
50
  docks-kit models [claude|codex] model catalogs (`--json`)
51
+ docks-kit omp [--model <m>|--pick] [args...] one omp session on a free model, nothing deployed changes
51
52
  docks-kit toolchain [check|ensure <tool>] verified-version floors for external tools
52
53
  docks-kit status [--json] deployed-vs-SoT drift + toolchain + counts
53
54
  docks-kit plugins list [--json] enabledPlugins tri-state vs installed
@@ -254,21 +254,42 @@ fail when the session starts.
254
254
 
255
255
  The choice persists per machine in `~/.docks-kit/state.json` under
256
256
  `ompSession`, next to `harnesses`. It survives across sessions. It is never
257
- committed. `--model <selector>` records one selector. `--pick` opens an
258
- interactive picker.
257
+ committed. `--model <selector>` records one selector and derives both levels
258
+ from the catalog row. `--pick` opens an interactive wizard.
259
259
 
260
260
  The picker lists only models the live `omp models --json` catalog reports at
261
261
  zero input and output cost (26 entries on 2026-09-11). This path cannot start
262
262
  a paid session. The catalog can advertise a free model that the account
263
263
  cannot call; omp reports that provider error unchanged.
264
264
 
265
+ After the model, the wizard asks about thinking levels. `ompOverlay.ts
266
+ planEffortChoice` decides which questions apply, because omp accepts a
267
+ `:level` suffix only for a level the chosen model publishes:
268
+
269
+ - No ladder: the wizard asks nothing and the overlay omits every level.
270
+ - One level: the wizard states that level and asks nothing.
271
+ - Two or more levels: the wizard asks whether every role uses the same
272
+ level. Yes takes the highest level the model offers. No asks one level for
273
+ all roles except the advisor, then one level for the advisor.
274
+
275
+ The advisor question leads with a recommendation from `ompOverlay.ts
276
+ advisorRecommendation`: two steps down the model own ladder, clamped to the
277
+ lowest level that model publishes. The recommendation is the first row
278
+ because `Prompt.Select` starts on the first entry and accepts no initial
279
+ index, so `Enter` takes it. The full ladder follows in ladder order. A
280
+ recommendation equal to the chosen level is omitted rather than duplicated.
281
+
282
+ A session whose advisor differs from the other roles reports both on the
283
+ launch line.
284
+
265
285
  An omp login is required. The kit owns no login flow. It surfaces omp's own
266
286
  authentication error unchanged.
267
287
 
268
288
  When the catalog renames the free variant, change only the default selector
269
- constant. A ladder change needs no code change, because both levels come from
270
- the catalog row of the chosen model. Refresh the recorded default and the
271
- ladder counts in these docs in the same commit.
289
+ constant. A ladder change needs no code change. Under `--model` both levels
290
+ are derived from the catalog row of the chosen model, and under `--pick` the
291
+ user chooses them from the ladder that same row publishes. Refresh the
292
+ recorded default and the ladder counts in these docs in the same commit.
272
293
 
273
294
  ## Maintenance
274
295
 
@@ -12,9 +12,11 @@ import {
12
12
  } from "../engine-native/harnesses"
13
13
  import {
14
14
  advisorLevelFor,
15
+ advisorRecommendation,
15
16
  buildOmpArgs,
16
17
  ladderCeiling,
17
18
  overlayFileName,
19
+ planEffortChoice,
18
20
  parseFreeModels,
19
21
  renderFreeOverlay,
20
22
  type CatalogModel
@@ -91,6 +93,64 @@ const loadFreeCatalog = (omp: string): Effect.Effect<ReadonlyArray<CatalogModel>
91
93
  return free
92
94
  })
93
95
 
96
+ /**
97
+ * Ask for the thinking levels of one model. Every choice comes from that
98
+ * model own ladder, because omp rejects a `:level` suffix the model does not
99
+ * publish. A model with no ladder and a model with one level are settled
100
+ * without a question.
101
+ */
102
+ const pickLevels = (model: CatalogModel) =>
103
+ Effect.gen(function* () {
104
+ const plan = planEffortChoice(model.thinking)
105
+ if (plan.kind === "none") {
106
+ yield* Console.error(`${model.name} publishes no thinking levels; the model default applies`)
107
+ return { thinking: undefined, advisorThinking: undefined }
108
+ }
109
+ if (plan.kind === "fixed") {
110
+ yield* Console.error(`${model.name} publishes one thinking level: ${plan.level}`)
111
+ return { thinking: plan.level, advisorThinking: plan.level }
112
+ }
113
+ const uniform = yield* Prompt.Select({
114
+ message: "Use the same thinking level for every role?",
115
+ choices: [
116
+ { title: "Yes", value: true, description: `every role runs at ${plan.highest}, the highest this model offers` },
117
+ { title: "No", value: false, description: "set the advisor apart from the other roles" }
118
+ ]
119
+ })
120
+ if (uniform) return { thinking: plan.highest, advisorThinking: plan.highest }
121
+ const ladderChoices = plan.levels.map((level) => ({
122
+ title: level,
123
+ value: level,
124
+ description:
125
+ level === plan.highest ? "highest this model offers" : level === plan.levels[0] ? "lowest this model offers" : ""
126
+ }))
127
+ const thinking = yield* Prompt.Select({
128
+ message: "Thinking level for all roles except the advisor",
129
+ choices: ladderChoices
130
+ })
131
+ const suggested = advisorRecommendation(plan.levels, thinking)
132
+ // The list leads with the suggestion, because Prompt.Select always starts
133
+ // on the first entry and offers no initial index. The full ladder follows
134
+ // in ladder order, so a different level stays one keypress away. A
135
+ // suggestion equal to the main level would only duplicate a row.
136
+ const advisorChoices =
137
+ suggested === undefined || suggested === thinking
138
+ ? ladderChoices
139
+ : [
140
+ {
141
+ title: suggested,
142
+ value: suggested,
143
+ description: `recommended, two levels below ${thinking}`
144
+ },
145
+ ...ladderChoices
146
+ ]
147
+ const advisorThinking = yield* Prompt.Select({
148
+ message: "Thinking level for the advisor role",
149
+ choices: advisorChoices
150
+ })
151
+ return { thinking, advisorThinking }
152
+ })
153
+
94
154
  export const ompCommand = Command.make("omp", { model, pick, args }, (config) =>
95
155
  Effect.gen(function* () {
96
156
  if (Option.isSome(config.model) && config.pick) {
@@ -139,9 +199,14 @@ export const ompCommand = Command.make("omp", { model, pick, args }, (config) =>
139
199
  })
140
200
  const match = free.find((candidate) => candidate.selector === chosen)
141
201
  if (match === undefined) return yield* bail(`Model '${chosen}' is not in the free catalog`)
142
- const thinking = ladderCeiling(match.thinking)
143
- const advisorThinking = advisorLevelFor(match.thinking)
144
- yield* Effect.sync(() => writeOmpSessionModel(home, { selector: match.selector, thinking, advisorThinking }))
202
+ const chosenLevels = yield* pickLevels(match)
203
+ yield* Effect.sync(() =>
204
+ writeOmpSessionModel(home, {
205
+ selector: match.selector,
206
+ thinking: chosenLevels.thinking,
207
+ advisorThinking: chosenLevels.advisorThinking
208
+ })
209
+ )
145
210
  }
146
211
 
147
212
  const session: OmpSessionModel = (yield* Effect.sync(() => readOmpSessionModel(home))) ??
@@ -163,12 +228,17 @@ export const ompCommand = Command.make("omp", { model, pick, args }, (config) =>
163
228
  })
164
229
 
165
230
  // A level-free model states the model default so the line never shows
166
- // an undefined level.
167
- const hasLevel = typeof session.thinking === "string" && session.thinking.trim() !== ""
231
+ // an undefined level. The advisor appears only when it differs, because
232
+ // a uniform session has nothing extra to report.
233
+ const level = typeof session.thinking === "string" && session.thinking.trim() !== "" ? session.thinking : undefined
234
+ const advisor =
235
+ typeof session.advisorThinking === "string" && session.advisorThinking.trim() !== ""
236
+ ? session.advisorThinking
237
+ : undefined
238
+ const levelText = level === undefined ? "the model default thinking level" : `${level} thinking`
239
+ const advisorText = advisor !== undefined && advisor !== level ? `, advisor at ${advisor}` : ""
168
240
  yield* Console.error(
169
- hasLevel
170
- ? `Starting omp with ${session.selector} at ${session.thinking} thinking (session only; deployed config unchanged)`
171
- : `Starting omp with ${session.selector} at the model default thinking level (session only; deployed config unchanged)`
241
+ `Starting omp with ${session.selector} at ${levelText}${advisorText} (session only; deployed config unchanged)`
172
242
  )
173
243
 
174
244
  const child = yield* Effect.sync(() =>
@@ -134,6 +134,43 @@ export function advisorLevelFor(levels: ReadonlyArray<string>): string | undefin
134
134
  return advisor ?? (known[0] as string)
135
135
  }
136
136
 
137
+ /**
138
+ * What the picker may ask about thinking levels for one model. omp accepts a
139
+ * `:level` suffix on a role only for a level the model publishes, so the
140
+ * question set comes from the model own ladder: no ladder means no question,
141
+ * a single level means no choice to make, and two or more levels mean the
142
+ * user can split the advisor from the other roles.
143
+ */
144
+ export type EffortPlan =
145
+ | { readonly kind: "none" }
146
+ | { readonly kind: "fixed"; readonly level: string }
147
+ | { readonly kind: "choose"; readonly levels: ReadonlyArray<string>; readonly highest: string }
148
+
149
+ export function planEffortChoice(levels: ReadonlyArray<string>): EffortPlan {
150
+ const known = THINKING_LADDER.filter((level) => levels.includes(level))
151
+ const only = known[0]
152
+ if (only === undefined) return { kind: "none" }
153
+ if (known.length === 1) return { kind: "fixed", level: only }
154
+ return { kind: "choose", levels: known, highest: known[known.length - 1] as string }
155
+ }
156
+
157
+ /**
158
+ * Recommend an advisor level for a chosen main level. The advisor runs quick
159
+ * review passes, so it sits two steps down the model own ladder, and it
160
+ * clamps to the lowest level the model publishes. Steps count positions in
161
+ * that model ladder, not the global ladder, so a sparse ladder such as
162
+ * `high, max` still recommends a real level.
163
+ */
164
+ export function advisorRecommendation(
165
+ levels: ReadonlyArray<string>,
166
+ main: string
167
+ ): string | undefined {
168
+ const known = THINKING_LADDER.filter((level) => levels.includes(level))
169
+ const index = known.indexOf(main)
170
+ if (index < 0) return known[0]
171
+ return known[Math.max(0, index - 2)]
172
+ }
173
+
137
174
  /**
138
175
  * Render the run overlay as YAML text. Empty fallback chains keep a retry
139
176
  * from falling back onto a paid model. A model without a level renders bare
@@ -1,7 +1,7 @@
1
1
  // Generated by cli/scripts/generate-sot-payload.ts. DO NOT EDIT.
2
2
  // Edit SoT/, notification.mp3, or package.json, then run: bun cli/scripts/generate-sot-payload.ts
3
3
 
4
- export const GENERATED_PACKAGE_VERSION = "0.17.0"
4
+ export const GENERATED_PACKAGE_VERSION = "0.17.1"
5
5
 
6
6
  export const GENERATED_PAYLOAD_TEXT = {
7
7
  "SoT/.agents/skills.txt": "# Universal AI-agent skill manifest intentionally empty.\n# Global skill discovery is opt-in: add one <owner>/<repo> slug per line.\n# EngineNative ignores comments and blank lines.\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
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",