geneseed 3.6.0 → 3.8.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 +74 -0
  2. package/DESIGN.md +6 -6
  3. package/QUICKSTART.md +1 -1
  4. package/README.md +33 -31
  5. package/SETUP.md +36 -10
  6. package/SHIPPED.md +3 -3
  7. package/adapters/claude-code/skills/geneseed/SKILL.md +1 -1
  8. package/adapters/openclaude/README.md +64 -0
  9. package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
  10. package/adapters/opencode/workflows/_runtime.js +1 -1
  11. package/docs/extending.md +18 -6
  12. package/docs/web/exclusions.md +1 -0
  13. package/docs/web/footprint.md +1 -1
  14. package/docs/web/harnesses.md +14 -14
  15. package/docs/web/install-quick.md +1 -1
  16. package/docs/web/rules.md +1 -1
  17. package/docs/web/themes-author.md +1 -1
  18. package/docs/web-ui.md +1 -1
  19. package/harness.config.json +1 -1
  20. package/js/README.md +3 -3
  21. package/js/build/driver.mjs +64 -14
  22. package/js/build/emit-claude.mjs +1 -1
  23. package/js/build/generate.mjs +92 -42
  24. package/js/build/source.mjs +5 -4
  25. package/js/cli-table.json +151 -2
  26. package/js/hosts/hooks.mjs +11 -4
  27. package/js/hosts/hosts.mjs +17 -2
  28. package/js/hosts/installs.mjs +32 -2
  29. package/js/hosts/mcp.mjs +8 -1
  30. package/js/hosts/settings.mjs +4 -1
  31. package/js/inspect/diff.mjs +1 -1
  32. package/js/inspect/doctor.mjs +3 -3
  33. package/js/inspect/excludes.mjs +7 -5
  34. package/js/inspect/status.mjs +44 -8
  35. package/js/maintain/migrate.mjs +1 -1
  36. package/js/maintain/setup.mjs +7 -2
  37. package/js/maintain/uninstall.mjs +3 -3
  38. package/js/web/actions.mjs +7 -5
  39. package/js/web/api.mjs +2 -2
  40. package/js/web/docs.mjs +1 -1
  41. package/package.json +2 -1
  42. package/src/doctrines/README.md +3 -2
  43. package/src/doctrines/comms.md +53 -0
  44. package/src/doctrines/process.md +0 -26
  45. package/src/skills/geneseed.md +9 -8
  46. package/src/skills/parallel-agents.md +1 -1
  47. package/src/skills/pipeline.md +1 -1
  48. package/src/skills/token-report/SKILL.md +2 -1
  49. package/src/skills/token-report/VENDOR.md +3 -3
  50. package/src/skills/token-report/scripts/token_report.mjs +14 -7
  51. package/src/skills/workflow.md +2 -2
  52. package/themes/_TEMPLATE.json +5 -3
  53. package/themes/biker.json +5 -3
  54. package/themes/commentator.json +5 -3
  55. package/themes/cyberpunk.json +5 -3
  56. package/themes/gamer.json +5 -3
  57. package/themes/imperial.json +5 -3
  58. package/themes/joker.json +5 -3
  59. package/themes/marvin.json +5 -3
  60. package/themes/mean.json +5 -3
  61. package/themes/military.json +5 -3
  62. package/themes/neutral.json +5 -3
  63. package/themes/pirate.json +5 -3
  64. package/themes/sports.json +5 -3
  65. package/themes/verstappen.json +5 -3
  66. package/themes/wizard.json +5 -3
  67. package/web/dist/assets/{Activity-Flgf4kHy.js → Activity-CH-7cPIM.js} +1 -1
  68. package/web/dist/assets/{ActivityDetail-DIpr85nh.js → ActivityDetail-DZvz0xS5.js} +1 -1
  69. package/web/dist/assets/{CatalogRow-CjyLjaLb.js → CatalogRow-DftXpZHY.js} +1 -1
  70. package/web/dist/assets/{Diff-Deisdha8.js → Diff-DvwdG0L9.js} +1 -1
  71. package/web/dist/assets/{Docs-DGUdY4l4.js → Docs-DrJNDPwI.js} +1 -1
  72. package/web/dist/assets/{Doctor-F74lJ69P.js → Doctor-OTmwmYZg.js} +1 -1
  73. package/web/dist/assets/{GreenhouseView-D072lBiD.js → GreenhouseView-XxrAdriX.js} +1 -1
  74. package/web/dist/assets/Harness-C_LYNk5c.js +1 -0
  75. package/web/dist/assets/{JournalView-DWFqZ0Iw.js → JournalView-BcuyeEaR.js} +1 -1
  76. package/web/dist/assets/{Laws-BN6g3HU7.js → Laws-CWHrTKY4.js} +1 -1
  77. package/web/dist/assets/{Library-CDOv4emM.js → Library-CA9IHghn.js} +1 -1
  78. package/web/dist/assets/{OperatorHudView-CwNIUVX8.js → OperatorHudView-BMRY1Tt4.js} +1 -1
  79. package/web/dist/assets/{Profile-CpP9BTne.js → Profile-wceET1JY.js} +1 -1
  80. package/web/dist/assets/{Rules-DFXsUCB7.js → Rules-BEXvY4Tx.js} +1 -1
  81. package/web/dist/assets/{Settings-BqZVUtJ5.js → Settings-DS2zF0Ez.js} +1 -1
  82. package/web/dist/assets/{Skills-Cu6kBgfF.js → Skills-DahlOJuI.js} +1 -1
  83. package/web/dist/assets/{drawable-BgfrEBer.js → drawable-BTHjTzF7.js} +1 -1
  84. package/web/dist/assets/{index-CAqrWhKl.js → index-AN5Emnrg.js} +2 -2
  85. package/web/dist/assets/{lawCats-BLgxl151.js → lawCats-Btjp3Hk_.js} +1 -1
  86. package/web/dist/index.html +1 -1
  87. package/web/src/pages/Laws.jsx +6 -2
  88. package/web/dist/assets/Harness-DNRP2Va7.js +0 -1
package/js/cli-table.json CHANGED
@@ -54,7 +54,7 @@
54
54
  },
55
55
  {
56
56
  "name": "build",
57
- "help": "render src/ -> Harness/",
57
+ "help": "render the harness (same flags as geneseed-build; an omitted flag takes the generator default, not the install's — see geneseed status --json for an install's own; dry run: geneseed validate)",
58
58
  "description": "",
59
59
  "positionals": [],
60
60
  "options": [
@@ -72,6 +72,141 @@
72
72
  "is_flag": false,
73
73
  "type": null,
74
74
  "hidden": false
75
+ },
76
+ {
77
+ "names": [
78
+ "--emit"
79
+ ],
80
+ "dest": "emit",
81
+ "metavar": null,
82
+ "help": "",
83
+ "choices": null,
84
+ "default": null,
85
+ "required": false,
86
+ "nargs": null,
87
+ "is_flag": false,
88
+ "type": null,
89
+ "hidden": false
90
+ },
91
+ {
92
+ "names": [
93
+ "--footprint"
94
+ ],
95
+ "dest": "footprint",
96
+ "metavar": null,
97
+ "help": "",
98
+ "choices": null,
99
+ "default": null,
100
+ "required": false,
101
+ "nargs": null,
102
+ "is_flag": false,
103
+ "type": null,
104
+ "hidden": false
105
+ },
106
+ {
107
+ "names": [
108
+ "--posture"
109
+ ],
110
+ "dest": "posture",
111
+ "metavar": null,
112
+ "help": "",
113
+ "choices": null,
114
+ "default": null,
115
+ "required": false,
116
+ "nargs": null,
117
+ "is_flag": false,
118
+ "type": null,
119
+ "hidden": false
120
+ },
121
+ {
122
+ "names": [
123
+ "--mode"
124
+ ],
125
+ "dest": "mode",
126
+ "metavar": null,
127
+ "help": "",
128
+ "choices": null,
129
+ "default": null,
130
+ "required": false,
131
+ "nargs": null,
132
+ "is_flag": false,
133
+ "type": null,
134
+ "hidden": false
135
+ },
136
+ {
137
+ "names": [
138
+ "--doctrines"
139
+ ],
140
+ "dest": "doctrines",
141
+ "metavar": null,
142
+ "help": "",
143
+ "choices": null,
144
+ "default": null,
145
+ "required": false,
146
+ "nargs": null,
147
+ "is_flag": false,
148
+ "type": null,
149
+ "hidden": false
150
+ },
151
+ {
152
+ "names": [
153
+ "--exclude-rules"
154
+ ],
155
+ "dest": "exclude_rules",
156
+ "metavar": null,
157
+ "help": "",
158
+ "choices": null,
159
+ "default": null,
160
+ "required": false,
161
+ "nargs": null,
162
+ "is_flag": false,
163
+ "type": null,
164
+ "hidden": false
165
+ },
166
+ {
167
+ "names": [
168
+ "--out"
169
+ ],
170
+ "dest": "out",
171
+ "metavar": null,
172
+ "help": "",
173
+ "choices": null,
174
+ "default": null,
175
+ "required": false,
176
+ "nargs": null,
177
+ "is_flag": false,
178
+ "type": null,
179
+ "hidden": false
180
+ },
181
+ {
182
+ "names": [
183
+ "--root"
184
+ ],
185
+ "dest": "root",
186
+ "metavar": null,
187
+ "help": "",
188
+ "choices": null,
189
+ "default": null,
190
+ "required": false,
191
+ "nargs": null,
192
+ "is_flag": false,
193
+ "type": null,
194
+ "hidden": false
195
+ },
196
+ {
197
+ "names": [
198
+ "--config-dir"
199
+ ],
200
+ "dest": "config_dir",
201
+ "metavar": null,
202
+ "help": "the config dir a -global emit writes to (default: the host's resolved one)",
203
+ "choices": null,
204
+ "default": null,
205
+ "required": false,
206
+ "nargs": null,
207
+ "is_flag": false,
208
+ "type": null,
209
+ "hidden": false
75
210
  }
76
211
  ],
77
212
  "mutex": []
@@ -681,7 +816,21 @@
681
816
  "help": "print the install dashboard as text (theme, mode, counts, memory, version)",
682
817
  "description": "",
683
818
  "positionals": [],
684
- "options": [],
819
+ "options": [
820
+ {
821
+ "names": ["--json"],
822
+ "dest": "json",
823
+ "metavar": null,
824
+ "help": "print the status payload as JSON — every install with its settings and the geneseed build command that rebuilds it",
825
+ "choices": null,
826
+ "default": false,
827
+ "required": false,
828
+ "nargs": "0",
829
+ "is_flag": true,
830
+ "type": null,
831
+ "hidden": false
832
+ }
833
+ ],
685
834
  "mutex": []
686
835
  },
687
836
  {
@@ -13,7 +13,8 @@
13
13
  * exactly where Claude asks, in Copilot's envelope. `--host bob` is Bob's own
14
14
  * protocol: PreToolUse ignores stdout and refuses only on EXIT CODE 2, so the two Laws
15
15
  * exit 2 with the reason on stderr and the rest is a stderr line with exit 0; SessionStart
16
- * context is plain stdout, as on Claude. Since P5b they are also what the emitted hooks name: `bin/build-driver.mjs` bakes
16
+ * context is plain stdout, as on Claude. `--host openclaude` speaks Claude's dialect verbatim
17
+ * (OpenClaude is a Claude Code fork); the flag only picks which root file counts as native. Since P5b they are also what the emitted hooks name: `bin/build-driver.mjs` bakes
17
18
  * `<node> <checkout>/bin/geneseed-hook.mjs` into the machine-wide shim, so an install this
18
19
  * driver emits has no Python in its hook path at all — which is what let the interpreter
19
20
  * discovery and its exit-4 refusal be deleted rather than merely bypassed.
@@ -223,7 +224,12 @@ const EXCLUDE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor',
223
224
  // host had its own root in EAGER_ROOT. Other tools' roots stay eager (a repo carrying a
224
225
  // hand-written AGENTS.md is worth showing Claude Code); only the host's OWN is dropped.
225
226
  // The OpenCode context plugin carries the same rule for AGENT.md/AGENTS.md/CLAUDE.md.
226
- const NATIVE_ROOT = { claude: ['CLAUDE.md'], bob: ['AGENTS.md'], copilot: ['AGENTS.md'] };
227
+ // OpenClaude's root file is AGENTS.md, or CLAUDE.md when AGENTS.md is absent — resolved
228
+ // per repo in `discoverContext`, since which one it loads depends on what is on disk.
229
+ const NATIVE_ROOT = {
230
+ claude: ['CLAUDE.md'], bob: ['AGENTS.md'], copilot: ['AGENTS.md'],
231
+ openclaude: ['AGENTS.md', 'CLAUDE.md'],
232
+ };
227
233
 
228
234
  // Eager injection budget: a root README of 40k chars was going out whole, every session and
229
235
  // every compaction, and cost more than the harness itself. Per-file cut at the last line
@@ -231,7 +237,7 @@ const NATIVE_ROOT = { claude: ['CLAUDE.md'], bob: ['AGENTS.md'], copilot: ['AGEN
231
237
  const EAGER_FILE_BYTES = 16 * 1024;
232
238
  const EAGER_TOTAL_BYTES = 48 * 1024;
233
239
 
234
- const CLAUDE_MARKERS = ['.claude', '.bob'];
240
+ const CLAUDE_MARKERS = ['.claude', '.bob', '.openclaude'];
235
241
  const GENESEED_MANIFEST = '.geneseed-manifest.json';
236
242
 
237
243
  /**
@@ -302,7 +308,8 @@ function rglobMd(dir, acc = []) {
302
308
  export function discoverContext(root, host = HOST) {
303
309
  const eager = new Map();
304
310
  const lazy = new Map();
305
- const native = NATIVE_ROOT[host] || [];
311
+ const native = host === 'openclaude' && isFile(path.join(root, 'AGENTS.md'))
312
+ ? ['AGENTS.md'] : NATIVE_ROOT[host] || [];
306
313
  for (const full of sortPaths(listDir(root).map((n) => path.join(root, n)))) {
307
314
  if (!isFile(full)) continue;
308
315
  const name = path.basename(full);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The four host config dirs, and the two path primitives they are built on.
2
+ * The five host config dirs, and the two path primitives they are built on.
3
3
  *
4
4
  * EXTRACTED IN P5c, and the reason is arithmetic rather than taste. `bin/build-driver.mjs`
5
5
  * owned these; `bin/geneseed-cli.mjs` needs the same four to find a global install, and a
@@ -208,9 +208,22 @@ export function bobConfigDir() {
208
208
  return resolvePath(path.join(os.homedir(), '.bob'));
209
209
  }
210
210
 
211
+ /**
212
+ * OpenClaude (`@gitlawb/openclaude`) — `~/.openclaude`, relocatable via `$OPENCLAUDE_CONFIG_DIR`.
213
+ *
214
+ * Unlike Claude's, this env branch is the HOST'S OWN documented variable, not a Geneseed knob:
215
+ * OpenClaude reads it in `src/utils/envUtils.ts` and never falls back to `~/.claude` or
216
+ * `$CLAUDE_CONFIG_DIR`, so honouring it is what keeps the two CLIs agreeing on the target.
217
+ */
218
+ export function openclaudeConfigDir() {
219
+ const env = process.env.OPENCLAUDE_CONFIG_DIR;
220
+ if (env) return resolvePath(env);
221
+ return resolvePath(path.join(os.homedir(), '.openclaude'));
222
+ }
223
+
211
224
  /**
212
225
  * `_build_global.HOSTS`, reduced to the two columns a non-emitting caller needs, and IN ITS
213
- * ORDER — opencode, claude, bob, copilot.
226
+ * ORDER — opencode, claude, bob, copilot, openclaude.
214
227
  *
215
228
  * An array rather than an object because the order is observable output, not an
216
229
  * implementation detail: `harness exclude add` walks it and prints one message per host, so
@@ -235,6 +248,7 @@ export const HOSTS = [
235
248
  { host: 'claude', configDir: claudeConfigDir, projectMarker: '.claude', agentFile: 'CLAUDE.md' },
236
249
  { host: 'bob', configDir: bobConfigDir, projectMarker: '.bob', agentFile: 'AGENTS.md' },
237
250
  { host: 'copilot', configDir: copilotConfigDir, projectMarker: '.github', agentFile: 'AGENTS.md' },
251
+ { host: 'openclaude', configDir: openclaudeConfigDir, projectMarker: '.openclaude', agentFile: 'CLAUDE.md' },
238
252
  ];
239
253
 
240
254
  /**
@@ -265,6 +279,7 @@ const NATIVE_CATALOG = {
265
279
  claude: { skills: true, agents: true },
266
280
  bob: { skills: true, agents: false },
267
281
  copilot: { skills: false, agents: false },
282
+ openclaude: { skills: true, agents: true },
268
283
  };
269
284
 
270
285
  /** `{ skills, agents }` for a known host, `false` for an unknown one. */
@@ -234,6 +234,24 @@ export const modeOfDir = (d) => leadOfDir(d, discoverNames('modes', 'direct'));
234
234
  /** The `Active packs:` marker line the template emits, in one of the five carriers. */
235
235
  const ACTIVE_PACKS_RE = /^Active packs:[ \t]*(.+?)[ \t]*$/m;
236
236
 
237
+ /**
238
+ * A carrier rendered BEFORE the comms pack existed — the address migration's one detector.
239
+ *
240
+ * The comms pack took `process 7` (reference codes) out of process, and `process 8` (one
241
+ * writer per file) was renumbered to `process 7`. An install built before that names
242
+ * `process` in its marker and not `comms`, and so does a later install whose owner turned
243
+ * comms OFF — the marker alone cannot tell them apart. The carrier text can: a pre-comms
244
+ * process pack had eight rules, so wherever it was active the carrier renders a
245
+ * `process 8` heading, and wherever that rule was excluded the `Excluded rules:` line names
246
+ * it. No current carrier can say `process 8` — the pack has seven rules and nothing cites
247
+ * an eighth.
248
+ *
249
+ * ponytail: text sniff, not a stamp. It holds only while process has seven rules; the day
250
+ * process gets an eighth, this must move to a build stamp (or be retired, once no install
251
+ * built before the migration can remain).
252
+ */
253
+ const legacyProcessCarrier = (text) => /\bprocess 8\b/.test(text);
254
+
237
255
  /**
238
256
  * The doctrine packs a DEPLOYED install carries — the third register read back off disk,
239
257
  * beside `postureOfDir` and `modeOfDir`, and the first that is a SET.
@@ -274,6 +292,12 @@ export function doctrinesOfDir(d) {
274
292
  if (m[1] === 'none') return [];
275
293
  const named = m[1].split(',').map((s) => s.trim());
276
294
  if (!named.every((n) => PACK_ORDER.includes(n))) return null;
295
+ // MIGRATION: a pre-comms install that carried process carried the codes rule inside it,
296
+ // so it keeps that rule by gaining comms. Its old `process 7` exclusion, if any, is
297
+ // re-addressed to `comms 1` by `excludedRulesOfDir` in the same pass.
298
+ if (named.includes('process') && !named.includes('comms') && legacyProcessCarrier(text)) {
299
+ named.push('comms');
300
+ }
277
301
  return PACK_ORDER.filter((pk) => named.includes(pk));
278
302
  });
279
303
  }
@@ -336,7 +360,12 @@ export function excludedRulesOfDir(d) {
336
360
  return parts.length === 2 && PACK_ORDER.includes(parts[0]) && /^\d+$/.test(parts[1])
337
361
  ? `${parts[0]}.${Number(parts[1])}` : null;
338
362
  });
339
- return ids.every(Boolean) ? [...new Set(ids)].sort() : [];
363
+ if (!ids.every(Boolean)) return [];
364
+ // MIGRATION: the same pre-comms carrier spells the old addresses — see
365
+ // `legacyProcessCarrier`. One map, applied once, so `process 8` cannot chain onward.
366
+ const MOVED = { 'process.7': 'comms.1', 'process.8': 'process.7' };
367
+ const out = legacyProcessCarrier(text) ? ids.map((id) => MOVED[id] ?? id) : ids;
368
+ return [...new Set(out)].sort();
340
369
  }, []);
341
370
  }
342
371
 
@@ -364,6 +393,7 @@ export const EMIT_HOST_SCOPE = new Map([
364
393
  ['claude', ['claude', 'project']], ['claude-global', ['claude', 'global']],
365
394
  ['bob', ['bob', 'project']], ['bob-global', ['bob', 'global']],
366
395
  ['copilot', ['copilot', 'project']], ['copilot-global', ['copilot', 'global']],
396
+ ['openclaude', ['openclaude', 'project']], ['openclaude-global', ['openclaude', 'global']],
367
397
  ]);
368
398
 
369
399
  /** `_harness_mcp._emit_host_scope_of` — `root`'s own marker, resolved, or null. */
@@ -405,7 +435,7 @@ export function installKind(root) {
405
435
 
406
436
  /** `_harness_mcp._install_state` — 'active' | 'disabled' | 'absent'. */
407
437
  export function installState(root, host = 'opencode', scope = 'global') {
408
- if (['claude', 'bob', 'copilot'].includes(host)) return claudeState(root, scope, host);
438
+ if (['claude', 'bob', 'copilot', 'openclaude'].includes(host)) return claudeState(root, scope, host);
409
439
  if (isDir(path.join(root, DISABLED_STASH))) return 'disabled';
410
440
  return installKind(root) !== null ? 'active' : 'absent';
411
441
  }
package/js/hosts/mcp.mjs CHANGED
@@ -44,7 +44,7 @@ import { opencodeTarget, readJsonc } from './settings.mjs';
44
44
  const dget = (obj, key, dflt) => (isDict(obj) && Object.hasOwn(obj, key) ? obj[key] : dflt);
45
45
 
46
46
  /** The hosts whose MCP config is strict JSON under `mcpServers`, with no `enabled` flag. */
47
- const FLAGLESS = ['claude', 'bob', 'copilot'];
47
+ const FLAGLESS = ['claude', 'bob', 'copilot', 'openclaude'];
48
48
 
49
49
  /**
50
50
  * `_harness_mcp._MCP_PRESETS` — the ready-to-wire servers the MCP screen can toggle.
@@ -248,6 +248,13 @@ export function mcpConfigFor(host, scope, root) {
248
248
  // file is documented, so a project install carries no MCP wiring.
249
249
  return scope === 'global' ? path.join(copilotConfigDir(), 'mcp-config.json') : null;
250
250
  }
251
+ if (host === 'openclaude') {
252
+ // Claude's `.mcp.json` per repo; globally `.openclaude.json` beside the home dir, or
253
+ // INSIDE `$OPENCLAUDE_CONFIG_DIR` when set — OpenClaude's own `getGlobalClaudeFile` rule.
254
+ if (scope === 'project') return path.join(root, '.mcp.json');
255
+ const env = process.env.OPENCLAUDE_CONFIG_DIR;
256
+ return resolvePath(path.join(env || os.homedir(), '.openclaude.json'));
257
+ }
251
258
  return null;
252
259
  }
253
260
 
@@ -716,7 +716,10 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
716
716
  preCompact: entry(`${run} learn ${mem} || exit 0`),
717
717
  };
718
718
  }
719
- const context = `${run} context --root "${cfg}" || exit 0`;
719
+ // OpenClaude takes Claude's whole group set and verdicts unchanged; only `context` needs
720
+ // the host, to know which root file OpenClaude already loads by itself.
721
+ const h = host === 'openclaude' ? ' --host openclaude' : '';
722
+ const context = `${run} context --root "${cfg}"${h} || exit 0`;
720
723
  const gate = `${run} git-gate --root "${cfg}"${consentRuleOn(doctrines, excluded) ? '' : ' --no-consent'}`;
721
724
  const ruleGate = `${run} rule-gate --root "${cfg}"`;
722
725
  const learn = `${run} learn ${mem} || exit 0`;
@@ -349,7 +349,7 @@ export function cmdDiff(args) {
349
349
  + 'installs against a fresh render (a project layer is diffed against '
350
350
  + 'its own repo, which this command does not do). Pass --target at a '
351
351
  + 'global config dir (opencode-global/claude-global/bob-global/'
352
- + 'copilot-global), or omit '
352
+ + 'copilot-global/openclaude-global), or omit '
353
353
  + '--target to use the default.\n');
354
354
  } else {
355
355
  printErr(`[diff] no global Geneseed install at ${target} (no ${GLOBAL_MANIFEST}). `
@@ -134,7 +134,7 @@ export function globalEmitProblems(themeName) {
134
134
  }
135
135
 
136
136
  /**
137
- * `_harness_build._claude_bob_emit_problems` — validate the claude/bob/copilot PER-REPO
137
+ * `_harness_build._claude_bob_emit_problems` — validate the claude/bob/copilot/openclaude PER-REPO
138
138
  * emits, which were never checked before.
139
139
  *
140
140
  * That is exactly why the CLAUDE.md/AGENTS.md skill-table dead links shipped unnoticed: the
@@ -143,7 +143,7 @@ export function globalEmitProblems(themeName) {
143
143
  * skills one level deeper than a bundle does.
144
144
  */
145
145
  export function claudeBobEmitProblems(themeName) {
146
- return emitProblemsOver(['claude', 'bob', 'copilot'], (tmp, label) => {
146
+ return emitProblemsOver(['claude', 'bob', 'copilot', 'openclaude'], (tmp, label) => {
147
147
  const root = path.join(tmp, 'root');
148
148
  return {
149
149
  label: `${themeName} ${label}`,
@@ -211,7 +211,7 @@ export function doctorCollect({
211
211
  problems = problems.concat(ran('global', `Global install (${themeName})`,
212
212
  globalEmitProblems(themeName)));
213
213
  problems = problems.concat(ran('claude_bob',
214
- `Claude/Bob/Copilot per-repo emit (${themeName})`, claudeBobEmitProblems(themeName)));
214
+ `Claude/Bob/Copilot/OpenClaude per-repo emit (${themeName})`, claudeBobEmitProblems(themeName)));
215
215
  }
216
216
  });
217
217
  problems = problems.concat(ran('parity', 'Theme parity', themeParityProblems()));
@@ -28,6 +28,10 @@ import { readText, writeText, printOut, printErr, isDir, isFile, isOsError } fro
28
28
  import { parseJson, jsonDumpsIndent, get, isDict } from '../lib/json.mjs';
29
29
  import { normcase, toPlatformPath } from '../lib/paths.mjs';
30
30
 
31
+ /** The per-repo settings file a claudeMdExcludes entry goes in (Claude and OpenClaude). */
32
+ const localSettings = (repo, host) =>
33
+ path.join(repo, HOSTS.find((h) => h.host === host).projectMarker, 'settings.local.json');
34
+
31
35
  /** `_global_installs` — every host whose GLOBAL config dir carries a Geneseed manifest. */
32
36
  function globalInstalls() {
33
37
  const out = [];
@@ -102,10 +106,9 @@ export function excludeAdd(target) {
102
106
  const prior = data.excludes.find((e) => isDict(e) && same(get(e, 'path'), repo));
103
107
  const priorWired = (isDict(prior) ? get(prior, 'wired') : null) || {};
104
108
  const wired = {};
105
- if (host === 'claude' && isDir(repo)) {
109
+ if ((host === 'claude' || host === 'openclaude') && isDir(repo)) {
106
110
  const entry = resolvePath(path.join(cfg, 'CLAUDE.md')).split(path.sep).join('/');
107
- const added = wireClaudeExcludes(
108
- path.join(repo, '.claude', 'settings.local.json'), [entry]);
111
+ const added = wireClaudeExcludes(localSettings(repo, host), [entry]);
109
112
  if (added.length) {
110
113
  wired.claude_md_excludes = added;
111
114
  } else if (get(priorWired, 'claude_md_excludes')) {
@@ -172,8 +175,7 @@ export function excludeRemove(target) {
172
175
  found = true;
173
176
  const wired = get(mine, 'wired') || {};
174
177
  if (get(wired, 'claude_md_excludes')) {
175
- unwireClaudeExcludes(path.join(repo, '.claude', 'settings.local.json'),
176
- get(wired, 'claude_md_excludes'));
178
+ unwireClaudeExcludes(localSettings(repo, host), get(wired, 'claude_md_excludes'));
177
179
  }
178
180
  if (get(wired, 'bob_rules_stub')) {
179
181
  const stub = path.join(repo, '.bob', 'rules', 'geneseed.md');
@@ -61,8 +61,8 @@ import { THEMES, ROOT, makeCfg } from '../build/source.mjs';
61
61
  import { tuiInventory } from './inventory.mjs';
62
62
  import { readVersion, sourceFingerprint } from '../build/version.mjs';
63
63
  import {
64
- claudeConfigDir, bobConfigDir, copilotConfigDir, opencodeConfigDir, resolvePath,
65
- resolveMemoryDir, sovereignBypass, GATE_LEDGER,
64
+ claudeConfigDir, bobConfigDir, copilotConfigDir, openclaudeConfigDir, opencodeConfigDir,
65
+ resolvePath, resolveMemoryDir, sovereignBypass, GATE_LEDGER,
66
66
  } from '../hosts/hosts.mjs';
67
67
  // P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
68
68
  // deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
@@ -70,8 +70,9 @@ import {
70
70
  // re-exported below because `tests/fixtures/pure_probe.mjs` names this module for two of them
71
71
  // and a corpus that followed the code to its new file would stop testing the caller's view.
72
72
  import {
73
- defaultTheme, installedDefaults, manifestIsClaude, readJsonMaybe, readMaybe,
73
+ defaultTheme, installedDefaults, installTargets, manifestIsClaude, readJsonMaybe, readMaybe,
74
74
  } from '../hosts/installs.mjs';
75
+ import { installProfile, rebuildCommand } from '../build/generate.mjs';
75
76
  import { printOut } from '../lib/fs.mjs';
76
77
  import { codePointLength, padEndToWidth } from '../lib/text.mjs';
77
78
 
@@ -100,7 +101,7 @@ export function cmdVersion(args) {
100
101
  if (installed === null) {
101
102
  // A claude/bob/copilot-only machine must not report "no install detected". The bundle
102
103
  // candidates are cwd-relative only — see the Python for why `ROOT / "Harness"` left.
103
- for (const base of [claudeConfigDir(), bobConfigDir(), copilotConfigDir(),
104
+ for (const base of [claudeConfigDir(), bobConfigDir(), copilotConfigDir(), openclaudeConfigDir(),
104
105
  path.join(process.cwd(), 'Harness'), process.cwd()]) {
105
106
  const v = readVersion(base);
106
107
  if (v) { installed = v; target = base; break; }
@@ -225,7 +226,7 @@ export function statusData() {
225
226
  let installedFp = null;
226
227
  let verTarget = null;
227
228
  const otherCfg = [];
228
- for (const fn of [claudeConfigDir, bobConfigDir, copilotConfigDir]) {
229
+ for (const fn of [claudeConfigDir, bobConfigDir, copilotConfigDir, openclaudeConfigDir]) {
229
230
  try { otherCfg.push(fn()); } catch { /* a missing host dir must not sink status */ }
230
231
  }
231
232
  // Same walk as `cmdVersion`'s fallback, and cwd-relative for the same reason.
@@ -262,6 +263,20 @@ export function statusData() {
262
263
  agent_md: agentMd ? String(agentMd) : null,
263
264
  agent_md_present: Boolean(agentMd && existsSync(agentMd)),
264
265
  gates: gateSummary([...(cfgDir ? [cfgDir] : []), ...otherCfg]),
266
+ // EVERY install, not just the one `installedDefaults` settles on — and read through the
267
+ // same `installProfile` `rebuild-all` uses, so the settings shown are the ones a rebuild
268
+ // keeps. `rebuild` is the pasteable command an agent edits one flag of. A broken install
269
+ // must not sink the panel, so it is reported rather than thrown. `absent` slots are
270
+ // filtered out here: an absent slot has no settings, so a row of defaults for it reads as
271
+ // an install that does not exist — noise in the panel and misinformation in `--json`.
272
+ installs: installTargets().map(([host, scope, root]) => {
273
+ try {
274
+ const p = installProfile(host, scope, root);
275
+ return { ...p, rebuild: rebuildCommand(p.argv) };
276
+ } catch (e) {
277
+ return { host, scope, root, state: 'unreadable', error: String(e?.message ?? e) };
278
+ }
279
+ }).filter((p) => p.state !== 'absent'),
265
280
  };
266
281
  }
267
282
 
@@ -343,6 +358,18 @@ export function statusLines(d, color = false) {
343
358
  rows.push(['gates', `${armed} ${DOT} ${g.total} ask${g.total === 1 ? '' : 's'}`
344
359
  + (byRule ? ` (${byRule})` : '')]);
345
360
  }
361
+ // Conditional for the recording reason again: no recorded panel carries `installs`.
362
+ for (const p of d.installs ?? []) {
363
+ const where = p.scope === 'project' ? ` (${p.root})` : '';
364
+ if (p.state === 'unreadable') {
365
+ rows.push([`${p.host}:${p.scope}`, `unreadable: ${p.error}${where}`]);
366
+ continue;
367
+ }
368
+ const packs = p.doctrines.length ? p.doctrines.join(',') : 'no packs';
369
+ const excl = p.excludeRules?.length ? `excluding ${p.excludeRules.join(',')}` : 'no rules excluded';
370
+ rows.push([`${p.host}:${p.scope}`, [p.state, p.theme, p.footprint, `${p.posture}/${p.mode}`,
371
+ packs, excl].join(` ${DOT} `) + where]);
372
+ }
346
373
 
347
374
  const labelW = Math.max(...rows.map(([k]) => codePointLength(k)));
348
375
  const body = rows.map(([k, v]) => ` ${padEndToWidth(k, labelW)} ${v}`);
@@ -365,8 +392,17 @@ export function statusLines(d, color = false) {
365
392
  return lines;
366
393
  }
367
394
 
368
- /** `_harness_status.cmd_status`. */
369
- export function cmdStatus() {
370
- for (const line of statusLines(statusData(), colorEnabled())) printOut(`${line}\n`);
395
+ /**
396
+ * `_harness_status.cmd_status` — and `--json`, the machine face of the same payload. An agent
397
+ * reading the panel had to scrape box-drawing; the object below is what the panel is drawn
398
+ * FROM, so the two cannot disagree.
399
+ */
400
+ export function cmdStatus(args) {
401
+ const d = statusData();
402
+ if (args?.json) {
403
+ printOut(`${JSON.stringify(d, null, 2)}\n`);
404
+ return 0;
405
+ }
406
+ for (const line of statusLines(d, colorEnabled())) printOut(`${line}\n`);
371
407
  return 0;
372
408
  }
@@ -59,7 +59,7 @@ const KNOWN_EMITS = new Set(EMIT_HOST_SCOPE.keys());
59
59
  * hosts, which is what the project's host-parity rule asks for.
60
60
  */
61
61
  function hookSettingsFile(root, host, scope) {
62
- if (host !== 'claude' && host !== 'bob') return null;
62
+ if (!['claude', 'bob', 'openclaude'].includes(host)) return null;
63
63
  const name = scope === 'project' && host !== 'bob' ? 'settings.local.json' : 'settings.json';
64
64
  return path.join(root, name);
65
65
  }
@@ -228,6 +228,7 @@ const DOCTRINE_BLURBS = {
228
228
  ops: 'how the machine is driven — tool discovery, non-blocking commands, teardown',
229
229
  process: 'how a session runs — planning, context economy, docs first, and the consent gate '
230
230
  + 'on every commit and push (drop this pack and that gate goes with it)',
231
+ comms: 'how answers are presented — stable reference codes, a diagram or a table only where it earns its place',
231
232
  };
232
233
 
233
234
  /**
@@ -279,7 +280,7 @@ export function doctrineOptions() {
279
280
  /**
280
281
  * `_harness_setup.EMIT_OPTIONS`.
281
282
  *
282
- * Exported since P6b: `/api/themes` returns the same nine as its `emits` list, and a copy
283
+ * Exported since P6b: `/api/themes` returns the same eleven as its `emits` list, and a copy
283
284
  * of a table under test silently stops being the table under test.
284
285
  */
285
286
  export const EMIT_OPTIONS = [
@@ -292,6 +293,10 @@ export const EMIT_OPTIONS = [
292
293
  ['copilot-global', 'GitHub Copilot personal config dir (~/.copilot) — copilot-instructions.md, agents, skills.'],
293
294
  ['copilot', 'Per-repo AGENTS.md + .github/ for GitHub Copilot, committed into one repository.'],
294
295
  ['files', 'Plain bundle for any AGENT.md tool.'],
296
+ // Appended AFTER `files`, not beside the other hosts: the menu is answered by number, and a
297
+ // scripted `setup` that pipes its answers must keep meaning what it meant.
298
+ ['openclaude-global', 'OpenClaude global config dir (~/.openclaude) — CLAUDE.md, agents, skills, hooks.'],
299
+ ['openclaude', 'Per-repo .openclaude/ (CLAUDE.md, agents, skills, hooks) for OpenClaude, committed into one repository.'],
295
300
  ];
296
301
 
297
302
  /** `_harness_setup.FOOTPRINT_OPTIONS`. */
@@ -375,7 +380,7 @@ export function collectSetupLines() {
375
380
  // Every PROJECT emit needs the repo root — claude/bob/copilot included: without `--out`
376
381
  // their CLAUDE.md/.claude land in the generator's default ./Harness, where the host never
377
382
  // looks.
378
- if (['opencode', 'claude', 'bob', 'copilot'].includes(emit)) {
383
+ if (['opencode', 'claude', 'bob', 'copilot', 'openclaude'].includes(emit)) {
379
384
  root = ask('Repo root to install into', '.');
380
385
  out = root;
381
386
  } else if (emit === 'files') {
@@ -71,7 +71,7 @@ import { comparePaths, isAbsolutePath, within } from '../lib/paths.mjs';
71
71
  const hostSpec = (host) => HOSTS.find((h) => h.host === host);
72
72
 
73
73
  /** The Claude-STYLE hosts — one manifest shape, one reversal. Spelled once. */
74
- const CLAUDE_STYLE = ['claude', 'bob', 'copilot'];
74
+ const CLAUDE_STYLE = ['claude', 'bob', 'copilot', 'openclaude'];
75
75
 
76
76
  /**
77
77
  * `shutil.rmtree(p, ignore_errors=True)`.
@@ -681,7 +681,7 @@ export function cmdUninstall(args) {
681
681
  const targetDesc = resolvePath(expanduser(args.target));
682
682
  printErr(`[uninstall] no Geneseed install detected at ${targetDesc}.\n`
683
683
  + '[uninstall] pass --target <repo> for a project install (.opencode/.claude/'
684
- + '.bob/.github) or --target <config dir> for a global one.\n');
684
+ + '.bob/.github/.openclaude) or --target <config dir> for a global one.\n');
685
685
  return 1;
686
686
  }
687
687
  const [host, scope, root] = hit;
@@ -700,7 +700,7 @@ export function cmdUninstall(args) {
700
700
  + `${hostSpec(host).agentFile} managed block, and Geneseed's `
701
701
  + '~/.copilot/settings.json hooks on a global install (your own keys/hooks and '
702
702
  + '.github files are kept).\n');
703
- } else if (host === 'claude' || host === 'bob') {
703
+ } else if (['claude', 'bob', 'openclaude'].includes(host)) {
704
704
  printOut('[uninstall] removes: agents/, skills/, markers, the '
705
705
  + `${hostSpec(host).agentFile} managed block, and Geneseed's `
706
706
  + 'settings.json hooks/excludes (your own keys/hooks are kept).\n');