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.
- package/CHANGELOG.md +74 -0
- package/DESIGN.md +6 -6
- package/QUICKSTART.md +1 -1
- package/README.md +33 -31
- package/SETUP.md +36 -10
- package/SHIPPED.md +3 -3
- package/adapters/claude-code/skills/geneseed/SKILL.md +1 -1
- package/adapters/openclaude/README.md +64 -0
- package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
- package/adapters/opencode/workflows/_runtime.js +1 -1
- package/docs/extending.md +18 -6
- package/docs/web/exclusions.md +1 -0
- package/docs/web/footprint.md +1 -1
- package/docs/web/harnesses.md +14 -14
- package/docs/web/install-quick.md +1 -1
- package/docs/web/rules.md +1 -1
- package/docs/web/themes-author.md +1 -1
- package/docs/web-ui.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +3 -3
- package/js/build/driver.mjs +64 -14
- package/js/build/emit-claude.mjs +1 -1
- package/js/build/generate.mjs +92 -42
- package/js/build/source.mjs +5 -4
- package/js/cli-table.json +151 -2
- package/js/hosts/hooks.mjs +11 -4
- package/js/hosts/hosts.mjs +17 -2
- package/js/hosts/installs.mjs +32 -2
- package/js/hosts/mcp.mjs +8 -1
- package/js/hosts/settings.mjs +4 -1
- package/js/inspect/diff.mjs +1 -1
- package/js/inspect/doctor.mjs +3 -3
- package/js/inspect/excludes.mjs +7 -5
- package/js/inspect/status.mjs +44 -8
- package/js/maintain/migrate.mjs +1 -1
- package/js/maintain/setup.mjs +7 -2
- package/js/maintain/uninstall.mjs +3 -3
- package/js/web/actions.mjs +7 -5
- package/js/web/api.mjs +2 -2
- package/js/web/docs.mjs +1 -1
- package/package.json +2 -1
- package/src/doctrines/README.md +3 -2
- package/src/doctrines/comms.md +53 -0
- package/src/doctrines/process.md +0 -26
- package/src/skills/geneseed.md +9 -8
- package/src/skills/parallel-agents.md +1 -1
- package/src/skills/pipeline.md +1 -1
- package/src/skills/token-report/SKILL.md +2 -1
- package/src/skills/token-report/VENDOR.md +3 -3
- package/src/skills/token-report/scripts/token_report.mjs +14 -7
- package/src/skills/workflow.md +2 -2
- package/themes/_TEMPLATE.json +5 -3
- package/themes/biker.json +5 -3
- package/themes/commentator.json +5 -3
- package/themes/cyberpunk.json +5 -3
- package/themes/gamer.json +5 -3
- package/themes/imperial.json +5 -3
- package/themes/joker.json +5 -3
- package/themes/marvin.json +5 -3
- package/themes/mean.json +5 -3
- package/themes/military.json +5 -3
- package/themes/neutral.json +5 -3
- package/themes/pirate.json +5 -3
- package/themes/sports.json +5 -3
- package/themes/verstappen.json +5 -3
- package/themes/wizard.json +5 -3
- package/web/dist/assets/{Activity-Flgf4kHy.js → Activity-CH-7cPIM.js} +1 -1
- package/web/dist/assets/{ActivityDetail-DIpr85nh.js → ActivityDetail-DZvz0xS5.js} +1 -1
- package/web/dist/assets/{CatalogRow-CjyLjaLb.js → CatalogRow-DftXpZHY.js} +1 -1
- package/web/dist/assets/{Diff-Deisdha8.js → Diff-DvwdG0L9.js} +1 -1
- package/web/dist/assets/{Docs-DGUdY4l4.js → Docs-DrJNDPwI.js} +1 -1
- package/web/dist/assets/{Doctor-F74lJ69P.js → Doctor-OTmwmYZg.js} +1 -1
- package/web/dist/assets/{GreenhouseView-D072lBiD.js → GreenhouseView-XxrAdriX.js} +1 -1
- package/web/dist/assets/Harness-C_LYNk5c.js +1 -0
- package/web/dist/assets/{JournalView-DWFqZ0Iw.js → JournalView-BcuyeEaR.js} +1 -1
- package/web/dist/assets/{Laws-BN6g3HU7.js → Laws-CWHrTKY4.js} +1 -1
- package/web/dist/assets/{Library-CDOv4emM.js → Library-CA9IHghn.js} +1 -1
- package/web/dist/assets/{OperatorHudView-CwNIUVX8.js → OperatorHudView-BMRY1Tt4.js} +1 -1
- package/web/dist/assets/{Profile-CpP9BTne.js → Profile-wceET1JY.js} +1 -1
- package/web/dist/assets/{Rules-DFXsUCB7.js → Rules-BEXvY4Tx.js} +1 -1
- package/web/dist/assets/{Settings-BqZVUtJ5.js → Settings-DS2zF0Ez.js} +1 -1
- package/web/dist/assets/{Skills-Cu6kBgfF.js → Skills-DahlOJuI.js} +1 -1
- package/web/dist/assets/{drawable-BgfrEBer.js → drawable-BTHjTzF7.js} +1 -1
- package/web/dist/assets/{index-CAqrWhKl.js → index-AN5Emnrg.js} +2 -2
- package/web/dist/assets/{lawCats-BLgxl151.js → lawCats-Btjp3Hk_.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/src/pages/Laws.jsx +6 -2
- 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
|
|
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
|
{
|
package/js/hosts/hooks.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
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);
|
package/js/hosts/hosts.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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. */
|
package/js/hosts/installs.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/js/hosts/settings.mjs
CHANGED
|
@@ -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
|
-
|
|
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`;
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -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}). `
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -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()));
|
package/js/inspect/excludes.mjs
CHANGED
|
@@ -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(
|
|
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');
|
package/js/inspect/status.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
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
|
}
|
package/js/maintain/migrate.mjs
CHANGED
|
@@ -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 (
|
|
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
|
}
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -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
|
|
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 (
|
|
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');
|