geneseed 3.1.1 → 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 +68 -1
- package/DESIGN.md +15 -15
- package/README.md +1 -1
- package/SETUP.md +1 -1
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +39 -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 +111 -83
- 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} +27 -28
- package/js/{render.mjs → build/render.mjs} +13 -9
- package/js/{checkout.mjs → build/source.mjs} +9 -9
- 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} +18 -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} +63 -55
- 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} +20 -17
- 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} +34 -32
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
- 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 +84 -85
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +72 -50
- 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 +21 -19
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-DyK7yRy7.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-DgRyOviS.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/{Laws-CWk3Mpri.js → Laws-BK07NKw9.js} +1 -1
- package/web/dist/assets/{Library-DXhTN9ju.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-eND7hJv9.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-CoAWuIeM.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-Dmb0FTAx.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-jrY4m9jP.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-Dg58ScBe.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +1 -1
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1750
- package/js/emit.mjs +0 -1411
- package/web/dist/assets/index-B-mXquBZ.css +0 -1
- package/web/dist/assets/index-CH_Z725C.js +0 -12
|
@@ -38,11 +38,10 @@
|
|
|
38
38
|
* have run, and inventing one writes a shim that names an interpreter the user may not have.
|
|
39
39
|
* P5b settled this shape for the hook shim — "Node bakes node" — and P3a settled the gate:
|
|
40
40
|
* the shim is byte-comparable once runner and entry are ARGUMENTS. So this bakes
|
|
41
|
-
* `process.execPath` + `bin/geneseed-cli.mjs`,
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* the
|
|
45
|
-
* being a port, and it disappears when P7 lands `tui`/`menu`/`home`.
|
|
41
|
+
* `process.execPath` + `bin/geneseed-cli.mjs`, the cell harness normalises the shim's argv
|
|
42
|
+
* line the way `js/web/jobs.mjs` normalises the job runner's, and the real content is
|
|
43
|
+
* asserted ABSOLUTELY in `tests/unit/win_user_path.test.mjs`. The shim reaches every verb in
|
|
44
|
+
* the `VERBS` table, `tui`/`menu`/`home` included since P7 — the gap the port opened is closed.
|
|
46
45
|
*
|
|
47
46
|
* THE DOUBLE CARRIAGE RETURN IS NOT A TYPO. The reference builds the shim with explicit
|
|
48
47
|
* `\r\n` and writes it with `Path.write_text`, whose default newline handling then turns
|
|
@@ -67,9 +66,10 @@
|
|
|
67
66
|
* and the verb takes its "removed the shim, said nothing about PATH" branch. A real arm,
|
|
68
67
|
* reached honestly, with the registry untouched.
|
|
69
68
|
*
|
|
70
|
-
* THE SUCCESS ARM OF `winUserPath` IS THEREFORE UNGATED, DELIBERATELY
|
|
71
|
-
*
|
|
72
|
-
* edit the machine running the gate.
|
|
69
|
+
* THE SUCCESS ARM OF `winUserPath` IS THEREFORE UNGATED, DELIBERATELY — declared here and
|
|
70
|
+
* in `tests/unit/win_user_path.test.mjs`'s coverage row rather than faked, because the only
|
|
71
|
+
* way to gate it is to edit the machine running the gate. What stands in for it is
|
|
72
|
+
* source-level: that file asserts the spawn's shape without running it.
|
|
73
73
|
*/
|
|
74
74
|
import {
|
|
75
75
|
chmodSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync,
|
|
@@ -78,9 +78,10 @@ import { spawnSync } from 'node:child_process';
|
|
|
78
78
|
import path from 'node:path';
|
|
79
79
|
import os from 'node:os';
|
|
80
80
|
|
|
81
|
-
import { ROOT } from '
|
|
81
|
+
import { ROOT } from '../build/source.mjs';
|
|
82
82
|
import { readMaybe } from './installs.mjs';
|
|
83
|
-
import {
|
|
83
|
+
import { printOut, printErr, writeText } from '../lib/fs.mjs';
|
|
84
|
+
import { toPlatformPath } from '../lib/paths.mjs';
|
|
84
85
|
|
|
85
86
|
const IS_WIN = process.platform === 'win32';
|
|
86
87
|
|
|
@@ -154,28 +155,28 @@ export function cmdLink(args) {
|
|
|
154
155
|
writeText(shim, '@echo off\r\n'
|
|
155
156
|
+ `"${process.execPath}" "${path.join(here, 'bin', 'geneseed-cli.mjs')}" %*\r\n`);
|
|
156
157
|
} catch (e) {
|
|
157
|
-
|
|
158
|
+
printErr(`geneseed: could not write ${shim} (${asOsError(e)})\n`);
|
|
158
159
|
return 1;
|
|
159
160
|
}
|
|
160
|
-
|
|
161
|
+
printOut(`geneseed: wrote shim ${shim}\n`);
|
|
161
162
|
const on_path = (process.env.PATH || '').toLowerCase().includes(bindir.toLowerCase());
|
|
162
163
|
if (on_path || winUserPath('add', bindir)) {
|
|
163
|
-
|
|
164
|
+
printOut(`geneseed: '${bindir}' is on your user PATH — open a NEW terminal, then run \`geneseed\`.\n`);
|
|
164
165
|
} else {
|
|
165
|
-
|
|
166
|
+
printOut(`geneseed: add '${bindir}' to your PATH manually, then run \`geneseed\` from anywhere.\n`);
|
|
166
167
|
}
|
|
167
168
|
return 0;
|
|
168
169
|
}
|
|
169
170
|
// Unix: write a shim into a bin dir (default ~/.local/bin, no sudo). See the shim
|
|
170
171
|
// comment below for why this is no longer a symlink to `ROOT/geneseed`.
|
|
171
|
-
// `Path(args.dir)` — and `
|
|
172
|
+
// `Path(args.dir)` — and `toPlatformPath` is that conversion, not decoration. The reference
|
|
172
173
|
// builds a `Path` here and prints `str(target_dir)` twice (the PATH notice and the `export
|
|
173
174
|
// PATH=` line) as well as comparing it against `PATH.split(os.pathsep)`, so an argument
|
|
174
175
|
// that is not already normalised diverges in three places at once. A trailing slash is the
|
|
175
176
|
// cheapest one: `str(Path('/x/bin/'))` is `/x/bin` and the raw string is not.
|
|
176
177
|
// `harness_golden`'s `link/an-explicit-dir-argument-is-used-instead-of-the-default` passes
|
|
177
178
|
// one, and it is the cell that found this.
|
|
178
|
-
let targetDir = args.dir ?
|
|
179
|
+
let targetDir = args.dir ? toPlatformPath(args.dir) : null;
|
|
179
180
|
if (targetDir === null) {
|
|
180
181
|
const local = path.join(os.homedir(), '.local', 'bin');
|
|
181
182
|
try {
|
|
@@ -202,16 +203,16 @@ export function cmdLink(args) {
|
|
|
202
203
|
+ `exec "${process.execPath}" "${entry}" "$@"\n`);
|
|
203
204
|
chmodSync(dest, 0o755);
|
|
204
205
|
} catch (e) {
|
|
205
|
-
|
|
206
|
+
printErr(`geneseed: could not write ${dest} (${asOsError(e)}) — pick a writable dir: `
|
|
206
207
|
+ 'geneseed link <dir>\n');
|
|
207
208
|
return 1;
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
+
printOut(`geneseed: linked ${dest} -> ${entry}\n`);
|
|
210
211
|
if ((process.env.PATH || '').split(path.delimiter).includes(targetDir)) {
|
|
211
|
-
|
|
212
|
+
printOut(`geneseed: '${targetDir}' is on PATH — run 'geneseed' from anywhere.\n`);
|
|
212
213
|
} else {
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
printOut(`geneseed: NOTE '${targetDir}' is not on your PATH. Add it, e.g.:\n`);
|
|
215
|
+
printOut(` echo 'export PATH="${targetDir}:$PATH"' >> ~/.zshrc # or ~/.bashrc\n`);
|
|
215
216
|
}
|
|
216
217
|
return 0;
|
|
217
218
|
}
|
|
@@ -226,15 +227,15 @@ export function cmdUnlink() {
|
|
|
226
227
|
try {
|
|
227
228
|
rmSync(shim);
|
|
228
229
|
removed = true;
|
|
229
|
-
|
|
230
|
+
printOut(`geneseed: removed ${shim}\n`);
|
|
230
231
|
} catch (e) {
|
|
231
|
-
|
|
232
|
+
printErr(`geneseed: could not remove ${shim} (${asOsError(e)})\n`);
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
if (winUserPath('remove', bindir)) {
|
|
235
|
-
|
|
236
|
+
printOut(`geneseed: removed '${bindir}' from your user PATH (open a new terminal).\n`);
|
|
236
237
|
}
|
|
237
|
-
if (!removed)
|
|
238
|
+
if (!removed) printOut('geneseed: no linked launcher found.\n');
|
|
238
239
|
return 0;
|
|
239
240
|
}
|
|
240
241
|
let removed = false;
|
|
@@ -256,12 +257,12 @@ export function cmdUnlink() {
|
|
|
256
257
|
if (ours) {
|
|
257
258
|
try {
|
|
258
259
|
rmSync(f);
|
|
259
|
-
|
|
260
|
+
printOut(`geneseed: removed ${f}\n`);
|
|
260
261
|
removed = true;
|
|
261
262
|
} catch { /* `except OSError: pass` */ }
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
|
-
if (!removed)
|
|
265
|
+
if (!removed) printOut('geneseed: no linked launcher found on PATH\n');
|
|
265
266
|
return 0;
|
|
266
267
|
}
|
|
267
268
|
|
|
@@ -281,6 +282,6 @@ function isSymlink(p) {
|
|
|
281
282
|
* in a different order. Not byte-reproducible and not worth pretending otherwise: both cells
|
|
282
283
|
* that can reach it assert the arm, not the errno text.
|
|
283
284
|
*/
|
|
284
|
-
function
|
|
285
|
+
function asOsError(e) {
|
|
285
286
|
return e && e.message ? e.message : String(e);
|
|
286
287
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* `1` on the way back out. `readJsonc` already learned this; the strict-JSON fork has to
|
|
25
25
|
* learn it separately, because it does not go through `readJsonc` at all.
|
|
26
26
|
*
|
|
27
|
-
* A
|
|
27
|
+
* A JsonNumber IS AN OBJECT TO `typeof`, which is why `isDict` is spelled out rather than
|
|
28
28
|
* written as `typeof v === 'object'`. Python's `isinstance(x, dict)` is false for a float;
|
|
29
29
|
* the JS twin has to be false for its wrapper.
|
|
30
30
|
*/
|
|
@@ -32,10 +32,11 @@ import { existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
|
32
32
|
import os from 'node:os';
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
|
|
35
|
-
import { bobConfigDir, copilotConfigDir,
|
|
35
|
+
import { bobConfigDir, copilotConfigDir, resolvePath } from './hosts.mjs';
|
|
36
36
|
import { installState, installTargets } from './installs.mjs';
|
|
37
|
-
import {
|
|
38
|
-
|
|
37
|
+
import { printOut, printErr, readText, writeText } from '../lib/fs.mjs';
|
|
38
|
+
import { jsonDumpsIndent, parseJson } from '../lib/json.mjs';
|
|
39
|
+
import { padEndToWidth } from '../lib/text.mjs';
|
|
39
40
|
import { opencodeTarget, readJsonc } from './settings.mjs';
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -240,7 +241,7 @@ export function mcpConfigFor(host, scope, root) {
|
|
|
240
241
|
if (host === 'opencode') return opencodeTarget(path.join(root, 'opencode.json'));
|
|
241
242
|
if (host === 'claude') {
|
|
242
243
|
return scope === 'project' ? path.join(root, '.mcp.json')
|
|
243
|
-
:
|
|
244
|
+
: resolvePath(path.join(os.homedir(), '.claude.json'));
|
|
244
245
|
}
|
|
245
246
|
if (host === 'bob') {
|
|
246
247
|
return scope === 'project' ? path.join(root, '.bob', 'settings.json')
|
|
@@ -305,18 +306,18 @@ export function cmdMcp() {
|
|
|
305
306
|
const targets = mcpInstallTargets()
|
|
306
307
|
.filter(([, , host, scope, root]) => installState(root, host, scope) === 'active');
|
|
307
308
|
if (!targets.length) {
|
|
308
|
-
|
|
309
|
+
printErr('[mcp] no active Geneseed install found. Run `geneseed setup` first.\n');
|
|
309
310
|
return 1;
|
|
310
311
|
}
|
|
311
312
|
for (const [label, p, host] of targets) {
|
|
312
313
|
const cfg = mcpLoad(p, host);
|
|
313
|
-
|
|
314
|
+
printOut(`[mcp] ${host} ${label}: ${p}${existsSync(p) ? '' : ' (not written yet)'}\n`);
|
|
314
315
|
if (mcpCommented(p)) {
|
|
315
|
-
|
|
316
|
+
printOut(' this config carries comments — edit it by hand so they survive.\n');
|
|
316
317
|
}
|
|
317
318
|
for (const name of mcpKnownNames(cfg, host)) {
|
|
318
319
|
const [lbl] = mcpMeta(name);
|
|
319
|
-
|
|
320
|
+
printOut(` ${padEndToWidth(mcpState(cfg, name, host), 8)} ${padEndToWidth(name, 12)} ${lbl}\n`);
|
|
320
321
|
}
|
|
321
322
|
}
|
|
322
323
|
return 0;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* one dialect per host (OpenCode / Claude Code / Copilot), vendored skill folders copied
|
|
7
7
|
* through verbatim, authoring templates shipped flat.
|
|
8
8
|
*
|
|
9
|
-
* WHY THIS IS THE HARD HALF. `js/render.mjs` is a pure function of `src/` and `themes/`;
|
|
9
|
+
* WHY THIS IS THE HARD HALF. `js/build/render.mjs` is a pure function of `src/` and `themes/`;
|
|
10
10
|
* this file is not, in three separate ways, and each one is a place a port can be subtly
|
|
11
11
|
* wrong while looking right:
|
|
12
12
|
*
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* (`<name>.agent.md`) that drags a link rewrite along with it.
|
|
20
20
|
*
|
|
21
21
|
* None of the three is reachable from the golden harness's happy path: golden emits into
|
|
22
|
-
* a fresh tree with an empty overrides stub. `tests/
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* a fresh tree with an empty overrides stub. `tests/unit/user_files.test.mjs` drives
|
|
23
|
+
* claim-on-create over the states a clean fixture cannot produce, and asserts the written
|
|
24
|
+
* tree, the return value AND the warning stream.
|
|
25
25
|
*
|
|
26
26
|
* NOTHING HERE MAY WRITE TO STDOUT. Python's warnings all go to stderr, and the emitted
|
|
27
27
|
* hook gates signal by printing JSON on stdout — a stray byte from a library on that path
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
32
32
|
import path from 'node:path';
|
|
33
|
-
import { writeText, copyFile,
|
|
34
|
-
|
|
33
|
+
import { writeText, copyFile, readText } from '../lib/fs.mjs';
|
|
34
|
+
import { jsonDumps, parseJson, formatValue, formatRepr, isTruthy } from '../lib/json.mjs';
|
|
35
35
|
|
|
36
36
|
/** Mirrors `_build_core.VENDORED_SKILL_DIRS`. */
|
|
37
37
|
export const VENDORED_SKILL_DIRS = new Set(['react-view-transitions', 'daydream', 'token-report']);
|
|
@@ -125,7 +125,7 @@ const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
|
125
125
|
* comment is stripped.
|
|
126
126
|
*
|
|
127
127
|
* `.replace` with a `g` regex, matching `re.sub`'s replace-all; the Python's pattern is
|
|
128
|
-
* non-global-looking but `re.sub` always is. `split('\n')` rather than `
|
|
128
|
+
* non-global-looking but `re.sub` always is. `split('\n')` rather than `splitLines`: the
|
|
129
129
|
* text has been through `readText`, and the reason `firstBlockquote` gives for the same
|
|
130
130
|
* choice applies verbatim to the same files.
|
|
131
131
|
*/
|
|
@@ -133,12 +133,12 @@ export function descBlockProblem(text) {
|
|
|
133
133
|
const nonblank = text.replace(HTML_COMMENT_RE, '').split('\n').filter((ln) => ln.trim());
|
|
134
134
|
if (!nonblank.length) return 'file is empty (after stripping authoring comments)';
|
|
135
135
|
if (!nonblank[0].replace(/^\s+/, '').startsWith('#')) {
|
|
136
|
-
return `first content line is not a title ('# ...'): ${
|
|
136
|
+
return `first content line is not a title ('# ...'): ${formatRepr(nonblank[0].trim())}`;
|
|
137
137
|
}
|
|
138
138
|
if (nonblank.length < 2) return 'has a title but no purpose blockquote after it';
|
|
139
139
|
const second = nonblank[1].trim();
|
|
140
140
|
if (!second.startsWith('>')) {
|
|
141
|
-
return `first block after the title is not a '>' blockquote: ${
|
|
141
|
+
return `first block after the title is not a '>' blockquote: ${formatRepr(second)}`;
|
|
142
142
|
}
|
|
143
143
|
if (!second.replace(/^>+/, '').trim()) return 'purpose blockquote is empty';
|
|
144
144
|
return '';
|
|
@@ -167,7 +167,7 @@ function agentColorMap(theme) {
|
|
|
167
167
|
if (VALID_AGENT_COLOR_SLOTS.has(v)) {
|
|
168
168
|
cleaned[k] = v;
|
|
169
169
|
} else {
|
|
170
|
-
warn(`[geneseed] WARN: AGENT_COLORS[${
|
|
170
|
+
warn(`[geneseed] WARN: AGENT_COLORS[${formatRepr(k)}] = ${formatRepr(v)} is not a valid `
|
|
171
171
|
+ `OpenCode theme slot (${[...VALID_AGENT_COLOR_SLOTS].sort().join(', ')}) — `
|
|
172
172
|
+ `falling back to 'secondary'`);
|
|
173
173
|
cleaned[k] = 'secondary';
|
|
@@ -216,23 +216,25 @@ function agentOverride(overrides, stem) {
|
|
|
216
216
|
* `model`/`variant` tests are truthiness and the `temperature`/`steps` tests are
|
|
217
217
|
* `is not None` — a configured `temperature: 0` must survive, an empty model must not.
|
|
218
218
|
*
|
|
219
|
-
* Python
|
|
220
|
-
* `_write_primary_agent`. One owner here rather than a second copy
|
|
221
|
-
*
|
|
222
|
-
*
|
|
219
|
+
* Python spelled these four ifs out twice, in `_opencode_agent_frontmatter` and in
|
|
220
|
+
* `_write_primary_agent`. One owner here rather than a second copy, and the reason is
|
|
221
|
+
* sharper now than parity was: the two call sites are `opencodeAgentFrontmatter` below and
|
|
222
|
+
* `writePrimaryAgent` in `js/hosts/opencode.mjs`, and the second is behind
|
|
223
|
+
* `GENESEED_PRIMARY` — so only `tests/unit/agent_overrides.test.mjs` reaches it. Sharing the
|
|
224
|
+
* body is what stops the opt-in path rotting away from the default one unobserved.
|
|
223
225
|
*/
|
|
224
226
|
export function pushOverrideLines(fm, ov) {
|
|
225
|
-
if (
|
|
226
|
-
if (given(ov.temperature)) fm.push(`temperature: ${
|
|
227
|
-
if (
|
|
228
|
-
if (given(ov.steps)) fm.push(`steps: ${
|
|
227
|
+
if (isTruthy(ov.model)) fm.push(`model: ${formatValue(ov.model)}`);
|
|
228
|
+
if (given(ov.temperature)) fm.push(`temperature: ${formatValue(ov.temperature)}`);
|
|
229
|
+
if (isTruthy(ov.variant)) fm.push(`variant: ${formatValue(ov.variant)}`);
|
|
230
|
+
if (given(ov.steps)) fm.push(`steps: ${formatValue(ov.steps)}`);
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
/** `_build_emit._claude_agent_frontmatter`. */
|
|
232
234
|
function claudeAgentFrontmatter(stem, text, overrides) {
|
|
233
235
|
const fm = [`name: ${stem}`, `description: ${jsonDumps(descOf(text))}`];
|
|
234
236
|
const ov = agentOverride(overrides, stem);
|
|
235
|
-
if (
|
|
237
|
+
if (isTruthy(ov.model)) fm.push(`model: ${formatValue(ov.model)}`);
|
|
236
238
|
if (isReadonly(text)) {
|
|
237
239
|
const denied = ['Write', 'Edit', 'NotebookEdit', 'WebFetch'];
|
|
238
240
|
if (!text.includes('<!-- bash: allow -->')) denied.push('Bash');
|
|
@@ -245,7 +247,7 @@ function claudeAgentFrontmatter(stem, text, overrides) {
|
|
|
245
247
|
function copilotAgentFrontmatter(stem, text, overrides) {
|
|
246
248
|
const fm = [`name: ${stem}`, `description: ${jsonDumps(descOf(text))}`];
|
|
247
249
|
const ov = agentOverride(overrides, stem);
|
|
248
|
-
if (
|
|
250
|
+
if (isTruthy(ov.model)) fm.push(`model: ${formatValue(ov.model)}`);
|
|
249
251
|
if (isReadonly(text)) {
|
|
250
252
|
const allowed = ['read', 'search', 'todo', 'agent'];
|
|
251
253
|
if (text.includes('<!-- bash: allow -->')) allowed.push('execute');
|
|
@@ -271,7 +273,7 @@ function opencodeAgentFrontmatter(stem, text, overrides, theme = null) {
|
|
|
271
273
|
export function loadAgentOverrides(base) {
|
|
272
274
|
let data;
|
|
273
275
|
try {
|
|
274
|
-
// parseJson, not JSON.parse: the override values are rendered with `
|
|
276
|
+
// parseJson, not JSON.parse: the override values are rendered with `formatValue`, which
|
|
275
277
|
// needs the int/float distinction the raw literal carries and `JSON.parse` discards.
|
|
276
278
|
data = parseJson(readText(path.join(base, 'agent-overrides.json')));
|
|
277
279
|
} catch {
|
|
@@ -297,7 +299,7 @@ function relPosix(base, target) {
|
|
|
297
299
|
* `items` are `render_all`'s, as `{ rel, text, src }` — `text` null for a binary that is
|
|
298
300
|
* copied rather than rendered. `src` is the absolute source path; `opts.src` is the
|
|
299
301
|
* source root it is relativised against (Python reads `_build_core.SRC` directly, which
|
|
300
|
-
* is the module-level single owner; here it travels explicitly, as in `js/render.mjs`).
|
|
302
|
+
* is the module-level single owner; here it travels explicitly, as in `js/build/render.mjs`).
|
|
301
303
|
*
|
|
302
304
|
* Returns `{ nAgents, nSkills, written }` — Python's 3-tuple, with `written` as absolute
|
|
303
305
|
* paths in write order.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The OpenCode-native extras, in Node — everything `emit_opencode` writes beyond the
|
|
3
|
-
* agents and skills that `js/native.mjs` already owns.
|
|
3
|
+
* agents and skills that `js/hosts/native.mjs` already owns.
|
|
4
4
|
*
|
|
5
5
|
* A translation of the second half of `_build_emit.py`: the branded and curated colour
|
|
6
6
|
* themes, the opt-in primary agent, the opt-in slash-command layer plus the always-on
|
|
@@ -15,17 +15,18 @@
|
|
|
15
15
|
* indent is what switches Python's separators to the ones `JSON.stringify` already uses.
|
|
16
16
|
*
|
|
17
17
|
* TWO STREAMS, AND THE SPLIT IS NOT TIDY. `_warn_if_overrides_stale` prints to STDOUT
|
|
18
|
-
* while every warning in `js/native.mjs` goes to stderr. That is the Python's own
|
|
18
|
+
* while every warning in `js/hosts/native.mjs` goes to stderr. That is the Python's own
|
|
19
19
|
* inconsistency and it is reproduced, not corrected: the parity gate compares both
|
|
20
|
-
*
|
|
20
|
+
* specification, and `tests/unit/agent_overrides.test.mjs` pins the stale notice to stdout.
|
|
21
|
+
* It is safe because this is the
|
|
21
22
|
* generator's CLI, which prints progress to stdout by design — unlike a hook path, where
|
|
22
23
|
* a stray stdout byte silently disarms a blocking gate.
|
|
23
24
|
*/
|
|
24
25
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
25
26
|
import path from 'node:path';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
} from '
|
|
27
|
+
import { writeText, readText, copyFile } from '../lib/fs.mjs';
|
|
28
|
+
import { jsonDumps, jsonDumpsIndent, parseJson } from '../lib/json.mjs';
|
|
29
|
+
import { comparePaths } from '../lib/paths.mjs';
|
|
29
30
|
import { descOf, stripSkillBodyLinks, pushOverrideLines } from './native.mjs';
|
|
30
31
|
|
|
31
32
|
/** ANSI colour-name -> integer (0-7). Mirrors `_build_emit._ANSI`. */
|
|
@@ -98,7 +99,11 @@ function truthyEnv(name) {
|
|
|
98
99
|
return ['1', 'on', 'true', 'yes'].includes((process.env[name] || '').toLowerCase());
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* `_build_core.source_release_version`. It reads `cfg.config`, so it takes `cfg` rather than
|
|
104
|
+
* importing core — and it stayed HERE when core landed as `js/build/source.mjs`, which is why
|
|
105
|
+
* `js/build/version.mjs` reaches back into the OpenCode module for it.
|
|
106
|
+
*/
|
|
102
107
|
export function sourceReleaseVersion(cfg) {
|
|
103
108
|
try {
|
|
104
109
|
const data = parseJson(readText(cfg.config));
|