geneseed 3.2.0 → 3.4.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 (171) hide show
  1. package/CHANGELOG.md +150 -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 +52 -25
  9. package/adapters/opencode/README.md +11 -3
  10. package/adapters/opencode/plugins/geneseed-guard.js +40 -6
  11. package/bin/build-driver.mjs +16 -10
  12. package/bin/geneseed-cli.mjs +55 -56
  13. package/bin/geneseed-hook.mjs +21 -14
  14. package/docs/candidate-hosts.md +74 -0
  15. package/docs/extending.md +10 -8
  16. package/docs/token-footprint.md +32 -21
  17. package/docs/web/agents.md +1 -1
  18. package/docs/web/first-session.md +1 -1
  19. package/docs/web/skills.md +1 -1
  20. package/docs/web-ui.md +5 -13
  21. package/harness.config.json +1 -1
  22. package/js/README.md +5 -6
  23. package/js/build/emit-claude.mjs +22 -7
  24. package/js/build/render.mjs +37 -19
  25. package/js/build/source.mjs +22 -2
  26. package/js/cli-table.json +84 -0
  27. package/js/hosts/hooks.mjs +240 -78
  28. package/js/hosts/hosts.mjs +56 -2
  29. package/js/hosts/link.mjs +1 -1
  30. package/js/hosts/mcp.mjs +3 -1
  31. package/js/hosts/native.mjs +86 -15
  32. package/js/hosts/opencode.mjs +14 -0
  33. package/js/hosts/settings.mjs +173 -5
  34. package/js/inspect/excludes.mjs +1 -1
  35. package/js/inspect/inventory.mjs +13 -8
  36. package/js/inspect/status.mjs +50 -2
  37. package/js/maintain/uninstall.mjs +38 -11
  38. package/js/maintain/update.mjs +2 -2
  39. package/js/web/actions.mjs +8 -8
  40. package/js/web/api.mjs +5 -10
  41. package/js/web/docs.mjs +9 -31
  42. package/js/web/handler.mjs +20 -30
  43. package/js/web/jobs.mjs +5 -26
  44. package/js/web/routes.mjs +18 -84
  45. package/js/web/server.mjs +3 -3
  46. package/package.json +1 -1
  47. package/registry.json +99 -78
  48. package/src/AGENT.md.tmpl +69 -28
  49. package/src/agents/architect.md +4 -1
  50. package/src/agents/explorer.md +3 -1
  51. package/src/agents/operator.md +13 -0
  52. package/src/agents/pragmatist.md +12 -0
  53. package/src/agents/researcher.md +72 -0
  54. package/src/agents/security.md +7 -1
  55. package/src/agents/skeptic.md +2 -1
  56. package/src/agents/tester.md +5 -1
  57. package/src/agents/user-advocate.md +13 -0
  58. package/src/doctrines/README.md +3 -3
  59. package/src/doctrines/rigor.md +14 -1
  60. package/src/laws/universal.md +98 -20
  61. package/src/ontology/universal.md +38 -13
  62. package/src/skills/_self-improvement.md +19 -0
  63. package/src/skills/_template.md +6 -13
  64. package/src/skills/brainstorm.md +37 -19
  65. package/src/skills/bruno-collection-generator.md +52 -0
  66. package/src/skills/bruno-test-writer.md +51 -0
  67. package/src/skills/ci-fix.md +49 -0
  68. package/src/skills/codebase-design.md +1 -12
  69. package/src/skills/commit.md +1 -12
  70. package/src/skills/consolidate-memory.md +50 -0
  71. package/src/skills/council.md +1 -12
  72. package/src/skills/debug.md +1 -12
  73. package/src/skills/deps-audit.md +46 -0
  74. package/src/skills/develop.md +38 -20
  75. package/src/skills/document-project.md +1 -12
  76. package/src/skills/domain-modeling.md +1 -12
  77. package/src/skills/forge-mcp.md +2 -13
  78. package/src/skills/fresh-eyes.md +1 -12
  79. package/src/skills/frontend-design.md +1 -12
  80. package/src/skills/geneseed-code-review.md +1 -12
  81. package/src/skills/geneseed.md +1 -12
  82. package/src/skills/git-archaeology.md +1 -12
  83. package/src/skills/git-rescue.md +1 -12
  84. package/src/skills/handoff.md +1 -12
  85. package/src/skills/herdr.md +2 -12
  86. package/src/skills/ingest.md +1 -12
  87. package/src/skills/migrate.md +1 -12
  88. package/src/skills/opencode-theme.md +2 -12
  89. package/src/skills/parallel-agents.md +1 -12
  90. package/src/skills/pipeline.md +2 -13
  91. package/src/skills/plan.md +1 -12
  92. package/src/skills/ponytail.md +1 -12
  93. package/src/skills/profile.md +1 -12
  94. package/src/skills/prose.md +1 -12
  95. package/src/skills/prototype.md +1 -12
  96. package/src/skills/quiz.md +43 -0
  97. package/src/skills/refactor.md +1 -12
  98. package/src/skills/release.md +1 -12
  99. package/src/skills/repo-map.md +1 -12
  100. package/src/skills/research.md +6 -14
  101. package/src/skills/review-response.md +1 -12
  102. package/src/skills/roast-me.md +1 -12
  103. package/src/skills/rule.md +1 -12
  104. package/src/skills/security-audit.md +51 -0
  105. package/src/skills/ship.md +1 -12
  106. package/src/skills/skill-forge.md +59 -0
  107. package/src/skills/teach.md +50 -0
  108. package/src/skills/tickets.md +1 -12
  109. package/src/skills/wayfinder.md +1 -12
  110. package/src/skills/wiki.md +1 -12
  111. package/src/skills/workflow.md +74 -26
  112. package/src/skills/worktree.md +52 -0
  113. package/themes/_TEMPLATE.json +12 -8
  114. package/themes/biker.json +12 -8
  115. package/themes/commentator.json +12 -8
  116. package/themes/cyberpunk.json +12 -8
  117. package/themes/gamer.json +12 -8
  118. package/themes/imperial.json +12 -8
  119. package/themes/joker.json +12 -8
  120. package/themes/marvin.json +12 -8
  121. package/themes/mean.json +12 -8
  122. package/themes/military.json +12 -8
  123. package/themes/neutral.json +12 -8
  124. package/themes/pirate.json +12 -8
  125. package/themes/sports.json +12 -8
  126. package/themes/verstappen.json +12 -8
  127. package/themes/wizard.json +12 -8
  128. package/web/dist/assets/Activity-DMGhhjQc.js +1 -0
  129. package/web/dist/assets/ActivityDetail-CeQcDA7e.js +1 -0
  130. package/web/dist/assets/{CatalogRow-DqS4e5XM.js → CatalogRow-CnSbDPyg.js} +1 -1
  131. package/web/dist/assets/{Diff-CIrGbiqm.js → Diff-D_ksMBD1.js} +1 -1
  132. package/web/dist/assets/{Doctor-CpI53oHY.js → Doctor-Brb3_oCT.js} +1 -1
  133. package/web/dist/assets/FilterInput-DbRWPhtT.js +1 -0
  134. package/web/dist/assets/GreenhouseView-F3VNFr73.js +2 -0
  135. package/web/dist/assets/{Harness-vRYs11kX.js → Harness-DJSwfYeF.js} +1 -1
  136. package/web/dist/assets/JournalView-BLRYRnB3.js +1 -0
  137. package/web/dist/assets/Laws-CKYns9qe.js +3 -0
  138. package/web/dist/assets/Library-BK0YloSv.js +1 -0
  139. package/web/dist/assets/{Markdown-kgpg9EQM.js → Markdown-BzTRzc7a.js} +1 -1
  140. package/web/dist/assets/OperatorHudView-D3OQSGp5.js +2 -0
  141. package/web/dist/assets/{Profile-CHr-PPf6.js → Profile-CPA2u_0g.js} +1 -1
  142. package/web/dist/assets/{Rules-CsC6hfqk.js → Rules-Lukel-Ru.js} +1 -1
  143. package/web/dist/assets/Skills-BKRLHH7m.js +1 -0
  144. package/web/dist/assets/{StatusBadge-gRrak0nO.js → StatusBadge-DI3weRKd.js} +1 -1
  145. package/web/dist/assets/{activity-B0-FXZCj.js → activity-C_H2Bx1P.js} +1 -1
  146. package/web/dist/assets/{index-Bx3I7cRW.js → index-B7_Wga23.js} +1 -1
  147. package/web/dist/assets/index-DzdQjvM1.css +1 -0
  148. package/web/dist/assets/{index-DJOhOwwR.js → index-N8dTrVro.js} +1 -1
  149. package/web/dist/assets/index-Py_IQjgK.js +10 -0
  150. package/web/dist/assets/jobBuckets-DfTUJUxR.js +1 -0
  151. package/web/dist/assets/lawCats-DUS9UTH4.js +1 -0
  152. package/web/dist/index.html +2 -2
  153. package/web/src/pages/Laws.jsx +82 -64
  154. package/js/web/graph.mjs +0 -0
  155. package/src/skills/clarify.md +0 -28
  156. package/src/skills/crash-course.md +0 -28
  157. package/src/skills/decode.md +0 -28
  158. package/src/skills/drill.md +0 -28
  159. package/src/skills/feynman.md +0 -26
  160. package/src/skills/gap-detector.md +0 -27
  161. package/src/skills/learning-path.md +0 -26
  162. package/src/skills/tdd.md +0 -31
  163. package/web/dist/assets/Activity-BTCG2gMl.js +0 -1
  164. package/web/dist/assets/ActivityDetail-julsmPDB.js +0 -1
  165. package/web/dist/assets/Graph-CS4tA7YS.js +0 -1
  166. package/web/dist/assets/Laws-BjbHJrJt.js +0 -3
  167. package/web/dist/assets/Library-CjC7BAWN.js +0 -1
  168. package/web/dist/assets/Skills-BvatJGfe.js +0 -1
  169. package/web/dist/assets/index-BnRWQbZb.js +0 -12
  170. package/web/dist/assets/index-D0f9tJlA.css +0 -1
  171. package/web/dist/assets/roman-DkYN0LX9.js +0 -1
package/js/web/docs.mjs CHANGED
@@ -7,11 +7,9 @@
7
7
  * the same file, so there is exactly one description of the CLI surface, not a second
8
8
  * transcription of it.
9
9
  *
10
- * So `NOT_PORTED_KINDS` declares what is left, `apiDocsPage` answers 501 for it, and
11
- * `tests/unit/web_server.test.mjs` requires `PORTED_KINDS` ∪ `NOT_PORTED_KINDS` to equal
12
- * `REF_KINDS` — a FROZEN LITERAL of the five kinds this daemon has ever needed to dispatch
13
- * on. Same partition shape as the route table, which is why it is a table here too and not
14
- * two `if`s.
10
+ * `tests/unit/web_server.test.mjs` holds `KINDS` against a written-out list of the five
11
+ * kinds this daemon dispatches on — same declaration-vs-dispatch shape as the route table,
12
+ * which is why it is a table here too and not five `if`s.
15
13
  *
16
14
  * THE `?harness=` QUERY PARAM IS the Docs selector, and it is the ONLY input to these
17
15
  * endpoints that is not the checkout itself — the one thing a test can vary. Every
@@ -430,22 +428,6 @@ function about(state) {
430
428
  };
431
429
  }
432
430
 
433
- /**
434
- * EMPTY, AND KEPT DECLARED — the third partition in this codebase to reach that state,
435
- * after `NOT_PORTED_POST` and `NOT_PORTED_ACTIONS`.
436
- *
437
- * `tests/unit/web_server.test.mjs` cross-checks this set against the kinds `apiDocsPage`
438
- * dispatches on, and that check is at its STRONGEST when the set is empty: it says every
439
- * kind this daemon can dispatch on is answered here. A sixth kind added later still cannot
440
- * quietly fall through to the `NotFound` at the bottom, and it has a place to be declared
441
- * rather than reading as a typo'd page id.
442
- *
443
- * THE 501 ARM HAS NO REACHABLE TARGET TODAY: with this set empty, no page names an
444
- * unported kind. Its correctness rests on the declaration check above, not on any request
445
- * that actually reaches it — worth knowing before "simplifying" it away as dead code.
446
- */
447
- export const NOT_PORTED_KINDS = new Set();
448
-
449
431
  export function apiDocs(state, harnessName = null) {
450
432
  const hn = normHarness(harnessName, state);
451
433
  const groups = visibleGroups(hn).map((g) => ({
@@ -466,20 +448,16 @@ export function apiDocsPage(state, pageId, harnessName = null) {
466
448
  const hn = normHarness(harnessName, state);
467
449
  const render = KIND_ROUTES[page.kind];
468
450
  if (render !== undefined) return render(state, pageId, page, hn);
469
- if (NOT_PORTED_KINDS.has(page.kind)) {
470
- const e = new Error(`not ported yet: docs kind '${page.kind}'`);
471
- e.notPorted = true;
472
- throw e;
473
- }
474
451
  throw new NotFound(pageId);
475
452
  }
476
453
 
477
454
  /**
478
- * The three kinds that cross, as a TABLE and not three `if`s.
455
+ * The five kinds, as a TABLE and not five `if`s.
479
456
  *
480
- * `tests/unit/web_server.test.mjs` requires `Object.keys(KIND_ROUTES) ∪ NOT_PORTED_KINDS` to
481
- * equal the kinds `apiDocsPage` dispatches on — this table is what the dispatcher actually
482
- * consults, so a declaration-only check cannot see a dispatcher that stopped using it.
457
+ * `tests/unit/web_server.test.mjs` holds `Object.keys(KIND_ROUTES)` against a written-out
458
+ * list of the kinds `apiDocsPage` must dispatch on — this table is what the dispatcher
459
+ * actually consults, so a declaration-only check cannot see a dispatcher that stopped
460
+ * using it.
483
461
  */
484
462
  const KIND_ROUTES = {
485
463
  markdown: (state, pageId, page, hn) => {
@@ -514,4 +492,4 @@ const KIND_ROUTES = {
514
492
  }),
515
493
  };
516
494
 
517
- export const PORTED_KINDS = Object.keys(KIND_ROUTES);
495
+ export const KINDS = Object.keys(KIND_ROUTES);
@@ -19,10 +19,10 @@ import { apiDeployCmd, apiInstallCmd, apiPickFolder, apiRestore, buildOverride,
19
19
  import { NotFound, PREFIX_ROUTES, STATE_ROUTES } from './api.mjs';
20
20
  import { openUrl, requestRestart } from './daemon.mjs';
21
21
  import { apiDocs, apiDocsPage } from './docs.mjs';
22
- import { NOT_PORTED_ACTIONS, actionCommands } from './jobs.mjs';
23
- import { POST_ROUTES, notPorted, notPortedPost, readJsonBody } from './routes.mjs';
22
+ import { actionCommands } from './jobs.mjs';
23
+ import { POST_ROUTES, readJsonBody } from './routes.mjs';
24
24
  import { isFile } from '../lib/fs.mjs';
25
- import { readFileSync } from 'node:fs';
25
+ import { readFileSync, statSync } from 'node:fs';
26
26
  import { basename, dirname, extname, join, sep } from 'node:path';
27
27
  import { gzipSync } from 'node:zlib';
28
28
 
@@ -57,13 +57,17 @@ const CTYPES = {
57
57
  // ---- the request handler ---------------------------------------------------
58
58
 
59
59
  export function makeHandler(state, jm, token, dist, holder = null) {
60
+ // fp -> { mtime, data, gz }. Keyed by path but VALIDATED by mtime on every hit, so a
61
+ // rebuilt web/dist serves fresh bytes without `web restart` — the re-stat costs one
62
+ // syscall against the readFileSync + gzipSync it saves. `gz` is the gzipped body,
63
+ // computed once on the first gzip-accepting request instead of per request.
60
64
  const staticCache = new Map();
61
65
 
62
- function sendBytes(res, body, ctype, code = 200, extra = null, acceptEncoding = '') {
66
+ function sendBytes(res, body, ctype, code = 200, extra = null, acceptEncoding = '', gzStore = null) {
63
67
  const headers = { ...(extra || {}) };
64
68
  if (body.length >= GZIP_MIN && GZIP_TYPES.some((t) => ctype.startsWith(t))
65
69
  && (acceptEncoding || '').includes('gzip')) {
66
- body = gzipSync(body, { level: 6 });
70
+ body = gzStore ? (gzStore.gz ??= gzipSync(body, { level: 6 })) : gzipSync(body, { level: 6 });
67
71
  headers['Content-Encoding'] = 'gzip';
68
72
  headers.Vary = 'Accept-Encoding';
69
73
  }
@@ -91,21 +95,25 @@ export function makeHandler(state, jm, token, dist, holder = null) {
91
95
  if (!isFile(fp)) {
92
96
  return sendJson(res, { error: 'web/dist missing — run the UI build' }, 500, acceptEncoding);
93
97
  }
94
- let data = staticCache.get(fp);
95
- if (data === undefined) {
96
- data = readFileSync(fp);
97
- staticCache.set(fp, data);
98
+ const mtime = statSync(fp).mtimeMs;
99
+ let entry = staticCache.get(fp);
100
+ if (entry === undefined || entry.mtime !== mtime) {
101
+ entry = { mtime, data: readFileSync(fp), gz: null };
102
+ staticCache.set(fp, entry);
98
103
  }
104
+ let data = entry.data;
99
105
  let extra = null;
100
106
  if (basename(fp) === 'index.html') {
101
107
  const inject = Buffer.from(`<script>window.__GENESEED_TOKEN__="${token}";</script>`, 'utf-8');
102
108
  data = replaceOnce(data, Buffer.from('</head>'), Buffer.concat([inject, Buffer.from('</head>')]));
103
109
  extra = { 'Cache-Control': 'no-store' };
110
+ // The injected bytes differ from `entry.data`, so the gz cache must not serve them.
111
+ entry = null;
104
112
  } else if (path.includes('/assets/')) {
105
113
  extra = { 'Cache-Control': 'public, max-age=31536000, immutable' };
106
114
  }
107
115
  return sendBytes(res, data, CTYPES[extname(fp)] || 'application/octet-stream',
108
- 200, extra, acceptEncoding);
116
+ 200, extra, acceptEncoding, entry);
109
117
  }
110
118
 
111
119
  function doGet(req, res, path, ae) {
@@ -120,14 +128,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
120
128
  }
121
129
  if (path.startsWith('/api/docs/page/')) {
122
130
  const pid = percentDecode(path.slice('/api/docs/page/'.length));
123
- try {
124
- return sendJson(res, apiDocsPage(state, pid, harnessParam(req.url)), 200, ae);
125
- } catch (e) {
126
- // An unimplemented doc kind says so explicitly (501), rather than falling through
127
- // to the `NotFound` at the bottom of `apiDocsPage` and claiming the page is missing.
128
- if (e && e.notPorted) return sendJson(res, { error: e.message }, 501, ae);
129
- throw e;
130
- }
131
+ return sendJson(res, apiDocsPage(state, pid, harnessParam(req.url)), 200, ae);
131
132
  }
132
133
  if (path === '/api/jobs') return sendJson(res, { jobs: jm.recent() }, 200, ae);
133
134
  if (path.startsWith('/api/jobs/')) {
@@ -137,9 +138,6 @@ export function makeHandler(state, jm, token, dist, holder = null) {
137
138
  return j ? sendJson(res, j, 200, ae)
138
139
  : sendJson(res, { error: 'no such job' }, 404, ae);
139
140
  }
140
- if (notPorted(path)) {
141
- return sendJson(res, { error: `not ported yet: ${path}` }, 501, ae);
142
- }
143
141
  return serveStatic(res, path, ae);
144
142
  }
145
143
 
@@ -230,9 +228,6 @@ export function makeHandler(state, jm, token, dist, holder = null) {
230
228
  if (path.startsWith('/api/actions/')) {
231
229
  return doAction(res, path.slice(path.lastIndexOf('/') + 1), readJsonBody(body), ae);
232
230
  }
233
- if (notPortedPost(path)) {
234
- return sendJson(res, { error: `not ported yet: ${path}` }, 501, ae);
235
- }
236
231
  return sendJson(res, { error: 'not found' }, 404, ae);
237
232
  }
238
233
 
@@ -253,9 +248,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
253
248
  * it succeeded: a failed update changed nothing worth reloading and bouncing would disconnect
254
249
  * the PWA for no reason.
255
250
  *
256
- * THEN the unported rows, and only then the table. Order matters: `link`/`unlink` are REAL
257
- * actions whose verb has not crossed, and letting them fall through to `actionCommands` would
258
- * answer `{"error": "unknown action link"}` — the same thing a typo gets.
251
+ * THEN the table, which answers a 404 for any action it does not name.
259
252
  */
260
253
  function doAction(res, action, body, ae) {
261
254
  if (action === 'restore') {
@@ -284,9 +277,6 @@ export function makeHandler(state, jm, token, dist, holder = null) {
284
277
  if (jid === null) return sendJson(res, { error: 'busy' }, 409, ae);
285
278
  return sendJson(res, { job_id: jid }, 202, ae);
286
279
  }
287
- if (NOT_PORTED_ACTIONS.has(action)) {
288
- return sendJson(res, { error: `not ported yet: /api/actions/${action}` }, 501, ae);
289
- }
290
280
  // Build can be re-themed/re-targeted from the UI picker; the other actions self-resolve
291
281
  // the deployed theme downstream. Footprint, posture and mode always follow the current
292
282
  // install, so a re-theme preserves lean/full, the register and the operating mode.
package/js/web/jobs.mjs CHANGED
@@ -44,14 +44,10 @@
44
44
  * `taskkill /T` is a machine primitive here, not this program.
45
45
  *
46
46
  * ---------------------------------------------------------------------------------------
47
- * THE PARTITION — all eight action rows cross, and the declaration outlives them.
48
- *
49
- * `doctor`, `build`, `build-all`, `export`, `uninstall`, `update`, `link` and `unlink` each
50
- * name a verb `bin/geneseed-cli.mjs` runs.
51
- *
52
- * `NOT_PORTED_ACTIONS` IS EMPTY AND STILL DECLARED, which is the point of it: a ninth action
53
- * added later cannot quietly answer "unknown action", because `tests/unit/web_jobs.test.mjs`
54
- * holds the two sets against `actionCommands`' own keys.
47
+ * THE ACTION SURFACE — `doctor`, `build`, `build-all`, `export`, `uninstall`, `update`,
48
+ * `link` and `unlink` each name a verb `bin/geneseed-cli.mjs` runs, plus the three
49
+ * `INLINE_ACTIONS` the dispatcher answers without the table. `tests/unit/web_jobs.test.mjs`
50
+ * holds the union against a written-out list, so a ninth action cannot appear unchecked.
55
51
  *
56
52
  * ---------------------------------------------------------------------------------------
57
53
  * THREE DETAILS OF `_run`, EACH A COMMENT EXPLAINING A BUG IT ALREADY FIXED.
@@ -381,27 +377,10 @@ const NODE = () => process.execPath;
381
377
  const CLI = () => path.join(ROOT, 'bin', 'geneseed-cli.mjs');
382
378
  const GEN = () => path.join(ROOT, 'bin', 'build-driver.mjs');
383
379
 
384
- /**
385
- * The rows whose VERB has not crossed. EMPTY, AND STILL DECLARED.
386
- *
387
- * WHY AN EMPTY SET RATHER THAN A DELETED ONE. This set is cross-checked against
388
- * `actionCommands`' own keys, and an empty set is the strongest form that check ever takes:
389
- * it asserts that EVERY action this daemon knows how to dispatch on is answered by the
390
- * runner. Deleting the declaration would delete that assertion, and the next unported
391
- * action would come back as a silent 404 — the thing the 501 exists to prevent.
392
- *
393
- * A 501 AND NOT THE 404 THE TABLE WOULD PRODUCE. `actionCommands` returns `null` for an
394
- * unknown action, mapped to `{"error": "unknown action <x>"}` at 404 — so a
395
- * real-but-unported action falling through would answer the same thing a typo gets. With
396
- * this set empty the 501 branch is UNREACHABLE by construction: every real action answers
397
- * 202 and only an invented one answers 404.
398
- */
399
- export const NOT_PORTED_ACTIONS = new Set();
400
-
401
380
  /**
402
381
  * The three actions the dispatcher answers INLINE, without consulting the table above:
403
382
  * `restore` is synchronous and returns a result rather than a job id, and `install`/`deploy`
404
- * resolve their argv from the request body first. Declared so the partition covers every
383
+ * resolve their argv from the request body first. Declared so the surface check covers every
405
384
  * action the dispatcher answers, not only the table's keys.
406
385
  */
407
386
  export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
package/js/web/routes.mjs CHANGED
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * WHAT THE WEB API ANSWERS — as a declaration, separate from the dispatcher that honours it.
3
3
  *
4
- * Eight exported sets and one map. They are not documentation: `tests/unit/web_server.mjs` reads
5
- * them as a TWO-SIDED PARTITION and probes the real handler against them, because a declaration is
6
- * not a dispatcher — a route can be declared in one and missing from the other, and nothing but
7
- * a probe notices.
4
+ * Two tables and two inline lists. They are not documentation: `tests/unit/web_server.test.mjs`
5
+ * holds their union against a surface list written out in the test, and probes the real handler,
6
+ * because a declaration is not a dispatcher — a route can be declared here and missing from the
7
+ * dispatch, and nothing but a probe notices.
8
+ *
9
+ * This file used to also carry the Python→Node port's NOT_PORTED / DECLINED partition — five
10
+ * sets, all empty once the port finished. They were deleted when the port bookkeeping retired
11
+ * (see git history); an unmatched GET falls through to `serveStatic`'s SPA fallback and an
12
+ * unmatched POST answers 404, which is what the empty sets already produced.
8
13
  *
9
14
  * Split out of `server.mjs` because it is the half a reader consults to answer "does the console
10
15
  * expose X?", which has nothing to do with sockets, daemons or gzip.
@@ -13,43 +18,6 @@ import { formatValue, isTruthy, parseJson } from '../lib/json.mjs';
13
18
  import { apiExcludesMutate, apiInstallToggle, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
14
19
  import { apiActivityToggle } from './activity.mjs';
15
20
 
16
- /**
17
- * The API paths this daemon does not answer — currently empty, but the DECLARATION STAYS:
18
- * without it, an unmatched GET falls through to `serveStatic`'s SPA fallback (200 +
19
- * index.html) instead of a clear failure. Half of a partition test, so an empty set still
20
- * asserts there is nothing left to declare.
21
- *
22
- * Declared separately from the POST sets below because five paths — `/api/mcp`,
23
- * `/api/excludes`, `/api/rules`, `/api/profile`, `/api/activity` — answer both verbs with
24
- * different bodies, so one set keyed on path alone could not track GET and POST status
25
- * independently.
26
- */
27
- export const NOT_PORTED = new Set([]);
28
- export const NOT_PORTED_PREFIXES = [];
29
-
30
- /**
31
- * The POSTs that have not crossed yet — currently empty, for the same reason the GET pair
32
- * above stays declared: deleting it would let an unrouted POST fall through to a
33
- * plausible-looking 404 instead of the 501 this set exists to produce.
34
- */
35
- export const NOT_PORTED_POST = new Set([]);
36
- export const NOT_PORTED_POST_PREFIXES = [];
37
-
38
- /**
39
- * The POSTs that will NEVER cross, which is a different claim from the set above — currently
40
- * EMPTY. `/api/pick-folder` was its one member (an OS-native folder chooser, "declined rather
41
- * than deferred" because there was no Node twin that was not a new GUI dependency) until the
42
- * user overrode that decision 2026-08-27; it now dispatches inline from `js/web/handler.mjs`'s
43
- * `doPost` — see `apiPickFolder` in `js/web/actions.mjs`.
44
- *
45
- * KEPT DECLARED AND EMPTY FOR THE SAME REASON `NOT_PORTED_POST` STAYS DECLARED WHEN IT EMPTIES
46
- * (see that set, just above): an empty set here is the partition asserting there is nothing
47
- * PERMANENTLY refused, not an omission a later reader has to double-check. `notPortedPost`
48
- * below still consults it, so a genuinely permanent decline — if one is ever added again —
49
- * needs no dispatcher change, only a member here.
50
- */
51
- export const DECLINED_POST = new Set([]);
52
-
53
21
  /**
54
22
  * The POSTs this daemon answers — and this table IS the dispatch, not a declaration beside
55
23
  * it: `doPost` looks routes up HERE, so a hand-kept declaration and the running dispatcher
@@ -79,8 +47,6 @@ export const POST_ROUTES = new Map([
79
47
  ['/api/profile', [apiProfileSave, true]],
80
48
  ]);
81
49
 
82
- export const PORTED_POST = [...POST_ROUTES.keys()];
83
-
84
50
  /**
85
51
  * The 409 column, exported so a test can assert it directly. Giving `/api/activity` the 409
86
52
  * treatment is the invisible mutation: nothing in normal use can force the `ok: false` arm
@@ -92,59 +58,27 @@ export const POST_ROUTES_CONVENTION = Object.fromEntries(
92
58
  );
93
59
 
94
60
  /**
95
- * The GET paths answered OUTSIDE the two tables, declared so the partition cross-check can
61
+ * The GET paths answered OUTSIDE the two tables, declared so the surface cross-check can
96
62
  * see them: `/api/ping` is the shell's own, and the two Docs routes take the `?harness=`
97
63
  * query param, which a `(state)` table entry has no way to receive. A probe checks all
98
64
  * three against the running handler too, not merely against this list.
99
65
  */
100
- export const PORTED_INLINE = ['/api/ping', '/api/docs', '/api/docs/page/',
66
+ export const GET_INLINE = ['/api/ping', '/api/docs', '/api/docs/page/',
101
67
  '/api/jobs', '/api/jobs/'];
102
68
 
103
69
  /**
104
70
  * The POSTs that answer outside `POST_ROUTES`, because they don't fit its shape:
105
71
  * `/api/jobs/<id>/cancel` answers 200 or 404 on a lookup, not an `ok` field, and
106
- * `/api/actions/<x>` spans six statuses (202, 409, 404, 501, 400, 200) over one prefix, none
72
+ * `/api/actions/<x>` spans five statuses (202, 409, 404, 400, 200) over one prefix, none
107
73
  * read off `ok`. Bending the 409 column to fit either would make it lie about the routes it
108
- * already describes correctly.
109
- *
110
- * `/api/pick-folder` joined 2026-08-27 for a THIRD reason: `POST_ROUTES`'s `fn(state, body)
111
- * -> object` shape answers the instant `fn` returns, and this endpoint cannot — it spawns a
112
- * native folder dialog and answers only once a human (or a timeout) closes it. See
74
+ * already describes correctly. `/api/reveal` answers 200/404 on an allowlist lookup, and
75
+ * `/api/pick-folder` cannot fit the table's `fn(state, body) -> object` shape at all: it
76
+ * spawns a native folder dialog and answers only once a human (or a timeout) closes it. See
113
77
  * `apiPickFolder` in `js/web/actions.mjs`, dispatched inline from `js/web/handler.mjs`'s
114
- * `doPost` the same way the other three rows here are.
115
- */
116
- export const PORTED_POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
117
- '/api/actions/', '/api/pick-folder'];
118
-
119
- /**
120
- * POST routes added after the ported reference surface was frozen. Kept separate from
121
- * `NOT_PORTED_POST` so that frozen record stays honest, while a new route is still
122
- * enumerated rather than appearing unchecked. Belongs here only once dispatched below — a
123
- * probe, not just this declaration, is what keeps that true.
78
+ * `doPost` the same way the other rows here are.
124
79
  */
125
- export const POST_BEYOND_REF = new Set([
126
- '/api/reveal',
127
- ]);
128
-
129
- /**
130
- * GET routes added after the ported reference surface was frozen — the same reason
131
- * `POST_BEYOND_REF` above exists, one verb over. `/api/recent` is a genuine gap: no catalog
132
- * row carries a date and a browser cannot stat the `source` path it is handed, so "what
133
- * changed lately" needed a server that looks. See `apiRecent` in `api.mjs` for what it will
134
- * and will not date.
135
- */
136
- export const GET_BEYOND_REF = new Set([
137
- '/api/recent',
138
- ]);
139
-
140
- export function notPorted(path) {
141
- return NOT_PORTED.has(path) || NOT_PORTED_PREFIXES.some((p) => path.startsWith(p));
142
- }
143
-
144
- export function notPortedPost(path) {
145
- return NOT_PORTED_POST.has(path) || DECLINED_POST.has(path)
146
- || NOT_PORTED_POST_PREFIXES.some((p) => path.startsWith(p));
147
- }
80
+ export const POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
81
+ '/api/actions/', '/api/pick-folder', '/api/reveal'];
148
82
 
149
83
  /**
150
84
  * The drained request body as a plain object, `{}` for ANYTHING else — never a raise, so a
package/js/web/server.mjs CHANGED
@@ -18,9 +18,9 @@
18
18
  * WHAT WAS STILL NOT HERE, SINCE P6i UNTIL 2026-08-27: `/api/pick-folder`, DECLINED rather
19
19
  * than deferred — an OS-native folder chooser had no Node twin that was not a new GUI
20
20
  * dependency. The user overrode that decline; the endpoint now dispatches inline from
21
- * `js/web/handler.mjs`'s `doPost` (see `apiPickFolder` in `js/web/actions.mjs`). Both
22
- * `NOT_PORTED_POST` and `DECLINED_POST` are empty now and stay declared regardless, because an
23
- * empty half of a partition is the partition asserting there is nothing left, not an omission.
21
+ * `js/web/handler.mjs`'s `doPost` (see `apiPickFolder` in `js/web/actions.mjs`). With it, the
22
+ * port's NOT_PORTED / DECLINED partition emptied for good and was retired — the declared
23
+ * surface now lives in `js/web/routes.mjs` and `tests/unit/web_server.test.mjs`.
24
24
  */
25
25
  import { GLOBAL_MANIFEST, resolvePath } from '../hosts/hosts.mjs';
26
26
  import { printOut } from '../lib/fs.mjs';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
3
3
  "name": "geneseed",
4
- "version": "3.2.0",
4
+ "version": "3.4.0",
5
5
  "description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
6
6
  "keywords": [
7
7
  "ai",