geneseed 4.9.4 → 4.10.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 +21 -0
- package/README.md +13 -0
- 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 +79 -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/extending.md +108 -4
- package/docs/guides/install-scripted.md +1 -1
- package/docs/reference/glossary.md +1 -0
- package/docs/web-ui.md +4 -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/native.mjs +14 -4
- package/js/inspect/checks-authoring.mjs +6 -3
- package/js/inspect/checks-build.mjs +7 -1
- package/js/inspect/checks-counts.mjs +153 -6
- package/js/inspect/checks-repo.mjs +95 -20
- package/js/inspect/diff.mjs +3 -1
- package/js/inspect/inventory.mjs +1 -0
- package/js/inspect/status.mjs +44 -3
- package/js/inspect/validate.mjs +3 -3
- package/js/maintain/setup.mjs +30 -6
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +30 -6
- package/js/web/api.mjs +40 -3
- package/js/web/routes.mjs +2 -1
- package/package.json +1 -1
- package/registry.json +63 -0
- package/src/AGENT.md.tmpl +2 -2
- package/src/agents/devops-engineer.md +45 -0
- package/src/agents/docker-expert.md +45 -0
- package/src/agents/kubernetes-specialist.md +45 -0
- package/src/agents/reviewer.md +2 -0
- package/src/agents/spring-boot-engineer.md +48 -0
- package/src/agents/spring-reviewer.md +49 -0
- package/src/agents/spring-security.md +50 -0
- package/src/kits/spring-boot/rules.md +10 -0
- package/src/kits/spring-boot.json +23 -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/brainstorm/builder-check.md +2 -0
- package/src/skills/brainstorm.md +4 -0
- package/src/skills/codebase-design.md +4 -0
- package/src/skills/daydream/VENDOR.md +7 -7
- package/src/skills/debug.md +5 -1
- package/src/skills/develop.md +2 -0
- package/src/skills/geneseed-code-review.md +6 -2
- package/src/skills/git-history.md +2 -0
- package/src/skills/handoff.md +2 -0
- package/src/skills/learn-mode.md +3 -2
- package/src/skills/parallel-agents.md +2 -0
- package/src/skills/pipeline.md +2 -0
- package/src/skills/plan.md +4 -0
- package/src/skills/prototype.md +2 -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/research.md +2 -0
- package/src/skills/review-response.md +2 -0
- package/src/skills/rule.md +2 -0
- package/src/skills/ship.md +2 -0
- package/src/skills/skill-forge.md +1 -0
- 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 +2 -0
- package/src/upstreams.json +59 -0
- package/themes/_TEMPLATE.json +9 -0
- package/themes/biker.json +9 -0
- package/themes/commentator.json +9 -0
- package/themes/cyberpunk.json +9 -0
- package/themes/gamer.json +9 -0
- package/themes/imperial.json +9 -0
- package/themes/joker.json +9 -0
- package/themes/marvin.json +9 -0
- package/themes/mean.json +9 -0
- package/themes/military.json +9 -0
- package/themes/neutral.json +9 -0
- package/themes/pirate.json +9 -0
- package/themes/sports.json +9 -0
- package/themes/verstappen.json +9 -0
- package/themes/wizard.json +9 -0
- package/web/dist/assets/{Activity-C8sZVIUU.js → Activity-BjyJaf8e.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Cc6vWUS4.js → ActivityDetail-Bo9nRDNV.js} +1 -1
- package/web/dist/assets/{Dashboard-C5ATa3ow.js → Dashboard-BQJGU7vE.js} +2 -2
- package/web/dist/assets/{Docs-tDW7i7mo.js → Docs-DLVckkew.js} +1 -1
- package/web/dist/assets/{GreenhouseView-zk2Uzc3A.js → GreenhouseView--DML7CW3.js} +1 -1
- package/web/dist/assets/{Installs-CB_rvbBP.js → Installs-CxCiln1F.js} +2 -2
- package/web/dist/assets/{JournalView-Bfj27XHC.js → JournalView-ymgZleHi.js} +1 -1
- package/web/dist/assets/{Laws-BA3CzUf5.js → Laws-BkKnZOBI.js} +1 -1
- package/web/dist/assets/Library-C8ercIZv.js +1 -0
- package/web/dist/assets/{Loops-DaPk-ckL.js → Loops-C95zhbFl.js} +1 -1
- package/web/dist/assets/{OperatorHudView-CyiGhiHk.js → OperatorHudView-B9qwc3w3.js} +1 -1
- package/web/dist/assets/{Personal-Bwd27CmT.js → Personal-DyR72AF-.js} +1 -1
- package/web/dist/assets/{Tabs-BOD7Kaz1.js → Tabs-ChCr_HqV.js} +1 -1
- package/web/dist/assets/api-BfpZmUMI.js +1 -0
- package/web/dist/assets/{drawable-CyLe1gtW.js → drawable-CgYi0Dtg.js} +1 -1
- package/web/dist/assets/index-B32EvfDT.js +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/Library-BWwiy9Vb.js +0 -1
- package/web/dist/assets/api-CiXlkbe0.js +0 -1
- package/web/dist/assets/index-DQXG_mqo.js +0 -2
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"
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
38
|
import {
|
|
39
|
-
CONFIG, GATE_SWITCHES, PACK_ORDER, THEMES, discoverNames, resolveSkillNames,
|
|
39
|
+
CONFIG, GATE_SWITCHES, PACK_ORDER, RETIRED_KIT_IDS, THEMES, discoverNames, loadKits, resolveSkillNames,
|
|
40
40
|
} from '../build/source.mjs';
|
|
41
41
|
import {
|
|
42
42
|
CLAUDE_STYLE, DISABLED_STASH, GLOBAL_MANIFEST, HOSTS, isHostGlobalDir, liveRecordedHooks,
|
|
@@ -585,6 +585,33 @@ export function excludedSkillsOfDir(d, host = null, notify = null) {
|
|
|
585
585
|
}, [], host);
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
/** `Active kits: spring-boot` — written only when a kit is on; the skills line's shape. */
|
|
589
|
+
const KITS_RE = /^Active kits:[ \t]*(.+?)[ \t]*$/m;
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The kits a DEPLOYED install has on, read back so `rebuild-all`, `upgrade`, `migrate`, `diff`, the wizard
|
|
593
|
+
* and the console re-render them (spec A3). A missing line is `[]`: no kits, the old install's
|
|
594
|
+
* state. Unlike `excludedSkillsOfDir`, a name this checkout does not ship drops ALONE, with a
|
|
595
|
+
* notice (spec R2): a retired kit must never reach `--kits`, which refuses it and would fail the
|
|
596
|
+
* whole `rebuild-all`, and the kits beside it are still the owner's choice. Silent without `notify`.
|
|
597
|
+
*/
|
|
598
|
+
export function kitsOfDir(d, host = null, notify = null) {
|
|
599
|
+
return firstCarrier(d, (carrierPath) => {
|
|
600
|
+
const text = readMaybe(carrierPath);
|
|
601
|
+
if (text === null) return undefined;
|
|
602
|
+
const m = KITS_RE.exec(text);
|
|
603
|
+
if (!m) return undefined;
|
|
604
|
+
const known = loadKits();
|
|
605
|
+
const names = [...new Set(m[1].split(',').map((s) => s.trim()).filter(Boolean))].sort();
|
|
606
|
+
for (const k of names.filter((n) => !known.has(n))) {
|
|
607
|
+
notify?.(Object.hasOwn(RETIRED_KIT_IDS, k)
|
|
608
|
+
? `kit '${k}' was retired (${RETIRED_KIT_IDS[k]}) — dropped`
|
|
609
|
+
: `kit '${k}' is not shipped by this Geneseed — dropped`);
|
|
610
|
+
}
|
|
611
|
+
return names.filter((n) => known.has(n));
|
|
612
|
+
}, [], host);
|
|
613
|
+
}
|
|
614
|
+
|
|
588
615
|
/**
|
|
589
616
|
* Whether a DEPLOYED install keeps one law's hook gate armed — `true` (armed) or `false` (its
|
|
590
617
|
* `<marker>: off` line, written by a build with that gate's `--no-*-gate` flag; `GATE_SWITCHES`).
|
|
@@ -861,7 +888,7 @@ export function installsReferencingShim(shimPath = hookShimPath(), targets = ins
|
|
|
861
888
|
export function installedDefaults() {
|
|
862
889
|
const found = {
|
|
863
890
|
theme: null, posture: null, mode: null, emit: null, footprint: null, doctrines: null,
|
|
864
|
-
excludeRules: null, excludeSkills: null,
|
|
891
|
+
excludeRules: null, excludeSkills: null, kits: null,
|
|
865
892
|
};
|
|
866
893
|
const candidates = [];
|
|
867
894
|
for (const { host, configDir } of HOSTS) {
|
|
@@ -904,6 +931,12 @@ export function installedDefaults() {
|
|
|
904
931
|
// indistinguishable here, which is exactly what the marker itself already says.
|
|
905
932
|
if (!found.excludeRules?.length) found.excludeRules = excludedRulesOfDir(base);
|
|
906
933
|
if (!found.excludeSkills?.length) found.excludeSkills = excludedSkillsOfDir(base);
|
|
934
|
+
// Kits walk the same two-state way: no line and `none` both read `[]`.
|
|
935
|
+
// A broken manifest must not crash the wizard here: the build that follows still fails loudly
|
|
936
|
+
// in `loadKits`. Not guarded inside `kitsOfDir`, which rebuild-all must keep failing on.
|
|
937
|
+
if (!found.kits?.length) {
|
|
938
|
+
try { found.kits = kitsOfDir(base); } catch { found.kits = []; }
|
|
939
|
+
}
|
|
907
940
|
}
|
|
908
941
|
return found;
|
|
909
942
|
}
|
package/js/hosts/native.mjs
CHANGED
|
@@ -579,7 +579,7 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
579
579
|
let nAgents = 0;
|
|
580
580
|
let nSkills = 0;
|
|
581
581
|
const written = [];
|
|
582
|
-
const sideFiles = []; // [stem, dest, text], written after the loop
|
|
582
|
+
const sideFiles = []; // [stem, dest, text, source], written after the loop
|
|
583
583
|
const claimedSkills = new Set(); // stems whose own SKILL.md claim held
|
|
584
584
|
const userOnlySkills = []; // claimed stems carrying the user-only marker
|
|
585
585
|
|
|
@@ -589,7 +589,13 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
589
589
|
written.push(dest);
|
|
590
590
|
};
|
|
591
591
|
|
|
592
|
-
for (const { text, src: source } of items) {
|
|
592
|
+
for (const { text, src: source, rel, licenceOf } of items) {
|
|
593
|
+
// A credited skill's upstream licence: copied verbatim beside its SKILL.md, held like a side
|
|
594
|
+
// file until that claim holds. Routed by `licenceOf`, since `source` is the upstream's file.
|
|
595
|
+
if (licenceOf) {
|
|
596
|
+
sideFiles.push([licenceOf, path.join(skillsDir, licenceOf, path.posix.basename(rel)), null, source]);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
593
599
|
const sparts = relPosix(src, source).split('/');
|
|
594
600
|
|
|
595
601
|
// Vendored third-party skill folders ride along verbatim, preserving their own
|
|
@@ -706,8 +712,12 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
706
712
|
if (kind === 'agent') nAgents += 1;
|
|
707
713
|
else nSkills += 1;
|
|
708
714
|
}
|
|
709
|
-
for (const [stem, dest, text] of sideFiles) {
|
|
710
|
-
if (claimedSkills.has(stem)
|
|
715
|
+
for (const [stem, dest, text, source] of sideFiles) {
|
|
716
|
+
if (!claimedSkills.has(stem) || !claim(dest)) continue;
|
|
717
|
+
if (text !== null) { write(dest, stripSkillBodyLinks(text)); continue; }
|
|
718
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
719
|
+
copyFile(source, dest);
|
|
720
|
+
written.push(dest);
|
|
711
721
|
}
|
|
712
722
|
return { nAgents, nSkills, written, userOnlySkills };
|
|
713
723
|
}
|
|
@@ -21,11 +21,11 @@ import { blankCodeSpans } from '../lib/text.mjs';
|
|
|
21
21
|
import { which } from '../lib/paths.mjs';
|
|
22
22
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
23
23
|
import {
|
|
24
|
-
aliasProblems, folderSkillProblems, registryProblems, secretProblems, sideFileProblems,
|
|
24
|
+
aliasProblems, creditProblems, folderSkillProblems, registryProblems, secretProblems, sideFileProblems,
|
|
25
25
|
vendorPinProblems,
|
|
26
26
|
} from './checks-repo.mjs';
|
|
27
27
|
import { constitutionProblems, leanBlockProblems } from './checks-constitution.mjs';
|
|
28
|
-
import { countTableProblems } from './checks-counts.mjs';
|
|
28
|
+
import { countTableProblems, kitProblems } from './checks-counts.mjs';
|
|
29
29
|
import { catalogProblems } from '../loop/catalog.mjs';
|
|
30
30
|
import { globSorted, isDir, isFile, rglob } from './scan.mjs';
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
@@ -55,6 +55,7 @@ const ALLOWED_COMMENT_RES = [
|
|
|
55
55
|
// js/web/docs.mjs HARNESS_OPEN_RE: `harness:` then one or more of its four ids, comma-separated.
|
|
56
56
|
new RegExp(`^harness:\\s*(?:${HARNESS_IDS})(?:\\s*,\\s*(?:${HARNESS_IDS}))*$`),
|
|
57
57
|
/^bash:\s*allow$/, // js/hosts/native.mjs BASH_MARKER
|
|
58
|
+
/^banned: ok$/, // checks-counts.mjs kitProblems: a kit line that names a banned string
|
|
58
59
|
];
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -114,7 +115,7 @@ export function authorNoteProblems(src = SRC) {
|
|
|
114
115
|
export {
|
|
115
116
|
HOOK_PINNED, constitutionProblems, doctrineMetaProblems, lawMetaProblems, leanBlockProblems,
|
|
116
117
|
} from './checks-constitution.mjs';
|
|
117
|
-
export { countTableProblems, proseMirrorProblems } from './checks-counts.mjs';
|
|
118
|
+
export { countTableProblems, kitProblems, proseMirrorProblems } from './checks-counts.mjs';
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
121
|
* The shipped loop catalogue parses and every shipped template passes the engine's own graph
|
|
@@ -192,10 +193,12 @@ export function authoringProblems() {
|
|
|
192
193
|
problems.push(...vendorPinProblems());
|
|
193
194
|
problems.push(...folderSkillProblems());
|
|
194
195
|
problems.push(...aliasProblems());
|
|
196
|
+
problems.push(...creditProblems());
|
|
195
197
|
problems.push(...sideFileProblems());
|
|
196
198
|
problems.push(...constitutionProblems());
|
|
197
199
|
problems.push(...leanBlockProblems());
|
|
198
200
|
problems.push(...countTableProblems());
|
|
201
|
+
problems.push(...kitProblems());
|
|
199
202
|
problems.push(...authorNoteProblems());
|
|
200
203
|
return problems;
|
|
201
204
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { stripCapabilityLinks } from '../build/emit-common.mjs';
|
|
11
11
|
import { STRUCTURE, renderAll } from '../build/render.mjs';
|
|
12
|
-
import { CONFIG, makeCfg } from '../build/source.mjs';
|
|
12
|
+
import { CONFIG, CREDIT_RE, makeCfg } from '../build/source.mjs';
|
|
13
13
|
import { hostCatalogsNatively, resolvePath } from '../hosts/hosts.mjs';
|
|
14
14
|
import { EMIT_HOST_SCOPE, footprintOfDir, themeFiles } from '../hosts/installs.mjs';
|
|
15
15
|
import { isVendoredPath } from '../hosts/native.mjs';
|
|
@@ -80,6 +80,12 @@ export function checkBuild(themeName, out, isVendored = isVendoredPath) {
|
|
|
80
80
|
for (const tok of sortedUnique(text.match(TOKEN_RE) ?? [])) {
|
|
81
81
|
problems.push(`[${themeName}] unresolved token ${tok} in ${rel}`);
|
|
82
82
|
}
|
|
83
|
+
// `TOKEN_RE` cannot see a credit (colon, lowercase key): one left standing names no upstream.
|
|
84
|
+
if (text.includes('{{CREDIT:')) {
|
|
85
|
+
for (const tok of sortedUnique(text.match(CREDIT_RE) ?? ['{{CREDIT:'])) {
|
|
86
|
+
problems.push(`[${themeName}] unresolved token ${tok} in ${rel}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
83
89
|
for (const p of linkProblems(md, text, outAbs, rel)) problems.push(`[${themeName}] ${p}`);
|
|
84
90
|
for (const p of descriptionProblems(text, rel)) problems.push(`[${themeName}] ${p}`);
|
|
85
91
|
}
|