geneseed 3.17.0 → 3.19.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 +39 -0
- package/DESIGN.md +1 -1
- package/README.md +23 -5
- package/SHIPPED.md +3 -3
- package/adapters/claude-code/skills/geneseed/SKILL.md +1 -1
- package/adapters/opencode/README.md +2 -2
- package/adapters/opencode/plugins/geneseed-context.js +19 -17
- package/adapters/opencode/plugins/geneseed-guard.js +7 -7
- package/bin/geneseed-cli.mjs +5 -0
- package/docs/concepts/exclusions.md +1 -1
- package/docs/concepts/skills.md +6 -2
- package/docs/extending.md +33 -0
- package/docs/guides/project-context.md +20 -0
- package/docs/guides/read-non-markdown.md +1 -1
- package/docs/guides/uninstall.md +1 -1
- package/docs/guides/upgrade.md +1 -1
- package/docs/guides/wiki.md +4 -4
- package/docs/opencode-plugin-setup.md +1 -1
- package/docs/reference/env-context.md +2 -2
- package/docs/reference/plugin-context.md +2 -2
- package/docs/reference/plugin-guard.md +1 -1
- package/docs/token-footprint.md +18 -15
- package/docs/understand/take-it-out.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +4 -1
- package/js/build/args.mjs +22 -20
- package/js/build/bundle.mjs +1 -3
- package/js/build/emit-claude.mjs +5 -4
- package/js/build/emit-opencode.mjs +9 -2
- package/js/build/generate.mjs +6 -6
- package/js/build/render.mjs +19 -12
- package/js/build/source.mjs +82 -7
- package/js/build/stubs.mjs +35 -6
- package/js/build/themes.mjs +1 -3
- package/js/cli-table.json +53 -0
- package/js/hosts/hooks-context.mjs +13 -6
- package/js/hosts/hosts.mjs +1 -3
- package/js/hosts/installs.mjs +13 -7
- package/js/hosts/mcp.mjs +1 -1
- package/js/hosts/native.mjs +60 -5
- package/js/hosts/opencode.mjs +37 -6
- package/js/hosts/settings.mjs +1 -0
- package/js/hosts/shim.mjs +1 -0
- package/js/inspect/checks-authoring.mjs +4 -1
- package/js/inspect/checks-counts.mjs +8 -3
- package/js/inspect/checks-repo.mjs +166 -2
- package/js/inspect/diff.mjs +5 -1
- package/js/inspect/doctor.mjs +2 -1
- package/js/inspect/inventory.mjs +7 -9
- package/js/inspect/scorecard-checks.mjs +571 -0
- package/js/inspect/scorecard-svg.mjs +85 -0
- package/js/inspect/scorecard.mjs +839 -0
- package/js/lib/json.mjs +1 -1
- package/js/maintain/migrate.mjs +2 -1
- package/js/maintain/uninstall.mjs +2 -3
- package/js/maintain/update.mjs +10 -3
- package/js/ui/menu.mjs +2 -2
- package/js/ui/tui.mjs +1 -0
- package/js/web/api.mjs +2 -0
- package/js/web/catalog.mjs +24 -7
- package/js/web/daemon.mjs +2 -1
- package/js/web/routes.mjs +2 -0
- package/js/web/server.mjs +1 -1
- package/js/web/user-files.mjs +1 -0
- package/package.json +11 -3
- package/registry.json +12 -75
- package/src/AGENT.md.tmpl +19 -24
- package/src/agents/docs.md +1 -1
- package/src/agents/researcher.md +1 -1
- package/src/agents/reviewer.md +3 -0
- package/src/bricks/bruno-test.md +2 -2
- package/src/bricks/ci-fix.md +3 -3
- package/src/bricks/deps-audit.md +3 -3
- package/src/bricks/migration-plan.md +2 -2
- package/src/doctrines/comms.md +19 -0
- package/src/doctrines/process.md +2 -2
- package/src/laws/universal.md +2 -2
- package/src/skills/brainstorm.md +14 -10
- package/src/skills/bruno.md +84 -0
- package/src/skills/codebase-design.md +41 -23
- package/src/skills/daydream/SKILL.md +4 -3
- package/src/skills/daydream/VENDOR.md +6 -2
- package/src/skills/daydream/instructions.md +1 -1
- package/src/skills/debug/ci.md +11 -0
- package/src/skills/debug.md +11 -8
- package/src/skills/dependencies.md +65 -0
- package/src/skills/develop.md +9 -4
- package/src/skills/document-project.md +27 -22
- package/src/skills/explain-changes/SKILL.md +1 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/geneseed-code-review.md +21 -9
- package/src/skills/geneseed.md +4 -1
- package/src/skills/git-history.md +78 -0
- package/src/skills/handoff.md +4 -3
- package/src/skills/parallel-agents.md +2 -1
- package/src/skills/pipeline.md +1 -1
- package/src/skills/plan.md +5 -1
- package/src/skills/ponytail.md +1 -1
- package/src/skills/prototype.md +10 -7
- package/src/skills/react-view-transitions/SKILL.md +1 -0
- package/src/skills/react-view-transitions/VENDOR.md +4 -0
- package/src/skills/research.md +6 -1
- package/src/skills/review-response.md +8 -2
- package/src/skills/roast-me.md +8 -7
- package/src/skills/rule/consolidate.md +32 -0
- package/src/skills/rule.md +16 -4
- package/src/skills/security-audit.md +1 -1
- package/src/skills/ship.md +16 -9
- package/src/skills/teach.md +5 -3
- package/src/skills/wayfinder.md +19 -16
- package/src/skills/wiki.md +2 -2
- package/themes/_TEMPLATE.json +9 -17
- package/themes/biker.json +9 -17
- package/themes/commentator.json +9 -17
- package/themes/cyberpunk.json +9 -17
- package/themes/gamer.json +9 -17
- package/themes/imperial.json +9 -17
- package/themes/joker.json +9 -17
- package/themes/marvin.json +9 -17
- package/themes/mean.json +9 -17
- package/themes/military.json +9 -17
- package/themes/neutral.json +9 -17
- package/themes/pirate.json +9 -17
- package/themes/sports.json +9 -17
- package/themes/verstappen.json +9 -17
- package/themes/wizard.json +9 -17
- package/web/dist/assets/{Activity-DonKN1pi.js → Activity-IbwdHHyx.js} +1 -1
- package/web/dist/assets/{ActivityDetail-e47a3cdE.js → ActivityDetail-B5uivdWI.js} +1 -1
- package/web/dist/assets/{Dashboard-Cr7i0PsO.js → Dashboard-v6WQ37bD.js} +2 -2
- package/web/dist/assets/{Docs-Lne3nuzD.js → Docs-CypIfDBX.js} +1 -1
- package/web/dist/assets/FilterInput-BSfpStOZ.js +1 -0
- package/web/dist/assets/{GreenhouseView-4CbIuTpl.js → GreenhouseView-ClxP0hQG.js} +1 -1
- package/web/dist/assets/{Installs-CIzbV1Ks.js → Installs-CPkQeFjO.js} +1 -1
- package/web/dist/assets/{JournalView-BlBfuvzj.js → JournalView-BeEInhUf.js} +1 -1
- package/web/dist/assets/{Laws-B-pjcjHr.js → Laws-Ds6U5xBQ.js} +1 -1
- package/web/dist/assets/Library-BraUKPyQ.js +1 -0
- package/web/dist/assets/{Loops-DIOxW1xH.js → Loops-DWcy3mwK.js} +1 -1
- package/web/dist/assets/Markdown-C6fRd4A4.js +67 -0
- package/web/dist/assets/{OperatorHudView-D7S7z7CJ.js → OperatorHudView-CQyrJQ9m.js} +1 -1
- package/web/dist/assets/{Personal-Ws3v35AB.js → Personal-YWqs6qFL.js} +1 -1
- package/web/dist/assets/{Tabs-6Z1eziAq.js → Tabs-EhzEC-Xh.js} +1 -1
- package/web/dist/assets/{activity-Dr7ZMBI3.js → activity-dcg5vF9j.js} +1 -1
- package/web/dist/assets/{drawable-ATNeYKbT.js → drawable-CEKPNV7X.js} +1 -1
- package/web/dist/assets/{index-CDg5XUhG.js → index-CPi22b7y.js} +2 -2
- package/web/dist/assets/react-DRhoX0kS.js +9 -0
- package/web/dist/assets/{usePoll-DD8VjtSg.js → usePoll-BmR631BB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +5 -0
- package/src/skills/bruno-collection-generator.md +0 -52
- package/src/skills/bruno-test-writer.md +0 -51
- package/src/skills/ci-fix.md +0 -49
- package/src/skills/consolidate-memory.md +0 -50
- package/src/skills/deps-audit.md +0 -46
- package/src/skills/domain-modeling.md +0 -27
- package/src/skills/git-archaeology.md +0 -36
- package/src/skills/git-rescue.md +0 -58
- package/src/skills/migrate.md +0 -34
- package/src/skills/opencode-theme.md +0 -42
- package/src/skills/repo-map.md +0 -25
- package/src/skills/tickets.md +0 -33
- package/web/dist/assets/FilterInput-CVyqmYWM.js +0 -1
- package/web/dist/assets/Library-x8Nzg6yq.js +0 -1
- package/web/dist/assets/Markdown-X2a3qvOh.js +0 -38
- package/web/dist/assets/react-B9Z_tXRV.js +0 -8
package/js/cli-table.json
CHANGED
|
@@ -1079,6 +1079,59 @@
|
|
|
1079
1079
|
],
|
|
1080
1080
|
"mutex": []
|
|
1081
1081
|
},
|
|
1082
|
+
{
|
|
1083
|
+
"name": "scorecard",
|
|
1084
|
+
"help": "grade a repo with the AI Harness Scorecard (31 checks, five pillars) — scores the repo itself (CI, tests, docs), not the harness files; a reading, never a gate",
|
|
1085
|
+
"description": "",
|
|
1086
|
+
"positionals": [
|
|
1087
|
+
{
|
|
1088
|
+
"names": [],
|
|
1089
|
+
"dest": "path",
|
|
1090
|
+
"metavar": null,
|
|
1091
|
+
"help": "the repo to score (default: the current directory)",
|
|
1092
|
+
"choices": null,
|
|
1093
|
+
"default": null,
|
|
1094
|
+
"required": false,
|
|
1095
|
+
"nargs": "?",
|
|
1096
|
+
"is_flag": false,
|
|
1097
|
+
"type": null,
|
|
1098
|
+
"hidden": false
|
|
1099
|
+
}
|
|
1100
|
+
],
|
|
1101
|
+
"options": [
|
|
1102
|
+
{
|
|
1103
|
+
"names": [
|
|
1104
|
+
"--json"
|
|
1105
|
+
],
|
|
1106
|
+
"dest": "json",
|
|
1107
|
+
"metavar": null,
|
|
1108
|
+
"help": "print the full assessment as JSON — every check with its score, evidence and fix",
|
|
1109
|
+
"choices": null,
|
|
1110
|
+
"default": false,
|
|
1111
|
+
"required": false,
|
|
1112
|
+
"nargs": "0",
|
|
1113
|
+
"is_flag": true,
|
|
1114
|
+
"type": null,
|
|
1115
|
+
"hidden": false
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
"names": [
|
|
1119
|
+
"--svg"
|
|
1120
|
+
],
|
|
1121
|
+
"dest": "svg",
|
|
1122
|
+
"metavar": "PATH",
|
|
1123
|
+
"help": "write the scorecard as an SVG card (grade + one bar per pillar) to PATH instead of printing — the README card is docs/assets/scorecard.svg",
|
|
1124
|
+
"choices": null,
|
|
1125
|
+
"default": null,
|
|
1126
|
+
"required": false,
|
|
1127
|
+
"nargs": null,
|
|
1128
|
+
"is_flag": false,
|
|
1129
|
+
"type": null,
|
|
1130
|
+
"hidden": false
|
|
1131
|
+
}
|
|
1132
|
+
],
|
|
1133
|
+
"mutex": []
|
|
1134
|
+
},
|
|
1082
1135
|
{
|
|
1083
1136
|
"name": "setup",
|
|
1084
1137
|
"help": "interactive install wizard (dependency-free, all OSes)",
|
|
@@ -81,8 +81,8 @@ let HOST = 'claude';
|
|
|
81
81
|
// Kept in step with adapters/opencode/plugins/geneseed-context.js.
|
|
82
82
|
const EAGER_ROOT = ['AGENTS.md', 'AGENT.md', 'CLAUDE.md', '.cursorrules',
|
|
83
83
|
'README.md', 'README.adoc', 'CONTRIBUTING.md', 'CONTRIBUTING.adoc', 'user-rules.md', 'PROFILE.md'];
|
|
84
|
-
// Project docs are Markdown or AsciiDoc: the document-project skill writes AsciiDoc
|
|
85
|
-
//
|
|
84
|
+
// Project docs are Markdown or AsciiDoc: the document-project skill writes AsciiDoc when the
|
|
85
|
+
// user asks for it or the docs already are, so a loader blind to `.adoc` would hide those docs.
|
|
86
86
|
// Agent-runtime files (AGENTS.md, CLAUDE.md, specs) stay Markdown whatever the project uses.
|
|
87
87
|
const DOC_EXT = new Set(['.md', '.adoc']);
|
|
88
88
|
const isDoc = (name) => DOC_EXT.has(path.extname(name).toLowerCase());
|
|
@@ -126,7 +126,10 @@ const cost = (s) => s.length + (s.match(/\n/g) || []).length + 2;
|
|
|
126
126
|
// honoured by `resolveContextSets` (the harness dir is its last candidate). The OpenCode
|
|
127
127
|
// context plugin carries the same list.
|
|
128
128
|
const SESSION_FILES = ['user-rules.md', 'PROFILE.md', 'memory/MEMORY.md', 'anamnesis/MEMORY.md',
|
|
129
|
-
'notebook/NOTEBOOK.md', 'wiki.jsonc'];
|
|
129
|
+
'notebook/NOTEBOOK.md', 'geneseed-wiki.jsonc'];
|
|
130
|
+
// The wiki manifest's earlier name, read when the new one is absent (an install not yet re-emitted,
|
|
131
|
+
// or a rename that failed). One release; `wiki.json`, older still, is the build's to rename.
|
|
132
|
+
const LEGACY_WIKI = 'wiki.jsonc';
|
|
130
133
|
// SHA-256 of each seed body in `js/build/stubs.mjs` (`SESSION_SEEDS`), CRLF folded. A file still
|
|
131
134
|
// byte-identical to its seed says nothing, so it is skipped rather than injected as noise.
|
|
132
135
|
// Hashes, not an import: stubs.mjs pulls in the build's writers, and this module loads on every
|
|
@@ -134,7 +137,9 @@ const SESSION_FILES = ['user-rules.md', 'PROFILE.md', 'memory/MEMORY.md', 'anamn
|
|
|
134
137
|
const SEED_SHA256 = new Set([
|
|
135
138
|
'5c2e92fb1acde041e02d9ebf158460d8ff7a07cbc2b447859631604610130721', // user-rules.md
|
|
136
139
|
'29e012c3c4349ea62a9082cbd04bb25599a0ec280d62c33f6e10742809471816', // PROFILE.md
|
|
137
|
-
'
|
|
140
|
+
'3a82c78ccc5d745033c16c354f5eda9d79350c704e9da8d605d380fb1e108d7a', // geneseed-wiki.jsonc
|
|
141
|
+
// The wiki seed before the rename: the build renames an untouched one byte for byte.
|
|
142
|
+
'cccc917c34e6b990e821620bdb4739090151885ab71b6e85046f30a3b71fa5e8', // wiki.jsonc (legacy)
|
|
138
143
|
'99c786049c260f6baf7aec68a5c0f59907861afe9b867da009440613bdddb907', // MEMORY.md
|
|
139
144
|
'9acb5c9d9cb5dac572104480f05b48cc144e676869bd60e86d8a15b1f6308184', // NOTEBOOK.md
|
|
140
145
|
]);
|
|
@@ -150,8 +155,9 @@ export function sessionFiles(hookRoot) {
|
|
|
150
155
|
const found = [];
|
|
151
156
|
for (const rel of SESSION_FILES) {
|
|
152
157
|
let abs = path.join(hookRoot, rel);
|
|
153
|
-
if (rel === 'wiki.jsonc'
|
|
154
|
-
abs = process.env.GENESEED_WIKI;
|
|
158
|
+
if (rel === 'geneseed-wiki.jsonc') {
|
|
159
|
+
if (process.env.GENESEED_WIKI && isFile(process.env.GENESEED_WIKI)) abs = process.env.GENESEED_WIKI;
|
|
160
|
+
else if (!isFile(abs)) abs = path.join(hookRoot, LEGACY_WIKI);
|
|
155
161
|
}
|
|
156
162
|
if (!isFile(abs)) continue;
|
|
157
163
|
let text;
|
|
@@ -256,6 +262,7 @@ export function discoverContext(root, host = HOST) {
|
|
|
256
262
|
*
|
|
257
263
|
* `recs` is a Map, not an object: JS reorders integer-like keys on a plain object, and
|
|
258
264
|
* these keys are absolute paths whose iteration order becomes the printed order.
|
|
265
|
+
* @returns {[Array<{path: string, description: string}>, Array<{path: string, description: string}>, string]}
|
|
259
266
|
*/
|
|
260
267
|
export function resolveContextSets(root, hookRoot = null) {
|
|
261
268
|
let manifest = null;
|
package/js/hosts/hosts.mjs
CHANGED
|
@@ -128,9 +128,7 @@ export function expanduser(p) {
|
|
|
128
128
|
+ "ntpath/posixpath would guess a home directory for 'user' — this port does not); "
|
|
129
129
|
+ 'pass an absolute path, or "~" / "~/…" for your own home directory';
|
|
130
130
|
printErr(`${msg}\n`);
|
|
131
|
-
|
|
132
|
-
e.exitCode = 1;
|
|
133
|
-
throw e;
|
|
131
|
+
throw Object.assign(new Error(msg), { exitCode: 1 });
|
|
134
132
|
}
|
|
135
133
|
return p;
|
|
136
134
|
}
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import { existsSync, readdirSync } from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
|
-
import { CONFIG, PACK_ORDER, THEMES, discoverNames,
|
|
38
|
+
import { CONFIG, PACK_ORDER, THEMES, discoverNames, resolveSkillNames } from '../build/source.mjs';
|
|
39
39
|
import { CLAUDE_STYLE, GLOBAL_MANIFEST, HOSTS, resolvePath } from './hosts.mjs';
|
|
40
40
|
import { registryRoots } from '../inspect/registry.mjs';
|
|
41
41
|
import { printErr, readText, isFile, isDir } from '../lib/fs.mjs';
|
|
@@ -408,7 +408,7 @@ export function excludedRulesOfDir(d) {
|
|
|
408
408
|
}, []);
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
/** `Excluded skills: bruno
|
|
411
|
+
/** `Excluded skills: bruno, daydream` — written only when something is excluded. */
|
|
412
412
|
const EXCLUDED_SKILLS_RE = /^Excluded skills:[ \t]*(.+?)[ \t]*$/m;
|
|
413
413
|
|
|
414
414
|
/**
|
|
@@ -416,17 +416,23 @@ const EXCLUDED_SKILLS_RE = /^Excluded skills:[ \t]*(.+?)[ \t]*$/m;
|
|
|
416
416
|
* re-render the same set. Same semantics as `excludedRulesOfDir`: a missing line is `[]`
|
|
417
417
|
* (nothing excluded), and a name this checkout does not ship condemns the whole line to `[]`
|
|
418
418
|
* — re-admitting a skill is the safe direction, and the flag would refuse the name anyway.
|
|
419
|
+
*
|
|
420
|
+
* A name that only got OLDER is not unknown: an alias reads back as its target and a retired
|
|
421
|
+
* name drops out (`resolveSkillNames`), so a skill merge does not quietly re-admit everything
|
|
422
|
+
* else the install left out. Silent unless the caller passes `notify` — status, diff and the
|
|
423
|
+
* console call this on every read; the replays that rebuild an install (`rebuild-all`,
|
|
424
|
+
* `upgrade`, `migrate`) pass it.
|
|
419
425
|
*/
|
|
420
|
-
export function excludedSkillsOfDir(d) {
|
|
426
|
+
export function excludedSkillsOfDir(d, notify = null) {
|
|
421
427
|
return firstCarrier(d, (carrierPath) => {
|
|
422
428
|
const text = readMaybe(carrierPath);
|
|
423
429
|
if (text === null) return undefined;
|
|
424
430
|
const m = EXCLUDED_SKILLS_RE.exec(text);
|
|
425
431
|
if (!m) return undefined;
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
return
|
|
432
|
+
const { ids, notices, unknown } = resolveSkillNames(m[1].split(',').map((s) => s.trim()).filter(Boolean));
|
|
433
|
+
if (unknown.length) return [];
|
|
434
|
+
if (notify) for (const n of notices) notify(n);
|
|
435
|
+
return ids;
|
|
430
436
|
}, []);
|
|
431
437
|
}
|
|
432
438
|
|
package/js/hosts/mcp.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* at "~90 new, already in js/: `_mcp_load`/`readJsonc` (P6c), `settings.mjs`'s atomic write".
|
|
6
6
|
* Measured against `js/`: `readJsonc` had crossed and NOTHING ELSE HAD. `js/web/api.mjs`
|
|
7
7
|
* carries a five-line `mcpLoad` that is the no-host, comment-tolerant reader P6c needed for
|
|
8
|
-
* `wiki.jsonc`, which is one arm of one of these thirteen functions. `_MCP_PRESETS`,
|
|
8
|
+
* `geneseed-wiki.jsonc`, which is one arm of one of these thirteen functions. `_MCP_PRESETS`,
|
|
9
9
|
* `_mcp_servers_key`, `_mcp_preset_block`, `_mcp_apply`, `_mcp_state`, `_mcp_set_enabled`,
|
|
10
10
|
* `_mcp_load`'s host fork, `_mcp_commented`, `_mcp_save`, `_mcp_config_for`,
|
|
11
11
|
* `_mcp_install_targets`, `_mcp_known_names` and `_mcp_meta` are 168 lines of Python, all
|
package/js/hosts/native.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import path from 'node:path';
|
|
|
32
32
|
import { writeText, copyFile, readText } from '../lib/fs.mjs';
|
|
33
33
|
import { jsonDumps, parseJson, formatValue, formatRepr, isTruthy, isDict } from '../lib/json.mjs';
|
|
34
34
|
import { relPosix } from '../lib/text.mjs';
|
|
35
|
+
import { aliasesOf } from '../build/source.mjs';
|
|
35
36
|
|
|
36
37
|
/** What a folder skill writes where it needs its own directory — see `writeNativeLayer`. */
|
|
37
38
|
const SKILL_DIR_PLACEHOLDER = '<this-skill-directory>';
|
|
@@ -253,6 +254,25 @@ export function stripSkillBodyLinks(body) {
|
|
|
253
254
|
return body.replace(SKILL_BODY_LINK_RE, '$1');
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* A first-party skill's SIDE FILES (`src/skills/<stem>/<file>`, beside the flat spec) are
|
|
259
|
+
* linked from the spec as `[…](<stem>/<file>)` — the path that resolves in `src/` and in the
|
|
260
|
+
* flat bundle, where doctor's dead-link scan proves it. A native skill sits one level deeper
|
|
261
|
+
* (`skills/<stem>/SKILL.md`), and an alias or a command copies the body somewhere else again,
|
|
262
|
+
* so there the link becomes a code-span path under `dir`: the placeholder a host that
|
|
263
|
+
* announces the skill's base directory resolves, or the answer for one that does not.
|
|
264
|
+
*
|
|
265
|
+
* A link with an `#anchor` (which `sideFileProblems` accepts) gets the same pointer with the
|
|
266
|
+
* anchor DROPPED: the pointer is a path the agent hands to a file-read tool, and
|
|
267
|
+
* `deep.md#two` names no file. Left unmatched, `stripSkillBodyLinks` would reduce the link to
|
|
268
|
+
* its label and the agent would never learn where the file is.
|
|
269
|
+
*/
|
|
270
|
+
export function pointSideFiles(body, stem, dir) {
|
|
271
|
+
// A function, not a `$1` string: `dir` is a real path and may itself hold a `$`.
|
|
272
|
+
return body.replace(new RegExp(`\\[[^\\]]+\\]\\(${stem}/([^)\\s#]+)(?:#[^)\\s]*)?\\)`, 'g'),
|
|
273
|
+
(_m, f) => `\`${dir}/${f}\``);
|
|
274
|
+
}
|
|
275
|
+
|
|
256
276
|
/** `_build_emit._agent_color_map` — validated, always a valid OpenCode slot. */
|
|
257
277
|
function agentColorMap(theme) {
|
|
258
278
|
let raw = null;
|
|
@@ -479,8 +499,8 @@ export function claimer(oldOwned, cfg, manifestExisted = true) {
|
|
|
479
499
|
* `<this-skill-directory>/…`; Claude Code resolves that because it announces each skill's
|
|
480
500
|
* base directory on load, while on Bob the model got the text with a hole in it and went
|
|
481
501
|
* hunting through the filesystem. With the option, every rendered `.md` of a vendored folder
|
|
482
|
-
* has the placeholder replaced by the answer.
|
|
483
|
-
*
|
|
502
|
+
* has the placeholder replaced by the answer. A flat skill with side files points at them
|
|
503
|
+
* through the same option (`pointSideFiles`), placeholder when it is absent.
|
|
484
504
|
*
|
|
485
505
|
* `opts.claim` — a `claimer` closure the caller shares with its other writers; built from
|
|
486
506
|
* `oldOwned`/`cfg`/`manifestExisted` when absent.
|
|
@@ -498,13 +518,15 @@ export function claimer(oldOwned, cfg, manifestExisted = true) {
|
|
|
498
518
|
*/
|
|
499
519
|
export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null, {
|
|
500
520
|
host = 'opencode', oldOwned = null, cfg = null, manifestExisted = true,
|
|
501
|
-
theme = null, src, skillDirOf = null, claim = claimer(oldOwned, cfg, manifestExisted),
|
|
521
|
+
theme = null, src = undefined, skillDirOf = null, claim = claimer(oldOwned, cfg, manifestExisted),
|
|
502
522
|
} = {}) {
|
|
503
523
|
const ov = overrides || {};
|
|
504
524
|
if (host === 'claude') warnClaudeIgnored(ov);
|
|
505
525
|
let nAgents = 0;
|
|
506
526
|
let nSkills = 0;
|
|
507
527
|
const written = [];
|
|
528
|
+
const sideFiles = []; // [stem, dest, text], written after the loop
|
|
529
|
+
const claimedSkills = new Set(); // stems whose own SKILL.md claim held
|
|
508
530
|
|
|
509
531
|
const write = (dest, text) => {
|
|
510
532
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -534,6 +556,15 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
534
556
|
continue;
|
|
535
557
|
}
|
|
536
558
|
if (text === null) continue;
|
|
559
|
+
// A first-party side file rides next to its skill's SKILL.md, read only when the body
|
|
560
|
+
// points the agent at it; not a skill, so not counted. `sideFileProblems` holds the
|
|
561
|
+
// layout (a flat spec beside it, one level, `.md`, linked from the body). Held until the
|
|
562
|
+
// loop ends: written only once its skill's own SKILL.md claim held (the alias rule), so a
|
|
563
|
+
// user's file at the target never gets a geneseed side file dropped beside it.
|
|
564
|
+
if (sparts.length === 3 && sparts[0] === 'skills') {
|
|
565
|
+
sideFiles.push([sparts[1], path.join(skillsDir, sparts[1], sparts[2]), text]);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
537
568
|
if (sparts.length !== 2 || !sparts[1].endsWith('.md')) continue;
|
|
538
569
|
const [folder, fname] = sparts;
|
|
539
570
|
const targetDir = { agents: agentsDir, skills: skillsDir }[folder];
|
|
@@ -567,18 +598,42 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
567
598
|
} else {
|
|
568
599
|
// Skills are BYTE-IDENTICAL across hosts: name + description, body link-stripped.
|
|
569
600
|
// The user-only key is emitted for every host too — Claude Code and Bob honour it,
|
|
570
|
-
// OpenCode ignores an unknown key — so the identity holds.
|
|
601
|
+
// OpenCode ignores an unknown key — so the identity holds. ALIASES are the one
|
|
602
|
+
// exception: OpenCode has no user-only skill, so an alias skill there would land in the
|
|
603
|
+
// model's catalogue as a duplicate; it gets a command instead (`writeAliasCommands`).
|
|
571
604
|
fm = [`name: ${stem}`, `description: ${jsonDumps(skillDescription(text))}`];
|
|
572
605
|
if (isUserInvokedOnly(text)) fm.push('disable-model-invocation: true');
|
|
573
|
-
body = stripSkillBodyLinks(body)
|
|
606
|
+
body = stripSkillBodyLinks(pointSideFiles(body, stem, skillDirOf ? skillDirOf(stem)
|
|
607
|
+
: SKILL_DIR_PLACEHOLDER));
|
|
574
608
|
dest = path.join(skillsDir, stem, 'SKILL.md');
|
|
575
609
|
kind = 'skill';
|
|
576
610
|
}
|
|
577
611
|
if (!claim(dest)) continue;
|
|
578
612
|
write(dest, `---\n${fm.join('\n')}\n---\n\n${body}`);
|
|
613
|
+
if (kind === 'skill') claimedSkills.add(stem);
|
|
614
|
+
// An old name keeps answering `/name`: user-only, so the model's catalogue lists the
|
|
615
|
+
// skill once. Read off the already-filtered items, so excluding the target drops its
|
|
616
|
+
// aliases; through `claim`, so manifest, prune and uninstall own them; not counted.
|
|
617
|
+
// Only once the target's own claim held: a user's file at the target gets no alias
|
|
618
|
+
// pointing at a skill this install never wrote.
|
|
619
|
+
if (kind === 'skill' && host === 'claude') {
|
|
620
|
+
// The host announces the ALIAS's folder as the base directory, so a side-file pointer
|
|
621
|
+
// climbs back to the target's.
|
|
622
|
+
const aliasBody = skillDirOf ? body : stripSkillBodyLinks(
|
|
623
|
+
pointSideFiles(lstripNewlines(text), stem, `${SKILL_DIR_PLACEHOLDER}/../${stem}`));
|
|
624
|
+
for (const alias of aliasesOf(text)) {
|
|
625
|
+
const aliasDest = path.join(skillsDir, alias, 'SKILL.md');
|
|
626
|
+
if (!claim(aliasDest)) continue;
|
|
627
|
+
write(aliasDest, `---\nname: ${alias}\ndescription: ${jsonDumps(`Alias of ${stem}.`)}\n`
|
|
628
|
+
+ `disable-model-invocation: true\n---\n\n${aliasBody}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
579
631
|
if (kind === 'agent') nAgents += 1;
|
|
580
632
|
else nSkills += 1;
|
|
581
633
|
}
|
|
634
|
+
for (const [stem, dest, text] of sideFiles) {
|
|
635
|
+
if (claimedSkills.has(stem) && claim(dest)) write(dest, stripSkillBodyLinks(text));
|
|
636
|
+
}
|
|
582
637
|
return { nAgents, nSkills, written };
|
|
583
638
|
}
|
|
584
639
|
|
package/js/hosts/opencode.mjs
CHANGED
|
@@ -27,7 +27,8 @@ import { writeText, readText, copyFile } from '../lib/fs.mjs';
|
|
|
27
27
|
import { jsonDumps, jsonDumpsIndent, parseJson } from '../lib/json.mjs';
|
|
28
28
|
import { comparePaths } from '../lib/paths.mjs';
|
|
29
29
|
import { sourceReleaseVersion } from '../build/version.mjs';
|
|
30
|
-
import {
|
|
30
|
+
import { aliasesOf } from '../build/source.mjs';
|
|
31
|
+
import { firstBlockquote, stripSkillBodyLinks, pointSideFiles, pushOverrideLines } from './native.mjs';
|
|
31
32
|
|
|
32
33
|
// Re-exported for `js/web/api.mjs` and the tests, which import it from here.
|
|
33
34
|
export { sourceReleaseVersion };
|
|
@@ -104,7 +105,7 @@ const AGENT_OVERRIDES_STUB = {
|
|
|
104
105
|
* `/code-review`. As a list the renamed entry missed its skill and was dropped in silence, so a
|
|
105
106
|
* missing skill is now a thrown error in `writeCommandLayer`.
|
|
106
107
|
*/
|
|
107
|
-
const COMMAND_SET = {
|
|
108
|
+
export const COMMAND_SET = {
|
|
108
109
|
commit: 'commit', plan: 'plan', 'code-review': 'geneseed-code-review',
|
|
109
110
|
'review-response': 'review-response', ship: 'ship', debug: 'debug', research: 'research',
|
|
110
111
|
};
|
|
@@ -276,7 +277,7 @@ export function ensureAgentOverridesStub(cfg, base) {
|
|
|
276
277
|
* `command/commit.md` without the check made uninstall delete it. Default: claim everything,
|
|
277
278
|
* for a caller with no manifest to consult.
|
|
278
279
|
*/
|
|
279
|
-
export function writePrimaryAgent(cfg, agentsDir, overrides, claim = () => true) {
|
|
280
|
+
export function writePrimaryAgent(cfg, agentsDir, overrides, claim = (_dest) => true) {
|
|
280
281
|
if (!truthyEnv('GENESEED_PRIMARY') || !existsSync(cfg.primaryAgentSrc)
|
|
281
282
|
|| !statSync(cfg.primaryAgentSrc).isFile()) {
|
|
282
283
|
return null;
|
|
@@ -296,7 +297,8 @@ export function writePrimaryAgent(cfg, agentsDir, overrides, claim = () => true)
|
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
/** `_build_emit._write_command_layer` — the opt-in /slash commands. */
|
|
299
|
-
export function writeCommandLayer(cfg, items, commandDir, claim = () => true
|
|
300
|
+
export function writeCommandLayer(cfg, items, commandDir, claim = (_dest) => true,
|
|
301
|
+
skillDirOf = (n) => path.join(path.dirname(commandDir), 'skills', n).split(path.sep).join('/')) {
|
|
300
302
|
if (!truthyEnv('GENESEED_COMMANDS')) return [];
|
|
301
303
|
const byName = new Map();
|
|
302
304
|
for (const { text, src } of items) {
|
|
@@ -318,7 +320,8 @@ export function writeCommandLayer(cfg, items, commandDir, claim = () => true) {
|
|
|
318
320
|
const dest = path.join(commandDir, `${name}.md`);
|
|
319
321
|
if (!claim(dest)) continue;
|
|
320
322
|
const desc = firstBlockquote(text);
|
|
321
|
-
|
|
323
|
+
// A command has no base directory, so a side-file pointer names the skill's own.
|
|
324
|
+
const body = stripSkillBodyLinks(pointSideFiles(text.replace(/^\n+/, ''), skill, skillDirOf(skill)));
|
|
322
325
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
323
326
|
writeText(dest, `---\ndescription: ${jsonDumps(desc)}\n---\n\n${body}`);
|
|
324
327
|
written.push(dest);
|
|
@@ -326,8 +329,36 @@ export function writeCommandLayer(cfg, items, commandDir, claim = () => true) {
|
|
|
326
329
|
return written;
|
|
327
330
|
}
|
|
328
331
|
|
|
332
|
+
/**
|
|
333
|
+
* A merged skill's OLD names as `/alias` commands — OpenCode's form of the user-only alias
|
|
334
|
+
* skill `writeNativeLayer` writes for the Claude dialect. A command, not a skill, because
|
|
335
|
+
* OpenCode has no user-only skill: an alias skill would sit in the model's catalogue as a
|
|
336
|
+
* second copy of its target. UNCONDITIONAL, unlike the `GENESEED_COMMANDS` set — an old name
|
|
337
|
+
* that only works behind an opt-in is a name that broke. Read off the already-filtered
|
|
338
|
+
* `items`, so excluding the target drops its aliases.
|
|
339
|
+
*/
|
|
340
|
+
export function writeAliasCommands(cfg, items, commandDir, claim = (_dest) => true,
|
|
341
|
+
skillDirOf = (n) => path.join(path.dirname(commandDir), 'skills', n).split(path.sep).join('/')) {
|
|
342
|
+
const written = [];
|
|
343
|
+
for (const { text, src } of items) {
|
|
344
|
+
if (text === null) continue;
|
|
345
|
+
const sp = path.relative(cfg.src, src).split(path.sep);
|
|
346
|
+
if (sp.length !== 2 || sp[0] !== 'skills' || !sp[1].endsWith('.md') || sp[1].startsWith('_')) continue;
|
|
347
|
+
const stem = sp[1].slice(0, -3);
|
|
348
|
+
const body = stripSkillBodyLinks(pointSideFiles(text.replace(/^\n+/, ''), stem, skillDirOf(stem)));
|
|
349
|
+
for (const alias of aliasesOf(text)) {
|
|
350
|
+
const dest = path.join(commandDir, `${alias}.md`);
|
|
351
|
+
if (!claim(dest)) continue;
|
|
352
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
353
|
+
writeText(dest, `---\ndescription: ${jsonDumps(`Alias of ${sp[1].slice(0, -3)}.`)}\n---\n\n${body}`);
|
|
354
|
+
written.push(dest);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return written;
|
|
358
|
+
}
|
|
359
|
+
|
|
329
360
|
/** `_build_emit._write_ponytail_command` — registered UNCONDITIONALLY; null when the user's. */
|
|
330
|
-
export function writePonytailCommand(commandDir, claim = () => true) {
|
|
361
|
+
export function writePonytailCommand(commandDir, claim = (_dest) => true) {
|
|
331
362
|
const dest = path.join(commandDir, 'ponytail.md');
|
|
332
363
|
if (!claim(dest)) return null;
|
|
333
364
|
mkdirSync(path.dirname(dest), { recursive: true });
|
package/js/hosts/settings.mjs
CHANGED
|
@@ -165,6 +165,7 @@ const OWNED_BASH = ['rm -rf *', 'git commit*', 'git push*', ...LAW_IV_BASH];
|
|
|
165
165
|
* caller warns on it. This is the one branch where the boundary can end up with NO Geneseed
|
|
166
166
|
* gate at all while the constitution states the rule — the unsafe direction — so it must not
|
|
167
167
|
* also be the silent one.
|
|
168
|
+
* @returns {[boolean, string[], string | null]}
|
|
168
169
|
*/
|
|
169
170
|
function reconcileOpencodePermission(perm, want) {
|
|
170
171
|
// A `permission` that is not an object, or whose `bash` is not one — OpenCode also accepts a
|
package/js/hosts/shim.mjs
CHANGED
|
@@ -215,6 +215,7 @@ function shimIsLive(p) {
|
|
|
215
215
|
* The fallback is strictly no worse than the old behaviour and far better than emitting
|
|
216
216
|
* commands naming a shim that does not exist — those fail on every hook (9009 under
|
|
217
217
|
* cmd.exe, 127 under sh) and take both gates down with them.
|
|
218
|
+
* @param {{ runner?: string, entry?: string, platform?: NodeJS.Platform }} [opts]
|
|
218
219
|
*/
|
|
219
220
|
export function hookPrefix({ runner, entry, platform = process.platform } = {}) {
|
|
220
221
|
// NOT a default. `runner`/`entry` have no computable fallback on this side — this process's
|
|
@@ -19,7 +19,8 @@ import { readText } from '../lib/fs.mjs';
|
|
|
19
19
|
import { which } from '../lib/paths.mjs';
|
|
20
20
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
21
21
|
import {
|
|
22
|
-
folderSkillProblems, registryProblems, secretProblems,
|
|
22
|
+
aliasProblems, folderSkillProblems, registryProblems, secretProblems, sideFileProblems,
|
|
23
|
+
vendorPinProblems,
|
|
23
24
|
} from './checks-repo.mjs';
|
|
24
25
|
import { constitutionProblems, leanBlockProblems } from './checks-constitution.mjs';
|
|
25
26
|
import { countTableProblems } from './checks-counts.mjs';
|
|
@@ -109,6 +110,8 @@ export function authoringProblems() {
|
|
|
109
110
|
problems.push(...secretProblems());
|
|
110
111
|
problems.push(...vendorPinProblems());
|
|
111
112
|
problems.push(...folderSkillProblems());
|
|
113
|
+
problems.push(...aliasProblems());
|
|
114
|
+
problems.push(...sideFileProblems());
|
|
112
115
|
problems.push(...constitutionProblems());
|
|
113
116
|
problems.push(...leanBlockProblems());
|
|
114
117
|
problems.push(...countTableProblems());
|
|
@@ -116,7 +116,7 @@ const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
|
116
116
|
* badges, against `src/`.
|
|
117
117
|
*
|
|
118
118
|
* The tables must list EXACTLY the spec files (no dead row, no orphaned spec) and each
|
|
119
|
-
* `agents`/`skills`/`laws`/`themes` badge must equal the real count. This is the
|
|
119
|
+
* `agents`/`skills`/`laws`/`doctrines`/`themes` badge must equal the real count. This is the
|
|
120
120
|
* authoring-time guarantee that lets tables, badges and prose stay hand-written without
|
|
121
121
|
* silently drifting from the source tree.
|
|
122
122
|
*/
|
|
@@ -151,7 +151,8 @@ export function countTableProblems() {
|
|
|
151
151
|
|
|
152
152
|
// The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS and the
|
|
153
153
|
// console's LAW_META are keyed the way the rendered catalogue numbers them.
|
|
154
|
-
const
|
|
154
|
+
const canonRules = ruleCanon(SRC).rules;
|
|
155
|
+
const lawRules = canonRules.filter((r) => r.kind === 'LAW');
|
|
155
156
|
const lawNums = lawRules.map((r) => r.label);
|
|
156
157
|
for (const num of lawNums) {
|
|
157
158
|
if (!has(LAW_CLASS, num)) {
|
|
@@ -179,6 +180,9 @@ export function countTableProblems() {
|
|
|
179
180
|
agents: srcStems('agents').size,
|
|
180
181
|
skills: shippedSkills.size,
|
|
181
182
|
laws: lawNums.length,
|
|
183
|
+
// Every doctrine rule in every pack, opt-in ones included: the laws badge alone told a
|
|
184
|
+
// reader the harness has 9 rules. It rides the laws badge (`laws-9 + 28 doctrines`).
|
|
185
|
+
doctrines: canonRules.filter((r) => r.kind === 'DOCTRINE').length,
|
|
182
186
|
themes: themeFiles().length,
|
|
183
187
|
plugins: existsSync(PLUGIN_SRC)
|
|
184
188
|
? readdirSync(PLUGIN_SRC).filter((f) => f.startsWith('geneseed-') && f.endsWith('.js')).length
|
|
@@ -190,7 +194,8 @@ export function countTableProblems() {
|
|
|
190
194
|
// same reason: `if (m && …)` is green when there is no badge, so deleting a badge silences its
|
|
191
195
|
// own gate.
|
|
192
196
|
for (const [key, n] of Object.entries(counts)) {
|
|
193
|
-
const m =
|
|
197
|
+
const m = (key === 'doctrines' ? /badge\/laws-\d+%20%2B%20(\d+)%20doctrines/
|
|
198
|
+
: new RegExp(`badge/${key}-(\\d+)`)).exec(readme);
|
|
194
199
|
if (!m) {
|
|
195
200
|
problems.push(`[authoring] README has no ${key} badge — deleting one is how this gate `
|
|
196
201
|
+ 'goes green while the reader is told nothing');
|