geneseed 3.9.0 → 3.11.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 (219) hide show
  1. package/CHANGELOG.md +89 -1
  2. package/DESIGN.md +1 -1
  3. package/README.md +52 -71
  4. package/SHIPPED.md +3 -3
  5. package/adapters/bob/README.md +18 -2
  6. package/adapters/claude-code/README.md +17 -12
  7. package/adapters/openclaude/README.md +2 -2
  8. package/adapters/opencode/README.md +4 -4
  9. package/adapters/opencode/plugins/geneseed-context.js +78 -8
  10. package/bin/build-driver.mjs +2 -0
  11. package/bin/geneseed-cli.mjs +17 -0
  12. package/bin/geneseed-hook.mjs +3 -1
  13. package/docs/README.md +42 -0
  14. package/docs/_groups.json +18 -0
  15. package/docs/candidate-hosts.md +12 -5
  16. package/docs/concepts/agents.md +44 -0
  17. package/docs/concepts/collaboration.md +62 -0
  18. package/docs/concepts/exclusions.md +55 -0
  19. package/docs/concepts/footprint.md +52 -0
  20. package/docs/concepts/foreman-mode.md +69 -0
  21. package/docs/concepts/hooks.md +101 -0
  22. package/docs/concepts/hosts.md +55 -0
  23. package/docs/concepts/lsp.md +50 -0
  24. package/docs/{web → concepts}/memory.md +2 -2
  25. package/docs/{web → concepts}/notebook.md +2 -2
  26. package/docs/concepts/rules.md +52 -0
  27. package/docs/concepts/skills.md +49 -0
  28. package/docs/concepts/themes.md +50 -0
  29. package/docs/extending.md +27 -21
  30. package/docs/guides/autostart.md +70 -0
  31. package/docs/guides/choose-your-setup.md +120 -0
  32. package/docs/guides/headless.md +22 -0
  33. package/docs/guides/install.md +161 -0
  34. package/docs/guides/mcp.md +195 -0
  35. package/docs/guides/migrate.md +41 -0
  36. package/docs/guides/project-context.md +87 -0
  37. package/docs/guides/run-anywhere.md +45 -0
  38. package/docs/guides/uninstall.md +51 -0
  39. package/docs/guides/upgrade.md +65 -0
  40. package/docs/guides/verify.md +65 -0
  41. package/docs/guides/wiki.md +69 -0
  42. package/docs/guides/worktree.md +47 -0
  43. package/docs/reference/about.md +7 -0
  44. package/docs/reference/cli.md +7 -0
  45. package/docs/reference/environment.md +99 -0
  46. package/docs/reference/glossary.md +33 -0
  47. package/docs/reference/opencode-plugins.md +131 -0
  48. package/docs/reference/troubleshooting.md +350 -0
  49. package/docs/token-footprint.md +26 -21
  50. package/docs/understand/a-day.md +77 -0
  51. package/docs/understand/enforced-vs-asked.md +64 -0
  52. package/docs/understand/harness.md +36 -0
  53. package/docs/understand/on-your-machine.md +76 -0
  54. package/docs/web-ui.md +2 -2
  55. package/harness.config.json +1 -1
  56. package/js/README.md +7 -6
  57. package/js/build/driver.mjs +26 -95
  58. package/js/build/emit-claude.mjs +76 -81
  59. package/js/build/generate.mjs +12 -2
  60. package/js/build/render.mjs +4 -0
  61. package/js/build/source.mjs +6 -0
  62. package/js/build/stubs.mjs +18 -2
  63. package/js/cli-table.json +6 -6
  64. package/js/hosts/hooks.mjs +86 -54
  65. package/js/hosts/hosts.mjs +5 -21
  66. package/js/hosts/installs.mjs +4 -6
  67. package/js/hosts/mcp.mjs +4 -14
  68. package/js/hosts/native.mjs +27 -50
  69. package/js/hosts/opencode.mjs +1 -1
  70. package/js/hosts/settings.mjs +17 -79
  71. package/js/inspect/checks-authoring.mjs +9 -4
  72. package/js/inspect/checks-repo.mjs +45 -1
  73. package/js/inspect/diff.mjs +11 -3
  74. package/js/inspect/doctor.mjs +4 -4
  75. package/js/inspect/excludes.mjs +0 -4
  76. package/js/inspect/inventory.mjs +1 -1
  77. package/js/inspect/registry.mjs +20 -3
  78. package/js/inspect/status.mjs +11 -6
  79. package/js/lib/node-floor.mjs +30 -0
  80. package/js/maintain/migrate.mjs +5 -5
  81. package/js/maintain/setup.mjs +112 -26
  82. package/js/maintain/uninstall.mjs +8 -42
  83. package/js/maintain/update.mjs +4 -15
  84. package/js/ui/cli.mjs +2 -2
  85. package/js/web/actions.mjs +4 -5
  86. package/js/web/api.mjs +7 -4
  87. package/js/web/daemon.mjs +52 -0
  88. package/js/web/docs.mjs +149 -65
  89. package/js/web/server.mjs +5 -2
  90. package/package.json +6 -5
  91. package/registry.json +7 -0
  92. package/src/AGENT.md.tmpl +73 -52
  93. package/src/laws/universal.md +3 -0
  94. package/src/ontology/universal.md +1 -1
  95. package/src/postures/mentor.md +1 -0
  96. package/src/skills/daydream/SKILL.md +10 -3
  97. package/src/skills/daydream/VENDOR.md +10 -2
  98. package/src/skills/daydream/instructions.md +2 -2
  99. package/src/skills/explain-changes/SKILL.md +1 -1
  100. package/src/skills/explain-changes/VENDOR.md +1 -1
  101. package/src/skills/geneseed.md +1 -1
  102. package/src/skills/learn-mode.md +89 -0
  103. package/src/skills/pipeline.md +2 -2
  104. package/src/skills/quiz.md +2 -1
  105. package/src/skills/react-view-transitions/SKILL.md +1 -1
  106. package/src/skills/react-view-transitions/VENDOR.md +7 -2
  107. package/src/skills/teach.md +2 -1
  108. package/src/skills/token-report/SKILL.md +2 -3
  109. package/src/skills/token-report/VENDOR.md +2 -3
  110. package/src/skills/token-report/scripts/token_report.mjs +7 -121
  111. package/src/skills/workflow.md +1 -1
  112. package/themes/_TEMPLATE.json +1 -0
  113. package/themes/biker.json +1 -0
  114. package/themes/commentator.json +1 -0
  115. package/themes/cyberpunk.json +1 -0
  116. package/themes/gamer.json +1 -0
  117. package/themes/imperial.json +1 -0
  118. package/themes/joker.json +1 -0
  119. package/themes/marvin.json +1 -0
  120. package/themes/mean.json +1 -0
  121. package/themes/military.json +1 -0
  122. package/themes/neutral.json +1 -0
  123. package/themes/pirate.json +1 -0
  124. package/themes/sports.json +1 -0
  125. package/themes/verstappen.json +1 -0
  126. package/themes/wizard.json +1 -0
  127. package/web/dist/assets/Activity-C52LEb-2.js +1 -0
  128. package/web/dist/assets/ActivityDetail-CjNLRvXg.js +1 -0
  129. package/web/dist/assets/Dashboard-B6_sSCHi.js +2 -0
  130. package/web/dist/assets/Docs-Dj7BmCSA.js +5 -0
  131. package/web/dist/assets/{GreenhouseView-Bq1QmaGO.js → GreenhouseView-BALwjQsA.js} +1 -1
  132. package/web/dist/assets/Installs-BbffDQm3.js +2 -0
  133. package/web/dist/assets/JournalView-JpAQ8Hqe.js +1 -0
  134. package/web/dist/assets/Laws-DK7eCWSm.js +3 -0
  135. package/web/dist/assets/Library-CoamHelE.js +1 -0
  136. package/web/dist/assets/{Markdown-CkFyfgNf.js → Markdown-qjM2_ofS.js} +5 -5
  137. package/web/dist/assets/{OperatorHudView-B-HVBqfY.js → OperatorHudView-DVx1HlBx.js} +1 -1
  138. package/web/dist/assets/Personal-eaXb1zcj.js +1 -0
  139. package/web/dist/assets/{Tabs-_pjUFIEI.js → Tabs-BmGtt8E2.js} +1 -1
  140. package/web/dist/assets/api-Dc8f3ca-.js +1 -0
  141. package/web/dist/assets/drawable-Du_PZrQj.js +1 -0
  142. package/web/dist/assets/index-Bz_KMRDH.css +1 -0
  143. package/web/dist/assets/index-CNFH7o55.js +2 -0
  144. package/web/dist/index.html +3 -2
  145. package/QUICKSTART.md +0 -50
  146. package/SETUP.md +0 -1183
  147. package/adapters/copilot/README.md +0 -105
  148. package/docs/web/_groups.json +0 -45
  149. package/docs/web/about.md +0 -6
  150. package/docs/web/adapters-claude-code.md +0 -8
  151. package/docs/web/adapters-opencode-loads.md +0 -8
  152. package/docs/web/adapters-opencode-spec.md +0 -8
  153. package/docs/web/adapters-opencode.md +0 -8
  154. package/docs/web/agents.md +0 -8
  155. package/docs/web/authoring.md +0 -11
  156. package/docs/web/autostart.md +0 -9
  157. package/docs/web/claude-hooks.md +0 -37
  158. package/docs/web/cli.md +0 -6
  159. package/docs/web/collaboration.md +0 -27
  160. package/docs/web/configure-wiki.md +0 -9
  161. package/docs/web/design.md +0 -7
  162. package/docs/web/env-knobs.md +0 -9
  163. package/docs/web/exclusions.md +0 -27
  164. package/docs/web/first-session.md +0 -28
  165. package/docs/web/footprint.md +0 -38
  166. package/docs/web/foreman-mode.md +0 -33
  167. package/docs/web/glossary.md +0 -6
  168. package/docs/web/harnesses.md +0 -30
  169. package/docs/web/headless.md +0 -9
  170. package/docs/web/ingest-docs.md +0 -9
  171. package/docs/web/install-paths.md +0 -9
  172. package/docs/web/install-quick.md +0 -35
  173. package/docs/web/lsp-overview.md +0 -37
  174. package/docs/web/mcp-claude-code.md +0 -10
  175. package/docs/web/mcp-filesystem.md +0 -9
  176. package/docs/web/mcp-gitlab.md +0 -9
  177. package/docs/web/mcp-markitdown.md +0 -9
  178. package/docs/web/mcp-overview.md +0 -34
  179. package/docs/web/mcp-trouble.md +0 -9
  180. package/docs/web/mcp-verify.md +0 -9
  181. package/docs/web/migrate.md +0 -39
  182. package/docs/web/model.md +0 -20
  183. package/docs/web/pipelines.md +0 -24
  184. package/docs/web/plugin-activity.md +0 -23
  185. package/docs/web/plugin-context.md +0 -26
  186. package/docs/web/plugin-guard.md +0 -24
  187. package/docs/web/plugin-learn.md +0 -27
  188. package/docs/web/plugin-notify.md +0 -24
  189. package/docs/web/plugin-ponytail.md +0 -23
  190. package/docs/web/plugin-setup.md +0 -7
  191. package/docs/web/plugin-workflow.md +0 -25
  192. package/docs/web/plugins.md +0 -17
  193. package/docs/web/project-context.md +0 -9
  194. package/docs/web/rules.md +0 -30
  195. package/docs/web/run-anywhere.md +0 -9
  196. package/docs/web/self-improve.md +0 -8
  197. package/docs/web/setup-choices.md +0 -64
  198. package/docs/web/skills.md +0 -8
  199. package/docs/web/themes-author.md +0 -20
  200. package/docs/web/themes.md +0 -9
  201. package/docs/web/token-footprint.md +0 -7
  202. package/docs/web/trouble.md +0 -38
  203. package/docs/web/upgrade.md +0 -9
  204. package/docs/web/verify.md +0 -30
  205. package/docs/web/wiki.md +0 -7
  206. package/docs/web/worktree.md +0 -10
  207. package/docs/wiki.md +0 -43
  208. package/web/dist/assets/Activity-DAzV-qQj.js +0 -1
  209. package/web/dist/assets/ActivityDetail-CwEwMtKl.js +0 -1
  210. package/web/dist/assets/Dashboard-C2ELBW-X.js +0 -2
  211. package/web/dist/assets/Docs-iZrvQP8J.js +0 -2
  212. package/web/dist/assets/Installs-BiqR8xdO.js +0 -2
  213. package/web/dist/assets/JournalView-6wilG5Zd.js +0 -1
  214. package/web/dist/assets/Laws-D8YXD_XE.js +0 -3
  215. package/web/dist/assets/Library-DwuTZ2-L.js +0 -1
  216. package/web/dist/assets/Personal-BbUTmZQR.js +0 -1
  217. package/web/dist/assets/drawable-CoIulEH2.js +0 -1
  218. package/web/dist/assets/index-BMwSp8BP.js +0 -2
  219. package/web/dist/assets/index-CDPDk2vj.css +0 -1
@@ -134,7 +134,7 @@ export function globalEmitProblems(themeName) {
134
134
  }
135
135
 
136
136
  /**
137
- * `_harness_build._claude_bob_emit_problems` — validate the claude/bob/copilot/openclaude PER-REPO
137
+ * `_harness_build._claude_bob_emit_problems` — validate the claude/bob/openclaude PER-REPO
138
138
  * emits, which were never checked before.
139
139
  *
140
140
  * That is exactly why the CLAUDE.md/AGENTS.md skill-table dead links shipped unnoticed: the
@@ -143,7 +143,7 @@ export function globalEmitProblems(themeName) {
143
143
  * skills one level deeper than a bundle does.
144
144
  */
145
145
  export function claudeBobEmitProblems(themeName) {
146
- return emitProblemsOver(['claude', 'bob', 'copilot', 'openclaude'], (tmp, label) => {
146
+ return emitProblemsOver(['claude', 'bob', 'openclaude'], (tmp, label) => {
147
147
  const root = path.join(tmp, 'root');
148
148
  return {
149
149
  label: `${themeName} ${label}`,
@@ -211,7 +211,7 @@ export function doctorCollect({
211
211
  problems = problems.concat(ran('global', `Global install (${themeName})`,
212
212
  globalEmitProblems(themeName)));
213
213
  problems = problems.concat(ran('claude_bob',
214
- `Claude/Bob/Copilot/OpenClaude per-repo emit (${themeName})`, claudeBobEmitProblems(themeName)));
214
+ `Claude/Bob/OpenClaude per-repo emit (${themeName})`, claudeBobEmitProblems(themeName)));
215
215
  }
216
216
  });
217
217
  problems = problems.concat(ran('parity', 'Theme parity', themeParityProblems()));
@@ -283,7 +283,7 @@ export function cmdDoctor(args) {
283
283
  // (`js/build/generate.mjs`), which forwards `--theme` and nothing else. So the front door
284
284
  // answered `unrecognized arguments: --sync-themes` to anyone who followed doctor's own
285
285
  // advice, for as long as the premise went unchecked. The flag belongs to the GENERATOR,
286
- // whose binary is `geneseed-build` — which is what `README.md` and `SETUP.md` have said
286
+ // whose binary is `geneseed-build` — which is what `README.md` and the install guide have said
287
287
  // all along. A hint is a command; if it is not runnable it is decoration.
288
288
  printOut(' tip: a theme is missing a key another theme defines — run '
289
289
  + '`geneseed-build --sync-themes` to fill it from _TEMPLATE.json, '
@@ -143,10 +143,6 @@ export function excludeAdd(target) {
143
143
  wired.bob_rules_stub = true;
144
144
  }
145
145
  }
146
- if (host === 'copilot') {
147
- messages.push('copilot: no native suppression exists — the global '
148
- + 'copilot-instructions.md still loads there (documented limitation)');
149
- }
150
146
  const entries = data.excludes.filter((e) => !(isDict(e) && same(get(e, 'path'), repo)));
151
147
  entries.push({ path: repo.split(path.sep).join('/'), wired });
152
148
  data.excludes = entries;
@@ -108,7 +108,7 @@ export const SKILL_CLASS = {
108
108
  // `understand` before, which made that class a grab-bag.
109
109
  wiki: 'harness', geneseed: 'harness', rule: 'harness', profile: 'harness',
110
110
  herdr: 'harness', 'skill-forge': 'harness', 'consolidate-memory': 'harness',
111
- teach: 'learn', quiz: 'learn',
111
+ teach: 'learn', quiz: 'learn', 'learn-mode': 'learn',
112
112
  };
113
113
 
114
114
  /** `k in dict` — own keys only, where `k in obj` also finds `Object.prototype` members. */
@@ -17,10 +17,10 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync } from 'n
17
17
  import path from 'node:path';
18
18
  import os from 'node:os';
19
19
 
20
- import { expanduser } from '../hosts/hosts.mjs';
20
+ import { expanduser, resolvePath } from '../hosts/hosts.mjs';
21
21
  import { writeText, isFile, isDir } from '../lib/fs.mjs';
22
22
  import { parseJson, jsonDumpsIndent } from '../lib/json.mjs';
23
- import { toPlatformPath } from '../lib/paths.mjs';
23
+ import { toPlatformPath, within } from '../lib/paths.mjs';
24
24
 
25
25
  /** `_install_registry._path()` — `$XDG_CONFIG_HOME/geneseed/installs.json`. */
26
26
  function registryPath() {
@@ -28,6 +28,21 @@ function registryPath() {
28
28
  return path.join(base, 'geneseed', 'installs.json');
29
29
  }
30
30
 
31
+ /**
32
+ * Is `root` a scratch install — inside the OS temp dir while the registry is not?
33
+ *
34
+ * Agent sessions emit into scratchpads under `%TEMP%`; recorded, each one is rebuilt by every
35
+ * `rebuild-all` and listed by every `status` for as long as the folder survives. The second
36
+ * clause is what keeps every test and golden cell meaningful: their registry lives in a
37
+ * sandbox under the temp dir too, so their temp roots are not scratch RELATIVE TO IT.
38
+ * `within` is case-insensitive on win32, so `C:\Users\X\AppData\Local\Temp` and its
39
+ * lower-cased spelling are one directory.
40
+ */
41
+ export function isScratchRoot(root, registryFile) {
42
+ const tmp = resolvePath(os.tmpdir());
43
+ return within(resolvePath(root), tmp) && !within(resolvePath(registryFile), tmp);
44
+ }
45
+
31
46
  function registryLoad() {
32
47
  try {
33
48
  const data = parseJson(readFileSync(registryPath(), 'utf8'));
@@ -59,6 +74,7 @@ export function registryRecord(dir) {
59
74
  try {
60
75
  let root;
61
76
  try { root = realpathSync.native(dir); } catch { root = path.resolve(dir); }
77
+ if (isScratchRoot(root, registryPath())) return;
62
78
  const cur = registryLoad();
63
79
  if (!cur.includes(root)) registrySave([...cur, root]);
64
80
  } catch { /* a registry hiccup must never fail a build */ }
@@ -75,6 +91,7 @@ export function registryRecord(dir) {
75
91
  */
76
92
  export function registryRoots() {
77
93
  const original = registryLoad();
94
+ const file = registryPath();
78
95
  const out = [];
79
96
  const kept = [];
80
97
  const seen = new Set();
@@ -96,7 +113,7 @@ export function registryRoots() {
96
113
  let key;
97
114
  try { key = existsSync(root) ? realpathSync.native(root) : root; } catch { key = root; }
98
115
  if (seen.has(key)) continue;
99
- if (isDir(root) && isFile(path.join(root, '.geneseed-emit'))) {
116
+ if (isDir(root) && isFile(path.join(root, '.geneseed-emit')) && !isScratchRoot(root, file)) {
100
117
  seen.add(key);
101
118
  kept.push(root);
102
119
  out.push(root);
@@ -61,7 +61,7 @@ import { THEMES, ROOT, makeCfg } from '../build/source.mjs';
61
61
  import { tuiInventory } from './inventory.mjs';
62
62
  import { readVersion, sourceFingerprint } from '../build/version.mjs';
63
63
  import {
64
- claudeConfigDir, bobConfigDir, copilotConfigDir, openclaudeConfigDir, opencodeConfigDir,
64
+ claudeConfigDir, bobConfigDir, openclaudeConfigDir, opencodeConfigDir,
65
65
  resolvePath, resolveMemoryDir, sovereignBypass, GATE_LEDGER,
66
66
  } from '../hosts/hosts.mjs';
67
67
  // P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
@@ -73,6 +73,7 @@ import {
73
73
  defaultTheme, installedDefaults, installTargets, manifestIsClaude, readJsonMaybe, readMaybe,
74
74
  } from '../hosts/installs.mjs';
75
75
  import { installProfile, rebuildCommand } from '../build/generate.mjs';
76
+ import { shimDead } from '../hosts/settings.mjs';
76
77
  import { printOut } from '../lib/fs.mjs';
77
78
  import { codePointLength, padEndToWidth } from '../lib/text.mjs';
78
79
 
@@ -99,9 +100,9 @@ export function cmdVersion(args) {
99
100
  let target = args.target ? resolvePath(args.target) : opencodeConfigDir();
100
101
  let installed = readVersion(target);
101
102
  if (installed === null) {
102
- // A claude/bob/copilot-only machine must not report "no install detected". The bundle
103
+ // A claude/bob/openclaude-only machine must not report "no install detected". The bundle
103
104
  // candidates are cwd-relative only — see the Python for why `ROOT / "Harness"` left.
104
- for (const base of [claudeConfigDir(), bobConfigDir(), copilotConfigDir(), openclaudeConfigDir(),
105
+ for (const base of [claudeConfigDir(), bobConfigDir(), openclaudeConfigDir(),
105
106
  path.join(process.cwd(), 'Harness'), process.cwd()]) {
106
107
  const v = readVersion(base);
107
108
  if (v) { installed = v; target = base; break; }
@@ -208,7 +209,9 @@ export function gateSummary(cfgDirs) {
208
209
  total += 1;
209
210
  }
210
211
  }
211
- return { standing_down: standingDown, asks, total };
212
+ // `dead` is the machine shim's missing targets: every gate below is armed on paper and never
213
+ // runs, so it outranks `armed` in the row.
214
+ return { standing_down: standingDown, asks, total, dead: shimDead() };
212
215
  }
213
216
 
214
217
  /** `_harness_status._status_data`. */
@@ -226,7 +229,7 @@ export function statusData() {
226
229
  let installedFp = null;
227
230
  let verTarget = null;
228
231
  const otherCfg = [];
229
- for (const fn of [claudeConfigDir, bobConfigDir, copilotConfigDir, openclaudeConfigDir]) {
232
+ for (const fn of [claudeConfigDir, bobConfigDir, openclaudeConfigDir]) {
230
233
  try { otherCfg.push(fn()); } catch { /* a missing host dir must not sink status */ }
231
234
  }
232
235
  // Same walk as `cmdVersion`'s fallback, and cwd-relative for the same reason.
@@ -350,7 +353,9 @@ export function statusLines(d, color = false) {
350
353
  // panels carry no `gates` key and must render unchanged.
351
354
  if (d.gates) {
352
355
  const g = d.gates;
353
- const armed = g.standing_down.length
356
+ const armed = g.dead?.length
357
+ ? `DEAD — hook shim points at ${g.dead.join(', ')}; run: geneseed rebuild-all`
358
+ : g.standing_down.length
354
359
  ? `STANDING DOWN for this cwd (excludes.json in ${g.standing_down.join(', ')})`
355
360
  : 'armed';
356
361
  const byRule = Object.entries(g.asks).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The Node floor — the FIRST import of all three binaries, so an old Node gets one sentence
3
+ * instead of a stack trace from whichever API it lacks.
4
+ *
5
+ * `engines` in package.json only makes npm warn, and `npx geneseed`, a global install and the
6
+ * hook shim all run the binaries directly. ESM links the whole graph before any body runs, so
7
+ * this only helps while every file still PARSES on the old Node; it runs before any of them
8
+ * executes. The hook exits 0: a hook must never block a tool call, and the message on stderr
9
+ * is what the host surfaces. Nothing here may print to stdout — the hook's decision channel.
10
+ */
11
+ import path from 'node:path';
12
+
13
+ export const NODE_FLOOR = '22.3.0';
14
+
15
+ /** Numeric, not string, comparison: '22.10.0' is above '22.3.0'. */
16
+ export function belowFloor(version, floor = NODE_FLOOR) {
17
+ const v = version.replace(/^v/, '').split('.').map(Number);
18
+ const f = floor.split('.').map(Number);
19
+ for (let i = 0; i < 3; i += 1) {
20
+ if ((v[i] || 0) !== f[i]) return (v[i] || 0) < f[i];
21
+ }
22
+ return false;
23
+ }
24
+
25
+ if (belowFloor(process.versions.node)) {
26
+ process.stderr.write(`geneseed needs Node 22.3 or newer; this is Node ${process.versions.node}. `
27
+ + 'Install a current Node from https://nodejs.org (or your package manager), then run the '
28
+ + 'command again.\n');
29
+ process.exit(path.basename(process.argv[1] || '').startsWith('geneseed-hook') ? 0 : 1);
30
+ }
@@ -52,11 +52,11 @@ const KNOWN_EMITS = new Set(EMIT_HOST_SCOPE.keys());
52
52
  /**
53
53
  * The settings file a host wires hooks into, or null for a host that has none.
54
54
  *
55
- * Only Claude and Bob carry hook commands. OpenCode's hooks are a PLUGIN (`js/hosts/opencode.mjs`
56
- * emits JS that runs in the host's own process and names neither the shim nor `harness.py`),
57
- * and Copilot has no hook mechanism at all. So those two classify as `none` and their whole
58
- * migration is the re-emit — which is the same re-emit, in the same change, on all three
59
- * hosts, which is what the project's host-parity rule asks for.
55
+ * Only the Claude-style hosts carry hook commands. OpenCode's hooks are a PLUGIN
56
+ * (`js/hosts/opencode.mjs` emits JS that runs in the host's own process and names neither the
57
+ * shim nor `harness.py`), so it classifies as `none` and its whole migration is the re-emit —
58
+ * the same re-emit, in the same change, on every host, which is what the project's
59
+ * host-parity rule asks for.
60
60
  */
61
61
  function hookSettingsFile(root, host, scope) {
62
62
  if (!['claude', 'bob', 'openclaude'].includes(host)) return null;
@@ -50,9 +50,10 @@ import { discoverNames, PACK_ORDER } from '../build/source.mjs';
50
50
  import { exportImprovements } from '../inspect/diff.mjs';
51
51
  import { cmdDoctor } from '../inspect/doctor.mjs';
52
52
  import { setupBuildArgs } from '../build/generate.mjs';
53
- import { GLOBAL_MANIFEST, opencodeConfigDir } from '../hosts/hosts.mjs';
53
+ import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir } from '../hosts/hosts.mjs';
54
54
  import {
55
- defaultMode, defaultPosture, defaultTheme, installedDefaults, themeFiles,
55
+ claudeCfg, defaultMode, defaultPosture, defaultTheme, EMIT_HOST_SCOPE, installedDefaults,
56
+ readJsonMaybe, themeFiles,
56
57
  } from '../hosts/installs.mjs';
57
58
  import { printOut, printErr } from '../lib/fs.mjs';
58
59
  import { which } from '../lib/paths.mjs';
@@ -280,7 +281,7 @@ export function doctrineOptions() {
280
281
  /**
281
282
  * `_harness_setup.EMIT_OPTIONS`.
282
283
  *
283
- * Exported since P6b: `/api/themes` returns the same eleven as its `emits` list, and a copy
284
+ * Exported since P6b: `/api/themes` returns the same nine as its `emits` list, and a copy
284
285
  * of a table under test silently stops being the table under test.
285
286
  */
286
287
  export const EMIT_OPTIONS = [
@@ -290,8 +291,6 @@ export const EMIT_OPTIONS = [
290
291
  ['claude', 'Per-repo CLAUDE.md + .claude/ committed into one repository.'],
291
292
  ['bob-global', 'IBM Bob global config dir (~/.bob) — rules/geneseed.md, agents, skills, settings.json.'],
292
293
  ['bob', 'Per-repo AGENTS.md + .bob/ for IBM Bob, committed into one repository.'],
293
- ['copilot-global', 'GitHub Copilot personal config dir (~/.copilot) — copilot-instructions.md, agents, skills.'],
294
- ['copilot', 'Per-repo AGENTS.md + .github/ for GitHub Copilot, committed into one repository.'],
295
294
  ['files', 'Plain bundle for any AGENT.md tool.'],
296
295
  // Appended AFTER `files`, not beside the other hosts: the menu is answered by number, and a
297
296
  // scripted `setup` that pipes its answers must keep meaning what it meant.
@@ -377,10 +376,10 @@ export function collectSetupLines() {
377
376
  inst.footprint || 'lean');
378
377
  let out = null;
379
378
  let root = null;
380
- // Every PROJECT emit needs the repo root — claude/bob/copilot included: without `--out`
379
+ // Every PROJECT emit needs the repo root — claude/bob/openclaude included: without `--out`
381
380
  // their CLAUDE.md/.claude land in the generator's default ./Harness, where the host never
382
381
  // looks.
383
- if (['opencode', 'claude', 'bob', 'copilot', 'openclaude'].includes(emit)) {
382
+ if (['opencode', 'claude', 'bob', 'openclaude'].includes(emit)) {
384
383
  root = ask('Repo root to install into', '.');
385
384
  out = root;
386
385
  } else if (emit === 'files') {
@@ -459,39 +458,77 @@ export function lspPrereqs() {
459
458
  + "SDKMAN `sdk install java 21-tem`, or your distro's package"]];
460
459
  }
461
460
 
461
+ /** The name each host goes by on screen — the same four labels `hostTools` prints. */
462
+ const TOOL_NAME = {
463
+ opencode: 'OpenCode', claude: 'Claude Code', bob: 'IBM Bob', openclaude: 'OpenClaude',
464
+ };
465
+
466
+ /**
467
+ * Where an emit put its root file, and — for the Claude-shaped hosts — its hooks.
468
+ *
469
+ * READ BACK, NOT RE-SPELLED: the placements differ by host and scope (Bob global's preamble is
470
+ * `rules/geneseed.md`, a Claude project's CLAUDE.md sits beside `.claude/`, a project's hooks go
471
+ * to `settings.local.json`, Bob global's to `settings/settings.json`), and the emit already
472
+ * records each one in the install's manifest (`managed.claude_md`, `settings_file`,
473
+ * `settings_hooks`). A summary that recomputed them would be a second owner of every rule.
474
+ */
475
+ function emitFacts(emit, out) {
476
+ const [host, scope] = EMIT_HOST_SCOPE.get(emit) ?? ['files', 'project'];
477
+ if (host === 'files' || host === 'opencode') {
478
+ const dir = emit === 'opencode-global' ? opencodeConfigDir() : resolveOut(out || 'Harness');
479
+ return { host, rootFile: path.join(dir, 'AGENT.md') };
480
+ }
481
+ const spec = HOSTS.find((h) => h.host === host);
482
+ const cfg = claudeCfg(scope === 'global' ? spec.configDir() : resolveOut(out || '.'), scope, host);
483
+ const managed = readJsonMaybe(path.join(cfg, GLOBAL_MANIFEST))?.managed ?? {};
484
+ const rootFile = managed.claude_md?.rel
485
+ ? path.resolve(cfg, managed.claude_md.rel)
486
+ : path.join(cfg, host === 'bob' ? path.join('rules', 'geneseed.md') : spec.agentFile);
487
+ return {
488
+ host,
489
+ rootFile,
490
+ settings: managed.settings_file ? path.join(cfg, managed.settings_file) : null,
491
+ hookGroups: Array.isArray(managed.settings_hooks) ? managed.settings_hooks.length : 0,
492
+ };
493
+ }
494
+
462
495
  /**
463
496
  * `_harness_setup._setup_summary_lines` — the post-build report as `[kind, text]` rows,
464
497
  * `kind` being ok | warn | info.
465
498
  *
466
499
  * `ok` is the build's own success, so the first row is the only one that can say "build
467
- * failed"; every later row assumes a tree on disk. The global-install warning is the row
468
- * with real behaviour in it: a bundle or project emit made while an OpenCode GLOBAL install
469
- * exists is loaded by nothing, and saying so is the difference between a wizard and a form.
500
+ * failed"; every later row assumes a tree on disk. HOST-AWARE since the install-hardening
501
+ * pass: it used to know only OpenCode's AGENT.md, so a successful claude-global install ended
502
+ * on "expected AGENT.md … but it is not there" and "start a NEW OpenCode session". The
503
+ * global-install warning is the row with real behaviour in it: a bundle or project emit made
504
+ * while an OpenCode GLOBAL install exists is loaded by nothing, and saying so is the
505
+ * difference between a wizard and a form.
470
506
  */
471
507
  export function setupSummaryLines(theme, emit, out, root, ok) {
472
- const agentMd = emit === 'opencode-global'
473
- ? path.join(opencodeConfigDir(), 'AGENT.md')
474
- : path.join(resolveOut(out || 'Harness'), 'AGENT.md');
508
+ const f = emitFacts(emit, out ?? root);
509
+ const name = path.basename(f.rootFile);
475
510
  const lines = [];
476
- if (ok && existsSync(agentMd)) {
477
- lines.push(['ok', `AGENT.md written to ${agentMd}`]);
511
+ if (ok && existsSync(f.rootFile)) {
512
+ lines.push(['ok', `${name} written to ${f.rootFile}`]);
478
513
  } else if (ok) {
479
- lines.push(['warn', `expected AGENT.md at ${agentMd} but it is not there`]);
514
+ lines.push(['warn', `expected ${name} at ${f.rootFile} but it is not there`]);
480
515
  } else {
481
516
  lines.push(['warn', 'build failed — see the output above']);
482
517
  }
483
- if (emit === 'opencode-global') {
484
- const cfgDir = opencodeConfigDir();
485
- const hint = process.platform === 'win32'
486
- ? `learn plugin: $env:GENESEED_HARNESS = "${cfgDir}" (persist: setx GENESEED_HARNESS "${cfgDir}")`
487
- : 'learn plugin: export GENESEED_HARNESS="$HOME/.config/opencode"';
488
- lines.push(['info', hint]);
489
- } else if (emit === 'files') {
490
- lines.push(['info', `point your tool's instructions at ${agentMd}`]);
518
+ if (ok && f.settings) {
519
+ lines.push(['ok', `${f.hookGroups} hook groups wired in ${f.settings}`]);
520
+ if (!process.env.GENESEED_LLM) {
521
+ lines.push(['info', 'memory learning is off until you set GENESEED_LLM '
522
+ + '(see docs/reference/environment.md)']);
523
+ }
524
+ }
525
+ if (emit === 'files') {
526
+ lines.push(['info', `point your tool's instructions at ${f.rootFile}`]);
491
527
  }
492
528
  try {
493
529
  const cfg = opencodeConfigDir();
494
- if (emit !== 'opencode-global' && existsSync(path.join(cfg, GLOBAL_MANIFEST))) {
530
+ // Only for an emit OpenCode itself would load — a Claude or Bob install is not shadowed.
531
+ if (['opencode', 'files'].includes(emit) && existsSync(path.join(cfg, GLOBAL_MANIFEST))) {
495
532
  lines.push(['warn', `a global install exists at ${cfg} — OpenCode loads THAT, `
496
533
  + `not this build; re-run with 'opencode-global' to change it`]);
497
534
  }
@@ -502,7 +539,12 @@ export function setupSummaryLines(theme, emit, out, root, ok) {
502
539
  lines.push(present ? ['ok', `${label} present`] : ['warn', `${label} missing — ${hint}`]);
503
540
  }
504
541
  }
505
- lines.push(['info', `theme is now '${theme}' — start a NEW OpenCode session for the new voice`]);
542
+ const tool = TOOL_NAME[f.host];
543
+ lines.push(['info', tool
544
+ ? `theme is now '${theme}' — start a NEW ${tool} session to load it`
545
+ : `theme is now '${theme}' — start a NEW session in your tool to load it`]);
546
+ lines.push(['info', 'what was installed and what each piece does: '
547
+ + 'https://github.com/Arylmera/Geneseed/blob/main/docs/understand/on-your-machine.md']);
506
548
  return lines;
507
549
  }
508
550
 
@@ -595,5 +637,49 @@ export function cmdSetup() {
595
637
  + ' geneseed build --emit opencode-global --theme neutral\n');
596
638
  return 1;
597
639
  }
640
+ if (!toolsReport()) return 1;
598
641
  return setupLines();
599
642
  }
643
+
644
+ /**
645
+ * The AI coding tools Geneseed installs into, as `[label, present, url]` rows in HOSTS order.
646
+ *
647
+ * PRESENT MEANS THE COMMAND IS ON PATH, and nothing else. The config dir looked like a second
648
+ * signal — a desktop-only install has one and no command — but GENESEED ITSELF CREATES IT: a
649
+ * `bob-global` emit writes `~/.bob` on a machine that never had Bob, and the first run of this
650
+ * check reported Bob present on exactly such a machine. A wrong "ok" sends the user to an
651
+ * install mode for a tool they cannot open; a wrong "not found" costs one line they can ignore.
652
+ *
653
+ * NO SPAWN, deliberately. `which` walks PATH by hand, so asking costs nothing on the CLI's
654
+ * child_process allow-list — a `claude --version` probe would have been a third entry there to
655
+ * learn nothing `which` does not already say. `lookup` is the seam for the test, which cannot
656
+ * rely on what this machine happens to have installed.
657
+ */
658
+ export function hostTools({ lookup = which } = {}) {
659
+ return [
660
+ ['OpenCode', 'opencode', 'https://opencode.ai/docs/'],
661
+ ['Claude Code', 'claude', 'https://code.claude.com/docs/en/setup'],
662
+ ['IBM Bob', 'bob', 'https://bob.ibm.com/'],
663
+ ['OpenClaude', 'openclaude', 'https://openclaude.gitlawb.com/'],
664
+ ].map(([label, cmd, url]) => [label, Boolean(lookup(cmd)), url]);
665
+ }
666
+
667
+ /**
668
+ * Show which tools are here before asking anything — the install-mode question is easier to
669
+ * answer knowing it — and when NONE is, ask whether to stop and install one first. It never
670
+ * installs one: on a managed machine that is IT's call, and each tool has its own installer.
671
+ * A missing tool among several present is a line, not a question; nobody needs all four.
672
+ * `false` is the user choosing to stop.
673
+ */
674
+ export function toolsReport(tools = hostTools()) {
675
+ printOut('AI coding tools on this machine:\n');
676
+ for (const [label, present, url] of tools) {
677
+ printOut(present ? ` ok ${label}\n` : ` -- ${label} not on PATH — install: ${url}\n`);
678
+ }
679
+ if (tools.some(([, present]) => present)) { printOut('\n'); return true; }
680
+ printOut('\nNone of them is installed. Install the one you use (links above), then run setup '
681
+ + 'again — or go on and pick the plain bundle, which any AGENT.md tool can read.\n');
682
+ if (confirm('Continue without one?', false)) { printOut('\n'); return true; }
683
+ printOut('Stopped. Run setup again once a tool is installed.\n');
684
+ return false;
685
+ }
@@ -59,10 +59,9 @@ import {
59
59
  } from '../hosts/hosts.mjs';
60
60
  import { mcpCommented } from '../hosts/mcp.mjs';
61
61
  import {
62
- copilotIntegrityCheck, managedBlockRead, managedBlockRemove, managedBlockWrite,
62
+ managedBlockRead, managedBlockRemove, managedBlockWrite,
63
63
  mergeClaudeSettings, opencodeTarget, readJsonc,
64
64
  settingsIntegrityCheck, wireClaudeExcludes, unwireClaudeExcludes, unwireClaudeSettings,
65
- unwireCopilotSettings,
66
65
  } from '../hosts/settings.mjs';
67
66
  import { printOut, printErr, readText, writeText, isFile, isDir, isOsError } from '../lib/fs.mjs';
68
67
  import { indexOfDeepEqual, jsonDumps, jsonDumpsIndent, deepEquals } from '../lib/json.mjs';
@@ -71,7 +70,7 @@ import { comparePaths, isAbsolutePath, within } from '../lib/paths.mjs';
71
70
  const hostSpec = (host) => HOSTS.find((h) => h.host === host);
72
71
 
73
72
  /** The Claude-STYLE hosts — one manifest shape, one reversal. Spelled once. */
74
- const CLAUDE_STYLE = ['claude', 'bob', 'copilot', 'openclaude'];
73
+ const CLAUDE_STYLE = ['claude', 'bob', 'openclaude'];
75
74
 
76
75
  /**
77
76
  * `shutil.rmtree(p, ignore_errors=True)`.
@@ -295,12 +294,8 @@ export function claudeUninstall(cfg, archiveMemory) {
295
294
  if (failed.length) warnSurvivors(failed);
296
295
  const hooks = managed.settings_hooks || [];
297
296
  const sf = settingsFile(cfg, managed);
298
- // Two shapes, two unwires: a Copilot install records `copilot_hooks` (event → one hook)
299
- // and never `settings_hooks`, so exactly one of these does anything.
300
- const unwired = unwireClaudeSettings(sf, hooks)
301
- | unwireCopilotSettings(sf, managed.copilot_hooks || []);
297
+ const unwired = unwireClaudeSettings(sf, hooks);
302
298
  unwireClaudeExcludes(sf, managed.settings_excludes || []);
303
- copilotIntegrityCheck(sf, managed.copilot_hooks || [], 'absent');
304
299
  // The unwire is VERIFIED, not assumed: a commented settings file is never rewritten, so a
305
300
  // supposedly-uninstalled repo could keep firing Geneseed's hooks. Loud, never fatal.
306
301
  settingsIntegrityCheck(sf, managed, 'absent');
@@ -320,7 +315,7 @@ export function claudeUninstall(cfg, archiveMemory) {
320
315
  /**
321
316
  * `_harness_mcp._uninstall_global` — reverse a global install at `target` via its manifest.
322
317
  *
323
- * Host-aware at the top, and that dispatch is the whole reason a Claude/Bob/Copilot global
318
+ * Host-aware at the top, and that dispatch is the whole reason a Claude/Bob/OpenClaude global
324
319
  * install and an OpenCode one can share one entry point: the Claude family has no
325
320
  * opencode.json to unmerge and a settings.json plus a managed block instead.
326
321
  */
@@ -681,7 +676,7 @@ export function cmdUninstall(args) {
681
676
  const targetDesc = resolvePath(expanduser(args.target));
682
677
  printErr(`[uninstall] no Geneseed install detected at ${targetDesc}.\n`
683
678
  + '[uninstall] pass --target <repo> for a project install (.opencode/.claude/'
684
- + '.bob/.github/.openclaude) or --target <config dir> for a global one.\n');
679
+ + '.bob/.openclaude) or --target <config dir> for a global one.\n');
685
680
  return 1;
686
681
  }
687
682
  const [host, scope, root] = hit;
@@ -695,12 +690,7 @@ export function cmdUninstall(args) {
695
690
  const data = installDataDir(root, host, scope);
696
691
  const stores = ['memory', 'notebook'].filter((n) => isDir(path.join(data, n)));
697
692
  printOut(`[uninstall] target: ${root} (${host}:${scope})\n`);
698
- if (host === 'copilot') {
699
- printOut('[uninstall] removes: agents/, skills/, markers, the '
700
- + `${hostSpec(host).agentFile} managed block, and Geneseed's `
701
- + '~/.copilot/settings.json hooks on a global install (your own keys/hooks and '
702
- + '.github files are kept).\n');
703
- } else if (['claude', 'bob', 'openclaude'].includes(host)) {
693
+ if (['claude', 'bob', 'openclaude'].includes(host)) {
704
694
  printOut('[uninstall] removes: agents/, skills/, markers, the '
705
695
  + `${hostSpec(host).agentFile} managed block, and Geneseed's `
706
696
  + 'settings.json hooks/excludes (your own keys/hooks are kept).\n');
@@ -1024,7 +1014,7 @@ export function installReactivate(root, host = 'opencode', scope = 'global') {
1024
1014
  return { ok: true, kind, moved };
1025
1015
  }
1026
1016
 
1027
- // ---- the Claude-style fork (Claude · IBM Bob · GitHub Copilot) ---------------------------
1017
+ // ---- the Claude-style fork (Claude · IBM Bob · OpenClaude) ---------------------------
1028
1018
  //
1029
1019
  // A Claude-style install has NO `instructions` array — the harness reaches the host through
1030
1020
  // the CLAUDE.md managed block and the settings.json hooks. So the reversal is three moving
@@ -1057,27 +1047,6 @@ function cleanHostStash(cfg, host = 'claude') {
1057
1047
  function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
1058
1048
  const data = claudeReadManifest(cfg);
1059
1049
  const managed = managedOf(data);
1060
- // A LEGACY Copilot install (single-slot `copilot_hooks`, the dead `toolCall` event):
1061
- // reactivating it re-wires the CURRENT Copilot shape — event → array, recorded under
1062
- // `settings_hooks` — rather than restoring hooks Copilot no longer reads. None of the
1063
- // pack/exclude axes below apply: the Copilot gate is not pack-conditional.
1064
- if (Array.isArray(managed.copilot_hooks)) {
1065
- const sf = settingsFile(cfg, managed);
1066
- unwireCopilotSettings(sf, managed.copilot_hooks);
1067
- const [, claims] = mergeClaudeSettings(sf, 'global', null, hookRunnerEntry(), null, [],
1068
- 'copilot', cfg);
1069
- if (Object.keys(data).length > 0) {
1070
- delete managed.copilot_hooks;
1071
- managed.settings_hooks = claims;
1072
- data.managed = managed;
1073
- const tmp = path.join(cfg, `${GLOBAL_MANIFEST}.tmp`);
1074
- try {
1075
- writeText(tmp, `${jsonDumpsIndent(data)}\n`);
1076
- renameSync(tmp, path.join(cfg, GLOBAL_MANIFEST));
1077
- } catch { /* except OSError: pass */ }
1078
- }
1079
- return;
1080
- }
1081
1050
  // `hookOpts` is the one argument `mergeClaudeSettings` will not default, and it is right
1082
1051
  // not to: there is no computable fallback for the runner/entry the shim bakes. The
1083
1052
  // emitter's own originator is imported rather than restated — a reactivate that wired a
@@ -1090,8 +1059,7 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
1090
1059
  // PROJECT install `cfg` is `<repo>/.claude` and the carrier is `<repo>/CLAUDE.md`, so
1091
1060
  // reading `cfg` alone answered `null` for every project reactivate and re-wired the gate
1092
1061
  // unconditionally (fail-closed, so not a hole — but wrong, and it made the toggle one-way
1093
- // for project installs). `root` first, `cfg` second: copilot keeps its carrier INSIDE the
1094
- // config dir (`.github/copilot-instructions.md`), and on a global install the two are the
1062
+ // for project installs). `root` first, `cfg` second: on a global install the two are the
1095
1063
  // same directory anyway. Both silent ⇒ `null` ⇒ the gate stays.
1096
1064
  const doctrines = doctrinesOfDir(root) ?? doctrinesOfDir(cfg);
1097
1065
  // Same two-carrier read for the second axis, and its own default: an absent line means
@@ -1134,12 +1102,10 @@ function claudeDeactivate(root, scope = 'global', host = 'claude') {
1134
1102
  return { ok: false, failed, rolled_back: done.length };
1135
1103
  }
1136
1104
  unwireClaudeSettings(settingsFile(cfg, managed), managed.settings_hooks || []);
1137
- unwireCopilotSettings(settingsFile(cfg, managed), managed.copilot_hooks || []);
1138
1105
  unwireClaudeExcludes(settingsFile(cfg, managed), managed.settings_excludes || []);
1139
1106
  // The same integrity check the uninstall path runs: a deactivate that silently failed to
1140
1107
  // unwire would leave hooks firing in a repo the user believes is off.
1141
1108
  settingsIntegrityCheck(settingsFile(cfg, managed), managed, 'absent');
1142
- copilotIntegrityCheck(settingsFile(cfg, managed), managed.copilot_hooks || [], 'absent');
1143
1109
  const cm = claudeMdPath(cfg, managed);
1144
1110
  const block = managedBlockRead(cm);
1145
1111
  mkdirSync(stash, { recursive: true }); // presence == disabled, even if nothing moved
@@ -50,7 +50,7 @@ import { appendText, copyFile, printOut, printErr, readText, writeText } from '.
50
50
  import { toPlatformPath, which } from '../lib/paths.mjs';
51
51
  import { WHITESPACE, parseIntStrict, stripWhitespace } from '../lib/text.mjs';
52
52
  import { NO_WINDOW } from '../lib/proc.mjs';
53
- import { restartDaemon } from '../web/daemon.mjs';
53
+ import { bounceDaemonIfRunning } from '../web/daemon.mjs';
54
54
 
55
55
  /**
56
56
  * `_CREDS_RE` / `_redact_url_creds` — strip a `user[:token]@` userinfo out of any URL in
@@ -578,20 +578,9 @@ export async function rebuildBundle(here, out, theme, emit, rootDir, log) {
578
578
  { cwd: String(here), stdio: 'inherit' });
579
579
  if (proc.status !== 0) return proc.status === null ? 1 : proc.status;
580
580
  // If a web daemon is running, bounce it so the new source and the freshly rebuilt web/dist
581
- // take effect — otherwise the open PWA keeps hitting the old code. EXCEPT when this
582
- // upgrade IS a web-daemon job: restarting the daemon here kills the process tracking this
583
- // very job (console stuck on 'running' forever); in that case the server restarts itself
584
- // once the job is recorded as finished. `js/web/jobs.mjs` sets the variable; this is the
585
- // side that reads it, and `upgrade/the-web-job-marker-...` is what gates the pair.
586
- if (process.env.GENESEED_WEB_JOB) {
587
- log('[geneseed] web daemon will restart itself after this job to load the new code.');
588
- return 0;
589
- }
590
- try {
591
- await restartDaemon(theme, 4747, false, true);
592
- } catch (e) { // never fail an upgrade on this
593
- log(`[geneseed] ⚠️ could not refresh the web daemon (${e && e.message ? e.message : e}) — \`geneseed web restart\` manually if it was running.`);
594
- }
581
+ // take effect — otherwise the open PWA keeps hitting the old code. The web-job exception
582
+ // (`upgrade/the-web-job-marker-...` gates it) lives with the helper.
583
+ await bounceDaemonIfRunning(theme, log);
595
584
  return 0;
596
585
  }
597
586
 
package/js/ui/cli.mjs CHANGED
@@ -69,7 +69,7 @@ function load() {
69
69
  try {
70
70
  return JSON.parse(readFileSync(CLI_JSON, 'utf-8'));
71
71
  } catch {
72
- return { prog: 'harness', commands: [] };
72
+ return { prog: 'geneseed', commands: [] };
73
73
  }
74
74
  }
75
75
 
@@ -88,7 +88,7 @@ const pageArgs = (args) => (args ?? [])
88
88
  export function cliReference() {
89
89
  const data = load();
90
90
  return {
91
- prog: data.prog ?? 'harness',
91
+ prog: data.prog ?? 'geneseed',
92
92
  commands: (data.commands ?? []).map((c) => ({
93
93
  name: c.name ?? '',
94
94
  help: c.help ?? '',