geneseed 3.1.3 → 3.3.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 (150) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +24 -14
  3. package/SETUP.md +38 -7
  4. package/SHIPPED.md +6 -5
  5. package/adapters/bob/README.md +40 -10
  6. package/adapters/claude-code/README.md +5 -0
  7. package/adapters/claude-code/settings.json +8 -1
  8. package/adapters/copilot/README.md +50 -24
  9. package/adapters/opencode/README.md +5 -1
  10. package/adapters/opencode/plugins/geneseed-guard.js +40 -6
  11. package/bin/build-driver.mjs +143 -89
  12. package/bin/geneseed-cli.mjs +63 -106
  13. package/bin/geneseed-hook.mjs +26 -16
  14. package/docs/candidate-hosts.md +74 -0
  15. package/docs/extending.md +6 -5
  16. package/docs/token-footprint.md +32 -21
  17. package/docs/web-ui.md +5 -13
  18. package/harness.config.json +1 -1
  19. package/js/README.md +5 -15
  20. package/js/build/emit-claude.mjs +26 -12
  21. package/js/build/emit-common.mjs +63 -114
  22. package/js/build/emit-opencode.mjs +74 -66
  23. package/js/build/generate.mjs +4 -6
  24. package/js/build/render.mjs +50 -32
  25. package/js/build/source.mjs +29 -8
  26. package/js/build/stubs.mjs +25 -33
  27. package/js/build/themes.mjs +1 -1
  28. package/js/build/version.mjs +1 -3
  29. package/js/cli-table.json +84 -0
  30. package/js/hosts/hooks.mjs +243 -81
  31. package/js/hosts/hosts.mjs +57 -5
  32. package/js/hosts/installs.mjs +35 -28
  33. package/js/hosts/link.mjs +2 -2
  34. package/js/hosts/mcp.mjs +3 -1
  35. package/js/hosts/native.mjs +54 -19
  36. package/js/hosts/opencode.mjs +39 -28
  37. package/js/hosts/settings.mjs +206 -115
  38. package/js/inspect/checks-authoring.mjs +5 -4
  39. package/js/inspect/checks-build.mjs +1 -1
  40. package/js/inspect/diff.mjs +1 -3
  41. package/js/inspect/doctor.mjs +59 -44
  42. package/js/inspect/excludes.mjs +5 -11
  43. package/js/inspect/inventory.mjs +38 -56
  44. package/js/inspect/registry.mjs +4 -7
  45. package/js/inspect/scan.mjs +8 -15
  46. package/js/inspect/status.mjs +51 -3
  47. package/js/lib/fs.mjs +34 -0
  48. package/js/lib/json.mjs +28 -1
  49. package/js/lib/paths.mjs +0 -10
  50. package/js/lib/udiff.mjs +11 -16
  51. package/js/maintain/memory.mjs +2 -4
  52. package/js/maintain/migrate.mjs +3 -3
  53. package/js/maintain/uninstall.mjs +127 -88
  54. package/js/maintain/update.mjs +4 -4
  55. package/js/ui/anim.mjs +1 -1
  56. package/js/ui/menu.mjs +1 -1
  57. package/js/ui/tui.mjs +3 -20
  58. package/js/web/actions.mjs +230 -217
  59. package/js/web/activity.mjs +71 -89
  60. package/js/web/api.mjs +130 -226
  61. package/js/web/daemon.mjs +92 -110
  62. package/js/web/docs.mjs +59 -126
  63. package/js/web/handler.mjs +94 -123
  64. package/js/web/jobs.mjs +116 -178
  65. package/js/web/routes.mjs +47 -192
  66. package/js/web/server.mjs +47 -82
  67. package/package.json +1 -1
  68. package/registry.json +14 -0
  69. package/src/AGENT.md.tmpl +55 -16
  70. package/src/agents/explorer.md +2 -1
  71. package/src/doctrines/README.md +3 -3
  72. package/src/doctrines/process.md +4 -2
  73. package/src/doctrines/rigor.md +13 -0
  74. package/src/laws/universal.md +133 -43
  75. package/src/ontology/universal.md +38 -13
  76. package/src/skills/_template.md +5 -1
  77. package/src/skills/bruno-collection-generator.md +63 -0
  78. package/src/skills/bruno-test-writer.md +62 -0
  79. package/src/skills/clarify.md +1 -1
  80. package/src/skills/crash-course.md +1 -0
  81. package/src/skills/decode.md +1 -0
  82. package/src/skills/drill.md +1 -0
  83. package/src/skills/feynman.md +1 -0
  84. package/src/skills/forge-mcp.md +2 -1
  85. package/src/skills/fresh-eyes.md +1 -1
  86. package/src/skills/herdr.md +1 -0
  87. package/src/skills/ingest.md +2 -1
  88. package/src/skills/learning-path.md +1 -0
  89. package/src/skills/opencode-theme.md +1 -0
  90. package/src/skills/parallel-agents.md +1 -1
  91. package/src/skills/ponytail.md +2 -2
  92. package/src/skills/research.md +4 -2
  93. package/themes/_TEMPLATE.json +5 -0
  94. package/themes/biker.json +5 -0
  95. package/themes/commentator.json +5 -0
  96. package/themes/cyberpunk.json +5 -0
  97. package/themes/gamer.json +5 -0
  98. package/themes/imperial.json +5 -0
  99. package/themes/joker.json +5 -0
  100. package/themes/marvin.json +5 -0
  101. package/themes/mean.json +5 -0
  102. package/themes/military.json +5 -0
  103. package/themes/neutral.json +5 -0
  104. package/themes/pirate.json +5 -0
  105. package/themes/sports.json +5 -0
  106. package/themes/verstappen.json +5 -0
  107. package/themes/wizard.json +5 -0
  108. package/web/dist/assets/Activity-BfVcLrda.js +1 -0
  109. package/web/dist/assets/ActivityDetail-CQXccGPl.js +1 -0
  110. package/web/dist/assets/CatalogRow-DALEQImZ.js +1 -0
  111. package/web/dist/assets/Diff-BIjxXx46.js +5 -0
  112. package/web/dist/assets/Doctor-CJLbgsrw.js +1 -0
  113. package/web/dist/assets/FilterInput-CkUCk_lY.js +1 -0
  114. package/web/dist/assets/GreenhouseView-DfC5IILJ.js +2 -0
  115. package/web/dist/assets/Harness-Dm5CCU-v.js +1 -0
  116. package/web/dist/assets/JournalView-Cpu2eI4s.js +1 -0
  117. package/web/dist/assets/Laws-DDDVFuQz.js +3 -0
  118. package/web/dist/assets/Library-Dao3T9na.js +1 -0
  119. package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-C__nuWX5.js} +15 -15
  120. package/web/dist/assets/OperatorHudView-DEpCtAjP.js +2 -0
  121. package/web/dist/assets/Profile-BlXtUlh2.js +1 -0
  122. package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-DIre6gxR.js} +1 -1
  123. package/web/dist/assets/Skills-rY13ZPhM.js +1 -0
  124. package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-wW_RS_my.js} +1 -1
  125. package/web/dist/assets/{activity-Cc0n2jwx.js → activity-DpYPKNrA.js} +1 -1
  126. package/web/dist/assets/index-BEZvPvpF.js +1 -0
  127. package/web/dist/assets/index-DNQE5ydy.js +10 -0
  128. package/web/dist/assets/index-DzdQjvM1.css +1 -0
  129. package/web/dist/assets/index-PmQGRS1X.js +2 -0
  130. package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
  131. package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
  132. package/web/dist/index.html +2 -2
  133. package/web/src/pages/Laws.jsx +119 -130
  134. package/js/web/graph.mjs +0 -0
  135. package/web/dist/assets/Activity-B49fE_ug.js +0 -1
  136. package/web/dist/assets/ActivityDetail-abt7hzD2.js +0 -1
  137. package/web/dist/assets/Diff-D3_bVkih.js +0 -5
  138. package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
  139. package/web/dist/assets/Graph-DvYMtlvl.js +0 -1
  140. package/web/dist/assets/Harness-BIWei7N5.js +0 -1
  141. package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
  142. package/web/dist/assets/Library-MjbOkHIW.js +0 -1
  143. package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
  144. package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
  145. package/web/dist/assets/Skills-DArA6IHS.js +0 -1
  146. package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
  147. package/web/dist/assets/index-DC3-GlCY.js +0 -1
  148. package/web/dist/assets/index-DP035wPa.js +0 -12
  149. package/web/dist/assets/index-dHi-7M-F.js +0 -2
  150. package/web/dist/assets/roman-DkYN0LX9.js +0 -1
package/js/web/daemon.mjs CHANGED
@@ -14,11 +14,9 @@ import { isTruthy, jsonDumpsCompact } from '../lib/json.mjs';
14
14
  import { webState } from './api.mjs';
15
15
  import { spawn } from 'node:child_process';
16
16
  import { chmodSync, mkdirSync, openSync, readFileSync, unlinkSync } from 'node:fs';
17
- import { request as httpRequest } from 'node:http';
18
17
  import { join, resolve as pathResolve } from 'node:path';
19
- import { fileURLToPath } from 'node:url';
20
18
 
21
- const ROOT = pathResolve(fileURLToPath(import.meta.url), '..', '..', '..');
19
+ const ROOT = pathResolve(import.meta.dirname, '..', '..');
22
20
 
23
21
  // ---- the daemon record -----------------------------------------------------
24
22
 
@@ -38,9 +36,9 @@ export function writeDaemon(target, data) {
38
36
  const p = statePath(target);
39
37
  writeText(p, jsonDumpsCompact(data, { bareInts: true }));
40
38
  // Owner-only: the record carries the API token. A no-op on Windows, where the
41
- // mode only maps to the read-only attribute — same as the reference's os.chmod.
39
+ // mode only maps to the read-only attribute.
42
40
  chmodSync(p, 0o600);
43
- } catch { /* the reference swallows OSError here too */ }
41
+ } catch { /* best-effort: a failed write here just means no daemon record */ }
44
42
  }
45
43
 
46
44
  export function clearDaemon(target) {
@@ -51,75 +49,62 @@ export function clearDaemon(target) {
51
49
 
52
50
  // ---- daemon mode -----------------------------------------------------------
53
51
  //
54
- // `geneseed web start|stop|status|restart`. The reference's own banner explains the design
55
- // — the running server records pid/port/token/url in a small JSON file and control is over
56
- // HTTP, so nothing here needs OS process-kill semantics, only a localhost request with the
57
- // token. Two things about the TWIN, neither of which is in that banner:
52
+ // `geneseed web start|stop|status|restart`. The running server records pid/port/token/url
53
+ // in a small JSON file and control is over HTTP, so nothing here needs OS process-kill
54
+ // semantics, only a localhost request with the token.
58
55
  //
59
- // * `_probe` and `_post_shutdown` are `urllib.request.urlopen` with a timeout, which is
60
- // synchronous. Node has no synchronous HTTP client, so every function below that
61
- // reaches one is `async` and `cmdWeb` awaits it — which is why `bin/geneseed-cli.mjs`
62
- // awaits its dispatch. Nothing about the ORDER of the reference's steps changes.
63
- // * `agent: false` on both requests. Node's global agent has kept sockets alive by
64
- // default since v19, and a pooled idle socket keeps the event loop referenced — a
65
- // `web status` that answered correctly would then hang for the keep-alive timeout
66
- // instead of exiting. Python's `urlopen` opens and closes a connection per call.
56
+ // * Every function below that touches the network is `async` (`fetch` has no
57
+ // synchronous equivalent), and `cmdWeb` awaits it.
58
+ // * Both requests are drained (`res.body?.cancel()`) whether or not the caller wants the
59
+ // body: an undrained `fetch` response can keep a keep-alive socket referenced, which is
60
+ // the same hang a `web status` that answered correctly would otherwise suffer while
61
+ // waiting to exit.
67
62
 
68
- /** `f"{url}/api/ping"` answered 200 — the cheap liveness probe both `status` and the
69
- * launcher's wait loop are built on. Every failure mode is False, as the reference's
70
- * `(URLError, OSError, ValueError)` catch is. */
71
- export function probe(url, timeout = 1.5) {
72
- return new Promise((done) => {
73
- let settled = false;
74
- const finish = (v) => { if (!settled) { settled = true; done(v); } };
75
- let req;
76
- try {
77
- req = httpRequest(`${url}/api/ping`, { method: 'GET', agent: false }, (res) => {
78
- res.resume(); // drain, or the socket never closes
79
- finish(res.statusCode === 200);
80
- });
81
- } catch {
82
- return finish(false); // a malformed url is `ValueError` there and a throw here
83
- }
84
- req.setTimeout(Math.round(timeout * 1000), () => { req.destroy(); finish(false); });
85
- req.on('error', () => finish(false));
86
- req.end();
87
- return undefined;
88
- });
63
+ /**
64
+ * `${url}/api/ping` answered 200 — the cheap liveness probe both `status` and the launcher's
65
+ * wait loop are built on. Every failure mode (network error, timeout, malformed url) is
66
+ * `false`.
67
+ */
68
+ export async function probe(url, timeout = 1.5) {
69
+ try {
70
+ const res = await fetch(`${url}/api/ping`, {
71
+ method: 'GET',
72
+ signal: AbortSignal.timeout(Math.round(timeout * 1000)),
73
+ });
74
+ const ok = res.status === 200;
75
+ // Drained after the verdict is already read: a failure canceling the body must not
76
+ // flip an already-successful ping to `false`.
77
+ await res.body?.cancel().catch(() => {});
78
+ return ok;
79
+ } catch {
80
+ // network error, timeout abort, or a malformed url — all become `false`.
81
+ return false;
82
+ }
89
83
  }
90
84
 
91
85
  /** The token-carrying POST `geneseed web stop` and the in-page Stop button both send. */
92
- export function postShutdown(url, token, timeout = 3.0) {
93
- return new Promise((done) => {
94
- let settled = false;
95
- const finish = (v) => { if (!settled) { settled = true; done(v); } };
96
- let req;
97
- try {
98
- req = httpRequest(`${url}/api/shutdown`, {
99
- method: 'POST',
100
- agent: false,
101
- headers: { 'X-Geneseed-Token': token, 'Content-Type': 'application/json' },
102
- }, (res) => {
103
- res.resume();
104
- finish(res.statusCode === 200);
105
- });
106
- } catch {
107
- return finish(false);
108
- }
109
- req.setTimeout(Math.round(timeout * 1000), () => { req.destroy(); finish(false); });
110
- req.on('error', () => finish(false));
111
- req.end('{}');
112
- return undefined;
113
- });
86
+ export async function postShutdown(url, token, timeout = 3.0) {
87
+ try {
88
+ const res = await fetch(`${url}/api/shutdown`, {
89
+ method: 'POST',
90
+ headers: { 'X-Geneseed-Token': token, 'Content-Type': 'application/json' },
91
+ body: '{}',
92
+ signal: AbortSignal.timeout(Math.round(timeout * 1000)),
93
+ });
94
+ const ok = res.status === 200;
95
+ await res.body?.cancel().catch(() => {});
96
+ return ok;
97
+ } catch {
98
+ return false;
99
+ }
114
100
  }
115
101
 
116
102
  /**
117
- * The record ONLY if a server is answering; otherwise the stale file is deleted.
118
- *
119
- * The deletion is the part a port drops, and it is what stops a machine that crashed with
120
- * a daemon running from re-probing a dead URL on every `status` forever. Both of its arms
121
- * are gated by a `harness_golden` cell — a record whose url nothing answers, and a record
122
- * carrying no url at all, which skips the probe and still clears.
103
+ * The record ONLY if a server is answering; otherwise the stale file is deleted — which is
104
+ * what stops a machine that crashed with a daemon running from re-probing a dead URL on
105
+ * every `status` forever. Both arms are gated by a `harness_golden` cell — a record whose
106
+ * url nothing answers, and a record carrying no url at all, which skips the probe and still
107
+ * clears.
123
108
  */
124
109
  export async function liveDaemon(target) {
125
110
  const st = readDaemon(target);
@@ -130,76 +115,76 @@ export async function liveDaemon(target) {
130
115
 
131
116
  const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
132
117
 
133
- /** `st.get(k)` rendered as Python renders it — a missing key prints `None`, not
134
- * `undefined`. Only reachable through a hand-edited record, and one character of
135
- * divergence in a message is still a divergence. */
118
+ /**
119
+ * A missing key prints `None`, not `undefined` — only reachable through a hand-edited
120
+ * record, kept for consistency with how every other missing value is reported here.
121
+ */
136
122
  export const none = (v) => (v === undefined || v === null ? 'None' : String(v));
137
123
 
138
124
  /**
139
- * `_spawn_detached` — the daemon launcher, and the second `_ALLOWED_SPAWNS` entry this
140
- * module owns.
125
+ * The daemon launcher, and the second `ALLOWED_SPAWNS` entry this module owns.
141
126
  *
142
127
  * WHY THERE IS NO IN-PROCESS EQUIVALENT: the whole contract is that the server OUTLIVES
143
128
  * the process that started it. `geneseed web start` prints a URL and returns to the shell;
144
129
  * the server it started must still be there. A thread, a `worker_thread` or an in-process
145
130
  * `serve()` all die with the launcher.
146
131
  *
147
- * WHAT IT STARTS IS THIS PROGRAM, named as `process.execPath` + `bin/geneseed-cli.mjs` —
148
- * the running interpreter by absolute path and this repo's own CLI. The reference names
149
- * `sys.executable` + `rituals/harness.py`; each side re-executes ITSELF, which is what
150
- * makes this not the passthrough the port exists to remove, and
151
- * `tests/test_web_daemon.py` asserts the twin's argv contains no `python`, no `harness.py`
152
- * and no `build.py` at all.
132
+ * WHAT IT STARTS IS THIS PROGRAM: `process.execPath` + `bin/geneseed-cli.mjs` — the running
133
+ * interpreter by absolute path and this repo's own CLI, re-executing itself rather than
134
+ * shelling out to anything else. `tests/unit/web_daemon.test.mjs:199-223` scans this
135
+ * function's own source for `python`/`harness.py`/`build.py`/`sys.executable` and asserts
136
+ * none of them appear — this daemon may re-execute only itself.
153
137
  */
154
138
  function spawnDetached(webArgs, log) {
155
139
  const cmd = [process.execPath, join(ROOT, 'bin', 'geneseed-cli.mjs'), 'web', ...webArgs];
156
140
  let out = 'ignore';
157
141
  try {
158
- out = openSync(log, 'a'); // `open(log, "ab")`; DEVNULL on OSError, as there
159
- } catch { /* the reference falls back to DEVNULL on exactly this failure */ }
142
+ out = openSync(log, 'a');
143
+ } catch { /* falls back to 'ignore' if the log can't be opened */ }
160
144
  const child = spawn(cmd[0], cmd.slice(1), {
161
- // `DETACHED_PROCESS | NEW_PROCESS_GROUP` on Windows and `start_new_session` on POSIX
162
- // are both `detached` here; `unref()` is what lets the launcher's event loop end while
163
- // the child runs on, which is `Popen`'s default and Node's is not.
145
+ // `detached: true` covers `DETACHED_PROCESS | NEW_PROCESS_GROUP` on Windows and
146
+ // `start_new_session` on POSIX; `unref()` is what then lets the launcher's own event
147
+ // loop end while the child keeps running — Node does not do this by default.
164
148
  detached: true, windowsHide: true, stdio: ['ignore', out, out],
165
149
  });
166
150
  child.unref();
167
151
  }
168
152
 
169
- /** The `web` argv the launcher passes. Pure, and compared against `_daemon_args` by the
170
- * corpus: the binary differs per side by design, the FLAGS may not. */
153
+ /** The `web` argv the launcher passes. Pure and side-effect free. */
171
154
  export function daemonArgs(port, theme) {
172
155
  const args = ['--daemon-internal', '--port', String(port), '--no-browser'];
173
156
  if (isTruthy(theme)) args.push('--theme', theme);
174
157
  return args;
175
158
  }
176
159
 
177
- /** The same for the out-of-band restart — `_restart_args`. */
160
+ /** The same for the out-of-band restart. */
178
161
  export function restartArgs(theme) {
179
162
  const args = ['restart', '--no-browser'];
180
163
  if (isTruthy(theme)) args.push('--theme', theme);
181
164
  return args;
182
165
  }
183
166
 
184
- /** `request_restart` — a detached `web restart`, so the new server survives the exit of
185
- * the very process asking for it. Called by POST `/api/restart`. */
167
+ /**
168
+ * A detached `web restart`, so the new server survives the exit of the very process asking
169
+ * for it. Called by POST `/api/restart`.
170
+ */
186
171
  export function requestRestart(theme) {
187
172
  const target = webState(theme).target;
188
173
  spawnDetached(restartArgs(theme), join(target, '.geneseed-web.log'));
189
174
  }
190
175
 
191
176
  /**
192
- * `webbrowser.open(url)`, which has no Node equivalent at all — there is no stdlib module
193
- * that knows what a desktop's default browser is.
177
+ * Opens a URL in the desktop's default browser. No Node stdlib API knows what that is, so
178
+ * this shells out to an OS-native opener (`start`/`open`/`xdg-open`).
194
179
  *
195
- * IT IS A MACHINE PRIMITIVE, the same class as `taskkill` and `java -version` and the
180
+ * IT IS A MACHINE PRIMITIVE, the same class as `taskkill` and `java -version`, and the
196
181
  * opposite of a passthrough: it asks the OS to open a URL and hands it nothing of this
197
182
  * program's work. Declining it instead would have been a silent regression in the one verb
198
183
  * whose job is to put a UI on screen — `geneseed web` would print an address and do
199
- * nothing, and no cell would say so because every cell passes `--no-browser`.
184
+ * nothing, and no automated test would catch it, because every test passes `--no-browser`.
200
185
  *
201
- * The reference swallows every failure here (`contextlib.suppress(Exception)` in two call
202
- * sites and a bare `except` in the third), and so does this.
186
+ * Every failure here is swallowed — a machine with no browser opener configured must not
187
+ * crash a server that is otherwise working fine.
203
188
  */
204
189
  export function openUrl(url) {
205
190
  const [file, args, extra] = process.platform === 'win32'
@@ -214,19 +199,17 @@ export function openUrl(url) {
214
199
  detached: true, stdio: 'ignore', windowsHide: true, ...extra,
215
200
  });
216
201
  // ⚠ ENOENT ARRIVES AS AN EVENT, NOT AS A THROW, AND THE `try` ABOVE CANNOT SEE IT.
217
- // This is where the reference's suppression failed to survive translation: Python's
218
- // `subprocess` raises `FileNotFoundError` synchronously, so `contextlib.suppress(Exception)`
219
- // really did swallow a missing opener. Node's `spawn` reports the same condition on the
220
- // child's `error` event, and an 'error' with no listener is re-thrown as an uncaught
221
- // exception. So the port's `try/catch` read as equivalent and suppressed nothing.
202
+ // Node's `spawn` reports a missing executable on the child's `error` event; an 'error'
203
+ // with no listener is re-thrown as an uncaught exception that would crash the daemon
204
+ // over a browser it doesn't strictly need to open. The listener below is what actually
205
+ // suppresses it — the `try`/`catch` around this call catches nothing here.
222
206
  //
223
- // MEASURED, in the packaged no-python container: `geneseed web` on a headless Linux box
224
- // printed `Error: spawn xdg-open ENOENT`. Found by the acceptance gate rather than by the
225
- // 690-cell corpus, and the docblock above says why the corpus never could — every cell
226
- // passes `--no-browser`, so no recorded cell has ever executed this line.
207
+ // MEASURED: `geneseed web` on a headless Linux box with no browser opener printed
208
+ // `Error: spawn xdg-open ENOENT` before this listener existed. No automated test catches
209
+ // this, because every test passes `--no-browser`.
227
210
  child.on('error', () => { /* no opener on this machine; the daemon is still serving */ });
228
211
  child.unref();
229
- } catch { /* the reference suppresses every exception from this call too */ }
212
+ } catch { /* spawn itself can still throw synchronously on some platforms; same rule */ }
230
213
  }
231
214
 
232
215
  export async function startDaemon(theme, port, openBrowser = true) {
@@ -240,8 +223,8 @@ export async function startDaemon(theme, port, openBrowser = true) {
240
223
  clearDaemon(target);
241
224
  const log = join(target, '.geneseed-web.log');
242
225
  spawnDetached(daemonArgs(port, theme), log);
243
- // `for _ in range(60): ... time.sleep(0.2)` — twelve seconds for the child to bind and
244
- // write its record. The probe timeout is the reference's shorter 0.5 here, not 1.5.
226
+ // Twelve seconds for the child to bind and write its record (60 * 200ms). The probe
227
+ // timeout here is the shorter 0.5s, not the default 1.5s.
245
228
  for (let i = 0; i < 60; i += 1) {
246
229
  const rec = readDaemon(target);
247
230
 
@@ -264,10 +247,9 @@ export async function stopDaemon(theme = null) {
264
247
  const { target } = webState(theme);
265
248
  const st = readDaemon(target);
266
249
  if (!isTruthy(st) || !isTruthy(st.url)) {
267
- // NO `clearDaemon` HERE, and the asymmetry with `liveDaemon` is the reference's. A
268
- // record that names no url is left exactly where it is; `web status` on the same file
269
- // deletes it. Both directions are cells, because a port that unified them would delete
270
- // a file the reference keeps and stdout would look identical.
250
+ // NO `clearDaemon` HERE — deliberately asymmetric with `liveDaemon`/`statusDaemon`,
251
+ // which DO delete a record naming no url. Unifying the two would delete a file this
252
+ // function is not supposed to touch.
271
253
  printOut('[web] no running server recorded.\n');
272
254
  return 0;
273
255
  }
@@ -304,7 +286,7 @@ export async function restartDaemon(theme = null, port = 4747, openBrowser = tru
304
286
  let open = openBrowser;
305
287
  if (live) {
306
288
  // A live daemon means a tab is already open on this (preserved) port and will
307
- // reconnect on its own — the reference's `ponytail:` note, kept.
289
+ // reconnect on its own — no need to open a fresh browser window here.
308
290
  open = false;
309
291
  await stopDaemon(theme);
310
292
  for (let i = 0; i < 50; i += 1) {