opencode-harness-kit 0.6.3 → 0.6.4

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/CHANGELOG.md CHANGED
@@ -12,6 +12,47 @@ is not happening.
12
12
 
13
13
  ---
14
14
 
15
+ ## [0.6.4] — 2026-10-23
16
+
17
+ Fixed, each one found by re-reading what 0.6.3 published, not by running
18
+ anything new:
19
+
20
+ - **The install command in the README named a package that does not exist.**
21
+ The Install section said `npx oc-harness init` "from the registry", and
22
+ `oc-harness` is this kit's binary name, not a package anyone can fetch: the
23
+ command every new reader was pointed at returns `npm error 404 'oc-harness'
24
+ is not in this registry`. Every install path in the README now reads
25
+ `npx -y opencode-harness-kit@latest <cmd>`, bare `oc-harness` is defined
26
+ once as shorthand for one of the two real forms, and a test pins every
27
+ `npx` reference in the README to this package. Released because the npm
28
+ page is a snapshot: the README on npmjs.com renders from the published
29
+ tarball, so fixing it in git changes nothing for the next visitor.
30
+ - **`profiles/index.json` listed `flint-40b-free` twice**, and the README's
31
+ shipped-profiles table repeated it with a second, differently-worded
32
+ note. `oc-harness list` printed twelve rows, `oc-harness init` offered
33
+ twelve choices, flint twice in both, and the README counted twelve
34
+ shipped profiles. Eleven profiles ship; one entry remains, carrying the
35
+ honest note of both. Tests now diff the index, the table and the loader,
36
+ with no Set to collapse the difference.
37
+ - **`retention.yml` repeated `opencode/flint-40b-free`, and the last key
38
+ won.** The hand-rolled parser overwrites `models[id]` on every repeat, so
39
+ the vague 2026-10-22 stub silently replaced the 2026-10-23 record that
40
+ quotes the provider's terms verbatim - same retention class, wrong date,
41
+ lost detail. A test now scans the file for repeated keys.
42
+ - **`install` from npx symlinked into the npx cache.** The installer always
43
+ symlinked the plugin, prompts, skills and GLOBAL.md back to the package
44
+ root - right from a git checkout ("edit here, live everywhere"), visible
45
+ breakage from an npx cache that `npm cache clean --force` deletes. From a
46
+ checkout nothing changes; from npx or any npm install, where `.git` is
47
+ absent, `install` now copies, so the harness on disk does not dangle the
48
+ day the cache is cleaned. `verify` and `uninstall` learn that a
49
+ byte-identical copy of the kit's file is the kit's, and a test installs
50
+ from a copy of the package with no `.git` into a temp HOME, asserts real
51
+ files where links used to go, and runs `verify` green.
52
+
53
+ No runtime change: `dist/`, the plugin and every model-facing artifact are
54
+ byte-for-byte what 0.6.3 shipped.
55
+
15
56
  ## [0.6.3] — 2026-10-23
16
57
 
17
58
  Nothing here changes the runtime. The plugin, the CLI and `dist/` are
@@ -210,15 +251,19 @@ default fleet was carrying a dead fallback rung in **all ten lanes** while
210
251
  release, and it is out of the repository now.
211
252
 
212
253
  A take is about one machine and one moment, and so is the recording of it.
213
- The `.gitignore` already said as much: "a recording stays on the machine
214
- that made it until somebody decides, deliberately, to publish it". The
215
- decision went the other way, so the file went back where that rule keeps
216
- it — `video/build/`, which is ignored — and the README no longer links a
217
- video the repository does not carry.
218
-
219
- What is published is still the old `docs/demo/pitch.mp4`: four minutes
220
- forty-seven seconds, recorded live from a Mac terminal, with a voice. The
221
- npm package never carried a video at all.
254
+ The `.gitignore` of that release said the same thing: "a recording stays on
255
+ the machine that made it until somebody decides, deliberately, to publish
256
+ it". The decision went the other way then, so the file went back where
257
+ that rule keeps it — `video/build/`, which is ignored — and the README no
258
+ longer links a video the repository does not carry. (Neither the rule nor
259
+ the recording survived 0.6.3, which removed the rule along with the
260
+ demonstration.)
261
+
262
+ At the time, what was published on GitHub was still the old
263
+ `docs/demo/pitch.mp4`: four minutes forty-seven seconds, recorded live
264
+ from a Mac terminal, with a voice. 0.6.3 removed it, and the history
265
+ rewrite of that release means no tag carries it today. The npm package
266
+ never carried a video at all.
222
267
 
223
268
  ## [0.5.8] — 2026-10-22
224
269
 
package/README.md CHANGED
@@ -20,7 +20,8 @@ git clone https://github.com/pham-canh/opencode-harness-kit.git
20
20
  ```
21
21
 
22
22
  The three steps `init` runs, spelled out for when you want to know what
23
- happened (prefix with `npx` or `./bin/oc-harness.mjs` to taste):
23
+ happened (prefix each with `npx -y opencode-harness-kit@latest` or
24
+ `./bin/oc-harness.mjs` to taste):
24
25
 
25
26
  ```bash
26
27
  oc-harness use fleet-safe # compile the profile
@@ -48,7 +49,7 @@ lanes.json ← the jobs
48
49
  src/ ← no model name anywhere in here
49
50
  ```
50
51
 
51
- Same plugin source, twelve shipped profiles, twelve different behaviours. The
52
+ Same plugin source, eleven shipped profiles, eleven different behaviours. The
52
53
  test suite asserts that rather than taking it on faith.
53
54
 
54
55
  ### How a request travels
@@ -237,7 +238,6 @@ it. One file covers ten jobs either way.
237
238
  | `nemotron-3.5-lightning-free` | 5 | 262,144 | — | **may train** |
238
239
  | `mimo-v2.6-flash-free` | 5 | 200,000 | — | **may train** |
239
240
  | `big-pickle` | 5 | 160,000 | — | **may train** |
240
- | `flint-40b-free` | 5 | 262,144 | `max` | **may train** |
241
241
 
242
242
  The five-lane profiles run everything on one model, which is the right shape
243
243
  when you want one route and want to know exactly which one.
@@ -306,7 +306,7 @@ A CI job that failed on the second kind would go red on a Tuesday because
306
306
  somebody else changed a provider's limits, and a job that goes red for a
307
307
  reason you cannot act on is a job people stop reading.
308
308
 
309
- It is not a hypothetical. The day this was written, four of the twelve
309
+ It is not a hypothetical. The day this was written, four of the shipped
310
310
  shipped profiles had already drifted from the live catalogue — the
311
311
  `temperature` flag had flipped on every one of them.
312
312
 
@@ -561,7 +561,8 @@ oc-harness auto --json the same reconcile, as a report a script can read
561
561
  oc-harness use <id> compile a profile into ~/.config/opencode/opencode.jsonc
562
562
  oc-harness use <id> --dryrun print a summary of what would be written, write nothing
563
563
  oc-harness refresh <id> re-derive a profile against the live model list
564
- oc-harness install symlink the plugin, prompts, skills and GLOBAL.md
564
+ oc-harness install symlink the plugin, prompts, skills and GLOBAL.md into the config dir
565
+ (from `npx`, they link into a package copy it makes first - see Install)
565
566
  oc-harness link just the plugin, no prompts or skills
566
567
  oc-harness verify check the install against the profile; non-zero on drift
567
568
  oc-harness agents report agents the effort tiering does not reach
@@ -857,12 +858,15 @@ One command if you are new here (`init` asks which free profile to run on,
857
858
  then runs the two commands below plus `verify`):
858
859
 
859
860
  ```bash
860
- npx oc-harness init
861
+ npx -y opencode-harness-kit@latest init
861
862
  ```
862
863
 
863
- Two commands, in this order, if you already know which profile you want
864
- (`npx oc-harness …` from the registry, or `./bin/oc-harness.mjs …` from a
865
- checkout — they are the same program):
864
+ Two commands, in this order, if you already know which profile you want.
865
+ Two forms of the same program: `npx -y opencode-harness-kit@latest …` from
866
+ the registry, or `./bin/oc-harness.mjs …` from a checkout. Bare `oc-harness`
867
+ elsewhere in this README is shorthand for whichever form installed it —
868
+ `npx` leaves no global binary, so every command already naming just
869
+ `oc-harness` is one of the two above in disguise:
866
870
 
867
871
  ```bash
868
872
  oc-harness use fleet-safe # writes opencode.jsonc
@@ -873,6 +877,15 @@ oc-harness install # writes everything that file points at
873
877
  `GLOBAL.md` into `~/.config/opencode/`, so this repo stays the single source
874
878
  of truth and an edit here takes effect on the next opencode start.
875
879
 
880
+ That is the checkout behaviour, and it relies on the repository staying put.
881
+ Run through `npx` — or any package install — there is no repository to hold
882
+ the truth, only a npm cache that `npm cache clean --force` can delete under
883
+ it: the installer then COPIES the package once to
884
+ `~/.config/opencode/kit/` and links into that instead, so `npm cache clean`
885
+ cannot dangle the harness out from under its own config. Same commands, same
886
+ config; `uninstall` takes the copy out with the links, and `verify` judges
887
+ both modes by the same ownership rules.
888
+
876
889
  ### The one command that asks nothing
877
890
 
878
891
  `init` is the front door for a person: it asks which free profile to run on.
@@ -12,7 +12,7 @@
12
12
  // The point of the kit: switching models is a data edit, not a code change.
13
13
  // Everything model-specific lives in profiles/<id>.json.
14
14
 
15
- import { chmodSync, copyFileSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, symlinkSync, readdirSync, readlinkSync, realpathSync, lstatSync, appendFileSync, renameSync, unlinkSync } from "node:fs"
15
+ import { chmodSync, copyFileSync, cpSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, symlinkSync, readdirSync, readlinkSync, realpathSync, lstatSync, appendFileSync, renameSync, unlinkSync } from "node:fs"
16
16
  import { homedir } from "node:os"
17
17
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
18
18
  import { execFileSync } from "node:child_process"
@@ -56,6 +56,74 @@ const CONFIG_DIR =
56
56
  const CONFIG_FILE = join(CONFIG_DIR, "opencode.jsonc")
57
57
  const ROOT = import.meta.dirname ? join(import.meta.dirname, "..") : process.cwd()
58
58
 
59
+ // A git CHECKOUT is the kit's own source of truth and is expected to stay
60
+ // where it is: everything the kit installs symlinks back at the repository,
61
+ // so an edit there takes effect on the next opencode start.
62
+ //
63
+ // Anything else - `npx`, a plain `npm install` - reached this file through a
64
+ // package cache npm is free to clean at any command, and a symlink INTO that
65
+ // cache is a dangling path the day `npm cache clean --force` runs. For those
66
+ // installs the package is COPIED once to a private, durable place under the
67
+ // config directory, and every install target links into the copy instead.
68
+ const CHECKOUT = existsSync(join(ROOT, ".git"))
69
+ const COPY_ROOT = join(CONFIG_DIR, "kit")
70
+
71
+ /**
72
+ * Mirror the package's runtime assets into `COPY_ROOT`.
73
+ *
74
+ * Runs at most once per version: a `.source` marker records which package
75
+ * version the copy was made from, and an install of the same version keeps
76
+ * the existing copy rather than re-wiping files freshly linked into.
77
+ *
78
+ * The copy is whole on purpose. The plugin bundle looks for its profiles and
79
+ * data by walking up from itself, the prompts and skills are linked per file
80
+ * out of it, and every asset the behavioral layer needs lives inside it -
81
+ * which is also what makes `uninstall` clean: one tree, created by one
82
+ * command, owned by nobody else.
83
+ */
84
+ function ensurePackageCopy() {
85
+ if (CHECKOUT) return
86
+ const want = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version
87
+ let have = null
88
+ try {
89
+ have = readFileSync(join(COPY_ROOT, ".source"), "utf8").trim()
90
+ } catch {}
91
+ if (have === want) return
92
+ rmSync(COPY_ROOT, { recursive: true, force: true })
93
+ mkdirSync(COPY_ROOT, { recursive: true })
94
+ for (const name of ["dist", "profiles", "prompts", "skills"]) {
95
+ cpSync(join(ROOT, name), join(COPY_ROOT, name), { recursive: true })
96
+ }
97
+ for (const name of ["lanes.json", "retention.yml", "GLOBAL.md"]) {
98
+ cpSync(join(ROOT, name), join(COPY_ROOT, name))
99
+ }
100
+ writeFileSync(join(COPY_ROOT, ".source"), `${want}\n`)
101
+ }
102
+
103
+ /**
104
+ * The root the kit installs FROM: the repository in a checkout, `COPY_ROOT`
105
+ * otherwise. Calling it at all triggers `ensurePackageCopy()`, so a caller
106
+ * holding the returned paths can rely on the files existing.
107
+ *
108
+ * @returns {string}
109
+ */
110
+ function kitSourceRoot() {
111
+ if (CHECKOUT) return ROOT
112
+ ensurePackageCopy()
113
+ return COPY_ROOT
114
+ }
115
+
116
+ /**
117
+ * The root installed files may POINT at: the repository in a checkout, the
118
+ * durable package copy otherwise. `isOurs(below)` judges ownership against
119
+ * it.
120
+ *
121
+ * @returns {string}
122
+ */
123
+ function kitTargetRoot() {
124
+ return CHECKOUT ? ROOT : COPY_ROOT
125
+ }
126
+
59
127
  const args = process.argv.slice(2)
60
128
  const cmd = args[0] ?? "list"
61
129
  const has = (f) => args.includes(f)
@@ -382,12 +450,13 @@ function cmdShow(id) {
382
450
  }
383
451
 
384
452
  /**
385
- * Link the built plugin into ~/.config/opencode/plugins so opencode loads it
386
- * without an npm install. This is the path to use while developing, and it
387
- * means the kit is usable before anything is published.
453
+ * Put the built plugin where opencode loads it from -
454
+ * ~/.config/opencode/plugins/opencode-harness-kit.js - symlinked at the kit
455
+ * itself from a checkout, at the durable package copy when the kit was
456
+ * reached through npm or npx.
388
457
  */
389
458
  function cmdLink() {
390
- const dist = join(ROOT, "dist", "index.js")
459
+ const dist = join(kitSourceRoot(), "dist", "index.js")
391
460
  if (!existsSync(dist)) {
392
461
  fail("dist/index.js missing. Run: npm run build")
393
462
  }
@@ -426,10 +495,14 @@ function cmdLink() {
426
495
 
427
496
  /** Where the behavioural layer has to land for the config to mean anything. */
428
497
  function installTargets() {
498
+ // `kitSourceRoot()` is the repository in a checkout, and the durable copy
499
+ // of the package under the config directory otherwise - an npx cache is
500
+ // npm's to clean, and links into it would dangle the day it goes.
501
+ const root = kitSourceRoot()
429
502
  return [
430
- { src: join(ROOT, "prompts"), dest: join(CONFIG_DIR, "prompts"), kind: "dir" },
431
- { src: join(ROOT, "skills"), dest: join(CONFIG_DIR, "skills"), kind: "dir" },
432
- { src: join(ROOT, "GLOBAL.md"), dest: join(CONFIG_DIR, "GLOBAL.md"), kind: "file" },
503
+ { src: join(root, "prompts"), dest: join(CONFIG_DIR, "prompts"), kind: "dir" },
504
+ { src: join(root, "skills"), dest: join(CONFIG_DIR, "skills"), kind: "dir" },
505
+ { src: join(root, "GLOBAL.md"), dest: join(CONFIG_DIR, "GLOBAL.md"), kind: "file" },
433
506
  ]
434
507
  }
435
508
 
@@ -607,7 +680,15 @@ function isLegacyAgentLink(full) {
607
680
  * prompts, the four skills, the global rules - was a `cp` line in the README
608
681
  * that nobody ran. Nothing checked it afterwards, which is how a config ended
609
682
  * up pointing at an instruction file that did not exist while still looking
610
- * correct. Symlinks rather than copies, so the repo stays the source of truth.
683
+ * correct.
684
+ *
685
+ * From a checkout everything symlinks at the repository: edit there, live
686
+ * everywhere. From npm or npx there is no repository to hold the truth - the
687
+ * cache that supplied this process can be cleaned at any moment - so the
688
+ * package is first COPIED to a private, durable place under the config
689
+ * directory (`~/.config/opencode/kit/`), and every link points in there.
690
+ * `linkOne` and the ownership checks are unchanged; only what sits at the far
691
+ * end of the link differs.
611
692
  */
612
693
  async function cmdInstall() {
613
694
  const dist = join(ROOT, "dist", "index.js")
@@ -1180,7 +1261,7 @@ function cmdVerify() {
1180
1261
  problems.push(
1181
1262
  `${kitInstruction} does not exist - the rules in it are not being loaded. Run: oc-harness install`,
1182
1263
  )
1183
- } else if (!pointsIntoKit(kitTarget)) {
1264
+ } else if (!isOurs(kitTarget)) {
1184
1265
  // The kit's own file, and not a file that happens to have its name. The
1185
1266
  // installer refuses to replace a real file, so a user who already had a
1186
1267
  // GLOBAL.md keeps it - and the check above was satisfied by its mere
@@ -1218,7 +1299,7 @@ function cmdVerify() {
1218
1299
  // The config's own reference is still checked below: the lane's prompt
1219
1300
  // path is a separate thing, and a broken one is worth saying whatever
1220
1301
  // the file at that path happens to be.
1221
- if (!pointsIntoKit(file)) {
1302
+ if (!isOurs(file)) {
1222
1303
  problems.push(
1223
1304
  `prompts/${role}.md is not the kit's file - the ${role} lane runs on somebody else's instructions. Run: oc-harness install`,
1224
1305
  )
@@ -1372,7 +1453,7 @@ function cmdVerify() {
1372
1453
  // This does not stop the frontmatter check below. A directory that is
1373
1454
  // not the kit's can still be a skill whose name has drifted away from
1374
1455
  // its directory, and that is a second thing worth saying.
1375
- if (!pointsIntoKit(join(skillsDir, name))) {
1456
+ if (!isOurs(join(skillsDir, name))) {
1376
1457
  problems.push(
1377
1458
  `skills/${name} is not the kit's skill - the ${name} step of the loop runs on somebody else's instructions. Run: oc-harness install`,
1378
1459
  )
@@ -2039,6 +2120,36 @@ function pointsIntoKit(p) {
2039
2120
  }
2040
2121
  }
2041
2122
 
2123
+ /**
2124
+ * Whether an installed path holds the kit's file.
2125
+ *
2126
+ * From a checkout that means a link into the repository itself. From npm or
2127
+ * npx the install copied the package to `COPY_ROOT` and linked everything at
2128
+ * THAT, so "ours" is a link into - or a file inside - the copy. Links are
2129
+ * resolved relative to the link and without requiring them to exist, so a
2130
+ * dangling link still counts as the kit's: the state `npm uninstall` leaves
2131
+ * behind, and `uninstall`'s job to fix.
2132
+ *
2133
+ * @param {string} p
2134
+ * @returns {boolean}
2135
+ */
2136
+ function isOurs(p) {
2137
+ if (CHECKOUT) return pointsIntoKit(p)
2138
+ let st
2139
+ try {
2140
+ st = lstatSync(p)
2141
+ } catch {
2142
+ return false
2143
+ }
2144
+ const root = resolve(COPY_ROOT)
2145
+ const under = (q) => q === root || q.startsWith(root + sep) || q.startsWith(root + "/")
2146
+ if (st.isSymbolicLink()) {
2147
+ const target = readlinkSync(p)
2148
+ return under(isAbsolute(target) ? target : resolve(dirname(p), target))
2149
+ }
2150
+ return under(resolve(p))
2151
+ }
2152
+
2042
2153
  /**
2043
2154
  * Copy the machine-derived fields of a live catalogue entry into a profile.
2044
2155
  *
@@ -2333,11 +2444,14 @@ function findProjectSpace(startDir) {
2333
2444
  * Remove what the kit installed.
2334
2445
  *
2335
2446
  * The install SYMLINKS the plugin, the prompts, the skills and GLOBAL.md out
2336
- * of the kit's directory and into the config directory. So `npm uninstall`
2337
- * the kit leaves four dangling symlinks behind, and opencode then loads
2338
- * neither the rules nor the guard - and says nothing about it.
2339
- *
2340
- * So the kit can take them back out, and it says what it removed.
2447
+ * of the kit - the repository in a checkout, the durable package copy
2448
+ * otherwise - and into the config directory. So `npm uninstall` the kit
2449
+ * leaves four dangling symlinks behind, and opencode then loads neither the
2450
+ * rules nor the guard - and says nothing about it.
2451
+ *
2452
+ * So the kit can take them back out, and it says what it removed. A copy of
2453
+ * the package under the config directory is the kit's ENTIRELY - it exists
2454
+ * only because `install` made it - so it goes with the links.
2341
2455
  */
2342
2456
  function cmdUninstall() {
2343
2457
  const removed = []
@@ -2358,14 +2472,16 @@ function cmdUninstall() {
2358
2472
  }
2359
2473
  // A symlink is not enough to make it ours. The user may have their own
2360
2474
  // symlink in that directory - to their own prompts, to a shared folder -
2361
- // and "a symlink in prompts/ is the kit's" would delete it. A link whose
2362
- // target is inside this package is the kit's; anything else is kept.
2475
+ // and "a symlink in prompts/ is the kit's" would delete it. A link that
2476
+ // is the kit's to manage goes; anything else is kept. Whose a link is is
2477
+ // `isOurs`'s decision: into the repository from a checkout, into the
2478
+ // package copy otherwise.
2363
2479
  //
2364
2480
  // The target is resolved WITHOUT requiring it to exist. A DANGLING link
2365
2481
  // is the case this command exists for - `npm uninstall` leaves four of
2366
2482
  // them behind - and `realpathSync` throws on those, which classified the
2367
2483
  // kit's own broken links as somebody else's and left them in place.
2368
- const ours = entries.filter((f) => pointsIntoKit(join(full, f)))
2484
+ const ours = entries.filter((f) => isOurs(join(full, f)))
2369
2485
  if (!ours.length) {
2370
2486
  kept.push(`${dir}/ holds real files`)
2371
2487
  continue
@@ -2388,14 +2504,14 @@ function cmdUninstall() {
2388
2504
  for (const t of targets) {
2389
2505
  if (!existsSync(t) && !isSymlink(t)) continue
2390
2506
  try {
2391
- // Only a link into this package is ours to remove. A real file the
2507
+ // Only a link the kit installed is ours to remove. A real file the
2392
2508
  // user put there is theirs, and so is a link to their own file -
2393
2509
  // `GLOBAL.md` is a standard opencode name.
2394
2510
  if (!isSymlink(t)) {
2395
2511
  kept.push(`${t} is a real file`)
2396
2512
  continue
2397
2513
  }
2398
- if (!pointsIntoKit(t)) {
2514
+ if (!isOurs(t)) {
2399
2515
  kept.push(`${t} points somewhere else`)
2400
2516
  continue
2401
2517
  }
@@ -2405,6 +2521,18 @@ function cmdUninstall() {
2405
2521
  kept.push(`${t} (could not be removed)`)
2406
2522
  }
2407
2523
  }
2524
+ // The package copy - `~/.config/opencode/kit/` when the kit was reached
2525
+ // through npm or npx - exists only because `install` created it, so it
2526
+ // goes out with the links. Unreachable from a checkout, where no copy
2527
+ // exists.
2528
+ if (!CHECKOUT && existsSync(COPY_ROOT)) {
2529
+ try {
2530
+ rmSync(COPY_ROOT, { recursive: true, force: true })
2531
+ removed.push("kit/ (the package copy this install was built from)")
2532
+ } catch {
2533
+ kept.push(`${COPY_ROOT} (could not be removed)`)
2534
+ }
2535
+ }
2408
2536
  console.log(`\n${bold("uninstall")}\n`)
2409
2537
  for (const r of removed) console.log(` ${green("✓")} removed ${r}`)
2410
2538
  for (const k of kept) console.log(` ${yellow("!")} left alone: ${k}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-harness-kit",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Harness kit for OpenCode: capability profiles, a fleet of job lanes with one free model each, role-based reasoning effort, context pruning, and measured baselines.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/pham-canh/opencode-harness-kit#readme",
@@ -110,7 +110,7 @@
110
110
  "topEffort": "max",
111
111
  "context": 262144,
112
112
  "default": false,
113
- "note": "Open-weight MoE with about 2.5B active parameters for local coding agents. Knowledge cutoff 2026-06, no image input. May train on prompts during its free period."
113
+ "note": "Open-weight MoE, 40B total with about 2.5B active parameters, built for coding agents and small enough to serve locally. Full effort ladder, 262k context, 64k output, no image input. Knowledge cutoff 2026-06. May train on prompts during its free period."
114
114
  },
115
115
  {
116
116
  "id": "big-pickle",
@@ -122,17 +122,6 @@
122
122
  "context": 160000,
123
123
  "default": false,
124
124
  "note": "Stealth model, no public identity, no effort levels, no image input. The provider may train on it during its free period."
125
- },
126
- {
127
- "id": "flint-40b-free",
128
- "file": "profiles/flint-40b-free.json",
129
- "title": "Flint 40B Free",
130
- "retention": "may-train",
131
- "free": true,
132
- "topEffort": "max",
133
- "context": 262144,
134
- "default": false,
135
- "note": "Open-weight MoE, 40B total with about 2.5B active, built for coding agents and small enough to serve locally. Full effort ladder, 262k context, 64k output, no image input."
136
125
  }
137
126
  ],
138
127
  "excluded": [],
package/retention.yml CHANGED
@@ -110,14 +110,6 @@ models:
110
110
  verified: 2026-10-19
111
111
  source: https://opencode.ai/docs/zen
112
112
 
113
- opencode/flint-40b-free:
114
- retention: may-train
115
- note: >-
116
- Free trial endpoint on opencode zen; treated as may-train until terms
117
- state otherwise.
118
- verified: 2026-10-22
119
- source: https://opencode.ai/docs/zen
120
-
121
113
  # --- Paid routes, kept because the catalog still serves them and a profile
122
114
  # may be pointed at one. ---
123
115