docks-kit 0.16.7 → 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 +3 -2
- package/cli/docs/plugins.md +11 -0
- package/cli/src/engine-native/claudePlugins.ts +35 -14
- package/cli/src/engine-native/codexSync.ts +7 -4
- package/cli/src/engine-native/failures.ts +16 -0
- package/cli/src/engine-native/index.ts +10 -1
- package/cli/src/engine-native/ompSync.ts +32 -14
- package/cli/src/engine-native/ompYaml.ts +50 -29
- package/cli/src/generated/sotPayload.ts +3 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -63,10 +63,10 @@ Codex SoT notes:
|
|
|
63
63
|
omp SoT notes:
|
|
64
64
|
- `SoT/.omp/AGENTS.md`, `config.yml`, `models.yml`, and `mcp.json` deploy to `~/.omp/agent/`.
|
|
65
65
|
- `SoT/.omp/intercom.json` deploys to `$PI_CODING_AGENT_DIR/intercom/config.json`. The default root is `~/.pi/agent`.
|
|
66
|
-
- `ompSync.ts syncMergedYaml` deep-merges `config.yml` through `mergeOmpConfig`.
|
|
66
|
+
- `ompSync.ts syncMergedYaml` deep-merges `config.yml` through `ompYaml.ts mergeOmpConfig` and `models.yml` through `mergeOmpModels`. Both wrap one generic mapping merge; only the config wrapper prunes stale `retry.fallbackChains` wildcards.
|
|
67
67
|
- `cycleOrder` ends with the `fable` role (`modelRoles.fable` = `anthropic/claude-fable-5-1:medium`, `modelTags.fable` visible, `retry.fallbackChains.fable` empty), so the model switcher reaches Fable 5.1 as its fourth stop and never falls back off it. The hidden `switch_fable` role points at the same model and level.
|
|
68
68
|
- `modelRoles.task` is `openai-codex/gpt-6-astra:low`, chosen for 2.60 s TTFT and 4k output tokens per task at cost parity with the previous `gpt-5.6-sol:high`. `task.agentModelOverrides` points four reviewer names at `@task`; only the bundled `reviewer` and `security-reviewer` are discoverable OMP agents, so those two inherit `task`, and the `code-reviewer` and `plan-reviewer` entries are dormant. Artificial Analysis publishes no per-level Astra Coding Agent Index score, so reviewer output is the signal to watch.
|
|
69
|
-
- `SoT/.omp/models.yml` deploys to `~/.omp/agent/models.yml` through
|
|
69
|
+
- `SoT/.omp/models.yml` deploys to `~/.omp/agent/models.yml` through `mergeOmpModels`, which keeps every deployed-only key. A user file may carry provider credentials, so whole-file replacement is wrong. It restricts the `gpt-6-astra` effort ladder to `low`, so every Astra subagent runs `low` while `task.maxEffort: high` lets `scout` and `sonic` reach Luna `high` with `effort: hi`. `cli/docs/omp-models.md` records the verification.
|
|
70
70
|
- `SoT/.omp/AGENTS.md` carries the rule `Please remove all mannered prose.` Anthropic's Fable 5.1 prompting guide documents mannered prose as a Fable 5.1 behavior and gives that sentence as its short-version fix: https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5-1
|
|
71
71
|
- `cli/docs/omp-models.md` (topic `omp-models`) records the role map rationale and the Artificial Analysis snapshot behind it. Model choices change with published benchmarks, so update that topic in the same commit as a role change.
|
|
72
72
|
- Sync registers the `docks` marketplace. It installs or upgrades `docks@docks` and `plan-lifecycle@docks` at user scope.
|
|
@@ -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.
|
package/cli/docs/plugins.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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,10 +12,11 @@ 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"
|
|
17
|
-
import { mergeOmpConfig } from "./ompYaml"
|
|
19
|
+
import { mergeOmpConfig, mergeOmpModels } from "./ompYaml"
|
|
18
20
|
import { field } from "./toolchain"
|
|
19
21
|
|
|
20
22
|
const MARKETPLACE_NAME = "docks"
|
|
@@ -35,8 +37,8 @@ export async function ompSync(ctx: Ctx): Promise<OmpState> {
|
|
|
35
37
|
|
|
36
38
|
syncWholeFile(ctx, "SoT/.omp/AGENTS.md", p(agentDir, "AGENTS.md"), "omp AGENTS.md already in sync", "omp AGENTS.md synced")
|
|
37
39
|
syncWholeFile(ctx, "SoT/.omp/mcp.json", p(agentDir, "mcp.json"), "omp mcp.json already in sync", "omp mcp.json synced")
|
|
38
|
-
syncMergedYaml(ctx, "SoT/.omp/config.yml", p(agentDir, "config.yml"))
|
|
39
|
-
syncMergedYaml(ctx, "SoT/.omp/models.yml", p(agentDir, "models.yml"))
|
|
40
|
+
syncMergedYaml(ctx, "SoT/.omp/config.yml", p(agentDir, "config.yml"), mergeOmpConfig)
|
|
41
|
+
syncMergedYaml(ctx, "SoT/.omp/models.yml", p(agentDir, "models.yml"), mergeOmpModels)
|
|
40
42
|
|
|
41
43
|
const intercomRootSetting = process.env["PI_CODING_AGENT_DIR"]
|
|
42
44
|
const intercomRoot = intercomRootSetting !== undefined && intercomRootSetting !== ""
|
|
@@ -112,7 +114,12 @@ function syncWholeFile(
|
|
|
112
114
|
ctx.nextStepTriggers.ompRestart = true
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
function syncMergedYaml(
|
|
117
|
+
function syncMergedYaml(
|
|
118
|
+
ctx: Ctx,
|
|
119
|
+
sourcePath: OmpTextPayloadPath,
|
|
120
|
+
target: string,
|
|
121
|
+
merge: (sotText: string, deployedText: string) => string
|
|
122
|
+
): void {
|
|
116
123
|
const { change, echo, verbose } = ctx.services.logger
|
|
117
124
|
const source = payloadDisplayPath(sourcePath)
|
|
118
125
|
const sotText = payloadText(sourcePath)
|
|
@@ -130,7 +137,7 @@ function syncMergedYaml(ctx: Ctx, sourcePath: OmpTextPayloadPath, target: string
|
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
const deployedText = readFileSync(target, "utf8")
|
|
133
|
-
const merged =
|
|
140
|
+
const merged = merge(sotText, deployedText)
|
|
134
141
|
if (merged === deployedText) {
|
|
135
142
|
verbose(`omp ${name} already in sync`)
|
|
136
143
|
return
|
|
@@ -185,7 +192,7 @@ function firstOutputLine(result: AsyncProcessResult): string {
|
|
|
185
192
|
* adoption and leaves the active registry present.
|
|
186
193
|
*/
|
|
187
194
|
async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFile?: string): Promise<void> {
|
|
188
|
-
const { change, clearProgress, echo, progress, verbose
|
|
195
|
+
const { change, clearProgress, echo, progress, verbose } = ctx.services.logger
|
|
189
196
|
const registered = registryHasDocks(registryFile)
|
|
190
197
|
const adoptable = !registered && legacyRegistryFile !== undefined && registryHasDocks(legacyRegistryFile)
|
|
191
198
|
|
|
@@ -201,6 +208,13 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
|
|
|
201
208
|
return
|
|
202
209
|
}
|
|
203
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
|
+
|
|
204
218
|
if (!registered && !adoptable) {
|
|
205
219
|
progress("Registering omp docks marketplace...")
|
|
206
220
|
const result = await spawnProcess("omp", ["plugin", "marketplace", "add", MARKETPLACE_SOURCE], {
|
|
@@ -210,7 +224,8 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
|
|
|
210
224
|
if (result.error === undefined && result.exitCode === 0) {
|
|
211
225
|
change("omp docks marketplace registered")
|
|
212
226
|
} else {
|
|
213
|
-
|
|
227
|
+
recordFailure(
|
|
228
|
+
ctx,
|
|
214
229
|
`omp docks marketplace registration failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace add ${MARKETPLACE_SOURCE}`
|
|
215
230
|
)
|
|
216
231
|
}
|
|
@@ -234,7 +249,8 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
|
|
|
234
249
|
if (listed.error === undefined && listed.exitCode === 0) {
|
|
235
250
|
change("omp docks marketplace registry adopted; refresh-only update skipped")
|
|
236
251
|
} else {
|
|
237
|
-
|
|
252
|
+
recordFailure(
|
|
253
|
+
ctx,
|
|
238
254
|
`omp docks marketplace adoption failed: ${firstOutputLine(listed)}; run manually: omp plugin marketplace list`
|
|
239
255
|
)
|
|
240
256
|
}
|
|
@@ -249,7 +265,8 @@ async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFil
|
|
|
249
265
|
if (result.error === undefined && result.exitCode === 0) {
|
|
250
266
|
verbose("omp docks marketplace refreshed")
|
|
251
267
|
} else {
|
|
252
|
-
|
|
268
|
+
recordFailure(
|
|
269
|
+
ctx,
|
|
253
270
|
`omp docks marketplace update failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace update ${MARKETPLACE_NAME}`
|
|
254
271
|
)
|
|
255
272
|
}
|
|
@@ -303,13 +320,14 @@ async function runPluginCommand(
|
|
|
303
320
|
plugin: string,
|
|
304
321
|
args: ReadonlyArray<string>
|
|
305
322
|
): Promise<boolean> {
|
|
306
|
-
const { clearProgress, progress
|
|
323
|
+
const { clearProgress, progress } = ctx.services.logger
|
|
307
324
|
progress(`Updating omp plugin ${plugin}...`)
|
|
308
325
|
const result = await spawnProcess("omp", args, { stdio: ["ignore", "pipe", "pipe"] })
|
|
309
326
|
clearProgress()
|
|
310
327
|
if (result.error === undefined && result.exitCode === 0) return true
|
|
311
328
|
|
|
312
|
-
|
|
329
|
+
recordFailure(
|
|
330
|
+
ctx,
|
|
313
331
|
`omp plugin operation failed for ${plugin}: ${firstOutputLine(result)}; run manually: omp ${args.join(" ")}`
|
|
314
332
|
)
|
|
315
333
|
return false
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* YAML merge for the omp config.yml and models.yml files.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* YAML merge for the omp config.yml and models.yml files. omp serialises both
|
|
3
|
+
* files itself, so the kit uses the yaml package rather than a line-based
|
|
4
|
+
* merge. Deployed-only keys are retained, except where the file-specific
|
|
5
|
+
* wrapper drops them: config.yml drops slash-bearing keys directly under
|
|
6
|
+
* retry.fallbackChains, because omp treats those as model or provider
|
|
7
|
+
* wildcards ahead of role chains, so a stale wildcard would silently override
|
|
8
|
+
* the kit-managed role chains. models.yml drops nothing; a user file may carry
|
|
9
|
+
* provider credentials and custom models the kit never declares.
|
|
7
10
|
*/
|
|
8
11
|
import { isMap, isScalar, parseDocument, type YAMLMap } from "yaml"
|
|
9
12
|
|
|
13
|
+
/** Decides whether a deployed-only key at `path` is dropped instead of kept. */
|
|
14
|
+
type DropDeployedKey = (path: ReadonlyArray<string>, key: string) => boolean
|
|
15
|
+
|
|
10
16
|
const fallbackChainsPath = ["retry", "fallbackChains"] as const
|
|
11
17
|
|
|
12
18
|
function stringKey(key: unknown): string | undefined {
|
|
@@ -15,29 +21,31 @@ function stringKey(key: unknown): string | undefined {
|
|
|
15
21
|
return undefined
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
function
|
|
19
|
-
return (
|
|
20
|
-
path.length === fallbackChainsPath.length &&
|
|
21
|
-
path[0] === fallbackChainsPath[0] &&
|
|
22
|
-
path[1] === fallbackChainsPath[1]
|
|
23
|
-
)
|
|
24
|
+
function keyName(key: unknown): string {
|
|
25
|
+
return isScalar(key) ? String(key.value) : String(key)
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const dropFallbackWildcard: DropDeployedKey = (path, key) =>
|
|
29
|
+
path.length === fallbackChainsPath.length &&
|
|
30
|
+
path[0] === fallbackChainsPath[0] &&
|
|
31
|
+
path[1] === fallbackChainsPath[1] &&
|
|
32
|
+
key.includes("/")
|
|
33
|
+
|
|
34
|
+
const keepEveryKey: DropDeployedKey = () => false
|
|
35
|
+
|
|
36
|
+
function pruneMapping(mapping: YAMLMap, path: ReadonlyArray<string>, drop: DropDeployedKey): void {
|
|
28
37
|
for (let index = mapping.items.length - 1; index >= 0; index--) {
|
|
29
38
|
const pair = mapping.items[index]
|
|
30
39
|
if (pair === undefined) continue
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
if (underFallbackChains && keyName.includes("/")) {
|
|
41
|
+
if (drop(path, keyName(pair.key))) {
|
|
34
42
|
mapping.items.splice(index, 1)
|
|
35
43
|
continue
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const key = stringKey(pair.key)
|
|
39
47
|
if (key !== undefined && isMap(pair.value)) {
|
|
40
|
-
|
|
48
|
+
pruneMapping(pair.value, [...path, key], drop)
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
}
|
|
@@ -45,7 +53,8 @@ function pruneFallbackWildcards(mapping: YAMLMap, path: ReadonlyArray<string>):
|
|
|
45
53
|
function mergeMappings(
|
|
46
54
|
sotMapping: YAMLMap,
|
|
47
55
|
deployedMapping: YAMLMap,
|
|
48
|
-
path: ReadonlyArray<string
|
|
56
|
+
path: ReadonlyArray<string>,
|
|
57
|
+
drop: DropDeployedKey
|
|
49
58
|
): void {
|
|
50
59
|
for (const sotPair of sotMapping.items) {
|
|
51
60
|
const key = stringKey(sotPair.key)
|
|
@@ -53,7 +62,7 @@ function mergeMappings(
|
|
|
53
62
|
|
|
54
63
|
const deployedPair = deployedMapping.items.find((candidate) => stringKey(candidate.key) === key)
|
|
55
64
|
if (deployedPair !== undefined && isMap(sotPair.value) && isMap(deployedPair.value)) {
|
|
56
|
-
mergeMappings(sotPair.value, deployedPair.value, [...path, key])
|
|
65
|
+
mergeMappings(sotPair.value, deployedPair.value, [...path, key], drop)
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
|
|
@@ -63,22 +72,24 @@ function mergeMappings(
|
|
|
63
72
|
key !== undefined && sotMapping.items.some((candidate) => stringKey(candidate.key) === key)
|
|
64
73
|
if (isManaged) continue
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
? String(deployedPair.key.value)
|
|
68
|
-
: String(deployedPair.key)
|
|
69
|
-
if (isFallbackChains(path) && keyName.includes("/")) continue
|
|
75
|
+
if (drop(path, keyName(deployedPair.key))) continue
|
|
70
76
|
if (key !== undefined && isMap(deployedPair.value)) {
|
|
71
|
-
|
|
77
|
+
pruneMapping(deployedPair.value, [...path, key], drop)
|
|
72
78
|
}
|
|
73
79
|
sotMapping.items.push(deployedPair)
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
function mergeYamlDocuments(
|
|
84
|
+
sotText: string,
|
|
85
|
+
deployedText: string,
|
|
86
|
+
name: string,
|
|
87
|
+
drop: DropDeployedKey
|
|
88
|
+
): string {
|
|
78
89
|
const deployedDoc = parseDocument(deployedText)
|
|
79
90
|
const deployedError = deployedDoc.errors[0]
|
|
80
91
|
if (deployedError !== undefined) {
|
|
81
|
-
throw new Error(`Invalid deployed omp YAML: ${deployedError.message}`)
|
|
92
|
+
throw new Error(`Invalid deployed omp ${name} YAML: ${deployedError.message}`)
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
const deployedContents = deployedDoc.contents
|
|
@@ -90,18 +101,28 @@ export function mergeOmpConfig(sotText: string, deployedText: string): string {
|
|
|
90
101
|
return sotText
|
|
91
102
|
}
|
|
92
103
|
if (!isMap(deployedContents)) {
|
|
93
|
-
throw new Error(
|
|
104
|
+
throw new Error(`Deployed omp ${name} YAML root must be a mapping`)
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
const sotDoc = parseDocument(sotText)
|
|
97
108
|
const sotError = sotDoc.errors[0]
|
|
98
109
|
if (sotError !== undefined) {
|
|
99
|
-
throw new Error(`Invalid SoT omp YAML: ${sotError.message}`)
|
|
110
|
+
throw new Error(`Invalid SoT omp ${name} YAML: ${sotError.message}`)
|
|
100
111
|
}
|
|
101
112
|
if (!isMap(sotDoc.contents)) {
|
|
102
|
-
throw new Error(
|
|
113
|
+
throw new Error(`SoT omp ${name} YAML root must be a mapping`)
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
mergeMappings(sotDoc.contents, deployedContents, [])
|
|
116
|
+
mergeMappings(sotDoc.contents, deployedContents, [], drop)
|
|
106
117
|
return sotDoc.toString()
|
|
107
118
|
}
|
|
119
|
+
|
|
120
|
+
/** config.yml merge: keeps user-only keys, drops stale fallback-chain wildcards. */
|
|
121
|
+
export function mergeOmpConfig(sotText: string, deployedText: string): string {
|
|
122
|
+
return mergeYamlDocuments(sotText, deployedText, "config.yml", dropFallbackWildcard)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** models.yml merge: keeps every user-only key, including credentials and custom models. */
|
|
126
|
+
export function mergeOmpModels(sotText: string, deployedText: string): string {
|
|
127
|
+
return mergeYamlDocuments(sotText, deployedText, "models.yml", keepEveryKey)
|
|
128
|
+
}
|
|
@@ -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.
|
|
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",
|
|
@@ -21,7 +21,7 @@ export const GENERATED_PAYLOAD_TEXT = {
|
|
|
21
21
|
"SoT/.omp/config.yml": "symbolPreset: unicode\n\ntheme:\n dark: titanium\n\nstatusLine:\n preset: default\n compactThinkingLevel: false\n showHookStatus: true\n sessionAccent: true\n transparent: false\n\nterminal:\n showProgress: false\n\ntui:\n textSizing: false\n tight: false\n\ndefaultThinkingLevel: high\n\ncycleOrder:\n - smol\n - default\n - slow\n - fable\n\ntier:\n openai: none\n anthropic: none\n\nadvisor:\n enabled: true\n syncBacklog: \"1\"\n\ngithub:\n enabled: true\n\ntask:\n eager: always\n showResolvedModelBadge: true\n softRequestBudget: 200\n softRequestBudgetNotice: true\n batch: true\n enableLsp: true\n enableEffort: true\n maxEffort: high\n maxConcurrency: 16\n maxRecursionDepth: 2\n maxRuntimeMs: 3600000\n agentModelOverrides:\n reviewer: \"@task\"\n security-reviewer: \"@task\"\n code-reviewer: \"@task\"\n plan-reviewer: \"@task\"\n\nmodelRoles:\n smol: openai-codex/gpt-5.6-luna:medium\n advisor: openai-codex/gpt-5.6-sol:medium\n designer: anthropic/claude-opus-5:high\n plan: anthropic/claude-opus-5:xhigh\n commit: openai-codex/gpt-5.6-luna:medium\n task: openai-codex/gpt-6-astra:low\n vision: anthropic/claude-opus-5:medium\n tiny: openai-codex/gpt-5.6-luna:low\n default: anthropic/claude-opus-5:high\n slow: anthropic/claude-opus-5:xhigh\n fable: anthropic/claude-fable-5-1:medium\n switch_fable: anthropic/claude-fable-5-1:medium\n\nmodelTags:\n fable:\n name: Fable 5.1\n switch_fable:\n name: Fable switch default\n hidden: true\n\ndisplay:\n shimmer: classic\n showTokenUsage: true\n\nproviders:\n anthropic:\n serverSideFallback: false\n fetch: auto\n webSearchTimeoutSeconds: 30\n webSearchOrder:\n - firecrawl\n - exa\n - perplexity\n - gemini\n - codex\n\nretry:\n usageAwareFallback: true\n fallbackChains:\n default:\n - openai-codex/gpt-5.6-sol:high\n advisor:\n - anthropic/claude-opus-5:medium\n task:\n - anthropic/claude-opus-5:high\n vision:\n - openai-codex/gpt-5.6-sol:medium\n smol:\n - anthropic/claude-opus-5:low\n tiny:\n - anthropic/claude-opus-5:low\n commit:\n - anthropic/claude-opus-5:medium\n switch_fable: []\n fable: []\n\nomitThinking: false\nincludeWorkspaceTree: false\nautocompleteMaxVisible: 10\nemojiAutocomplete: true\n\nbranchSummary:\n enabled: true\n\nreadLineNumbers: false\n\ncommands:\n enableOpencodeProject: false\n enableOpencodeUser: false\n\nskills:\n enableClaudeUser: false\n enableCodexUser: false\n enableAgentsUser: true\n\nsteeringMode: all\ninterruptMode: immediate\n\ndev:\n autoqa: false\n autoqaConsent: denied\n\ncompaction:\n thresholdTokens: 231200\n idleEnabled: true\n handoffSaveToDisk: true\n\ndoubleEscapeAction: tree\nhideThinkingBlock: false\nautoResume: false\ntextVerbosity: low\n\nfeatures:\n unexpectedStopDetection: smart\n\ncodexResets:\n autoRedeem: \"no\"\n\nstartup:\n quiet: true\n changelogMode: summary\n\ntools:\n approvalMode: yolo\n",
|
|
22
22
|
"SoT/.omp/intercom.json": "{\n \"brokerCommand\": \"bun\",\n \"brokerArgs\": []\n}\n",
|
|
23
23
|
"SoT/.omp/mcp.json": "{\n \"$schema\": \"https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json\",\n \"disabledServers\": [\n \"chrome-devtools\",\n \"context7:context7\",\n \"openaiDeveloperDocs\"\n ]\n}\n",
|
|
24
|
-
"SoT/.omp/models.yml": "providers:\n openai-codex:\n modelOverrides:\n gpt-6-astra:\n thinking:\n mode: effort\n efforts
|
|
24
|
+
"SoT/.omp/models.yml": "providers:\n openai-codex:\n modelOverrides:\n gpt-6-astra:\n thinking:\n mode: effort\n efforts:\n - low\n defaultLevel: low\n"
|
|
25
25
|
} as const
|
|
26
26
|
|
|
27
27
|
export const GENERATED_PAYLOAD_BASE64 = {
|
|
@@ -50,4 +50,4 @@ export const GENERATED_PAYLOAD_PATHS = [
|
|
|
50
50
|
"notification.mp3"
|
|
51
51
|
] as const
|
|
52
52
|
|
|
53
|
-
export const GENERATED_PAYLOAD_HASH = "
|
|
53
|
+
export const GENERATED_PAYLOAD_HASH = "5f9f8b3d00c15f6611af2bd8d92bbfcd12481442ff6e0e249846ac2e320b4e37"
|
package/package.json
CHANGED