geneseed 3.14.0 → 3.15.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 (88) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/SHIPPED.md +1 -1
  3. package/adapters/opencode/plugins/geneseed-context.js +23 -9
  4. package/adapters/opencode/plugins/geneseed-guard.js +16 -4
  5. package/adapters/opencode/plugins/geneseed-learn.js +37 -11
  6. package/adapters/opencode/plugins/geneseed-notify.js +37 -19
  7. package/adapters/opencode/plugins/geneseed-workflow.js +3 -1
  8. package/adapters/opencode/workflows/_runtime.js +16 -3
  9. package/bin/geneseed-cli.mjs +22 -20
  10. package/bin/geneseed-hook.mjs +6 -3
  11. package/harness.config.json +1 -1
  12. package/js/README.md +9 -5
  13. package/js/build/args.mjs +4 -3
  14. package/js/build/bundle.mjs +10 -10
  15. package/js/build/driver.mjs +8 -7
  16. package/js/build/emit-claude.mjs +3 -2
  17. package/js/build/emit-common.mjs +2 -14
  18. package/js/build/emit-opencode.mjs +12 -8
  19. package/js/build/generate.mjs +15 -11
  20. package/js/build/render.mjs +1 -1
  21. package/js/build/source.mjs +6 -6
  22. package/js/build/stubs.mjs +6 -5
  23. package/js/build/version.mjs +21 -2
  24. package/js/hosts/hooks-context.mjs +2 -1
  25. package/js/hosts/hooks-prims.mjs +2 -2
  26. package/js/hosts/hosts.mjs +24 -26
  27. package/js/hosts/installs.mjs +33 -14
  28. package/js/hosts/link.mjs +75 -26
  29. package/js/hosts/memory-files.mjs +13 -4
  30. package/js/hosts/native.mjs +106 -48
  31. package/js/hosts/opencode.mjs +44 -36
  32. package/js/hosts/shim.mjs +4 -2
  33. package/js/inspect/diff.mjs +5 -6
  34. package/js/inspect/excludes.mjs +10 -5
  35. package/js/inspect/status.mjs +18 -21
  36. package/js/inspect/validate.mjs +2 -2
  37. package/js/lib/fs.mjs +12 -11
  38. package/js/lib/json.mjs +4 -4
  39. package/js/lib/paths.mjs +27 -33
  40. package/js/lib/proc.mjs +3 -3
  41. package/js/lib/prompt.mjs +132 -0
  42. package/js/lib/text.mjs +18 -4
  43. package/js/lib/udiff.mjs +3 -2
  44. package/js/maintain/memory.mjs +5 -6
  45. package/js/maintain/migrate.mjs +4 -3
  46. package/js/maintain/setup.mjs +9 -131
  47. package/js/maintain/uninstall.mjs +10 -9
  48. package/js/maintain/update.mjs +115 -34
  49. package/js/ui/anim.mjs +8 -12
  50. package/js/ui/cli.mjs +29 -35
  51. package/js/ui/menu.mjs +7 -13
  52. package/js/ui/tui.mjs +14 -123
  53. package/js/web/actions.mjs +82 -386
  54. package/js/web/activity.mjs +1 -1
  55. package/js/web/api.mjs +33 -717
  56. package/js/web/catalog.mjs +527 -0
  57. package/js/web/daemon.mjs +21 -3
  58. package/js/web/handler.mjs +51 -27
  59. package/js/web/jobs.mjs +24 -2
  60. package/js/web/routes.mjs +61 -6
  61. package/js/web/server.mjs +4 -4
  62. package/js/web/state.mjs +138 -0
  63. package/js/web/user-files.mjs +339 -0
  64. package/package.json +1 -1
  65. package/web/dist/assets/{Activity-CjRqZnr-.js → Activity-kpWxd70F.js} +1 -1
  66. package/web/dist/assets/{ActivityDetail-OQTxKBj2.js → ActivityDetail-Ccr942VL.js} +1 -1
  67. package/web/dist/assets/Dashboard-D1lE7jcg.js +2 -0
  68. package/web/dist/assets/{Docs-CvOZ8akL.js → Docs-BHyaggFy.js} +3 -3
  69. package/web/dist/assets/{GreenhouseView-6KDjX8da.js → GreenhouseView-Sc_UDMOE.js} +1 -1
  70. package/web/dist/assets/Installs-Gw-pJ3Zi.js +2 -0
  71. package/web/dist/assets/JournalView-_gxpsxXi.js +1 -0
  72. package/web/dist/assets/{Laws-Du0-Zyp2.js → Laws-S9fq1r9T.js} +1 -1
  73. package/web/dist/assets/{Library-DOkO0TtB.js → Library-MBxadRzn.js} +1 -1
  74. package/web/dist/assets/{Loops-Dv_aLByJ.js → Loops-Da_JhlzM.js} +2 -2
  75. package/web/dist/assets/{OperatorHudView-BPP1dtLz.js → OperatorHudView--TkvDybH.js} +1 -1
  76. package/web/dist/assets/{Personal-26VPYkK0.js → Personal-CTXKHVbV.js} +1 -1
  77. package/web/dist/assets/Tabs-B0cdf8k6.js +1 -0
  78. package/web/dist/assets/api-CxgfYu18.js +1 -0
  79. package/web/dist/assets/drawable-BbWQxe82.js +1 -0
  80. package/web/dist/assets/index-ujKG1ZNf.js +2 -0
  81. package/web/dist/index.html +2 -2
  82. package/web/dist/assets/Dashboard-J-xCe_Gk.js +0 -2
  83. package/web/dist/assets/Installs-MWuhNjSm.js +0 -2
  84. package/web/dist/assets/JournalView-C8MrOz2c.js +0 -1
  85. package/web/dist/assets/Tabs-BzKuFskD.js +0 -1
  86. package/web/dist/assets/api-D_ixdyEY.js +0 -1
  87. package/web/dist/assets/drawable-zVVDbsyw.js +0 -1
  88. package/web/dist/assets/index-KHt3wE_K.js +0 -2
package/js/ui/tui.mjs CHANGED
@@ -21,13 +21,11 @@
21
21
  * deleted the day the real one crossed, would be a second user interface and not a port.
22
22
  *
23
23
  * WHAT DOES CROSS is the alignment contract — the functions below that decide what a row
24
- * looks like before any curses call is made. They have no caller in this file yet, and that
25
- * is deliberate and dated: **P7c is the due date**, when `_tui_loop` and the eighteen
26
- * screens of `_harness_tui_views.py` arrive and consume them. They are here now because they
27
- * are the half that can be gated exhaustively, and `tests/test_pure_function_parity.py`
28
- * gates every one of them in both glyph modes. Deleting any of them turns that gate red —
29
- * this port has shipped code before whose absence nothing would have noticed, and these are
30
- * not that.
24
+ * looks like: display width, fitting, glyph tiers, the catalogue's entries and detail lines.
25
+ * Each has a caller outside this file (the catalogue, the status panel, the web console).
26
+ * The panel itself was REJECTED, not deferred, so the helpers that existed only for it —
27
+ * the spinner, the wordmark, scroll clamping, the progress bar and the theme preview/flair
28
+ * readers — were deleted rather than kept waiting for a screen that will not come.
31
29
  *
32
30
  * WHAT IS DELIBERATELY NOT HERE: `_bx` (its non-ASCII arm returned `curses.ACS_*` chtype
33
31
  * ints, which belonged with the window) and every `(stdscr, curses, pal)` drawing helper.
@@ -37,14 +35,10 @@
37
35
  * **DO NOT WRITE A SECOND LINE BREAKER.** This block used to argue that `textwrap.wrap` was
38
36
  * "a second `difflib` — a stdlib payload with no Node twin". It has one since P1:
39
37
  * `wrapText` in `js/ui/cli.mjs`, written because `--help` was the caller that finally
40
- * required it, frozen at `tests/__snapshots__/textwrap.json` at every width from 11 to 198.
38
+ * required it, pinned by the written-out rows in `tests/unit/text_layout.test.mjs`.
41
39
  * Anything that needs wrapping wraps THAT. `_parse_laws`, `load_registry`, `entity_status` and
42
40
  * `_tui_inventory` crossed in P6c and live in `js/inspect/inventory.mjs`.
43
41
  */
44
- import path from 'node:path';
45
-
46
- import { THEMES } from '../build/source.mjs';
47
- import { readJsonMaybe } from '../hosts/installs.mjs';
48
42
  import { splitLines } from '../lib/udiff.mjs';
49
43
  import { printOut } from '../lib/fs.mjs';
50
44
 
@@ -58,7 +52,6 @@ import { printOut } from '../lib/fs.mjs';
58
52
  const TUI_ASCII = !!process.env.GENESEED_TUI_ASCII;
59
53
  const TUI_PLAIN = !!process.env.GENESEED_TUI_PLAIN;
60
54
  const TUI_EMOJI = !(TUI_ASCII || TUI_PLAIN);
61
- const TUI_ANIM = TUI_EMOJI;
62
55
 
63
56
  /** `_glyphs` — the one glyph table for the whole TUI, unicode or ASCII stand-ins. */
64
57
  export function glyphs(asciiMode) {
@@ -269,119 +262,18 @@ export function mark(kind) {
269
262
  return TUI_ASCII ? asc : (TUI_EMOJI ? emoji : sym);
270
263
  }
271
264
 
272
- const SPIN = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
273
-
274
- /** `_spin` — one spinner frame for tick `i`; a static dot in the non-animated tiers. */
275
- export function spin(i) {
276
- if (!TUI_ANIM) return TUI_ASCII ? '-' : '·';
277
- const frames = TUI_ASCII ? '|/-\\' : SPIN;
278
- // `frames[i % len]` on the reference indexes CODE POINTS; the braille frames are BMP so
279
- // UTF-16 indexing agrees, but spelling it through `[...frames]` keeps that an argument
280
- // about the data rather than a coincidence. Python's `%` on a negative `i` returns a
281
- // non-negative remainder and JavaScript's does not, so the modulo is corrected.
282
- const f = [...frames];
283
- return f[((i % f.length) + f.length) % f.length];
284
- }
285
-
286
- const LOGO_FONT = {
287
- G: [' ### ', '# ', '# ## ', '# # ', ' ### '],
288
- E: ['#### ', '# ', '### ', '# ', '#### '],
289
- N: ['# #', '## #', '# # #', '# ##', '# #'],
290
- S: [' ####', '# ', ' ### ', ' #', '#### '],
291
- D: ['### ', '# # ', '# #', '# # ', '### '],
292
- };
293
-
294
- /** `_logo_lines` — the GENESEED wordmark as 5 text rows. */
295
- export function logoLines() {
296
- const ink = TUI_ASCII ? '#' : '█';
297
- const rows = [];
298
- for (let r = 0; r < 5; r += 1) {
299
- rows.push([...'GENESEED'].map((c) => LOGO_FONT[c][r].replaceAll('#', ink)).join(' '));
300
- }
301
- return rows;
302
- }
303
-
304
- // ---- layout maths --------------------------------------------------------------------
305
-
306
- /** `_clamp` — clamp a scroll offset so [top, top+viewH) stays inside `total`. */
307
- export function clamp(top, total, viewH) {
308
- return Math.max(0, Math.min(top, Math.max(0, total - viewH)));
309
- }
310
-
311
- const BAR_EIGHTHS = ' ▏▎▍▌▋▊▉█';
312
-
313
- /** `_progress_bar` — a determinate bar exactly `width` display columns wide. */
314
- export function progressBar(frac, width = 24) {
315
- const f = Math.max(0.0, Math.min(1.0, frac));
316
- if (TUI_ASCII) {
317
- const filled = roundHalfEven(f * width);
318
- return '#'.repeat(filled) + '-'.repeat(width - filled);
319
- }
320
- const eighths = roundHalfEven(f * width * 8);
321
- const full = Math.floor(eighths / 8);
322
- const rem = eighths % 8;
323
- if (full >= width) return '█'.repeat(width);
324
- return '█'.repeat(full) + [...BAR_EIGHTHS][rem] + ' '.repeat(width - full - 1);
325
- }
326
-
327
- /**
328
- * `round()` — Python rounds HALF TO EVEN and `Math.round` rounds half UP, and this is the
329
- * one place in the file where that shows: a bar at exactly 0.5 of a cell is the frontier
330
- * case the eighths resolution exists to draw, and `round(0.5)` is 0 there and 1 here.
331
- */
332
- function roundHalfEven(x) {
333
- const f = Math.floor(x);
334
- const d = x - f;
335
- if (d > 0.5) return f + 1;
336
- if (d < 0.5) return f;
337
- return f % 2 === 0 ? f : f + 1;
338
- }
339
-
340
- // ---- theme reads ---------------------------------------------------------------------
341
-
342
- /** `_theme_preview` — right-panel preview rows for a theme, as (kind, text) pairs. */
343
- export function themePreview(key) {
344
- const data = readJsonMaybe(path.join(THEMES, `${key}.json`));
345
- if (!data || typeof data !== 'object') return [['dim', '(no preview available)']];
346
- const lines = [['title', key], ['', '']];
347
- if (data.TAGLINE) lines.push(['dim', data.TAGLINE], ['', '']);
348
- if (data.LOADED_SIGIL) lines.push(['ok', data.LOADED_SIGIL], ['', '']);
349
- if (data.VOICE) lines.push(['', `Voice — ${data.VOICE}`], ['', '']);
350
- if (data.LEX_SEALED_SECRETS) lines.push(['', `e.g. Rule I — ${data.LEX_SEALED_SECRETS}`]);
351
- if (data.BENEDICTION) lines.push(['', ''], ['dim', data.BENEDICTION]);
352
- return lines;
353
- }
354
-
355
- /** `_theme_flair` — the voice elements the setup chrome speaks in once a theme is chosen. */
356
- export function themeFlair(theme) {
357
- const data = readJsonMaybe(path.join(THEMES, `${theme}.json`)) || {};
358
- const banner = data.BANNER ?? '';
359
- return {
360
- accent: data.ACCENT ?? 'cyan',
361
- tagline: data.TAGLINE ?? '',
362
- sigil: data.LOADED_SIGIL ?? '',
363
- // `str.splitlines()` — `js/lib/udiff.mjs`' twin, already gated. It answers `[]` for
364
- // the empty string where `''.split('\n')` answers `['']`, which would put a blank row
365
- // in the banner of every theme that omits one; and its boundary set is Python's rather
366
- // than `\s`, which P6d measured apart.
367
- banner: splitLines(banner),
368
- benediction: data.BENEDICTION ?? '',
369
- };
370
- }
371
-
372
265
  // ---- the inventory's two consumers ----------------------------------------------------
373
266
 
374
267
  /**
375
268
  * `_tui_entries` — the ordered (kind, label, data) rows for the left list. `head` is a
376
269
  * section divider and is not selectable.
377
270
  *
378
- * ⚠ THE TWO NEW TIERS ARE GUARDED, AND THE GUARD IS NOT DEFENSIVENESS. This function has two
379
- * recorded cases per platform in `tests/__snapshots__/primitives/`, replayed under
380
- * `deepStrictEqual`, and their argument is a synthetic inventory carrying exactly
381
- * `{agents, skills, laws, theme}` — no recorder survives to re-bless them. Reading
382
- * `inv.doctrines.length` there THROWS; emitting a head unconditionally changes the recorded
383
- * result array. Keyed on presence, the recording renders byte-identical and a real inventory
384
- * gets all three tiers.
271
+ * ⚠ THE TWO NEW TIERS ARE GUARDED, AND THE GUARD IS NOT DEFENSIVENESS. When they landed, this
272
+ * function had two recorded cases per platform in the former Python suite's corpus, and their
273
+ * argument was a synthetic inventory carrying exactly `{agents, skills, laws, theme}`. Reading
274
+ * `inv.doctrines.length` on such an inventory THROWS; emitting a head unconditionally changes
275
+ * the result array. Keyed on presence, an inventory without the tiers renders as it always did
276
+ * and a real inventory gets all three tiers.
385
277
  *
386
278
  * The order is CONSTITUTIONAL and not alphabetical: ontology, then invariants, then doctrines
387
279
  * — the order they are read in, the order AGENT.md renders them, and the order the console's
@@ -435,12 +327,11 @@ export function tuiEntries(inv) {
435
327
  * because line 554 moves `sel` onto the first SELECTABLE row — but line 555 is
436
328
  * `if not selectable: sel = 0`, and row 0 is always the AGENTS header. So an inventory with
437
329
  * no agents, no skills AND no laws crashes the panel on its first frame instead of drawing
438
- * an empty state. `_TUI_INV_EMPTY` in `tests/test_pure_function_parity.py` is the corpus
330
+ * an empty state. `_TUI_INV_EMPTY` in the former Python parity suite is the corpus
439
331
  * entry that surfaced it. **P7c must not reproduce the crash when it ports `_tui_loop`** —
440
332
  * the fix is one `or []` at the call site, and it belongs in the same change as the port.
441
333
  *
442
- * Its only consumer with direct assertions is `tests/unit/setup.test.mjs`; tests/fixtures/pure_probe.mjs
443
- * also dispatches it to exercise wizard/web_first test scenarios.
334
+ * Its only consumer with direct assertions is `tests/unit/setup.test.mjs`.
444
335
  */
445
336
  export function detailLines(kind, label, data) {
446
337
  // The three constitutional tiers share one treatment — the label, a blank, then the body —