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
@@ -41,12 +41,13 @@
41
41
  * a cell could not have varied the answers at all.
42
42
  */
43
43
  import { spawnSync } from 'node:child_process';
44
- import { existsSync } from 'node:fs';
44
+ import { existsSync, readdirSync } from 'node:fs';
45
+ import os from 'node:os';
45
46
  import path from 'node:path';
46
47
 
47
48
  import { main as driverMain, resolveOut } from '../build/driver.mjs';
48
49
  import { playLine } from '../ui/anim.mjs';
49
- import { discoverNames, GATE_SWITCHES, PACK_ORDER } from '../build/source.mjs';
50
+ import { discoverNames, GATE_SWITCHES, PACK_ORDER, loadKits } from '../build/source.mjs';
50
51
  import { exportImprovements } from '../inspect/diff.mjs';
51
52
  import { cmdDoctor } from '../inspect/doctor.mjs';
52
53
  import { setupBuildArgs } from '../build/generate.mjs';
@@ -227,6 +228,25 @@ function askDoctrines(deployed = null) {
227
228
  known === null || known.includes(name) ? 'yes' : 'no') === 'yes');
228
229
  }
229
230
 
231
+ /**
232
+ * The Kits questions (spec A5): one yes/no per shipped kit, each defaulting to whether the install
233
+ * has it on (`deployed`, its `Active kits:` marker), so holding Enter keeps the installed set. A
234
+ * single pick could not hold two kits at once, which is why the menu became per kit when the second
235
+ * kit shipped. Asked ONLY when this checkout ships a kit, so a kit-less wizard reads exactly the
236
+ * stdin it always did (and has no kit to carry: `kitsOfDir` drops an unshipped one).
237
+ * Returns the kit list, or null for none (`setupBuildArgs` omits `--kits`).
238
+ */
239
+ export function askKits(deployed = null, kits = null) {
240
+ try { kits ??= loadKits(); } catch (e) {
241
+ process.stderr.write(`warning: kit question skipped — ${e.message}\n`);
242
+ return null;
243
+ }
244
+ const on = (deployed ?? []).filter((k) => kits.has(k));
245
+ const picked = [...kits].filter(([k, m]) => askChoice(`Kit ${k} — ${m.description ?? k}`,
246
+ [['yes', `install the ${k} kit`], ['no', `leave ${k} out`]], on.includes(k) ? 'yes' : 'no') === 'yes');
247
+ return picked.length ? picked.map(([k]) => k) : null;
248
+ }
249
+
230
250
  /**
231
251
  * `_harness_setup._collect_setup_lines` — the six questions, the plan, the confirm.
232
252
  *
@@ -260,13 +280,15 @@ function targetGates(emit, out) {
260
280
  }
261
281
  }
262
282
 
263
- export function collectSetupLines() {
283
+ export function collectSetupLines(kitMap = null) {
264
284
  printOut('Geneseed setup — answer a few questions; nothing is written until you confirm.\n');
265
285
  const inst = installedDefaults();
266
286
  const theme = askChoice('Theme', themeOptions(), inst.theme || defaultTheme());
267
287
  const posture = askChoice('Posture', postureOptions(), inst.posture || defaultPosture());
268
288
  const mode = askChoice('Mode', modeOptions(), inst.mode || defaultMode());
269
289
  const doctrines = askDoctrines(inst.doctrines);
290
+ // Kits on stay on (spec A3); `kitsOfDir` already dropped a retired one.
291
+ const kits = askKits(inst.kits, kitMap);
270
292
  const emit = askChoice('Install mode', EMIT_OPTIONS, inst.emit || 'opencode-global');
271
293
  const footprint = askChoice('Footprint', FOOTPRINT_OPTIONS,
272
294
  inst.footprint || 'lean');
@@ -304,13 +326,13 @@ export function collectSetupLines() {
304
326
  const gated = Object.values(gates).some((on) => on === false);
305
327
  printOut(`\nAbout to run: geneseed build ${
306
328
  setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks,
307
- excludeRules, undefined, excludeSkills, gates).join(' ')}\n`);
329
+ excludeRules, undefined, excludeSkills, gates, kits).join(' ')}\n`);
308
330
  if (!confirm('Proceed?', true)) return null;
309
331
  // The key only when there is something to carry, so a selection that excludes no skill and
310
332
  // switches off no gate is the same object it was before either existed.
311
333
  return {
312
334
  theme, posture, mode, doctrines, excludeRules, ...(excludeSkills ? { excludeSkills } : {}),
313
- ...(gated ? { gates } : {}),
335
+ ...(gated ? { gates } : {}), ...(kits ? { kits } : {}),
314
336
  emit, out, root, footprint,
315
337
  };
316
338
  }
@@ -494,11 +516,11 @@ export function setupLines() {
494
516
  }
495
517
  const {
496
518
  theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
497
- doctrines = null, excludeRules = null, excludeSkills = null, gates = {},
519
+ doctrines = null, excludeRules = null, excludeSkills = null, gates = {}, kits = null,
498
520
  } = sel;
499
521
  if (emit === 'opencode-global') {
500
- // The build below overwrites the deployed global harness, and the self-improvement loops
501
- // may have edited it in place. Preserve that drift first.
522
+ // The build below overwrites the deployed global harness, which may carry hand edits (or
523
+ // in-place edits from a harness older than the notebook notes). Preserve that drift first.
502
524
  try {
503
525
  const [ipath] = exportImprovements();
504
526
  if (ipath) {
@@ -513,7 +535,7 @@ export function setupLines() {
513
535
  // runs must carry the pack selection the plan line just previewed, or the wizard would print
514
536
  // one command and run another.
515
537
  const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
516
- doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates);
538
+ doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates, kits);
517
539
  printOut(`Running: geneseed build ${argv.join(' ')}\n`);
518
540
  const rc = driverMain(argv);
519
541
  if (rc !== 0) {
@@ -551,26 +573,83 @@ export function cmdSetup() {
551
573
  }
552
574
 
553
575
  /**
554
- * The AI coding tools Geneseed installs into, as `[label, present, url]` rows in HOSTS order.
576
+ * The AI coding tools Geneseed installs into, as `[label, present, url, where]` rows in HOSTS
577
+ * order. `where` is the path found OFF the PATH (see `userInstall`), else null.
555
578
  *
556
- * PRESENT MEANS THE COMMAND IS ON PATH, and nothing else. The config dir looked like a second
557
- * signal — a desktop-only install has one and no command — but GENESEED ITSELF CREATES IT: a
558
- * `bob-global` emit writes `~/.bob` on a machine that never had Bob, and the first run of this
559
- * check reported Bob present on exactly such a machine. A wrong "ok" sends the user to an
560
- * install mode for a tool they cannot open; a wrong "not found" costs one line they can ignore.
579
+ * PRESENT MEANS THE TOOL ITSELF WAS FOUND — its command on PATH, or its program in a per-user
580
+ * install location — and never its config dir. That dir looked like a signal — a desktop-only
581
+ * install has one and no command — but GENESEED ITSELF CREATES IT: a `bob-global` emit writes
582
+ * `~/.bob` on a machine that never had Bob, and the first run of this check reported Bob present
583
+ * on exactly such a machine. A wrong "ok" sends the user to an install mode for a tool they
584
+ * cannot open; a wrong "not found" costs a line, and `toolsReport` lets them name the path.
561
585
  *
562
586
  * NO SPAWN, deliberately. `which` walks PATH by hand, so asking costs nothing on the CLI's
563
587
  * child_process allow-list — a `claude --version` probe would have been a third entry there to
564
- * learn nothing `which` does not already say. `lookup` is the seam for the test, which cannot
565
- * rely on what this machine happens to have installed.
588
+ * learn nothing `which` does not already say. `lookup` and `userLookup` are the seams for the
589
+ * test, which cannot rely on what this machine happens to have installed.
566
590
  */
567
- export function hostTools({ lookup = which } = {}) {
591
+ export function hostTools({ lookup = which, userLookup = userInstall } = {}) {
568
592
  return [
569
593
  ['OpenCode', 'opencode', 'https://opencode.ai/docs/'],
570
594
  ['Claude Code', 'claude', 'https://code.claude.com/docs/en/setup'],
571
595
  ['IBM Bob', 'bob', 'https://bob.ibm.com/'],
572
596
  ['OpenClaude', 'openclaude', 'https://openclaude.gitlawb.com/'],
573
- ].map(([label, cmd, url]) => [label, Boolean(lookup(cmd)), url]);
597
+ ].map(([label, cmd, url]) => {
598
+ if (lookup(cmd)) return [label, true, url, null];
599
+ const where = userLookup(cmd);
600
+ return [label, Boolean(where), url, where || null];
601
+ });
602
+ }
603
+
604
+ /**
605
+ * Per-host off-PATH places: a bin dir a tool's own installer uses, and a name pattern for the
606
+ * desktop app folder. Only Bob ships as a desktop IDE; matching `claude` against app folders
607
+ * would find the Claude desktop chat app, which is not Claude Code.
608
+ */
609
+ const USER_BIN_DIRS = {
610
+ opencode: ['.opencode/bin'],
611
+ claude: ['.claude/local'],
612
+ bob: [],
613
+ openclaude: [],
614
+ };
615
+ const APP_PATTERN = { bob: /\bbob\b/i };
616
+
617
+ /**
618
+ * Where a tool installed FOR ONE USER lives when its installer did not put it on PATH — the
619
+ * report of 2026-10 was IBM Bob installed under the user's own profile, which `which` never sees.
620
+ *
621
+ * Three kinds of place, each one the tool's own program, never its config dir:
622
+ * * a per-user bin dir: `~/.local/bin` and `~/bin` for every tool, plus the dir its own
623
+ * installer uses (`USER_BIN_DIRS`) — an executable file, through `which` and its PATHEXT;
624
+ * * on Windows, `%APPDATA%\npm` (a per-user `npm i -g`) and a desktop app folder under
625
+ * `%LOCALAPPDATA%\Programs` whose name matches `APP_PATTERN`;
626
+ * * on macOS, a matching `.app` in `/Applications` or `~/Applications`.
627
+ * The first hit wins. Everything a machine could differ on is a parameter, for the test.
628
+ */
629
+ export function userInstall(cmd, {
630
+ home = os.homedir(), env = process.env, platform = process.platform,
631
+ find = which, list = listDir,
632
+ } = {}) {
633
+ const bins = ['.local/bin', 'bin', ...(USER_BIN_DIRS[cmd] ?? [])].map((d) => path.join(home, d));
634
+ if (platform === 'win32' && env.APPDATA) bins.push(path.join(env.APPDATA, 'npm'));
635
+ const hit = find(cmd, bins.join(path.delimiter));
636
+ if (hit) return hit;
637
+ const pattern = APP_PATTERN[cmd];
638
+ if (!pattern) return null;
639
+ const appDirs = platform === 'win32'
640
+ ? (env.LOCALAPPDATA ? [path.join(env.LOCALAPPDATA, 'Programs')] : [])
641
+ : platform === 'darwin' ? ['/Applications', path.join(home, 'Applications')] : [];
642
+ for (const dir of appDirs) {
643
+ const name = list(dir).find((n) => pattern.test(n)
644
+ && (platform !== 'darwin' || n.endsWith('.app')));
645
+ if (name) return path.join(dir, name);
646
+ }
647
+ return null;
648
+ }
649
+
650
+ /** `readdirSync`, with a missing or unreadable dir as no entries. */
651
+ function listDir(dir) {
652
+ try { return readdirSync(dir); } catch { return []; }
574
653
  }
575
654
 
576
655
  /**
@@ -578,16 +657,25 @@ export function hostTools({ lookup = which } = {}) {
578
657
  * answer knowing it — and when NONE is, ask whether to stop and install one first. It never
579
658
  * installs one: on a managed machine that is IT's call, and each tool has its own installer.
580
659
  * A missing tool among several present is a line, not a question; nobody needs all four.
660
+ * When none is found, the user can name where theirs is before being asked to stop — the
661
+ * search covers PATH and the usual per-user places, not every folder an installer may pick.
581
662
  * `false` is the user choosing to stop.
582
663
  */
583
664
  export function toolsReport(tools = hostTools()) {
584
665
  printOut('AI coding tools on this machine:\n');
585
- for (const [label, present, url] of tools) {
586
- printOut(present ? ` ok ${label}\n` : ` -- ${label} not on PATH — install: ${url}\n`);
666
+ for (const [label, present, url, where] of tools) {
667
+ if (!present) printOut(` -- ${label} not found — install: ${url}\n`);
668
+ else printOut(where ? ` ok ${label} (${where}, not on PATH)\n` : ` ok ${label}\n`);
587
669
  }
588
670
  if (tools.some(([, present]) => present)) { printOut('\n'); return true; }
589
- printOut('\nNone of them is installed. Install the one you use (links above), then run setup '
590
- + 'again — or go on and pick the plain bundle, which any AGENT.md tool can read.\n');
671
+ printOut('\nNone of them was found on PATH or in the usual per-user folders.\n');
672
+ // Quotes stripped: a path dragged into a Windows console arrives wrapped in them.
673
+ const given = ask('Installed somewhere else? Path to the tool (Enter to skip)')
674
+ .replace(/^"(.*)"$/, '$1');
675
+ if (given && existsSync(given)) { printOut(` ok ${given}\n\n`); return true; }
676
+ if (given) printOut(` -- nothing at ${given}\n`);
677
+ printOut('Install the one you use (links above), then run setup again — or go on and pick the '
678
+ + 'plain bundle, which any AGENT.md tool can read.\n');
591
679
  if (confirm('Continue without one?', false)) { printOut('\n'); return true; }
592
680
  printOut('Stopped. Run setup again once a tool is installed.\n');
593
681
  return false;
@@ -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
  /**
@@ -796,8 +796,8 @@ export async function upgrade(ref = null, themeArg = null) {
796
796
  * `_harness_lifecycle.cmd_upgrade` — the harness verb, which is `export_improvements()` and
797
797
  * then `upgrade()`.
798
798
  *
799
- * THE EXPORT RUNS FIRST AND MUST NEVER BLOCK. The self-improvement loops edit the deployed
800
- * harness in place and the rebuild is about to overwrite those edits, so the drift has to be
799
+ * THE EXPORT RUNS FIRST AND MUST NEVER BLOCK. A deployed harness can carry in-place edits (by
800
+ * hand, or by a harness older than the notebook notes) and the rebuild is about to overwrite them, so the drift has to be
801
801
  * captured against the PRE-refresh source. Anything it throws is a warning on stderr and the
802
802
  * upgrade continues — a failed export is a lost note, a blocked upgrade is a broken install.
803
803
  */
@@ -34,7 +34,7 @@ import path from 'node:path';
34
34
 
35
35
  import { emitGlobalInto } from '../build/driver.mjs';
36
36
  import {
37
- ROOT, discoverNames, knownRuleIds, PACK_ORDER,
37
+ ROOT, discoverNames, knownRuleIds, loadKits, PACK_ORDER,
38
38
  } from '../build/source.mjs';
39
39
  import { withStdoutSwallowed } from '../inspect/diff.mjs';
40
40
  import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
@@ -46,9 +46,10 @@ import {
46
46
  opencodeConfigDir, resolvePath,
47
47
  } from '../hosts/hosts.mjs';
48
48
  import {
49
- EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir,
49
+ EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir, kitsOfDir,
50
50
  footprintOfDir, gatesOfDir,
51
- installState, installTargets, legacyProcessCarrierOfDir, modeOfDir, postureOfDir, trustOfDir,
51
+ installState, installTargets, legacyProcessCarrierOfDir, modeOfDir, postureOfDir, themeOfDir,
52
+ trustOfDir,
52
53
  } from '../hosts/installs.mjs';
53
54
  import {
54
55
  isDict, mcpCommented, mcpInstallTargets, mcpLoad, mcpServerNames, mcpState,
@@ -131,6 +132,15 @@ function bodyExcludeRules(body) {
131
132
  (n) => n.trim().replace(/[ \t]+/, '.'));
132
133
  }
133
134
 
135
+ /**
136
+ * A request body's `kits` — REPLACES the install's kits (the Kits tab sends the whole set it
137
+ * wants): `[]`/`'none'` give `[]`, an omitted key gives `null` (keep the install's own), and an
138
+ * unknown name gives `null` too, which `apiInstallCmd` refuses for a body that HAS the key.
139
+ */
140
+ function bodyKits(body) {
141
+ return bodyList(body, 'kits', () => [...loadKits().keys()]);
142
+ }
143
+
134
144
  /**
135
145
  * The gate switches across the trust boundary (`deletionGate`, `secretsGate`, `doneGate`), each
136
146
  * on its own: only a real JSON boolean counts. Anything else — a string `"false"`, a number,
@@ -465,6 +475,14 @@ export function buildActionCmds(state, body) {
465
475
  const [, , ...rest] = installProfile(host, scope, state.root).argv;
466
476
  return actionCommands('build', { buildArgv: ['--theme', theme, ...rest] });
467
477
  }
478
+ // Only a GLOBAL emit has a home of its own. Any other carries no folder on this route, so the
479
+ // generator fell back to its default `./Harness` — and jobs run with the Geneseed checkout as
480
+ // cwd, so a Bob "project" install landed inside the clone (reported 2026-10), where the doctor's
481
+ // bundle check then read it as 178 missing files. The `deploy` action takes a chosen folder.
482
+ if (!emit.endsWith('-global')) {
483
+ return { error: `${emit} installs into a folder you choose — use Deploy with a folder, `
484
+ + 'not Build' };
485
+ }
468
486
  return actionCommands('build', {
469
487
  theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
470
488
  trust: state.trust,
@@ -539,6 +557,8 @@ export function apiRestore(state, files) {
539
557
  // re-states a rule whose hook gate the install deliberately left unwired.
540
558
  excludeRules: excludedRulesOfDir(target),
541
559
  excludeSkills: excludedSkillsOfDir(target),
560
+ // The kits: a restored AGENT.md keeps its Kits block and `Active kits:` line.
561
+ kits: kitsOfDir(target),
542
562
  // The gate switches: a restored AGENT.md must keep the install's `<gate>: off` lines.
543
563
  ...gatesOfDir(target),
544
564
  }));
@@ -607,8 +627,9 @@ export function apiInstallCmd(state, body) {
607
627
  if (emit === undefined) return { error: `no install mode for ${host}:${scope}` };
608
628
  // Every read-back below is narrowed to `host`'s OWN carrier (host-compat B1) — see the
609
629
  // comment at `installProfile` in `js/build/generate.mjs`.
630
+ // The theme default is THIS install's own, not the viewed one's: the Kits tab acts on any row.
610
631
  const { theme, footprint: fp, posture: pos, mode, trust } = bodyAxes(body, {
611
- theme: state.theme, footprint: footprintOfDir(root),
632
+ theme: themeOfDir(root, host) || state.theme, footprint: footprintOfDir(root),
612
633
  posture: postureOfDir(root, host) || 'peer',
613
634
  mode: modeOfDir(root, host) || 'direct', trust: trustOfDir(root, host) || DEFAULT_PRESET });
614
635
  // Unspecified means "keep what this install already has", exactly as theme, footprint,
@@ -626,9 +647,20 @@ export function apiInstallCmd(state, body) {
626
647
  // The gate switches (the Constitution page's toggles): a strict boolean per gate in the body,
627
648
  // else the install's own answer — so a pack Apply keeps every switch as it is.
628
649
  const gates = bodyGates(body, root, host);
650
+ // The page sends `kits` deliberately, so an unknown name is refused: ignoring it would run a
651
+ // no-op rebuild and report success. An omitted key keeps the install's own kits.
652
+ let kits = null;
653
+ try {
654
+ kits = bodyKits(body);
655
+ if (kits === null && bget(body, 'kits') !== null) {
656
+ return { error: `unknown kit in ${formatRepr(bget(body, 'kits'))} — this Geneseed ships: `
657
+ + [...loadKits().keys()].join(', ') };
658
+ }
659
+ } catch (e) { return { error: String(e?.message ?? e) }; }
629
660
  const out = scope === 'global' ? null : String(root);
630
661
  const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
631
- PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates);
662
+ PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates,
663
+ kits ?? kitsOfDir(root, host));
632
664
  return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
633
665
  }
634
666
 
@@ -730,6 +762,6 @@ export function apiDeployCmd(state, body) {
730
762
  const gates = bodyGates(body, root, host);
731
763
  // project-scope emit name == host name (opencode / claude / bob / openclaude)
732
764
  const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
733
- PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates);
765
+ PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates, kitsOfDir(root, host));
734
766
  return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
735
767
  }
package/js/web/api.mjs CHANGED
@@ -8,31 +8,33 @@
8
8
  * bind paths to these functions are `routes.mjs`. This file imports none of its route siblings,
9
9
  * which is what keeps the old `api ↔ actions` and `api ↔ activity` cycles from coming back.
10
10
  */
11
- import { statSync } from 'node:fs';
11
+ import { readdirSync, statSync } from 'node:fs';
12
12
  import path from 'node:path';
13
13
 
14
- import { CONFIG, ROOT, THEMES, discoverNames, makeCfg } from '../build/source.mjs';
14
+ import { CONFIG, CREDIT_RE, ROOT, SRC, THEMES, discoverNames, loadKits, makeCfg, upstreamOf } from '../build/source.mjs';
15
15
  import { readVersion, sourceFingerprint } from '../build/version.mjs';
16
16
  import { sourceReleaseVersion } from '../hosts/opencode.mjs';
17
17
  import { alwaysOnCost } from '../inspect/cost.mjs';
18
+ import { installProfile } from '../build/generate.mjs';
18
19
  import { diffCollect, renderMemo } from '../inspect/diff.mjs';
19
20
  import { excludesSnapshot } from '../inspect/excludes.mjs';
20
- import { CLAUDE_STYLE, HOSTS, VERSION_MARKER, resolvePath } from '../hosts/hosts.mjs';
21
+ import { VERSION_MARKER, resolvePath } from '../hosts/hosts.mjs';
21
22
  import {
22
- footprintOfDir, gatesOfDir, installState, installTargets, modeOfDir, trustOfDir,
23
- postureOfDir, readJsonMaybe, themeOfDir,
23
+ footprintOfDir, gatesOfDir, installState, installTargets, legacyProcessCarrierOfDir, modeOfDir,
24
+ trustOfDir,
25
+ postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
24
26
  } from '../hosts/installs.mjs';
25
27
  import { PRESETS } from '../loop/score.mjs';
26
28
  import { loadCatalog, catalogProblems } from '../loop/catalog.mjs';
27
29
  import { activeLoops } from '../loop/registry.mjs';
28
30
  import { EMIT_OPTIONS, themeOptions } from '../maintain/setup.mjs';
29
- import { accentFor, statusData } from '../inspect/status.mjs';
31
+ import { accentFor, keptKitFiles, statusData } from '../inspect/status.mjs';
30
32
  import { isFile } from '../lib/fs.mjs';
31
33
  import { comparePaths, normcase } from '../lib/paths.mjs';
32
34
  import {
33
35
  configItems, deployed, dget, memoryItems, notebookItems, wikiItems,
34
36
  } from './catalog.mjs';
35
- import { stampMinute } from './state.mjs';
37
+ import { stampMinute, viewCfg } from './state.mjs';
36
38
  import { updateStatus } from './update-check.mjs';
37
39
 
38
40
  /**
@@ -106,16 +108,9 @@ export function apiDiff(state) {
106
108
  /** The union across every global install, verbatim. */
107
109
  export const apiExcludes = () => excludesSnapshot();
108
110
 
109
- /**
110
- * The data dir an install's inventory/memory/diff is read from. Host-driven, so a new
111
- * nested-marker host cannot silently read the bare root.
112
- */
113
- export function viewCfg(host, scope, root) {
114
- if (scope === 'project' && CLAUDE_STYLE.includes(host)) {
115
- return path.join(root, HOSTS.find((h) => h.host === host).projectMarker);
116
- }
117
- return root;
118
- }
111
+ // `viewCfg` moved to `state.mjs`, which needs it to pick a default view and cannot import
112
+ // this module back (this one imports `stampMinute` from it).
113
+ export { viewCfg } from './state.mjs';
119
114
 
120
115
  const DAY_MS = 24 * 60 * 60 * 1000;
121
116
 
@@ -231,6 +226,70 @@ export function apiFleet(state) {
231
226
  return { installs, source_fp: sourceFp, doctor: state.doctor };
232
227
  }
233
228
 
229
+ /** Source files of a kit member: `<dir>/<name>.md`, or every `.md` in a `<dir>/<name>/` folder. */
230
+ function memberTexts(dir, name) {
231
+ const one = readMaybe(path.join(SRC, dir, `${name}.md`));
232
+ if (one !== null && one !== undefined) return [one];
233
+ const d = path.join(SRC, dir, name);
234
+ try { return readdirSync(d).filter((f) => f.endsWith('.md')).map((f) => readMaybe(path.join(d, f)) ?? ''); } catch { return []; }
235
+ }
236
+
237
+ /** Upstream credits cited by a kit's rules and member sources; an unknown key is skipped, never a 500. */
238
+ function kitCredits(m, rules) {
239
+ const texts = [rules, ...(m.skills ?? []).flatMap((n) => memberTexts('skills', n)),
240
+ ...(m.agents ?? []).flatMap((n) => memberTexts('agents', n))];
241
+ const keys = new Set();
242
+ for (const t of texts) for (const x of t.matchAll(CREDIT_RE)) keys.add(x[1]);
243
+ const out = [];
244
+ for (const key of [...keys].sort()) {
245
+ let u;
246
+ try { u = upstreamOf(key); } catch { u = null; }
247
+ if (!u) continue;
248
+ out.push({ key, repo: u.repo, author: u.author, license: u.license, commit: u.commit,
249
+ compareUrl: `https://github.com/${u.repo}/compare/${u.commit}...HEAD` });
250
+ }
251
+ return out;
252
+ }
253
+
254
+ /**
255
+ * EVERY INSTALL'S KITS — the Kits tab. Its own route, like `apiFleet`: it re-reads each carrier
256
+ * and the kit manifests, which the sidebar's `/api/installs` refetch must not pay for.
257
+ *
258
+ * `kits` per row is `installProfile(...).kits`, the reader `rebuild-all` uses, so the tab shows
259
+ * what a rebuild would keep. A malformed manifest answers 200 with `error` and EMPTY lists, never
260
+ * rows without kits (which would read as "no kit on") and never a 500. `editable:false` carries
261
+ * the two refusals `apiInstallCmd` makes, so the page can say why before the click.
262
+ */
263
+ export function apiKits() {
264
+ let known;
265
+ try { known = loadKits(); } catch (e) {
266
+ return { kits: [], installs: [], error: String(e?.message ?? e) };
267
+ }
268
+ const kits = [...known].map(([name, m]) => {
269
+ const rules = readMaybe(path.join(SRC, 'kits', name, 'rules.md')) ?? '';
270
+ return { name, description: m.description ?? '', skills: m.skills ?? [], agents: m.agents ?? [],
271
+ rules, rulesWordBudget: m.rulesWordBudget ?? null, banned: m.banned ?? [],
272
+ credits: kitCredits(m, rules) };
273
+ });
274
+ const installs = [];
275
+ for (const [host, scope, root] of installTargets()) {
276
+ const st = installState(root, host, scope);
277
+ if (st === 'absent') continue;
278
+ try {
279
+ const p = installProfile(host, scope, root);
280
+ const reason = st === 'disabled' ? 'install is disabled — reactivate it first'
281
+ : legacyProcessCarrierOfDir(root, host) ? 'predates the comms pack — run geneseed setup for it first'
282
+ : null;
283
+ installs.push({ ...installRowBase(host, scope, root, st), kits: p.kits,
284
+ keptKitFiles: keptKitFiles(host, scope, root, p.kits), editable: reason === null, reason });
285
+ } catch (e) {
286
+ installs.push({ ...installRowBase(host, scope, root, 'unreadable'),
287
+ error: String(e?.message ?? e) });
288
+ }
289
+ }
290
+ return { kits, installs };
291
+ }
292
+
234
293
  /** The dashboard aggregate. */
235
294
  export function apiOverview(state) {
236
295
  const inv = state.inventory;
@@ -24,6 +24,7 @@ import { readText, withDiscardableStderr, isDir, isFile } from '../lib/fs.mjs';
24
24
  import { comparePaths, normcase, within } from '../lib/paths.mjs';
25
25
  import { stripWhitespace } from '../lib/text.mjs';
26
26
  import { readJsonc } from '../hosts/settings.mjs';
27
+ import { loadKits } from '../build/source.mjs';
27
28
 
28
29
  /** A requested catalog section or item that does not exist. */
29
30
  export class NotFound extends Error {}
@@ -466,16 +467,29 @@ function constitutionItems(inv) {
466
467
  ];
467
468
  }
468
469
 
470
+ /** `Map<'skills/x' | 'agents/y', kit>` from the shipped manifests; a broken manifest means no badges, not a 500. */
471
+ function kitOwners() {
472
+ const owner = new Map();
473
+ try {
474
+ for (const [kit, m] of loadKits()) {
475
+ for (const n of m.skills ?? []) owner.set(`skill:${n}`, kit);
476
+ for (const n of m.agents ?? []) owner.set(`agent:${n}`, kit);
477
+ }
478
+ } catch { /* shortcut: unreadable manifests hide the badge; apiKits reports the error */ }
479
+ return owner;
480
+ }
481
+
469
482
  export function apiCatalog(state, section) {
470
483
  if (!SECTIONS.includes(section)) throw new NotFound(section);
471
484
  const inv = state.inventory;
472
485
  let items;
486
+ const owner = kitOwners();
473
487
  if (section === 'agents') {
474
488
  items = inv.agents.map((e) => ({ name: e.name, title: e.name, desc: e.desc,
475
- source: e.source ?? null, status: e.status ?? 'unknown' }));
489
+ source: e.source ?? null, status: e.status ?? 'unknown', kit: owner.get(`agent:${e.name}`) ?? null }));
476
490
  } else if (section === 'skills') {
477
491
  items = inv.skills.map((e) => ({ name: e.name, title: e.name, desc: e.desc,
478
- source: e.source ?? null, klass: e.klass ?? 'build', status: e.status ?? 'unknown' }));
492
+ source: e.source ?? null, klass: e.klass ?? 'build', status: e.status ?? 'unknown', kit: owner.get(`skill:${e.name}`) ?? null }));
479
493
  } else if (section === 'laws') {
480
494
  items = constitutionItems(inv);
481
495
  } else if (section === 'memory') {
@@ -499,6 +513,7 @@ export function apiItem(state, type, name) {
499
513
  links: resolveLinks(state, e.body), source: e.source ?? null };
500
514
  if (type === 'skill') out.klass = e.klass ?? 'build';
501
515
  out.status = e.status ?? 'unknown';
516
+ out.kit = kitOwners().get(`${type}:${name}`) ?? null;
502
517
  return out;
503
518
  }
504
519
  if (type === 'law') {
@@ -303,6 +303,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
303
303
  // Build resolves its argv off the install (`buildActionCmds`); the other actions take no
304
304
  // axes and self-resolve the deployed theme downstream.
305
305
  const cmds = action === 'build' ? buildActionCmds(state, body) : actionCommands(action);
306
+ if (cmds && Object.hasOwn(cmds, 'error')) return sendJson(res, cmds, 400, ae);
306
307
  if (!isTruthy(cmds)) {
307
308
  return sendJson(res, { error: `unknown action ${action}` }, 404, ae);
308
309
  }
package/js/web/routes.mjs CHANGED
@@ -21,7 +21,7 @@ import { percentDecode } from '../lib/text.mjs';
21
21
  import { apiExcludesMutate, apiInstallToggle, apiLoopsPresetMutate, apiMcp, apiMemoryDelete, apiSelectView } from './actions.mjs';
22
22
  import { apiActivity, apiActivityDetail, apiActivityToggle } from './activity.mjs';
23
23
  import {
24
- apiDiff, apiDoctor, apiExcludes, apiFleet, apiInstalls, apiLoops, apiLoopsActive, apiOverview,
24
+ apiDiff, apiDoctor, apiExcludes, apiFleet, apiInstalls, apiKits, apiLoops, apiLoopsActive, apiOverview,
25
25
  apiRecent, apiSetup, apiThemes, apiUpdate,
26
26
  } from './api.mjs';
27
27
  import { NotFound, apiCatalog, apiItem } from './catalog.mjs';
@@ -151,6 +151,7 @@ export const STATE_ROUTES = {
151
151
  new URL(url || '', 'http://x').searchParams.has('fresh')),
152
152
  '/api/installs': apiInstalls,
153
153
  '/api/fleet': apiFleet,
154
+ '/api/kits': apiKits,
154
155
  '/api/update': apiUpdate,
155
156
  '/api/usage': apiUsage,
156
157
  '/api/gates': apiGates,
package/js/web/server.mjs CHANGED
@@ -27,7 +27,7 @@ import { isFile, printOut } from '../lib/fs.mjs';
27
27
  import { which } from '../lib/paths.mjs';
28
28
  import { parseIntStrict } from '../lib/text.mjs';
29
29
  import { promptLine } from '../lib/prompt.mjs';
30
- import { warmDoctor, webState } from './state.mjs';
30
+ import { fallbackView, warmDoctor, webState } from './state.mjs';
31
31
  import { clearDaemon, codeStamp, openUrl, readDaemon, restartDaemon, startDaemon, statusDaemon, stopDaemon, writeDaemon } from './daemon.mjs';
32
32
  import { makeHandler } from './handler.mjs';
33
33
  import { JobManager } from './jobs.mjs';
@@ -132,6 +132,12 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
132
132
  }
133
133
  }
134
134
  const state = webState(theme);
135
+ // The daemon record and the job file stay at the DEFAULT target, captured before the view
136
+ // moves: `geneseed web stop`/`status` find them through `webState(theme).target`, which never
137
+ // moves, and a view that followed a Bob-only install into ~/.bob would hide the record.
138
+ const home = state.target;
139
+ const fallback = fallbackView(home);
140
+ if (fallback) state.selectView(...fallback);
135
141
  if (!existsSync(join(state.target, GLOBAL_MANIFEST))) {
136
142
  printOut(`[web] no deployed harness at ${state.target}.\n`);
137
143
  printOut(' Run `geneseed setup` first — serving anyway (read-only UI).\n');
@@ -143,7 +149,7 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
143
149
  const holder = {};
144
150
  // The console's job list survives a reload and a restart because it is a FILE on disk,
145
151
  // not held in process memory.
146
- const jm = new JobManager(join(state.target, '.geneseed-web-runs.json'));
152
+ const jm = new JobManager(join(home, '.geneseed-web-runs.json'));
147
153
  const srv = createServer(makeHandler(state, jm, token, dist, holder));
148
154
  holder.srv = srv;
149
155
  return new Promise((done) => {
@@ -157,7 +163,7 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
157
163
  const hostPort = /** @type {import('node:net').AddressInfo} */ (srv.address()).port;
158
164
  const url = `http://127.0.0.1:${hostPort}`;
159
165
  if (daemon) {
160
- writeDaemon(state.target, {
166
+ writeDaemon(home, {
161
167
  pid: process.pid, port: hostPort, url,
162
168
  token, theme: state.theme, started: Math.floor(Date.now() / 1000), code: startCode,
163
169
  });
@@ -177,8 +183,8 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
177
183
  });
178
184
  srv.on('close', () => {
179
185
  if (daemon) {
180
- const st = readDaemon(state.target);
181
- if (!st || st.pid === process.pid) clearDaemon(state.target);
186
+ const st = readDaemon(home);
187
+ if (!st || st.pid === process.pid) clearDaemon(home);
182
188
  }
183
189
  done(0);
184
190
  });