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
|
@@ -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/cost.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { isFile, readText } from '../lib/fs.mjs';
|
|
|
35
35
|
* console draws it on the cost chart, so it lives here, once, where both read it. Raise it by
|
|
36
36
|
* exactly the measured overrun and say why in that test's docblock, which keeps its history.
|
|
37
37
|
*/
|
|
38
|
-
export const CARRIER_CEILING = { full:
|
|
38
|
+
export const CARRIER_CEILING = { full: 62_263, lean: 39_156 };
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* A frontmatter value as the host shows it. The emit writes descriptions JSON-escaped
|
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
|
};
|
|
@@ -319,8 +321,38 @@ export function diffCollect({
|
|
|
319
321
|
/** The records that are drift: everything but a `linked` path, which is the user's own. */
|
|
320
322
|
export const drifted = (files) => files.filter((f) => f.status !== 'linked');
|
|
321
323
|
|
|
324
|
+
/** Where the self-improvement loops write, relative to an install's `notebook/`. */
|
|
325
|
+
export const NOTES_DIRS = ['skill-notes', 'agent-notes'];
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The self-improvement notes an install has accumulated, as `{rel, body}` sorted by `rel`.
|
|
329
|
+
*
|
|
330
|
+
* WHY THESE ARE NOT DRIFT. The loops used to edit the rendered skill and agent files in place,
|
|
331
|
+
* which the doctor reported as stale and the next rebuild erased. They now write a sidecar
|
|
332
|
+
* under the notebook — seed-once and agent-owned, never in the manifest, so no rebuild touches
|
|
333
|
+
* it. That makes the notes the back-port QUEUE rather than an endangered edit: `diff` lists
|
|
334
|
+
* them and `--out` carries them so they can reach `src/`, but the upgrade export does not
|
|
335
|
+
* fire on them alone, because there is nothing for the rebuild to destroy. Empty files are
|
|
336
|
+
* skipped — an agent that cleared its notes has nothing to back-port.
|
|
337
|
+
*/
|
|
338
|
+
export function collectNotes(dir) {
|
|
339
|
+
const notes = [];
|
|
340
|
+
for (const sub of NOTES_DIRS) {
|
|
341
|
+
const d = path.join(dir, 'notebook', sub);
|
|
342
|
+
let names;
|
|
343
|
+
try { names = readdirSync(d); } catch { continue; }
|
|
344
|
+
for (const n of names.filter((x) => x.endsWith('.md')).sort()) {
|
|
345
|
+
const a = path.join(d, n);
|
|
346
|
+
if (!isFile(a)) continue;
|
|
347
|
+
const body = readText(a);
|
|
348
|
+
if (body.trim()) notes.push({ rel: `notebook/${sub}/${n}`, body });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return notes;
|
|
352
|
+
}
|
|
353
|
+
|
|
322
354
|
/** `_harness_diff._improvements_md` — pure; `when` is the caller's timestamp. */
|
|
323
|
-
export function improvementsMd(target, theme, allFiles, when) {
|
|
355
|
+
export function improvementsMd(target, theme, allFiles, when, notes = []) {
|
|
324
356
|
// A linked path is never a back-port item: the file is the user's, not a local edit of ours.
|
|
325
357
|
const files = drifted(allFiles);
|
|
326
358
|
const edited = files.filter((f) => f.status === 'edited');
|
|
@@ -335,8 +367,8 @@ export function improvementsMd(target, theme, allFiles, when) {
|
|
|
335
367
|
`- ${edited.length} edited · ${added.length} added in deployed · `
|
|
336
368
|
+ `${missing.length} missing from deployed`,
|
|
337
369
|
'',
|
|
338
|
-
'The deployed harness drifted from a fresh render of `src/` — typically
|
|
339
|
-
'
|
|
370
|
+
'The deployed harness drifted from a fresh render of `src/` — typically',
|
|
371
|
+
'agent/skill files edited in place, by hand or by an older harness. Hand this file to',
|
|
340
372
|
'an agent in the Geneseed source repo and ask it to fold the changes below',
|
|
341
373
|
'back into `src/`. Diffs read source -> deployed; the expected copy was',
|
|
342
374
|
'rendered in the deployed theme, so only genuine local edits appear.',
|
|
@@ -350,6 +382,17 @@ export function improvementsMd(target, theme, allFiles, when) {
|
|
|
350
382
|
for (const f of files) {
|
|
351
383
|
lines.push(`## \`${f.rel}\` (${label[f.status]})`, '', '```diff', ...f.diff, '```', '');
|
|
352
384
|
}
|
|
385
|
+
// Only when there are notes, so a report without them stays byte-identical.
|
|
386
|
+
if (notes.length) {
|
|
387
|
+
lines.push('# Self-improvement notes', '',
|
|
388
|
+
'Corrections the agent recorded against a skill or agent instead of editing it.',
|
|
389
|
+
'Fold each one into the matching file under `src/`, then clear the note. Judge it by',
|
|
390
|
+
'its `seen:` evidence — a line without one is a claim, not a lesson — and weigh a',
|
|
391
|
+
'`[general]` line as a law, doctrine or memory rather than an edit to that one file.', '');
|
|
392
|
+
for (const n of notes) {
|
|
393
|
+
lines.push(`## \`${n.rel}\``, '', '```markdown', ...splitLines(n.body), '```', '');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
353
396
|
return `${lines.join('\n')}\n`;
|
|
354
397
|
}
|
|
355
398
|
|
|
@@ -383,7 +426,9 @@ function stamp(d) {
|
|
|
383
426
|
* the same instant, so a run that crosses a second boundary cannot name a file one second
|
|
384
427
|
* away from what it says inside.
|
|
385
428
|
*/
|
|
386
|
-
export function writeImprovements(
|
|
429
|
+
export function writeImprovements(
|
|
430
|
+
target, theme, files, outPath = null, notes = collectNotes(target),
|
|
431
|
+
) {
|
|
387
432
|
const now = stamp(new Date());
|
|
388
433
|
// `Path(out_path).expanduser()` — again NOT resolved, and again observable: the path is
|
|
389
434
|
// printed, so `--out p.md` reports `p.md`.
|
|
@@ -391,7 +436,7 @@ export function writeImprovements(target, theme, files, outPath = null) {
|
|
|
391
436
|
? expanduser(outPath)
|
|
392
437
|
: path.join(target, 'improvements', now.file);
|
|
393
438
|
mkdirSync(path.dirname(p), { recursive: true });
|
|
394
|
-
writeText(p, improvementsMd(target, theme, files, now.when));
|
|
439
|
+
writeText(p, improvementsMd(target, theme, files, now.when, notes));
|
|
395
440
|
return p;
|
|
396
441
|
}
|
|
397
442
|
|
|
@@ -475,18 +520,21 @@ export function cmdDiff(args) {
|
|
|
475
520
|
+ `${missing.length} missing-from-deployed\n`);
|
|
476
521
|
// Its own line, printed only when there is one, so the summary above is unchanged otherwise.
|
|
477
522
|
if (linked.length) printOut(`[diff] ${linked.length} linked (yours, left alone)\n`);
|
|
523
|
+
const notes = collectNotes(target);
|
|
524
|
+
if (notes.length) printOut(`[diff] ${notes.length} self-improvement note(s) to back-port\n`);
|
|
478
525
|
for (const f of edited) printOut(` ~ ${f.rel} (edited in deployed — review to back-port)\n`);
|
|
479
526
|
for (const f of added) printOut(` + ${f.rel} (only in deployed — your addition)\n`);
|
|
480
527
|
for (const f of missing) printOut(` - ${f.rel} (in source, not deployed — re-emit to add)\n`);
|
|
481
528
|
for (const f of linked) printOut(` = ${f.rel} (a link — yours, left alone)\n`);
|
|
529
|
+
for (const n of notes) printOut(` * ${n.rel} (self-improvement note — review to back-port)\n`);
|
|
482
530
|
if (args.out) {
|
|
483
|
-
if (drifted(files).length) {
|
|
531
|
+
if (drifted(files).length || notes.length) {
|
|
484
532
|
// Bare `--out` is argparse's `nargs="?"` const: True, meaning "the default timestamped
|
|
485
533
|
// path". `parse()` gives a flag `true` and an option a string, so the two arrive
|
|
486
534
|
// distinguishable here without a third kind of entry in the VERBS table.
|
|
487
535
|
const outPath = args.out === true ? null : args.out;
|
|
488
536
|
printOut(`[diff] improvements file written: ${
|
|
489
|
-
writeImprovements(target, theme, files, outPath)}\n`);
|
|
537
|
+
writeImprovements(target, theme, files, outPath, notes)}\n`);
|
|
490
538
|
} else {
|
|
491
539
|
printOut('[diff] no differences — nothing written.\n');
|
|
492
540
|
}
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -96,6 +96,8 @@ 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',
|
|
100
|
+
react: 'build', 'react-testing': 'build',
|
|
99
101
|
'geneseed-code-review': 'review', 'fresh-eyes': 'review', 'security-audit': 'review',
|
|
100
102
|
'roast-me': 'review', 'review-response': 'review', ponytail: 'review',
|
|
101
103
|
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/lib/prompt.mjs
CHANGED
|
@@ -12,6 +12,9 @@ import { readSync } from 'node:fs';
|
|
|
12
12
|
import { printOut } from './fs.mjs';
|
|
13
13
|
import { parseIntStrict } from './text.mjs';
|
|
14
14
|
|
|
15
|
+
/** A cell to `Atomics.wait` on: the synchronous pause between `readLine`'s EAGAIN retries. */
|
|
16
|
+
const PAUSE = new Int32Array(new SharedArrayBuffer(4));
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* `_harness_setup._ask` — `input(f"{prompt}{suffix}: ").strip()`, or the default at EOF.
|
|
17
20
|
*
|
|
@@ -71,14 +74,28 @@ export function promptLine(prompt) {
|
|
|
71
74
|
* the wizard's first non-ASCII answer is what exposed it, and the corpus caught it on the
|
|
72
75
|
* first run. Scanning for the terminators a byte at a time is still correct: `\n` and `\r`
|
|
73
76
|
* are ASCII, and no continuation byte of a UTF-8 sequence is below 0x80.
|
|
77
|
+
*
|
|
78
|
+
* EAGAIN IS "NOTHING TYPED YET", NOT EOF. Touching `process.stdin` — even `.isTTY`, which
|
|
79
|
+
* `cmdSetup` reads first — makes libuv put a terminal fd 0 in non-blocking mode, and from then
|
|
80
|
+
* on `readSync` throws EAGAIN until a key is pressed. Caught as EOF, every prompt took its
|
|
81
|
+
* default the instant it printed: setup's "Continue without one? (y/N)" answered itself N and
|
|
82
|
+
* the install stopped before anyone could type (reported 2026-10, reproduced under a pty). So
|
|
83
|
+
* it waits 20 ms and reads again; any other error is still the end of input. `read` is the
|
|
84
|
+
* test seam.
|
|
74
85
|
*/
|
|
75
|
-
function readLine() {
|
|
86
|
+
export function readLine(read = readSync) {
|
|
76
87
|
const buf = Buffer.alloc(1);
|
|
77
88
|
const bytes = [];
|
|
78
89
|
let eof = false;
|
|
79
90
|
for (;;) {
|
|
80
91
|
let n = 0;
|
|
81
|
-
try {
|
|
92
|
+
try {
|
|
93
|
+
n = read(0, buf, 0, 1, null);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (e && e.code === 'EAGAIN') { Atomics.wait(PAUSE, 0, 0, 20); continue; }
|
|
96
|
+
eof = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
82
99
|
if (n === 0) { eof = true; break; }
|
|
83
100
|
if (buf[0] === 0x0a) break;
|
|
84
101
|
if (buf[0] !== 0x0d) bytes.push(buf[0]);
|