geneseed 3.15.0 → 3.17.0

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/SHIPPED.md +1 -1
  3. package/adapters/opencode/plugins/geneseed-context.js +36 -8
  4. package/adapters/opencode/plugins/geneseed-guard.js +28 -0
  5. package/bin/geneseed.mjs +17 -0
  6. package/docs/concepts/exclusions.md +21 -9
  7. package/docs/concepts/hooks.md +1 -0
  8. package/docs/concepts/loop-notes.md +3 -0
  9. package/docs/concepts/loops.md +11 -0
  10. package/docs/guides/headless.md +1 -1
  11. package/docs/guides/install-scripted.md +1 -1
  12. package/docs/guides/project-context.md +3 -3
  13. package/docs/guides/read-non-markdown.md +1 -1
  14. package/docs/reference/plugin-context.md +2 -2
  15. package/docs/reference/plugin-guard.md +1 -1
  16. package/docs/token-footprint.md +10 -7
  17. package/harness.config.json +1 -1
  18. package/js/build/args.mjs +34 -3
  19. package/js/build/driver.mjs +8 -7
  20. package/js/build/generate.mjs +11 -5
  21. package/js/build/render.mjs +33 -3
  22. package/js/build/source.mjs +37 -1
  23. package/js/cli-table.json +15 -0
  24. package/js/hosts/hooks-context.mjs +112 -56
  25. package/js/hosts/hooks.mjs +34 -1
  26. package/js/hosts/installs.mjs +25 -2
  27. package/js/hosts/opencode.mjs +2 -0
  28. package/js/hosts/settings.mjs +2 -1
  29. package/js/inspect/checks-constitution.mjs +2 -2
  30. package/js/inspect/diff.mjs +4 -1
  31. package/js/inspect/status.mjs +2 -1
  32. package/js/inspect/validate.mjs +3 -1
  33. package/js/loop/graph.mjs +2 -1
  34. package/js/loop/state.mjs +30 -1
  35. package/js/maintain/setup.mjs +11 -4
  36. package/js/maintain/update.mjs +1 -1
  37. package/js/web/actions.mjs +5 -3
  38. package/package.json +1 -1
  39. package/src/AGENT.md.tmpl +1 -1
  40. package/src/agents/docs.md +5 -0
  41. package/src/bricks/apply.md +3 -0
  42. package/src/bricks/docs-update.md +2 -0
  43. package/src/bricks/test.md +17 -7
  44. package/src/doctrines/process.md +9 -5
  45. package/src/loops/api-endpoint.json +1 -1
  46. package/src/loops/bugfix.json +1 -1
  47. package/src/loops/ci-repair.json +1 -1
  48. package/src/loops/deps-upgrade.json +1 -1
  49. package/src/loops/enforce-architecture-rule.json +1 -1
  50. package/src/loops/feature.json +1 -1
  51. package/src/loops/fix-flaky-tests.json +1 -1
  52. package/src/loops/legacy-refactor.json +1 -1
  53. package/src/loops/legacy-tests.json +1 -1
  54. package/src/loops/refactor.json +1 -1
  55. package/src/loops/remove-dead-code.json +1 -1
  56. package/src/loops/spec-first-feature.json +1 -1
  57. package/src/loops/tdd.json +1 -1
  58. package/src/loops/update-contract.json +1 -1
  59. package/src/skills/document-project.md +91 -22
  60. package/web/dist/assets/{Activity-kpWxd70F.js → Activity-DonKN1pi.js} +1 -1
  61. package/web/dist/assets/{ActivityDetail-Ccr942VL.js → ActivityDetail-e47a3cdE.js} +1 -1
  62. package/web/dist/assets/{Dashboard-D1lE7jcg.js → Dashboard-Cr7i0PsO.js} +2 -2
  63. package/web/dist/assets/{Docs-BHyaggFy.js → Docs-Lne3nuzD.js} +1 -1
  64. package/web/dist/assets/{GreenhouseView-Sc_UDMOE.js → GreenhouseView-4CbIuTpl.js} +1 -1
  65. package/web/dist/assets/{Installs-Gw-pJ3Zi.js → Installs-CIzbV1Ks.js} +1 -1
  66. package/web/dist/assets/{JournalView-_gxpsxXi.js → JournalView-BlBfuvzj.js} +1 -1
  67. package/web/dist/assets/{Laws-S9fq1r9T.js → Laws-B-pjcjHr.js} +1 -1
  68. package/web/dist/assets/{Library-MBxadRzn.js → Library-x8Nzg6yq.js} +1 -1
  69. package/web/dist/assets/{Loops-Da_JhlzM.js → Loops-DIOxW1xH.js} +1 -1
  70. package/web/dist/assets/{OperatorHudView--TkvDybH.js → OperatorHudView-D7S7z7CJ.js} +1 -1
  71. package/web/dist/assets/{Personal-CTXKHVbV.js → Personal-Ws3v35AB.js} +1 -1
  72. package/web/dist/assets/{Tabs-B0cdf8k6.js → Tabs-6Z1eziAq.js} +1 -1
  73. package/web/dist/assets/{drawable-BbWQxe82.js → drawable-ATNeYKbT.js} +1 -1
  74. package/web/dist/assets/{index-ujKG1ZNf.js → index-CDg5XUhG.js} +2 -2
  75. package/web/dist/index.html +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,41 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.17.0] — 2026-10-08
12
+
13
+ ### Added
14
+
15
+ - Docs in AsciiDoc. `document-project`, the docs agent and the `docs-update` brick detect a project's docs format: a `.md`-only tree stays Markdown, an `.adoc`-only tree stays AsciiDoc, a mix asks you, and a project with no docs defaults to AsciiDoc. The choice is recorded as `Docs format:` in `AGENTS.md`. AsciiDoc mode uses header attributes, `.adoc` names and `[source,mermaid]` diagrams that GitHub renders, and it finishes with an `asciidoctor --failure-level=WARN` lint (or an explicit SKIPPED). With no docs yet, `document-project` interviews you first. Agent-runtime files stay Markdown.
16
+ - C4 architecture diagrams in `document-project`. The audience you name sets the levels: L1 and L2 always, L3 only for containers you call complex, never L4. Diagrams are Mermaid `flowchart`s styled as C4 (typed nodes, `intent · protocol` edges, a title and a legend), redrawn only inside their own section. The `AGENTS.md` it writes sends a cross-container change to the L2 diagram first.
17
+ - The context hook and the OpenCode plugin pick up `.adoc` files wherever they pick up `.md`; `README.adoc` and `CONTRIBUTING.adoc` load eagerly.
18
+ - Protected checks. List the checks and tests your agent is judged by in `.geneseed/protected-checks.txt` at the repo root, one path per line, and a write to any of them asks you first on Claude Code, is blocked on Bob and refused on OpenCode, citing *External Gate*. A check the agent can edit is not a check. Nothing changes until a project adds the file. Shell edits are not seen, so keep running the checks in CI.
19
+
20
+ ### Changed
21
+
22
+ - Loops repair once, then stop for you. The test repair rings of every shipped template (`apply-test`, `fix-test`, `write-run`, `stabilise`) are now `max: 1`: one repair, then the attempt is discarded for one fresh try, then the loop stops. A second revision makes code worse, not better — 0.82 correct after one revision, 0.673 after two ([arXiv 2607.24604](https://arxiv.org/abs/2607.24604)). A ring's `max` now binds its own repair edge even when a wider ring shares its nodes; before, `review-fix` (max 3) quietly let `apply-test` retry four times.
23
+ - The `test` brick runs checks in order — build gate, then every focused check (none hides another), then the full suite only when all passed — and lists each one as `PASS`, `FAIL`, `ERROR` or `SKIPPED (reason)`. Each failure is four lines: purpose, property, a one-line diagnostic, the rerun command.
24
+ - `error` is a reserved loop outcome: a check that crashed or was never wired stops the loop for a human instead of feeding a repair. A brick may declare it without an edge.
25
+ - Each `test` report replaces the earlier `test` notes, so a repair never reads findings about code that has since changed.
26
+ - Doctrine *Bound the Loop* says the same outside the loop engine: one repair, then a fresh attempt or a hand-back; only a verdict on the final artifact counts.
27
+
28
+ ## [3.16.1] — 2026-10-07
29
+
30
+ ### Fixed
31
+
32
+ - `geneseed upgrade` from a checkout older than the August rename of `bin/geneseed.mjs` to `bin/build-driver.mjs` no longer fails with `Cannot find module '…/bin/geneseed.mjs'` and `[E-BUILD]` after a successful pull and doctor gate. The old updater (and an old web daemon's build jobs) spawn the old path from the code they started with; `bin/geneseed.mjs` is back as a forwarder to the driver. If you already hit it, run `geneseed upgrade` again — it is up to date now and rebuilds with the new code.
33
+
34
+ ## [3.16.0] — 2026-10-05
35
+
36
+ **Upgrading:** re-emit (`geneseed rebuild-all`) so the context hook stays under Claude Code's output cap. To drop skills you never use, rebuild an install with `--exclude-skills <name,…>` (see [Exclusions](docs/concepts/exclusions.md)).
37
+
38
+ ### Added
39
+
40
+ - `--exclude-skills <name,…>` leaves skills you never use out of an install, on every host: their files, their catalogue rows and any OpenCode command that runs them. The install records the list on an `Excluded skills:` line, and `upgrade`, `rebuild-all`, `diff`, `setup` and the web console keep it. Unknown skills are refused, and so are the ten the harness links to by path. `status` shows `skills off: …`.
41
+
42
+ ### Fixed
43
+
44
+ - The context hook (Claude Code, Bob, OpenClaude) now keeps its whole output under 9,000 characters. Claude Code shows the model only a preview of a hook output past ~10k, so a doc-heavy repo lost its context silently — this repo's was 25.6 KB, now 2.5 KB. Eager docs that do not fit are listed lazy with the reason; the lazy listing folds three or more docs in one folder into `dir/ — N docs` and ends in a count if it is still too long. The OpenCode plugin folds its project listing the same way.
45
+
11
46
  ## [3.15.0] — 2026-10-05
12
47
 
13
48
  **Upgrading:** re-emit (`geneseed rebuild-all`) so OpenCode installs get the hardened plugins and
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
20
20
  > **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
21
21
  > human-readable label only. The canonical identity of an installed harness is the
22
22
  > **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
23
- > string. Current label: **3.15.0**.
23
+ > string. Current label: **3.17.0**.
24
24
 
25
25
  ## Capabilities
26
26
 
@@ -117,6 +117,8 @@ const MAX_DEPTH = 6
117
117
  // the head of each file (enough for an H1) rather than the whole thing — a large
118
118
  // docs/ tree must not cost one full-file read per entry on every session start.
119
119
  const LAZY_HEADING_LIMIT = envNum("GENESEED_LAZY_HEADINGS", 64)
120
+ // Same threshold as the Claude hook's LAZY_FOLD_AT.
121
+ const LAZY_FOLD_AT = 3
120
122
  const HEADING_SLICE_BYTES = 4096
121
123
 
122
124
  // Quiet by default — OpenCode surfaces a plugin's stderr in the UI (red text). Set
@@ -154,8 +156,12 @@ function log(msg) { if (DEBUG) console.error(`[geneseed-context] ${msg}`) }
154
156
  // (OpenCode's own native roots) are deliberately absent: OpenCode already has them, and
155
157
  // injecting them again re-sent the whole harness on every request until 2026-09.
156
158
  const EAGER_ROOT = new Set([
157
- ".cursorrules", "README.md", "CONTRIBUTING.md", "user-rules.md", "PROFILE.md",
159
+ ".cursorrules", "README.md", "README.adoc", "CONTRIBUTING.md", "CONTRIBUTING.adoc",
160
+ "user-rules.md", "PROFILE.md",
158
161
  ])
162
+ // Project docs are Markdown or AsciiDoc (document-project writes AsciiDoc when a project has
163
+ // none yet). Same set as js/hosts/hooks-context.mjs DOC_EXT.
164
+ const isDoc = (name) => /\.(md|adoc)$/i.test(name)
159
165
  // Doc trees walked recursively; everything found is lazy (listed, not injected).
160
166
  const LAZY_DIRS = ["docs", "doc", "documentation", "architecture", "adr", "ADR"]
161
167
  // Never descend into these.
@@ -275,11 +281,13 @@ function globToRegExp(glob) {
275
281
  return new RegExp("^" + re + "$")
276
282
  }
277
283
 
278
- // First H1 / heading line of a markdown file, for the lazy listing.
284
+ // First heading line of a markdown or AsciiDoc file, for the lazy listing. An AsciiDoc
285
+ // title is "= Title"; the space is required so a setext "====" underline never matches.
279
286
  function firstHeading(text) {
280
287
  for (const line of text.split("\n")) {
281
288
  const s = line.trim()
282
289
  if (s.startsWith("#")) return s.replace(/^#+\s*/, "").trim()
290
+ if (/^=+\s/.test(s)) return s.replace(/^=+\s*/, "").trim()
283
291
  }
284
292
  return ""
285
293
  }
@@ -423,7 +431,7 @@ async function walkMd(dir, root, depth, acc) {
423
431
  if (e.isDirectory()) {
424
432
  if (EXCLUDE_DIRS.has(e.name)) continue
425
433
  await walkMd(full, root, depth + 1, acc)
426
- } else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
434
+ } else if (e.isFile() && isDoc(e.name)) {
427
435
  acc.push(full)
428
436
  }
429
437
  }
@@ -446,7 +454,7 @@ async function discover(root) {
446
454
  if (!e.isFile()) continue
447
455
  const abs = path.join(root, e.name)
448
456
  if (EAGER_ROOT.has(e.name)) add(eager, abs)
449
- else if (e.name.toLowerCase().endsWith(".md")) add(lazy, abs) // misc root .md
457
+ else if (isDoc(e.name)) add(lazy, abs) // misc root .md / .adoc
450
458
  }
451
459
 
452
460
  // 2. Doc trees (recursive) -> lazy.
@@ -467,8 +475,10 @@ async function discover(root) {
467
475
  try { pkgs = await fs.readdir(base, { withFileTypes: true }) } catch { continue }
468
476
  for (const p of pkgs) {
469
477
  if (!p.isDirectory() || EXCLUDE_DIRS.has(p.name)) continue
470
- const readme = path.join(base, p.name, "README.md")
471
- if (await isFile(readme) && !eager.has(readme)) add(lazy, readme)
478
+ for (const name of ["README.md", "README.adoc"]) {
479
+ const readme = path.join(base, p.name, name)
480
+ if (await isFile(readme) && !eager.has(readme)) add(lazy, readme)
481
+ }
472
482
  }
473
483
  }
474
484
 
@@ -664,7 +674,7 @@ async function wikiSets() {
664
674
  // Render one eager+lazy set into `out`. The budget state (eager bytes, heading
665
675
  // reads) is SHARED across segments — project context and every wiki draw from the
666
676
  // same caps, so the whole block stays bounded no matter how many wikis are declared.
667
- async function renderSet(out, { eager, lazy }, state) {
677
+ async function renderSet(out, { eager, lazy }, state, { fold = false } = {}) {
668
678
  const perFile = EAGER_FILE_KB * 1024
669
679
  const total = EAGER_TOTAL_KB * 1024
670
680
  const demoted = []
@@ -692,8 +702,26 @@ async function renderSet(out, { eager, lazy }, state) {
692
702
  state.injected++
693
703
  }
694
704
 
705
+ // The project listing folds like the Claude hook's (js/hosts/hooks-context.mjs): three or more
706
+ // undescribed docs sharing a parent directory become one `dir/ — N docs` line — a docs/ tree
707
+ // of 186 files was 186 lines on every session. Root files never fold. A wiki listing keeps its
708
+ // per-note headings: a vault is navigated by them, and it carries its own cap.
709
+ const byDir = new Map()
710
+ if (fold) {
711
+ for (const l of lazy) {
712
+ const dir = path.posix.dirname(l.rel.replace(/\\/g, "/"))
713
+ if (!l.desc && dir !== ".") byDir.set(dir, (byDir.get(dir) || 0) + 1)
714
+ }
715
+ }
716
+ const folded = new Set()
695
717
  const lazyLines = []
696
718
  for (const l of lazy) {
719
+ const dir = path.posix.dirname(l.rel.replace(/\\/g, "/"))
720
+ if (!l.desc && byDir.get(dir) >= LAZY_FOLD_AT) {
721
+ if (!folded.has(dir)) lazyLines.push(` - ${dir}/ — ${byDir.get(dir)} docs`)
722
+ folded.add(dir)
723
+ continue
724
+ }
697
725
  let head = l.desc
698
726
  // Only crack open files we have no description for, and cap how many — bounded by
699
727
  // a head-slice read so a big docs/ tree stays cheap on every session start.
@@ -734,7 +762,7 @@ async function buildBlock(sets, wikis = [], commands = [], session = []) {
734
762
  }
735
763
 
736
764
  const proj = []
737
- await renderSet(proj, sets, state)
765
+ await renderSet(proj, sets, state, { fold: true })
738
766
 
739
767
  if (commands.length) {
740
768
  proj.push("--- Project commands (runnable targets discovered in the repo root) ---")
@@ -288,6 +288,32 @@ function ruleStoreTarget(p) {
288
288
  return nocase(abs).startsWith(nocase(mem) + "/") ? "memory" : null
289
289
  }
290
290
 
291
+ // ---- protected checks (External Gate) ------------------------------------------
292
+ // Twin of js/hosts/hooks.mjs's `protectedCheck` — keep in step. A project lists the checks its agent
293
+ // must not edit in `.geneseed/protected-checks.txt` at its git root, one repo-relative path per
294
+ // line; the list protects itself. Found from the WRITE TARGET up to the first `.git`, re-read on
295
+ // every write (a short file, read only on a write call). No ask tier here, so a hit is a deny.
296
+ const SENSOR_LIST = ".geneseed/protected-checks.txt"
297
+ function protectedCheck(p) {
298
+ try {
299
+ const target = path.resolve(p)
300
+ let root = path.dirname(target)
301
+ while (!existsSync(path.join(root, ".git"))) {
302
+ const up = path.dirname(root)
303
+ if (up === root) return null
304
+ root = up
305
+ }
306
+ const text = readFileSync(path.join(root, SENSOR_LIST), "utf8")
307
+ const fold = (s) => nocase(s.replace(/\\/g, "/"))
308
+ const rel = fold(path.relative(root, target))
309
+ const entries = [SENSOR_LIST, ...text.split(/\r?\n/).map((l) => l.trim().replace(/[\\/]+$/, ""))
310
+ .filter((l) => l && !l.startsWith("#"))]
311
+ return entries.find((e) => rel === fold(e) || rel.startsWith(fold(e) + "/")) ?? null
312
+ } catch {
313
+ return null // no list (the common case) or an unreadable one: nothing is protected
314
+ }
315
+ }
316
+
291
317
  // Mutation-class tools for the wiki check — wider than WRITE_TOOLS because moving,
292
318
  // renaming, or deleting a protected note is as destructive as overwriting it. Same
293
319
  // substring stance: over-matching the class is harmless (see WRITE_TOOLS note).
@@ -437,6 +463,8 @@ export const GeneseedGuard = async (ctx) => {
437
463
  const p = pickPath(args)
438
464
  if (p && SECRET_RE.some((re) => re.test(p))) { await deny(`write to secret/key file ${p} (Sealed Secrets)`, "law-1"); return }
439
465
  if (p && SECRET_WARN_RE.some((re) => re.test(p))) log(`WARN: writing ${p} — keep secrets out of tracked files (Sealed Secrets)`)
466
+ const sensor = p && protectedCheck(p)
467
+ if (sensor) { await deny(`${p} is a protected check (${sensor}, listed in ${SENSOR_LIST}) — a check the agent can edit is not a check (External Gate)`, "rigor-5"); return }
440
468
  const store = p && ruleStoreTarget(p)
441
469
  if (store && !RULE_STORE_BUMPED.has(p)) {
442
470
  RULE_STORE_BUMPED.add(p)
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The generator's PRE-RENAME path, kept as a forwarder to `bin/build-driver.mjs`.
4
+ *
5
+ * WHY A RENAMED FILE STILL HAS TO EXIST. `geneseed upgrade` runs the updater it started with:
6
+ * its module graph is loaded before the `git pull`, so an updater older than the rename
7
+ * (5c970dc9, 2026-08) pulls the new tree and then spawns `bin/geneseed.mjs` to rebuild —
8
+ * a file the tree it just pulled no longer has. The doctor gate passes (it spawns
9
+ * `geneseed-cli.mjs`, which never moved), and the rebuild dies with MODULE_NOT_FOUND and
10
+ * `[E-BUILD]`. An old web daemon's build jobs spawn the same path. Nothing in this tree
11
+ * calls it, and it has no npm name on purpose: it is a landing pad for old processes, not a
12
+ * fourth entry point.
13
+ *
14
+ * Importing the driver entry runs it on this process's argv, which is the argv the old
15
+ * caller passed — so the forwarding is the import and nothing else.
16
+ */
17
+ import './build-driver.mjs';
@@ -4,18 +4,18 @@ order: 12
4
4
  title: "Exclusions"
5
5
  kind: "concept"
6
6
  section: "Configuration"
7
- description: "Two ways to switch things off: one rule, or every hook in a folder."
7
+ description: "Three ways to switch things off: one rule, one skill, or every hook in a folder."
8
8
  link: {"hash": "#/harness", "label": "Manage in Harness →"}
9
9
  ---
10
- Geneseed has two different ways to switch something off, and they work at different scales:
10
+ Geneseed has three ways to switch something off, and they work at different scales:
11
11
 
12
- | | Sovereign repo | Per-rule exclusion |
13
- | --- | --- | --- |
14
- | **Switches off** | the whole global install, inside one folder | one doctrine rule, everywhere the install applies |
15
- | **Scope** | a folder and everything under it | the install |
16
- | **Applies to** | **global** installs only | any install |
17
- | **Set with** | `geneseed exclude add <path>` | `--exclude-rules` at build time |
18
- | **Takes effect** | on the next tool call, no rebuild | on rebuild |
12
+ | | Sovereign repo | Per-rule exclusion | Per-skill exclusion |
13
+ | --- | --- | --- | --- |
14
+ | **Switches off** | the whole global install, inside one folder | one doctrine rule, everywhere the install applies | one skill, everywhere the install applies |
15
+ | **Scope** | a folder and everything under it | the install | the install |
16
+ | **Applies to** | **global** installs only | any install | any install |
17
+ | **Set with** | `geneseed exclude add <path>` | `--exclude-rules` at build time | `--exclude-skills` at build time |
18
+ | **Takes effect** | on the next tool call, no rebuild | on rebuild | on rebuild |
19
19
 
20
20
  > **You already know this:** `.gitignore` versus disabling one lint rule. One makes a tool ignore a whole folder; the other keeps the tool running everywhere with one check turned off.
21
21
 
@@ -55,3 +55,15 @@ geneseed build --emit claude-global --exclude-rules "process 7"
55
55
  ```
56
56
 
57
57
  The install records it (`Excluded rules: process 7` beside its pack list) and every rebuild and upgrade keeps it. The always-on Rules and the Ethos cannot be excluded. Picking packs and rules is covered in [Doctrine packs and single rules](../guides/setup-doctrines.md); what the layers are is in [Rules](rules.md).
58
+
59
+ ### Per-skill exclusion — skills you never use
60
+
61
+ Every installed skill costs a line in the host's skill listing, every session. Skills you never use can be left out of an install:
62
+
63
+ ```bash
64
+ geneseed build --emit claude-global --exclude-skills "bruno-test-writer,bruno-collection-generator,react-view-transitions"
65
+ ```
66
+
67
+ The skill's files are not written, its catalogue row goes, and an OpenCode `/command` that runs it goes too. The install records the list (`Excluded skills: …` under its skill section), and every rebuild and upgrade keeps it. `--exclude-skills none` puts them all back.
68
+
69
+ A few skills cannot be excluded, because the harness itself links to them by path — `council`, `rule`, `skill-forge`, `workflow` and a handful more. The flag names them when it refuses one. Skills you installed yourself are not Geneseed's to exclude: remove those from the host's skills folder directly.
@@ -36,6 +36,7 @@ Geneseed writes its hooks into the host's settings file. Every hook calls one pr
36
36
  **rule-gate.** Runs before every file write.
37
37
  - Content that looks like a credential (a cloud access key, a GitHub or Anthropic token, a Slack token, a private-key block) going into any file other than `.env*` gets a prompt citing *Sealed Secrets*.
38
38
  - A write to `user-rules.md`, `MEMORY.md` or a file in the install's `memory/` gets a prompt citing *Persist Insight*: whether something is a standing rule or a fact to remember is your call, settled through the `rule` [skill](skills.md).
39
+ - A write to a check the project protects gets a prompt citing *External Gate* (on Bob, it is blocked). A project opts in with `.geneseed/protected-checks.txt` at its git root: one repo-relative file or folder per line, `#` for comments — the checks and tests the agent is judged by. A check the agent can edit is not a check. The list protects itself. Only file-write tools are seen: a shell `sed -i` or redirect is not, so run the same checks in CI as the boundary that holds.
39
40
  - Ordinary edits pass untouched.
40
41
 
41
42
  **learn.** Takes the tail of the transcript and asks a model to distil what is worth keeping into one-fact-per-file entries under `memory/`, skipping duplicates. It needs `GENESEED_LLM` set to a model command, for example:
@@ -21,6 +21,9 @@ brick's behalf. This applies to `plan` and `review` as much as to the newer bric
21
21
  brick whose finding the next brick needs writes it to a file in the OS temp directory and reports
22
22
  that file's path with the outcome.
23
23
 
24
+ One exception keeps the verdict fresh: each `test` report replaces every earlier `test` note, so a
25
+ repair reads only the findings for the code as it stands, never a trace from an older version.
26
+
24
27
  ## `description` and `rules`: what a template is for, and what every brick must do
25
28
 
26
29
  A template's JSON carries two prose fields, and they are never confused:
@@ -47,6 +47,17 @@ all gets exactly 1 — no re-entry — which is also why **every cycle inside an
47
47
  named inner loop** in the graph JSON: `geneseed loop check` (and `loop init`, which runs the same
48
48
  check) refuses a graph with an undeclared cycle inside an iteration, before a single node runs.
49
49
 
50
+ **A ring's `max` is also its own repair budget.** The edge that returns to a ring's first node
51
+ from inside it — `test` → `apply` — is charged to the smallest ring holding both ends, and that
52
+ ring stops at its `max` even when a wider ring raises the node's shared budget. The shipped test
53
+ rings (`apply-test`, `fix-test`, `write-run`, `stabilise`) are `max: 1`: one repair, then the
54
+ attempt is discarded for one fresh try, then the loop stops for you. A second revision makes the
55
+ result worse, not better — 0.82 correct after one, 0.673 after two (arXiv 2607.24604).
56
+
57
+ **`error` always stops.** A brick may declare the outcome `error` without an edge for it; the
58
+ engine sends it to `$stop`. The shipped `test` brick reports it when a check crashed or its
59
+ evidence was never wired — no verdict is not a failure to repair.
60
+
50
61
  ## Limits
51
62
 
52
63
  - **No parallel nodes.** The graph runs one node at a time; there is no fan-out.
@@ -11,7 +11,7 @@ Once the harness is installed for OpenCode (global or per repo), OpenCode can ru
11
11
 
12
12
  ```
13
13
  opencode run "review the staged diff and list any correctness bugs" # one shot, prints to stdout
14
- opencode run -m anthropic/claude-sonnet-4-5 "…" # pin a model for this run
14
+ opencode run -m anthropic/claude-sonnet-5 "…" # pin a model for this run
15
15
  cat issue.md | opencode run "triage this and propose a fix plan" # pipe input in
16
16
  ```
17
17
 
@@ -12,7 +12,7 @@ description: "The build command to run in a script, a CI job, or a shell with no
12
12
  geneseed build --emit opencode-global --theme neutral
13
13
  ```
14
14
 
15
- `geneseed build` takes the same flags as the generator `geneseed-build`. `--emit` picks the host and its scope: `files`, `opencode`, `opencode-global`, `claude`, `claude-global`, `bob`, `bob-global`, `openclaude` or `openclaude-global`. A `-global` emit writes into the tool's own config directory. A project emit writes into the repo you name with `--out <repo> --root <repo>`. All the other flags (`--theme`, `--footprint`, `--posture`, `--mode`, `--trust`, `--doctrines`, `--exclude-rules`) are covered in [Choose your setup](choose-your-setup.md) and the two pages after it.
15
+ `geneseed build` takes the same flags as the generator `geneseed-build`. `--emit` picks the host and its scope: `files`, `opencode`, `opencode-global`, `claude`, `claude-global`, `bob`, `bob-global`, `openclaude` or `openclaude-global`. A `-global` emit writes into the tool's own config directory. A project emit writes into the repo you name with `--out <repo> --root <repo>`. All the other flags (`--theme`, `--footprint`, `--posture`, `--mode`, `--trust`, `--doctrines`, `--exclude-rules`) are covered in [Choose your setup](choose-your-setup.md) and the two pages after it. `--exclude-skills` leaves skills you never use out of the install; see [Exclusions](../concepts/exclusions.md).
16
16
 
17
17
  To test a build before writing anything, use `geneseed validate` with the same flags. See [Verify](verify.md).
18
18
 
@@ -21,11 +21,11 @@ The context step sorts every doc it finds into one of two lists:
21
21
 
22
22
  With no configuration, the context step finds these files:
23
23
 
24
- - **Eager:** root `AGENTS.md`, `AGENT.md`, `CLAUDE.md` and `.cursorrules`, plus `README.md` and `CONTRIBUTING.md`. The root file your tool already loads by itself is skipped, so you never pay for it twice.
25
- - **Lazy:** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/` and `ADR/`, monorepo `packages/*/README.md` and `apps/*/README.md`, and any other root `*.md`.
24
+ - **Eager:** root `AGENTS.md`, `AGENT.md`, `CLAUDE.md` and `.cursorrules`, plus `README.md` and `CONTRIBUTING.md` (or their AsciiDoc `.adoc` twins). The root file your tool already loads by itself is skipped, so you never pay for it twice.
25
+ - **Lazy:** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/` and `ADR/`, monorepo `packages/*/README.md` and `apps/*/README.md`, and any other root `*.md`. AsciiDoc docs (`.adoc`) are found everywhere a `.md` is.
26
26
  - **Never scanned:** `node_modules`, `.git`, `dist`, `build`, `vendor`, `.next`, `target`, `.venv`, `__pycache__`, `.opencode` and `.harness`.
27
27
 
28
- Eager files share a 48 KB budget per session. An eager file that would go over the budget is listed instead, to be read on demand.
28
+ On Claude Code, Bob and OpenClaude the whole injection stays under 9,000 characters, because Claude Code hands the model only a short preview of a hook output past about 10,000 and keeps the rest in a file. Your session files come first, then eager files whole; an eager file that does not fit is listed instead, to be read on demand. On OpenCode eager files share a 48 KB budget. On every host, three or more docs in one folder are listed as that folder with a count (`docs/guides/ — 30 docs`), not file by file.
29
29
 
30
30
  On OpenCode the context plugin does this work. On Claude Code, Bob and OpenClaude, the session-start hook does it (`geneseed-hook context`). Both read the same override file, described next.
31
31
 
@@ -6,7 +6,7 @@ kind: "concept"
6
6
  section: "Set up"
7
7
  description: "Let the agent read PDF, Word, PowerPoint, Excel and HTML through a converter."
8
8
  ---
9
- The agent's doc discovery only sees markdown. The `ingest` skill teaches the agent to convert a PDF, Word, PowerPoint, Excel or HTML file, or a URL, to markdown before reading it. Install one converter and the skill uses it:
9
+ The agent's doc discovery sees markdown (`.md`) and AsciiDoc (`.adoc`) only. The `ingest` skill teaches the agent to convert a PDF, Word, PowerPoint, Excel or HTML file, or a URL, to markdown before reading it. Install one converter and the skill uses it:
10
10
 
11
11
  - **MarkItDown**: the broadest. Use [its MCP server](mcp-markitdown.md) (preferred on a host with MCP: nothing to install per call, one cheap tool), or `pip install markitdown`.
12
12
  - **Pandoc**: excellent for Office and HTML, and a single binary.
@@ -10,8 +10,8 @@ One of the [OpenCode plugins](opencode-plugins.md). Its switches are also listed
10
10
 
11
11
  **What it does.** Puts your project's documentation (and your machine wiki) into the agent's context before your first turn, so the agent never has to be trusted to read it. It needs **no per-repo file**: it discovers docs by convention.
12
12
 
13
- - **Injected in full (eager):** root `README.md`, `CONTRIBUTING.md`, `.cursorrules`, `user-rules.md`, `PROFILE.md`. Budget-capped; an oversized file is listed instead, never silently truncated. (`AGENT.md`, `AGENTS.md` and `CLAUDE.md` are not injected — OpenCode already loads them.)
14
- - **Listed (lazy, path + first heading, read on demand):** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/`, and `packages/*/README.md` / `apps/*/README.md` in a monorepo. `node_modules`, `.git`, `dist`, `build`, `vendor` and similar are never scanned.
13
+ - **Injected in full (eager):** root `README.md`, `CONTRIBUTING.md` (or `README.adoc`, `CONTRIBUTING.adoc`), `.cursorrules`, `user-rules.md`, `PROFILE.md`. Budget-capped; an oversized file is listed instead, never silently truncated. (`AGENT.md`, `AGENTS.md` and `CLAUDE.md` are not injected — OpenCode already loads them.)
14
+ - **Listed (lazy, path + first heading, read on demand):** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/`, and `packages/*/README.md` / `apps/*/README.md` in a monorepo, AsciiDoc `.adoc` docs included. `node_modules`, `.git`, `dist`, `build`, `vendor` and similar are never scanned.
15
15
  - **Machine wiki:** the wikis declared in `wiki.jsonc` ride in the same block, on the same budgets. See [Wiki](../guides/wiki.md).
16
16
  - **Explicit manifest:** `$GENESEED_CONTEXT`, `.harness/context.json` or a root `context.json` takes over from discovery; `"extend": true` layers it on top instead. See [Project context](../guides/project-context.md).
17
17
 
@@ -10,7 +10,7 @@ One of the [OpenCode plugins](opencode-plugins.md). [Environment variables](envi
10
10
 
11
11
  **What it does.** Enforces the safety rules at the tool boundary — before a tool call runs, not after. High-confidence patterns only, so legitimate work is not caught.
12
12
 
13
- - **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root (*Deletion Is Deliberate*), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `wiki.jsonc`.
13
+ - **Blocks** writes to private-key and credential files (*Sealed Secrets*), catastrophic shell commands such as deleting the filesystem root (*Deletion Is Deliberate*), and any change — write, move, rename, delete — under a wiki folder declared `protected` in `wiki.jsonc`, and writes to a check listed in the project's `.geneseed/protected-checks.txt` (*External Gate* — see [Hooks](../concepts/hooks.md)).
14
14
  - **Warns** (logged, allowed) on `.env` writes and force-push.
15
15
  - **Speed-bumps** the *first* write to `user-rules.md` or a memory file: it is refused once, naming *Persist Insight* — whether something is a standing rule or a fact to remember is your call, settled through the `rule` skill. The re-issued write goes through.
16
16
 
@@ -35,11 +35,13 @@ pointer to the host's own inventory instead, ~1.9k tokens lighter. A session fil
35
35
  byte-identical to its seed is skipped — it says nothing — so a fresh install injects none
36
36
  of them; until 2026-10 the hook read them from the repo root, where a Claude-shaped install
37
37
  never puts them, and none reached the session at all. The
38
- eager-injection path is budget-capped identically everywhere — 16 KB per file
39
- (cut at a line break, with a marker saying so) and 48 KB per session (files past
40
- the budget are listed lazy with the reason) — so a 40k-character README no
41
- longer leaves whole every session, and growing Memory degrades every host the
42
- same way instead of one silently falling behind. The hook also **never injects
38
+ eager-injection path is budget-capped on every host. The SessionStart hook (Claude Code,
39
+ Bob, OpenClaude) keeps its whole payload under 9,000 characters: Claude Code persists a
40
+ hook output past ~10k to a file and shows the model a 2 KB preview, so until 2026-10 a
41
+ doc-heavy repo's context was cut off unannounced. Session files come first, eager docs
42
+ are injected whole or listed lazy with the reason. The OpenCode plugin has no such cap
43
+ and keeps 16 KB per file / 48 KB per session. Both fold a lazy listing: three or more
44
+ docs in one folder become one `dir/ — N docs` line. The hook also **never injects
43
45
  the root file the host loads by itself** (`CLAUDE.md` on Claude Code,
44
46
  `AGENTS.md` on Bob, `AGENT.md`/`AGENTS.md`/`CLAUDE.md` on OpenCode):
45
47
  until 2026-09 it did, and the whole harness was paid twice per session.
@@ -106,11 +108,12 @@ token counter also includes, none of which Geneseed controls:
106
108
  2. **Your repo's docs, injected eagerly** — the context delivery
107
109
  (plugin on OpenCode, SessionStart hook on Claude Code / Bob / OpenClaude) discovers and
108
110
  injects `README.md`, `CONTRIBUTING.md`, and files under `docs/`, up to the
109
- 48 KB budget (≈12k tokens). A doc-heavy repo fills it.
111
+ budget: 9,000 characters for the whole hook payload (≈2.3k tokens), 48 KB on
112
+ OpenCode (≈12k tokens).
110
113
  3. **Wiki eager entries** plus the lazy listing of the rest, if a wiki is
111
114
  configured.
112
115
  4. **Grown Memory/Notebook** — the emitted starter set is ~1.2k, but both
113
- accumulate with use (same 48 KB shared budget).
116
+ accumulate with use (same shared budget).
114
117
  5. **Other instruction files the host also loads** — a global install plus a
115
118
  per-repo install, a personal `~/.claude/CLAUDE.md`, or MCP servers each add
116
119
  their own share.
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "3.15.0"
5
+ "version": "3.17.0"
6
6
  }
package/js/build/args.mjs CHANGED
@@ -10,7 +10,9 @@ import { existsSync, readFileSync } from 'node:fs';
10
10
  import { parseArgs as nodeParseArgs } from 'node:util';
11
11
  import { withPlatformNewlines } from '../lib/fs.mjs';
12
12
  import path from 'node:path';
13
- import { CONFIG, discoverNames, PACK_ORDER, knownRuleIds } from './source.mjs';
13
+ import {
14
+ CONFIG, discoverNames, PACK_ORDER, knownRuleIds, knownSkillIds, linkedSkillIds,
15
+ } from './source.mjs';
14
16
  // The loop engine's trust presets — the `--trust` choices. `score.mjs` imports nothing, which is
15
17
  // what lets it sit inside this module's `child_process` ban.
16
18
  import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
@@ -30,7 +32,7 @@ const EMITS = ['files', 'opencode', 'opencode-global', 'claude', 'claude-global'
30
32
  export function configDefaults() {
31
33
  const d = {
32
34
  theme: 'neutral', posture: 'peer', mode: 'direct', trust: DEFAULT_PRESET,
33
- doctrines: [...PACK_ORDER], excludeRules: [],
35
+ doctrines: [...PACK_ORDER], excludeRules: [], excludeSkills: [],
34
36
  };
35
37
  if (!existsSync(CONFIG)) return d;
36
38
  try {
@@ -93,7 +95,7 @@ export function configDefaults() {
93
95
  */
94
96
  const VALUED = {
95
97
  '--theme': 'theme', '--posture': 'posture', '--mode': 'mode', '--trust': 'trust',
96
- '--doctrines': 'doctrines', '--exclude-rules': 'excludeRules',
98
+ '--doctrines': 'doctrines', '--exclude-rules': 'excludeRules', '--exclude-skills': 'excludeSkills',
97
99
  '--out': 'out', '--target': 'out', '--emit': 'emit',
98
100
  '--footprint': 'footprint', '--root': 'root', '--config-dir': 'cfgDir',
99
101
  };
@@ -177,6 +179,33 @@ function parseExcludeRules(value) {
177
179
  return [...new Set(out)].sort();
178
180
  }
179
181
 
182
+ /**
183
+ * `--exclude-skills "bruno-test-writer,openscad"` -> the cfg's skill exclusion array.
184
+ *
185
+ * Refused, by name: a skill this checkout does not ship (a typo would exclude nothing in
186
+ * silence), and a skill the constitution or another skill links to by path — excluding it
187
+ * would ship that link dead (`linkedSkillIds`). Sorted and deduped for marker stability.
188
+ */
189
+ function parseExcludeSkills(value) {
190
+ const s = value.trim();
191
+ if (s === 'none' || s === '') return [];
192
+ const known = knownSkillIds();
193
+ const linked = linkedSkillIds();
194
+ const out = [];
195
+ for (const name of s.split(',').map((x) => x.trim()).filter(Boolean)) {
196
+ if (!known.includes(name)) {
197
+ die(2, `argument --exclude-skills: unknown skill '${name}' (choose from `
198
+ + `${known.filter((k) => !linked.includes(k)).join(', ')}, or 'none')`);
199
+ }
200
+ if (linked.includes(name)) {
201
+ die(2, `argument --exclude-skills: '${name}' cannot be excluded — the harness links to it `
202
+ + `by path (protected: ${linked.join(', ')})`);
203
+ }
204
+ out.push(name);
205
+ }
206
+ return [...new Set(out)].sort();
207
+ }
208
+
180
209
  /**
181
210
  * `--doctrines craft,rigor` / `--doctrines none` -> the cfg's pack array.
182
211
  *
@@ -271,6 +300,7 @@ export function parseArgs(argv, defaults) {
271
300
  trust: defaults.trust,
272
301
  doctrines: defaults.doctrines,
273
302
  excludeRules: defaults.excludeRules,
303
+ excludeSkills: defaults.excludeSkills ?? [],
274
304
  out: 'Harness', emit: 'files', footprint: 'lean', root: null,
275
305
  syncThemes: false, validateOnly: false, verbose: false,
276
306
  };
@@ -314,6 +344,7 @@ export function parseArgs(argv, defaults) {
314
344
  // already the array `configDefaults` built, and re-parsing an array would stringify it.
315
345
  if (typeof args.doctrines === 'string') args.doctrines = parseDoctrines(args.doctrines);
316
346
  if (typeof args.excludeRules === 'string') args.excludeRules = parseExcludeRules(args.excludeRules);
347
+ if (typeof args.excludeSkills === 'string') args.excludeSkills = parseExcludeSkills(args.excludeSkills);
317
348
  return args;
318
349
  }
319
350
 
@@ -596,16 +596,17 @@ const PROJECT_EMITS = {
596
596
  * mode readers return `null` for "undetectable" too, which an explicit `posture: null` would
597
597
  * carry straight past the parameter default.
598
598
  */
599
- function axesCfg({ posture, mode, trust, doctrines, excludeRules }) {
599
+ function axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }) {
600
600
  return makeCfg({
601
601
  ...(posture ? { posture } : {}), ...(mode ? { mode } : {}), ...(trust ? { trust } : {}),
602
602
  ...(doctrines ? { doctrines } : {}), ...(excludeRules ? { excludeRules } : {}),
603
+ ...(excludeSkills ? { excludeSkills } : {}),
603
604
  });
604
605
  }
605
606
 
606
607
  export function emitProjectInto(host, {
607
608
  theme, out, root, footprint = 'full', posture = null, mode = null, trust = null,
608
- doctrines = null, excludeRules = null,
609
+ doctrines = null, excludeRules = null, excludeSkills = null,
609
610
  }) {
610
611
  const emit = PROJECT_EMITS[host];
611
612
  // The axes ride the same rail into every `*Into`, and for the same reason: a drift reader
@@ -613,7 +614,7 @@ export function emitProjectInto(host, {
613
614
  // edited on every narrowed install, and `validate --doctrines craft` that ignored its flag
614
615
  // checked an all-packs render nobody asked for.
615
616
  return withPlatformNewlines(() => emit(
616
- axesCfg({ posture, mode, trust, doctrines, excludeRules }), { theme, footprint, root }, out,
617
+ axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }), { theme, footprint, root }, out,
617
618
  ));
618
619
  }
619
620
 
@@ -627,15 +628,15 @@ export function emitProjectInto(host, {
627
628
  */
628
629
  export function buildInto({
629
630
  theme, out, footprint = 'lean', posture = null, mode = null, trust = null, doctrines = null,
630
- excludeRules = null,
631
+ excludeRules = null, excludeSkills = null,
631
632
  }) {
632
- return withPlatformNewlines(() => build(axesCfg({ posture, mode, trust, doctrines, excludeRules }),
633
+ return withPlatformNewlines(() => build(axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }),
633
634
  theme, out, { footprint, nativeCatalog: false }));
634
635
  }
635
636
 
636
637
  export function emitGlobalInto(host, {
637
638
  theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
638
- excludeRules = null,
639
+ excludeRules = null, excludeSkills = null,
639
640
  }) {
640
641
  // `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
641
642
  // raising, because the host comes from a marker file a user can edit.
@@ -651,7 +652,7 @@ export function emitGlobalInto(host, {
651
652
  // edited on every install that chose a register, narrowed its packs or excluded a rule —
652
653
  // the same scar the footprint left in `diffCollect`, one axis over each time.
653
654
  return withPlatformNewlines(() => emit(
654
- axesCfg({ posture, mode, trust, doctrines, excludeRules }),
655
+ axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }),
655
656
  { theme, footprint, root: null, cfgDir }, out,
656
657
  ));
657
658
  }
@@ -40,7 +40,7 @@ import { DEFAULT_PRESET } from '../loop/score.mjs';
40
40
  import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
41
41
  import {
42
42
  EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
43
- excludedRulesOfDir, footprintOfDir, installState, installTargets, modeOfDir, postureOfDir,
43
+ excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, installState, installTargets, modeOfDir, postureOfDir,
44
44
  readMaybe, themeOfDir, trustOfDir,
45
45
  } from '../hosts/installs.mjs';
46
46
  import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
@@ -97,7 +97,7 @@ function sysExit(msg) {
97
97
  const BUILD_FORWARD = [
98
98
  ['theme', '--theme'], ['emit', '--emit'], ['footprint', '--footprint'],
99
99
  ['posture', '--posture'], ['mode', '--mode'], ['trust', '--trust'], ['doctrines', '--doctrines'],
100
- ['excludeRules', '--exclude-rules'], ['out', '--out'], ['root', '--root'],
100
+ ['excludeRules', '--exclude-rules'], ['excludeSkills', '--exclude-skills'], ['out', '--out'], ['root', '--root'],
101
101
  ['configDir', '--config-dir'],
102
102
  ];
103
103
 
@@ -140,7 +140,7 @@ export function cmdBuild(args) {
140
140
  */
141
141
  export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
142
142
  posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
143
- excludeRules = null, trust = DEFAULT_PRESET) {
143
+ excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null) {
144
144
  const argv = ['--theme', theme, '--emit', emit];
145
145
  if (!emit.endsWith('-global')) {
146
146
  if (out) argv.push('--out', out);
@@ -164,6 +164,10 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
164
164
  if (Array.isArray(excludeRules)) {
165
165
  argv.push('--exclude-rules', excludeRules.length ? excludeRules.join(',') : 'none');
166
166
  }
167
+ // Skills elide when nothing is excluded: the driver's default IS nothing (no config key
168
+ // feeds it), so the flag's absence and `none` are the same answer, and every argv that
169
+ // excludes nothing stays the bytes it was before the flag existed.
170
+ if (excludeSkills?.length) argv.push('--exclude-skills', excludeSkills.join(','));
167
171
  return argv;
168
172
  }
169
173
 
@@ -217,9 +221,11 @@ export function installProfile(host, scope, root) {
217
221
  // dropped it would quietly hand back every rule its owner switched off, and `process 5` is
218
222
  // one of them. `null` (no marker) stays `null` so the argv omits the flag, as it always did.
219
223
  const excludeRules = excludedRulesOfDir(root);
224
+ // Skills left out stay left out: an upgrade that forgot them would put them all back.
225
+ const excludeSkills = excludedSkillsOfDir(root);
220
226
  const out = scope === 'global' ? null : root;
221
227
  const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
222
- PACK_ORDER, excludeRules, trust);
228
+ PACK_ORDER, excludeRules, trust, excludeSkills);
223
229
  // A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
224
230
  // honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
225
231
  // same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
@@ -227,7 +233,7 @@ export function installProfile(host, scope, root) {
227
233
  if (scope === 'global') argv.push('--config-dir', root);
228
234
  return {
229
235
  host, scope, root, state: installState(root, host, scope),
230
- emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, argv,
236
+ emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills, argv,
231
237
  };
232
238
  }
233
239