docks-kit 0.17.2 → 0.17.3

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
@@ -28,7 +28,8 @@ launcher can fall back to Bun source.
28
28
 
29
29
  | Path | Purpose |
30
30
  |------|---------|
31
- | `docks-kit` / `docks-kit.ps1` | POSIX and Windows CLI launchers. On supported hosts, each runs the matching binary in `cli/dist/` only when its `--version` matches `package.json`. Otherwise it runs Bun-from-source and auto-installs Bun plus `node_modules`. Hosts outside the support matrix fail before source fallback. The standalone platform release binary provides no-Bun recovery. |
31
+ | `docks-kit` / `docks-kit.ps1` | POSIX and Windows CLI launchers. On supported hosts, each runs the matching binary in `cli/dist/` only when its `--version` matches `package.json`. Otherwise it runs Bun-from-source and auto-installs Bun plus `node_modules`, and the mismatch warning names the rebuild remedy. A launcher never deletes a binary: on a host without Bun it is the only remaining recovery path. Hosts outside the support matrix fail before source fallback. The standalone platform release binary provides no-Bun recovery. |
32
+ | `cli/build-binaries.sh` | Compiles the six release binaries. It checksums the six known artifact names rather than globbing `docks-kit-*`, so a packed `docks-kit-<version>.tgz` is never attested as a release binary. It stamps `cli/dist/VERSION` only when the directory holds one version, and on the next build discards every artifact that stamp attributes to a different version, because a launcher refuses to run one and keeping it makes `SHA256SUMS` span two versions. An artifact with no stamp has unknown provenance and may be a hand-built or downloaded recovery binary, so the script keeps it and warns; `--prune` discards those too. Nothing here removes an artifact the stamp vouches for. |
32
33
  | `cli/src/engine-native/` | EngineNative implementation for `sync`, `model`, and `toolchain`; idempotent, flag-gated for destructive reconciliation |
33
34
  | `cli/src/engine-native/ompSync.ts` | omp file deployment, marketplace registration, and plugin synchronization |
34
35
  | `cli/src/commands/omp.ts` | `docks-kit omp` session launcher: renders the free-model run overlay and forwards args to omp |
@@ -70,11 +71,14 @@ omp SoT notes:
70
71
  - `SoT/.omp/AGENTS.md`, `config.yml`, `models.yml`, and `mcp.json` deploy to `~/.omp/agent/`.
71
72
  - `SoT/.omp/intercom.json` deploys to `$PI_CODING_AGENT_DIR/intercom/config.json`. The default root is `~/.pi/agent`.
72
73
  - `ompSync.ts syncMergedYaml` deep-merges `config.yml` through `ompYaml.ts mergeOmpConfig` and `models.yml` through `mergeOmpModels`. Both wrap one generic mapping merge; only the config wrapper prunes stale `retry.fallbackChains` wildcards.
74
+ - `ompSync.ts` runs `ompRemovals.ts syncOmpRemovals, retired-key inventory` right after the config merge, and that pass force-prunes retired kit-owned keys from `~/.omp/agent/config.yml` on every sync, without `--reconcile`. The pass is required because `mergeOmpConfig` is additive, so removing a key from the SoT alone never removes it from a deployed file. A key retired with a recorded value is pruned only while the deployed value still matches that value, so a user edit survives; a key retired outright, such as `providers.webSearchOrder`, is pruned at any value.
73
75
  - `cycleOrder` ends with `astra` as its fifth stop. `modelRoles.astra` is `openai-codex/gpt-6-astra:xhigh`, and `modelTags.astra` is visible. Astra and Fable fall back to each other through concrete selectors. `modelRoles.fable` remains `anthropic/claude-fable-5-1:medium`, with visible `modelTags.fable`. The hidden `switch_fable` role uses the same Fable selector and keeps an empty fallback chain.
74
76
  - `modelRoles.task` is `openai-codex/gpt-5.6-sol:high`. The four reviewer entries in `task.agentModelOverrides` inherit it through `@task`. Only bundled `reviewer` and `security-reviewer` are discoverable OMP agents; `code-reviewer` and `plan-reviewer` stay dormant.
77
+ - `modelRoles.web` is `web/firecrawl` and `retry.fallbackChains.web` carries the explicit 27-entry provider order. The kit declares both keys because the legacy `providers.webSearchOrder` key is retired: omp expands it in memory into these two keys and then drops it, and never writes that expansion back to disk. An explicit chain replaces omp's built-in web order wholesale, so the list must stay complete.
75
78
  - `SoT/.omp/models.yml` declares Astra's full `low, medium, high, xhigh, max` ladder with `defaultLevel: xhigh` as the worked provider ladder-override example. `ompYaml.ts mergeOmpModels` preserves deployed-only keys in `~/.omp/agent/models.yml`, because a user file may carry provider credentials. Whole-file replacement is wrong.
76
79
  - `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
77
80
  - `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.
81
+ - `SoT/.omp/config.yml` sets `compaction.thresholdTokens` to `-1`, omp's schema default sentinel selecting reserve-based behavior: the trigger becomes `contextWindow` minus `max(floor(contextWindow * 0.15), 16384)`, which is 231,200 on the 272,000-token Codex window and 850,000 on the 1,000,000-token Anthropic window. The key ships as `-1` rather than being deleted because `ompYaml.ts mergeOmpConfig` is additive, so removing a key from SoT never removes it from a deployed file. `cli/docs/omp-context.md` (topic `omp-context`) carries the derivation and the measured evidence, so update that topic in the same commit as any compaction-setting change.
78
82
  - Sync registers the `docks` marketplace. It installs or upgrades `docks@docks` and `plan-lifecycle@docks` at user scope.
79
83
  - Sync installs `pi-intercom` at the verified version from `SoT/toolchain.json`.
80
84
  - The omp CLI is upstream-owned and self-updating through `omp update`. Sync never installs or upgrades the CLI.
@@ -115,7 +119,7 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`, `SoT/.omp/`), see the m
115
119
  - **One exemption: the kit's own package.** `install.sh` and `install.ps1` end with `bun add -g docks-kit@latest`, because a global installer that pinned itself would install a fixed old kit forever, and pinning it to `package.json` would request an unpublished version between the release-prep commit and the npm publish. The exemption covers `docks-kit` alone. Both installers still pin the Bun installer they download to the manifest's verified version, and `cli/test/unit/install.test.ts` asserts that pin in all four launcher and installer scripts.
116
120
  ## Testing
117
121
 
118
- Run `bun run check` before and after each change: oxlint, `tsc --noEmit`, unit tests, and both golden suites. Scripts: `lint`, `typecheck`, `test:unit`, `test:integration` (goldens), `test`, `format`; CI runs `check` on push and pull request with Bun 1.4.2. Prove-red modes must exit non-zero after detecting planted mismatches. Also verify user-facing changes via `./docks-kit sync --dry-run`, per-tool sanity (`/doctor`, `/plugin`, etc.), `bun run test:runtime:posix`, and `diff <(jq -S . <SoT>) <(jq -S . <deployed>)` recipes from the per-tool file.
122
+ Run `bun run check` before and after each change: oxlint, `oxfmt --check`, `tsc --noEmit`, unit tests, and both golden suites. Scripts: `lint`, `format:check`, `typecheck`, `test:unit`, `test:integration` (goldens), `test`, `format`; CI runs `check` on push and pull request with Bun 1.4.2. Prove-red modes must exit non-zero after detecting planted mismatches. The golden writer emits two-space JSON that `oxfmt` re-serializes, so run `bun run format` after `--update-goldens` or `format:check` fails. Also verify user-facing changes via `./docks-kit sync --dry-run`, per-tool sanity (`/doctor`, `/plugin`, etc.), `bun run test:runtime:posix`, and `diff <(jq -S . <SoT>) <(jq -S . <deployed>)` recipes from the per-tool file.
119
123
 
120
124
  Use direct acceptance and focused regressions while iterating, then run the full unit/golden gate once at the pre-commit or release boundary. Reuse still-matching evidence; a later relevant edit invalidates only the affected rung and final gate, not every prior check.
121
125
 
@@ -0,0 +1,104 @@
1
+ # omp context and compaction
2
+
3
+ This topic records why `SoT/.omp/config.yml` does not pin a compaction
4
+ threshold, and what omp's reserve-based default computes on each model lane.
5
+
6
+ ## How omp resolves the trigger
7
+
8
+ `resolveThresholdTokens` in omp source
9
+ `packages/agent/src/compaction/compaction.ts` selects the trigger from three
10
+ branches, in priority order.
11
+
12
+ 1. A positive `compaction.thresholdTokens` wins. omp clamps it to the range 1
13
+ through `contextWindow - 1`.
14
+ 2. Otherwise a positive `compaction.thresholdPercent` applies. omp clamps the
15
+ percent to the range 1 through 99 and computes
16
+ `floor(contextWindow * percent / 100)`.
17
+ 3. Otherwise, when both keys hold `-1`, reserve-based behavior applies.
18
+
19
+ The reserve-based branch computes the trigger from the window itself:
20
+
21
+ ```text
22
+ threshold = contextWindow - max(floor(contextWindow * 0.15), 16384)
23
+ ```
24
+
25
+ omp names the 16384 constant `DEFAULT_RESERVE_TOKENS`. The 15% proportional
26
+ reserve dominates on any window above roughly 109,227 tokens, so every lane
27
+ the kit uses resolves through the proportional term and never through the flat
28
+ constant.
29
+
30
+ ## Per-lane result
31
+
32
+ | Lane | Window | Reserve | Threshold | Percent of window |
33
+ |---|---:|---:|---:|---:|
34
+ | `openai-codex/*` | 272,000 | 40,800 | 231,200 | 85.0% |
35
+ | `anthropic/*` | 1,000,000 | 150,000 | 850,000 | 85.0% |
36
+
37
+ Both lanes compact at the same fraction of their own window. The absolute
38
+ trigger differs only because the windows differ.
39
+
40
+ ## Why the kit no longer pins 231200
41
+
42
+ The kit previously shipped `compaction.thresholdTokens: 231200`. That value is
43
+ exactly the reserve-based default for a 272,000-token window. It was therefore
44
+ identical to the default on the Codex lane and changed nothing there. Its only
45
+ real effect was forcing the Anthropic lane to compact at 23.1% of its window
46
+ instead of 85%.
47
+
48
+ The evidence below records 83 compactions measured between 2026-09-12 and
49
+ 2026-09-21, after omp 18.1.18 introduced the Anthropic server-side compaction
50
+ lane.
51
+
52
+ | Lane | Compactions | Maximum tokens before | Over window | Warnings |
53
+ |---|---:|---:|---:|---:|
54
+ | `anthropic` | 75 | 248,583 (24.9% of window) | 0 | 0 |
55
+ | `openai-codex` | 8 | 263,591 (96.9% of window) | 0 | 0 |
56
+
57
+ The median context freed was 64.8%. The pin was safe rather than dangerous.
58
+ No session ran past its window, and omp raised no warning on either lane.
59
+ Under the reserve-based default none of the 75 Anthropic compactions would
60
+ have fired, because no session reached 850,000.
61
+
62
+ ## Why the key ships as -1 rather than being deleted
63
+
64
+ `ompYaml.ts` `mergeOmpConfig` is additive. Its `mergeMappings, deployed-key
65
+ retention loop` returns every deployed key that the SoT omits to the merged
66
+ output, and the only drop predicate is `dropFallbackWildcard`, which is scoped
67
+ to `retry.fallbackChains`.
68
+
69
+ Deleting the key from the SoT would therefore leave the stale `231200` in
70
+ every already-deployed `~/.omp/agent/config.yml` permanently. A present key
71
+ holding the schema default sentinel `-1` overwrites that stale value on the
72
+ next sync.
73
+
74
+ Verify the deployed value after a sync:
75
+
76
+ ```bash
77
+ omp config get compaction.thresholdTokens
78
+ ```
79
+
80
+ The command must print `-1`.
81
+
82
+ ## The clamp hazard a fixed pin carries
83
+
84
+ The fixed-token branch clamps the pinned value to `contextWindow - 1`. On any
85
+ model whose window is below the pinned value, the reserve collapses to one
86
+ token, so omp compacts only once the context is already full. The
87
+ reserve-based default always leaves at least 15% of the window free.
88
+
89
+ No current role model has a window below 272,000, so the hazard was latent
90
+ rather than live. Not pinning removes it entirely, including for any future
91
+ model with a smaller window.
92
+
93
+ ## Related settings left at their omp defaults
94
+
95
+ | Setting | Value | Reason |
96
+ |---|---|---|
97
+ | `extendedContext` | `false` | omp 17.4.0 added the setting defaulting on, and a later release flipped the default to off. It caps a model carrying a premium long-context price tier at that model's standard-pricing window, so `openai-codex/gpt-5.6-sol` reports 272,000 rather than about 1,050,000. Enabling it makes a Codex window overrun structurally impossible. The cost is a cliff rather than a marginal rate: any request above the 272,000 threshold bills the whole request at input $10 instead of $4, cache read $1.00 instead of $0.40, and cache write $12.50 instead of $5.00. |
98
+ | `compaction.idleEnabled` | `true` | omp default. Idle compaction fires below the reserve-based trigger on both lanes. A compaction whose reason is `idle` skips the progress guard. |
99
+ | `compaction.idleThresholdTokens` | `200000` | omp default. |
100
+ | `compaction.idleTimeoutSeconds` | `300` | omp default. |
101
+ | `compaction.methodOrder` | `["remote","snapcompact","handoff","shake","soft"]` | omp default, remote first. Since omp 18.1.18 the Anthropic server-side lane handles Anthropic models, so snapcompact is a fallback rather than the primary path. |
102
+
103
+ Every row records a decision to leave the omp default in place. None of these
104
+ keys carries a kit value that differs from the default omp already applies.
@@ -20,12 +20,13 @@ against.
20
20
  | `fable` | `anthropic/claude-fable-5-1` | medium | 49 | $2.98 | 9.65 s | n/a |
21
21
  | `switch_fable` | `anthropic/claude-fable-5-1` | medium | 49 | $2.98 | 9.65 s | n/a |
22
22
  | `astra` | `openai-codex/gpt-6-astra` | xhigh | 53 | $2.31 | 161.65 s | n/a |
23
+ | `web` | `web/firecrawl` | n/a | n/a | n/a | n/a | n/a |
23
24
 
24
25
  The table reports the measured Artificial Analysis figures for each assigned
25
26
  model and level. It states no motive that the config or omp's own
26
27
  documentation does not establish. AA lists no cost per task for Luna medium
27
28
  and low, and no Coding Agent Index for any Astra or Fable 5.1 level except
28
- max.
29
+ max. AA measures no web search provider, so the `web` row carries no figures.
29
30
 
30
31
  What omp's settings catalog establishes about these roles:
31
32
 
@@ -53,6 +54,20 @@ chains, `retry.fallbackChains.default` would send either stop to
53
54
  Chain entries are concrete selectors, not role aliases, so this pair cannot
54
55
  recurse. The hidden `switch_fable` chain stays empty.
55
56
 
57
+ `modelRoles.web` is `web/firecrawl`, and `retry.fallbackChains.web` lists the
58
+ explicit 27-entry provider order that follows it. The two keys replace the
59
+ retired `providers.webSearchOrder` key, which omp no longer carries in its
60
+ settings schema. omp still accepts that key in a deployed file, expands it in
61
+ memory into the same two keys, and then drops it, but it never writes the
62
+ expansion back to disk. The kit therefore declares both keys itself.
63
+
64
+ The chain is the verbatim expansion omp produces today, read back with
65
+ `omp config get retry.fallbackChains`. It must stay complete. An explicit
66
+ chain replaces omp's built-in web order wholesale, so a shortened list drops
67
+ providers instead of reordering them. The first five entries keep the previous
68
+ Firecrawl, Exa, Perplexity, Gemini, Codex preference; the remaining entries
69
+ are omp's own ordering of the providers behind it.
70
+
56
71
  ## Artificial Analysis snapshot
57
72
 
58
73
  Source: `https://artificialanalysis.ai`, read on 2026-09-08. Every score below
@@ -205,8 +220,8 @@ quick answer.
205
220
  to choose a thinking level.
206
221
  - `task.enableEffort` is `true`, so a caller can pass `effort: lo`, `med`, or
207
222
  `hi`, which overrides `auto`.
208
- - `task.maxEffort` is `high`, so `scout` and `sonic` run Luna `medium` by
209
- default and Luna `high` with `effort: hi`.
223
+ - `task.maxEffort` is `max`, so `scout` and `sonic` run Luna `medium` by
224
+ default and Luna `max` with `effort: hi`.
210
225
  - The bundled `reviewer` and `security-reviewer` inherit `@task`, now Sol high.
211
226
  - The `code-reviewer` and `plan-reviewer` override entries remain dormant.
212
227
  Both point to `@task`, now Sol high. omp's task tool rejects both names as
@@ -11,6 +11,7 @@ import syncLayers from "../../docs/sync-layers.md" with { type: "text" };
11
11
  import install from "../../docs/install.md" with { type: "text" };
12
12
  import platforms from "../../docs/platforms.md" with { type: "text" };
13
13
  import ompModels from "../../docs/omp-models.md" with { type: "text" };
14
+ import ompContext from "../../docs/omp-context.md" with { type: "text" };
14
15
 
15
16
  const TOPICS: Record<string, { summary: string; body: string }> = {
16
17
  overview: { summary: "What docks-kit is and how the pieces fit", body: overview },
@@ -38,6 +39,10 @@ const TOPICS: Record<string, { summary: string; body: string }> = {
38
39
  summary: "omp role map and the Artificial Analysis snapshot behind it",
39
40
  body: ompModels,
40
41
  },
42
+ "omp-context": {
43
+ summary: "omp compaction trigger, the reserve-based default, and context-window lanes",
44
+ body: ompContext,
45
+ },
41
46
  };
42
47
 
43
48
  const topic = Argument.String("topic").pipe(
@@ -0,0 +1,159 @@
1
+ /**
2
+ * EngineNative `sync omp` retired-key pruning. `ompYaml.ts mergeOmpConfig` is
3
+ * additive: its `mergeMappings, deployed-key retention loop` returns every
4
+ * deployed key absent from the SoT to the merged result. Removing a key from
5
+ * `SoT/.omp/config.yml` therefore never removes it from an existing
6
+ * `~/.omp/agent/config.yml`. This pass force-prunes an inventory of retired
7
+ * kit-owned keys on every sync, without `--reconcile`. Message strings and
8
+ * prune semantics are part of the contract.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { isMap, isScalar, parseDocument, type YAMLMap } from "yaml";
12
+ import type { Ctx } from "./index";
13
+
14
+ type RetiredScalar = string | number | boolean;
15
+
16
+ /**
17
+ * Keys the kit used to deploy, each paired with the exact value it deployed. A
18
+ * key is pruned only while the deployed value still equals that value, so a
19
+ * user who changed the setting keeps it. Every recorded value also equals omp's
20
+ * current default, so pruning changes no behavior today and lets a later
21
+ * upstream default change take effect.
22
+ */
23
+ const OMP_RETIRED_VALUES: ReadonlyArray<readonly [string, RetiredScalar]> = [
24
+ ["symbolPreset", "unicode"],
25
+ ["theme.dark", "titanium"],
26
+ ["statusLine.preset", "default"],
27
+ ["statusLine.showHookStatus", true],
28
+ ["statusLine.sessionAccent", true],
29
+ ["statusLine.transparent", false],
30
+ ["terminal.showProgress", false],
31
+ ["tui.textSizing", false],
32
+ ["tui.tight", false],
33
+ ["display.shimmer", "classic"],
34
+ ["omitThinking", false],
35
+ ["hideThinkingBlock", false],
36
+ ["autocompleteMaxVisible", 10],
37
+ ["emojiAutocomplete", true],
38
+ ["readLineNumbers", false],
39
+ ["interruptMode", "immediate"],
40
+ ["autoResume", false],
41
+ ["startup.changelogMode", "summary"],
42
+ ];
43
+
44
+ /**
45
+ * Keys retired outright and pruned at any value, because the key itself no
46
+ * longer means anything. omp migrates `providers.webSearchOrder` in memory into
47
+ * `modelRoles.web` plus `retry.fallbackChains.web` and then drops it, but never
48
+ * writes that expansion back to the file.
49
+ */
50
+ const OMP_RETIRED_KEYS: ReadonlyArray<string> = ["providers.webSearchOrder"];
51
+
52
+ function indexOfKey(mapping: YAMLMap, key: string): number {
53
+ return mapping.items.findIndex((pair) => {
54
+ const name: unknown = pair.key;
55
+ if (typeof name === "string") return name === key;
56
+ return isScalar(name) && name.value === key;
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Deletes `segments` when `matches` accepts the leaf value, then removes every
62
+ * ancestor mapping the deletion left empty. An ancestor holding another key
63
+ * survives, so `statusLine` keeps `compactThinkingLevel` while `tui` disappears
64
+ * once both of its keys are gone.
65
+ */
66
+ function deletePath(
67
+ root: YAMLMap,
68
+ segments: ReadonlyArray<string>,
69
+ matches: (value: unknown) => boolean,
70
+ ): boolean {
71
+ const owners: Array<YAMLMap> = [root];
72
+ let cursor: YAMLMap = root;
73
+ for (const segment of segments.slice(0, -1)) {
74
+ const index = indexOfKey(cursor, segment);
75
+ const pair = index < 0 ? undefined : cursor.items[index];
76
+ if (pair === undefined || !isMap(pair.value)) return false;
77
+ cursor = pair.value;
78
+ owners.push(cursor);
79
+ }
80
+
81
+ const leaf = segments[segments.length - 1];
82
+ if (leaf === undefined) return false;
83
+ const leafIndex = indexOfKey(cursor, leaf);
84
+ if (leafIndex < 0) return false;
85
+ const leafPair = cursor.items[leafIndex];
86
+ if (leafPair === undefined || !matches(leafPair.value)) return false;
87
+
88
+ // `commentBefore` can hold several blocks separated by a blank line. Only the
89
+ // final block sits against this key; anything above it is a file or section
90
+ // header that happens to precede the key, so it survives the removal instead
91
+ // of leaving with it.
92
+ const leafKey: unknown = leafPair.key;
93
+ const priorComment =
94
+ isScalar(leafKey) && typeof leafKey.commentBefore === "string"
95
+ ? leafKey.commentBefore.slice(0, Math.max(0, leafKey.commentBefore.lastIndexOf("\n\n")))
96
+ : "";
97
+ cursor.items.splice(leafIndex, 1);
98
+ if (priorComment !== "") {
99
+ const heir: unknown = cursor.items[leafIndex]?.key;
100
+ if (isScalar(heir)) {
101
+ heir.commentBefore =
102
+ typeof heir.commentBefore === "string"
103
+ ? `${priorComment}\n\n${heir.commentBefore}`
104
+ : priorComment;
105
+ } else {
106
+ // The key was last in its mapping, so there is no later key to carry the
107
+ // header. A trailing mapping comment keeps the text in the file.
108
+ cursor.comment =
109
+ typeof cursor.comment === "string" ? `${cursor.comment}\n${priorComment}` : priorComment;
110
+ }
111
+ }
112
+
113
+ for (let depth = owners.length - 1; depth >= 1; depth--) {
114
+ const mapping = owners[depth];
115
+ if (mapping === undefined || mapping.items.length > 0) break;
116
+ const owner = owners[depth - 1];
117
+ const segment = segments[depth - 1];
118
+ if (owner === undefined || segment === undefined) break;
119
+ const index = indexOfKey(owner, segment);
120
+ if (index >= 0) owner.items.splice(index, 1);
121
+ }
122
+ return true;
123
+ }
124
+
125
+ /** Prunes retired kit-owned keys from a deployed omp config.yml. */
126
+ export function syncOmpRemovals(ctx: Ctx, configFile: string): number {
127
+ const { change, echo, verbose, warn } = ctx.services.logger;
128
+ if (!existsSync(configFile)) return 0;
129
+
130
+ const doc = parseDocument(readFileSync(configFile, "utf8"));
131
+ const parseError = doc.errors[0];
132
+ if (parseError !== undefined) {
133
+ warn(`omp config.yml unreadable, retired keys not pruned: ${parseError.message}`);
134
+ return 0;
135
+ }
136
+ const contents = doc.contents;
137
+ if (!isMap(contents)) return 0;
138
+
139
+ const pruned: Array<string> = [];
140
+ for (const [path, value] of OMP_RETIRED_VALUES) {
141
+ const matches = (node: unknown): boolean => isScalar(node) && node.value === value;
142
+ if (deletePath(contents, path.split("."), matches)) pruned.push(path);
143
+ }
144
+ for (const path of OMP_RETIRED_KEYS) {
145
+ if (deletePath(contents, path.split("."), () => true)) pruned.push(path);
146
+ }
147
+
148
+ if (pruned.length === 0) {
149
+ verbose("omp config.yml carries no retired keys");
150
+ return 0;
151
+ }
152
+ if (ctx.dryRun) {
153
+ echo(`[dry-run] prune ${configFile}: ${pruned.join(", ")}`);
154
+ return pruned.length;
155
+ }
156
+ writeFileSync(configFile, String(doc));
157
+ change(`omp config.yml pruned ${pruned.length} retired key(s): ${pruned.join(", ")}`);
158
+ return pruned.length;
159
+ }
@@ -7,8 +7,9 @@
7
7
  * subcommand runs under ctx.dryRun.
8
8
  *
9
9
  * Split: file deploy lives in `ompFileDeploy.ts`, the marketplace pass in
10
- * `ompMarketplace.ts`, and the plugin pass in `ompPlugins.ts`. This module
11
- * keeps the orchestration plus the summary surface.
10
+ * `ompMarketplace.ts`, the plugin pass in `ompPlugins.ts`, and the retired-key
11
+ * prune in `ompRemovals.ts`. This module keeps the orchestration plus the
12
+ * summary surface.
12
13
  */
13
14
  import { isAbsolute, resolve } from "node:path";
14
15
 
@@ -20,6 +21,7 @@ import { mergeOmpConfig, mergeOmpModels } from "./ompYaml";
20
21
  import { ensureDirectory, syncMergedYaml, syncWholeFile } from "./ompFileDeploy";
21
22
  import { syncMarketplace } from "./ompMarketplace";
22
23
  import { syncPlugins } from "./ompPlugins";
24
+ import { syncOmpRemovals } from "./ompRemovals";
23
25
 
24
26
  export interface OmpState {
25
27
  readonly pluginsInstalled: number;
@@ -51,6 +53,8 @@ export async function ompSync(ctx: Ctx): Promise<OmpState> {
51
53
  "omp mcp.json synced",
52
54
  );
53
55
  syncMergedYaml(ctx, "SoT/.omp/config.yml", p(agentDir, "config.yml"), mergeOmpConfig);
56
+ // mergeOmpConfig is additive, so the prune must run on the merged result.
57
+ syncOmpRemovals(ctx, p(agentDir, "config.yml"));
54
58
  syncMergedYaml(ctx, "SoT/.omp/models.yml", p(agentDir, "models.yml"), mergeOmpModels);
55
59
 
56
60
  const intercomRootSetting = process.env["PI_CODING_AGENT_DIR"];
@@ -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.2"
4
+ export const GENERATED_PACKAGE_VERSION = "0.17.3"
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",
@@ -18,7 +18,7 @@ export const GENERATED_PAYLOAD_TEXT = {
18
18
  "SoT/.codex/plugins/marketplace.json": "{\n \"name\": \"docks\",\n \"interface\": {\n \"displayName\": \"DocksDocks\"\n },\n \"plugins\": [\n {\n \"name\": \"docks\",\n \"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/DocksDocks/docks.git\",\n \"path\": \"./plugins/docks\",\n \"ref\": \"main\"\n },\n \"policy\": {\n \"installation\": \"AVAILABLE\",\n \"authentication\": \"ON_INSTALL\"\n },\n \"category\": \"Productivity\"\n },\n {\n \"name\": \"plan-lifecycle\",\n \"source\": {\n \"source\": \"git-subdir\",\n \"url\": \"https://github.com/DocksDocks/docks.git\",\n \"path\": \"./plugins/plan-lifecycle\",\n \"ref\": \"main\"\n },\n \"policy\": {\n \"installation\": \"AVAILABLE\",\n \"authentication\": \"ON_INSTALL\"\n },\n \"category\": \"Productivity\"\n }\n ]\n}\n",
19
19
  "SoT/.codex/rules/docks.rules": "prefix_rule(pattern=[\"pwd\"], decision=\"allow\")\nprefix_rule(pattern=[\"ls\"], decision=\"allow\")\nprefix_rule(pattern=[\"cat\"], decision=\"allow\")\nprefix_rule(pattern=[\"head\"], decision=\"allow\")\nprefix_rule(pattern=[\"tail\"], decision=\"allow\")\nprefix_rule(pattern=[\"wc\"], decision=\"allow\")\nprefix_rule(pattern=[\"nl\"], decision=\"allow\")\nprefix_rule(pattern=[\"grep\"], decision=\"allow\")\nprefix_rule(pattern=[\"sort\"], decision=\"allow\")\nprefix_rule(pattern=[\"uniq\"], decision=\"allow\")\nprefix_rule(pattern=[\"diff\"], decision=\"allow\")\nprefix_rule(pattern=[\"which\"], decision=\"allow\")\nprefix_rule(pattern=[\"date\"], decision=\"allow\")\nprefix_rule(pattern=[\"basename\"], decision=\"allow\")\nprefix_rule(pattern=[\"dirname\"], decision=\"allow\")\nprefix_rule(pattern=[\"realpath\"], decision=\"allow\")\nprefix_rule(pattern=[\"readlink\"], decision=\"allow\")\nprefix_rule(pattern=[\"jq\"], decision=\"allow\")\nprefix_rule(pattern=[\"tree\"], decision=\"allow\")\nprefix_rule(pattern=[\"cut\"], decision=\"allow\")\nprefix_rule(pattern=[\"tr\"], decision=\"allow\")\nprefix_rule(pattern=[\"echo\"], decision=\"allow\")\nprefix_rule(pattern=[\"printf\"], decision=\"allow\")\nprefix_rule(pattern=[\"printenv\"], decision=\"allow\")\nprefix_rule(pattern=[\"uname\"], decision=\"allow\")\nprefix_rule(pattern=[\"file\"], decision=\"allow\")\nprefix_rule(pattern=[\"stat\"], decision=\"allow\")\nprefix_rule(pattern=[\"du\"], decision=\"allow\")\nprefix_rule(pattern=[\"id\"], decision=\"allow\")\nprefix_rule(pattern=[\"whoami\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"git\", \"status\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"diff\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"log\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"show\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"blame\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"rev-parse\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"ls-files\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"grep\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"ls-tree\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"branch\", \"--show-current\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"branch\", \"-vv\"], decision=\"allow\")\nprefix_rule(pattern=[\"git\", \"mv\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"gh\", \"pr\", \"view\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"list\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"diff\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"status\"], decision=\"allow\")\nprefix_rule(pattern=[\"gh\", \"pr\", \"checks\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"docker\", \"ps\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"git\", \"push\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"reset\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"clean\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"merge\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"rebase\"], decision=\"prompt\")\nprefix_rule(pattern=[\"git\", \"checkout\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"mv\"], decision=\"prompt\")\nprefix_rule(pattern=[\"chmod\"], decision=\"prompt\")\nprefix_rule(pattern=[\"chown\"], decision=\"prompt\")\nprefix_rule(pattern=[\"kill\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pkill\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"npm\", \"install\"], decision=\"prompt\")\nprefix_rule(pattern=[\"npm\", \"uninstall\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pnpm\", \"add\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pnpm\", \"remove\"], decision=\"prompt\")\nprefix_rule(pattern=[\"yarn\", \"add\"], decision=\"prompt\")\nprefix_rule(pattern=[\"yarn\", \"remove\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip\", \"install\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip\", \"uninstall\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip3\", \"install\"], decision=\"prompt\")\nprefix_rule(pattern=[\"pip3\", \"uninstall\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"docker\", \"run\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"stop\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"volume\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"system\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"up\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"down\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker\", \"compose\", \"stop\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"up\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"down\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"rm\"], decision=\"prompt\")\nprefix_rule(pattern=[\"docker-compose\", \"stop\"], decision=\"prompt\")\n\n# `tail -f`/`--follow` never returns and hangs the agent (bare `tail` stays allowed above).\nprefix_rule(pattern=[\"tail\", \"-f\"], decision=\"prompt\")\nprefix_rule(pattern=[\"tail\", \"--follow\"], decision=\"prompt\")\n# rg and `sed -n` are prompt, NOT allow: argv-prefix matching cannot gate their\n# code-exec forms (rg --pre=CMD or a reordered --pre; sed -n 'e CMD' / -ni) while a\n# shorter allow prefix would auto-approve the whole command.\nprefix_rule(pattern=[\"rg\"], decision=\"prompt\")\nprefix_rule(pattern=[\"sed\", \"-n\"], decision=\"prompt\")\nprefix_rule(pattern=[\"find\"], decision=\"prompt\")\nprefix_rule(pattern=[\"sed\", \"-i\"], decision=\"prompt\")\nprefix_rule(pattern=[\"sed\", \"--in-place\"], decision=\"prompt\")\nprefix_rule(pattern=[\"awk\"], decision=\"prompt\")\nprefix_rule(pattern=[\"xargs\"], decision=\"prompt\")\nprefix_rule(pattern=[\"tee\"], decision=\"prompt\")\nprefix_rule(pattern=[\"curl\"], decision=\"prompt\")\nprefix_rule(pattern=[\"env\"], decision=\"prompt\")\n\nprefix_rule(pattern=[\"sudo\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"eval\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"mkfs\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"dd\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"--force\", \"origin\", \"main\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"--force\", \"origin\", \"master\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"-f\", \"origin\", \"main\"], decision=\"forbidden\")\nprefix_rule(pattern=[\"git\", \"push\", \"-f\", \"origin\", \"master\"], decision=\"forbidden\")\n",
20
20
  "SoT/.omp/AGENTS.md": "# Global OMP guidance\n\n- Verify unfamiliar or version-sensitive APIs and configuration against current official documentation before implementation.\n- When the user asks for an assessment rather than a change, report findings without editing.\n- For Docks plan reviews, cross-company review is standing-authorized; host security policy still applies.\n- Please remove all mannered prose.\n\n## Asking me things\n\nA question typed in prose is just text I may or may not act on. The `ask` tool renders a\nblocking picker, waits indefinitely (`ask.timeout = 0`), and records my answer in the\ntranscript. If you actually need an answer, it MUST go through `ask`.\n\nMUST use `ask` before:\n- Anything irreversible or destructive: deleting/overwriting files or data you did not create,\n force-push, history rewrite, dropping tables, running migrations, mass rename, touching\n secrets/credentials, or publishing outward (release, upstream PR, issue, comment).\n- Two or more viable approaches whose tradeoffs are mine to own: schema/API/protocol shape,\n adding a dependency, or establishing a convention this repo does not already have.\n- A fact only I hold: intended semantics of an ambiguous requirement, which of several\n conflicting existing patterns is canonical, or which environment/account/target to use.\n- A request that contradicts the repo: surface the conflict and let me resolve it; never\n silently pick one side.\n\nIf `ask` is not registered — subagent, headless, or `-p` print runs, where `hasUI` is false —\nthe MUST above cannot be satisfied: do not fabricate the call and do not stall on it. Take the\nconservative reversible option and put the question, plus the assumption you made, in your\nfinal report so whoever spawned you can decide.\n\nNEVER use `ask` for:\n- Permission to begin, or to confirm scope already stated in the request.\n- Anything a tool, grep, or doc can answer — go read it.\n- A cheap reversible choice — take the conservative option and say which you took.\n- Something already answered earlier in the conversation.\n\nBatch every open question into one `ask` call with multiple questions; do not serialize\nround trips. Being overruled ends the discussion — execute my call without relitigating.\n\n## Output Standard\n\nApply Simplified Technical English to all agent text. This includes responses, messages,\ndocumentation, comments, and interface text.\n\nReply in the language I use, and apply every rule below to that language.\n\nA rule that names English grammar applies only to English. The contraction ban is one\nsuch rule. In another language, follow the normal grammar of that language. Portuguese,\nSpanish, French, Italian, and German merge a preposition with an article, and that merge\nis required, not optional.\n\nTreat the word limits as approximate outside English. Some languages need more words to\ncarry the same content.\n\n- Use the simplest precise technical term.\n- Use each term consistently.\n- Expand an abbreviation at its first occurrence.\n- Explain a technical term when I ask for an explanation.\n- Write complete and grammatically correct sentences.\n- Use active voice and identify the actor.\n- Use the imperative form for instructions.\n- Put only one action in each instruction sentence.\n- Put a necessary condition before its instruction.\n- Use simple verb tenses.\n- Do not use contractions, idioms, or slang. Avoid humor and rhetorical questions.\n- Keep procedural sentences to 20 words or fewer.\n- Keep descriptive sentences to 25 words or fewer.\n- Keep each paragraph to one topic and six sentences or fewer.\n- Do not use more than three nouns together.\n- Use vertical lists for complex information.\n- Put a warning or caution before a related hazardous instruction.\n\n### Naming\n\n- Say what the thing does before you name it. Put the technical term after the plain\n description, once, in parentheses.\n- Do not use a technical term as the only name for something you just introduced.\n- Prefer the short common word. Use \"use\", not \"utilize\". Use \"set up\", not \"provision\".\n- Do not explain by metaphor alone. A metaphor may follow a literal statement.\n",
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 - astra\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-5.6-sol:high\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 astra: openai-codex/gpt-6-astra:xhigh\n\nmodelTags:\n fable:\n name: Fable 5.1\n switch_fable:\n name: Fable switch default\n hidden: true\n astra:\n name: GPT-6 Astra\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 - openai-codex/gpt-6-astra:xhigh\n astra:\n - anthropic/claude-fable-5-1:medium\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",
21
+ "SoT/.omp/config.yml": "statusLine:\n compactThinkingLevel: false\n\ndefaultThinkingLevel: high\n\ncycleOrder:\n - smol\n - default\n - slow\n - fable\n - astra\n\ntier:\n openai: none\n anthropic: none\n\nadvisor:\n enabled: true\n syncBacklog: \"1\"\n\ngithub:\n enabled: true\n\nfind:\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: max\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-5.6-sol:high\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 astra: openai-codex/gpt-6-astra:xhigh\n web: web/firecrawl\n\nmodelTags:\n fable:\n name: Fable 5.1\n switch_fable:\n name: Fable switch default\n hidden: true\n astra:\n name: GPT-6 Astra\n\ndisplay:\n showTokenUsage: true\n\nproviders:\n anthropic:\n serverSideFallback: false\n fetch: auto\n webSearchTimeoutSeconds: 30\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 - openai-codex/gpt-6-astra:xhigh\n astra:\n - anthropic/claude-fable-5-1:medium\n web:\n - web/exa\n - web/perplexity\n - google/gemini-2.5-flash\n - openai-codex/gpt-5.6-luna\n - web/parallel\n - google-antigravity/gemini-2.5-flash\n - anthropic/claude-haiku-4-5\n - openai-codex/gpt-5.6\n - openai-codex/gpt-5.5\n - xai/grok-4.5\n - xai-oauth/grok-4.5\n - web/zai\n - web/tinyfish\n - web/jina\n - web/kagi\n - web/tavily\n - web/brave\n - web/kimi\n - web/synthetic\n - web/ollama\n - web/searxng\n - web/startpage\n - web/duckduckgo\n - web/ecosia\n - web/google\n - web/mojeek\n - web/public\n\nincludeWorkspaceTree: false\n\nbranchSummary:\n enabled: true\n\ncommands:\n enableOpencodeProject: false\n enableOpencodeUser: false\n\nskills:\n enableClaudeUser: false\n enableCodexUser: false\n enableAgentsUser: true\n\nsteeringMode: all\n\ndev:\n autoqa: false\n autoqaConsent: denied\n\ncompaction:\n thresholdTokens: -1\n idleEnabled: true\n handoffSaveToDisk: true\n\ndoubleEscapeAction: tree\ntextVerbosity: low\n\nfeatures:\n unexpectedStopDetection: smart\n\ncodexResets:\n autoRedeem: \"no\"\n\nstartup:\n quiet: true\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
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 - medium\n - high\n - xhigh\n - max\n defaultLevel: xhigh\n"
@@ -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 = "f8598828ed56e2ff48b5273f789dcbd4f7e6beb86994385f2f51ae1fdb0212fa"
53
+ export const GENERATED_PAYLOAD_HASH = "89c8d1e1b65fe34bf9fddf07c9a1992357fffe2b59342cbd2a0610f8ca39d808"
package/docks-kit CHANGED
@@ -37,9 +37,20 @@ if [[ -x "$REPO_DIR/cli/dist/$KIT_BIN" ]]; then
37
37
  BIN_VERSION="$("$REPO_DIR/cli/dist/$KIT_BIN" --version 2>/dev/null || true)"
38
38
  BIN_VERSION="${BIN_VERSION%$'\r'}"
39
39
  if [[ -n "$CHECKOUT_VERSION" && "$BIN_VERSION" == "$CHECKOUT_VERSION" ]]; then
40
- exec "$REPO_DIR/cli/dist/$KIT_BIN" "$@"
40
+ # A matching version does not prove matching content. Unreleased work does
41
+ # not move package.json, so a binary compiled before it still reports the
42
+ # checkout version and would silently shadow the source it predates.
43
+ NEWER_SOURCE=""
44
+ if [[ -d "$REPO_DIR/cli/src" ]]; then
45
+ NEWER_SOURCE="$(find "$REPO_DIR/cli/src" -type f -newer "$REPO_DIR/cli/dist/$KIT_BIN" -print -quit 2>/dev/null || true)"
46
+ fi
47
+ if [[ -z "$NEWER_SOURCE" ]]; then
48
+ exec "$REPO_DIR/cli/dist/$KIT_BIN" "$@"
49
+ fi
50
+ echo "[docks-kit] ignoring cli/dist/$KIT_BIN; ${NEWER_SOURCE#"$REPO_DIR/"} is newer than the binary — running from source; run 'bun run build:binaries' to refresh it or delete it" >&2
51
+ else
52
+ echo "[docks-kit] ignoring stale cli/dist/$KIT_BIN ${BIN_VERSION:-<unknown>}; checkout is ${CHECKOUT_VERSION:-<unknown>} — running from source; run 'bun run build:binaries' to refresh it or delete it" >&2
41
53
  fi
42
- echo "[docks-kit] ignoring stale cli/dist/$KIT_BIN ${BIN_VERSION:-<unknown>}; checkout is ${CHECKOUT_VERSION:-<unknown>} — running from source" >&2
43
54
  fi
44
55
 
45
56
  # Locate bun: PATH, then the known install locations that sit off the
package/docks-kit.ps1 CHANGED
@@ -56,13 +56,28 @@ if (Test-Path -LiteralPath $KitPath -PathType Leaf) {
56
56
  }
57
57
 
58
58
  if (-not [string]::IsNullOrEmpty($CheckoutVersion) -and $BinVersion -eq $CheckoutVersion) {
59
- & $KitPath @args
60
- exit $LASTEXITCODE
59
+ # A matching version does not prove matching content. Unreleased work does
60
+ # not move package.json, so a binary compiled before it still reports the
61
+ # checkout version and would silently shadow the source it predates.
62
+ $NewerSource = $null
63
+ $SrcDir = Join-Path $RepoDir 'cli\src'
64
+ if (Test-Path -LiteralPath $SrcDir -PathType Container) {
65
+ $BinWritten = (Get-Item -LiteralPath $KitPath).LastWriteTimeUtc
66
+ $NewerSource = Get-ChildItem -LiteralPath $SrcDir -Recurse -File -ErrorAction SilentlyContinue |
67
+ Where-Object { $_.LastWriteTimeUtc -gt $BinWritten } |
68
+ Select-Object -First 1
69
+ }
70
+ if ($null -eq $NewerSource) {
71
+ & $KitPath @args
72
+ exit $LASTEXITCODE
73
+ }
74
+ $NewerRelative = $NewerSource.FullName.Substring($RepoDir.Length).TrimStart('\', '/')
75
+ [Console]::Error.WriteLine("[docks-kit] ignoring cli/dist/$KitBin; $NewerRelative is newer than the binary — running from source; run 'bun run build:binaries' to refresh it or delete it")
76
+ } else {
77
+ $DisplayedBinVersion = if ([string]::IsNullOrEmpty($BinVersion)) { '<unknown>' } else { $BinVersion }
78
+ $DisplayedCheckoutVersion = if ([string]::IsNullOrEmpty($CheckoutVersion)) { '<unknown>' } else { $CheckoutVersion }
79
+ [Console]::Error.WriteLine("[docks-kit] ignoring stale cli/dist/$KitBin $DisplayedBinVersion; checkout is $DisplayedCheckoutVersion — running from source; run 'bun run build:binaries' to refresh it or delete it")
61
80
  }
62
-
63
- $DisplayedBinVersion = if ([string]::IsNullOrEmpty($BinVersion)) { '<unknown>' } else { $BinVersion }
64
- $DisplayedCheckoutVersion = if ([string]::IsNullOrEmpty($CheckoutVersion)) { '<unknown>' } else { $CheckoutVersion }
65
- [Console]::Error.WriteLine("[docks-kit] ignoring stale cli/dist/$KitBin $DisplayedBinVersion; checkout is $DisplayedCheckoutVersion — running from source")
66
81
  }
67
82
 
68
83
  function Find-Bun {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
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",
@@ -37,12 +37,13 @@
37
37
  "test:unit": "vitest run",
38
38
  "test:runtime:posix": "bun cli/test/statusline-runtime-smoke.mjs posix",
39
39
  "test:runtime:windows": "bun cli/test/statusline-runtime-smoke.mjs windows",
40
- "test:ci": "bun run check:generated && bun run --parallel typecheck test:unit golden:dryrun golden:mutation && bun run test:runtime:posix",
40
+ "test:ci": "bun run check:generated && bun run --parallel typecheck test:unit golden:dryrun golden:mutation format:check && bun run test:runtime:posix",
41
41
  "test:integration": "bun run --parallel golden:dryrun golden:mutation",
42
42
  "test": "bun run --parallel test:unit test:integration",
43
43
  "lint": "oxlint",
44
44
  "format": "oxfmt --write cli/src cli/test cli/scripts vitest.config.ts",
45
- "check": "bun run --parallel lint typecheck test"
45
+ "format:check": "oxfmt --check cli/src cli/test cli/scripts vitest.config.ts",
46
+ "check": "bun run --parallel lint format:check typecheck test"
46
47
  },
47
48
  "dependencies": {
48
49
  "@effect/platform-bun": "4.0.0-rc.115",