geneseed 3.1.0 → 3.1.2
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 +110 -0
- package/DESIGN.md +31 -20
- package/README.md +2 -2
- package/SETUP.md +30 -3
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +88 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +113 -85
- package/docs/web/rules.md +2 -0
- package/docs/web/setup-choices.md +13 -1
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +45 -31
- package/js/{render.mjs → build/render.mjs} +72 -11
- package/js/{checkout.mjs → build/source.mjs} +52 -11
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +68 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +94 -69
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +34 -20
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +49 -37
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +124 -89
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +88 -58
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +29 -21
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/AGENT.md.tmpl +1 -1
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/Laws-BK07NKw9.js +3 -0
- package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +135 -69
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1712
- package/js/emit.mjs +0 -1410
- package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
- package/web/dist/assets/index-1pPSz7Kl.css +0 -1
- package/web/dist/assets/index-C-gb-q-r.js +0 -12
package/js/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# `js/` — the module map
|
|
2
|
+
|
|
3
|
+
Fifty-six modules in seven folders. This page is the address book: **what each one owns**, and
|
|
4
|
+
**which file to open first** for a given task. It is not documentation of behaviour — every module
|
|
5
|
+
has a docblock for that, and the docblock is the thing to read before editing.
|
|
6
|
+
|
|
7
|
+
`tests/unit/module_map.test.mjs` keeps this page honest: a module with no row here, or a row naming
|
|
8
|
+
a module that does not exist, is a test failure. `geneseed doctor` reports the same thing.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## The shape
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
bin/geneseed-cli.mjs the `geneseed` CLI — 20+ verbs, may NOT spawn
|
|
16
|
+
bin/geneseed-hook.mjs the hook entry — 4 verbs, loads on EVERY tool call (~14 ms)
|
|
17
|
+
bin/build-driver.mjs `geneseed-build` — the generator's flags and emit targets
|
|
18
|
+
|
|
19
|
+
js/build/ src/ + themes/ → rendered text → a bundle → a per-host install
|
|
20
|
+
js/hosts/ where it stops being a renderer and touches the machine
|
|
21
|
+
js/inspect/ the read-only half: doctor, status, diff, validate, catalog
|
|
22
|
+
js/lib/ primitives, every one a deliberate divergence from the Node default
|
|
23
|
+
js/maintain/ lifecycle verbs that change an install that already exists
|
|
24
|
+
js/ui/ what it prints at a terminal
|
|
25
|
+
js/web/ the `geneseed web` console
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Dependency direction is one-way: `lib/` ← everything; `build/` ← `hosts/`; `inspect/` and `web/`
|
|
29
|
+
read what `build/` produced. Nothing in `lib/` imports anything above it.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## `js/build/` — the generator
|
|
34
|
+
|
|
35
|
+
Turns the checkout's `src/` + `themes/` into rendered text, writes that into a bundle, and emits it
|
|
36
|
+
as a per-host install at project or global scope.
|
|
37
|
+
|
|
38
|
+
| module | owns |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `source.mjs` | Where the SOURCE is: `ROOT`/`SRC`/`THEMES`/`CONFIG`, `PACK_ORDER`, rule ids, the cfg object |
|
|
41
|
+
| `render.mjs` | The pure text pipeline: theme load, `INCLUDE` inlining, CATALOG blocks, `{{TOKENS}}`, dest paths |
|
|
42
|
+
| `bundle.mjs` | `build` — render a theme into a bundle dir, plus the source-completeness refusal. Sits UNDER both emits |
|
|
43
|
+
| `emit-claude.mjs` | The Claude/Bob/Copilot emit — the render half plus the settings + CLAUDE.md managed-block wire |
|
|
44
|
+
| `emit-opencode.mjs` | The OpenCode emits — a project's `.opencode/` and the global config dir. Deliberate twin of the above |
|
|
45
|
+
| `emit-common.mjs` | Constants, tree walkers, and the three writers BOTH global emits share. Decides no host |
|
|
46
|
+
| `stubs.mjs` | Write-once seed files: context, wiki, rules, excludes, profile, `.gitignore`, memory/notebook indexes |
|
|
47
|
+
| `version.mjs` | The release marker: fingerprint the sources, read/compare/write an install's version, warn on downgrade |
|
|
48
|
+
| `generate.mjs` | CLI verbs `build`, `prompt`, `theme`, `rebuild-all` — the thin face over `bin/build-driver.mjs` |
|
|
49
|
+
| `catalog.mjs` | `geneseed catalog` — prints the shipped roster. Classifies nothing itself |
|
|
50
|
+
| `themes.mjs` | Maintainer-only `--sync-themes`: inserts `_TEMPLATE.json`'s missing keys into the committed themes |
|
|
51
|
+
|
|
52
|
+
**Before editing:** `render.mjs` writes nothing, and its output is what every emit target
|
|
53
|
+
serialises — a change there is visible in all 14 themes at once. `emit-claude.mjs`'s
|
|
54
|
+
wire half edits files the **user** co-owns — get its prior-claim pruning wrong and you delete
|
|
55
|
+
someone's own settings keys. `emit-opencode.mjs` is shaped as a twin on purpose: a change that
|
|
56
|
+
lands on one host and not the other is the historical defect here, so edit both or neither.
|
|
57
|
+
`stubs.mjs` bodies are compared byte-for-byte by a recording — editing prose in one is a product
|
|
58
|
+
change. `themes.mjs` rewrites hand-written files in the repo, and `git diff` is where you would
|
|
59
|
+
find out about a slip.
|
|
60
|
+
|
|
61
|
+
## `js/hosts/` — touching the machine
|
|
62
|
+
|
|
63
|
+
Resolving each host tool's config directories, writing host-native agents and skills into files the
|
|
64
|
+
user co-owns, and the hook verbs those emitted configs then run.
|
|
65
|
+
|
|
66
|
+
| module | owns |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `hosts.mjs` | The four host config dirs (opencode/claude/bob/copilot), plus `resolvePath`/`expanduser` |
|
|
69
|
+
| `hooks.mjs` | The verbs the emitted hooks run every session: `context`, `git-gate`, `rule-gate`, `learn` |
|
|
70
|
+
| `settings.mjs` | Merges into user-owned `settings.json`/`opencode.json`; owns the hook shim and the managed blocks |
|
|
71
|
+
| `native.mjs` | Capability specs → host-native subagents and skills. The impure half of the emit |
|
|
72
|
+
| `opencode.mjs` | OpenCode-only extras: colour themes, primary agent, `/commands`, plugins, overrides stub |
|
|
73
|
+
| `installs.mjs` | Detects deployed installs and reads back their theme, footprint, posture and mode |
|
|
74
|
+
| `mcp.mjs` | MCP server presets, the per-host `mcpServers` config read/write, and toggle state |
|
|
75
|
+
| `link.mjs` | `geneseed link`/`unlink` — the PATH shim, and the only Windows USER-Path registry edit |
|
|
76
|
+
|
|
77
|
+
**Before editing:** `hooks.mjs` **is the hook path** — the verdict travels as JSON on stdout and
|
|
78
|
+
every arm exits 0, so one stray printed byte turns a blocking gate silently permissive, and anything
|
|
79
|
+
imported here is paid on every tool call. `hosts.mjs` is shared by the generator, the CLI and the
|
|
80
|
+
hook path, so it must stay free of `child_process`. `installs.mjs` reads through `readMaybe`, which
|
|
81
|
+
folds CRLF — a bare `readFileSync` here is a bug.
|
|
82
|
+
|
|
83
|
+
## `js/inspect/` — the read-only half
|
|
84
|
+
|
|
85
|
+
Everything that inspects an already-built or already-deployed harness and reports on it.
|
|
86
|
+
|
|
87
|
+
| module | owns |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `doctor.mjs` | The `geneseed doctor` **driver** only: picks themes, runs the checks, prints the verdict |
|
|
90
|
+
| `checks-build.mjs` | Gates over what the build PRODUCED: links, tokens, theme parity, OpenCode colours, bundle drift |
|
|
91
|
+
| `checks-repo.mjs` | Gates over the repo's own records: `registry.json`, committed secrets, the hook shim, vendored pins |
|
|
92
|
+
| `checks-authoring.mjs` | Self-consistency: law/doctrine metadata, constitution numbering, the counts README and the web quote |
|
|
93
|
+
| `scan.mjs` | The shared walk/sort/strip/test primitives all three `checks-*` use. Owns no judgement itself |
|
|
94
|
+
| `validate.mjs` | The `--validate-only` verb: renders a sandbox, scans it for tokens and dead links, own exit contract |
|
|
95
|
+
| `inventory.mjs` | The one catalog walk and its taxonomy tables, read by status, the TUI and the web console |
|
|
96
|
+
| `status.mjs` | `geneseed status` and `version` — the panel data, counts, accent colour, version verdict |
|
|
97
|
+
| `diff.mjs` | `geneseed diff` — what a deployed install has that a fresh render of `src/` does not |
|
|
98
|
+
| `excludes.mjs` | `geneseed exclude add|remove|list` — the WRITER for `excludes.json`; `hooks.mjs` only reads it |
|
|
99
|
+
| `registry.mjs` | `installs.json` under XDG — the persistent deploy-root list `rebuild-all` re-emits into |
|
|
100
|
+
|
|
101
|
+
**Before editing:** every check returns an array of problem strings, empty when clean — and since
|
|
102
|
+
deleting any check leaves a clean run **byte-identical**, each one is gated by a PLANTED FAULT, not
|
|
103
|
+
by a comparison. `scan.mjs`'s `TOKEN_RE` must stay in step with `render.mjs`'s. `registry.mjs`
|
|
104
|
+
swallows its own errors on purpose: a registry hiccup must never fail a build.
|
|
105
|
+
|
|
106
|
+
## `js/lib/` — the primitives
|
|
107
|
+
|
|
108
|
+
Zero dependencies. **Every function here exists because the Node default is wrong for this tool** —
|
|
109
|
+
each one's docblock says which default, and why. Change a rule and you change what the tool writes
|
|
110
|
+
into files users have committed, so change the test that states it in the same commit.
|
|
111
|
+
|
|
112
|
+
| module | owns |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `fs.mjs` | All disk and stdout/stderr writes. The single owner of the `\n` → `os.linesep` translation |
|
|
115
|
+
| `json.mjs` | The int/float-preserving parser (`JsonNumber`) plus every dumps/repr renderer |
|
|
116
|
+
| `paths.mjs` | Case folding, ordering, containment, spelling, PATH lookup — the platform-shaped rules |
|
|
117
|
+
| `text.mjs` | String primitives: `codePointLength`, `WHITESPACE`, `parseIntStrict`, `percentDecode` |
|
|
118
|
+
| `udiff.mjs` | `geneseed diff`'s own difflib clone — the alignment IS the printed hunks — plus `splitLines` |
|
|
119
|
+
| `proc.mjs` | One constant: the `windowsHide` flag every spawn in `js/` must reuse |
|
|
120
|
+
|
|
121
|
+
**Before editing:** parse with `parseJson`, never `JSON.parse` — the latter collapses int `1` and
|
|
122
|
+
float `1.0` into one double, and that value gets written into a user's file. Do not swap in a diff
|
|
123
|
+
library for `udiff.mjs`: it is not Myers, ties resolve to the earliest match. Every function in
|
|
124
|
+
`paths.mjs` branches on `process.platform`, so green on one OS proves nothing about the other.
|
|
125
|
+
|
|
126
|
+
## `js/maintain/` — lifecycle
|
|
127
|
+
|
|
128
|
+
Verbs that change an install that already exists, as opposed to rendering one.
|
|
129
|
+
|
|
130
|
+
| module | owns |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `update.mjs` | `upgrade`/`sync-self`/`bootstrap` — git pull, doctor gate, rebuild, each via a respawn |
|
|
133
|
+
| `setup.mjs` | The install wizard, and the CLI's only synchronous stdin readers (`ask`/`confirm`/`askChoice`) |
|
|
134
|
+
| `uninstall.mjs` | Removes or disables an install: manifest-driven reversal, plus stash deactivate/reactivate |
|
|
135
|
+
| `migrate.mjs` | `geneseed migrate` — all-or-nothing re-emit moving installs onto the npm install shape |
|
|
136
|
+
| `memory.mjs` | `geneseed memory list|rm` — CLI-side fact deletion, twin of the web DELETE endpoint |
|
|
137
|
+
|
|
138
|
+
**Before editing:** `update.mjs` is the one module that MUST spawn rather than import — the code on
|
|
139
|
+
disk is replaced mid-run, so an in-process call would validate the *old* source and then render it.
|
|
140
|
+
`setup.mjs` is the whole program's stdin layer, not just setup's. `memory.mjs` hard-unlinks user
|
|
141
|
+
facts with no archive path.
|
|
142
|
+
|
|
143
|
+
## `js/ui/` — the terminal
|
|
144
|
+
|
|
145
|
+
| module | owns |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `cli.mjs` | The whole CLI surface as data (`js/cli-table.json`) plus `--help` rendering and text wrapping |
|
|
148
|
+
| `menu.mjs` | Entry points for a bare `geneseed` and for `menu` — the web-vs-terminal fork |
|
|
149
|
+
| `tui.mjs` | The `tui` verb plus the panel's pure layout half: display width, glyphs, fit/truncate, rows |
|
|
150
|
+
| `anim.mjs` | Per-theme install ASCII art and its line animation — the only ANSI-emitting file here |
|
|
151
|
+
|
|
152
|
+
**Before editing:** there is no argument-parser *definition* in the code — every verb, flag, default
|
|
153
|
+
and help string lives in `js/cli-table.json`, and the table IS the parser. Most of `tui.mjs`'s
|
|
154
|
+
exports have no caller inside it: they are the curses-free half of a panel that does not exist.
|
|
155
|
+
|
|
156
|
+
## `js/web/` — the console
|
|
157
|
+
|
|
158
|
+
A dependency-free localhost HTTP daemon that serves the built React UI from `web/dist` and exposes
|
|
159
|
+
the deployed harness as JSON.
|
|
160
|
+
|
|
161
|
+
| module | owns |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `server.mjs` | `serve()`/`cmdWeb`: UI-build precheck, socket bind, token mint, daemon record wiring |
|
|
164
|
+
| `routes.mjs` | Route DECLARATION only: the `POST_ROUTES` table and the ported/unported/declined sets |
|
|
165
|
+
| `handler.mjs` | One function per request: dispatch, gzip, Host/CSRF guards, static SPA fallback |
|
|
166
|
+
| `api.mjs` | `WebState` plus every GET read endpoint; owns `STATE_ROUTES`, `PREFIX_ROUTES`, `NotFound` |
|
|
167
|
+
| `actions.mjs` | The mutating endpoints — rules, profile, memory, MCP, excludes — plus install/deploy argv |
|
|
168
|
+
| `daemon.mjs` | Detached web start/stop/status/restart, the `.geneseed-web.json` record, and `openUrl` |
|
|
169
|
+
| `jobs.mjs` | `JobManager` and the action→argv table. The only module here that spawns job children |
|
|
170
|
+
| `docs.mjs` | The Docs tab: the `docs/web/` page registry, `?harness=` filtering, the five page kinds |
|
|
171
|
+
| `graph.mjs` | `/api/graph` alone — the agent/skill/law cross-link matrix built from rendered bodies |
|
|
172
|
+
| `activity.mjs` | The `activity/` session files: pid-liveness prune, list, detail, and the on/off flag |
|
|
173
|
+
|
|
174
|
+
**Before editing:** `makeHandler(state, jm, token, dist, holder)`'s arity is load-bearing — three
|
|
175
|
+
test call sites construct one directly and drive it without a socket. Four of `routes.mjs`'s
|
|
176
|
+
exported sets are deliberately EMPTY and must stay declared: each is half of a partition test.
|
|
177
|
+
`graph.mjs` contains a literal NUL byte, so `grep`/`ripgrep` treat it as binary and skip it.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Where do I add…
|
|
182
|
+
|
|
183
|
+
| I want to | open first | then |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| a doctrine rule | `src/doctrines/<pack>.md` — APPEND, ids run contiguously | `docs/extending.md` §2a — five files, `--sync-themes`, restyle 14 voices by hand |
|
|
186
|
+
| a law (invariant) | `src/laws/universal.md` — **APPEND, never insert** | `docs/extending.md` §2c. Amend the FIRST sentence: lean footprint keeps nothing else |
|
|
187
|
+
| a skill or an agent | `src/skills/<name>.md` or `src/agents/<name>.md` | `docs/extending.md` §3 — eight steps. Write the FILE before the table row or the build aborts |
|
|
188
|
+
| a doctor check | `js/inspect/checks-{build,repo,authoring}.mjs` | register with `ran(...)` in `doctor.mjs`, then a PLANTED FAULT in `tests/unit/harness.test.mjs` |
|
|
189
|
+
| a web endpoint | GET → `js/web/api.mjs`; POST → `js/web/actions.mjs` | declare it in `js/web/routes.mjs`; the partition test probes the real handler |
|
|
190
|
+
| a CLI verb | `js/cli-table.json` — the table IS the parser | the `VERBS` row in `bin/geneseed-cli.mjs`, the row count in `cli_table.test.mjs`, `NATIVE` in **two** test files |
|
|
191
|
+
| a hook verb | `js/hosts/hooks.mjs` | `claudeHookGroups` in `settings.mjs`, `VERBS` in `bin/geneseed-hook.mjs`. Keep the import graph tiny |
|
|
192
|
+
| a console doc page | `docs/web/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
|
|
193
|
+
| an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
|
|
194
|
+
| to change a `js/lib/` primitive | `tests/fixtures/pure_probe.mjs` **first** | the docblock says which Node default it deliberately departs from — change that sentence too, or the next reader restores the default |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Known debris
|
|
199
|
+
|
|
200
|
+
* **`js/hosts/settings.mjs` carries a private second copy of Python's whitespace class**, duplicating
|
|
201
|
+
`WHITESPACE` in `js/lib/text.mjs`. `text.mjs` is the owner.
|
|
202
|
+
* **`web/src/pages/Skills.jsx`'s `SKILL_CATS` is a fourth, ungated copy of the skill taxonomy** —
|
|
203
|
+
nothing compares it to `SKILL_CLASS`, and an unknown category renders as Build with no warning.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `build` — render one theme's sources into a bundle directory, and the two checks that
|
|
3
|
+
* decide whether it may.
|
|
4
|
+
*
|
|
5
|
+
* The LAYER BOTH HOST EMITS SIT ON, and that is why it is not in `emit.mjs` with them: the
|
|
6
|
+
* first map left it there, and the derived imports came back with `emit-opencode.mjs` and
|
|
7
|
+
* `emit-claude.mjs` importing `emit.mjs` while `emit.mjs` imported them back. A bundle is
|
|
8
|
+
* produced before any host is chosen, so it belongs UNDER the emitters, not beside them.
|
|
9
|
+
*
|
|
10
|
+
* `assertSourceComplete` scans the TEMPLATE rather than the render, and the src-dirs marker
|
|
11
|
+
* is how a re-emit knows which directory names the last one used.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { VERSION_MARKER } from '../hosts/hosts.mjs';
|
|
15
|
+
import { copyFile, readText, writeText } from '../lib/fs.mjs';
|
|
16
|
+
import { jsonDumpsIndent, parseJson, formatReprAscii, isTruthy } from '../lib/json.mjs';
|
|
17
|
+
import { OWNED_SRC_DIRS, SRC_DIRS_MARKER, TMPL_SPEC_RE, isDir, isFile } from './emit-common.mjs';
|
|
18
|
+
import { SRC_DIR_TOKENS, renderAll } from './render.mjs';
|
|
19
|
+
import {
|
|
20
|
+
ensureBundleGitignore, ensureContextStub, ensureMemoryIndex, ensureNotebookIndex,
|
|
21
|
+
ensureProfileStub, ensureRulesStub, ensureWikiStub,
|
|
22
|
+
} from './stubs.mjs';
|
|
23
|
+
import { writeVersion } from './version.mjs';
|
|
24
|
+
import { existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Source completeness
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** `_build_render._missing_referenced_specs` — scans the TEMPLATE, not the render. */
|
|
31
|
+
function missingReferencedSpecs(cfg) {
|
|
32
|
+
let tmpl;
|
|
33
|
+
try {
|
|
34
|
+
tmpl = readText(path.join(cfg.src, 'AGENT.md.tmpl'));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const missing = new Set();
|
|
39
|
+
for (const [, kind, name] of tmpl.matchAll(TMPL_SPEC_RE)) {
|
|
40
|
+
const dir = kind.toLowerCase();
|
|
41
|
+
if (name !== '_template' && !isFile(path.join(cfg.src, dir, `${name}.md`))) {
|
|
42
|
+
missing.add(`${dir}/${name}.md`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...missing].sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `_build_render.assert_source_complete`.
|
|
50
|
+
*
|
|
51
|
+
* Python raises `SystemExit(1)` after writing to stderr. Here the message goes to the
|
|
52
|
+
* captured stderr buffer and the throw carries the exit code, which the driver turns
|
|
53
|
+
* into a process status by `bin/build-driver.mjs`'s `main` — so the caller sees the same stream
|
|
54
|
+
* bytes and the same exit status whichever runtime rendered.
|
|
55
|
+
*/
|
|
56
|
+
export function assertSourceComplete(cfg, context = '') {
|
|
57
|
+
const missing = missingReferencedSpecs(cfg);
|
|
58
|
+
if (!missing.length) return;
|
|
59
|
+
const where = context ? ` (${context})` : '';
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
`[geneseed][E-INCOMPLETE] ✗ source is incomplete${where}: AGENT.md references `
|
|
62
|
+
+ `${missing.length} spec(s) with no file under src/:\n`
|
|
63
|
+
+ missing.map((m) => ` - ${m}\n`).join('')
|
|
64
|
+
+ '[geneseed] ✗ Refusing to emit — a partial source would write dead links '
|
|
65
|
+
+ 'and a global re-emit would delete the good copies in an existing install.\n'
|
|
66
|
+
+ '[geneseed] ✗ Re-sync the source (./geneseed update, or re-run the upgrade) '
|
|
67
|
+
+ 'and try again.\n');
|
|
68
|
+
const e = new Error('source is incomplete');
|
|
69
|
+
e.exitCode = 1;
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// The owned-dir marker
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/** `_build_render._read_prior_src_dirs` — `{}` on anything unreadable or not an object. */
|
|
78
|
+
function readPriorSrcDirs(out) {
|
|
79
|
+
let data;
|
|
80
|
+
try {
|
|
81
|
+
data = parseJson(readText(path.join(out, SRC_DIRS_MARKER)));
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `_build_render._write_src_dirs_marker` — temp + replace. */
|
|
89
|
+
function writeSrcDirsMarker(out, resolved) {
|
|
90
|
+
const tmp = path.join(out, `${SRC_DIRS_MARKER}.tmp`);
|
|
91
|
+
writeText(tmp, `${jsonDumpsIndent(resolved)}\n`);
|
|
92
|
+
renameSync(tmp, path.join(out, SRC_DIRS_MARKER));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The guard on the prune of a RENAMED owned dir — `build`'s one destructive path driven
|
|
97
|
+
* by file content rather than by code.
|
|
98
|
+
*
|
|
99
|
+
* `.geneseed-srcdirs.json` is a plain file a user (or another tool) can edit, and the
|
|
100
|
+
* name in it is joined onto `out` and handed to a recursive delete. `".."` would take out
|
|
101
|
+
* the bundle's PARENT, an absolute path replaces `out` entirely under Python's
|
|
102
|
+
* `Path.__truediv__`, and `"a/b"` reaches into nested content. Only a plain
|
|
103
|
+
* single-segment name resolving directly under `out` is ever deleted.
|
|
104
|
+
*/
|
|
105
|
+
function safePriorDirName(out, priorName) {
|
|
106
|
+
return typeof priorName === 'string'
|
|
107
|
+
&& priorName !== '.' && priorName !== '..'
|
|
108
|
+
&& !path.isAbsolute(priorName)
|
|
109
|
+
&& path.basename(priorName) === priorName
|
|
110
|
+
&& path.dirname(path.resolve(out, priorName)) === path.resolve(out);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// build
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
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
|
+
/**
|
|
125
|
+
* THE PHASE BOUNDARY MARKER — T8, and the whole of what this port owes
|
|
126
|
+
* `tests/test_emit_phase_order.py`.
|
|
127
|
+
*
|
|
128
|
+
* Every emit runs its stages in one order: RENDER* → WIRE* → PRUNE → MANIFEST → VERIFY. RENDER
|
|
129
|
+
* writes files Geneseed owns wholesale; WIRE reconciles files the USER co-owns (the
|
|
130
|
+
* CLAUDE.md/AGENTS.md managed block, settings(.local).json, opencode.json) by reading what is
|
|
131
|
+
* already there and merging into it. WIRE must precede MANIFEST because wiring is what fills the
|
|
132
|
+
* `managed` record the manifest writes, and every teardown path unwires exactly what was wired.
|
|
133
|
+
*
|
|
134
|
+
* WHY A RUNTIME LOG AND NOT A SOURCE WALK. The reference gated this with five `ast` walks over
|
|
135
|
+
* its own generator, and could, because it keeps `_claude_render` / `_claude_wire` as two
|
|
136
|
+
* dispatchers and therefore two statements at every call site — a shape its own docstring calls
|
|
137
|
+
* "not incidental, it is what leaves this walker something to check". THIS PORT DOES NOT HAVE
|
|
138
|
+
* THAT SHAPE: `claudeWire` is a real function but the two OpenCode emits inline their merge
|
|
139
|
+
* (`emitOpencodeRender`, `emitOpencodeGlobalRender`), and PRUNE/MANIFEST/VERIFY are not in this
|
|
140
|
+
* module at all — they are in `bin/build-driver.mjs`'s driver bodies. A walker would have to span two
|
|
141
|
+
* files and would still be reading source rather than watching a run.
|
|
142
|
+
*
|
|
143
|
+
* So the order is made OBSERVABLE instead, which is strictly the better gate: it reports what
|
|
144
|
+
* actually executed, in the order it executed, including the branch a given host took.
|
|
145
|
+
*
|
|
146
|
+
* ⚠ BEHIND AN ENV VAR THE THREE REPLAYERS DO NOT SET, and that is the entire safety argument.
|
|
147
|
+
* `cellEnv` clears every `GENESEED_*` knob by prefix, so no recorded cell can turn this on and
|
|
148
|
+
* the emit corpus cannot move by one byte. It writes to STDERR, never stdout: stdout is the
|
|
149
|
+
* decision channel every Geneseed hook signals through, and a stray byte there turns a blocking
|
|
150
|
+
* gate into a silently permissive one.
|
|
151
|
+
*
|
|
152
|
+
* Read at CALL time, not at import, so one process can drive several emits under it.
|
|
153
|
+
*/
|
|
154
|
+
export function phaseLog(phase) {
|
|
155
|
+
if (!process.env.GENESEED_PHASE_LOG) return;
|
|
156
|
+
process.stderr.write(`[geneseed:phase] ${phase}\n`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function build(cfg, themeName, out, { footprint = 'full', nativeCatalog = false } = {}) {
|
|
160
|
+
phaseLog('RENDER');
|
|
161
|
+
const { theme, items } = renderAll(cfg, themeName, { footprint, nativeCatalog });
|
|
162
|
+
assertSourceComplete(cfg, `theme '${themeName}'`);
|
|
163
|
+
mkdirSync(out, { recursive: true });
|
|
164
|
+
|
|
165
|
+
// Wipe the owned dirs ONLY inside an established bundle: a first render into an
|
|
166
|
+
// arbitrary repo (`--out .`) must never delete an agents/ or skills/ the USER owns.
|
|
167
|
+
const isBundle = isFile(path.join(out, '.geneseed-theme')) || isFile(path.join(out, VERSION_MARKER));
|
|
168
|
+
const priorSrcDirs = isBundle ? readPriorSrcDirs(out) : {};
|
|
169
|
+
const resolvedSrcDirs = {};
|
|
170
|
+
for (const srcDir of OWNED_SRC_DIRS) {
|
|
171
|
+
// `theme.get(token, srcDir)` — `??` agrees with it except for an explicit JSON null,
|
|
172
|
+
// where Python would go on to join `None` onto a path and raise. No theme has one.
|
|
173
|
+
const dirname = theme[SRC_DIR_TOKENS[srcDir]] ?? srcDir;
|
|
174
|
+
resolvedSrcDirs[srcDir] = dirname;
|
|
175
|
+
const managed = path.join(out, dirname);
|
|
176
|
+
|
|
177
|
+
// `isTruthy`, not `priorName &&`: the marker is user-editable JSON, and Python's
|
|
178
|
+
// `if prior_name` is false for `[]`, `{}` and `0` where JS's is true for the first
|
|
179
|
+
// two. Getting that wrong warns about a value Python skips in silence — a stderr
|
|
180
|
+
// divergence reachable only from a hand-edited file, which is the one place nobody
|
|
181
|
+
// would look for it.
|
|
182
|
+
const priorName = priorSrcDirs[srcDir];
|
|
183
|
+
if (isBundle && isTruthy(priorName) && priorName !== dirname) {
|
|
184
|
+
if (safePriorDirName(out, priorName)) {
|
|
185
|
+
const stale = path.join(out, priorName);
|
|
186
|
+
if (isDir(stale)) rmSync(stale, { recursive: true, force: true });
|
|
187
|
+
} else {
|
|
188
|
+
process.stderr.write('[geneseed] WARN: ignoring suspicious prior dir name '
|
|
189
|
+
+ `${formatReprAscii(priorName)} recorded in ${SRC_DIRS_MARKER} - not pruned.\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!isDir(managed)) continue;
|
|
194
|
+
if (isBundle) {
|
|
195
|
+
rmSync(managed, { recursive: true, force: true });
|
|
196
|
+
} else {
|
|
197
|
+
// STDOUT, and ASCII-only: this print crashed with UnicodeEncodeError on a cp1252
|
|
198
|
+
// Windows console when it carried a warning-sign emoji.
|
|
199
|
+
process.stdout.write(`[geneseed] WARN: ${managed} already exists and ${out} is not a `
|
|
200
|
+
+ 'Geneseed bundle — keeping it; rendered files merge into it.\n');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const nbDirname = theme[SRC_DIR_TOKENS.notebook] ?? 'notebook';
|
|
205
|
+
for (const { rel, text, src } of items) {
|
|
206
|
+
const dest = path.join(out, rel);
|
|
207
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
208
|
+
// The notebook is the agent's sovereign space: seeded files are written once and
|
|
209
|
+
// never re-emitted, so the agent may rewrite its own rules. Only `.gitignore` is
|
|
210
|
+
// re-asserted — the one law it cannot lift.
|
|
211
|
+
const parts = rel.split('/');
|
|
212
|
+
if (parts[0] === nbDirname && path.basename(rel) !== '.gitignore' && existsSync(dest)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (text !== null) writeText(dest, text);
|
|
216
|
+
else copyFile(src, dest);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
writeText(path.join(out, '.geneseed-theme'), `${themeName}\n`);
|
|
220
|
+
writeVersion(cfg, out);
|
|
221
|
+
writeSrcDirsMarker(out, resolvedSrcDirs);
|
|
222
|
+
ensureContextStub(out);
|
|
223
|
+
ensureWikiStub(out);
|
|
224
|
+
ensureRulesStub(out);
|
|
225
|
+
ensureProfileStub(out);
|
|
226
|
+
ensureBundleGitignore(out);
|
|
227
|
+
ensureMemoryIndex(path.join(out, theme[SRC_DIR_TOKENS.memory] ?? 'memory'));
|
|
228
|
+
ensureNotebookIndex(path.join(out, theme[SRC_DIR_TOKENS.notebook] ?? 'notebook'));
|
|
229
|
+
process.stdout.write(`[geneseed] built theme '${themeName}' -> ${out} (${items.length} files)\n`);
|
|
230
|
+
}
|
|
231
|
+
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* NOTHING HERE PAINTS. No cursor moves, no clears, no alternate screen: this writes lines
|
|
24
24
|
* and stops, exactly as `status` does. That is what keeps the dropped panel dropped.
|
|
25
25
|
*/
|
|
26
|
-
import { helpWidth } from '
|
|
27
|
-
import { tuiInventory } from '
|
|
28
|
-
import { defaultTheme, installedDefaults } from '
|
|
29
|
-
import {
|
|
30
|
-
import { dwidth, fit, icon, truncd, tuiEntries } from '
|
|
26
|
+
import { helpWidth } from '../ui/cli.mjs';
|
|
27
|
+
import { tuiInventory } from '../inspect/inventory.mjs';
|
|
28
|
+
import { defaultTheme, installedDefaults } from '../hosts/installs.mjs';
|
|
29
|
+
import { printOut } from '../lib/fs.mjs';
|
|
30
|
+
import { dwidth, fit, icon, truncd, tuiEntries } from '../ui/tui.mjs';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* The listing's section names, and the row kind each one selects.
|
|
@@ -129,9 +129,9 @@ export function cmdCatalog(args) {
|
|
|
129
129
|
const lines = catalogLines(tuiInventory(theme, inst.doctrines), args.section,
|
|
130
130
|
helpWidth() + 2, args.source);
|
|
131
131
|
if (!lines.length) {
|
|
132
|
-
|
|
132
|
+
printOut(`[catalog] nothing to list${args.section ? ` under ${args.section}` : ''}.\n`);
|
|
133
133
|
return 1;
|
|
134
134
|
}
|
|
135
|
-
for (const line of lines)
|
|
135
|
+
for (const line of lines) printOut(`${line}\n`);
|
|
136
136
|
return 0;
|
|
137
137
|
}
|