geneseed 4.9.4 → 4.14.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 +106 -0
- package/README.md +16 -2
- package/SHIPPED.md +1 -1
- package/docs/README.md +1 -1
- package/docs/concepts/collaboration.md +1 -1
- package/docs/concepts/enforcement-table.md +1 -1
- package/docs/concepts/foreman-mode.md +1 -1
- package/docs/concepts/kits.md +91 -0
- package/docs/concepts/loop-branch.md +1 -1
- package/docs/concepts/loop-bricks.md +1 -1
- package/docs/concepts/loop-contracts.md +1 -1
- package/docs/concepts/loop-gates.md +1 -1
- package/docs/concepts/loop-notes.md +1 -1
- package/docs/concepts/loop-risk.md +1 -1
- package/docs/concepts/loop-templates.md +1 -1
- package/docs/concepts/loops.md +1 -1
- package/docs/concepts/lsp.md +1 -1
- package/docs/concepts/skills.md +1 -1
- package/docs/extending.md +107 -4
- package/docs/guides/install-scripted.md +1 -1
- package/docs/guides/run-anywhere.md +2 -2
- package/docs/guides/upgrade.md +2 -2
- package/docs/reference/glossary.md +1 -0
- package/docs/reference/troubleshoot-install.md +1 -1
- package/docs/web-ui.md +9 -0
- package/harness.config.json +1 -1
- package/js/build/args.mjs +27 -6
- package/js/build/driver.mjs +29 -8
- package/js/build/emit-claude.mjs +3 -1
- package/js/build/generate.mjs +10 -5
- package/js/build/render.mjs +94 -11
- package/js/build/source.mjs +117 -4
- package/js/cli-table.json +15 -0
- package/js/hosts/installs.mjs +35 -2
- package/js/hosts/link.mjs +88 -1
- package/js/hosts/native.mjs +14 -4
- package/js/inspect/checks-authoring.mjs +6 -3
- package/js/inspect/checks-build.mjs +15 -3
- package/js/inspect/checks-counts.mjs +153 -6
- package/js/inspect/checks-repo.mjs +95 -20
- package/js/inspect/cost.mjs +1 -1
- package/js/inspect/diff.mjs +56 -8
- package/js/inspect/inventory.mjs +2 -0
- package/js/inspect/status.mjs +44 -3
- package/js/inspect/validate.mjs +3 -3
- package/js/lib/prompt.mjs +19 -2
- package/js/maintain/setup.mjs +111 -23
- package/js/maintain/update.mjs +3 -3
- package/js/web/actions.mjs +38 -6
- package/js/web/api.mjs +76 -17
- package/js/web/catalog.mjs +17 -2
- package/js/web/handler.mjs +1 -0
- package/js/web/routes.mjs +2 -1
- package/js/web/server.mjs +11 -5
- package/js/web/state.mjs +38 -3
- package/package.json +1 -1
- package/registry.json +91 -0
- package/src/AGENT.md.tmpl +7 -7
- package/src/agents/_self-improvement.md +11 -0
- package/src/agents/_template.md +1 -8
- package/src/agents/advocate.md +1 -8
- package/src/agents/architect.md +1 -8
- package/src/agents/developer.md +1 -8
- package/src/agents/devops-engineer.md +37 -0
- package/src/agents/docker-expert.md +37 -0
- package/src/agents/docs.md +1 -8
- package/src/agents/empiricist.md +1 -8
- package/src/agents/explorer.md +1 -8
- package/src/agents/framer.md +1 -8
- package/src/agents/historian.md +1 -8
- package/src/agents/kubernetes-specialist.md +37 -0
- package/src/agents/memory-curator.md +1 -8
- package/src/agents/operator.md +1 -8
- package/src/agents/performance.md +1 -8
- package/src/agents/pragmatist.md +1 -8
- package/src/agents/react-engineer.md +38 -0
- package/src/agents/react-reviewer.md +42 -0
- package/src/agents/researcher.md +5 -10
- package/src/agents/reviewer.md +3 -8
- package/src/agents/security.md +1 -8
- package/src/agents/skeptic.md +1 -8
- package/src/agents/spring-boot-engineer.md +40 -0
- package/src/agents/spring-reviewer.md +41 -0
- package/src/agents/spring-security.md +42 -0
- package/src/agents/steward.md +1 -8
- package/src/agents/tester.md +1 -8
- package/src/agents/user-advocate.md +1 -8
- package/src/agents/visionary.md +1 -8
- package/src/kits/react/rules.md +13 -0
- package/src/kits/react.json +18 -0
- package/src/kits/spring-boot/rules.md +10 -0
- package/src/kits/spring-boot.json +23 -0
- package/src/licenses/everything-claude-code.txt +21 -0
- package/src/licenses/mattpocock-skills.txt +21 -0
- package/src/licenses/piomin-spring-boot.txt +190 -0
- package/src/licenses/superpowers.txt +21 -0
- package/src/licenses/vibe-wise.txt +21 -0
- package/src/skills/_self-improvement.md +22 -5
- package/src/skills/brainstorm/builder-check.md +2 -0
- package/src/skills/brainstorm.md +11 -2
- package/src/skills/codebase-design.md +10 -2
- package/src/skills/daydream/VENDOR.md +7 -7
- package/src/skills/debug.md +6 -2
- package/src/skills/develop.md +6 -2
- package/src/skills/geneseed-code-review.md +6 -2
- package/src/skills/git-history.md +2 -0
- package/src/skills/handoff.md +3 -1
- package/src/skills/learn-mode.md +3 -2
- package/src/skills/parallel-agents.md +5 -1
- package/src/skills/pipeline.md +2 -0
- package/src/skills/plan.md +9 -2
- package/src/skills/prototype.md +6 -2
- package/src/skills/react/a11y.md +33 -0
- package/src/skills/react/vite.md +35 -0
- package/src/skills/react/webpack.md +40 -0
- package/src/skills/react-testing.md +68 -0
- package/src/skills/react-view-transitions/AGENTS.md +646 -221
- package/src/skills/react-view-transitions/SKILL.md +28 -16
- package/src/skills/react-view-transitions/VENDOR.md +10 -8
- package/src/skills/react-view-transitions/references/css-recipes.md +72 -2
- package/src/skills/react-view-transitions/references/implementation.md +14 -26
- package/src/skills/react-view-transitions/references/nextjs.md +94 -37
- package/src/skills/react-view-transitions/references/patterns.md +60 -48
- package/src/skills/react-view-transitions/references/troubleshooting.md +47 -0
- package/src/skills/react.md +63 -0
- package/src/skills/research.md +6 -2
- package/src/skills/review-response.md +2 -0
- package/src/skills/rule.md +6 -2
- package/src/skills/ship.md +8 -4
- package/src/skills/skill-forge.md +11 -5
- package/src/skills/spring-boot/cloud.md +103 -0
- package/src/skills/spring-boot/java-modern.md +71 -0
- package/src/skills/spring-boot/logging.md +89 -0
- package/src/skills/spring-boot/security.md +112 -0
- package/src/skills/spring-boot/web.md +112 -0
- package/src/skills/spring-boot.md +57 -0
- package/src/skills/spring-data-jpa/patterns.md +68 -0
- package/src/skills/spring-data-jpa.md +81 -0
- package/src/skills/spring-testing.md +93 -0
- package/src/skills/wayfinder.md +8 -2
- package/src/skills/wiki.md +18 -5
- package/src/upstreams.json +66 -0
- package/themes/_TEMPLATE.json +13 -0
- package/themes/biker.json +13 -0
- package/themes/commentator.json +13 -0
- package/themes/cyberpunk.json +13 -0
- package/themes/gamer.json +13 -0
- package/themes/imperial.json +13 -0
- package/themes/joker.json +13 -0
- package/themes/marvin.json +13 -0
- package/themes/mean.json +13 -0
- package/themes/military.json +13 -0
- package/themes/neutral.json +13 -0
- package/themes/pirate.json +13 -0
- package/themes/sports.json +13 -0
- package/themes/verstappen.json +13 -0
- package/themes/wizard.json +13 -0
- package/web/dist/assets/Activity-44rbWUQI.js +1 -0
- package/web/dist/assets/ActivityDetail-Dk9k893a.js +1 -0
- package/web/dist/assets/Dashboard-DCoUN60W.js +2 -0
- package/web/dist/assets/{Docs-tDW7i7mo.js → Docs-C_je2lRG.js} +3 -3
- package/web/dist/assets/FilterInput-CZREvrjX.js +1 -0
- package/web/dist/assets/GreenhouseView-BXAOIznW.js +2 -0
- package/web/dist/assets/Installs-DAXF403D.js +3 -0
- package/web/dist/assets/JournalView-DZ184wKB.js +1 -0
- package/web/dist/assets/KitApply-pjhvGha0.js +1 -0
- package/web/dist/assets/Kits-BCCKjSIY.js +1 -0
- package/web/dist/assets/{Laws-BA3CzUf5.js → Laws-BgBUpRkE.js} +1 -1
- package/web/dist/assets/Library-CW0Zl6RF.js +1 -0
- package/web/dist/assets/Loops-BvFOzjEw.js +2 -0
- package/web/dist/assets/{Markdown-iHvBrpRN.js → Markdown-zaXXXHz9.js} +13 -13
- package/web/dist/assets/OperatorHudView-Bs4unPNi.js +2 -0
- package/web/dist/assets/Personal-C23XaT-a.js +1 -0
- package/web/dist/assets/Tabs-b2XFu4OS.js +1 -0
- package/web/dist/assets/activity-Jw8XcPiP.js +1 -0
- package/web/dist/assets/api-BfpZmUMI.js +1 -0
- package/web/dist/assets/{drawable-CyLe1gtW.js → drawable-BERkKhx_.js} +1 -1
- package/web/dist/assets/index-C9R_bRw_.js +2 -0
- package/web/dist/assets/{index-BCJz4_8C.css → index-jYlWihN-.css} +1 -1
- package/web/dist/assets/{react-BOF6yL40.js → react-DlYQp10C.js} +1 -1
- package/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/web/dist/assets/sections-ChwaobfW.js +1 -0
- package/web/dist/assets/usePoll-bDFN-Hr6.js +1 -0
- package/web/dist/index.html +6 -5
- package/web/dist/assets/Activity-C8sZVIUU.js +0 -1
- package/web/dist/assets/ActivityDetail-Cc6vWUS4.js +0 -1
- package/web/dist/assets/Dashboard-C5ATa3ow.js +0 -2
- package/web/dist/assets/FilterInput-zLFXoSt6.js +0 -1
- package/web/dist/assets/GreenhouseView-zk2Uzc3A.js +0 -2
- package/web/dist/assets/Installs-CB_rvbBP.js +0 -3
- package/web/dist/assets/JournalView-Bfj27XHC.js +0 -1
- package/web/dist/assets/Library-BWwiy9Vb.js +0 -1
- package/web/dist/assets/Loops-DaPk-ckL.js +0 -2
- package/web/dist/assets/OperatorHudView-CyiGhiHk.js +0 -2
- package/web/dist/assets/Personal-Bwd27CmT.js +0 -1
- package/web/dist/assets/Tabs-BOD7Kaz1.js +0 -1
- package/web/dist/assets/activity-DvPxoS4Y.js +0 -1
- package/web/dist/assets/api-CiXlkbe0.js +0 -1
- package/web/dist/assets/index-DQXG_mqo.js +0 -2
- package/web/dist/assets/sections-BET3c1WT.js +0 -1
- package/web/dist/assets/usePoll-NSTzLOF4.js +0 -1
package/docs/web-ui.md
CHANGED
|
@@ -114,6 +114,11 @@ streams the output of background jobs and keeps their history across reloads.
|
|
|
114
114
|
**Promote to rule** control turns a recurring lesson into a trial rule in
|
|
115
115
|
`user-rules.md` — provenance recorded, the source fact deleted so it isn't loaded
|
|
116
116
|
twice — landing you on the Rules page.
|
|
117
|
+
- **Kits** (`#/kits`, `#/kits/<kit>`) — the list of shipped kits and, per kit, a detail page
|
|
118
|
+
with its rules, word budget, banned APIs, upstream credits and one switch per install.
|
|
119
|
+
`GET /api/kits` carries `rules`, `rulesWordBudget`, `banned` and `credits` per kit; the
|
|
120
|
+
catalog and item endpoints for skills and agents carry `kit` (the owning kit or `null`),
|
|
121
|
+
which the Library shows as a `kit:` badge.
|
|
117
122
|
- **Loops** (`#/loops`) — the loop catalogue `geneseed loop` runs from, read-only, laid out
|
|
118
123
|
like the Library: a rail of sections (**Templates**, **Bricks**, **Active**, each with its
|
|
119
124
|
count; `#/loops/<section>/<item>` opens an entry), with the selected section's groups
|
|
@@ -194,6 +199,10 @@ streams the output of background jobs and keeps their history across reloads.
|
|
|
194
199
|
card (shown once a global install exists) manages sovereign repos — folders where every
|
|
195
200
|
global harness goes dormant (hooks stay silent, the global preamble never loads);
|
|
196
201
|
add or remove one here, the web mirror of `harness exclude add|remove|list`.
|
|
202
|
+
- **Kits** (`#/installs/kits`) — every install (disabled ones read-only) as a panel with one switch per kit.
|
|
203
|
+
Toggling only stages the set; **Apply** asks first, then rebuilds that install with
|
|
204
|
+
exactly that set as a console job. Files you edited in a kit that is now off are kept,
|
|
205
|
+
and the row lists them.
|
|
197
206
|
- **Settings** (`#/settings`) — a **Console direction** picker (the visual flavour of the
|
|
198
207
|
console: Cultivar, Operator, Matrix or Atlas) with a **Dashboard layout** control (Auto follows each theme's designed Status
|
|
199
208
|
lens; Cultivar / Greenhouse / Operator / Journal force one regardless of skin), the
|
package/harness.config.json
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
"_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
|
|
3
3
|
"theme": "neutral",
|
|
4
4
|
"product": "Geneseed",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.14.0"
|
|
6
6
|
}
|
package/js/build/args.mjs
CHANGED
|
@@ -11,7 +11,8 @@ import { parseArgs as nodeParseArgs } from 'node:util';
|
|
|
11
11
|
import { withPlatformNewlines } from '../lib/fs.mjs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import {
|
|
14
|
-
CONFIG, discoverNames, GATE_SWITCHES, PACK_ORDER, knownRuleIds, knownSkillIds, linkedSkillIds,
|
|
14
|
+
CONFIG, discoverNames, GATE_SWITCHES, PACK_ORDER, SRC, knownRuleIds, knownSkillIds, linkedSkillIds, loadKits,
|
|
15
|
+
resolveSkillNames, skillAliases,
|
|
15
16
|
} from './source.mjs';
|
|
16
17
|
// The loop engine's trust presets — the `--trust` choices. `score.mjs` imports nothing, which is
|
|
17
18
|
// what lets it sit inside this module's `child_process` ban.
|
|
@@ -32,7 +33,7 @@ const EMITS = ['files', 'opencode', 'opencode-global', 'claude', 'claude-global'
|
|
|
32
33
|
export function configDefaults() {
|
|
33
34
|
const d = {
|
|
34
35
|
theme: 'neutral', posture: 'peer', mode: 'direct', trust: DEFAULT_PRESET,
|
|
35
|
-
doctrines: [...PACK_ORDER], excludeRules: [], excludeSkills: [],
|
|
36
|
+
doctrines: [...PACK_ORDER], excludeRules: [], excludeSkills: [], kits: [],
|
|
36
37
|
};
|
|
37
38
|
if (!existsSync(CONFIG)) return d;
|
|
38
39
|
try {
|
|
@@ -96,6 +97,7 @@ export function configDefaults() {
|
|
|
96
97
|
const VALUED = {
|
|
97
98
|
'--theme': 'theme', '--posture': 'posture', '--mode': 'mode', '--trust': 'trust',
|
|
98
99
|
'--doctrines': 'doctrines', '--exclude-rules': 'excludeRules', '--exclude-skills': 'excludeSkills',
|
|
100
|
+
'--kits': 'kits',
|
|
99
101
|
'--out': 'out', '--target': 'out', '--emit': 'emit',
|
|
100
102
|
'--footprint': 'footprint', '--root': 'root', '--config-dir': 'cfgDir',
|
|
101
103
|
};
|
|
@@ -196,13 +198,13 @@ function parseExcludeRules(value) {
|
|
|
196
198
|
* through `resolveSkillNames` and leave one stderr line each; the linked refusal stays for a
|
|
197
199
|
* name typed as itself.
|
|
198
200
|
*/
|
|
199
|
-
function parseExcludeSkills(value) {
|
|
201
|
+
export function parseExcludeSkills(value, src = SRC) {
|
|
200
202
|
const s = value.trim();
|
|
201
203
|
if (s === 'none' || s === '') return [];
|
|
202
|
-
const known = knownSkillIds();
|
|
203
|
-
const linked = linkedSkillIds();
|
|
204
|
+
const known = knownSkillIds(src);
|
|
205
|
+
const linked = linkedSkillIds(src);
|
|
204
206
|
const names = s.split(',').map((x) => x.trim()).filter(Boolean);
|
|
205
|
-
const { ids, notices, unknown } = resolveSkillNames(names, { known, linked });
|
|
207
|
+
const { ids, notices, unknown } = resolveSkillNames(names, { known, linked, aliases: skillAliases(src) });
|
|
206
208
|
if (unknown.length) {
|
|
207
209
|
die(2, `argument --exclude-skills: unknown skill '${unknown[0]}' (choose from `
|
|
208
210
|
+ `${known.filter((k) => !linked.includes(k)).join(', ')}, or 'none')`);
|
|
@@ -216,6 +218,23 @@ function parseExcludeSkills(value) {
|
|
|
216
218
|
return ids;
|
|
217
219
|
}
|
|
218
220
|
|
|
221
|
+
/**
|
|
222
|
+
* `--kits a,b` / `--kits none` -> the cfg's kit list, sorted and deduped for the same marker
|
|
223
|
+
* stability as `--exclude-skills`. `none` and '' mean no kits; an unknown kit is refused, naming
|
|
224
|
+
* the known ones (spec R1). No `harness.config.json` key feeds it, for `excludeSkills`' reason.
|
|
225
|
+
*/
|
|
226
|
+
export function parseKits(value, known = [...loadKits().keys()]) {
|
|
227
|
+
const s = value.trim();
|
|
228
|
+
if (s === 'none' || s === '') return [];
|
|
229
|
+
const names = [...new Set(s.split(',').map((x) => x.trim()).filter(Boolean))].sort();
|
|
230
|
+
const unknown = names.find((n) => !known.includes(n));
|
|
231
|
+
if (unknown) {
|
|
232
|
+
const choices = known.length ? `${known.join(', ')}, or 'none'` : "'none'";
|
|
233
|
+
die(2, `argument --kits: unknown kit '${unknown}' (choose from ${choices})`);
|
|
234
|
+
}
|
|
235
|
+
return names;
|
|
236
|
+
}
|
|
237
|
+
|
|
219
238
|
/**
|
|
220
239
|
* `--doctrines craft,rigor` / `--doctrines none` -> the cfg's pack array.
|
|
221
240
|
*
|
|
@@ -313,6 +332,7 @@ export function parseArgs(argv, defaults) {
|
|
|
313
332
|
doctrines: defaults.doctrines,
|
|
314
333
|
excludeRules: defaults.excludeRules,
|
|
315
334
|
excludeSkills: defaults.excludeSkills ?? [],
|
|
335
|
+
kits: defaults.kits ?? [],
|
|
316
336
|
out: 'Harness', emit: 'files', footprint: 'lean', root: null,
|
|
317
337
|
syncThemes: false, validateOnly: false, verbose: false,
|
|
318
338
|
...Object.fromEntries(GATE_SWITCHES.map((g) => [g.arg, false])),
|
|
@@ -356,6 +376,7 @@ export function parseArgs(argv, defaults) {
|
|
|
356
376
|
if (typeof args.doctrines === 'string') args.doctrines = parseDoctrines(args.doctrines);
|
|
357
377
|
if (typeof args.excludeRules === 'string') args.excludeRules = parseExcludeRules(args.excludeRules);
|
|
358
378
|
if (typeof args.excludeSkills === 'string') args.excludeSkills = parseExcludeSkills(args.excludeSkills);
|
|
379
|
+
if (typeof args.kits === 'string') args.kits = parseKits(args.kits);
|
|
359
380
|
return args;
|
|
360
381
|
}
|
|
361
382
|
|
package/js/build/driver.mjs
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
// registry to find every install it must re-emit, and a CLI verb may not reach into a driver
|
|
66
66
|
// for a reader. See js/inspect/registry.mjs.
|
|
67
67
|
import { registryRecord, registryRoots } from '../inspect/registry.mjs';
|
|
68
|
+
import { kitsOfDir } from '../hosts/installs.mjs';
|
|
68
69
|
|
|
69
70
|
// P2. `--sync-themes` crossed into its own module rather than into this file: it is 90 lines
|
|
70
71
|
// of textual surgery over committed files with a corpus of its own, and it is the half of the
|
|
@@ -651,12 +652,12 @@ const PROJECT_EMITS = {
|
|
|
651
652
|
* carry straight past the parameter default.
|
|
652
653
|
*/
|
|
653
654
|
function axesCfg({
|
|
654
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates
|
|
655
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, kits, ...gates
|
|
655
656
|
}) {
|
|
656
657
|
return makeCfg({
|
|
657
658
|
...(posture ? { posture } : {}), ...(mode ? { mode } : {}), ...(trust ? { trust } : {}),
|
|
658
659
|
...(doctrines ? { doctrines } : {}), ...(excludeRules ? { excludeRules } : {}),
|
|
659
|
-
...(excludeSkills ? { excludeSkills } : {}),
|
|
660
|
+
...(excludeSkills ? { excludeSkills } : {}), ...(kits ? { kits } : {}),
|
|
660
661
|
// The gate switches (`GATE_SWITCHES` keys): only an explicit `false` disarms; `null` (no
|
|
661
662
|
// opinion) keeps the gate.
|
|
662
663
|
...Object.fromEntries(GATE_SWITCHES.filter((g) => gates[g.key] === false).map((g) => [g.arg, true])),
|
|
@@ -665,7 +666,7 @@ function axesCfg({
|
|
|
665
666
|
|
|
666
667
|
export function emitProjectInto(host, {
|
|
667
668
|
theme, out, root, footprint = 'full', posture = null, mode = null, trust = null,
|
|
668
|
-
doctrines = null, excludeRules = null, excludeSkills = null, ...gates
|
|
669
|
+
doctrines = null, excludeRules = null, excludeSkills = null, kits = null, ...gates
|
|
669
670
|
}) {
|
|
670
671
|
const emit = PROJECT_EMITS[host];
|
|
671
672
|
// The axes ride the same rail into every `*Into`, and for the same reason: a drift reader
|
|
@@ -674,7 +675,7 @@ export function emitProjectInto(host, {
|
|
|
674
675
|
// checked an all-packs render nobody asked for.
|
|
675
676
|
return withPlatformNewlines(() => emit(
|
|
676
677
|
axesCfg({
|
|
677
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates }),
|
|
678
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, kits, ...gates }),
|
|
678
679
|
{ theme, footprint, root }, out,
|
|
679
680
|
));
|
|
680
681
|
}
|
|
@@ -689,16 +690,16 @@ export function emitProjectInto(host, {
|
|
|
689
690
|
*/
|
|
690
691
|
export function buildInto({
|
|
691
692
|
theme, out, footprint = 'lean', posture = null, mode = null, trust = null, doctrines = null,
|
|
692
|
-
excludeRules = null, excludeSkills = null, ...gates
|
|
693
|
+
excludeRules = null, excludeSkills = null, kits = null, ...gates
|
|
693
694
|
}) {
|
|
694
695
|
return withPlatformNewlines(() => build(axesCfg({
|
|
695
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates }),
|
|
696
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, kits, ...gates }),
|
|
696
697
|
theme, out, { footprint, nativeCatalog: false }));
|
|
697
698
|
}
|
|
698
699
|
|
|
699
700
|
export function emitGlobalInto(host, {
|
|
700
701
|
theme, out, cfgDir, footprint, posture = null, mode = null, doctrines = null, trust = null,
|
|
701
|
-
excludeRules = null, excludeSkills = null, ...gates
|
|
702
|
+
excludeRules = null, excludeSkills = null, kits = null, ...gates
|
|
702
703
|
}) {
|
|
703
704
|
// `build.HOSTS.get(host, build.HOSTS["opencode"])` — an unknown host falls back rather than
|
|
704
705
|
// raising, because the host comes from a marker file a user can edit.
|
|
@@ -715,7 +716,7 @@ export function emitGlobalInto(host, {
|
|
|
715
716
|
// the same scar the footprint left in `diffCollect`, one axis over each time.
|
|
716
717
|
return withPlatformNewlines(() => emit(
|
|
717
718
|
axesCfg({
|
|
718
|
-
posture, mode, trust, doctrines, excludeRules, excludeSkills, ...gates }),
|
|
719
|
+
posture, mode, trust, doctrines, excludeRules, excludeSkills, kits, ...gates }),
|
|
719
720
|
{ theme, footprint, root: null, cfgDir }, out,
|
|
720
721
|
));
|
|
721
722
|
}
|
|
@@ -749,6 +750,25 @@ export function main(argv) {
|
|
|
749
750
|
});
|
|
750
751
|
}
|
|
751
752
|
|
|
753
|
+
/**
|
|
754
|
+
* Spec B3: a build does what its flags say, so one without `--kits` turns off a kit the target
|
|
755
|
+
* install has on. Said BEFORE the emit rewrites the marker. The target is the emit's own: the
|
|
756
|
+
* config dir for a `-global` emit, `out` otherwise. `kitsOfDir` has already dropped a kit this
|
|
757
|
+
* checkout no longer ships, so the advice never names one `--kits` would refuse. Best-effort.
|
|
758
|
+
*/
|
|
759
|
+
function warnDroppedKits(args, out) {
|
|
760
|
+
try {
|
|
761
|
+
const global = args.emit.endsWith('-global');
|
|
762
|
+
const host = global ? args.emit.slice(0, -'-global'.length) : (args.emit === 'files' ? null : args.emit);
|
|
763
|
+
const target = global ? (args.cfgDir ?? HOSTS.find((h) => h.host === host).configDir()) : out;
|
|
764
|
+
for (const k of kitsOfDir(target, host).filter((x) => !args.kits.includes(x))) {
|
|
765
|
+
process.stderr.write(`[geneseed] WARN: this install has kit ${k}; this build turns it off — `
|
|
766
|
+
+ `pass --kits ${k} to keep it
|
|
767
|
+
`);
|
|
768
|
+
}
|
|
769
|
+
} catch { /* a warning never fails a build */ }
|
|
770
|
+
}
|
|
771
|
+
|
|
752
772
|
function run(argv) {
|
|
753
773
|
const args = parseArgs(argv, configDefaults());
|
|
754
774
|
|
|
@@ -783,6 +803,7 @@ function run(argv) {
|
|
|
783
803
|
args.cfgDir = resolveOut(args.cfgDir);
|
|
784
804
|
}
|
|
785
805
|
const cfg = makeCfg(args);
|
|
806
|
+
warnDroppedKits(args, out);
|
|
786
807
|
|
|
787
808
|
// The marker directory is the emit's TARGET, which for a global emit is the config dir it
|
|
788
809
|
// just rendered into and not `--out` at all — so the emit returns it rather than the
|
package/js/build/emit-claude.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { assertSourceComplete, phaseLog, ensureMemoryStore } from './bundle.mjs'
|
|
|
19
19
|
import {
|
|
20
20
|
claimAndWrite, globalMemory, globalNotebook, shipLeanLaws, stripCapabilityLinks,
|
|
21
21
|
} from './emit-common.mjs';
|
|
22
|
-
import { renderAll, renderFile } from './render.mjs';
|
|
22
|
+
import { kitsBody, renderAll, renderFile } from './render.mjs';
|
|
23
23
|
import {
|
|
24
24
|
BOB_RULES_STUB, ensureContextStub, ensureExcludesStub, ensureNotebookIndex,
|
|
25
25
|
ensureProfileStub,
|
|
@@ -82,6 +82,8 @@ export function emitClaudeRender(cfg, job) {
|
|
|
82
82
|
ptheme[tok] = prefix + (ptheme[tok] ?? tok.split('_').slice(1).join('_').toLowerCase());
|
|
83
83
|
}
|
|
84
84
|
ptheme.AT_HARNESS = prefix;
|
|
85
|
+
// The Kits block was substituted with the unprefixed DIR_* tokens; rebuild it, or its links dangle.
|
|
86
|
+
ptheme.KITS_BODY = kitsBody(cfg, ptheme, '');
|
|
85
87
|
// Same catalogue decision as the renderAll above — a re-render that forgot it would
|
|
86
88
|
// quietly put the stripped tables back.
|
|
87
89
|
return renderFile(cfg, tmplItem.src, ptheme, footprint, '', new Set(), nativeCatalog);
|
package/js/build/generate.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
|
40
40
|
import { opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
41
41
|
import {
|
|
42
42
|
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
|
43
|
-
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, gatesOfDir, installState, installTargets,
|
|
43
|
+
excludedRulesOfDir, excludedSkillsOfDir, footprintOfDir, kitsOfDir, gatesOfDir, installState, installTargets,
|
|
44
44
|
legacyProcessCarrierOfDir, modeOfDir, postureOfDir, readMaybe, themeOfDir, trustOfDir,
|
|
45
45
|
} from '../hosts/installs.mjs';
|
|
46
46
|
import { colorThemeFiles, colorThemeJson, PALETTE_ROLES } from '../hosts/opencode.mjs';
|
|
@@ -95,7 +95,7 @@ function sysExit(msg) {
|
|
|
95
95
|
const BUILD_FORWARD = [
|
|
96
96
|
['theme', '--theme'], ['emit', '--emit'], ['footprint', '--footprint'],
|
|
97
97
|
['posture', '--posture'], ['mode', '--mode'], ['trust', '--trust'], ['doctrines', '--doctrines'],
|
|
98
|
-
['excludeRules', '--exclude-rules'], ['excludeSkills', '--exclude-skills'], ['out', '--out'], ['root', '--root'],
|
|
98
|
+
['excludeRules', '--exclude-rules'], ['excludeSkills', '--exclude-skills'], ['kits', '--kits'], ['out', '--out'], ['root', '--root'],
|
|
99
99
|
['configDir', '--config-dir'],
|
|
100
100
|
];
|
|
101
101
|
|
|
@@ -141,7 +141,7 @@ export function cmdBuild(args) {
|
|
|
141
141
|
*/
|
|
142
142
|
export function setupBuildArgs(theme, emit, out = null, root = null, footprint = 'lean',
|
|
143
143
|
posture = 'peer', mode = 'direct', doctrines = null, allPacks = PACK_ORDER,
|
|
144
|
-
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, gates = {}) {
|
|
144
|
+
excludeRules = null, trust = DEFAULT_PRESET, excludeSkills = null, gates = {}, kits = null) {
|
|
145
145
|
const argv = ['--theme', theme, '--emit', emit];
|
|
146
146
|
if (!emit.endsWith('-global')) {
|
|
147
147
|
if (out) argv.push('--out', out);
|
|
@@ -174,6 +174,9 @@ export function setupBuildArgs(theme, emit, out = null, root = null, footprint =
|
|
|
174
174
|
// `gates` is one object keyed as `GATE_SWITCHES` keys it (`gatesOfDir`, a profile, a body).
|
|
175
175
|
// Law order, so a Law IV-only install still ends on `--no-deletion-gate` as it did.
|
|
176
176
|
for (const g of GATE_SWITCHES) if (gates?.[g.key] === false) argv.push(g.flag);
|
|
177
|
+
// Kits elide when none is on, as skills do: no config key feeds them, so absence and `none`
|
|
178
|
+
// are the same answer. Last, so an argv naming no kit is the bytes it always was.
|
|
179
|
+
if (kits?.length) argv.push('--kits', [...kits].sort().join(','));
|
|
177
180
|
return argv;
|
|
178
181
|
}
|
|
179
182
|
|
|
@@ -235,9 +238,11 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
235
238
|
// The gate switches: a rebuild that forgot one would silently re-arm (or, read wrong, disarm)
|
|
236
239
|
// the install. A missing marker reads as armed.
|
|
237
240
|
const gates = gatesOfDir(root, host);
|
|
241
|
+
// Kits on stay on (spec A3); one this checkout no longer ships drops with a notice (R2).
|
|
242
|
+
const kits = kitsOfDir(root, host, notify);
|
|
238
243
|
const out = scope === 'global' ? null : root;
|
|
239
244
|
const argv = setupBuildArgs(theme, emit, out, out, footprint, posture, mode, doctrines,
|
|
240
|
-
PACK_ORDER, excludeRules, trust, excludeSkills, gates);
|
|
245
|
+
PACK_ORDER, excludeRules, trust, excludeSkills, gates, kits);
|
|
241
246
|
// A GLOBAL ROW NAMES ITS OWN DIR. `root` came from the host's config-dir resolver, which
|
|
242
247
|
// honours env overrides (`OPENCODE_CONFIG_DIR`, `CLAUDE_CONFIG_DIR`…); without the flag the
|
|
243
248
|
// same argv run from a shell lacking that variable re-emits into the DEFAULT dir — a second
|
|
@@ -246,7 +251,7 @@ export function installProfile(host, scope, root, notify = null) {
|
|
|
246
251
|
return {
|
|
247
252
|
host, scope, root, state: installState(root, host, scope),
|
|
248
253
|
emit, theme, footprint, posture, mode, trust, doctrines, excludeRules, excludeSkills,
|
|
249
|
-
...gates, argv,
|
|
254
|
+
kits, ...gates, argv,
|
|
250
255
|
};
|
|
251
256
|
}
|
|
252
257
|
|
package/js/build/render.mjs
CHANGED
|
@@ -34,7 +34,10 @@ import { readText } from '../lib/fs.mjs';
|
|
|
34
34
|
import { parseJson } from '../lib/json.mjs';
|
|
35
35
|
import { normcase, comparePaths } from '../lib/paths.mjs';
|
|
36
36
|
// `js/build/source.mjs` imports nothing but node builtins, so this direction cannot cycle.
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
CREDIT_RE, GATE_SWITCHES, PACK_ORDER, kitMembers, loadKits, readPackText, resolveCredits, resolveRuleIds, ruleCanon, upstreamOf,
|
|
39
|
+
} from './source.mjs';
|
|
40
|
+
import { VENDORED_SKILL_DIRS } from '../hosts/native.mjs';
|
|
38
41
|
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
39
42
|
|
|
40
43
|
/** Document STRUCTURE is theme-INDEPENDENT — mirrors `_build_render.STRUCTURE`. */
|
|
@@ -66,6 +69,9 @@ export const SRC_DIR_TOKENS = {
|
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
const TEXT_SUFFIXES = new Set(['.md', '.tmpl', '.json', '.txt', '.yml', '.yaml']);
|
|
72
|
+
/** Top-level `src/` entries the build reads but never ships: upstream-credit provenance, and the
|
|
73
|
+
* kit manifests (a kit's members ship flat from `skills/` and `agents/`). */
|
|
74
|
+
const NOT_BUNDLED = new Set(['upstreams.json', 'licenses', 'kits']);
|
|
69
75
|
|
|
70
76
|
// Digits are legal after the first character: `DOC_<PACK>_<n>` (the doctrine rule titles) are
|
|
71
77
|
// the first token names in the project to carry one, and without `0-9` here `{{DOC_CRAFT_1}}`
|
|
@@ -160,11 +166,59 @@ export function substitute(text, theme) {
|
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
/** `_build_render._resolve_catalogs`. */
|
|
163
|
-
function resolveCatalogs(text, nativeCatalog, excludeSkills = []) {
|
|
169
|
+
function resolveCatalogs(text, nativeCatalog, excludeSkills = [], cfg = null) {
|
|
164
170
|
// `true`/`false` still mean "both kinds", for the plain-bundle callers that pass a boolean.
|
|
165
171
|
const nat = nativeCatalog === true ? { skills: true, agents: true } : (nativeCatalog || {});
|
|
166
|
-
return text.replace(CATALOG_BLOCK_RE, (_m, kind, table, pointer) =>
|
|
167
|
-
|
|
172
|
+
return text.replace(CATALOG_BLOCK_RE, (_m, kind, table, pointer) => {
|
|
173
|
+
if (nat[kind]) return pointer;
|
|
174
|
+
// Active kits' rows close the table, before `--exclude-skills` drops rows (R5: exclusion wins).
|
|
175
|
+
const full = withTableRows(table, activeKits(cfg).flatMap(([, m]) => m.catalogue?.[kind] ?? []));
|
|
176
|
+
return kind === 'skills' ? dropExcludedSkillRows(full, excludeSkills) : full;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* `rows` inserted after the last row of the block's first table — inside it, not after the
|
|
182
|
+
* folder-skills list that follows §4's table (a table row there renders as broken markup). A
|
|
183
|
+
* block with no table gets them at its end; no rows returns the block as it was.
|
|
184
|
+
*/
|
|
185
|
+
function withTableRows(block, rows) {
|
|
186
|
+
if (!rows.length) return block;
|
|
187
|
+
const lines = block.split('\n');
|
|
188
|
+
let i = lines.findIndex((l) => l.startsWith('|'));
|
|
189
|
+
if (i < 0) i = lines.length - 1;
|
|
190
|
+
else while (i < lines.length && lines[i].startsWith('|')) i++;
|
|
191
|
+
lines.splice(i, 0, ...rows);
|
|
192
|
+
return lines.join('\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `loadKits(cfg.src)`, read once per cfg: a render asks for it per CATALOG block. */
|
|
196
|
+
const KITS = new WeakMap();
|
|
197
|
+
function kitsOf(cfg) {
|
|
198
|
+
let kits = KITS.get(cfg);
|
|
199
|
+
if (!kits) KITS.set(cfg, (kits = loadKits(cfg.src)));
|
|
200
|
+
return kits;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `[kit, manifest]` for each kit `cfg.kits` names, in its (sorted) order. */
|
|
204
|
+
function activeKits(cfg) {
|
|
205
|
+
if (!cfg?.kits?.length) return [];
|
|
206
|
+
const kits = kitsOf(cfg);
|
|
207
|
+
return cfg.kits.map((k) => {
|
|
208
|
+
if (!kits.has(k)) throw new Error(`unknown kit '${k}' (no src/kits/${k}.json)`);
|
|
209
|
+
const m = kits.get(k);
|
|
210
|
+
if (typeof m.rules !== 'string') throw new Error(`src/kits/${k}.json has no 'rules' path`);
|
|
211
|
+
return [k, m];
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The Kits block: each active kit's rules.md, rendered against `theme`. Exported because a
|
|
217
|
+
* carrier re-rendered with prefixed DIR_* tokens (`prefixedAgentText`) must rebuild it too.
|
|
218
|
+
*/
|
|
219
|
+
export function kitsBody(cfg, theme, lawsPrefix = '') {
|
|
220
|
+
return activeKits(cfg).map(([, m]) => `\n${renderFile(cfg, path.join(cfg.src, m.rules), theme,
|
|
221
|
+
'full', lawsPrefix).trim()}\n`).join('');
|
|
168
222
|
}
|
|
169
223
|
|
|
170
224
|
/**
|
|
@@ -240,8 +294,8 @@ function readSource(cfg, p) {
|
|
|
240
294
|
let seen = SRC_TEXT.get(cfg);
|
|
241
295
|
if (!seen) SRC_TEXT.set(cfg, (seen = new Map()));
|
|
242
296
|
let text = seen.get(p);
|
|
243
|
-
// Rule ids resolve here, once per file, so every stage after sees the addressed form.
|
|
244
|
-
if (text === undefined) seen.set(p, (text = resolveRuleIds(readText(p), cfg.src)));
|
|
297
|
+
// Rule ids and credits resolve here, once per file, so every stage after sees the addressed form.
|
|
298
|
+
if (text === undefined) seen.set(p, (text = resolveCredits(resolveRuleIds(readText(p), cfg.src), cfg.src)));
|
|
245
299
|
return text;
|
|
246
300
|
}
|
|
247
301
|
|
|
@@ -276,7 +330,7 @@ export function renderFile(cfg, filePath, theme, footprint = 'full', lawsPrefix
|
|
|
276
330
|
return inner;
|
|
277
331
|
});
|
|
278
332
|
|
|
279
|
-
text = resolveCatalogs(text, nativeCatalog, cfg.excludeSkills ?? []);
|
|
333
|
+
text = resolveCatalogs(text, nativeCatalog, cfg.excludeSkills ?? [], cfg);
|
|
280
334
|
text = resolveLean(text, footprint);
|
|
281
335
|
return substitute(text, theme);
|
|
282
336
|
}
|
|
@@ -444,6 +498,12 @@ export function effectiveTheme(cfg, themeName, { footprint = 'full', lawsPrefix
|
|
|
444
498
|
const skipped = [...(cfg.excludeSkills ?? [])].sort();
|
|
445
499
|
// It fills a template line of its own, so its value carries the blank line around it.
|
|
446
500
|
theme.EXCLUDED_SKILLS_LINE = skipped.length ? `\nExcluded skills: ${skipped.join(', ')}\n` : '';
|
|
501
|
+
// Kits, under the same contract: both empty with no kit, so a default build stays
|
|
502
|
+
// byte-identical. The marker has the skills line's shape so one parser reads both; the block is
|
|
503
|
+
// each active kit's rules.md, plain text (no rule ids, no LEAN halves), the same at either footprint.
|
|
504
|
+
const kits = activeKits(cfg);
|
|
505
|
+
theme.KITS_LINE = kits.length ? `\nActive kits: ${kits.map(([k]) => k).join(', ')}\n` : '';
|
|
506
|
+
theme.KITS_BODY = kitsBody(cfg, theme, lawsPrefix);
|
|
447
507
|
// The gate switches (`GATE_SWITCHES`: Laws I, III, IV), under the same contract: a line is
|
|
448
508
|
// written only when its gate is OFF, so a default build stays byte-identical and a missing
|
|
449
509
|
// line reads back as armed (`gatesOfDir`). The OpenCode plugins read these lines at load, so
|
|
@@ -488,7 +548,8 @@ export function destRel(rel) {
|
|
|
488
548
|
*
|
|
489
549
|
* Returns `{ theme, items }` where each item is `{ rel, text, src }` — `rel` is the
|
|
490
550
|
* themed POSIX output path, `text` is the rendered text or `null` for a binary that the
|
|
491
|
-
* caller copies from `src`.
|
|
551
|
+
* caller copies from `src`. A credited skill's licence copy also carries `licenceOf` (the
|
|
552
|
+
* skill's stem), since its `src` is not under that skill. Python returns a tuple of tuples; the object form is the
|
|
492
553
|
* same data with the field names the Python docstring already uses.
|
|
493
554
|
* @param {any} cfg
|
|
494
555
|
* @param {string} themeName
|
|
@@ -500,11 +561,16 @@ export function renderAll(cfg, themeName, {
|
|
|
500
561
|
const theme = effectiveTheme(cfg, themeName, { footprint, lawsPrefix });
|
|
501
562
|
const items = [];
|
|
502
563
|
const skipped = new Set(cfg.excludeSkills ?? []);
|
|
564
|
+
// Members of every kit `--kits` did not name: kit logic, not `excludeSkills`, so no marker.
|
|
565
|
+
const off = kitMembers(new Map([...kitsOf(cfg)].filter(([k]) => !(cfg.kits ?? []).includes(k))));
|
|
566
|
+
const licences = new Map(); // `<stem>/LICENSE-<key>.txt` -> the upstream's licence file
|
|
503
567
|
for (const file of sortedSourceFiles(cfg.src)) {
|
|
504
568
|
const rel = path.relative(cfg.src, file);
|
|
505
569
|
// `--exclude-skills`: the one filter every host's emit and the plain bundle read through.
|
|
506
570
|
const sp = rel.split(path.sep);
|
|
507
571
|
if (sp[0] === 'skills' && sp.length >= 2 && skipped.has(sp[1].replace(/\.md$/, ''))) continue;
|
|
572
|
+
if (NOT_BUNDLED.has(sp[0])) continue;
|
|
573
|
+
if (sp.length >= 2 && Object.hasOwn(off, sp[0]) && off[sp[0]].has(sp[1].replace(/\.md$/, ''))) continue;
|
|
508
574
|
const outRel = destRel(themedRel(rel, theme)).split(path.sep).join('/'); // as_posix()
|
|
509
575
|
// The on-disk `laws/`, `ontology/` and `doctrines/` are the "complete text" the two lean
|
|
510
576
|
// pointers promise — §1's and the Doctrines section's. They render at full whatever the
|
|
@@ -513,9 +579,26 @@ export function renderAll(cfg, themeName, {
|
|
|
513
579
|
// accident, which is a different thing from shipping whole by contract.
|
|
514
580
|
// Keyed on the SOURCE rel, not the themed one, so a DIR_* rename cannot un-exempt them.
|
|
515
581
|
const fp = /^(?:laws|ontology|doctrines)[\\/]/.test(rel) ? 'full' : footprint;
|
|
516
|
-
|
|
517
|
-
?
|
|
518
|
-
|
|
582
|
+
const text = TEXT_SUFFIXES.has(suffixOf(path.basename(file)))
|
|
583
|
+
? renderFile(cfg, file, theme, fp, lawsPrefix, new Set(), nativeCatalog) : null;
|
|
584
|
+
items.push({ rel: outRel, text, src: file });
|
|
585
|
+
// A credited skill ships its upstream's licence beside it, one copy per (skill, key). The
|
|
586
|
+
// token is gone from `text`, so the raw source is scanned; an unknown key or a null
|
|
587
|
+
// licenseFile yields no copy. A vendored folder's VENDOR.md carries a credit too, but the
|
|
588
|
+
// folder already ships its upstream's LICENSE verbatim, so it gets no second copy.
|
|
589
|
+
const stem = sp[0] === 'skills' && sp.length >= 2 ? sp[1].replace(/\.md$/, '') : null;
|
|
590
|
+
if (stem && !VENDORED_SKILL_DIRS.has(stem) && text !== null) {
|
|
591
|
+
for (const [, key] of readText(file).matchAll(CREDIT_RE)) {
|
|
592
|
+
const lic = upstreamOf(key, cfg.src)?.licenseFile;
|
|
593
|
+
if (lic) licences.set(`${stem}/LICENSE-${key}.txt`, path.join(cfg.src, lic));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Copied, not rendered (`text: null`): the licence ships byte for byte. `licenceOf` names the
|
|
598
|
+
// skill, because the copy's `src` is the upstream's file, not a path under that skill.
|
|
599
|
+
for (const [name, lic] of [...licences].sort(([a], [b]) => comparePaths(a, b))) {
|
|
600
|
+
items.push({ rel: destRel(themedRel(path.join('skills', name), theme)).split(path.sep).join('/'),
|
|
601
|
+
text: null, src: lic, licenceOf: name.split('/')[0] });
|
|
519
602
|
}
|
|
520
603
|
return { theme, items };
|
|
521
604
|
}
|
package/js/build/source.mjs
CHANGED
|
@@ -90,6 +90,14 @@ export const RETIRED_RULE_IDS = {
|
|
|
90
90
|
'absence-is-a-claim': { was: 'Law XI', liveAs: null, note: 'folded into verify-before-asserting' },
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Kit ids that must never name a kit (`src/kits/<id>.json`), the reason beside each — the
|
|
95
|
+
* contract `RETIRED_RULE_IDS` keeps for rules. `none` was never a kit: `--kits none` means no kits.
|
|
96
|
+
*/
|
|
97
|
+
export const RETIRED_KIT_IDS = {
|
|
98
|
+
none: 'reserved: --kits none switches every kit off',
|
|
99
|
+
};
|
|
100
|
+
|
|
93
101
|
/** @type {Array<[number, string]>} */
|
|
94
102
|
const ROMAN = [[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'],
|
|
95
103
|
[50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']];
|
|
@@ -192,6 +200,82 @@ export function resolveRuleIds(text, src = SRC) {
|
|
|
192
200
|
.replace(CITE_RE, (whole, kind, id) => { const r = known(kind, id); return r ? `*${r.name}*` : whole; });
|
|
193
201
|
}
|
|
194
202
|
|
|
203
|
+
/** `{{CREDIT:<key>:<path>}}` — `key` names a `src/upstreams.json` entry, `path` a file upstream. */
|
|
204
|
+
export const CREDIT_RE = /\{\{CREDIT:([a-z0-9]+(?:-[a-z0-9]+)*):([A-Za-z0-9._/-]+)\}\}/g;
|
|
205
|
+
|
|
206
|
+
const _upstreams = new Map();
|
|
207
|
+
/**
|
|
208
|
+
* `src/upstreams.json`, parsed — memoised on its raw text the way `ruleCanon` is, so the web
|
|
209
|
+
* daemon sees an edit. A tree without the file credits nothing.
|
|
210
|
+
*/
|
|
211
|
+
export function upstreams(src = SRC) {
|
|
212
|
+
const file = path.join(src, 'upstreams.json');
|
|
213
|
+
let text;
|
|
214
|
+
// Not readRawText: its mtime key misses a rewrite inside one mtime tick (seen on Windows CI).
|
|
215
|
+
try { text = readFileSync(file, 'utf8'); } catch { return {}; }
|
|
216
|
+
const hit = _upstreams.get(file);
|
|
217
|
+
if (hit && hit.text === text) return hit.entries;
|
|
218
|
+
const entries = JSON.parse(text);
|
|
219
|
+
_upstreams.set(file, { text, entries });
|
|
220
|
+
return entries;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The entry a credit key names, or null — `hasOwn`, so `constructor` is not a key. */
|
|
224
|
+
export function upstreamOf(key, src = SRC) {
|
|
225
|
+
const u = upstreams(src);
|
|
226
|
+
return Object.hasOwn(u, key) ? u[key] : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Resolve every credit in `text` into its attribution line. An unknown key is left exactly as
|
|
231
|
+
* written, as `resolveRuleIds` leaves an unknown id, so the doctor can name it.
|
|
232
|
+
*/
|
|
233
|
+
export function resolveCredits(text, src = SRC) {
|
|
234
|
+
if (!text.includes('{{CREDIT:')) return text;
|
|
235
|
+
return text.replace(CREDIT_RE, (whole, key, file) => {
|
|
236
|
+
const u = upstreamOf(key, src);
|
|
237
|
+
return u ? `*Adapted from [${u.repo}](https://github.com/${u.repo}) (\`${file}\`) `
|
|
238
|
+
+ `by ${u.author} — ${u.license}, modified.*` : whole;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* `src/kits/<kit>.json`, parsed, as `Map<kit, manifest>` in sorted kit order. A manifest is
|
|
244
|
+
* `{ description, skills, agents, rules, rulesWordBudget, catalogue, banned? }`, the sole writer of its
|
|
245
|
+
* kit's membership; the `<kit>/` folder beside it holds the kit's `rules.md` and is not read
|
|
246
|
+
* here. A tree without `src/kits/` has no kits.
|
|
247
|
+
*/
|
|
248
|
+
export function loadKits(src = SRC) {
|
|
249
|
+
const dir = path.join(src, 'kits');
|
|
250
|
+
let names;
|
|
251
|
+
try { names = readdirSync(dir).filter((n) => n.endsWith('.json')).sort(); } catch (e) {
|
|
252
|
+
if (e.code === 'ENOENT') return new Map(); // only an absent src/kits means "no kits"
|
|
253
|
+
throw new Error(`src/kits: ${e.message}`, { cause: e });
|
|
254
|
+
}
|
|
255
|
+
return new Map(names.map((n) => {
|
|
256
|
+
let m;
|
|
257
|
+
try { m = JSON.parse(readFileSync(path.join(dir, n), 'utf8')); } catch (e) {
|
|
258
|
+
throw new Error(`src/kits/${n}: ${e.message}`, { cause: e });
|
|
259
|
+
}
|
|
260
|
+
if (m === null || typeof m !== 'object' || Array.isArray(m)) throw new Error(`src/kits/${n}: a manifest must be a JSON object`);
|
|
261
|
+
if (m.banned !== undefined && !(Array.isArray(m.banned) && m.banned.every((b) => typeof b === 'string' && b))) {
|
|
262
|
+
throw new Error(`src/kits/${n}: 'banned' must be an array of non-empty strings`);
|
|
263
|
+
}
|
|
264
|
+
return [n.slice(0, -'.json'.length), m];
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Every skill and agent the kits handed in claim (a `loadKits` Map, or a subset of one). */
|
|
269
|
+
export function kitMembers(kits) {
|
|
270
|
+
const skills = new Set();
|
|
271
|
+
const agents = new Set();
|
|
272
|
+
for (const m of kits.values()) {
|
|
273
|
+
for (const s of m.skills ?? []) skills.add(s);
|
|
274
|
+
for (const a of m.agents ?? []) agents.add(a);
|
|
275
|
+
}
|
|
276
|
+
return { skills, agents };
|
|
277
|
+
}
|
|
278
|
+
|
|
195
279
|
/**
|
|
196
280
|
* `_build_core.ROOT` — the checkout, from this file's own location (`js/..`).
|
|
197
281
|
*
|
|
@@ -295,20 +379,47 @@ export function knownSkillIds(src = SRC) {
|
|
|
295
379
|
* not listed, so a new link protects its target the day it is written.
|
|
296
380
|
*/
|
|
297
381
|
export function linkedSkillIds(src = SRC) {
|
|
298
|
-
|
|
382
|
+
// A kit's links to its own members do not protect them (spec R5: excluding a kit skill is
|
|
383
|
+
// allowed); a kit file's link to a CORE skill still does, or excluding it would leave the link dead.
|
|
384
|
+
return [...new Set(specLinks(src).filter((l) => l.kind === 'skills' && !(l.kit && l.kit === l.owner))
|
|
385
|
+
.map((l) => l.name))].sort();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Every `{{DIR_SKILLS}}/<name>` / `{{DIR_AGENTS}}/<name>` link in `src`'s .md/.tmpl files, outside
|
|
390
|
+
* the §4 skills catalogue table: `{ rel, kind, name, kit, owner }`. `kit` is the kit that owns the
|
|
391
|
+
* file (`kits/<kit>.json`, `kits/<kit>/**`, a member's spec or side file), `owner` the kit the
|
|
392
|
+
* target is a member of; each null for core. `linkedSkillIds` and the doctor's R4 arm
|
|
393
|
+
* (`kitProblems`) read the same walk.
|
|
394
|
+
*/
|
|
395
|
+
export function specLinks(src = SRC) {
|
|
396
|
+
// Member -> its kit, per kind; the first kit wins a member two kits claim (R3 names that).
|
|
397
|
+
const owners = { skills: new Map(), agents: new Map() };
|
|
398
|
+
for (const [kit, m] of loadKits(src)) {
|
|
399
|
+
for (const kind of ['skills', 'agents']) for (const n of m[kind] ?? []) if (!owners[kind].has(n)) owners[kind].set(n, kit);
|
|
400
|
+
}
|
|
401
|
+
// Under kits/, only `<kit>.json` and `<kit>/**` belong to a kit; any other file there (a README) is nobody's.
|
|
402
|
+
const ofKit = (sp) => (sp[0] === 'kits'
|
|
403
|
+
? (sp.length > 2 ? sp[1] : /\.json$/.test(sp[1]) ? sp[1].slice(0, -'.json'.length) : null)
|
|
404
|
+
: (sp.length >= 2 && Object.hasOwn(owners, sp[0]) && owners[sp[0]].get(sp[1].replace(/\.md$/, ''))) || null);
|
|
405
|
+
const links = [];
|
|
299
406
|
const walk = (d) => {
|
|
300
407
|
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
301
408
|
const f = path.join(d, e.name);
|
|
302
409
|
if (e.isDirectory()) walk(f);
|
|
303
410
|
else if (/\.(md|tmpl)$/.test(e.name)) {
|
|
411
|
+
const sp = path.relative(src, f).split(path.sep);
|
|
304
412
|
const text = readFileSync(f, 'utf8')
|
|
305
413
|
.replace(/<!-- CATALOG:begin skills -->[\s\S]*?<!-- CATALOG:end -->/, '');
|
|
306
|
-
for (const m of text.matchAll(/\{\{
|
|
414
|
+
for (const m of text.matchAll(/\{\{DIR_(SKILLS|AGENTS)\}\}\/([a-z0-9-]+)/g)) {
|
|
415
|
+
const kind = m[1].toLowerCase();
|
|
416
|
+
links.push({ rel: sp.join('/'), kind, name: m[2], kit: ofKit(sp), owner: owners[kind].get(m[2]) ?? null });
|
|
417
|
+
}
|
|
307
418
|
}
|
|
308
419
|
}
|
|
309
420
|
};
|
|
310
421
|
walk(src);
|
|
311
|
-
return
|
|
422
|
+
return links;
|
|
312
423
|
}
|
|
313
424
|
|
|
314
425
|
/**
|
|
@@ -412,7 +523,7 @@ export const GATE_SWITCHES = [
|
|
|
412
523
|
*/
|
|
413
524
|
export function makeCfg({
|
|
414
525
|
posture = 'peer', mode = 'direct', trust = DEFAULT_PRESET, doctrines = PACK_ORDER,
|
|
415
|
-
excludeRules = [], excludeSkills = [], ...switches
|
|
526
|
+
excludeRules = [], excludeSkills = [], kits = [], ...switches
|
|
416
527
|
} = {}) {
|
|
417
528
|
return {
|
|
418
529
|
root: ROOT,
|
|
@@ -440,6 +551,8 @@ export function makeCfg({
|
|
|
440
551
|
excludeRules: Array.isArray(excludeRules) ? [...excludeRules] : [],
|
|
441
552
|
// Skill names left out of this install — same "unspecified takes nothing away" default.
|
|
442
553
|
excludeSkills: Array.isArray(excludeSkills) ? [...excludeSkills] : [],
|
|
554
|
+
// Kits switched on for this install — off unless named, so a default build is unchanged.
|
|
555
|
+
kits: Array.isArray(kits) ? [...kits] : [],
|
|
443
556
|
// The gate switches (`GATE_SWITCHES`: `secretsGate`, `doneGate`, `deletionGate`), each armed
|
|
444
557
|
// unless its `no*` arg says so in so many words: only a literal `true` disarms, so every
|
|
445
558
|
// caller that never heard of a switch keeps the gate.
|
package/js/cli-table.json
CHANGED
|
@@ -193,6 +193,21 @@
|
|
|
193
193
|
"type": null,
|
|
194
194
|
"hidden": false
|
|
195
195
|
},
|
|
196
|
+
{
|
|
197
|
+
"names": [
|
|
198
|
+
"--kits"
|
|
199
|
+
],
|
|
200
|
+
"dest": "kits",
|
|
201
|
+
"metavar": null,
|
|
202
|
+
"help": "switch on opt-in kits, comma-separated; 'none' turns every kit off (default: no kits)",
|
|
203
|
+
"choices": null,
|
|
204
|
+
"default": null,
|
|
205
|
+
"required": false,
|
|
206
|
+
"nargs": null,
|
|
207
|
+
"is_flag": false,
|
|
208
|
+
"type": null,
|
|
209
|
+
"hidden": false
|
|
210
|
+
},
|
|
196
211
|
{
|
|
197
212
|
"names": [
|
|
198
213
|
"--out"
|