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.
- package/CHANGELOG.md +35 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +36 -8
- package/adapters/opencode/plugins/geneseed-guard.js +28 -0
- package/bin/geneseed.mjs +17 -0
- package/docs/concepts/exclusions.md +21 -9
- package/docs/concepts/hooks.md +1 -0
- package/docs/concepts/loop-notes.md +3 -0
- package/docs/concepts/loops.md +11 -0
- package/docs/guides/headless.md +1 -1
- package/docs/guides/install-scripted.md +1 -1
- package/docs/guides/project-context.md +3 -3
- package/docs/guides/read-non-markdown.md +1 -1
- package/docs/reference/plugin-context.md +2 -2
- package/docs/reference/plugin-guard.md +1 -1
- package/docs/token-footprint.md +10 -7
- package/harness.config.json +1 -1
- package/js/build/args.mjs +34 -3
- package/js/build/driver.mjs +8 -7
- package/js/build/generate.mjs +11 -5
- package/js/build/render.mjs +33 -3
- package/js/build/source.mjs +37 -1
- package/js/cli-table.json +15 -0
- package/js/hosts/hooks-context.mjs +112 -56
- package/js/hosts/hooks.mjs +34 -1
- package/js/hosts/installs.mjs +25 -2
- package/js/hosts/opencode.mjs +2 -0
- package/js/hosts/settings.mjs +2 -1
- package/js/inspect/checks-constitution.mjs +2 -2
- package/js/inspect/diff.mjs +4 -1
- package/js/inspect/status.mjs +2 -1
- package/js/inspect/validate.mjs +3 -1
- package/js/loop/graph.mjs +2 -1
- package/js/loop/state.mjs +30 -1
- package/js/maintain/setup.mjs +11 -4
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +5 -3
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +1 -1
- package/src/agents/docs.md +5 -0
- package/src/bricks/apply.md +3 -0
- package/src/bricks/docs-update.md +2 -0
- package/src/bricks/test.md +17 -7
- package/src/doctrines/process.md +9 -5
- package/src/loops/api-endpoint.json +1 -1
- package/src/loops/bugfix.json +1 -1
- package/src/loops/ci-repair.json +1 -1
- package/src/loops/deps-upgrade.json +1 -1
- package/src/loops/enforce-architecture-rule.json +1 -1
- package/src/loops/feature.json +1 -1
- package/src/loops/fix-flaky-tests.json +1 -1
- package/src/loops/legacy-refactor.json +1 -1
- package/src/loops/legacy-tests.json +1 -1
- package/src/loops/refactor.json +1 -1
- package/src/loops/remove-dead-code.json +1 -1
- package/src/loops/spec-first-feature.json +1 -1
- package/src/loops/tdd.json +1 -1
- package/src/loops/update-contract.json +1 -1
- package/src/skills/document-project.md +91 -22
- package/web/dist/assets/{Activity-kpWxd70F.js → Activity-DonKN1pi.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Ccr942VL.js → ActivityDetail-e47a3cdE.js} +1 -1
- package/web/dist/assets/{Dashboard-D1lE7jcg.js → Dashboard-Cr7i0PsO.js} +2 -2
- package/web/dist/assets/{Docs-BHyaggFy.js → Docs-Lne3nuzD.js} +1 -1
- package/web/dist/assets/{GreenhouseView-Sc_UDMOE.js → GreenhouseView-4CbIuTpl.js} +1 -1
- package/web/dist/assets/{Installs-Gw-pJ3Zi.js → Installs-CIzbV1Ks.js} +1 -1
- package/web/dist/assets/{JournalView-_gxpsxXi.js → JournalView-BlBfuvzj.js} +1 -1
- package/web/dist/assets/{Laws-S9fq1r9T.js → Laws-B-pjcjHr.js} +1 -1
- package/web/dist/assets/{Library-MBxadRzn.js → Library-x8Nzg6yq.js} +1 -1
- package/web/dist/assets/{Loops-Da_JhlzM.js → Loops-DIOxW1xH.js} +1 -1
- package/web/dist/assets/{OperatorHudView--TkvDybH.js → OperatorHudView-D7S7z7CJ.js} +1 -1
- package/web/dist/assets/{Personal-CTXKHVbV.js → Personal-Ws3v35AB.js} +1 -1
- package/web/dist/assets/{Tabs-B0cdf8k6.js → Tabs-6Z1eziAq.js} +1 -1
- package/web/dist/assets/{drawable-BbWQxe82.js → drawable-ATNeYKbT.js} +1 -1
- package/web/dist/assets/{index-ujKG1ZNf.js → index-CDg5XUhG.js} +2 -2
- 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.
|
|
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", "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
471
|
-
|
|
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)
|
package/bin/geneseed.mjs
ADDED
|
@@ -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: "
|
|
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
|
|
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.
|
package/docs/concepts/hooks.md
CHANGED
|
@@ -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:
|
package/docs/concepts/loops.md
CHANGED
|
@@ -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.
|
package/docs/guides/headless.md
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/docs/token-footprint.md
CHANGED
|
@@ -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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
package/harness.config.json
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
|
package/js/build/driver.mjs
CHANGED
|
@@ -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
|
}
|
package/js/build/generate.mjs
CHANGED
|
@@ -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
|
|