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
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* is the reader that feeds it.
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
DOCS, DOC_FOLDERS, PLUGIN_SRC, RETIRED_KIT_IDS, ROOT, SRC, kitMembers, loadKits, makeCfg, ruleCanon, specLinks, upstreams,
|
|
12
|
+
} from '../build/source.mjs';
|
|
13
|
+
import { effectiveTheme, renderFile } from '../build/render.mjs';
|
|
11
14
|
import { themeFiles } from '../hosts/installs.mjs';
|
|
12
15
|
import { readText } from '../lib/fs.mjs';
|
|
13
16
|
import { formatRepr } from '../lib/json.mjs';
|
|
@@ -25,7 +28,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
25
28
|
* so they drift silently — they already had. Pure over its inputs, so the corpus can feed it
|
|
26
29
|
* crafted drift without touching the tree.
|
|
27
30
|
*/
|
|
28
|
-
export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '') {
|
|
31
|
+
export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '', upstreamKeys = undefined) {
|
|
29
32
|
const problems = [];
|
|
30
33
|
const { laws, agents, skills, plugins } = counts;
|
|
31
34
|
|
|
@@ -68,6 +71,32 @@ export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '
|
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
// The README Credits section names EXACTLY the `src/upstreams.json` keys, both directions,
|
|
75
|
+
// as the skills row does. A missing section is a problem, or deleting it would turn the gate
|
|
76
|
+
// green. `upstreamKeys` is optional, like `plugins`, because the corpus predates it.
|
|
77
|
+
if (upstreamKeys !== undefined) {
|
|
78
|
+
const lines = readme.split('\n').map((ln) => ln.replace(/\r$/, ''));
|
|
79
|
+
const at = lines.findIndex((ln) => ln.trim() === '## 🙏 Credits');
|
|
80
|
+
if (at < 0) {
|
|
81
|
+
problems.push("[authoring] README has no '## 🙏 Credits' section — src/upstreams.json has "
|
|
82
|
+
+ `${upstreamKeys.length} upstreams to credit`);
|
|
83
|
+
} else {
|
|
84
|
+
const end = lines.findIndex((ln, i) => i > at && ln.startsWith('## '));
|
|
85
|
+
const listed = new Set();
|
|
86
|
+
for (const ln of lines.slice(at + 1, end < 0 ? undefined : end).filter((l) => l.startsWith('- '))) {
|
|
87
|
+
const k = /^- `([^`]+)`/.exec(ln);
|
|
88
|
+
if (k) listed.add(k[1]);
|
|
89
|
+
else problems.push(`[authoring] README Credits bullet names no upstream key in backticks: '${ln}'`);
|
|
90
|
+
}
|
|
91
|
+
for (const key of [...upstreamKeys].filter((k) => !listed.has(k)).sort()) {
|
|
92
|
+
problems.push(`[authoring] README Credits omits upstream '${key}' (src/upstreams.json)`);
|
|
93
|
+
}
|
|
94
|
+
for (const key of [...listed].filter((k) => !upstreamKeys.includes(k)).sort()) {
|
|
95
|
+
problems.push(`[authoring] README Credits names '${key}', which src/upstreams.json does not define`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
for (const m of web.matchAll(/(\d+) universal (?:laws|Rules)/g)) {
|
|
72
101
|
if (Number(m[1]) !== laws) {
|
|
73
102
|
problems.push(`[authoring] docs/ prose says '${m[1]} universal laws/Rules' but `
|
|
@@ -127,8 +156,13 @@ export function countTableProblems() {
|
|
|
127
156
|
return [`[authoring] AGENT.md.tmpl unreadable: ${e.message}`];
|
|
128
157
|
}
|
|
129
158
|
|
|
159
|
+
// A kit member's row lives in its kit's manifest, not the template (spec A6). A manifest that
|
|
160
|
+
// does not parse is `kitProblems`' to name; here it means no kit rows.
|
|
161
|
+
let kits;
|
|
162
|
+
try { kits = loadKits(); } catch { kits = new Map(); }
|
|
163
|
+
const kitRows = [...kits.values()].flatMap((m) => [...(m.catalogue?.skills ?? []), ...(m.catalogue?.agents ?? [])]);
|
|
130
164
|
const linked = { agents: new Set(), skills: new Set() };
|
|
131
|
-
for (const m of ttext.matchAll(TMPL_SPEC_RE)) {
|
|
165
|
+
for (const m of [ttext, ...kitRows].join('\n').matchAll(TMPL_SPEC_RE)) {
|
|
132
166
|
if (m[2] !== '_template') linked[m[1] === 'AGENTS' ? 'agents' : 'skills'].add(m[2]);
|
|
133
167
|
}
|
|
134
168
|
for (const folder of ['agents', 'skills']) {
|
|
@@ -173,11 +207,14 @@ export function countTableProblems() {
|
|
|
173
207
|
// the SHIPPED.md triple count it. It is added HERE rather than in `srcStems`, whose other
|
|
174
208
|
// readers above — the AGENT.md table links and SKILL_CLASS — are about flat specs only.
|
|
175
209
|
const skillsDir = path.join(SRC, 'skills');
|
|
210
|
+
// Core only: a kit is off by default, so its members are in no badge, README list or SHIPPED triple.
|
|
211
|
+
const members = kitMembers(kits);
|
|
176
212
|
const shippedSkills = new Set([...skillFiles, ...(isDir(skillsDir) ? readdirSync(skillsDir) : [])
|
|
177
|
-
.filter((d) => !d.startsWith('_') && isFile(path.join(skillsDir, d, 'SKILL.md')))]
|
|
213
|
+
.filter((d) => !d.startsWith('_') && isFile(path.join(skillsDir, d, 'SKILL.md')))]
|
|
214
|
+
.filter((s) => !members.skills.has(s)));
|
|
178
215
|
|
|
179
216
|
const counts = {
|
|
180
|
-
agents: srcStems('agents').
|
|
217
|
+
agents: [...srcStems('agents')].filter((a) => !members.agents.has(a)).length,
|
|
181
218
|
skills: shippedSkills.size,
|
|
182
219
|
laws: lawNums.length,
|
|
183
220
|
// Every doctrine rule in every pack, opt-in ones included: the laws badge alone told a
|
|
@@ -242,6 +279,116 @@ export function countTableProblems() {
|
|
|
242
279
|
problems.push('[authoring] SHIPPED.md carries no \'N laws, N agents, N skills\' line — '
|
|
243
280
|
+ 'deleting it is how that gate goes green while the count drifts');
|
|
244
281
|
}
|
|
245
|
-
|
|
282
|
+
// A bad upstreams.json is creditProblems' to name; here it means no keys to hold the README to.
|
|
283
|
+
let upstreamKeys;
|
|
284
|
+
try { upstreamKeys = Object.keys(upstreams()); } catch { upstreamKeys = []; }
|
|
285
|
+
problems.push(...proseMirrorProblems(readme, web, counts, shippedSkills, shipped, upstreamKeys));
|
|
286
|
+
return problems;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The `.md` files directly in `d`, sorted; none for a missing folder. */
|
|
290
|
+
const mdIn = (d) => { try { return readdirSync(d).filter((f) => f.endsWith('.md')).sort(); } catch { return []; } };
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The kit arm (spec A6, R3, R4, B7): each `src/kits/<kit>.json` against `src`. Refused: a
|
|
294
|
+
* manifest that does not parse or lacks `description`/`rules`/`skills`/`agents`; a reserved kit
|
|
295
|
+
* id (`RETIRED_KIT_IDS`); `rules` naming a missing file, or rendering (neutral theme) to more
|
|
296
|
+
* words than its `rulesWordBudget` (default 300); a member in two kits, or a core one (a row in
|
|
297
|
+
* AGENT.md.tmpl) — R3; a member with no spec (or a vendored folder skill) or no `registry.json` row; a catalogue row not of the shape `| [<m>]({{DIR_*}}/<m>.md) | <text> |`, or a member without exactly one; any `banned` string (manifest) in a member file or the rules, unless the line carries `<!-- banned: ok -->`; and a core file linking a
|
|
298
|
+
* kit member, which dangles with the kit off — R4. `registry.json` sits beside `src`, as at ROOT.
|
|
299
|
+
*/
|
|
300
|
+
export function kitProblems(src = SRC) {
|
|
301
|
+
let kits;
|
|
302
|
+
try { kits = loadKits(src); } catch (e) { return [`[authoring] ${e.message}`]; }
|
|
303
|
+
if (!kits.size) return [];
|
|
304
|
+
const problems = [];
|
|
305
|
+
const core = { agents: new Set(), skills: new Set() };
|
|
306
|
+
let tmpl = '';
|
|
307
|
+
try { tmpl = readText(path.join(src, 'AGENT.md.tmpl')); } catch { /* countTableProblems names it */ }
|
|
308
|
+
for (const m of tmpl.matchAll(TMPL_SPEC_RE)) core[m[1] === 'AGENTS' ? 'agents' : 'skills'].add(m[2]);
|
|
309
|
+
// null = no usable registry: `registryProblems` already names that (and every member it lacks a
|
|
310
|
+
// row for), so a member's missing row is only reported here when there IS an entities object.
|
|
311
|
+
let registry = null;
|
|
312
|
+
try { registry = JSON.parse(readText(path.join(path.dirname(src), 'registry.json'))).entities ?? null; } catch {
|
|
313
|
+
// registryProblems names an unreadable registry
|
|
314
|
+
}
|
|
315
|
+
const owner = new Map();
|
|
316
|
+
const cfg = { ...makeCfg(), src };
|
|
317
|
+
let theme = null;
|
|
318
|
+
for (const [kit, m] of kits) {
|
|
319
|
+
const at = `src/kits/${kit}.json`;
|
|
320
|
+
if (Object.hasOwn(RETIRED_KIT_IDS, kit)) {
|
|
321
|
+
problems.push(`[authoring] ${at}: kit id '${kit}' is reserved (${RETIRED_KIT_IDS[kit]})`);
|
|
322
|
+
}
|
|
323
|
+
const lacking = ['description', 'rules', 'skills', 'agents'].filter((k) => (
|
|
324
|
+
k === 'skills' || k === 'agents' ? !Array.isArray(m[k]) : typeof m[k] !== 'string'));
|
|
325
|
+
for (const k of lacking) problems.push(`[authoring] ${at} has no '${k}'`);
|
|
326
|
+
if (lacking.length) continue;
|
|
327
|
+
for (const kind of ['skills', 'agents']) {
|
|
328
|
+
for (const name of m[kind]) {
|
|
329
|
+
const key = `${kind}/${name}`;
|
|
330
|
+
if (owner.has(key)) problems.push(`[authoring] ${key} is in two kits: '${owner.get(key)}' and '${kit}'`);
|
|
331
|
+
else owner.set(key, kit);
|
|
332
|
+
if (core[kind].has(name)) problems.push(`[authoring] ${key} is core (AGENT.md.tmpl lists it), but kit '${kit}' claims it`);
|
|
333
|
+
if (!isFile(path.join(src, kind, `${name}.md`))) {
|
|
334
|
+
problems.push(kind === 'skills' && isFile(path.join(src, kind, name, 'SKILL.md'))
|
|
335
|
+
? `[authoring] kit '${kit}' lists ${name}, a vendored folder skill; kit members must be flat specs`
|
|
336
|
+
: `[authoring] kit '${kit}' lists ${key}, but src/${key}.md does not exist`);
|
|
337
|
+
}
|
|
338
|
+
if (registry !== null && !Object.hasOwn(registry, key)) problems.push(`[authoring] ${key} (kit '${kit}') has no row in registry.json`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// A6: a kit's catalogue rows are its own members' — a core row here would show twice with the
|
|
342
|
+
// kit on, or vanish with it off. Each row is `| [<m>]({{DIR_*}}/<m>.md) | <text> |`, one per member.
|
|
343
|
+
for (const kind of ['skills', 'agents']) {
|
|
344
|
+
const dir = kind === 'skills' ? 'SKILLS' : 'AGENTS';
|
|
345
|
+
const seen = new Map();
|
|
346
|
+
for (const row of m.catalogue?.[kind] ?? []) {
|
|
347
|
+
const hit = new RegExp(`^\\| \\[([a-z0-9-]+)\\]\\(\\{\\{DIR_${dir}\\}\\}/\\1\\.md\\) \\| \\S.* \\|$`).exec(row);
|
|
348
|
+
const named = hit?.[1] ?? row.match(/\{\{DIR_(?:AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/)?.[1];
|
|
349
|
+
if (named && !m[kind].includes(named)) {
|
|
350
|
+
problems.push(`[authoring] ${at} catalogue lists ${kind}/${named}, which is not a member of kit '${kit}'`);
|
|
351
|
+
} else if (!hit) {
|
|
352
|
+
problems.push(`[authoring] ${at} catalogue.${kind} row is not \`| [<member>]({{DIR_${dir}}}/<member>.md) | <text> |\`: ${row}`);
|
|
353
|
+
} else seen.set(named, (seen.get(named) ?? 0) + 1);
|
|
354
|
+
}
|
|
355
|
+
for (const name of m[kind]) {
|
|
356
|
+
const n = seen.get(name) ?? 0;
|
|
357
|
+
if (n !== 1) problems.push(`[authoring] ${at} catalogue.${kind} has ${n} rows for ${name}, want exactly one`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// T4: strings no member file may contain; a line carrying the marker is exempt (a rule that names the construct to forbid it).
|
|
361
|
+
const files = [m.rules, ...m.skills.flatMap((n) => [`skills/${n}.md`, ...mdIn(path.join(src, 'skills', n)).map((f) => `skills/${n}/${f}`)]),
|
|
362
|
+
...m.agents.map((n) => `agents/${n}.md`)];
|
|
363
|
+
for (const rel of files) {
|
|
364
|
+
if (!isFile(path.join(src, rel))) continue;
|
|
365
|
+
readText(path.join(src, rel)).split('\n').forEach((line, i) => {
|
|
366
|
+
if (line.includes('<!-- banned: ok -->')) return;
|
|
367
|
+
for (const b of m.banned ?? []) if (line.includes(b)) problems.push(`[authoring] src/${rel}:${i + 1} contains '${b}', banned by kit '${kit}'`);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const rules = path.join(src, m.rules);
|
|
371
|
+
if (!isFile(rules)) {
|
|
372
|
+
problems.push(`[authoring] ${at}: rules '${m.rules}' does not exist`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
theme ??= effectiveTheme(cfg, 'neutral');
|
|
376
|
+
const words = renderFile(cfg, rules, theme).split(/\s+/).filter(Boolean).length;
|
|
377
|
+
const budget = m.rulesWordBudget ?? 300;
|
|
378
|
+
if (words > budget) {
|
|
379
|
+
problems.push(`[authoring] src/${m.rules} renders to ${words} words, over kit '${kit}' rulesWordBudget of ${budget}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// R4. A core member (R3, named above) is left out: every core file linking it would repeat that.
|
|
383
|
+
const r4 = new Set();
|
|
384
|
+
for (const l of specLinks(src)) {
|
|
385
|
+
// Links run kit -> core only: a core file, or ANOTHER kit's file, linking a member dangles
|
|
386
|
+
// whenever that member's kit is off.
|
|
387
|
+
if (!l.owner || l.kit === l.owner || core[l.kind].has(l.name)) continue;
|
|
388
|
+
r4.add(l.kit
|
|
389
|
+
? `[authoring] src/${l.rel} (kit '${l.kit}') links ${l.kind}/${l.name}, a member of kit '${l.owner}' — links run kit → core only`
|
|
390
|
+
: `[authoring] src/${l.rel} links ${l.kind}/${l.name}, a kit member — the link dangles when the kit is off`);
|
|
391
|
+
}
|
|
392
|
+
problems.push(...r4);
|
|
246
393
|
return problems;
|
|
247
394
|
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import {
|
|
12
|
-
ALIASES_RE, PLUGIN_SRC, RETIRED_SKILL_IDS, ROOT, SRC, THEMES, WORKFLOW_SRC, aliasesOf,
|
|
12
|
+
ALIASES_RE, CREDIT_RE, PLUGIN_SRC, RETIRED_SKILL_IDS, ROOT, SRC, THEMES, WORKFLOW_SRC, aliasesOf,
|
|
13
|
+
knownSkillIds, upstreams,
|
|
13
14
|
} from '../build/source.mjs';
|
|
14
15
|
import { HOSTS } from '../hosts/hosts.mjs';
|
|
15
16
|
import { installTargets, installsReferencingShim } from '../hosts/installs.mjs';
|
|
@@ -22,7 +23,7 @@ import { splitLines } from '../lib/udiff.mjs';
|
|
|
22
23
|
import { ENTITY_STATUSES } from './inventory.mjs';
|
|
23
24
|
import { assessRepo } from './scorecard.mjs';
|
|
24
25
|
import { badgeUrl, renderSvg } from './scorecard-svg.mjs';
|
|
25
|
-
import { isDir, isFile, rglob, srcStems } from './scan.mjs';
|
|
26
|
+
import { isDir, isFile, rglob, srcStems, within } from './scan.mjs';
|
|
26
27
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -225,14 +226,17 @@ const MOVING_REFS = new Set(['main', 'master', 'develop', 'dev', 'head', 'latest
|
|
|
225
226
|
|
|
226
227
|
/**
|
|
227
228
|
* `_harness_build._vendor_pin_problems` — every vendored skill folder records where it came
|
|
228
|
-
* from
|
|
229
|
+
* from: `**Upstream key:** <key>`, naming its `src/upstreams.json` entry, which holds the repo,
|
|
230
|
+
* the pinned commit and the licence (`creditProblems` checks those facts).
|
|
229
231
|
*
|
|
230
232
|
* First-party folders that merely ride the vendored mechanism for their multi-file layout
|
|
231
|
-
* declare that instead (`**Upstream:** this
|
|
232
|
-
* upstream to drift against.
|
|
233
|
+
* declare that instead (`**Upstream:** this …`, plus a `**License:**`) and have no key: there is
|
|
234
|
+
* no upstream to drift against.
|
|
233
235
|
*/
|
|
234
236
|
export function vendorPinProblems() {
|
|
235
237
|
const problems = [];
|
|
238
|
+
let entries;
|
|
239
|
+
try { entries = upstreams(); } catch { entries = {}; } // creditProblems names the bad JSON
|
|
236
240
|
for (const name of VENDORED_SKILL_DIRS) {
|
|
237
241
|
const folder = path.join(SRC, 'skills', name);
|
|
238
242
|
if (!isDir(folder)) {
|
|
@@ -246,24 +250,95 @@ export function vendorPinProblems() {
|
|
|
246
250
|
+ 'pinned commit and license are unrecorded');
|
|
247
251
|
continue;
|
|
248
252
|
}
|
|
249
|
-
if (!/\*\*License:\*\*/.test(text)) {
|
|
250
|
-
problems.push(`[authoring] skills/${name}/VENDOR.md records no '**License:**'`);
|
|
251
|
-
}
|
|
252
253
|
const upstream = /\*\*Upstream:\*\*\s*(\S+)/.exec(text);
|
|
253
|
-
if (
|
|
254
|
-
|
|
254
|
+
if (upstream && upstream[1].toLowerCase().startsWith('this')) { // first-party
|
|
255
|
+
if (!/\*\*License:\*\*/.test(text)) {
|
|
256
|
+
problems.push(`[authoring] skills/${name}/VENDOR.md records no '**License:**'`);
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const key = /\*\*Upstream key:\*\*\s*(\S+)/.exec(text);
|
|
261
|
+
if (!key) {
|
|
262
|
+
problems.push(`[authoring] skills/${name}/VENDOR.md declares no '**Upstream key:**' `
|
|
263
|
+
+ "(nor '**Upstream:** this repository' for a first-party folder)");
|
|
264
|
+
} else if (!Object.hasOwn(entries, key[1])) {
|
|
265
|
+
problems.push(`[authoring] skills/${name}/VENDOR.md names upstream key '${key[1]}', `
|
|
266
|
+
+ 'which src/upstreams.json does not define');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return problems;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The files a `{{CREDIT:` may sit in: those `renderAll` derives a licence copy for, plus agents. */
|
|
273
|
+
const CREDITABLE_RE = /^(?:skills\/[^/_][^/]*(?:\/[^/_][^/]*)?|agents\/[^/_][^/]*)\.md$/;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Every credit ships with what it promises. A `{{CREDIT:<key>:…}}` key resolves in
|
|
277
|
+
* `src/upstreams.json`; every entry there is credited by a token or a VENDOR.md
|
|
278
|
+
* `**Upstream key:**`; its `licenseFile` exists (the emit copies it, and throws mid-build if
|
|
279
|
+
* not); its commit is an immutable sha. A token in a `_` partial is refused: it is included
|
|
280
|
+
* into other files, whose raw source `renderAll` scans for the licence copy, so it would credit
|
|
281
|
+
* without shipping the licence. A `licenseFile: null` is only for a key credited from a vendored
|
|
282
|
+
* folder alone, which ships its upstream's own LICENSE: anywhere else the licence text must
|
|
283
|
+
* ship with the work, so a token outside those folders needs a licence file.
|
|
284
|
+
*/
|
|
285
|
+
export function creditProblems(src = SRC) {
|
|
286
|
+
let entries;
|
|
287
|
+
try { entries = upstreams(src); } catch {
|
|
288
|
+
return ['[authoring] src/upstreams.json is not valid JSON'];
|
|
289
|
+
}
|
|
290
|
+
const problems = [];
|
|
291
|
+
const used = new Set();
|
|
292
|
+
/** key -> the first non-vendored file whose token credits it. */
|
|
293
|
+
const needsLicence = new Map();
|
|
294
|
+
for (const file of rglob(src)) {
|
|
295
|
+
if (!file.endsWith('.md') || !isFile(file)) continue;
|
|
296
|
+
const rel = path.relative(src, file).split(path.sep).join('/');
|
|
297
|
+
const text = readText(file);
|
|
298
|
+
const vendorKey = path.basename(file) === 'VENDOR.md' && /\*\*Upstream key:\*\*\s*(\S+)/.exec(text);
|
|
299
|
+
if (vendorKey) used.add(vendorKey[1]);
|
|
300
|
+
if (!text.includes('{{CREDIT:')) continue;
|
|
301
|
+
if (!CREDITABLE_RE.test(rel)) {
|
|
302
|
+
problems.push(`[authoring] ${rel} carries a {{CREDIT:}} token, but only skills/<stem>.md, `
|
|
303
|
+
+ 'skills/<stem>/*.md and agents/*.md may — the licence copy is derived for those alone');
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
for (const key of new Set([...text.matchAll(CREDIT_RE)].map((m) => m[1]))) {
|
|
307
|
+
used.add(key);
|
|
308
|
+
const sp = rel.split('/');
|
|
309
|
+
if (!(sp.length > 2 && VENDORED_SKILL_DIRS.has(sp[1])) && !needsLicence.has(key)) needsLicence.set(key, rel);
|
|
310
|
+
if (!Object.hasOwn(entries, key)) {
|
|
311
|
+
problems.push(`[authoring] ${rel} credits '${key}', which src/upstreams.json does not define`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const [key, u] of Object.entries(entries)) {
|
|
316
|
+
const where = `src/upstreams.json entry '${key}'`;
|
|
317
|
+
if (!used.has(key)) problems.push(`[authoring] ${where} is credited by no {{CREDIT:}} token and no VENDOR.md`);
|
|
318
|
+
const pin = String(u.commit ?? '');
|
|
319
|
+
if (MOVING_REFS.has(pin.toLowerCase())) {
|
|
320
|
+
problems.push(`[authoring] ${where} pins '${pin}', a moving branch — record the commit sha instead`);
|
|
321
|
+
} else if (!HEX40_RE.test(pin.toLowerCase())) {
|
|
322
|
+
problems.push(`[authoring] ${where} pin '${pin}' is not a 40-character commit sha`);
|
|
323
|
+
}
|
|
324
|
+
for (const field of ['repo', 'author', 'license']) {
|
|
325
|
+
if (!u[field]) problems.push(`[authoring] ${where} has no '${field}'`);
|
|
326
|
+
}
|
|
327
|
+
if (u.frozen !== undefined && typeof u.frozen !== 'boolean') {
|
|
328
|
+
problems.push(`[authoring] ${where} has frozen ${JSON.stringify(u.frozen)}, which is not a boolean`);
|
|
329
|
+
}
|
|
330
|
+
if (!u.licenseFile) {
|
|
331
|
+
if (needsLicence.has(key)) {
|
|
332
|
+
problems.push(`[authoring] ${where} has licenseFile null, but ${needsLicence.get(key)} credits it `
|
|
333
|
+
+ 'outside a vendored folder — its licence text must ship with the work');
|
|
334
|
+
}
|
|
255
335
|
continue;
|
|
256
336
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
problems.push(`[authoring] skills/${name}/VENDOR.md pins '${pin[1]}', a `
|
|
263
|
-
+ 'moving branch — record the commit sha instead');
|
|
264
|
-
} else if (!HEX40_RE.test(pin[1].toLowerCase())) {
|
|
265
|
-
problems.push(`[authoring] skills/${name}/VENDOR.md pin '${pin[1]}' is not `
|
|
266
|
-
+ 'a 40-character commit sha');
|
|
337
|
+
// The emit copies it into every install, so it must be a file of this tree.
|
|
338
|
+
if (!within(path.resolve(src, u.licenseFile), path.resolve(src))) {
|
|
339
|
+
problems.push(`[authoring] ${where} names licenseFile '${u.licenseFile}', which resolves outside src/`);
|
|
340
|
+
} else if (!isFile(path.join(src, u.licenseFile))) {
|
|
341
|
+
problems.push(`[authoring] ${where} names licenseFile '${u.licenseFile}', which does not exist under src/`);
|
|
267
342
|
}
|
|
268
343
|
}
|
|
269
344
|
return problems;
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
} from '../hosts/hosts.mjs';
|
|
50
50
|
import {
|
|
51
51
|
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, excludedRulesOfDir,
|
|
52
|
-
excludedSkillsOfDir, gatesOfDir,
|
|
52
|
+
excludedSkillsOfDir, gatesOfDir, kitsOfDir,
|
|
53
53
|
footprintOfDir, modeOfDir, postureOfDir,
|
|
54
54
|
trustOfDir,
|
|
55
55
|
readJsonMaybe, readMaybe, themeOfDir,
|
|
@@ -265,6 +265,8 @@ export function diffCollect({
|
|
|
265
265
|
excludeRules: excludedRulesOfDir(dir),
|
|
266
266
|
// The excluded skills, or every skill the install left out reads as deleted.
|
|
267
267
|
excludeSkills: excludedSkillsOfDir(dir),
|
|
268
|
+
// The kits on, or every kit file reads as added and the Kits block as drift.
|
|
269
|
+
kits: kitsOfDir(dir),
|
|
268
270
|
// The gate switches, or a disarmed install reads its `<gate>: off` line as drift.
|
|
269
271
|
...gatesOfDir(dir),
|
|
270
272
|
};
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -96,6 +96,7 @@ export const SKILL_CLASS = {
|
|
|
96
96
|
develop: 'build', refactor: 'build', debug: 'build', dependencies: 'build',
|
|
97
97
|
'frontend-design': 'build', prototype: 'build', deck: 'build',
|
|
98
98
|
'forge-mcp': 'build', bruno: 'build', worktree: 'build',
|
|
99
|
+
'spring-boot': 'build', 'spring-data-jpa': 'build', 'spring-testing': 'build',
|
|
99
100
|
'geneseed-code-review': 'review', 'fresh-eyes': 'review', 'security-audit': 'review',
|
|
100
101
|
'roast-me': 'review', 'review-response': 'review', ponytail: 'review',
|
|
101
102
|
commit: 'ship', ship: 'ship', release: 'ship', handoff: 'ship',
|
package/js/inspect/status.mjs
CHANGED
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
59
59
|
import path from 'node:path';
|
|
60
60
|
|
|
61
|
-
import { GATE_SWITCHES, THEMES, ROOT, makeCfg } from '../build/source.mjs';
|
|
61
|
+
import { GATE_SWITCHES, THEMES, ROOT, loadKits, makeCfg } from '../build/source.mjs';
|
|
62
62
|
import { tuiInventory } from './inventory.mjs';
|
|
63
63
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
64
64
|
import {
|
|
65
65
|
claudeConfigDir, bobConfigDir, openclaudeConfigDir, opencodeConfigDir, opencodeShadowedInstall,
|
|
66
|
-
isHostGlobalDir, resolvePath, resolveMemoryDir, sovereignBypass, CLAUDE_STYLE, GATE_LEDGER,
|
|
66
|
+
isHostGlobalDir, resolvePath, resolveMemoryDir, sovereignBypass, CLAUDE_STYLE, GATE_LEDGER, HOSTS,
|
|
67
67
|
} from '../hosts/hosts.mjs';
|
|
68
68
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
69
69
|
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
@@ -238,6 +238,40 @@ export function gateSummary(cfgDirs, platform = process.platform) {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
/** `_harness_status._status_data`. */
|
|
241
|
+
/**
|
|
242
|
+
* Spec R6: the member files of every kit NOT on in this install that are still on disk — the ones
|
|
243
|
+
* `pruneOwned` kept because their owner edited them — as `{ kit: [rel, ...] }`, or `null` when
|
|
244
|
+
* there are none. No new state: the kit manifests say which rels a member owns (`skills/<m>.md`,
|
|
245
|
+
* `skills/<m>/...`, `agents/<m>.md`), the install's layer dir says where to look.
|
|
246
|
+
*/
|
|
247
|
+
export function keptKitFiles(host, scope, root, active) {
|
|
248
|
+
const marker = HOSTS.find((h) => h.host === host)?.projectMarker;
|
|
249
|
+
const dir = scope === 'global' || !marker ? root : path.join(root, marker);
|
|
250
|
+
const walk = (rel) => {
|
|
251
|
+
const abs = path.join(dir, rel);
|
|
252
|
+
let entries;
|
|
253
|
+
try { entries = readdirSync(abs, { withFileTypes: true }); } catch { return []; }
|
|
254
|
+
return entries.flatMap((e) => (e.isDirectory() ? walk(`${rel}/${e.name}`)
|
|
255
|
+
: e.isFile() ? [`${rel}/${e.name}`] : []));
|
|
256
|
+
};
|
|
257
|
+
const kept = {};
|
|
258
|
+
let kits;
|
|
259
|
+
try { kits = loadKits(); } catch (e) {
|
|
260
|
+
process.stderr.write(`warning: kit rows skipped — ${e.message}\n`);
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
for (const [kit, m] of kits) {
|
|
264
|
+
if (active.includes(kit)) continue;
|
|
265
|
+
const rels = [
|
|
266
|
+
...(m.skills ?? []).flatMap((n) => [`skills/${n}.md`].filter((r) => existsSync(path.join(dir, r)))
|
|
267
|
+
.concat(walk(`skills/${n}`))),
|
|
268
|
+
...(m.agents ?? []).map((n) => `agents/${n}.md`).filter((r) => existsSync(path.join(dir, r))),
|
|
269
|
+
].sort();
|
|
270
|
+
if (rels.length) kept[kit] = rels;
|
|
271
|
+
}
|
|
272
|
+
return Object.keys(kept).length ? kept : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
241
275
|
export function statusData() {
|
|
242
276
|
const inst = installedDefaults();
|
|
243
277
|
const theme = inst.theme || defaultTheme();
|
|
@@ -304,10 +338,12 @@ export function statusData() {
|
|
|
304
338
|
installs: installTargets().map(([host, scope, root]) => {
|
|
305
339
|
try {
|
|
306
340
|
const p = installProfile(host, scope, root);
|
|
341
|
+
// Only when something is kept, so `--json` without kits stays the bytes it was.
|
|
342
|
+
const kept = keptKitFiles(host, scope, root, p.kits);
|
|
307
343
|
const rebuild = legacyProcessCarrierOfDir(root, host)
|
|
308
344
|
? "geneseed setup # pre-comms carrier (names 'process 8') — run setup for this install, not a rebuild"
|
|
309
345
|
: rebuildCommand(p.argv);
|
|
310
|
-
return { ...p, rebuild };
|
|
346
|
+
return { ...p, ...(kept ? { keptKitFiles: kept } : {}), rebuild };
|
|
311
347
|
} catch (e) {
|
|
312
348
|
return { host, scope, root, state: 'unreadable', error: String(e?.message ?? e) };
|
|
313
349
|
}
|
|
@@ -422,10 +458,15 @@ export function statusLines(d, color = false) {
|
|
|
422
458
|
const excl = p.excludeRules?.length ? `excluding ${p.excludeRules.join(',')}` : 'no rules excluded';
|
|
423
459
|
rows.push([`${p.host}:${p.scope}`, [p.state, p.theme, p.footprint, `${p.posture}/${p.mode}/${p.trust}`,
|
|
424
460
|
packs, excl, ...(p.excludeSkills?.length ? [`skills off: ${p.excludeSkills.join(',')}`] : []),
|
|
461
|
+
...(p.kits?.length ? [`kits: ${p.kits.join(',')}`] : []),
|
|
425
462
|
// Each elided when armed. The law's text still binds; only its tool-boundary gate is off.
|
|
426
463
|
...GATE_SWITCHES.filter((g) => p[g.key] === false)
|
|
427
464
|
.map((g) => `${g.marker.toLowerCase()}: off (Law ${g.law} text still binds)`)]
|
|
428
465
|
.join(` ${DOT} `) + where]);
|
|
466
|
+
// R6: a member its owner edited outlives its kit; name it, so turning the kit off is visible.
|
|
467
|
+
for (const [kit, rels] of Object.entries(p.keptKitFiles ?? {})) {
|
|
468
|
+
rows.push([`${p.host}:${p.scope}`, `kept from inactive kit ${kit}: ${rels.join(', ')}${where}`]);
|
|
469
|
+
}
|
|
429
470
|
}
|
|
430
471
|
|
|
431
472
|
const labelW = Math.max(...rows.map(([k]) => codePointLength(k)));
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -34,8 +34,8 @@ export const VALIDATE_USAGE = [
|
|
|
34
34
|
'usage: geneseed validate [-h] [--theme THEME] [--emit EMIT] [--out OUT] [--root ROOT]',
|
|
35
35
|
' [--footprint {lean,full}] [--posture POSTURE] [--mode MODE]',
|
|
36
36
|
' [--trust TRUST] [--doctrines DOCTRINES] [--exclude-rules RULES]',
|
|
37
|
-
' [--exclude-skills SKILLS] [--
|
|
38
|
-
' [--no-deletion-gate] [-v]',
|
|
37
|
+
' [--exclude-skills SKILLS] [--kits KITS] [--no-secrets-gate]',
|
|
38
|
+
' [--no-done-gate] [--no-deletion-gate] [-v]',
|
|
39
39
|
'',
|
|
40
40
|
"Render and emit into a throwaway sandbox, run the doctor's checks, and write nothing real.",
|
|
41
41
|
"Takes the generator's render flags (see geneseed-build --help) except --sync-themes and",
|
|
@@ -130,7 +130,7 @@ export function cmdValidate(args) {
|
|
|
130
130
|
const opts = {
|
|
131
131
|
theme: args.theme, footprint: args.footprint, posture: args.posture, mode: args.mode,
|
|
132
132
|
trust: args.trust, doctrines: args.doctrines, excludeRules: args.excludeRules,
|
|
133
|
-
excludeSkills: args.excludeSkills,
|
|
133
|
+
excludeSkills: args.excludeSkills, kits: args.kits,
|
|
134
134
|
...Object.fromEntries(GATE_SWITCHES.map((g) => [g.key, args[g.arg] ? false : null])),
|
|
135
135
|
};
|
|
136
136
|
let scanDirs;
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -46,7 +46,7 @@ import path from 'node:path';
|
|
|
46
46
|
|
|
47
47
|
import { main as driverMain, resolveOut } from '../build/driver.mjs';
|
|
48
48
|
import { playLine } from '../ui/anim.mjs';
|
|
49
|
-
import { discoverNames, GATE_SWITCHES, PACK_ORDER } from '../build/source.mjs';
|
|
49
|
+
import { discoverNames, GATE_SWITCHES, PACK_ORDER, loadKits } from '../build/source.mjs';
|
|
50
50
|
import { exportImprovements } from '../inspect/diff.mjs';
|
|
51
51
|
import { cmdDoctor } from '../inspect/doctor.mjs';
|
|
52
52
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
@@ -227,6 +227,28 @@ function askDoctrines(deployed = null) {
|
|
|
227
227
|
known === null || known.includes(name) ? 'yes' : 'no') === 'yes');
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The Kits question (spec A5): one pick, a kit or `none`, defaulting to what the install has on
|
|
232
|
+
* (`deployed`, its `Active kits:` marker). Asked ONLY when this checkout ships a kit, so a kit-less
|
|
233
|
+
* wizard reads exactly the stdin it always did (and has no kit to carry: `kitsOfDir` drops an unshipped one).
|
|
234
|
+
* Returns the kit list, or null for none (`setupBuildArgs` omits `--kits`).
|
|
235
|
+
*/
|
|
236
|
+
export function askKits(deployed = null, kits = null) {
|
|
237
|
+
try { kits ??= loadKits(); } catch (e) {
|
|
238
|
+
process.stderr.write(`warning: kit question skipped — ${e.message}\n`);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const on = (deployed ?? []).filter((k) => kits.has(k));
|
|
242
|
+
if (!kits.size) return null;
|
|
243
|
+
const opts = [...kits].map(([k, m]) => [k, m.description ?? '']);
|
|
244
|
+
// shortcut: one pick per run, so several deployed kits are offered as one "keep" option; ask
|
|
245
|
+
// per kit once a second kit ships.
|
|
246
|
+
if (on.length > 1) opts.push([on.join(','), 'keep the installed kits']);
|
|
247
|
+
opts.push(['none', 'no kit']);
|
|
248
|
+
const pick = askChoice('Kits', opts, on.length ? on.join(',') : 'none');
|
|
249
|
+
return pick === 'none' ? null : pick.split(',');
|
|
250
|
+
}
|
|
251
|
+
|
|
230
252
|
/**
|
|
231
253
|
* `_harness_setup._collect_setup_lines` — the six questions, the plan, the confirm.
|
|
232
254
|
*
|
|
@@ -260,13 +282,15 @@ function targetGates(emit, out) {
|
|
|
260
282
|
}
|
|
261
283
|
}
|
|
262
284
|
|
|
263
|
-
export function collectSetupLines() {
|
|
285
|
+
export function collectSetupLines(kitMap = null) {
|
|
264
286
|
printOut('Geneseed setup — answer a few questions; nothing is written until you confirm.\n');
|
|
265
287
|
const inst = installedDefaults();
|
|
266
288
|
const theme = askChoice('Theme', themeOptions(), inst.theme || defaultTheme());
|
|
267
289
|
const posture = askChoice('Posture', postureOptions(), inst.posture || defaultPosture());
|
|
268
290
|
const mode = askChoice('Mode', modeOptions(), inst.mode || defaultMode());
|
|
269
291
|
const doctrines = askDoctrines(inst.doctrines);
|
|
292
|
+
// Kits on stay on (spec A3); `kitsOfDir` already dropped a retired one.
|
|
293
|
+
const kits = askKits(inst.kits, kitMap);
|
|
270
294
|
const emit = askChoice('Install mode', EMIT_OPTIONS, inst.emit || 'opencode-global');
|
|
271
295
|
const footprint = askChoice('Footprint', FOOTPRINT_OPTIONS,
|
|
272
296
|
inst.footprint || 'lean');
|
|
@@ -304,13 +328,13 @@ export function collectSetupLines() {
|
|
|
304
328
|
const gated = Object.values(gates).some((on) => on === false);
|
|
305
329
|
printOut(`\nAbout to run: geneseed build ${
|
|
306
330
|
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks,
|
|
307
|
-
excludeRules, undefined, excludeSkills, gates).join(' ')}\n`);
|
|
331
|
+
excludeRules, undefined, excludeSkills, gates, kits).join(' ')}\n`);
|
|
308
332
|
if (!confirm('Proceed?', true)) return null;
|
|
309
333
|
// The key only when there is something to carry, so a selection that excludes no skill and
|
|
310
334
|
// switches off no gate is the same object it was before either existed.
|
|
311
335
|
return {
|
|
312
336
|
theme, posture, mode, doctrines, excludeRules, ...(excludeSkills ? { excludeSkills } : {}),
|
|
313
|
-
...(gated ? { gates } : {}),
|
|
337
|
+
...(gated ? { gates } : {}), ...(kits ? { kits } : {}),
|
|
314
338
|
emit, out, root, footprint,
|
|
315
339
|
};
|
|
316
340
|
}
|
|
@@ -494,7 +518,7 @@ export function setupLines() {
|
|
|
494
518
|
}
|
|
495
519
|
const {
|
|
496
520
|
theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
|
|
497
|
-
doctrines = null, excludeRules = null, excludeSkills = null, gates = {},
|
|
521
|
+
doctrines = null, excludeRules = null, excludeSkills = null, gates = {}, kits = null,
|
|
498
522
|
} = sel;
|
|
499
523
|
if (emit === 'opencode-global') {
|
|
500
524
|
// The build below overwrites the deployed global harness, and the self-improvement loops
|
|
@@ -513,7 +537,7 @@ export function setupLines() {
|
|
|
513
537
|
// runs must carry the pack selection the plan line just previewed, or the wizard would print
|
|
514
538
|
// one command and run another.
|
|
515
539
|
const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
|
|
516
|
-
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates);
|
|
540
|
+
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates, kits);
|
|
517
541
|
printOut(`Running: geneseed build ${argv.join(' ')}\n`);
|
|
518
542
|
const rc = driverMain(argv);
|
|
519
543
|
if (rc !== 0) {
|
package/js/maintain/update.mjs
CHANGED
|
@@ -632,7 +632,7 @@ export async function installAxes(dir, { installProfile, setupBuildArgs }, notif
|
|
|
632
632
|
if (legacyProcessCarrierOfDir(String(dir), 'opencode')) return null;
|
|
633
633
|
const p = installProfile('opencode', 'global', String(dir), notify);
|
|
634
634
|
return setupBuildArgs('', 'files', null, null, p.footprint, p.posture, p.mode, p.doctrines,
|
|
635
|
-
undefined, p.excludeRules, p.trust, p.excludeSkills, p).slice(4);
|
|
635
|
+
undefined, p.excludeRules, p.trust, p.excludeSkills, p, p.kits).slice(4);
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
/**
|