geneseed 3.14.0 → 3.16.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 +66 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +45 -11
- package/adapters/opencode/plugins/geneseed-guard.js +16 -4
- package/adapters/opencode/plugins/geneseed-learn.js +37 -11
- package/adapters/opencode/plugins/geneseed-notify.js +37 -19
- package/adapters/opencode/plugins/geneseed-workflow.js +3 -1
- package/adapters/opencode/workflows/_runtime.js +16 -3
- package/bin/geneseed-cli.mjs +22 -20
- package/bin/geneseed-hook.mjs +6 -3
- package/docs/concepts/exclusions.md +21 -9
- package/docs/guides/install-scripted.md +1 -1
- package/docs/guides/project-context.md +1 -1
- package/docs/token-footprint.md +10 -7
- package/harness.config.json +1 -1
- package/js/README.md +9 -5
- package/js/build/args.mjs +38 -6
- package/js/build/bundle.mjs +10 -10
- package/js/build/driver.mjs +16 -14
- package/js/build/emit-claude.mjs +3 -2
- package/js/build/emit-common.mjs +2 -14
- package/js/build/emit-opencode.mjs +12 -8
- package/js/build/generate.mjs +26 -16
- package/js/build/render.mjs +34 -4
- package/js/build/source.mjs +43 -7
- package/js/build/stubs.mjs +6 -5
- package/js/build/version.mjs +21 -2
- package/js/cli-table.json +15 -0
- package/js/hosts/hooks-context.mjs +85 -34
- package/js/hosts/hooks-prims.mjs +2 -2
- package/js/hosts/hosts.mjs +24 -26
- package/js/hosts/installs.mjs +58 -16
- package/js/hosts/link.mjs +75 -26
- package/js/hosts/memory-files.mjs +13 -4
- package/js/hosts/native.mjs +106 -48
- package/js/hosts/opencode.mjs +46 -36
- package/js/hosts/shim.mjs +4 -2
- package/js/inspect/diff.mjs +9 -7
- package/js/inspect/excludes.mjs +10 -5
- package/js/inspect/status.mjs +20 -22
- package/js/inspect/validate.mjs +5 -3
- package/js/lib/fs.mjs +12 -11
- package/js/lib/json.mjs +4 -4
- package/js/lib/paths.mjs +27 -33
- package/js/lib/proc.mjs +3 -3
- package/js/lib/prompt.mjs +132 -0
- package/js/lib/text.mjs +18 -4
- package/js/lib/udiff.mjs +3 -2
- package/js/maintain/memory.mjs +5 -6
- package/js/maintain/migrate.mjs +4 -3
- package/js/maintain/setup.mjs +20 -135
- package/js/maintain/uninstall.mjs +10 -9
- package/js/maintain/update.mjs +115 -34
- package/js/ui/anim.mjs +8 -12
- package/js/ui/cli.mjs +29 -35
- package/js/ui/menu.mjs +7 -13
- package/js/ui/tui.mjs +14 -123
- package/js/web/actions.mjs +87 -389
- package/js/web/activity.mjs +1 -1
- package/js/web/api.mjs +33 -717
- package/js/web/catalog.mjs +527 -0
- package/js/web/daemon.mjs +21 -3
- package/js/web/handler.mjs +51 -27
- package/js/web/jobs.mjs +24 -2
- package/js/web/routes.mjs +61 -6
- package/js/web/server.mjs +4 -4
- package/js/web/state.mjs +138 -0
- package/js/web/user-files.mjs +339 -0
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +1 -1
- package/web/dist/assets/{Activity-CjRqZnr-.js → Activity-kpWxd70F.js} +1 -1
- package/web/dist/assets/{ActivityDetail-OQTxKBj2.js → ActivityDetail-Ccr942VL.js} +1 -1
- package/web/dist/assets/Dashboard-D1lE7jcg.js +2 -0
- package/web/dist/assets/{Docs-CvOZ8akL.js → Docs-BHyaggFy.js} +3 -3
- package/web/dist/assets/{GreenhouseView-6KDjX8da.js → GreenhouseView-Sc_UDMOE.js} +1 -1
- package/web/dist/assets/Installs-Gw-pJ3Zi.js +2 -0
- package/web/dist/assets/JournalView-_gxpsxXi.js +1 -0
- package/web/dist/assets/{Laws-Du0-Zyp2.js → Laws-S9fq1r9T.js} +1 -1
- package/web/dist/assets/{Library-DOkO0TtB.js → Library-MBxadRzn.js} +1 -1
- package/web/dist/assets/{Loops-Dv_aLByJ.js → Loops-Da_JhlzM.js} +2 -2
- package/web/dist/assets/{OperatorHudView-BPP1dtLz.js → OperatorHudView--TkvDybH.js} +1 -1
- package/web/dist/assets/{Personal-26VPYkK0.js → Personal-CTXKHVbV.js} +1 -1
- package/web/dist/assets/Tabs-B0cdf8k6.js +1 -0
- package/web/dist/assets/api-CxgfYu18.js +1 -0
- package/web/dist/assets/drawable-BbWQxe82.js +1 -0
- package/web/dist/assets/index-ujKG1ZNf.js +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/Dashboard-J-xCe_Gk.js +0 -2
- package/web/dist/assets/Installs-MWuhNjSm.js +0 -2
- package/web/dist/assets/JournalView-C8MrOz2c.js +0 -1
- package/web/dist/assets/Tabs-BzKuFskD.js +0 -1
- package/web/dist/assets/api-D_ixdyEY.js +0 -1
- package/web/dist/assets/drawable-zVVDbsyw.js +0 -1
- package/web/dist/assets/index-KHt3wE_K.js +0 -2
package/bin/geneseed-hook.mjs
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* and each alone would be enough:
|
|
8
8
|
*
|
|
9
9
|
* * The generator driver is under a hard `child_process` ban
|
|
10
|
-
* (`
|
|
10
|
+
* (`tests/unit/hook_cli.test.mjs`'s 'the generator driver still reaches no child-process
|
|
11
|
+
* module'), which is half of the proof that
|
|
11
12
|
* it is a real implementation rather than a passthrough to `build.py`. `learn` MUST
|
|
12
13
|
* spawn — the whole verb is "hand these notes to whatever `$GENESEED_LLM` names".
|
|
13
14
|
* Putting it in that file means either breaking the gate or weakening it.
|
|
@@ -15,7 +16,8 @@
|
|
|
15
16
|
* the Python side; a Node twin needs its own, invoked as `<node> <this> context
|
|
16
17
|
* --root "<cfg>"`.
|
|
17
18
|
* * `bin/build-driver.mjs` parses generator FLAGS (`--emit`, `--theme`, `--footprint`), and
|
|
18
|
-
*
|
|
19
|
+
* 'the driver classifies every emit' (`tests/unit/node_driver.test.mjs`) asserts a
|
|
20
|
+
* partition over them. Verbs
|
|
19
21
|
* are a different shape and would sit awkwardly inside that.
|
|
20
22
|
*
|
|
21
23
|
* WHAT BAKES THIS FILE, SINCE P5b. `bin/build-driver.mjs` writes `<node> <checkout>/bin/
|
|
@@ -123,7 +125,8 @@ async function main(argv) {
|
|
|
123
125
|
if (!spec) {
|
|
124
126
|
// "elsewhere", not a file name: naming `bin/geneseed-cli.mjs` here would mean this entry
|
|
125
127
|
// carrying a copy of its sibling's verb table — the one thing
|
|
126
|
-
// `
|
|
128
|
+
// 'the two entry points carry disjoint verb sets' (`tests/unit/hook_cli.test.mjs`) exists to
|
|
129
|
+
// keep from happening.
|
|
127
130
|
// The COMMAND is not a table: `geneseed` is this package's `bin` entry for the CLI
|
|
128
131
|
// (package.json), it answers every non-hook verb, and it survives the deletion of the
|
|
129
132
|
// interpreter-plus-script invocation this line used to print.
|
|
@@ -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.
|
|
@@ -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
|
|
|
@@ -25,7 +25,7 @@ With no configuration, the context step finds these files:
|
|
|
25
25
|
- **Lazy:** `docs/`, `doc/`, `documentation/`, `architecture/`, `adr/` and `ADR/`, monorepo `packages/*/README.md` and `apps/*/README.md`, and any other root `*.md`.
|
|
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
|
|
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.16.0"
|
|
6
6
|
}
|
package/js/README.md
CHANGED
|
@@ -129,6 +129,7 @@ into files users have committed, so change the test that states it in the same c
|
|
|
129
129
|
| `text.mjs` | String primitives: `codePointLength`, `WHITESPACE`, `parseIntStrict`, `percentDecode` |
|
|
130
130
|
| `udiff.mjs` | `geneseed diff`'s own difflib clone — the alignment IS the printed hunks — plus `splitLines` |
|
|
131
131
|
| `proc.mjs` | One constant: the `windowsHide` flag every spawn in `js/` must reuse |
|
|
132
|
+
| `prompt.mjs` | The line-mode stdin readers — `ask`, `promptLine`, `confirm`, `askChoice` — read a byte at a time so they stop where Enter stops. Shared by `setup`, `uninstall` and `web` without dragging in the wizard |
|
|
132
133
|
| `node-floor.mjs` | The first import of all three binaries: below Node 22.3, one sentence on stderr and exit 1 (0 in the hook) instead of a stack trace |
|
|
133
134
|
|
|
134
135
|
**Before editing:** parse with `parseJson`, never `JSON.parse` — the latter collapses int `1` and
|
|
@@ -202,10 +203,13 @@ the deployed harness as JSON.
|
|
|
202
203
|
| module | owns |
|
|
203
204
|
|---|---|
|
|
204
205
|
| `server.mjs` | `serve()`/`cmdWeb`: UI-build precheck, socket bind, token mint, daemon record wiring |
|
|
205
|
-
| `routes.mjs` |
|
|
206
|
-
| `handler.mjs` | One function per request: dispatch, gzip, Host/CSRF guards, static SPA fallback |
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
206
|
+
| `routes.mjs` | The route tables — `STATE_ROUTES`, `PREFIX_ROUTES`, `POST_ROUTES` (+ its 409 column) — and the inline lists |
|
|
207
|
+
| `handler.mjs` | One function per request: dispatch, gzip, Host/CSRF guards and body cap (before the body is read), static SPA fallback |
|
|
208
|
+
| `state.mjs` | `webState` — the resolved install view every endpoint reads, with its cached inventory and doctor |
|
|
209
|
+
| `catalog.mjs` | The Library read model: inventory, constitution tiers, memory/notebook/wiki/config items, `apiCatalog`/`apiItem`, `NotFound` |
|
|
210
|
+
| `api.mjs` | The dashboard and settings reads: overview, recent, themes, setup, doctor, diff, installs, excludes, loops |
|
|
211
|
+
| `user-files.mjs` | The fingerprint-guarded editors for `user-rules.md` (incl. memory promotion) and `PROFILE.md`, read and write |
|
|
212
|
+
| `actions.mjs` | The other mutating endpoints — memory delete, MCP, excludes, installs, view, restore — plus install/deploy argv |
|
|
209
213
|
| `daemon.mjs` | Detached web start/stop/status/restart, the `.geneseed-web.json` record, and `openUrl` |
|
|
210
214
|
| `jobs.mjs` | `JobManager` and the action→argv table. The only module here that spawns job children |
|
|
211
215
|
| `docs.mjs` | The Docs tab: the `docs/<folder>/` page registry, `?harness=` filtering, link rewriting, the six page kinds |
|
|
@@ -232,4 +236,4 @@ and inline lists are held against a surface list written out in
|
|
|
232
236
|
| a hook verb | `js/hosts/hooks.mjs` (a gate) or its own `js/hosts/hooks-<verb>.mjs` | `claudeHookGroups` in `settings.mjs`, the `VERBS` row (with its `load`) in `bin/geneseed-hook.mjs`. Keep the import graph tiny |
|
|
233
237
|
| a console doc page | `docs/<understand|guides|concepts|reference>/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
|
|
234
238
|
| an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
|
|
235
|
-
| to change a `js/lib/` primitive | `tests/
|
|
239
|
+
| to change a `js/lib/` primitive | `tests/unit/lib_primitives.test.mjs` **first** | the docblock says which Node default it deliberately departs from — change that sentence too, or the next reader restores the default |
|
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';
|
|
@@ -24,13 +26,13 @@ const EMITS = ['files', 'opencode', 'opencode-global', 'claude', 'claude-global'
|
|
|
24
26
|
*
|
|
25
27
|
* The corrupt-file branch is reproduced including its imprecision: the warning says only
|
|
26
28
|
* "using theme 'neutral'", but the branch also resets posture and mode. Matching the
|
|
27
|
-
* Python text
|
|
28
|
-
* byte-for-byte by
|
|
29
|
+
* Python text mattered more than fixing it here, because the two CLIs' stderr was compared
|
|
30
|
+
* byte-for-byte by the former Python golden harness; nothing pins the wording now.
|
|
29
31
|
*/
|
|
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
|
|
|
@@ -325,7 +356,8 @@ export function parseArgs(argv, defaults) {
|
|
|
325
356
|
* `--validate-only` reads exactly the flags `build.py`'s parser already produced, so the port
|
|
326
357
|
* hands the CLI this parser rather than a second one beside it. That is not tidiness: a
|
|
327
358
|
* hand-rolled copy would have its own `--target` alias, its own `choices` lists and its own
|
|
328
|
-
* `-h`, and `
|
|
359
|
+
* `-h`, and `tests/unit/node_driver.test.mjs`'s '--help names every flag the parser takes' gates only
|
|
360
|
+
* this one.
|
|
329
361
|
*
|
|
330
362
|
* `withPlatformNewlines` because the refusal and `-h` paths WRITE: called from `bin/build-driver.mjs`
|
|
331
363
|
* they sit inside `main`'s funnel, and called from the CLI binary they would not.
|
package/js/build/bundle.mjs
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
15
|
-
import { copyFile, readText, writeText } from '../lib/fs.mjs';
|
|
15
|
+
import { copyFile, isDir, isFile, readText, writeText } from '../lib/fs.mjs';
|
|
16
16
|
import { jsonDumpsIndent, parseJson, formatReprAscii, isTruthy } from '../lib/json.mjs';
|
|
17
|
-
import { OWNED_SRC_DIRS, SRC_DIRS_MARKER, TMPL_SPEC_RE
|
|
17
|
+
import { OWNED_SRC_DIRS, SRC_DIRS_MARKER, TMPL_SPEC_RE } from './emit-common.mjs';
|
|
18
18
|
import { SRC_DIR_TOKENS, renderAll } from './render.mjs';
|
|
19
19
|
import {
|
|
20
20
|
ensureBundleGitignore, ensureContextStub, ensureMemoryIndex, ensureNotebookIndex,
|
|
@@ -114,16 +114,9 @@ function safePriorDirName(out, priorName) {
|
|
|
114
114
|
// build
|
|
115
115
|
// ---------------------------------------------------------------------------
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
* `_build_render.build` — render the bundle into `out`.
|
|
119
|
-
*
|
|
120
|
-
* `out` arrives as the STRING Python already resolved, and is echoed into the progress
|
|
121
|
-
* lines verbatim: Python interpolates `str(Path)`, which is backslash-separated on
|
|
122
|
-
* Windows, and re-deriving it here would differ by separator in every message.
|
|
123
|
-
*/
|
|
124
117
|
/**
|
|
125
118
|
* THE PHASE BOUNDARY MARKER — T8, and the whole of what this port owes
|
|
126
|
-
* `tests/
|
|
119
|
+
* `tests/unit/emit_phase_order.test.mjs`.
|
|
127
120
|
*
|
|
128
121
|
* Every emit runs its stages in one order: RENDER* → WIRE* → PRUNE → MANIFEST → VERIFY. RENDER
|
|
129
122
|
* writes files Geneseed owns wholesale; WIRE reconciles files the USER co-owns (the
|
|
@@ -156,6 +149,13 @@ export function phaseLog(phase) {
|
|
|
156
149
|
process.stderr.write(`[geneseed:phase] ${phase}\n`);
|
|
157
150
|
}
|
|
158
151
|
|
|
152
|
+
/**
|
|
153
|
+
* `_build_render.build` — render the bundle into `out`.
|
|
154
|
+
*
|
|
155
|
+
* `out` arrives as the STRING Python already resolved, and is echoed into the progress
|
|
156
|
+
* lines verbatim: Python interpolates `str(Path)`, which is backslash-separated on
|
|
157
|
+
* Windows, and re-deriving it here would differ by separator in every message.
|
|
158
|
+
*/
|
|
159
159
|
export function build(cfg, themeName, out, { footprint = 'full', nativeCatalog = false } = {}) {
|
|
160
160
|
phaseLog('RENDER');
|
|
161
161
|
const { theme, items } = renderAll(cfg, themeName, { footprint, nativeCatalog });
|
package/js/build/driver.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { settingsIntegrityCheck } from '../hosts/settings.mjs';
|
|
|
42
42
|
import { hookRunnerEntry } from '../hosts/shim.mjs';
|
|
43
43
|
import { writeText, withPlatformNewlines, isFile } from '../lib/fs.mjs';
|
|
44
44
|
import { parseJson, jsonDumpsIndent } from '../lib/json.mjs';
|
|
45
|
+
import { relPosix } from '../lib/text.mjs';
|
|
45
46
|
// P5c moved these out of this file: `bin/geneseed-cli.mjs` needs the same four resolvers to
|
|
46
47
|
// find a global install, and a resolver that decides WHERE a driver writes is the last thing
|
|
47
48
|
// that should exist twice. golden.py's 259 cells are what made the move safe to attempt.
|
|
@@ -90,7 +91,8 @@ const PRIMARY_AGENT_SRC = path.join(ROOT, 'adapters', 'opencode', 'agents', 'orc
|
|
|
90
91
|
* It is invisible on a machine whose paths are already canonical, which is every machine
|
|
91
92
|
* this ran on until GitHub's Windows runner handed it a `C:\\Users\\RUNNER~1\\...` and the
|
|
92
93
|
* two CLIs printed the same directory under two different names — a 70-byte difference in a
|
|
93
|
-
* line compared BYTE for byte
|
|
94
|
+
* line the former Python suite compared BYTE for byte; `tests/unit/resolve_out.test.mjs` pins
|
|
95
|
+
* it now. The
|
|
94
96
|
* argument to `resolvePath` is made absolute first so its `expanduser` cannot fire: the
|
|
95
97
|
* reference does not expand `~` here, and a bare `~` is a legal directory name. */
|
|
96
98
|
export function resolveOut(raw) {
|
|
@@ -149,10 +151,10 @@ function warnBobGlobalOverProject() {
|
|
|
149
151
|
* would be written into `opencode.json`'s instruction path.
|
|
150
152
|
*/
|
|
151
153
|
function relUnder(out, root) {
|
|
152
|
-
const rel =
|
|
154
|
+
const rel = relPosix(root, out);
|
|
153
155
|
if (rel === '' || rel === '.') return '';
|
|
154
156
|
if (rel.startsWith('..') || path.isAbsolute(rel)) return '';
|
|
155
|
-
return rel
|
|
157
|
+
return rel;
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
/**
|
|
@@ -255,7 +257,7 @@ function projectSurvivors(emitName) {
|
|
|
255
257
|
* RENDER and WIRE already ran in Node before P4; what lived only in Python was the stage
|
|
256
258
|
* either side of them: reading the previous manifest (PRE), pruning what this layer no
|
|
257
259
|
* longer owns, writing the manifest atomically, and the summary line. All five stages now
|
|
258
|
-
* run in one process, in the order `
|
|
260
|
+
* run in one process, in the order `tests/unit/emit_phase_order.test.mjs` pins:
|
|
259
261
|
* RENDER -> WIRE -> PRUNE -> MANIFEST -> VERIFY (opencode has no VERIFY; it writes no
|
|
260
262
|
* settings file of its own).
|
|
261
263
|
*/
|
|
@@ -511,9 +513,8 @@ const claudeShaped = (name) => (cfg, args, out) => {
|
|
|
511
513
|
|
|
512
514
|
/**
|
|
513
515
|
* build.py:437-466 — the POST stage, which writes markers and records the install and
|
|
514
|
-
* wires NOTHING. `
|
|
515
|
-
*
|
|
516
|
-
* the same reason.
|
|
516
|
+
* wires NOTHING. `tests/unit/emit_phase_order.test.mjs`'s 'the post-emit marker stage wires
|
|
517
|
+
* nothing' classifies it, and that is why it stays after the dispatch.
|
|
517
518
|
*
|
|
518
519
|
* `writeText`, never `writeFileSync`: Python's `Path.write_text` opens in text mode with
|
|
519
520
|
* `newline=None` and translates the trailing `\n` to CRLF on Windows. A marker written
|
|
@@ -595,16 +596,17 @@ const PROJECT_EMITS = {
|
|
|
595
596
|
* mode readers return `null` for "undetectable" too, which an explicit `posture: null` would
|
|
596
597
|
* carry straight past the parameter default.
|
|
597
598
|
*/
|
|
598
|
-
function axesCfg({ posture, mode, trust, doctrines, excludeRules }) {
|
|
599
|
+
function axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }) {
|
|
599
600
|
return makeCfg({
|
|
600
601
|
...(posture ? { posture } : {}), ...(mode ? { mode } : {}), ...(trust ? { trust } : {}),
|
|
601
602
|
...(doctrines ? { doctrines } : {}), ...(excludeRules ? { excludeRules } : {}),
|
|
603
|
+
...(excludeSkills ? { excludeSkills } : {}),
|
|
602
604
|
});
|
|
603
605
|
}
|
|
604
606
|
|
|
605
607
|
export function emitProjectInto(host, {
|
|
606
608
|
theme, out, root, footprint = 'full', posture = null, mode = null, trust = null,
|
|
607
|
-
doctrines = null, excludeRules = null,
|
|
609
|
+
doctrines = null, excludeRules = null, excludeSkills = null,
|
|
608
610
|
}) {
|
|
609
611
|
const emit = PROJECT_EMITS[host];
|
|
610
612
|
// The axes ride the same rail into every `*Into`, and for the same reason: a drift reader
|
|
@@ -612,7 +614,7 @@ export function emitProjectInto(host, {
|
|
|
612
614
|
// edited on every narrowed install, and `validate --doctrines craft` that ignored its flag
|
|
613
615
|
// checked an all-packs render nobody asked for.
|
|
614
616
|
return withPlatformNewlines(() => emit(
|
|
615
|
-
axesCfg({ posture, mode, trust, doctrines, excludeRules }), { theme, footprint, root }, out,
|
|
617
|
+
axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }), { theme, footprint, root }, out,
|
|
616
618
|
));
|
|
617
619
|
}
|
|
618
620
|
|
|
@@ -626,15 +628,15 @@ export function emitProjectInto(host, {
|
|
|
626
628
|
*/
|
|
627
629
|
export function buildInto({
|
|
628
630
|
theme, out, footprint = 'lean', posture = null, mode = null, trust = null, doctrines = null,
|
|
629
|
-
excludeRules = null,
|
|
631
|
+
excludeRules = null, excludeSkills = null,
|
|
630
632
|
}) {
|
|
631
|
-
return withPlatformNewlines(() => build(axesCfg({ posture, mode, trust, doctrines, excludeRules }),
|
|
633
|
+
return withPlatformNewlines(() => build(axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }),
|
|
632
634
|
theme, out, { footprint, nativeCatalog: false }));
|
|
633
635
|
}
|
|
634
636
|
|
|
635
637
|
export function emitGlobalInto(host, {
|
|
636
638
|
theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
|
|
637
|
-
excludeRules = null,
|
|
639
|
+
excludeRules = null, excludeSkills = null,
|
|
638
640
|
}) {
|
|
639
641
|
// `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
|
|
640
642
|
// raising, because the host comes from a marker file a user can edit.
|
|
@@ -650,7 +652,7 @@ export function emitGlobalInto(host, {
|
|
|
650
652
|
// edited on every install that chose a register, narrowed its packs or excluded a rule —
|
|
651
653
|
// the same scar the footprint left in `diffCollect`, one axis over each time.
|
|
652
654
|
return withPlatformNewlines(() => emit(
|
|
653
|
-
axesCfg({ posture, mode, trust, doctrines, excludeRules }),
|
|
655
|
+
axesCfg({ posture, mode, trust, doctrines, excludeRules, excludeSkills }),
|
|
654
656
|
{ theme, footprint, root: null, cfgDir }, out,
|
|
655
657
|
));
|
|
656
658
|
}
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
unwireClaudeExcludes, unwireClaudeSettings, wireClaudeExcludes,
|
|
15
15
|
} from '../hosts/settings.mjs';
|
|
16
16
|
import { writeText } from '../lib/fs.mjs';
|
|
17
|
-
import { isTruthy } from '../lib/json.mjs';
|
|
17
|
+
import { get, isDict, isTruthy } from '../lib/json.mjs';
|
|
18
|
+
import { relPosix } from '../lib/text.mjs';
|
|
18
19
|
import { assertSourceComplete, phaseLog } from './bundle.mjs';
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
+
globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
|
|
21
22
|
} from './emit-common.mjs';
|
|
22
23
|
import { renderAll, renderFile } from './render.mjs';
|
|
23
24
|
import {
|
package/js/build/emit-common.mjs
CHANGED
|
@@ -10,18 +10,11 @@
|
|
|
10
10
|
* `emit-claude.mjs` do that; `bundle.mjs` renders the bundle they read from.
|
|
11
11
|
*/
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
import { copyFile, writeText,
|
|
14
|
-
import {
|
|
13
|
+
import { copyFile, writeText, isDir } from '../lib/fs.mjs';
|
|
14
|
+
import { relPosix } from '../lib/text.mjs';
|
|
15
15
|
import { SRC_DIR_TOKENS, destRel } from './render.mjs';
|
|
16
16
|
import { mkdirSync, readdirSync, cpSync } from 'node:fs';
|
|
17
17
|
|
|
18
|
-
// `isFile`/`isDir`/`get`/`isDict` are owned by `js/lib` now (single owner across `hosts/`,
|
|
19
|
-
// `build/`, `inspect/` and `web/` — layering forbids `build/` reaching into `inspect/scan.mjs`
|
|
20
|
-
// for them, so `js/lib` is the one home both sides can reach). Re-exported here so this
|
|
21
|
-
// module's own existing callers (`emit-claude.mjs`, `emit-opencode.mjs`, `version.mjs`,
|
|
22
|
-
// `bundle.mjs`) keep importing them from `./emit-common.mjs` unchanged.
|
|
23
|
-
export { isFile, isDir, get, isDict };
|
|
24
|
-
|
|
25
18
|
/** `_build_render.SRC_DIRS_MARKER`. */
|
|
26
19
|
export const SRC_DIRS_MARKER = '.geneseed-srcdirs.json';
|
|
27
20
|
/**
|
|
@@ -79,11 +72,6 @@ export function rglobFiles(root) {
|
|
|
79
72
|
.map((d) => path.join(d.parentPath, d.name));
|
|
80
73
|
}
|
|
81
74
|
|
|
82
|
-
/** `p.relative_to(base).as_posix()`. */
|
|
83
|
-
export function relPosix(base, p) {
|
|
84
|
-
return path.relative(base, p).split(path.sep).join('/');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
75
|
/**
|
|
88
76
|
* `_build_emit._strip_capability_links` — `re.sub(r"\1")`, so the pattern needs `g`.
|
|
89
77
|
*
|
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
11
|
-
import { loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
11
|
+
import { claimer, loadAgentOverrides, writeNativeLayer } from '../hosts/native.mjs';
|
|
12
12
|
import {
|
|
13
13
|
copyPlugins, copyWorkflows, ensureAgentOverridesStub, writeColorThemes, writeCommandLayer,
|
|
14
14
|
writePonytailCommand, writePrimaryAgent, writeTheme,
|
|
15
15
|
} from '../hosts/opencode.mjs';
|
|
16
16
|
import { mergeOpencodeJson } from '../hosts/settings.mjs';
|
|
17
|
-
import { readText, writeText } from '../lib/fs.mjs';
|
|
17
|
+
import { isFile, readText, writeText } from '../lib/fs.mjs';
|
|
18
|
+
import { relPosix } from '../lib/text.mjs';
|
|
18
19
|
import { assertSourceComplete, build, phaseLog } from './bundle.mjs';
|
|
19
20
|
import {
|
|
20
|
-
globalMemory, globalNotebook,
|
|
21
|
+
globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
|
|
21
22
|
} from './emit-common.mjs';
|
|
22
23
|
import { renderAll } from './render.mjs';
|
|
23
24
|
import {
|
|
@@ -56,17 +57,20 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
|
56
57
|
} = opts;
|
|
57
58
|
ensureAgentOverridesStub(cfg, overridesDir);
|
|
58
59
|
const overrides = loadAgentOverrides(overridesDir);
|
|
60
|
+
// One claim for every writer below whose path lands in `owned` — see `claimer`.
|
|
61
|
+
const claim = claimer(oldOwned, dir, manifestExisted);
|
|
59
62
|
|
|
60
63
|
const { nAgents, nSkills, written } = writeNativeLayer(
|
|
61
64
|
items, path.join(dir, 'agents'), path.join(dir, 'skills'), overrides,
|
|
62
|
-
{ host: 'opencode',
|
|
65
|
+
{ host: 'opencode', theme, src: cfg.src, claim });
|
|
63
66
|
for (const p of written) owned.push(relPosix(dir, p));
|
|
64
67
|
|
|
65
|
-
const primary = writePrimaryAgent(cfg, path.join(dir, 'agents'), overrides);
|
|
68
|
+
const primary = writePrimaryAgent(cfg, path.join(dir, 'agents'), overrides, claim);
|
|
66
69
|
if (primary) owned.push(relPosix(dir, primary));
|
|
67
70
|
|
|
68
|
-
const commands = writeCommandLayer(cfg, items, path.join(dir, 'command'));
|
|
69
|
-
|
|
71
|
+
const commands = writeCommandLayer(cfg, items, path.join(dir, 'command'), claim);
|
|
72
|
+
const ponytail = writePonytailCommand(path.join(dir, 'command'), claim); // always-on /ponytail
|
|
73
|
+
if (ponytail) commands.push(ponytail);
|
|
70
74
|
for (const p of commands) owned.push(relPosix(dir, p));
|
|
71
75
|
|
|
72
76
|
owned.push(relPosix(dir, writeTheme(path.join(dir, 'themes'), themeName, theme)));
|
|
@@ -97,7 +101,7 @@ function opencodeLayer(cfg, items, themeName, theme, dir, owned, opts) {
|
|
|
97
101
|
* manifest is the DRIVER's stage: it reads the file, hands the prior claim in, and prunes
|
|
98
102
|
* and rewrites it after this returns. One owner of the file, and this function is not it.
|
|
99
103
|
*
|
|
100
|
-
* `nativeCatalog` likewise arrives decided. It is `
|
|
104
|
+
* `nativeCatalog` likewise arrives decided. It is `hostCatalogsNatively('opencode')` from
|
|
101
105
|
* `js/hosts/hosts.mjs`, resolved by the driver before the call; passing the decision rather
|
|
102
106
|
* than re-reading the registry keeps one owner of it.
|
|
103
107
|
*/
|