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.
Files changed (201) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +16 -2
  3. package/SHIPPED.md +1 -1
  4. package/docs/README.md +1 -1
  5. package/docs/concepts/collaboration.md +1 -1
  6. package/docs/concepts/enforcement-table.md +1 -1
  7. package/docs/concepts/foreman-mode.md +1 -1
  8. package/docs/concepts/kits.md +91 -0
  9. package/docs/concepts/loop-branch.md +1 -1
  10. package/docs/concepts/loop-bricks.md +1 -1
  11. package/docs/concepts/loop-contracts.md +1 -1
  12. package/docs/concepts/loop-gates.md +1 -1
  13. package/docs/concepts/loop-notes.md +1 -1
  14. package/docs/concepts/loop-risk.md +1 -1
  15. package/docs/concepts/loop-templates.md +1 -1
  16. package/docs/concepts/loops.md +1 -1
  17. package/docs/concepts/lsp.md +1 -1
  18. package/docs/concepts/skills.md +1 -1
  19. package/docs/extending.md +107 -4
  20. package/docs/guides/install-scripted.md +1 -1
  21. package/docs/guides/run-anywhere.md +2 -2
  22. package/docs/guides/upgrade.md +2 -2
  23. package/docs/reference/glossary.md +1 -0
  24. package/docs/reference/troubleshoot-install.md +1 -1
  25. package/docs/web-ui.md +9 -0
  26. package/harness.config.json +1 -1
  27. package/js/build/args.mjs +27 -6
  28. package/js/build/driver.mjs +29 -8
  29. package/js/build/emit-claude.mjs +3 -1
  30. package/js/build/generate.mjs +10 -5
  31. package/js/build/render.mjs +94 -11
  32. package/js/build/source.mjs +117 -4
  33. package/js/cli-table.json +15 -0
  34. package/js/hosts/installs.mjs +35 -2
  35. package/js/hosts/link.mjs +88 -1
  36. package/js/hosts/native.mjs +14 -4
  37. package/js/inspect/checks-authoring.mjs +6 -3
  38. package/js/inspect/checks-build.mjs +15 -3
  39. package/js/inspect/checks-counts.mjs +153 -6
  40. package/js/inspect/checks-repo.mjs +95 -20
  41. package/js/inspect/cost.mjs +1 -1
  42. package/js/inspect/diff.mjs +56 -8
  43. package/js/inspect/inventory.mjs +2 -0
  44. package/js/inspect/status.mjs +44 -3
  45. package/js/inspect/validate.mjs +3 -3
  46. package/js/lib/prompt.mjs +19 -2
  47. package/js/maintain/setup.mjs +111 -23
  48. package/js/maintain/update.mjs +3 -3
  49. package/js/web/actions.mjs +38 -6
  50. package/js/web/api.mjs +76 -17
  51. package/js/web/catalog.mjs +17 -2
  52. package/js/web/handler.mjs +1 -0
  53. package/js/web/routes.mjs +2 -1
  54. package/js/web/server.mjs +11 -5
  55. package/js/web/state.mjs +38 -3
  56. package/package.json +1 -1
  57. package/registry.json +91 -0
  58. package/src/AGENT.md.tmpl +7 -7
  59. package/src/agents/_self-improvement.md +11 -0
  60. package/src/agents/_template.md +1 -8
  61. package/src/agents/advocate.md +1 -8
  62. package/src/agents/architect.md +1 -8
  63. package/src/agents/developer.md +1 -8
  64. package/src/agents/devops-engineer.md +37 -0
  65. package/src/agents/docker-expert.md +37 -0
  66. package/src/agents/docs.md +1 -8
  67. package/src/agents/empiricist.md +1 -8
  68. package/src/agents/explorer.md +1 -8
  69. package/src/agents/framer.md +1 -8
  70. package/src/agents/historian.md +1 -8
  71. package/src/agents/kubernetes-specialist.md +37 -0
  72. package/src/agents/memory-curator.md +1 -8
  73. package/src/agents/operator.md +1 -8
  74. package/src/agents/performance.md +1 -8
  75. package/src/agents/pragmatist.md +1 -8
  76. package/src/agents/react-engineer.md +38 -0
  77. package/src/agents/react-reviewer.md +42 -0
  78. package/src/agents/researcher.md +5 -10
  79. package/src/agents/reviewer.md +3 -8
  80. package/src/agents/security.md +1 -8
  81. package/src/agents/skeptic.md +1 -8
  82. package/src/agents/spring-boot-engineer.md +40 -0
  83. package/src/agents/spring-reviewer.md +41 -0
  84. package/src/agents/spring-security.md +42 -0
  85. package/src/agents/steward.md +1 -8
  86. package/src/agents/tester.md +1 -8
  87. package/src/agents/user-advocate.md +1 -8
  88. package/src/agents/visionary.md +1 -8
  89. package/src/kits/react/rules.md +13 -0
  90. package/src/kits/react.json +18 -0
  91. package/src/kits/spring-boot/rules.md +10 -0
  92. package/src/kits/spring-boot.json +23 -0
  93. package/src/licenses/everything-claude-code.txt +21 -0
  94. package/src/licenses/mattpocock-skills.txt +21 -0
  95. package/src/licenses/piomin-spring-boot.txt +190 -0
  96. package/src/licenses/superpowers.txt +21 -0
  97. package/src/licenses/vibe-wise.txt +21 -0
  98. package/src/skills/_self-improvement.md +22 -5
  99. package/src/skills/brainstorm/builder-check.md +2 -0
  100. package/src/skills/brainstorm.md +11 -2
  101. package/src/skills/codebase-design.md +10 -2
  102. package/src/skills/daydream/VENDOR.md +7 -7
  103. package/src/skills/debug.md +6 -2
  104. package/src/skills/develop.md +6 -2
  105. package/src/skills/geneseed-code-review.md +6 -2
  106. package/src/skills/git-history.md +2 -0
  107. package/src/skills/handoff.md +3 -1
  108. package/src/skills/learn-mode.md +3 -2
  109. package/src/skills/parallel-agents.md +5 -1
  110. package/src/skills/pipeline.md +2 -0
  111. package/src/skills/plan.md +9 -2
  112. package/src/skills/prototype.md +6 -2
  113. package/src/skills/react/a11y.md +33 -0
  114. package/src/skills/react/vite.md +35 -0
  115. package/src/skills/react/webpack.md +40 -0
  116. package/src/skills/react-testing.md +68 -0
  117. package/src/skills/react-view-transitions/AGENTS.md +646 -221
  118. package/src/skills/react-view-transitions/SKILL.md +28 -16
  119. package/src/skills/react-view-transitions/VENDOR.md +10 -8
  120. package/src/skills/react-view-transitions/references/css-recipes.md +72 -2
  121. package/src/skills/react-view-transitions/references/implementation.md +14 -26
  122. package/src/skills/react-view-transitions/references/nextjs.md +94 -37
  123. package/src/skills/react-view-transitions/references/patterns.md +60 -48
  124. package/src/skills/react-view-transitions/references/troubleshooting.md +47 -0
  125. package/src/skills/react.md +63 -0
  126. package/src/skills/research.md +6 -2
  127. package/src/skills/review-response.md +2 -0
  128. package/src/skills/rule.md +6 -2
  129. package/src/skills/ship.md +8 -4
  130. package/src/skills/skill-forge.md +11 -5
  131. package/src/skills/spring-boot/cloud.md +103 -0
  132. package/src/skills/spring-boot/java-modern.md +71 -0
  133. package/src/skills/spring-boot/logging.md +89 -0
  134. package/src/skills/spring-boot/security.md +112 -0
  135. package/src/skills/spring-boot/web.md +112 -0
  136. package/src/skills/spring-boot.md +57 -0
  137. package/src/skills/spring-data-jpa/patterns.md +68 -0
  138. package/src/skills/spring-data-jpa.md +81 -0
  139. package/src/skills/spring-testing.md +93 -0
  140. package/src/skills/wayfinder.md +8 -2
  141. package/src/skills/wiki.md +18 -5
  142. package/src/upstreams.json +66 -0
  143. package/themes/_TEMPLATE.json +13 -0
  144. package/themes/biker.json +13 -0
  145. package/themes/commentator.json +13 -0
  146. package/themes/cyberpunk.json +13 -0
  147. package/themes/gamer.json +13 -0
  148. package/themes/imperial.json +13 -0
  149. package/themes/joker.json +13 -0
  150. package/themes/marvin.json +13 -0
  151. package/themes/mean.json +13 -0
  152. package/themes/military.json +13 -0
  153. package/themes/neutral.json +13 -0
  154. package/themes/pirate.json +13 -0
  155. package/themes/sports.json +13 -0
  156. package/themes/verstappen.json +13 -0
  157. package/themes/wizard.json +13 -0
  158. package/web/dist/assets/Activity-44rbWUQI.js +1 -0
  159. package/web/dist/assets/ActivityDetail-Dk9k893a.js +1 -0
  160. package/web/dist/assets/Dashboard-DCoUN60W.js +2 -0
  161. package/web/dist/assets/{Docs-tDW7i7mo.js → Docs-C_je2lRG.js} +3 -3
  162. package/web/dist/assets/FilterInput-CZREvrjX.js +1 -0
  163. package/web/dist/assets/GreenhouseView-BXAOIznW.js +2 -0
  164. package/web/dist/assets/Installs-DAXF403D.js +3 -0
  165. package/web/dist/assets/JournalView-DZ184wKB.js +1 -0
  166. package/web/dist/assets/KitApply-pjhvGha0.js +1 -0
  167. package/web/dist/assets/Kits-BCCKjSIY.js +1 -0
  168. package/web/dist/assets/{Laws-BA3CzUf5.js → Laws-BgBUpRkE.js} +1 -1
  169. package/web/dist/assets/Library-CW0Zl6RF.js +1 -0
  170. package/web/dist/assets/Loops-BvFOzjEw.js +2 -0
  171. package/web/dist/assets/{Markdown-iHvBrpRN.js → Markdown-zaXXXHz9.js} +13 -13
  172. package/web/dist/assets/OperatorHudView-Bs4unPNi.js +2 -0
  173. package/web/dist/assets/Personal-C23XaT-a.js +1 -0
  174. package/web/dist/assets/Tabs-b2XFu4OS.js +1 -0
  175. package/web/dist/assets/activity-Jw8XcPiP.js +1 -0
  176. package/web/dist/assets/api-BfpZmUMI.js +1 -0
  177. package/web/dist/assets/{drawable-CyLe1gtW.js → drawable-BERkKhx_.js} +1 -1
  178. package/web/dist/assets/index-C9R_bRw_.js +2 -0
  179. package/web/dist/assets/{index-BCJz4_8C.css → index-jYlWihN-.css} +1 -1
  180. package/web/dist/assets/{react-BOF6yL40.js → react-DlYQp10C.js} +1 -1
  181. package/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  182. package/web/dist/assets/sections-ChwaobfW.js +1 -0
  183. package/web/dist/assets/usePoll-bDFN-Hr6.js +1 -0
  184. package/web/dist/index.html +6 -5
  185. package/web/dist/assets/Activity-C8sZVIUU.js +0 -1
  186. package/web/dist/assets/ActivityDetail-Cc6vWUS4.js +0 -1
  187. package/web/dist/assets/Dashboard-C5ATa3ow.js +0 -2
  188. package/web/dist/assets/FilterInput-zLFXoSt6.js +0 -1
  189. package/web/dist/assets/GreenhouseView-zk2Uzc3A.js +0 -2
  190. package/web/dist/assets/Installs-CB_rvbBP.js +0 -3
  191. package/web/dist/assets/JournalView-Bfj27XHC.js +0 -1
  192. package/web/dist/assets/Library-BWwiy9Vb.js +0 -1
  193. package/web/dist/assets/Loops-DaPk-ckL.js +0 -2
  194. package/web/dist/assets/OperatorHudView-CyiGhiHk.js +0 -2
  195. package/web/dist/assets/Personal-Bwd27CmT.js +0 -1
  196. package/web/dist/assets/Tabs-BOD7Kaz1.js +0 -1
  197. package/web/dist/assets/activity-DvPxoS4Y.js +0 -1
  198. package/web/dist/assets/api-CiXlkbe0.js +0 -1
  199. package/web/dist/assets/index-DQXG_mqo.js +0 -2
  200. package/web/dist/assets/sections-BET3c1WT.js +0 -1
  201. 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, knownSkillIds,
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, under which license, and at WHICH immutable commit.
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 …`) and are exempt from the pin: there is no
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 (!upstream) {
254
- problems.push(`[authoring] skills/${name}/VENDOR.md declares no '**Upstream:**'`);
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
- if (upstream[1].toLowerCase().startsWith('this')) continue; // first-party
258
- const pin = /\*\*Commit:\*\*\s*(\S+)/.exec(text);
259
- if (!pin) {
260
- problems.push(`[authoring] skills/${name}/VENDOR.md has no '**Commit:**' pin`);
261
- } else if (MOVING_REFS.has(pin[1].toLowerCase())) {
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;
@@ -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: 62_219, lean: 39_111 };
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
@@ -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 the',
339
- 'self-improvement loops editing agent/skill files in place. Hand this file to',
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(target, theme, files, outPath = null) {
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
  }
@@ -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',
@@ -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)));
@@ -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] [--no-secrets-gate] [--no-done-gate]',
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 { n = readSync(0, buf, 0, 1, null); } catch { eof = true; break; }
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]);