geneseed 3.1.2 → 3.2.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 (120) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +2 -2
  3. package/SHIPPED.md +3 -3
  4. package/bin/build-driver.mjs +127 -79
  5. package/bin/geneseed-cli.mjs +42 -84
  6. package/bin/geneseed-hook.mjs +5 -2
  7. package/docs/web/exclusions.md +1 -1
  8. package/docs/web/footprint.md +1 -1
  9. package/docs/web/foreman-mode.md +2 -2
  10. package/docs/web/themes-author.md +1 -1
  11. package/docs/web-ui.md +41 -29
  12. package/harness.config.json +1 -1
  13. package/js/README.md +1 -10
  14. package/js/build/emit-claude.mjs +4 -5
  15. package/js/build/emit-common.mjs +63 -114
  16. package/js/build/emit-opencode.mjs +74 -66
  17. package/js/build/generate.mjs +4 -6
  18. package/js/build/render.mjs +13 -13
  19. package/js/build/source.mjs +7 -6
  20. package/js/build/stubs.mjs +25 -33
  21. package/js/build/themes.mjs +1 -1
  22. package/js/build/version.mjs +1 -3
  23. package/js/hosts/hooks.mjs +3 -3
  24. package/js/hosts/hosts.mjs +2 -4
  25. package/js/hosts/installs.mjs +35 -28
  26. package/js/hosts/link.mjs +1 -1
  27. package/js/hosts/native.mjs +37 -19
  28. package/js/hosts/opencode.mjs +39 -28
  29. package/js/hosts/settings.mjs +33 -110
  30. package/js/inspect/checks-authoring.mjs +5 -4
  31. package/js/inspect/checks-build.mjs +1 -1
  32. package/js/inspect/diff.mjs +1 -3
  33. package/js/inspect/doctor.mjs +59 -44
  34. package/js/inspect/excludes.mjs +4 -10
  35. package/js/inspect/inventory.mjs +37 -55
  36. package/js/inspect/registry.mjs +4 -7
  37. package/js/inspect/scan.mjs +8 -15
  38. package/js/inspect/status.mjs +1 -1
  39. package/js/lib/fs.mjs +34 -0
  40. package/js/lib/json.mjs +28 -1
  41. package/js/lib/paths.mjs +0 -10
  42. package/js/lib/udiff.mjs +11 -16
  43. package/js/maintain/memory.mjs +2 -4
  44. package/js/maintain/migrate.mjs +3 -3
  45. package/js/maintain/uninstall.mjs +90 -78
  46. package/js/maintain/update.mjs +2 -2
  47. package/js/ui/anim.mjs +1 -1
  48. package/js/ui/menu.mjs +1 -1
  49. package/js/ui/tui.mjs +3 -20
  50. package/js/web/actions.mjs +230 -217
  51. package/js/web/activity.mjs +71 -89
  52. package/js/web/api.mjs +210 -220
  53. package/js/web/daemon.mjs +92 -110
  54. package/js/web/docs.mjs +66 -111
  55. package/js/web/graph.mjs +0 -0
  56. package/js/web/handler.mjs +78 -97
  57. package/js/web/jobs.mjs +124 -165
  58. package/js/web/routes.mjs +79 -136
  59. package/js/web/server.mjs +47 -82
  60. package/package.json +1 -1
  61. package/src/agents/explorer.md +2 -1
  62. package/src/doctrines/process.md +4 -2
  63. package/src/laws/universal.md +55 -43
  64. package/src/skills/clarify.md +1 -1
  65. package/src/skills/forge-mcp.md +2 -1
  66. package/src/skills/fresh-eyes.md +1 -1
  67. package/src/skills/ingest.md +2 -1
  68. package/src/skills/parallel-agents.md +1 -1
  69. package/src/skills/ponytail.md +2 -2
  70. package/src/skills/research.md +4 -2
  71. package/themes/_TEMPLATE.json +2 -0
  72. package/themes/biker.json +2 -0
  73. package/themes/commentator.json +2 -0
  74. package/themes/cyberpunk.json +2 -0
  75. package/themes/gamer.json +2 -0
  76. package/themes/imperial.json +2 -0
  77. package/themes/joker.json +2 -0
  78. package/themes/marvin.json +2 -0
  79. package/themes/mean.json +2 -0
  80. package/themes/military.json +2 -0
  81. package/themes/neutral.json +2 -0
  82. package/themes/pirate.json +2 -0
  83. package/themes/sports.json +2 -0
  84. package/themes/verstappen.json +2 -0
  85. package/themes/wizard.json +2 -0
  86. package/web/dist/assets/{Activity-z0uXdS7J.js → Activity-BTCG2gMl.js} +1 -1
  87. package/web/dist/assets/{ActivityDetail-CEj5IJpF.js → ActivityDetail-julsmPDB.js} +1 -1
  88. package/web/dist/assets/CatalogRow-DqS4e5XM.js +1 -0
  89. package/web/dist/assets/Diff-CIrGbiqm.js +5 -0
  90. package/web/dist/assets/Doctor-CpI53oHY.js +1 -0
  91. package/web/dist/assets/{Graph-BV6a6t2l.js → Graph-CS4tA7YS.js} +1 -1
  92. package/web/dist/assets/Harness-vRYs11kX.js +1 -0
  93. package/web/dist/assets/Laws-BjbHJrJt.js +3 -0
  94. package/web/dist/assets/Library-CjC7BAWN.js +1 -0
  95. package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-kgpg9EQM.js} +15 -15
  96. package/web/dist/assets/Profile-CHr-PPf6.js +1 -0
  97. package/web/dist/assets/{Rules-DobVPd80.js → Rules-CsC6hfqk.js} +1 -1
  98. package/web/dist/assets/Skills-BvatJGfe.js +1 -0
  99. package/web/dist/assets/{StatusBadge-BhJ3HhdI.js → StatusBadge-gRrak0nO.js} +1 -1
  100. package/web/dist/assets/{activity-D9mfv7pY.js → activity-B0-FXZCj.js} +1 -1
  101. package/web/dist/assets/index-BnRWQbZb.js +12 -0
  102. package/web/dist/assets/index-Bx3I7cRW.js +2 -0
  103. package/web/dist/assets/index-D0f9tJlA.css +1 -0
  104. package/web/dist/assets/index-DJOhOwwR.js +1 -0
  105. package/web/dist/index.html +2 -2
  106. package/web/src/pages/Laws.jsx +46 -89
  107. package/web/dist/assets/About-CPvIFZGh.js +0 -1
  108. package/web/dist/assets/Diff-BMR8fPuG.js +0 -5
  109. package/web/dist/assets/Doctor-Bz5qE4r9.js +0 -1
  110. package/web/dist/assets/Harnesses-C5Ag5PaQ.js +0 -1
  111. package/web/dist/assets/Laws-BK07NKw9.js +0 -3
  112. package/web/dist/assets/Library-CiXN9sC3.js +0 -1
  113. package/web/dist/assets/Markdown-DG6FjUz-.js +0 -1
  114. package/web/dist/assets/Profile-B2oSvdxH.js +0 -1
  115. package/web/dist/assets/Skills-DQPsvsn9.js +0 -1
  116. package/web/dist/assets/Themes-BP38FBU9.js +0 -2
  117. package/web/dist/assets/index-BE226Gv6.css +0 -1
  118. package/web/dist/assets/index-BZ7cPLMI.js +0 -1
  119. package/web/dist/assets/index-BgIs5eIa.js +0 -12
  120. package/web/dist/assets/index-DGM5j2hB.js +0 -2
@@ -56,6 +56,7 @@ import { cmdExclude } from '../js/inspect/excludes.mjs';
56
56
  import { cmdBuild, cmdPrompt, cmdRebuildAll, cmdTheme } from '../js/build/generate.mjs';
57
57
  import { cmdMigrate } from '../js/maintain/migrate.mjs';
58
58
  import { parseIntStrict } from '../js/lib/text.mjs';
59
+ import { printErr } from '../js/lib/fs.mjs';
59
60
  import { cmdLink, cmdUnlink } from '../js/hosts/link.mjs';
60
61
  import { cmdMcp } from '../js/hosts/mcp.mjs';
61
62
  import { cmdMemory } from '../js/maintain/memory.mjs';
@@ -83,56 +84,31 @@ import { cmdWeb } from '../js/web/server.mjs';
83
84
  * `tests/test_hook_cli_parity.py` scrapes `const VERBS = {` for its three matrix gates, and
84
85
  * `main`'s refusal lists `Object.keys(VERBS)` IN THIS ORDER, which `harness_golden` cells
85
86
  * assert verbatim — the order is the old table's and is not alphabetical.
87
+ *
88
+ * FLATTENED TO `verb: fn` since Task 5: every row was a single-field `{ fn: cmdX }` object,
89
+ * the one field this table has left after `positionals`/`options`/`flags`/etc moved to
90
+ * `js/cli-table.json` above. `main` reads `VERBS[verb]` directly rather than
91
+ * `VERBS[verb].fn` now; the scraping regexes in `tests/unit/{cli_table,hook_cli,docs}.test.mjs`
92
+ * match on the KEY and the colon only; the literal string `const VERBS = {` and the key
93
+ * order neither changed.
86
94
  */
87
95
  const VERBS = {
88
- exclude: {
89
- fn: cmdExclude,
90
- },
91
- status: {
92
- fn: cmdStatus,
93
- },
94
- version: {
95
- fn: cmdVersion,
96
- },
97
- build: {
98
- fn: cmdBuild,
99
- },
100
- prompt: {
101
- fn: cmdPrompt,
102
- },
103
- theme: {
104
- fn: cmdTheme,
105
- },
106
- diff: {
107
- fn: cmdDiff,
108
- },
109
- 'rebuild-all': {
110
- fn: cmdRebuildAll,
111
- },
112
- migrate: {
113
- fn: cmdMigrate,
114
- },
115
- doctor: {
116
- fn: cmdDoctor,
117
- },
118
- uninstall: {
119
- fn: cmdUninstall,
120
- },
121
- link: {
122
- fn: cmdLink,
123
- },
124
- unlink: {
125
- fn: cmdUnlink,
126
- },
127
- setup: {
128
- fn: cmdSetup,
129
- },
130
- web: {
131
- fn: cmdWeb,
132
- },
133
- upgrade: {
134
- fn: cmdUpgrade,
135
- },
96
+ exclude: cmdExclude,
97
+ status: cmdStatus,
98
+ version: cmdVersion,
99
+ build: cmdBuild,
100
+ prompt: cmdPrompt,
101
+ theme: cmdTheme,
102
+ diff: cmdDiff,
103
+ 'rebuild-all': cmdRebuildAll,
104
+ migrate: cmdMigrate,
105
+ doctor: cmdDoctor,
106
+ uninstall: cmdUninstall,
107
+ link: cmdLink,
108
+ unlink: cmdUnlink,
109
+ setup: cmdSetup,
110
+ web: cmdWeb,
111
+ upgrade: cmdUpgrade,
136
112
  // `up.add_parser(..., aliases=["update"])`, reproduced in P8c as a ROW OF ITS OWN rather than
137
113
  // as an `aliases` field on the row above — because a field would be a DECLARATION and the
138
114
  // three matrix gates read this table as the DISPATCH (rule 7, and M23 is where it was
@@ -144,28 +120,18 @@ const VERBS = {
144
120
  //
145
121
  // It is NOT what the web console's `update` ACTION needs: that row's argv names `upgrade`,
146
122
  // the subparser, and always did. The two were coupled only in a handoff's note.
147
- update: {
148
- fn: cmdUpgrade,
149
- },
150
- 'sync-self': {
151
- // Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
152
- // `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
153
- // about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
154
- // and `sync-self v1.2.3` differently. See `js/maintain/update.mjs`'s `syncSelf` for both halves.
155
- fn: cmdSyncSelf,
156
- },
157
- bootstrap: {
158
- fn: cmdBootstrap,
159
- },
123
+ update: cmdUpgrade,
124
+ // Points at `cmdSyncSelf` rather than at `cmdUpgrade`, and that is not tidiness:
125
+ // `sync-self` DROPS its `ref` before `upgrade` ever sees it, where `upgrade` both WARNS
126
+ // about one and re-reads it as a theme — so the two verbs answer `sync-self cyberpunk`
127
+ // and `sync-self v1.2.3` differently. See `js/maintain/update.mjs`'s `syncSelf` for both halves.
128
+ 'sync-self': cmdSyncSelf,
129
+ bootstrap: cmdBootstrap,
160
130
  // P7a. Both are DISPATCHERS whose off-TTY arm is the whole of what a cell can reach, and
161
131
  // `menu`'s on-TTY arm falls back rather than opening a panel — `js/ui/menu.mjs`'s header
162
132
  // argues both.
163
- menu: {
164
- fn: cmdMenu,
165
- },
166
- home: {
167
- fn: cmdHome,
168
- },
133
+ menu: cmdMenu,
134
+ home: cmdHome,
169
135
  // P7b, and it is the twenty-fifth and last. `cmd_tui`'s FIRST arm is `if not sys.stdin.
170
136
  // isatty()`, so off a TTY — which is every cell there is — the verb is one line and an
171
137
  // exit code, and that arm crosses byte for byte. The panel behind it is P7c's;
@@ -173,9 +139,7 @@ const VERBS = {
173
139
  // panel-unavailable line rather than inventing a second full-screen UI, and
174
140
  // `tests/test_tui_boundary.py` asserts that the arm it declares is genuinely unreachable
175
141
  // here rather than merely untested.
176
- tui: {
177
- fn: cmdTui,
178
- },
142
+ tui: cmdTui,
179
143
  // THE THREE THAT NEVER HAD A PYTHON ORIGINAL, and they are last for that reason rather
180
144
  // than by alphabet: every row above is the twin of a subparser, and these three are not.
181
145
  // The information behind them existed only behind the web console — the catalog endpoint,
@@ -190,21 +154,15 @@ const VERBS = {
190
154
  // ABSOLUTE — `tests/snapshot/cli_help.test.mjs` splits the two populations by name and refuses a
191
155
  // verb that falls into neither, and each has its own unit gate stating what it does rather
192
156
  // than that it agrees.
193
- catalog: {
194
- fn: cmdCatalog,
195
- },
196
- mcp: {
197
- fn: cmdMcp,
198
- },
199
- memory: {
200
- fn: cmdMemory,
201
- },
157
+ catalog: cmdCatalog,
158
+ mcp: cmdMcp,
159
+ memory: cmdMemory,
202
160
  };
203
161
 
204
162
  function die(code, msg) {
205
163
  // CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse writes
206
- // this line through `sys.stderr`, which does.
207
- process.stderr.write(`geneseed: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
164
+ // this line through `sys.stderr`, which does. `printErr` owns the `\n`-to-`os.linesep` rule.
165
+ printErr(`geneseed: error: ${msg}\n`);
208
166
  return code;
209
167
  }
210
168
 
@@ -358,8 +316,8 @@ async function main(argv) {
358
316
  throw e;
359
317
  }
360
318
  }
361
- const spec = VERBS[verb];
362
- if (!spec) {
319
+ const fn = VERBS[verb];
320
+ if (!fn) {
363
321
  // "and every other harness subcommand is still Python — run the interpreter against the
364
322
  // old harness script" is GONE, and its removal is not cosmetic: with the four hook verbs
365
323
  // named, the two entry points now cover every subcommand there is, so the clause was not
@@ -395,7 +353,7 @@ async function main(argv) {
395
353
  // it `process.exitCode` would be assigned a PENDING PROMISE, which Node coerces to 0
396
354
  // and prints nothing about. `web status` would then report "not running" and exit 0.
397
355
  // Awaiting a number is a no-op for the ten synchronous verbs.
398
- return await spec.fn(parsed.args);
356
+ return await fn(parsed.args);
399
357
  } catch (e) {
400
358
  // `e.exitCode` is the generator's existing marker for a DELIBERATE refusal that has
401
359
  // already explained itself on stderr — `js/build/emit-claude.mjs` reads the same flag, and it is
@@ -39,6 +39,9 @@
39
39
  */
40
40
  import { cmdContext, cmdGitGate, cmdRuleGate, cmdLearn } from '../js/hosts/hooks.mjs';
41
41
  import { printHelp } from '../js/ui/cli.mjs';
42
+ // Not a new module on the hot path: `js/hosts/hooks.mjs` above already imports
43
+ // `js/lib/fs.mjs`, so this edge names a file the process has already loaded.
44
+ import { printErr } from '../js/lib/fs.mjs';
42
45
 
43
46
  const VERBS = {
44
47
  context: { fn: cmdContext, flags: { '--root': 'root' } },
@@ -54,8 +57,8 @@ const VERBS = {
54
57
 
55
58
  function die(code, msg) {
56
59
  // CRLF on Windows, for the same reason `js/hosts/hooks.mjs`'s funnels translate: argparse
57
- // writes this line through `sys.stderr`, which does.
58
- process.stderr.write(`geneseed-hook: error: ${msg}${process.platform === 'win32' ? '\r\n' : '\n'}`);
60
+ // writes this line through `sys.stderr`, which does. `printErr` owns the `\n`-to-`os.linesep` rule.
61
+ printErr(`geneseed-hook: error: ${msg}\n`);
59
62
  return code;
60
63
  }
61
64
 
@@ -3,7 +3,7 @@ group: concepts
3
3
  order: 9
4
4
  title: "Sovereign repos (exclusions)"
5
5
  kind: "concept"
6
- link: {"hash": "#/harnesses", "label": "Manage in Harnesses →"}
6
+ link: {"hash": "#/harness", "label": "Manage in Harness →"}
7
7
  ---
8
8
  A **sovereign repo** is a folder where every GLOBAL harness install goes fully dormant: hooks stay silent and the global preamble (`AGENT.md`/`CLAUDE.md`) is never loaded there. A repo that ships its own agent config you don't want a global harness layered onto is the usual case.
9
9
 
@@ -29,7 +29,7 @@ Neither setting hides text from the agent; they hide it from *every turn*.
29
29
  It's set-and-forget — stored in the `.geneseed-footprint` marker, preserved across every rebuild, identical on every host (OpenCode, Claude Code, Bob, Copilot). Changing it re-emits the install.
30
30
 
31
31
  - **Settings** — the **Footprint** toggle flips the current install (full ⇄ lean) and rebuilds it in place.
32
- - **Harnesses tab** — a per-harness dropdown sets it for any one install independently, then **Apply**.
32
+ - **Harness tab** — on an active install, a per-harness dropdown sets it for that install independently, then **Apply**. On a host that is not installed yet, it is step 2 of **Install…**.
33
33
  - **Setup / re-theme wizard** — asks for footprint alongside voice, posture, mode, and doctrine packs.
34
34
  - **CLI** — `geneseed-build --footprint lean` (with any `--emit`).
35
35
 
@@ -3,7 +3,7 @@ group: concepts
3
3
  order: 12
4
4
  title: "Foreman mode"
5
5
  kind: "concept"
6
- link: {"hash": "#/harnesses", "label": "Set the mode in Harnesses →"}
6
+ link: {"hash": "#/harness", "label": "Set the mode in Harness →"}
7
7
  ---
8
8
  **Mode** is the session's *operating* register — how work gets executed — one of four build-time dials, beside posture (the *relationship* register), [doctrine packs](#/docs/setup-choices) (which practice rules bind) and footprint (how much of the constitution loads inline). Two modes ship, set at build time so the choice holds steady instead of drifting mid-session, and preserved across every rebuild and re-theme.
9
9
 
@@ -24,7 +24,7 @@ Because the crew runs apart from the conversation, the session stays responsive
24
24
 
25
25
  Set-and-forget, stored beside the install and preserved across rebuilds and re-themes:
26
26
 
27
- - **Harnesses tab** — the per-install **Mode** dropdown (beside Voice, Footprint, and Posture), then **Apply**.
27
+ - **Harness tab** — on an active install, the per-install **Mode** dropdown (beside Voice, Footprint, and Posture), then **Apply**. On a host that is not installed yet, the same four choices are steps inside **Install…**.
28
28
  - **Setup / re-theme wizard** — asks for mode alongside voice, posture, doctrine packs, and footprint.
29
29
  - **CLI** — `geneseed-build --mode foreman`; switch back with `--mode direct`.
30
30
 
@@ -3,7 +3,7 @@ group: extend
3
3
  order: 1
4
4
  title: "Write a new theme"
5
5
  kind: "concept"
6
- link: {"hash": "#/themes", "label": "Open the theme gallery →"}
6
+ link: {"hash": "#/harness", "label": "Open the voice gallery →"}
7
7
  ---
8
8
  A theme is one JSON file under `themes/` declaring voice tokens only: `BANNER`, `TAGLINE`, `LOADED_SIGIL`, `VOICE`, the core nouns (`ONTOLOGY`/`LAW(S)`/`DOCTRINE(S)`/`AGENT(S)`/`SKILL(S)`/`MEMORY`/`NOTEBOOK`/`WIKI`), the section intros `INTRO_*`, the epigraphs `EPI_*`, the `BENEDICTION`, the `ROAST_PERSONA`, and `DESC_*` blurbs. Copy `themes/neutral.json` and edit. `geneseed build --theme yours` renders it; `doctor` checks for missing tokens.
9
9
 
package/docs/web-ui.md CHANGED
@@ -50,12 +50,17 @@ The left rail mirrors the harness's own shape:
50
50
 
51
51
  | Group | Views |
52
52
  | --- | --- |
53
- | **🧬 Harness** | Dashboard · Activity · Constitution · Rules · Profile · Skills · Agents · Library · Docs · Graph |
54
- | **🔧 Maintain** | Changes · Doctor |
55
- | **🎨 Configure** | Harnesses · Themes · Settings |
56
- | **ℹ️ About** | About |
53
+ | *(ungrouped)* | Dashboard |
54
+ | **📖 Codex** | Constitution · Rules · Profile · Skills · Agents · Library · Docs · Graph |
55
+ | **🩺 Care** | Activity · Changes · Doctor |
56
+ | **🎨 Setup** | Harness · Settings |
57
57
 
58
- A global **Spotlight** search lives in the topbar — press <kbd>/</kbd> to focus it, type
58
+ Above the index sits a **vitals** card — the germination ring, drift, the doctor's verdict
59
+ and how long ago the harness was built — so those four facts are on screen from every page.
60
+ It is hidden where the rail collapses to icons (≤960px) and returns with the phone drawer.
61
+
62
+ A global **Spotlight** search lives in the topbar — press <kbd>/</kbd> to focus it (or
63
+ <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>K</kbd>, which also works from inside a text field), type
59
64
  to jump to any agent, skill, doc, spec, MCP server, or constitutional rule — the `laws`
60
65
  catalog it indexes is the whole constitution, so an ontology section, an invariant and a
61
66
  doctrine rule are all reachable by name; <kbd>↑</kbd>/<kbd>↓</kbd> to
@@ -129,32 +134,39 @@ streams the output of background jobs and keeps their history across reloads.
129
134
  theme, parity, links, authoring gates, drift), grouping any problems per check. Re-run
130
135
  on demand.
131
136
 
132
- ### 🎨 Configure
133
-
134
- - **Harnesses** (`#/harnesses`) — every detected install (host × scope), one row each,
135
- with per-row **Voice**, **Footprint**, **Posture**, and **Mode** pickers plus
136
- Install/Apply, an on/off switch, and remove. **Posture** is the relationship register
137
- (peer/mentor/expert/assistant/artisan — default **peer**); **Mode** is the operating
138
- register (direct/foreman — default **direct**; foreman triages tasks into isolated
139
- pipelines instead of working every one itself). Both are orthogonal to voice and to
140
- each other, picked here or via the setup wizard, and preserved across every rebuild.
141
- "Deploy to folder…" and "Rebuild all" apply to the whole set. An **Excluded folders**
142
- card (shown once a global install exists) manages sovereign repos — folders where every
143
- global harness goes dormant (hooks stay silent, the global preamble never loads);
144
- add or remove one here, the web mirror of `harness exclude add|remove|list`.
145
- - **Themes** (`#/themes`) — a gallery of the shipped voices (name, tagline, sigil, accent
146
- glow). **Apply** a voice to rebuild the deployed harness in that theme; the current one
147
- is marked.
137
+ ### 🎨 Setup
138
+
139
+ - **Harness** (`#/harness`) — this machine's own install first (path, voice, footprint,
140
+ and when it was built), then the **Voice** reference, then every detected install.
141
+ `#/harnesses` and `#/themes` both still resolve here, so old links and bookmarks keep
142
+ working.
143
+ - The **Voice** card is the former Themes tab, kept as a REFERENCE rather than a second
144
+ control: a collapsed row that opens to every shipped voice with its tagline, the
145
+ deployed one pinned first and marked. It is the one place you can read what a voice
146
+ sounds like — the per-install picker below is a list of bare names. Applying happens
147
+ there, per install, beside that install's own footprint, posture and mode; a gallery at
148
+ the top of the page could only ever act on one of them.
149
+ - **Every install** lists each detected install (host × scope), one row each. An
150
+ ACTIVE row carries per-row **Voice**, **Footprint**, **Posture** and **Mode**
151
+ pickers plus **Apply**, an on/off switch, and remove; a host that is NOT installed
152
+ yet shows a single **Install…** that discloses the same four choices as steps.
153
+ **Posture** is the relationship register (peer/mentor/expert/assistant/artisan —
154
+ default **peer**); **Mode** is the operating register (direct/foreman — default
155
+ **direct**; foreman triages tasks into isolated pipelines instead of working every
156
+ one itself). Both are orthogonal to voice and to each other, picked here or via the
157
+ setup wizard, and preserved across every rebuild.
158
+ - "Deploy to folder…" and "Rebuild all" apply to the whole set. An **Excluded folders**
159
+ card (shown once a global install exists) manages sovereign repos — folders where every
160
+ global harness goes dormant (hooks stay silent, the global preamble never loads);
161
+ add or remove one here, the web mirror of `harness exclude add|remove|list`.
148
162
  - **Settings** (`#/settings`) — a **Console direction** picker (the visual flavour of the
149
163
  console) with a **Dashboard layout** control (Auto follows each theme's designed Status
150
- lens; Cultivar / Greenhouse / Operator force one regardless of skin), the install snapshot
151
- (deployed/target/theme/version/memory store), a Maintenance card (PATH link/unlink,
152
- uninstall), an offline package download, and server controls (Stop). Build, update, and MCP
153
- wiring live in the Harnesses tab.
154
-
155
- ### ℹ️ About
156
-
157
- - **About** (`#/about`) — project + creator credits and the source link.
164
+ lens; Cultivar / Greenhouse / Operator / Journal force one regardless of skin), the
165
+ install snapshot (deployed/target/theme/version/memory store), a Maintenance card (PATH
166
+ link/unlink, uninstall), an offline package download, server controls (Stop), and the
167
+ **About** colophon — project + creator credits and the source link, folded in from the
168
+ retired `#/about` tab (which still resolves here). Build, update, and MCP wiring live in
169
+ the Harness tab.
158
170
 
159
171
  ## Rebuilding the UI
160
172
 
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "3.1.2"
5
+ "version": "3.2.0"
6
6
  }
package/js/README.md CHANGED
@@ -172,7 +172,7 @@ the deployed harness as JSON.
172
172
  | `activity.mjs` | The `activity/` session files: pid-liveness prune, list, detail, and the on/off flag |
173
173
 
174
174
  **Before editing:** `makeHandler(state, jm, token, dist, holder)`'s arity is load-bearing — three
175
- test call sites construct one directly and drive it without a socket. Four of `routes.mjs`'s
175
+ test call sites construct one directly and drive it without a socket. Five of `routes.mjs`'s
176
176
  exported sets are deliberately EMPTY and must stay declared: each is half of a partition test.
177
177
  `graph.mjs` contains a literal NUL byte, so `grep`/`ripgrep` treat it as binary and skip it.
178
178
 
@@ -192,12 +192,3 @@ exported sets are deliberately EMPTY and must stay declared: each is half of a p
192
192
  | a console doc page | `docs/web/<id>.md` — the stem is the id | `docs/extending.md` §4c. Only `kind: "concept"` gets `{N_*}` substitution |
193
193
  | an OpenCode plugin | `adapters/opencode/plugins/geneseed-<name>.js` | `docs/extending.md` §4a — the `geneseed-` prefix is mechanically load-bearing |
194
194
  | to change a `js/lib/` primitive | `tests/fixtures/pure_probe.mjs` **first** | the docblock says which Node default it deliberately departs from — change that sentence too, or the next reader restores the default |
195
-
196
- ---
197
-
198
- ## Known debris
199
-
200
- * **`js/hosts/settings.mjs` carries a private second copy of Python's whitespace class**, duplicating
201
- `WHITESPACE` in `js/lib/text.mjs`. `text.mjs` is the owner.
202
- * **`web/src/pages/Skills.jsx`'s `SKILL_CATS` is a fourth, ungated copy of the skill taxonomy** —
203
- nothing compares it to `SKILL_CLASS`, and an unknown category renders as Build with no warning.
@@ -54,8 +54,7 @@ export function emitClaudeRender(cfg, job) {
54
54
 
55
55
  // `os.path.relpath(cfg, claude_md.parent)` — note the reversed argument order, and
56
56
  // that Python answers '.' for an identical pair where `path.relative` answers ''.
57
- const relCfg = path.relative(path.dirname(claudeMd), cfgDir).split(path.sep).join('/')
58
- || '.';
57
+ const relCfg = relPosix(path.dirname(claudeMd), cfgDir) || '.';
59
58
  const lawsPrefix = relCfg === '.' ? '' : `${relCfg}/`;
60
59
  const { theme, items } = renderAll(cfg, themeName, { footprint, lawsPrefix, nativeCatalog });
61
60
  assertSourceComplete(cfg, `claude-${scope}`);
@@ -98,7 +97,7 @@ export function emitClaudeRender(cfg, job) {
98
97
  const rulesMd = path.join(cfgDir, 'rules', 'geneseed.md');
99
98
  mkdirSync(path.dirname(rulesMd), { recursive: true });
100
99
  writeText(rulesMd, scope === 'project' ? BOB_RULES_STUB
101
- : stripCapabilityLinks(cfg, prefixedAgentText('../') || agentText));
100
+ : stripCapabilityLinks(prefixedAgentText('../') || agentText));
102
101
  owned.push('rules/geneseed.md');
103
102
  }
104
103
 
@@ -146,7 +145,7 @@ export function emitClaudeRender(cfg, job) {
146
145
  // consumes and no more.
147
146
  let claudeMdText = null;
148
147
  if (agentText !== null && !(isBob && scope === 'global')) {
149
- claudeMdText = stripCapabilityLinks(cfg, prefixedAgentText(lawsPrefix) || agentText);
148
+ claudeMdText = stripCapabilityLinks(prefixedAgentText(lawsPrefix) || agentText);
150
149
  }
151
150
 
152
151
  // WIRE — see `claudeWire` below. One child per emit, so the two halves are two
@@ -212,7 +211,7 @@ function claudeWire(job, claudeMdText, hasAgentText, doctrines = null, excludeRu
212
211
  // above — unreachable here (CLAUDE.md is a file, cfgDir a directory, so the pair is
213
212
  // never identical) and spelled anyway, because the two call sites are 400 lines apart
214
213
  // and only one of them is obviously safe.
215
- const rel = path.relative(cfgDir, claudeMd).split(path.sep).join('/') || '.';
214
+ const rel = relPosix(cfgDir, claudeMd) || '.';
216
215
  managed.claude_md = { rel };
217
216
  } else if (isBob && scope === 'global' && isTruthy(get(old, 'claude_md'))) {
218
217
  const oldCm = isDict(get(old, 'claude_md')) ? get(old, 'claude_md') : {};