docks-kit 0.16.8 → 0.16.9

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
@@ -78,6 +78,7 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the m
78
78
  ## Engineering rules
79
79
 
80
80
  - **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.
81
+ - **Failed harness-CLI operations fail the sync.** A kit-managed marketplace or plugin command that exits non-zero is recorded through `failures.ts recordFailure` — it still warns in place, and `index.ts engineSync, failure ledger` lists every entry under `--- Failures ---` after the summary, then returns exit 1. A skip is not a failure: a missing harness CLI, missing git, a missing `SoT/toolchain.json` pin, and an unavailable plugin inventory stay warn-only with exit 0, and `ompSync.ts syncMarketplace, probe guard` returns before spawning so a host without omp never records one. Aggregate roll-up warnings (`N plugin operation(s) failed`) stay warns, because each counted site already records itself.
81
82
  - **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.
82
83
  - **Effect 4 CLI stack.** The CLI pins `effect@4.0.0-rc.109` (including `effect/unstable/cli`), `@effect/platform-bun@4.0.0-rc.109` (`BunServices.layer`, `BunRuntime.runMain`), `@effect/vitest@4.0.0-rc.109`, and `vitest@4.1.11` (inside the `@effect/vitest` peer range `>=4.1.0 <5.0.0`; 4.1.11 fixes GHSA-82fw-gwwq-j7x9). `@effect/cli` and `@effect/platform` are removed and must not be reintroduced.
83
84
  - **Effect skill routing.** Effect work in this checkout must verify migration and API call shapes against the installed declarations under `node_modules/effect/dist/unstable/cli/`, never from memory or a mutable dist-tag. The `effect-ts-setup`, `effect-ts-port`, and `effect-ts-specialist` skills target Effect 3.x and do not apply.
@@ -21,6 +21,17 @@ is absent from user settings.
21
21
  5. Re-assert SoT enabled-state (undoes install's enable side effect on
22
22
  false-keyed plugins)
23
23
 
24
+ ## Failed operations fail the sync
25
+
26
+ A marketplace or plugin command that exits non-zero is warned in place AND
27
+ recorded, then listed under a `--- Failures ---` block after the summary; the
28
+ sync exits 1. A stale plugin therefore no longer looks like a clean sync.
29
+
30
+ Not a failure: a missing harness CLI, missing git, a missing
31
+ `SoT/toolchain.json` pin, and an unavailable plugin inventory. Those stay
32
+ warn-and-skip with exit 0, so a host without Claude, Codex, or omp installed
33
+ still syncs its deployed config cleanly.
34
+
24
35
  ## Optional opt-ins
25
36
 
26
37
  Situational plugins are kept OUT of the SoT and opted in per machine:
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { existsSync, renameSync, writeFileSync } from "node:fs"
7
7
  import { p, spawnProcess } from "./exec"
8
+ import { recordFailure } from "./failures"
8
9
  import type { Ctx } from "./index"
9
10
  import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, readJsonFile, type Json } from "./jq"
10
11
  import { field } from "./toolchain"
@@ -95,7 +96,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
95
96
  if (marketplaceResult.ok) {
96
97
  addedMp++
97
98
  } else {
98
- warn(`Failed to add marketplace: ${mpName} (${repo})`)
99
+ recordFailure(ctx, `Failed to add marketplace: ${mpName} (${repo})`)
99
100
  f1++
100
101
  }
101
102
  }
@@ -104,6 +105,8 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
104
105
  // refreshing each source marketplace once so the install resolves a current snapshot.
105
106
  let addedPl = 0
106
107
  let f2 = 0
108
+ let f3 = 0
109
+ let f4 = 0
107
110
  const refreshedMarketplaces = new Set<string>()
108
111
  for (const pluginId of sortedKeys(sotPlugins)) {
109
112
  if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
@@ -111,8 +114,12 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
111
114
  const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
112
115
  if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
113
116
  progress(`Refreshing marketplace ${mpName}...`)
114
- await cli(["plugin", "marketplace", "update", mpName])
117
+ const refreshResult = await cli(["plugin", "marketplace", "update", mpName])
115
118
  clearProgress()
119
+ if (!refreshResult.ok) {
120
+ recordFailure(ctx, `Failed to refresh marketplace: ${mpName}`)
121
+ f3++
122
+ }
116
123
  refreshedMarketplaces.add(mpName)
117
124
  }
118
125
  progress(`Installing plugin ${pluginId}...`)
@@ -121,7 +128,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
121
128
  if (installResult.ok) {
122
129
  addedPl++
123
130
  } else {
124
- warn(`Failed to install plugin: ${pluginId}`)
131
+ recordFailure(ctx, `Failed to install plugin: ${pluginId}`)
125
132
  f2++
126
133
  }
127
134
  }
@@ -143,12 +150,21 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
143
150
  }
144
151
 
145
152
  // Pass 3 — refresh the kit-owned marketplaces unless the update command
146
- // selected its install-missing-only fast path.
153
+ // selected its install-missing-only fast path. Pass 2 already refreshed the
154
+ // source marketplace of every plugin it installed, so skip those: a second
155
+ // fetch seconds later cannot resolve a newer snapshot, and re-running it
156
+ // would duplicate one failure in the ledger and in the failed-operation count.
147
157
  if (!ctx.skipPluginRefresh) {
148
158
  for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
159
+ if (refreshedMarketplaces.has(mpName)) continue
149
160
  progress(`Refreshing marketplace ${mpName}...`)
150
- await cli(["plugin", "marketplace", "update", mpName])
161
+ const refreshResult = await cli(["plugin", "marketplace", "update", mpName])
151
162
  clearProgress()
163
+ refreshedMarketplaces.add(mpName)
164
+ if (!refreshResult.ok) {
165
+ recordFailure(ctx, `Failed to refresh marketplace: ${mpName}`)
166
+ f3++
167
+ }
152
168
  }
153
169
  // Pass 4 — update the kit-owned installed plugins.
154
170
  for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
@@ -156,7 +172,12 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
156
172
  progress(`Updating plugin ${pluginId}...`)
157
173
  const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
158
174
  clearProgress()
159
- if (updateResult.out.includes("Successfully updated")) updatedPl++
175
+ if (!updateResult.ok) {
176
+ recordFailure(ctx, `Failed to update plugin: ${pluginId}`)
177
+ f4++
178
+ } else if (updateResult.out.includes("Successfully updated")) {
179
+ updatedPl++
180
+ }
160
181
  }
161
182
  }
162
183
 
@@ -175,7 +196,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
175
196
  if (uninstallResult.ok) {
176
197
  removedPl++
177
198
  } else {
178
- warn(`Failed to uninstall plugin: ${pluginId}`)
199
+ recordFailure(ctx, `Failed to uninstall plugin: ${pluginId}`)
179
200
  f5++
180
201
  }
181
202
  }
@@ -191,7 +212,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
191
212
  if (removeResult.ok) {
192
213
  removedMp++
193
214
  } else {
194
- warn(`Failed to remove marketplace: ${mpName}`)
215
+ recordFailure(ctx, `Failed to remove marketplace: ${mpName}`)
195
216
  f6++
196
217
  }
197
218
  }
@@ -203,7 +224,7 @@ export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
203
224
  ctx.nextStepTriggers.claudePlugins = true
204
225
  }
205
226
 
206
- const failed = f1 + f2 + f5 + f6
227
+ const failed = f1 + f2 + f3 + f4 + f5 + f6
207
228
  if (addedMp > 0 || addedPl > 0 || updatedPl > 0 || removedPl > 0 || removedMp > 0) {
208
229
  change(`Plugins synced (marketplaces: +${addedMp} -${removedMp}, plugins: +${addedPl} ~${updatedPl} -${removedPl})`)
209
230
  ctx.nextStepTriggers.claudePlugins = true
@@ -229,7 +250,7 @@ async function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, us
229
250
  if ((await cli(["plugin", "disable", pluginId])).ok) {
230
251
  cliDisabled = true
231
252
  } else {
232
- warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
253
+ recordFailure(ctx, `Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
233
254
  }
234
255
  }
235
256
 
@@ -250,7 +271,7 @@ async function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, us
250
271
  // ------------------------------------------------------ optional plugins ----
251
272
 
252
273
  async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
253
- const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
274
+ const { change, clearProgress, progress, verbose } = ctx.services.logger
254
275
  const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
255
276
  const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
256
277
  const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
@@ -261,7 +282,7 @@ async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: strin
261
282
  const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
262
283
  if (!has) {
263
284
  if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
264
- warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
285
+ recordFailure(ctx, `Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
265
286
  return false
266
287
  }
267
288
  marketplaceAdded = true
@@ -275,7 +296,7 @@ async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: strin
275
296
  clearProgress()
276
297
  if (!installResult.ok) {
277
298
  if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
278
- warn(`Failed to install optional plugin ${pluginId}`)
299
+ recordFailure(ctx, `Failed to install optional plugin ${pluginId}`)
279
300
  return marketplaceAdded
280
301
  }
281
302
  }
@@ -288,7 +309,7 @@ async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: strin
288
309
 
289
310
  if (!(await cli(["plugin", "enable", pluginId])).ok) {
290
311
  if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
291
- warn(`Failed to enable optional plugin ${pluginId}`)
312
+ recordFailure(ctx, `Failed to enable optional plugin ${pluginId}`)
292
313
  return marketplaceAdded || !wasInstalled
293
314
  }
294
315
  const changed = marketplaceAdded || !wasInstalled || !wasEnabled
@@ -7,6 +7,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync,
7
7
 
8
8
  import { mergeTableSettings, mergeTopLevelSettings, syncCodexEffort, syncCodexModel } from "./codexToml"
9
9
  import { p, spawnProcess } from "./exec"
10
+ import { recordFailure } from "./failures"
10
11
  import type { Ctx } from "./index"
11
12
  import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
12
13
  import { hostOs } from "./os"
@@ -466,7 +467,7 @@ function marketplaceSource(marketplace: string, configFile: string): string {
466
467
  }
467
468
 
468
469
  async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promise<void> {
469
- const { change, echo, warn } = ctx.services.logger
470
+ const { change, echo } = ctx.services.logger
470
471
  if (ctx.dryRun) {
471
472
  echo("[dry-run] remove legacy configured Codex Docks marketplace when personal marketplace is deployed")
472
473
  return
@@ -481,7 +482,7 @@ async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promi
481
482
  change("Removed legacy configured Codex Docks marketplace; using personal marketplace file")
482
483
  ctx.nextStepTriggers.codexRestart = true
483
484
  } else {
484
- warn("Failed to remove legacy configured Codex Docks marketplace")
485
+ recordFailure(ctx, "Failed to remove legacy configured Codex Docks marketplace")
485
486
  }
486
487
  }
487
488
 
@@ -592,13 +593,15 @@ async function syncPlugins(ctx: Ctx, sotConfigText: string): Promise<void> {
592
593
  if (res.error === undefined && res.exitCode === 0) {
593
594
  refreshed++
594
595
  } else if (addOut.includes("could not find a Codex CLI binary")) {
595
- warn(
596
+ recordFailure(
597
+ ctx,
596
598
  `Codex plugin refresh hit a stale launcher/wrapper on PATH - install current standalone Codex with: ${standaloneInstallCommand(ctx)}`
597
599
  )
598
600
  failed++
599
601
  } else {
600
602
  const failureLine = addOut.split("\n")[0] ?? ""
601
- warn(
603
+ recordFailure(
604
+ ctx,
602
605
  `Codex plugin refresh failed for ${pluginId}: ${failureLine !== "" ? failureLine : "unknown error"}; run manually: codex plugin add ${pluginId}`
603
606
  )
604
607
  failed++
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Failure ledger for harness-CLI operations. A kit-managed marketplace or
3
+ * plugin command that exits non-zero is recorded here so engineSync can list
4
+ * it and exit non-zero instead of reporting a clean sync.
5
+ */
6
+ import type { Ctx } from "./index"
7
+
8
+ /**
9
+ * Record a failed harness-CLI operation. The warning still prints immediately so
10
+ * position in the output is preserved; the recorded message also lets engineSync
11
+ * list the failure in the summary and exit non-zero.
12
+ */
13
+ export function recordFailure(ctx: Ctx, message: string): void {
14
+ ctx.failures.push(message)
15
+ ctx.services.logger.warn(message)
16
+ }
@@ -117,6 +117,8 @@ export interface Ctx {
117
117
  skillsRestart: boolean
118
118
  ompRestart: boolean
119
119
  }
120
+ /** Harness-CLI operations that failed this run; a non-empty list fails the sync. */
121
+ readonly failures: Array<string>
120
122
  }
121
123
 
122
124
  /** Globals default from env using the historical ${VAR:-default} contract. */
@@ -170,7 +172,8 @@ function makeCtx(services: EngineServices): Ctx {
170
172
  codexRestart: false,
171
173
  skillsRestart: false,
172
174
  ompRestart: false
173
- }
175
+ },
176
+ failures: []
174
177
  }
175
178
  }
176
179
 
@@ -291,6 +294,12 @@ async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number
291
294
  echo("")
292
295
  for (const line of advice) echo(line)
293
296
  }
297
+ if (ctx.failures.length > 0) {
298
+ echo("")
299
+ echo("--- Failures ---")
300
+ for (const failure of ctx.failures) echo(`- ${failure}`)
301
+ return 1
302
+ }
294
303
  return 0
295
304
  }
296
305
 
@@ -1,7 +1,8 @@
1
1
  /**
2
- * EngineNative `sync omp` pipeline. config.yml and models.yml merge through
3
- * mergeOmpConfig to preserve user-only keys. Paths come from ompPaths because
4
- * profiles, PI_CONFIG_DIR, PI_CODING_AGENT_DIR, and XDG roots each move them.
2
+ * EngineNative `sync omp` pipeline. config.yml merges through mergeOmpConfig
3
+ * and models.yml through mergeOmpModels. Both preserve user-only keys.
4
+ * Paths come from ompPaths because profiles, PI_CONFIG_DIR,
5
+ * PI_CODING_AGENT_DIR, and XDG roots each move them.
5
6
  * Resolution stays within the environment and filesystem probes so no omp
6
7
  * subcommand runs under ctx.dryRun.
7
8
  */
@@ -11,6 +12,7 @@ import { basename, isAbsolute, resolve } from "node:path"
11
12
  import { payloadDisplayPath, payloadText, type PayloadPath } from "../payload"
12
13
  import { bunBootstrap } from "./bun"
13
14
  import { p, spawnProcess, type AsyncProcessResult } from "./exec"
15
+ import { recordFailure } from "./failures"
14
16
  import type { Ctx } from "./index"
15
17
  import { isObject, parseJson } from "./jq"
16
18
  import { ompPaths } from "./ompPaths"
@@ -190,7 +192,7 @@ function firstOutputLine(result: AsyncProcessResult): string {
190
192
  * adoption and leaves the active registry present.
191
193
  */
192
194
  async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFile?: string): Promise<void> {
193
- const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
195
+ const { change, clearProgress, echo, progress, verbose } = ctx.services.logger
194
196
  const registered = registryHasDocks(registryFile)
195
197
  const adoptable = !registered && legacyRegistryFile !== undefined && registryHasDocks(legacyRegistryFile)
196
198
 
@@ -206,6 +208,13 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
206
208
  return
207
209
  }
208
210
 
211
+ // syncPlugins owns the single skip message for both missing CLIs, and it runs
212
+ // right after this pass. Without omp no marketplace command can run at all,
213
+ // and without git a marketplace clone cannot resolve, so both are deliberate
214
+ // skips: return silently instead of spawning and recording a failure.
215
+ if (ctx.services.deps.probe("omp").state === "missing") return
216
+ if (ctx.services.deps.probe("git").state === "missing") return
217
+
209
218
  if (!registered && !adoptable) {
210
219
  progress("Registering omp docks marketplace...")
211
220
  const result = await spawnProcess("omp", ["plugin", "marketplace", "add", MARKETPLACE_SOURCE], {
@@ -215,7 +224,8 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
215
224
  if (result.error === undefined && result.exitCode === 0) {
216
225
  change("omp docks marketplace registered")
217
226
  } else {
218
- warn(
227
+ recordFailure(
228
+ ctx,
219
229
  `omp docks marketplace registration failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace add ${MARKETPLACE_SOURCE}`
220
230
  )
221
231
  }
@@ -239,7 +249,8 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
239
249
  if (listed.error === undefined && listed.exitCode === 0) {
240
250
  change("omp docks marketplace registry adopted; refresh-only update skipped")
241
251
  } else {
242
- warn(
252
+ recordFailure(
253
+ ctx,
243
254
  `omp docks marketplace adoption failed: ${firstOutputLine(listed)}; run manually: omp plugin marketplace list`
244
255
  )
245
256
  }
@@ -254,7 +265,8 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
254
265
  if (result.error === undefined && result.exitCode === 0) {
255
266
  verbose("omp docks marketplace refreshed")
256
267
  } else {
257
- warn(
268
+ recordFailure(
269
+ ctx,
258
270
  `omp docks marketplace update failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace update ${MARKETPLACE_NAME}`
259
271
  )
260
272
  }
@@ -308,13 +320,14 @@ async function runPluginCommand(
308
320
  plugin: string,
309
321
  args: ReadonlyArray<string>
310
322
  ): Promise<boolean> {
311
- const { clearProgress, progress, warn } = ctx.services.logger
323
+ const { clearProgress, progress } = ctx.services.logger
312
324
  progress(`Updating omp plugin ${plugin}...`)
313
325
  const result = await spawnProcess("omp", args, { stdio: ["ignore", "pipe", "pipe"] })
314
326
  clearProgress()
315
327
  if (result.error === undefined && result.exitCode === 0) return true
316
328
 
317
- warn(
329
+ recordFailure(
330
+ ctx,
318
331
  `omp plugin operation failed for ${plugin}: ${firstOutputLine(result)}; run manually: omp ${args.join(" ")}`
319
332
  )
320
333
  return false
@@ -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.16.8"
4
+ export const GENERATED_PACKAGE_VERSION = "0.16.9"
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.16.8",
3
+ "version": "0.16.9",
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",