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/js/hosts/native.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A faithful translation of `_build_emit._write_native_layer` and the frontmatter
|
|
5
5
|
* builders it dispatches to. Capability specs become host-native subagents and skills:
|
|
6
|
-
* one dialect per host (OpenCode / Claude Code), vendored skill folders copied
|
|
6
|
+
* one agent dialect per host family (OpenCode / Claude Code), vendored skill folders copied
|
|
7
7
|
* through verbatim, authoring templates shipped flat.
|
|
8
8
|
*
|
|
9
9
|
* WHY THIS IS THE HARD HALF. `js/build/render.mjs` is a pure function of `src/` and `themes/`;
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* 2. Claim-on-create decides which files get written AT ALL: a target that already
|
|
16
16
|
* exists and was not in the previous manifest belongs to the user, and is skipped
|
|
17
17
|
* with a warning. So the output is a function of what is already on disk.
|
|
18
|
-
* 3.
|
|
19
|
-
* (`<name>.agent.md`) that drags a link rewrite along with it.
|
|
18
|
+
* 3. Two agent dialects diverge inside one loop, on the same filenames.
|
|
20
19
|
*
|
|
21
20
|
* None of the three is reachable from the golden harness's happy path: golden emits into
|
|
22
21
|
* a fresh tree with an empty overrides stub. `tests/unit/user_files.test.mjs` drives
|
|
@@ -32,9 +31,10 @@ import { existsSync, mkdirSync } from 'node:fs';
|
|
|
32
31
|
import path from 'node:path';
|
|
33
32
|
import { writeText, copyFile, readText } from '../lib/fs.mjs';
|
|
34
33
|
import { jsonDumps, parseJson, formatValue, formatRepr, isTruthy, isDict } from '../lib/json.mjs';
|
|
34
|
+
import { relPosix } from '../lib/text.mjs';
|
|
35
35
|
|
|
36
36
|
/** What a folder skill writes where it needs its own directory — see `writeNativeLayer`. */
|
|
37
|
-
|
|
37
|
+
const SKILL_DIR_PLACEHOLDER = '<this-skill-directory>';
|
|
38
38
|
|
|
39
39
|
/** Mirrors `_build_core.VENDORED_SKILL_DIRS`. */
|
|
40
40
|
export const VENDORED_SKILL_DIRS = new Set(['react-view-transitions', 'daydream', 'token-report', 'explain-changes']);
|
|
@@ -108,11 +108,6 @@ export function firstBlockquote(text) {
|
|
|
108
108
|
return '';
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** `_build_emit.desc_of`. */
|
|
112
|
-
export function descOf(text) {
|
|
113
|
-
return firstBlockquote(text);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
111
|
/**
|
|
117
112
|
* The `**Trigger:**` paragraph of a skill, flattened to one line — markdown links reduced
|
|
118
113
|
* to their text, HTML comments dropped, whitespace collapsed. '' when the skill has none.
|
|
@@ -155,7 +150,7 @@ function triggerOf(text) {
|
|
|
155
150
|
*/
|
|
156
151
|
const DESCRIPTION_CAP = 320;
|
|
157
152
|
function skillDescription(text) {
|
|
158
|
-
return withWhen(
|
|
153
|
+
return withWhen(firstBlockquote(text), triggerOf(text));
|
|
159
154
|
}
|
|
160
155
|
|
|
161
156
|
/**
|
|
@@ -188,7 +183,7 @@ function dispatchOf(text) {
|
|
|
188
183
|
|
|
189
184
|
/** The emitted `description:` of an agent — purpose + its first dispatch condition. */
|
|
190
185
|
export function agentDescription(text) {
|
|
191
|
-
return withWhen(
|
|
186
|
+
return withWhen(firstBlockquote(text), dispatchOf(text));
|
|
192
187
|
}
|
|
193
188
|
|
|
194
189
|
/** Purpose + ` Use when: ` + the first sentence of `trig`, capped — see `skillDescription`. */
|
|
@@ -299,11 +294,9 @@ function given(v) {
|
|
|
299
294
|
* `Object.prototype` for an agent named `constructor`, `toString` or `valueOf`, and would
|
|
300
295
|
* hand back a function where Python hands back `None`.
|
|
301
296
|
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* while this returns undefined for every key and emits no override lines. Not a byte
|
|
306
|
-
* difference on any path the gate covers — a difference in which malformed input crashes.
|
|
297
|
+
* A value that is present but NOT an object (`"reviewer": "haiku"` instead of
|
|
298
|
+
* `"reviewer": {"model": "haiku"}`) never reaches here from a file: `loadAgentOverrides`
|
|
299
|
+
* warns and drops it. A caller handing one in directly gets no override lines.
|
|
307
300
|
*/
|
|
308
301
|
function agentOverride(overrides, stem) {
|
|
309
302
|
return (Object.hasOwn(overrides, stem) && overrides[stem]) || {};
|
|
@@ -366,13 +359,36 @@ function opencodeAgentFrontmatter(stem, text, overrides, theme = null) {
|
|
|
366
359
|
return fm;
|
|
367
360
|
}
|
|
368
361
|
|
|
369
|
-
/** `
|
|
362
|
+
/** The keys the OpenCode dialect renders. Claude's honours `model` alone — see `writeNativeLayer`. */
|
|
363
|
+
const OVERRIDE_KEYS = ['model', 'temperature', 'variant', 'steps'];
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* '' when `v` may be written into a frontmatter line, else why not.
|
|
367
|
+
*
|
|
368
|
+
* Each value lands VERBATIM after `key: `, so a string carrying a line break writes frontmatter
|
|
369
|
+
* keys of its own (`"haiku\npermission: allow"`), and anything `formatValue` cannot render — a
|
|
370
|
+
* boolean, a list, an object — threw a bare TypeError that aborted the emit with a stack trace
|
|
371
|
+
* naming nothing the user wrote.
|
|
372
|
+
*/
|
|
373
|
+
function overrideProblem(v) {
|
|
374
|
+
if (typeof v === 'string') return /[\r\n\u2028\u2029]/.test(v) ? 'contains a line break' : '';
|
|
375
|
+
// `parseJson` yields exactly one object `isDict` refuses: its number wrapper.
|
|
376
|
+
const isNumber = typeof v === 'object' && v !== null && !Array.isArray(v) && !isDict(v);
|
|
377
|
+
if (isNumber && Number.isFinite(+v)) return '';
|
|
378
|
+
return 'is not a string or a finite number';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* `_build_emit._load_agent_overrides`, with every value that would break the frontmatter
|
|
383
|
+
* dropped and named on stderr — the user's file, so a bad entry is a warning, never an abort.
|
|
384
|
+
*/
|
|
370
385
|
export function loadAgentOverrides(base) {
|
|
386
|
+
const file = path.join(base, 'agent-overrides.json');
|
|
371
387
|
let data;
|
|
372
388
|
try {
|
|
373
389
|
// parseJson, not JSON.parse: the override values are rendered with `formatValue`, which
|
|
374
390
|
// needs the int/float distinction the raw literal carries and `JSON.parse` discards.
|
|
375
|
-
data = parseJson(readText(
|
|
391
|
+
data = parseJson(readText(file));
|
|
376
392
|
} catch {
|
|
377
393
|
// Missing or malformed => no overrides, so agents inherit the host model.
|
|
378
394
|
// Wider than Python's `except (OSError, json.JSONDecodeError)` by one case: a file
|
|
@@ -382,12 +398,72 @@ export function loadAgentOverrides(base) {
|
|
|
382
398
|
return {};
|
|
383
399
|
}
|
|
384
400
|
const agents = isDict(data) ? data.agents : null;
|
|
385
|
-
|
|
401
|
+
if (!isDict(agents)) return {};
|
|
402
|
+
// In place: `data` is a fresh parse, and deleting keeps a `__proto__`-named entry an own
|
|
403
|
+
// property, where copying into a new object would assign a prototype instead.
|
|
404
|
+
for (const [stem, entry] of Object.entries(agents)) {
|
|
405
|
+
if (entry === null) continue;
|
|
406
|
+
if (!isDict(entry)) {
|
|
407
|
+
warn(`[geneseed] WARN: ${file}: agents.${stem} is not an object — skipped`);
|
|
408
|
+
delete agents[stem];
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
for (const key of OVERRIDE_KEYS) {
|
|
412
|
+
if (!given(entry[key])) continue;
|
|
413
|
+
const why = overrideProblem(entry[key]);
|
|
414
|
+
if (!why) continue;
|
|
415
|
+
warn(`[geneseed] WARN: ${file}: agents.${stem}.${key} ${why} — skipped`);
|
|
416
|
+
delete entry[key];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return agents;
|
|
386
420
|
}
|
|
387
421
|
|
|
388
|
-
/**
|
|
389
|
-
|
|
390
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Warn once per override key the Claude dialect drops. The seeded `agent-overrides.json`
|
|
424
|
+
* advertises all four keys on every host, and Claude Code's agent frontmatter takes `model`
|
|
425
|
+
* only — so a `temperature` set there was silently a no-op.
|
|
426
|
+
*/
|
|
427
|
+
function warnClaudeIgnored(overrides) {
|
|
428
|
+
const ignored = new Set();
|
|
429
|
+
for (const entry of Object.values(overrides)) {
|
|
430
|
+
if (!isDict(entry)) continue;
|
|
431
|
+
for (const key of OVERRIDE_KEYS.slice(1)) if (given(entry[key])) ignored.add(key);
|
|
432
|
+
}
|
|
433
|
+
for (const key of ignored) {
|
|
434
|
+
warn(`[geneseed] WARN: agent-overrides.json sets '${key}', which Claude-family hosts do `
|
|
435
|
+
+ "not support — only 'model' is applied");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Claim-on-create, as a closure over one emit's prior manifest: `claim(dest)` is true when
|
|
441
|
+
* `dest` may be (over)written, false for a pre-existing file no previous emit owned — the
|
|
442
|
+
* user's, left alone, named on stderr, and kept out of the manifest by the caller.
|
|
443
|
+
*
|
|
444
|
+
* Its own function because every writer whose path lands in `owned` needs it, and uninstall
|
|
445
|
+
* deletes what `owned` names: the OpenCode command and primary-agent writers pushed their
|
|
446
|
+
* paths without it, so uninstall removed a user's own `command/commit.md`. ONE closure per
|
|
447
|
+
* emit, shared by all of them, so the pre-manifest header still prints once.
|
|
448
|
+
*
|
|
449
|
+
* `oldOwned === null` (or no `cfg`) is a caller doing no ownership at all: claim everything.
|
|
450
|
+
*/
|
|
451
|
+
export function claimer(oldOwned, cfg, manifestExisted = true) {
|
|
452
|
+
const oldSet = oldOwned !== null && oldOwned !== undefined ? new Set(oldOwned) : null;
|
|
453
|
+
let headerPrinted = false;
|
|
454
|
+
return (dest) => {
|
|
455
|
+
if (oldSet === null || cfg === null || !existsSync(dest)) return true;
|
|
456
|
+
const rel = relPosix(cfg, dest);
|
|
457
|
+
if (oldSet.has(rel)) return true;
|
|
458
|
+
if (!manifestExisted && !headerPrinted) {
|
|
459
|
+
warn('[geneseed] first emit over a pre-manifest install — existing files '
|
|
460
|
+
+ 'are treated as yours');
|
|
461
|
+
headerPrinted = true;
|
|
462
|
+
}
|
|
463
|
+
warn(`[geneseed] kept your existing ${rel} — skipped Geneseed's copy to avoid `
|
|
464
|
+
+ 'clobbering it');
|
|
465
|
+
return false;
|
|
466
|
+
};
|
|
391
467
|
}
|
|
392
468
|
|
|
393
469
|
/**
|
|
@@ -406,6 +482,9 @@ function relPosix(base, target) {
|
|
|
406
482
|
* has the placeholder replaced by the answer. Only vendored folders carry the placeholder — a
|
|
407
483
|
* flat skill has no siblings to point at.
|
|
408
484
|
*
|
|
485
|
+
* `opts.claim` — a `claimer` closure the caller shares with its other writers; built from
|
|
486
|
+
* `oldOwned`/`cfg`/`manifestExisted` when absent.
|
|
487
|
+
*
|
|
409
488
|
* Returns `{ nAgents, nSkills, written }` — Python's 3-tuple, with `written` as absolute
|
|
410
489
|
* paths in write order.
|
|
411
490
|
*
|
|
@@ -419,30 +498,13 @@ function relPosix(base, target) {
|
|
|
419
498
|
*/
|
|
420
499
|
export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null, {
|
|
421
500
|
host = 'opencode', oldOwned = null, cfg = null, manifestExisted = true,
|
|
422
|
-
theme = null, src, skillDirOf = null,
|
|
501
|
+
theme = null, src, skillDirOf = null, claim = claimer(oldOwned, cfg, manifestExisted),
|
|
423
502
|
} = {}) {
|
|
424
503
|
const ov = overrides || {};
|
|
425
|
-
|
|
504
|
+
if (host === 'claude') warnClaudeIgnored(ov);
|
|
426
505
|
let nAgents = 0;
|
|
427
506
|
let nSkills = 0;
|
|
428
507
|
const written = [];
|
|
429
|
-
let headerPrinted = false;
|
|
430
|
-
|
|
431
|
-
// True -> ok to (over)write; false -> a pre-existing file we never owned, so it is the
|
|
432
|
-
// user's: leave it, warn, and keep it out of the manifest.
|
|
433
|
-
const claim = (dest) => {
|
|
434
|
-
if (oldSet === null || cfg === null || !existsSync(dest)) return true;
|
|
435
|
-
const rel = relPosix(cfg, dest);
|
|
436
|
-
if (oldSet.has(rel)) return true;
|
|
437
|
-
if (!manifestExisted && !headerPrinted) {
|
|
438
|
-
warn('[geneseed] first emit over a pre-manifest install — existing files '
|
|
439
|
-
+ 'are treated as yours');
|
|
440
|
-
headerPrinted = true;
|
|
441
|
-
}
|
|
442
|
-
warn(`[geneseed] kept your existing ${rel} — skipped Geneseed's copy to avoid `
|
|
443
|
-
+ 'clobbering it');
|
|
444
|
-
return false;
|
|
445
|
-
};
|
|
446
508
|
|
|
447
509
|
const write = (dest, text) => {
|
|
448
510
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -498,13 +560,9 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
498
560
|
let dest;
|
|
499
561
|
let kind;
|
|
500
562
|
if (folder === 'agents') {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
} else {
|
|
505
|
-
fm = opencodeAgentFrontmatter(stem, text, ov, theme);
|
|
506
|
-
dest = path.join(agentsDir, `${stem}.md`);
|
|
507
|
-
}
|
|
563
|
+
fm = host === 'claude' ? claudeAgentFrontmatter(stem, text, ov)
|
|
564
|
+
: opencodeAgentFrontmatter(stem, text, ov, theme);
|
|
565
|
+
dest = path.join(agentsDir, `${stem}.md`);
|
|
508
566
|
kind = 'agent';
|
|
509
567
|
} else {
|
|
510
568
|
// Skills are BYTE-IDENTICAL across hosts: name + description, body link-stripped.
|
package/js/hosts/opencode.mjs
CHANGED
|
@@ -16,18 +16,21 @@
|
|
|
16
16
|
*
|
|
17
17
|
* TWO STREAMS, AND THE SPLIT IS NOT TIDY. `_warn_if_overrides_stale` prints to STDOUT
|
|
18
18
|
* while every warning in `js/hosts/native.mjs` goes to stderr. That is the Python's own
|
|
19
|
-
* inconsistency and it is reproduced, not corrected:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* a stray stdout byte silently disarms a blocking gate.
|
|
19
|
+
* inconsistency and it is reproduced, not corrected: `tests/unit/agent_overrides.test.mjs`
|
|
20
|
+
* pins the stale notice to stdout. It is safe because this is the generator's CLI, which
|
|
21
|
+
* prints progress to stdout by design — unlike a hook path, where a stray stdout byte
|
|
22
|
+
* silently disarms a blocking gate.
|
|
24
23
|
*/
|
|
25
24
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
26
25
|
import path from 'node:path';
|
|
27
26
|
import { writeText, readText, copyFile } from '../lib/fs.mjs';
|
|
28
27
|
import { jsonDumps, jsonDumpsIndent, parseJson } from '../lib/json.mjs';
|
|
29
28
|
import { comparePaths } from '../lib/paths.mjs';
|
|
30
|
-
import {
|
|
29
|
+
import { sourceReleaseVersion } from '../build/version.mjs';
|
|
30
|
+
import { firstBlockquote, stripSkillBodyLinks, pushOverrideLines } from './native.mjs';
|
|
31
|
+
|
|
32
|
+
// Re-exported for `js/web/api.mjs` and the tests, which import it from here.
|
|
33
|
+
export { sourceReleaseVersion };
|
|
31
34
|
|
|
32
35
|
/** ANSI colour-name -> integer (0-7). Mirrors `_build_emit._ANSI`. */
|
|
33
36
|
const ANSI = {
|
|
@@ -94,9 +97,17 @@ const AGENT_OVERRIDES_STUB = {
|
|
|
94
97
|
},
|
|
95
98
|
};
|
|
96
99
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
/**
|
|
101
|
+
* `_build_emit.COMMAND_SET` — the hot skills worth a one-keystroke trigger, as command name ->
|
|
102
|
+
* skill name. A map, not a list, because the two can differ: `code-review` became
|
|
103
|
+
* `geneseed-code-review` (so it no longer shadows a host built-in) while the command stays
|
|
104
|
+
* `/code-review`. As a list the renamed entry missed its skill and was dropped in silence, so a
|
|
105
|
+
* missing skill is now a thrown error in `writeCommandLayer`.
|
|
106
|
+
*/
|
|
107
|
+
const COMMAND_SET = {
|
|
108
|
+
commit: 'commit', plan: 'plan', 'code-review': 'geneseed-code-review',
|
|
109
|
+
'review-response': 'review-response', ship: 'ship', debug: 'debug', research: 'research',
|
|
110
|
+
};
|
|
100
111
|
|
|
101
112
|
/** Mirrors `_build_emit.PONYTAIL_COMMAND_BODY`. */
|
|
102
113
|
const PONYTAIL_COMMAND_BODY =
|
|
@@ -113,22 +124,6 @@ function truthyEnv(name) {
|
|
|
113
124
|
return ['1', 'on', 'true', 'yes'].includes((process.env[name] || '').toLowerCase());
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
/**
|
|
117
|
-
* `_build_core.source_release_version`. It reads `cfg.config`, so it takes `cfg` rather than
|
|
118
|
-
* importing core — and it stayed HERE when core landed as `js/build/source.mjs`, which is why
|
|
119
|
-
* `js/build/version.mjs` reaches back into the OpenCode module for it.
|
|
120
|
-
*/
|
|
121
|
-
export function sourceReleaseVersion(cfg) {
|
|
122
|
-
try {
|
|
123
|
-
const data = parseJson(readText(cfg.config));
|
|
124
|
-
const v = data && typeof data === 'object' && !Array.isArray(data) ? data.version : null;
|
|
125
|
-
if (typeof v === 'string' && v) return v;
|
|
126
|
-
} catch {
|
|
127
|
-
// OSError / JSONDecodeError — degrade to "not newer" rather than raising.
|
|
128
|
-
}
|
|
129
|
-
return '0.0.0';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
127
|
const GRAY = 8;
|
|
133
128
|
const GREEN = 2;
|
|
134
129
|
const RED = 1;
|
|
@@ -181,7 +176,6 @@ export function writeTheme(themesDir, themeName, theme) {
|
|
|
181
176
|
return dest;
|
|
182
177
|
}
|
|
183
178
|
|
|
184
|
-
/** `_build_emit._color_theme_json`. */
|
|
185
179
|
/**
|
|
186
180
|
* `_build_emit._PALETTE_ROLES` — every role some slot reads, as a set.
|
|
187
181
|
*
|
|
@@ -192,6 +186,7 @@ export function writeTheme(themesDir, themeName, theme) {
|
|
|
192
186
|
*/
|
|
193
187
|
export const PALETTE_ROLES = new Set(Object.values(SLOT_ROLE));
|
|
194
188
|
|
|
189
|
+
/** `_build_emit._color_theme_json`. */
|
|
195
190
|
export function colorThemeJson(palette, transparent) {
|
|
196
191
|
const t = {};
|
|
197
192
|
for (const [slot, role] of Object.entries(SLOT_ROLE)) {
|
|
@@ -272,8 +267,16 @@ export function ensureAgentOverridesStub(cfg, base) {
|
|
|
272
267
|
writeText(dest, `${jsonDumpsIndent(stub, { ensureAscii: false })}\n`);
|
|
273
268
|
}
|
|
274
269
|
|
|
275
|
-
/**
|
|
276
|
-
|
|
270
|
+
/**
|
|
271
|
+
* `_build_emit._write_primary_agent` — the opt-in `mode: primary` orchestrator.
|
|
272
|
+
*
|
|
273
|
+
* `claim` is `claimer`'s closure from `js/hosts/native.mjs`, and this and the two command
|
|
274
|
+
* writers below take it for the reason `writeNativeLayer` does: their paths are pushed into
|
|
275
|
+
* `owned`, and uninstall deletes what `owned` names — so writing over a user's own
|
|
276
|
+
* `command/commit.md` without the check made uninstall delete it. Default: claim everything,
|
|
277
|
+
* for a caller with no manifest to consult.
|
|
278
|
+
*/
|
|
279
|
+
export function writePrimaryAgent(cfg, agentsDir, overrides, claim = () => true) {
|
|
277
280
|
if (!truthyEnv('GENESEED_PRIMARY') || !existsSync(cfg.primaryAgentSrc)
|
|
278
281
|
|| !statSync(cfg.primaryAgentSrc).isFile()) {
|
|
279
282
|
return null;
|
|
@@ -286,13 +289,14 @@ export function writePrimaryAgent(cfg, agentsDir, overrides) {
|
|
|
286
289
|
&& overrides.orchestrator) || {};
|
|
287
290
|
pushOverrideLines(fm, ov);
|
|
288
291
|
const dest = path.join(agentsDir, 'orchestrator.md');
|
|
292
|
+
if (!claim(dest)) return null;
|
|
289
293
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
290
294
|
writeText(dest, `---\n${fm.join('\n')}\n---\n\n${body}`);
|
|
291
295
|
return dest;
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
/** `_build_emit._write_command_layer` — the opt-in /slash commands. */
|
|
295
|
-
export function writeCommandLayer(cfg, items, commandDir) {
|
|
299
|
+
export function writeCommandLayer(cfg, items, commandDir, claim = () => true) {
|
|
296
300
|
if (!truthyEnv('GENESEED_COMMANDS')) return [];
|
|
297
301
|
const byName = new Map();
|
|
298
302
|
for (const { text, src } of items) {
|
|
@@ -304,12 +308,17 @@ export function writeCommandLayer(cfg, items, commandDir) {
|
|
|
304
308
|
}
|
|
305
309
|
}
|
|
306
310
|
const written = [];
|
|
307
|
-
for (const name of COMMAND_SET) {
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
const
|
|
311
|
-
|
|
311
|
+
for (const [name, skill] of Object.entries(COMMAND_SET)) {
|
|
312
|
+
// A command runs a skill; `--exclude-skills` took that skill out, so the command goes too.
|
|
313
|
+
if ((cfg.excludeSkills ?? []).includes(skill)) continue;
|
|
314
|
+
const text = byName.get(skill);
|
|
315
|
+
if (text === undefined) {
|
|
316
|
+
throw new Error(`COMMAND_SET: /${name} runs skill '${skill}', which src/skills/ does not have`);
|
|
317
|
+
}
|
|
312
318
|
const dest = path.join(commandDir, `${name}.md`);
|
|
319
|
+
if (!claim(dest)) continue;
|
|
320
|
+
const desc = firstBlockquote(text);
|
|
321
|
+
const body = stripSkillBodyLinks(text.replace(/^\n+/, ''));
|
|
313
322
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
314
323
|
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${body}`);
|
|
315
324
|
written.push(dest);
|
|
@@ -317,9 +326,10 @@ export function writeCommandLayer(cfg, items, commandDir) {
|
|
|
317
326
|
return written;
|
|
318
327
|
}
|
|
319
328
|
|
|
320
|
-
/** `_build_emit._write_ponytail_command` — registered UNCONDITIONALLY. */
|
|
321
|
-
export function writePonytailCommand(commandDir) {
|
|
329
|
+
/** `_build_emit._write_ponytail_command` — registered UNCONDITIONALLY; null when the user's. */
|
|
330
|
+
export function writePonytailCommand(commandDir, claim = () => true) {
|
|
322
331
|
const dest = path.join(commandDir, 'ponytail.md');
|
|
332
|
+
if (!claim(dest)) return null;
|
|
323
333
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
324
334
|
const desc = 'Set the ponytail minimal-code level for the session: lite | full | ultra | off';
|
|
325
335
|
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${PONYTAIL_COMMAND_BODY}`);
|
package/js/hosts/shim.mjs
CHANGED
|
@@ -273,7 +273,8 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
273
273
|
* interpreter silently disables every hook in the install. Both halves are gone. `runner` is
|
|
274
274
|
* `process.execPath` — the node already running this file, which by construction exists —
|
|
275
275
|
* so there is nothing left to discover and no way for discovery to fail. P4e kept the
|
|
276
|
-
* unreachable exit-3 branch because
|
|
276
|
+
* unreachable exit-3 branch because 'the driver classifies every emit'
|
|
277
|
+
* (`tests/unit/node_driver.test.mjs`) asserts a
|
|
277
278
|
* partition it belongs to; nothing asserts a partition over this one, so keeping it would
|
|
278
279
|
* be keeping code no test can reach for no stated reason.
|
|
279
280
|
*
|
|
@@ -287,7 +288,8 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
287
288
|
*
|
|
288
289
|
* That is correct exactly while the two entry points answer the same verbs the same way, so
|
|
289
290
|
* the gate that used to be a formality is now load-bearing:
|
|
290
|
-
*
|
|
291
|
+
* 'the hook entry carries exactly the verbs the emitter wires' (`tests/unit/hook_cli.test.mjs`)
|
|
292
|
+
* reads the emitter's wiring
|
|
291
293
|
* and `bin/geneseed-hook.mjs`'s VERBS table and requires them EQUAL — a wired verb the entry
|
|
292
294
|
* lacks is a dead hook on every install on the machine, not just this one. The alternative
|
|
293
295
|
* considered and rejected was a per-driver shim path: the path is baked into every already
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
* * **The diff algorithm.** A cell can produce ONE edited file and see one hunk. It cannot
|
|
23
23
|
* produce the shapes where `difflib`'s choices differ from any other diff's — ties, the
|
|
24
24
|
* recursion's alignment, and above all `autojunk`, which only engages at 200 lines and
|
|
25
|
-
* changes the hunks on every real harness file.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* switched off, so the corpus is not describing a constant).
|
|
25
|
+
* changes the hunks on every real harness file. The former Python suite gated it as a
|
|
26
|
+
* corpus, measured in both directions (28 of its 176 cases disagreed when `autojunk` was
|
|
27
|
+
* switched off). That corpus is gone and no Node test drives `unifiedDiff` directly, so
|
|
28
|
+
* beyond the one hunk the `diff/--full-…` cell sees, nothing pins these shapes today.
|
|
30
29
|
* * **The timestamp.** `_write_improvements` names the file `improvements-%Y%m%d-%H%M%S.md`
|
|
31
30
|
* and stamps the report with `datetime.now()`, so the two implementations cannot write the
|
|
32
31
|
* same bytes across a second boundary — and a cell runs them in sequence, so the boundary
|
|
@@ -45,7 +44,8 @@ import {
|
|
|
45
44
|
GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
46
45
|
} from '../hosts/hosts.mjs';
|
|
47
46
|
import {
|
|
48
|
-
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, excludedRulesOfDir,
|
|
47
|
+
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir,
|
|
48
|
+
footprintOfDir, modeOfDir, postureOfDir,
|
|
49
49
|
trustOfDir,
|
|
50
50
|
readJsonMaybe, readMaybe, themeOfDir,
|
|
51
51
|
} from '../hosts/installs.mjs';
|
|
@@ -177,10 +177,12 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
177
177
|
// makes explicit rather than leaving to `makeCfg`'s parameter default two layers down.
|
|
178
178
|
posture: postureOfDir(dir), mode: modeOfDir(dir), doctrines: doctrinesForBuild(dir),
|
|
179
179
|
// The loop skill's preset, or its `Default trust preset:` line reads as edited.
|
|
180
|
-
trust: trustOfDir(dir),
|
|
180
|
+
trust: trustOfDir(dir, host),
|
|
181
181
|
// The excluded rules, or an install built `--exclude-rules "process 5"` reads its
|
|
182
182
|
// carrier as drift: `expected` would re-state the rule the deployment took away.
|
|
183
183
|
excludeRules: excludedRulesOfDir(dir),
|
|
184
|
+
// The excluded skills, or every skill the install left out reads as deleted.
|
|
185
|
+
excludeSkills: excludedSkillsOfDir(dir),
|
|
184
186
|
}));
|
|
185
187
|
// The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
186
188
|
// folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
package/js/inspect/excludes.mjs
CHANGED
|
@@ -78,14 +78,19 @@ const canon = (p) => resolvePath(p);
|
|
|
78
78
|
/**
|
|
79
79
|
* `_same` — is this stored entry the same folder as `repo`?
|
|
80
80
|
*
|
|
81
|
-
* `
|
|
82
|
-
* `
|
|
83
|
-
*
|
|
84
|
-
*
|
|
81
|
+
* Resolved like `repo` itself (`canon`: `..` collapsed, symlinks followed), the one rule the
|
|
82
|
+
* hook's `sovereignBypass` and the OpenCode plugins apply too — so an entry that EXCLUDES a
|
|
83
|
+
* folder is exactly the entry `exclude remove` can take back. It used to compare lexically,
|
|
84
|
+
* mirroring the former Python `Path()`, which made a hand-edited `<repo>/../repo` an entry the
|
|
85
|
+
* plugins honoured, the hook ignored, and `remove` could not find.
|
|
85
86
|
*/
|
|
86
87
|
function same(stored, repo) {
|
|
87
88
|
if (typeof stored !== 'string' || !stored) return false;
|
|
88
|
-
|
|
89
|
+
try {
|
|
90
|
+
return normcase(canon(stored)) === normcase(repo);
|
|
91
|
+
} catch {
|
|
92
|
+
return false; // a `~user` form `expanduser` refuses names no folder
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
package/js/inspect/status.mjs
CHANGED
|
@@ -34,25 +34,26 @@
|
|
|
34
34
|
* became the second and third. Nothing about them changed in the move — the 166-cell
|
|
35
35
|
* acceptance matrix is what licensed attempting it, `status`'s own eleven cells among them.
|
|
36
36
|
*
|
|
37
|
-
* WHAT NO CELL CAN REACH, and where it is gated instead.
|
|
38
|
-
* status/version section
|
|
37
|
+
* WHAT NO CELL CAN REACH, and where it is gated instead. The former Python matrix's
|
|
38
|
+
* status/version section carried the long form; the short form is that ROOT is not
|
|
39
39
|
* redirectable across a process boundary, so three things here are unreachable from any
|
|
40
40
|
* cell: `versionVerdict`'s "up to date" (needs a marker holding a fingerprint no cell can
|
|
41
41
|
* know), `accentFor`'s cyan fallback (an unknown theme is refused upstream by
|
|
42
42
|
* `effectiveTheme`), and the whole ANSI half of `statusLines` (`_color_enabled` is
|
|
43
43
|
* `sys.stdout.isatty()`, and every harness captures stdout through a pipe). All three are
|
|
44
|
-
* PURE FUNCTIONS of their arguments, so
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* PURE FUNCTIONS of their arguments, so the former Python suite gated all three as a corpus
|
|
45
|
+
* instead — a third answer to the colour question, which had been posed as a choice between
|
|
46
|
+
* shipping it ungated and not shipping it. That corpus is gone: `tests/unit/lifecycle.test.mjs`
|
|
47
|
+
* now calls `versionVerdict` and both halves of `statusLines` directly, and nothing pins
|
|
48
|
+
* `accentFor`'s fallback.
|
|
49
49
|
*
|
|
50
50
|
* `manifestIsClaude` was a FOURTH until wave 2 of the P0/P1 review: it is only reached for
|
|
51
51
|
* a candidate with no known host, and `ROOT/"Harness"` was ordered ahead of the sandbox's
|
|
52
52
|
* own. Both ROOT-relative bundle candidates are gone from this walk and from
|
|
53
53
|
* `installedDefaults`, so the only no-known-host candidate left is `cwd/"Harness"`, which
|
|
54
54
|
* is inside the sandbox. It is reachable now; no cell seeds a manifest without an emit
|
|
55
|
-
* marker, so it is
|
|
55
|
+
* marker, so it is gated only by the direct calls in `tests/unit/claude.test.mjs`, and that is
|
|
56
|
+
* a gap rather than a wall.
|
|
56
57
|
*/
|
|
57
58
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
58
59
|
import path from 'node:path';
|
|
@@ -66,19 +67,15 @@ import {
|
|
|
66
67
|
} from '../hosts/hosts.mjs';
|
|
67
68
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
68
69
|
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
69
|
-
// three verbs now read them.
|
|
70
|
-
// re-exported below because `tests/fixtures/pure_probe.mjs` names this module for two of them
|
|
71
|
-
// and a corpus that followed the code to its new file would stop testing the caller's view.
|
|
70
|
+
// three verbs now read them.
|
|
72
71
|
import {
|
|
73
|
-
defaultTheme, installedDefaults, installTargets,
|
|
72
|
+
defaultTheme, installedDefaults, installTargets, readJsonMaybe, readMaybe,
|
|
74
73
|
} from '../hosts/installs.mjs';
|
|
75
74
|
import { installProfile, rebuildCommand } from '../build/generate.mjs';
|
|
76
75
|
import { shimDead } from '../hosts/shim.mjs';
|
|
77
76
|
import { printOut } from '../lib/fs.mjs';
|
|
78
77
|
import { codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
79
78
|
|
|
80
|
-
export { defaultTheme, manifestIsClaude };
|
|
81
|
-
|
|
82
79
|
// ---- version ---------------------------------------------------------------------------
|
|
83
80
|
|
|
84
81
|
/** `_harness_status._version_verdict`. */
|
|
@@ -331,13 +328,13 @@ export function statusLines(d, color = false) {
|
|
|
331
328
|
+ `(${d.facts} fact${d.facts === 1 ? '' : 's'})`],
|
|
332
329
|
['version', `${d.installed_fp || '(none)'} ${DOT} source ${d.source_fp}`],
|
|
333
330
|
];
|
|
334
|
-
// ⚠ A ROW, NOT A WIDER `components` LINE
|
|
335
|
-
//
|
|
336
|
-
// `
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
331
|
+
// ⚠ A ROW, NOT A WIDER `components` LINE. When this landed, `statusLines` had 30 recorded
|
|
332
|
+
// cases per platform in the former Python suite's corpus, every one holding the box's exact
|
|
333
|
+
// bytes — including `17 agents · 47 skills · 37 laws` — and no recorder left to re-bless them.
|
|
334
|
+
// Touching `components` would have moved all 30; adding a row would too, since the box width
|
|
335
|
+
// is measured over `rows`. So the row is CONDITIONAL on a key those arguments did not carry,
|
|
336
|
+
// exactly as `agent_md` below already is. The corpus is gone; a caller without the key still
|
|
337
|
+
// gets the box it got before.
|
|
341
338
|
//
|
|
342
339
|
// It carries the two tiers `components` cannot: `components` counts the invariants as
|
|
343
340
|
// `laws`, which is still true, and says nothing about the ontology or the packs.
|
|
@@ -373,7 +370,8 @@ export function statusLines(d, color = false) {
|
|
|
373
370
|
const packs = p.doctrines.length ? p.doctrines.join(',') : 'no packs';
|
|
374
371
|
const excl = p.excludeRules?.length ? `excluding ${p.excludeRules.join(',')}` : 'no rules excluded';
|
|
375
372
|
rows.push([`${p.host}:${p.scope}`, [p.state, p.theme, p.footprint, `${p.posture}/${p.mode}/${p.trust}`,
|
|
376
|
-
packs, excl
|
|
373
|
+
packs, excl, ...(p.excludeSkills?.length ? [`skills off: ${p.excludeSkills.join(',')}`] : [])]
|
|
374
|
+
.join(` ${DOT} `) + where]);
|
|
377
375
|
}
|
|
378
376
|
|
|
379
377
|
const labelW = Math.max(...rows.map(([k]) => codePointLength(k)));
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -32,7 +32,8 @@ import { isDir, isFile, rglob, withTempDir, within } from './scan.mjs';
|
|
|
32
32
|
export const VALIDATE_USAGE = [
|
|
33
33
|
'usage: geneseed validate [-h] [--theme THEME] [--emit EMIT] [--out OUT] [--root ROOT]',
|
|
34
34
|
' [--footprint {lean,full}] [--posture POSTURE] [--mode MODE]',
|
|
35
|
-
' [--trust TRUST] [--doctrines DOCTRINES] [--exclude-rules RULES]
|
|
35
|
+
' [--trust TRUST] [--doctrines DOCTRINES] [--exclude-rules RULES]',
|
|
36
|
+
' [--exclude-skills SKILLS] [-v]',
|
|
36
37
|
'',
|
|
37
38
|
"Render and emit into a throwaway sandbox, run the doctor's checks, and write nothing real.",
|
|
38
39
|
"Takes the generator's render flags (see geneseed-build --help) except --sync-themes and",
|
|
@@ -127,6 +128,7 @@ export function cmdValidate(args) {
|
|
|
127
128
|
const opts = {
|
|
128
129
|
theme: args.theme, footprint: args.footprint, posture: args.posture, mode: args.mode,
|
|
129
130
|
trust: args.trust, doctrines: args.doctrines, excludeRules: args.excludeRules,
|
|
131
|
+
excludeSkills: args.excludeSkills,
|
|
130
132
|
};
|
|
131
133
|
let scanDirs;
|
|
132
134
|
try {
|
|
@@ -149,8 +151,8 @@ export function cmdValidate(args) {
|
|
|
149
151
|
// which ends `raise SystemExit(res.get("exit", 1))` — an INTEGER — so every refusal
|
|
150
152
|
// that crosses the seam arrives here as `SystemExit(1)` and `str(e)` is `'1'`, not the
|
|
151
153
|
// sentence `load_theme` wrote on stderr on the way out. A port that printed `e.message`
|
|
152
|
-
// says `unknown theme 'x'` where the reference says `1`. The
|
|
153
|
-
//
|
|
154
|
+
// says `unknown theme 'x'` where the reference says `1`. The former Python
|
|
155
|
+
// cross-implementation corpus is what found it; nothing else could.
|
|
154
156
|
if (e && e.exitCode !== undefined) {
|
|
155
157
|
printOut(`[validate-only] render/emit FAILED for theme '${args.theme}' `
|
|
156
158
|
+ `emit '${emit}': ${e.exitCode}\n`);
|