geneseed 2.0.0 → 3.0.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 +217 -5
- package/DESIGN.md +149 -1326
- package/QUICKSTART.md +1 -1
- package/README.md +30 -22
- package/SETUP.md +98 -24
- package/SHIPPED.md +6 -5
- package/adapters/bob/README.md +3 -3
- package/adapters/claude-code/README.md +6 -4
- package/adapters/claude-code/skills/geneseed/SKILL.md +10 -6
- package/adapters/copilot/README.md +5 -4
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +11 -6
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +1 -1
- package/adapters/opencode/README.md +15 -9
- package/adapters/opencode/agents/orchestrator.md +1 -1
- package/adapters/opencode/opencode.json +0 -9
- package/adapters/opencode/plugins/geneseed-context.js +7 -7
- package/adapters/opencode/plugins/geneseed-guard.js +6 -6
- package/adapters/opencode/workflows/dispatch.js +1 -1
- package/bin/geneseed.mjs +74 -8
- package/docs/declined.md +53 -2
- package/docs/design-history.md +1290 -0
- package/docs/extending.md +672 -0
- package/docs/limits.md +141 -82
- package/docs/token-footprint.md +15 -4
- package/docs/web/agents.md +1 -1
- package/docs/web/authoring.md +1 -1
- package/docs/web/claude-hooks.md +2 -2
- package/docs/web/collaboration.md +3 -3
- package/docs/web/first-session.md +2 -2
- package/docs/web/footprint.md +14 -7
- package/docs/web/foreman-mode.md +2 -2
- package/docs/web/harnesses.md +5 -3
- package/docs/web/install-quick.md +1 -1
- package/docs/web/model.md +3 -3
- package/docs/web/plugin-context.md +2 -2
- package/docs/web/plugin-guard.md +3 -3
- package/docs/web/plugin-ponytail.md +1 -1
- package/docs/web/rules.md +23 -3
- package/docs/web/setup-choices.md +20 -7
- package/docs/web/themes-author.md +13 -1
- package/docs/web-ui.md +36 -12
- package/geneseed +0 -0
- package/harness.config.json +1 -1
- package/js/catalog.mjs +29 -4
- package/js/checkout.mjs +24 -1
- package/js/cli-table.json +9 -7
- package/js/diff.mjs +8 -3
- package/js/doctor.mjs +374 -26
- package/js/emit.mjs +48 -11
- package/js/generate.mjs +39 -5
- package/js/hooks.mjs +8 -7
- package/js/installs.mjs +101 -4
- package/js/inventory.mjs +150 -20
- package/js/lib/fs.mjs +6 -2
- package/js/mcp.mjs +3 -15
- package/js/migrate.mjs +10 -4
- package/js/render.mjs +116 -11
- package/js/settings.mjs +297 -28
- package/js/setup.mjs +95 -5
- package/js/status.mjs +45 -4
- package/js/themes.mjs +1 -1
- package/js/tui.mjs +46 -3
- package/js/uninstall.mjs +30 -11
- package/js/web/actions.mjs +76 -9
- package/js/web/api.mjs +95 -16
- package/js/web/docs.mjs +33 -5
- package/js/web/jobs.mjs +12 -3
- package/js/web/server.mjs +59 -2
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +75 -65
- package/src/agents/_template.md +4 -4
- package/src/agents/advocate.md +4 -4
- package/src/agents/architect.md +2 -2
- package/src/agents/developer.md +5 -5
- package/src/agents/docs.md +2 -2
- package/src/agents/empiricist.md +3 -3
- package/src/agents/explorer.md +3 -3
- package/src/agents/framer.md +3 -3
- package/src/agents/historian.md +3 -3
- package/src/agents/operator.md +3 -3
- package/src/agents/pragmatist.md +3 -3
- package/src/agents/reviewer.md +2 -2
- package/src/agents/security.md +2 -2
- package/src/agents/skeptic.md +4 -4
- package/src/agents/steward.md +3 -3
- package/src/agents/tester.md +2 -2
- package/src/agents/user-advocate.md +3 -3
- package/src/agents/visionary.md +3 -3
- package/src/doctrines/README.md +41 -0
- package/src/doctrines/craft.md +51 -0
- package/src/doctrines/ops.md +61 -0
- package/src/doctrines/process.md +87 -0
- package/src/doctrines/rigor.md +52 -0
- package/src/laws/universal.md +17 -301
- package/src/memory/README.md +2 -2
- package/src/modes/README.md +2 -2
- package/src/modes/foreman.md +1 -1
- package/src/notebook/README.md +2 -2
- package/src/ontology/universal.md +89 -0
- package/src/postures/README.md +3 -3
- package/src/postures/artisan.md +3 -3
- package/src/postures/assistant.md +1 -1
- package/src/postures/expert.md +1 -1
- package/src/postures/mentor.md +1 -1
- package/src/postures/peer.md +2 -2
- package/src/skills/_template.md +2 -2
- package/src/skills/brainstorm.md +2 -2
- package/src/skills/clarify.md +2 -2
- package/src/skills/codebase-design.md +1 -1
- package/src/skills/commit.md +5 -5
- package/src/skills/council.md +4 -4
- package/src/skills/crash-course.md +1 -1
- package/src/skills/daydream/SKILL.md +1 -1
- package/src/skills/debug.md +2 -2
- package/src/skills/decode.md +1 -1
- package/src/skills/develop.md +6 -6
- package/src/skills/document-project.md +4 -4
- package/src/skills/domain-modeling.md +1 -1
- package/src/skills/drill.md +1 -1
- package/src/skills/feynman.md +1 -1
- package/src/skills/forge-mcp.md +3 -3
- package/src/skills/fresh-eyes.md +4 -4
- package/src/skills/frontend-design.md +1 -1
- package/src/skills/gap-detector.md +1 -1
- package/src/skills/geneseed-code-review.md +1 -1
- package/src/skills/geneseed.md +4 -4
- package/src/skills/git-archaeology.md +1 -1
- package/src/skills/git-rescue.md +3 -3
- package/src/skills/handoff.md +4 -4
- package/src/skills/herdr.md +1 -1
- package/src/skills/ingest.md +3 -3
- package/src/skills/learning-path.md +1 -1
- package/src/skills/migrate.md +5 -5
- package/src/skills/opencode-theme.md +1 -1
- package/src/skills/parallel-agents.md +2 -2
- package/src/skills/pipeline.md +2 -2
- package/src/skills/plan.md +2 -2
- package/src/skills/ponytail.md +1 -1
- package/src/skills/profile.md +1 -1
- package/src/skills/prose.md +2 -2
- package/src/skills/prototype.md +2 -2
- package/src/skills/refactor.md +2 -2
- package/src/skills/release.md +4 -4
- package/src/skills/repo-map.md +5 -5
- package/src/skills/research.md +6 -6
- package/src/skills/review-response.md +3 -3
- package/src/skills/roast-me.md +1 -1
- package/src/skills/rule.md +5 -5
- package/src/skills/ship.md +3 -3
- package/src/skills/tdd.md +2 -2
- package/src/skills/tickets.md +1 -1
- package/src/skills/wayfinder.md +1 -1
- package/src/skills/wiki.md +4 -4
- package/src/skills/workflow.md +2 -2
- package/themes/README.md +17 -7
- package/themes/_TEMPLATE.json +37 -33
- package/themes/biker.json +37 -33
- package/themes/commentator.json +37 -33
- package/themes/cyberpunk.json +37 -33
- package/themes/gamer.json +37 -33
- package/themes/imperial.json +37 -33
- package/themes/joker.json +37 -33
- package/themes/marvin.json +37 -33
- package/themes/mean.json +37 -33
- package/themes/military.json +37 -33
- package/themes/neutral.json +37 -33
- package/themes/pirate.json +37 -33
- package/themes/sports.json +37 -33
- package/themes/verstappen.json +37 -33
- package/themes/wizard.json +37 -33
- package/web/dist/assets/{About-DX1-VTBP.js → About-C0Mk0igT.js} +1 -1
- package/web/dist/assets/{Activity-Dp9NaXOn.js → Activity-C4aJJVXz.js} +1 -1
- package/web/dist/assets/{ActivityDetail-HpOBtrXS.js → ActivityDetail-DaQ85za0.js} +1 -1
- package/web/dist/assets/{Diff-Ct4G3Se8.js → Diff-B3tlO2Yj.js} +1 -1
- package/web/dist/assets/{Doctor-DGjkZFuA.js → Doctor-BNC-5Dmo.js} +1 -1
- package/web/dist/assets/{Graph-b8o-S6kv.js → Graph-us0T6ZcV.js} +1 -1
- package/web/dist/assets/Harnesses-DAMgyfM_.js +1 -0
- package/web/dist/assets/Laws-rOxegJ75.js +1 -0
- package/web/dist/assets/{Library-BEZscyaQ.js → Library-KmwrHA1J.js} +1 -1
- package/web/dist/assets/{Markdown-k_LSLBGq.js → Markdown-0eQOVGSF.js} +1 -1
- package/web/dist/assets/{Profile-DydukTxP.js → Profile-ZU7WV6Xl.js} +1 -1
- package/web/dist/assets/{Rules-CIRG4Ldt.js → Rules-CtDoPhhf.js} +1 -1
- package/web/dist/assets/{Skills-Buoi1IBr.js → Skills-BhoOxdkD.js} +1 -1
- package/web/dist/assets/{StatusBadge-CE_AJttl.js → StatusBadge-CGvYVCwa.js} +1 -1
- package/web/dist/assets/{Themes-CI4o3gJC.js → Themes-Dr7OOkD7.js} +1 -1
- package/web/dist/assets/{activity-BCW7e6Cy.js → activity-B8aq3qtl.js} +1 -1
- package/web/dist/assets/index-2wqPDxuN.css +1 -0
- package/web/dist/assets/{index-B1PIx7RM.js → index-AhMzkdbI.js} +1 -1
- package/web/dist/assets/index-CRftEtBX.js +12 -0
- package/web/dist/assets/{index-Ck2iCdaC.js → index-D0lPN_qW.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +282 -88
- package/web/dist/assets/Harnesses-gr3-aHBF.js +0 -1
- package/web/dist/assets/Laws-DTwF5tTC.js +0 -1
- package/web/dist/assets/index-BnTQUSUz.js +0 -12
- package/web/dist/assets/index-DUCsDv7z.css +0 -1
package/js/setup.mjs
CHANGED
|
@@ -46,7 +46,7 @@ import path from 'node:path';
|
|
|
46
46
|
|
|
47
47
|
import { main as driverMain, resolveOut } from '../bin/geneseed.mjs';
|
|
48
48
|
import { playLine } from './anim.mjs';
|
|
49
|
-
import { discoverNames } from './checkout.mjs';
|
|
49
|
+
import { discoverNames, PACK_ORDER } from './checkout.mjs';
|
|
50
50
|
import { exportImprovements } from './diff.mjs';
|
|
51
51
|
import { cmdDoctor } from './doctor.mjs';
|
|
52
52
|
import { setupBuildArgs } from './generate.mjs';
|
|
@@ -211,6 +211,23 @@ const MODE_BLURBS = {
|
|
|
211
211
|
foreman: 'triages tasks, spawns pipelines for substantial work',
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* One line per doctrine pack, written for someone who has never read `src/doctrines/README.md`
|
|
216
|
+
* — this is the only description of a pack an installer ever sees.
|
|
217
|
+
*
|
|
218
|
+
* `process` says what turning it OFF costs, and it is the only one that has to. The other three
|
|
219
|
+
* trade practices for context; process 5 is the commit/push consent gate, so dropping the pack
|
|
220
|
+
* drops the rule that every commit and every push is asked for. A blurb that read "planning and
|
|
221
|
+
* context economy" would have sold that away in a menu.
|
|
222
|
+
*/
|
|
223
|
+
const DOCTRINE_BLURBS = {
|
|
224
|
+
craft: 'how code is written — reuse first, house conventions, docs, the smallest diff',
|
|
225
|
+
rigor: 'how work is proven — idempotence, honest tests, coverage, gates that can fail',
|
|
226
|
+
ops: 'how the machine is driven — tool discovery, non-blocking commands, teardown',
|
|
227
|
+
process: 'how a session runs — planning, context economy, docs first, and the consent gate '
|
|
228
|
+
+ 'on every commit and push (drop this pack and that gate goes with it)',
|
|
229
|
+
};
|
|
230
|
+
|
|
214
231
|
/**
|
|
215
232
|
* `_harness_setup._theme_options`.
|
|
216
233
|
*
|
|
@@ -243,6 +260,20 @@ export function modeOptions() {
|
|
|
243
260
|
return discoverNames('modes', 'direct').map((n) => [n, MODE_BLURBS[n] ?? '']);
|
|
244
261
|
}
|
|
245
262
|
|
|
263
|
+
/**
|
|
264
|
+
* The doctrine packs, in `PACK_ORDER` — the third option table, and the one that is a SET.
|
|
265
|
+
*
|
|
266
|
+
* The order is NARRATIVE and comes from `PACK_ORDER`; `discoverNames` only says which packs
|
|
267
|
+
* this checkout actually ships. Ordering by discovery instead would read `craft, ops, process,
|
|
268
|
+
* rigor` — it `.sort()`s — and the menu a human answers would stop matching the `Active packs:`
|
|
269
|
+
* line the build writes, which is normalised into `PACK_ORDER` at the driver. Same intersection
|
|
270
|
+
* the driver's `parseDoctrines` and the config loader take, for the same reason.
|
|
271
|
+
*/
|
|
272
|
+
export function doctrineOptions() {
|
|
273
|
+
const known = discoverNames('doctrines', PACK_ORDER[0]);
|
|
274
|
+
return PACK_ORDER.filter((n) => known.includes(n)).map((n) => [n, DOCTRINE_BLURBS[n] ?? '']);
|
|
275
|
+
}
|
|
276
|
+
|
|
246
277
|
/**
|
|
247
278
|
* `_harness_setup.EMIT_OPTIONS`.
|
|
248
279
|
*
|
|
@@ -272,12 +303,60 @@ const FOOTPRINT_OPTIONS = [
|
|
|
272
303
|
// --------------------------------------------------------------------------------------
|
|
273
304
|
|
|
274
305
|
/**
|
|
275
|
-
*
|
|
306
|
+
* The Doctrine-packs question — one `all`/`choose` gate, then a y/n per pack.
|
|
307
|
+
*
|
|
308
|
+
* TWO LEVELS RATHER THAN FOUR QUESTIONS, because the overwhelmingly common answer is "all of
|
|
309
|
+
* them" and a wizard that asks four yes/nos to get there taxes every install for the rare one.
|
|
310
|
+
* The gate's default is `all`, so an installer who holds Enter through the whole wizard gets
|
|
311
|
+
* the full set — which is what every pre-packs install already had.
|
|
312
|
+
*
|
|
313
|
+
* `askChoice` and not `confirm` for the per-pack question: `confirm` reads the first character
|
|
314
|
+
* and prints no description, so the pack's blurb — the only place a reader is told what the
|
|
315
|
+
* pack costs — would have nowhere to go. Two named options give it a line.
|
|
316
|
+
*
|
|
317
|
+
* Answering `no` to all four is legal and yields `[]`, which `setupBuildArgs` spells
|
|
318
|
+
* `--doctrines none`. That is a real configuration (invariants and ontology only), not a
|
|
319
|
+
* mistake to guard against, so it is not second-guessed here.
|
|
320
|
+
*
|
|
321
|
+
* `deployed` is what the machine's carriers already say (`installedDefaults().doctrines`), and
|
|
322
|
+
* it drives BOTH levels: a narrowed install opens on `choose` with its own packs pre-ticked,
|
|
323
|
+
* so holding Enter through the wizard KEEPS the selection instead of silently widening it back
|
|
324
|
+
* to all four. `null` — no carrier said — still opens on `all`, which is the same resolution
|
|
325
|
+
* `doctrinesForBuild` gives unknown, so the wizard and the build agree about silence.
|
|
326
|
+
*
|
|
327
|
+
* ⚠ It never returns `null`, at either level, and `tests/unit/setup.test.mjs` pins that: this
|
|
328
|
+
* is the one call site allowed to reach `setupBuildArgs` without a `doctrinesForBuild`
|
|
329
|
+
* fallback, and that exemption is only sound while the function is total.
|
|
330
|
+
*/
|
|
331
|
+
function askDoctrines(deployed = null) {
|
|
332
|
+
const opts = doctrineOptions();
|
|
333
|
+
const all = opts.map(([n]) => n);
|
|
334
|
+
const known = Array.isArray(deployed) ? all.filter((n) => deployed.includes(n)) : null;
|
|
335
|
+
const narrowed = known !== null && known.length !== all.length;
|
|
336
|
+
const pick = askChoice('Doctrine packs', [
|
|
337
|
+
['all', `${all.join(' + ')}${narrowed ? '' : ' (default)'}`],
|
|
338
|
+
['choose', `pick packs${narrowed ? ` (installed: ${known.join(', ') || 'none'})` : ''}`],
|
|
339
|
+
], narrowed ? 'choose' : 'all');
|
|
340
|
+
if (pick !== 'choose') return all;
|
|
341
|
+
return all.filter((name) => askChoice(`Include ${name} — ${DOCTRINE_BLURBS[name] ?? name}`,
|
|
342
|
+
[['yes', `build ${name} into AGENT.md`], ['no', `leave ${name} out`]],
|
|
343
|
+
known === null || known.includes(name) ? 'yes' : 'no') === 'yes');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* `_harness_setup._collect_setup_lines` — the six questions, the plan, the confirm.
|
|
276
348
|
*
|
|
277
349
|
* Returns the selection, or null when the confirm is declined. Every default is the
|
|
278
350
|
* DEPLOYED value first (`installedDefaults`) and the CONFIGURED one second, which is why
|
|
279
351
|
* P5i is the phase that had to give `installedDefaults` its posture, mode and footprint keys
|
|
280
352
|
* back: three of the five questions read them.
|
|
353
|
+
*
|
|
354
|
+
* Doctrines reads the same walk, and joined it last because the `Active packs:` marker reader
|
|
355
|
+
* did not exist when the question shipped. Until it did, every re-run of the wizard offered
|
|
356
|
+
* `all` to an installer who had deliberately narrowed their packs — the one register where
|
|
357
|
+
* holding Enter SILENTLY WIDENED a selection instead of keeping it. `inst.doctrines` is passed
|
|
358
|
+
* whole, `null` included, because `askDoctrines` distinguishes "no carrier said" from "a
|
|
359
|
+
* carrier said none" and the two open the question differently.
|
|
281
360
|
*/
|
|
282
361
|
export function collectSetupLines() {
|
|
283
362
|
pyPrint('Geneseed setup — answer a few questions; nothing is written until you confirm.\n');
|
|
@@ -285,6 +364,7 @@ export function collectSetupLines() {
|
|
|
285
364
|
const theme = askChoice('Theme', themeOptions(), inst.theme || defaultTheme());
|
|
286
365
|
const posture = askChoice('Posture', postureOptions(), inst.posture || defaultPosture());
|
|
287
366
|
const mode = askChoice('Mode', modeOptions(), inst.mode || defaultMode());
|
|
367
|
+
const doctrines = askDoctrines(inst.doctrines);
|
|
288
368
|
const emit = askChoice('Install mode', EMIT_OPTIONS, inst.emit || 'opencode-global');
|
|
289
369
|
const footprint = askChoice('Footprint', FOOTPRINT_OPTIONS.map(([k, d]) => [k, d]),
|
|
290
370
|
inst.footprint || 'lean');
|
|
@@ -299,10 +379,15 @@ export function collectSetupLines() {
|
|
|
299
379
|
} else if (emit === 'files') {
|
|
300
380
|
out = ask('Output dir for the bundle', 'Harness');
|
|
301
381
|
}
|
|
382
|
+
// The full pack list is passed as the elision default rather than left to `setupBuildArgs`'s
|
|
383
|
+
// own: the menu the user just answered was built from `doctrineOptions()`, so "they picked
|
|
384
|
+
// everything" must be judged against that same list, not against a second one.
|
|
385
|
+
const allPacks = doctrineOptions().map(([n]) => n);
|
|
302
386
|
pyPrint(`\nAbout to run: geneseed build ${
|
|
303
|
-
setupBuildArgs(theme, emit, out, root, footprint, posture, mode
|
|
387
|
+
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks)
|
|
388
|
+
.join(' ')}\n`);
|
|
304
389
|
if (!confirm('Proceed?', true)) return null;
|
|
305
|
-
return { theme, posture, mode, emit, out, root, footprint };
|
|
390
|
+
return { theme, posture, mode, doctrines, emit, out, root, footprint };
|
|
306
391
|
}
|
|
307
392
|
|
|
308
393
|
// --------------------------------------------------------------------------------------
|
|
@@ -441,6 +526,7 @@ export function setupLines() {
|
|
|
441
526
|
}
|
|
442
527
|
const {
|
|
443
528
|
theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
|
|
529
|
+
doctrines = null,
|
|
444
530
|
} = sel;
|
|
445
531
|
if (emit === 'opencode-global') {
|
|
446
532
|
// The build below overwrites the deployed global harness, and the self-improvement loops
|
|
@@ -455,7 +541,11 @@ export function setupLines() {
|
|
|
455
541
|
pyPrint(`! could not export local edits (${e && e.message}) — continuing.\n`);
|
|
456
542
|
}
|
|
457
543
|
}
|
|
458
|
-
|
|
544
|
+
// Same list the question was built from — see `collectSetupLines`. The build that actually
|
|
545
|
+
// runs must carry the pack selection the plan line just previewed, or the wizard would print
|
|
546
|
+
// one command and run another.
|
|
547
|
+
const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
|
|
548
|
+
doctrineOptions().map(([n]) => n));
|
|
459
549
|
pyPrint(`Running: geneseed build ${argv.join(' ')}\n`);
|
|
460
550
|
const rc = driverMain(argv);
|
|
461
551
|
if (rc !== 0) {
|
package/js/status.mjs
CHANGED
|
@@ -125,9 +125,24 @@ export function cmdVersion(args) {
|
|
|
125
125
|
* it, and the eleven `status` cells and the catalog cells now gate the same classifier
|
|
126
126
|
* from opposite ends.
|
|
127
127
|
*/
|
|
128
|
-
export function inventoryCounts(themeName) {
|
|
129
|
-
const inv = tuiInventory(themeName);
|
|
130
|
-
|
|
128
|
+
export function inventoryCounts(themeName, doctrines = null) {
|
|
129
|
+
const inv = tuiInventory(themeName, doctrines);
|
|
130
|
+
const on = inv.doctrines.filter((p) => p.active);
|
|
131
|
+
return {
|
|
132
|
+
agents: inv.agents.length,
|
|
133
|
+
skills: inv.skills.length,
|
|
134
|
+
laws: inv.laws.length,
|
|
135
|
+
ontology: inv.ontology.length,
|
|
136
|
+
// ⚠ THREE NUMBERS AND NOT ONE, because a pack-off install is a real configuration and
|
|
137
|
+
// "23 rules" would be a lie in it. `packs`/`packsTotal` is the fraction the console and
|
|
138
|
+
// the panel both print; `doctrineRules` counts only the ACTIVE packs' rules — what this
|
|
139
|
+
// install is actually bound by, not what its bundle happens to carry. The catalogue is
|
|
140
|
+
// always whole (every pack file ships), so the total is recoverable from `tuiInventory`
|
|
141
|
+
// by anything that needs it and is not duplicated here.
|
|
142
|
+
packs: on.length,
|
|
143
|
+
packsTotal: inv.doctrines.length,
|
|
144
|
+
doctrineRules: on.reduce((n, p) => n + p.rules.length, 0),
|
|
145
|
+
};
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
/**
|
|
@@ -163,7 +178,10 @@ export function statusData() {
|
|
|
163
178
|
const inst = installedDefaults();
|
|
164
179
|
const theme = inst.theme || defaultTheme();
|
|
165
180
|
const mdir = resolveMemoryDir(null);
|
|
166
|
-
|
|
181
|
+
// `inst.doctrines` is what the machine's carriers say — `null` when none of them does, which
|
|
182
|
+
// `tuiInventory` reads as every pack, the same resolution `doctrinesForBuild` gives unknown.
|
|
183
|
+
// This is a DISPLAY read: nothing here decides what a build emits.
|
|
184
|
+
const inv = inventoryCounts(theme, inst.doctrines);
|
|
167
185
|
let cfgDir = null;
|
|
168
186
|
try { cfgDir = opencodeConfigDir(); } catch { cfgDir = null; }
|
|
169
187
|
const sourceFp = sourceFingerprint(makeCfg());
|
|
@@ -189,6 +207,15 @@ export function statusData() {
|
|
|
189
207
|
agents: inv.agents,
|
|
190
208
|
skills: inv.skills,
|
|
191
209
|
laws: inv.laws,
|
|
210
|
+
// ⚠ NAMED ONE BY ONE, AND THAT IS THE TRAP THIS FUNCTION SETS TWICE. `inv` is already
|
|
211
|
+
// `inventoryCounts`' reduction, and this literal names its keys again — so a count added
|
|
212
|
+
// to the inventory and not to BOTH places is dropped here in silence, with nothing to
|
|
213
|
+
// fail. There is deliberately no spread: the panel's payload is a stated contract, and
|
|
214
|
+
// `js/web/api.mjs` spreads THIS, so anything reaching that endpoint passes through here.
|
|
215
|
+
ontology: inv.ontology,
|
|
216
|
+
packs: inv.packs,
|
|
217
|
+
packs_total: inv.packsTotal,
|
|
218
|
+
doctrine_rules: inv.doctrineRules,
|
|
192
219
|
memory_dir: mdir ? String(mdir) : null,
|
|
193
220
|
facts: mdir ? memoryFactCount(mdir) : 0,
|
|
194
221
|
source_fp: sourceFp,
|
|
@@ -248,6 +275,20 @@ export function statusLines(d, color = false) {
|
|
|
248
275
|
+ `(${d.facts} fact${d.facts === 1 ? '' : 's'})`],
|
|
249
276
|
['version', `${d.installed_fp || '(none)'} ${DOT} source ${d.source_fp}`],
|
|
250
277
|
];
|
|
278
|
+
// ⚠ A ROW, NOT A WIDER `components` LINE, AND THE REASON IS THAT THE CORPUS CANNOT BE
|
|
279
|
+
// RE-BLESSED. `statusLines` has 30 recorded cases per platform in
|
|
280
|
+
// `tests/__snapshots__/primitives/`, every one of them holding the box's exact bytes —
|
|
281
|
+
// including `17 agents · 47 skills · 37 laws` — and the recorder was Python and is gone.
|
|
282
|
+
// Touching `components` moves all 30; adding a row would too, since the box width is
|
|
283
|
+
// measured over `rows`. So the row is CONDITIONAL on a key the recorded arguments do not
|
|
284
|
+
// carry, exactly as `agent_md` below already is, and the recording renders unchanged.
|
|
285
|
+
//
|
|
286
|
+
// It carries the two tiers `components` cannot: `components` counts the invariants as
|
|
287
|
+
// `laws`, which is still true, and says nothing about the ontology or the packs.
|
|
288
|
+
if (d.packs_total !== undefined) {
|
|
289
|
+
rows.push(['constitution', `${d.ontology} ontology sections ${DOT} ${d.doctrine_rules} `
|
|
290
|
+
+ `doctrine rules in ${d.packs}/${d.packs_total} packs`]);
|
|
291
|
+
}
|
|
251
292
|
if (d.agent_md) {
|
|
252
293
|
rows.push(['AGENT.md',
|
|
253
294
|
`${d.agent_md} (${d.agent_md_present ? 'present' : 'MISSING'})`]);
|
package/js/themes.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
* - the three `strip` spellings are Python's whitespace set, not `String.prototype.trim`'s;
|
|
60
60
|
* - `jsonDumpsCompact(v, {ensureAscii: false})` is `json.dumps(v, ensure_ascii=False)`,
|
|
61
61
|
* including the `', '`/`': '` separators — which matter for exactly ONE key,
|
|
62
|
-
* `AGENT_COLORS`, the only non-string among `_TEMPLATE.json`'s
|
|
62
|
+
* `AGENT_COLORS`, the only non-string among `_TEMPLATE.json`'s 140 values. `ensureAscii`
|
|
63
63
|
* is false because the reference passes `ensure_ascii=False`, and 33 template values
|
|
64
64
|
* carry non-ASCII text that would otherwise be escaped into the committed file;
|
|
65
65
|
* - `pyEq`, because the safety net compares two `json.loads` results and `===` is identity
|
package/js/tui.mjs
CHANGED
|
@@ -246,6 +246,11 @@ const ICONS = {
|
|
|
246
246
|
agent: ['🤖', '◆', '@'],
|
|
247
247
|
skill: ['✨', '✦', '*'],
|
|
248
248
|
law: ['📜', '§', '#'],
|
|
249
|
+
// The other two tiers. Safe to add: `icon` has 32 recorded cases and none of them names a
|
|
250
|
+
// key that did not exist, so a new entry changes no recorded answer — unlike `glyphs`,
|
|
251
|
+
// whose recorded results are the whole object and which is therefore left alone.
|
|
252
|
+
ontology: ['🧭', '◉', 'O'],
|
|
253
|
+
doctrine: ['📐', '⌘', 'D'],
|
|
249
254
|
library: ['📚', '❒', 'L'],
|
|
250
255
|
notebook: ['📓', '✎', 'N'],
|
|
251
256
|
wiki: ['📘', '▥', 'W'],
|
|
@@ -387,14 +392,47 @@ export function themeFlair(theme) {
|
|
|
387
392
|
/**
|
|
388
393
|
* `_tui_entries` — the ordered (kind, label, data) rows for the left list. `head` is a
|
|
389
394
|
* section divider and is not selectable.
|
|
395
|
+
*
|
|
396
|
+
* ⚠ THE TWO NEW TIERS ARE GUARDED, AND THE GUARD IS NOT DEFENSIVENESS. This function has two
|
|
397
|
+
* recorded cases per platform in `tests/__snapshots__/primitives/`, replayed under
|
|
398
|
+
* `deepStrictEqual`, and their argument is a synthetic inventory carrying exactly
|
|
399
|
+
* `{agents, skills, laws, theme}` — no recorder survives to re-bless them. Reading
|
|
400
|
+
* `inv.doctrines.length` there THROWS; emitting a head unconditionally changes the recorded
|
|
401
|
+
* result array. Keyed on presence, the recording renders byte-identical and a real inventory
|
|
402
|
+
* gets all three tiers.
|
|
403
|
+
*
|
|
404
|
+
* The order is CONSTITUTIONAL and not alphabetical: ontology, then invariants, then doctrines
|
|
405
|
+
* — the order they are read in, the order AGENT.md renders them, and the order the console's
|
|
406
|
+
* three bands appear in. `Rule` and `Doctrine` are hardcoded English here, as `Rule` already
|
|
407
|
+
* was: this function receives an inventory, not a theme's nouns.
|
|
390
408
|
*/
|
|
391
409
|
export function tuiEntries(inv) {
|
|
392
410
|
const rows = [['head', `AGENTS (${inv.agents.length})`, null]];
|
|
393
411
|
for (const e of inv.agents) rows.push(['agent', e.name, e]);
|
|
394
412
|
rows.push(['head', `SKILLS (${inv.skills.length})`, null]);
|
|
395
413
|
for (const e of inv.skills) rows.push(['skill', e.name, e]);
|
|
414
|
+
if (inv.ontology?.length) {
|
|
415
|
+
rows.push(['head', `ONTOLOGY (${inv.ontology.length})`, null]);
|
|
416
|
+
for (const e of inv.ontology) rows.push(['ontology', e.title, e]);
|
|
417
|
+
}
|
|
396
418
|
rows.push(['head', `LAWS (${inv.laws.length})`, null]);
|
|
397
419
|
for (const e of inv.laws) rows.push(['law', `Rule ${e.num} — ${e.title}`, e]);
|
|
420
|
+
if (inv.doctrines?.length) {
|
|
421
|
+
// The head counts the ACTIVE packs' rules and names the fraction, because a pack that is
|
|
422
|
+
// not built in is still listed — greyed rather than absent, so "off" never looks like
|
|
423
|
+
// "gone" — and a head reading `DOCTRINES (23)` over an install bound by six would be the
|
|
424
|
+
// one number in this panel that lies.
|
|
425
|
+
const on = inv.doctrines.filter((p) => p.active);
|
|
426
|
+
const n = on.reduce((a, p) => a + p.rules.length, 0);
|
|
427
|
+
rows.push(['head',
|
|
428
|
+
`DOCTRINES (${n} in ${on.length}/${inv.doctrines.length} packs)`, null]);
|
|
429
|
+
for (const p of inv.doctrines) {
|
|
430
|
+
for (const r of p.rules) {
|
|
431
|
+
rows.push(['doctrine', `Doctrine ${r.pack} ${r.n} — ${r.title}`,
|
|
432
|
+
{ ...r, active: p.active, source: p.source }]);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
398
436
|
return rows;
|
|
399
437
|
}
|
|
400
438
|
|
|
@@ -420,7 +458,12 @@ export function tuiEntries(inv) {
|
|
|
420
458
|
* the fix is one `or []` at the call site, and it belongs in the same change as the port.
|
|
421
459
|
*/
|
|
422
460
|
export function detailLines(kind, label, data) {
|
|
423
|
-
|
|
461
|
+
// The three constitutional tiers share one treatment — the label, a blank, then the body —
|
|
462
|
+
// because all three are a titled rule rather than a rendered spec. New kinds only: the nine
|
|
463
|
+
// recorded cases name kinds that already existed, so no recorded answer moves.
|
|
464
|
+
if (kind === 'law' || kind === 'doctrine' || kind === 'ontology') {
|
|
465
|
+
return [label, ''].concat(data ? pySplitLines(data.body) : []);
|
|
466
|
+
}
|
|
424
467
|
if ((kind === 'agent' || kind === 'skill') && data) return pySplitLines(data.body);
|
|
425
468
|
return null;
|
|
426
469
|
}
|
|
@@ -433,12 +476,12 @@ export function detailLines(kind, label, data) {
|
|
|
433
476
|
*/
|
|
434
477
|
export function cmdTui() {
|
|
435
478
|
if (!process.stdin.isTTY) {
|
|
436
|
-
pyPrint('[tui] not an interactive terminal. Use `
|
|
479
|
+
pyPrint('[tui] not an interactive terminal. Use `geneseed setup`, `geneseed doctor`, or `geneseed build`.\n');
|
|
437
480
|
return 1;
|
|
438
481
|
}
|
|
439
482
|
// The pointer to the Python panel is gone for `js/menu.mjs`'s reason: the
|
|
440
483
|
// panel it named is the Python being deleted. What is left is true and stays true.
|
|
441
484
|
pyPrint('[tui] full-screen panel unavailable (this entry carries the TUI\'s layout '
|
|
442
|
-
+ 'half, not its screens). Use `
|
|
485
|
+
+ 'half, not its screens). Use `geneseed setup`, `geneseed doctor`, or `geneseed build`.\n');
|
|
443
486
|
return 1;
|
|
444
487
|
}
|
package/js/uninstall.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `geneseed uninstall` — the first verb in this port that DELETES.
|
|
3
3
|
*
|
|
4
4
|
* Thirteen subcommands crossed before this one and every one of them either writes or
|
|
5
5
|
* reports. `uninstall` removes a deployed install: the manifest's owned files, the empty
|
|
@@ -50,7 +50,7 @@ import path from 'node:path';
|
|
|
50
50
|
import { hookRunnerEntry } from '../bin/geneseed.mjs';
|
|
51
51
|
import { confirm } from './setup.mjs';
|
|
52
52
|
import {
|
|
53
|
-
claudeCfg, claudeReadManifest, emitHostScopeOf, installKind, installState,
|
|
53
|
+
claudeCfg, claudeReadManifest, doctrinesOfDir, emitHostScopeOf, installKind, installState,
|
|
54
54
|
registeredTargets, readMaybe, DISABLED_STASH,
|
|
55
55
|
} from './installs.mjs';
|
|
56
56
|
import {
|
|
@@ -187,7 +187,7 @@ function warnSurvivors(failed) {
|
|
|
187
187
|
|
|
188
188
|
function warnMarkersKept() {
|
|
189
189
|
pyPrintErr('[uninstall] WARN: the manifest and markers were KEPT so '
|
|
190
|
-
+ '`
|
|
190
|
+
+ '`geneseed uninstall` can be retried once the file(s) are '
|
|
191
191
|
+ 'unlocked/removable.\n');
|
|
192
192
|
}
|
|
193
193
|
|
|
@@ -510,7 +510,7 @@ export function installUninstall(root, host = 'opencode', scope = 'global', memo
|
|
|
510
510
|
// With `failed` set the reversal already warned twice — no third overlapping WARN.
|
|
511
511
|
pyPrintErr('[uninstall] WARN: could not fully remove the install — still present: '
|
|
512
512
|
+ `${survivors.join(', ')}. The install marker was KEPT so you can retry `
|
|
513
|
-
+ '`
|
|
513
|
+
+ '`geneseed uninstall` once the file(s) are unlocked/removable.\n');
|
|
514
514
|
}
|
|
515
515
|
const out = {
|
|
516
516
|
ok: true,
|
|
@@ -626,7 +626,7 @@ export function printSurvivingProjectInventory(removedRoot) {
|
|
|
626
626
|
+ 'does not affect them (each is self-contained):\n');
|
|
627
627
|
for (const [host, scope, root] of survivors) {
|
|
628
628
|
pyPrint(` - ${root} (${host}:${scope}) — remove with: `
|
|
629
|
-
+ `
|
|
629
|
+
+ `geneseed uninstall --target "${root}" --yes\n`);
|
|
630
630
|
}
|
|
631
631
|
}
|
|
632
632
|
|
|
@@ -733,7 +733,7 @@ export function cmdUninstall(args) {
|
|
|
733
733
|
if (s.incomplete && s.incomplete.length) {
|
|
734
734
|
pyPrint(`[uninstall] INCOMPLETE — removed ${s.removed} file(s), but `
|
|
735
735
|
+ `${s.incomplete.length} item(s) survived (see the WARN above); `
|
|
736
|
-
+ 'the install marker was kept — retry `
|
|
736
|
+
+ 'the install marker was kept — retry `geneseed uninstall` once they\'re '
|
|
737
737
|
+ `removable. ${cfgfile} updated where needed; memory/notebook ${mem}.\n`);
|
|
738
738
|
} else {
|
|
739
739
|
pyPrint(`[uninstall] done — removed ${s.removed} file(s); ${cfgfile} updated where `
|
|
@@ -1013,15 +1013,27 @@ function cleanHostStash(cfg, host = 'claude') {
|
|
|
1013
1013
|
* file and swallows its own OSError, exactly as the reference does — a failed bookkeeping
|
|
1014
1014
|
* write must not fail the reactivate that has already moved every file back.
|
|
1015
1015
|
*/
|
|
1016
|
-
function remergeClaudeHooks(cfg) {
|
|
1016
|
+
function remergeClaudeHooks(cfg, root = cfg) {
|
|
1017
1017
|
const data = claudeReadManifest(cfg);
|
|
1018
1018
|
const managed = managedOf(data);
|
|
1019
1019
|
// `hookOpts` is the one argument `mergeClaudeSettings` will not default, and it is right
|
|
1020
1020
|
// not to: there is no computable fallback for the runner/entry the shim bakes. The
|
|
1021
1021
|
// emitter's own originator is imported rather than restated — a reactivate that wired a
|
|
1022
1022
|
// different shim path would point every re-added hook at a file the emitter never writes.
|
|
1023
|
+
// The pack selection is read back off the reactivated carrier, not defaulted: a reactivate
|
|
1024
|
+
// that re-wired the git-gate into an install whose owner had turned the process pack off
|
|
1025
|
+
// would put the boundary back at odds with the prompt. `null` (no marker) keeps the gate.
|
|
1026
|
+
//
|
|
1027
|
+
// ⚠ THE CARRIER IS NOT ALWAYS UNDER `cfg`, WHICH IS WHY `root` IS A SECOND PARAMETER. On a
|
|
1028
|
+
// PROJECT install `cfg` is `<repo>/.claude` and the carrier is `<repo>/CLAUDE.md`, so
|
|
1029
|
+
// reading `cfg` alone answered `null` for every project reactivate and re-wired the gate
|
|
1030
|
+
// unconditionally (fail-closed, so not a hole — but wrong, and it made the toggle one-way
|
|
1031
|
+
// for project installs). `root` first, `cfg` second: copilot keeps its carrier INSIDE the
|
|
1032
|
+
// config dir (`.github/copilot-instructions.md`), and on a global install the two are the
|
|
1033
|
+
// same directory anyway. Both silent ⇒ `null` ⇒ the gate stays.
|
|
1034
|
+
const doctrines = doctrinesOfDir(root) ?? doctrinesOfDir(cfg);
|
|
1023
1035
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed), 'global',
|
|
1024
|
-
managed.settings_hooks ?? null, hookRunnerEntry());
|
|
1036
|
+
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines);
|
|
1025
1037
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1026
1038
|
if (!pyEq(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1027
1039
|
managed.settings_hooks = claims;
|
|
@@ -1106,7 +1118,7 @@ function claudeReactivate(root, scope = 'global', host = 'claude') {
|
|
|
1106
1118
|
&& isFile(path.join(cfg, 'rules', 'geneseed.md'));
|
|
1107
1119
|
}
|
|
1108
1120
|
if (relive) {
|
|
1109
|
-
remergeClaudeHooks(cfg); // ensure the hooks are present (and the claims exact)
|
|
1121
|
+
remergeClaudeHooks(cfg, root); // ensure the hooks are present (and the claims exact)
|
|
1110
1122
|
cleanHostStash(cfg, host);
|
|
1111
1123
|
return { ok: true,
|
|
1112
1124
|
note: 'install was re-created while disabled; discarded the stashed snapshot' };
|
|
@@ -1126,12 +1138,19 @@ function claudeReactivate(root, scope = 'global', host = 'claude') {
|
|
|
1126
1138
|
moved += 1;
|
|
1127
1139
|
}
|
|
1128
1140
|
if (leftovers.length) return { ok: false, failed: leftovers, moved };
|
|
1129
|
-
remergeClaudeHooks(cfg);
|
|
1130
1141
|
const managed = claudeReadManifest(cfg).managed || {};
|
|
1131
|
-
|
|
1142
|
+
// ⚠ THE CARRIER IS RESTORED BEFORE THE HOOKS ARE RE-MERGED, AND THE ORDER IS LOAD-BEARING.
|
|
1143
|
+
// `remergeClaudeHooks` reads the install's pack selection back off the carrier, and on a
|
|
1144
|
+
// Claude-style host that selection lives inside the MANAGED BLOCK — which the deactivate
|
|
1145
|
+
// stashed. Re-merging first therefore asked a CLAUDE.md with its block cut out, got `null`
|
|
1146
|
+
// ("unknown"), and fail-closed straight back to the consent gate: every reactivate re-wired a
|
|
1147
|
+
// git-gate the owner had turned off, in both scopes. The two writes touch different files
|
|
1148
|
+
// (`CLAUDE.md` vs `settings*.json`), so nothing else depends on which goes first.
|
|
1132
1149
|
if (existsSync(blockFile)) {
|
|
1133
1150
|
managedBlockWrite(claudeMdPath(cfg, managed), readText(blockFile));
|
|
1134
1151
|
}
|
|
1152
|
+
remergeClaudeHooks(cfg, root);
|
|
1153
|
+
wireClaudeExcludes(settingsFile(cfg, managed), managed.settings_excludes || []);
|
|
1135
1154
|
cleanHostStash(cfg, host);
|
|
1136
1155
|
return { ok: true, kind: host, moved };
|
|
1137
1156
|
}
|
package/js/web/actions.mjs
CHANGED
|
@@ -104,7 +104,7 @@ import os from 'node:os';
|
|
|
104
104
|
import path from 'node:path';
|
|
105
105
|
|
|
106
106
|
import { emitGlobalInto } from '../../bin/geneseed.mjs';
|
|
107
|
-
import { ROOT, discoverNames } from '../checkout.mjs';
|
|
107
|
+
import { ROOT, discoverNames, PACK_ORDER } from '../checkout.mjs';
|
|
108
108
|
import { withStdoutSwallowed } from '../diff.mjs';
|
|
109
109
|
import { excludeAdd, excludeRemove } from '../excludes.mjs';
|
|
110
110
|
import { setupBuildArgs } from '../generate.mjs';
|
|
@@ -114,8 +114,8 @@ import {
|
|
|
114
114
|
pyResolve,
|
|
115
115
|
} from '../hosts.mjs';
|
|
116
116
|
import {
|
|
117
|
-
EMIT_HOST_SCOPE, footprintOfDir, installState, installTargets, modeOfDir,
|
|
118
|
-
readMaybe,
|
|
117
|
+
EMIT_HOST_SCOPE, doctrinesForBuild, footprintOfDir, installState, installTargets, modeOfDir,
|
|
118
|
+
postureOfDir, readMaybe,
|
|
119
119
|
} from '../installs.mjs';
|
|
120
120
|
import {
|
|
121
121
|
MCP_PRESETS, isDict, mcpApply, mcpCommented, mcpInstallTargets, mcpKnownNames, mcpLoad,
|
|
@@ -140,6 +140,36 @@ const bget = (body, key, dflt = null) => (isDict(body) && Object.hasOwn(body, ke
|
|
|
140
140
|
/** `str(x or "")` — Python's truthiness, then Python's `str`. */
|
|
141
141
|
const strOr = (v) => (pyTruthy(v) ? pyStr(v) : '');
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* A request body's `doctrines` -> a normalised pack list, or `null` for "said nothing usable".
|
|
145
|
+
*
|
|
146
|
+
* THE SAME TRUST BOUNDARY THE THEME CROSSES, and it needs more care than posture or mode do
|
|
147
|
+
* because this value is a LIST: `discoverNames(...).includes(x)` closes a scalar in one call,
|
|
148
|
+
* but a list has to be closed member by member or one bogus element rides in beside three good
|
|
149
|
+
* ones and reaches an argv. Every name is checked against DISCOVERY — what this checkout
|
|
150
|
+
* actually ships — and the result is re-ordered through `PACK_ORDER`, because the
|
|
151
|
+
* `Active packs:` line the build writes is a marker that a later reader parses back out and a
|
|
152
|
+
* marker whose contents depend on request-body order compares unequal to itself.
|
|
153
|
+
*
|
|
154
|
+
* `null` rather than a substituted default on every rejection, so the caller decides what
|
|
155
|
+
* "unspecified" means for its own endpoint — a rebuild keeps the deployment's answer, a fresh
|
|
156
|
+
* deploy takes the configured one. That mirrors the bogus-theme fallback right beside it.
|
|
157
|
+
*
|
|
158
|
+
* Accepts an array (what the console sends) or a comma string (what a curl by hand sends), and
|
|
159
|
+
* `none`/`[]` both mean the deliberate empty selection — a real configuration, not a rejection.
|
|
160
|
+
*/
|
|
161
|
+
function bodyDoctrines(body) {
|
|
162
|
+
const raw = bget(body, 'doctrines');
|
|
163
|
+
if (Array.isArray(raw) && !raw.length) return [];
|
|
164
|
+
const names = Array.isArray(raw) ? raw
|
|
165
|
+
: (typeof raw === 'string' ? raw.split(',').map((s) => s.trim()).filter(Boolean) : null);
|
|
166
|
+
if (names === null || !names.length) return null;
|
|
167
|
+
if (names.length === 1 && names[0] === 'none') return [];
|
|
168
|
+
const known = discoverNames('doctrines', PACK_ORDER[0]);
|
|
169
|
+
if (!names.every((n) => typeof n === 'string' && known.includes(n))) return null;
|
|
170
|
+
return PACK_ORDER.filter((p) => names.includes(p));
|
|
171
|
+
}
|
|
172
|
+
|
|
143
173
|
/** `str.split()` with no argument — runs of PYTHON whitespace, no empties. */
|
|
144
174
|
const pyWords = (s) => s.split(new RegExp(`[${PY_SPACE}]+`)).filter(Boolean);
|
|
145
175
|
|
|
@@ -534,14 +564,30 @@ export function apiMcp() {
|
|
|
534
564
|
* `,]`. A file that will not parse is refused rather than clobbered: it may be
|
|
535
565
|
* `~/.claude.json`, which holds projects and history far beyond MCP wiring.
|
|
536
566
|
*/
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
567
|
+
/**
|
|
568
|
+
* The MCP config paths a request body is allowed to name, as `path -> [path, host]`.
|
|
569
|
+
*
|
|
570
|
+
* LIFTED OUT OF `apiMcpToggle` BECAUSE IT NOW HAS A SECOND CALLER, not for tidiness. `/api/reveal`
|
|
571
|
+
* hands a path to the desktop's file manager, and the allowlist is the whole security of that
|
|
572
|
+
* endpoint exactly as it is of the toggle: an unlisted path must 404 before anything is opened.
|
|
573
|
+
* Two copies of a security check drift, and the copy that drifts is the one nobody re-read.
|
|
574
|
+
*
|
|
575
|
+
* It is rebuilt per call rather than cached: installs come and go while the daemon runs, and a
|
|
576
|
+
* stale allowlist is wrong in both directions — refusing a real target, or naming a dead one.
|
|
577
|
+
*/
|
|
578
|
+
export function mcpTargetPaths() {
|
|
541
579
|
const known = new Map();
|
|
542
580
|
for (const [, cfgPath, host] of mcpInstallTargets()) {
|
|
543
581
|
known.set(String(cfgPath), [cfgPath, host]);
|
|
544
582
|
}
|
|
583
|
+
return known;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function apiMcpToggle(state, body) {
|
|
587
|
+
const name = strOr(bget(body, 'name'));
|
|
588
|
+
const want = pyTruthy(bget(body, 'enabled'));
|
|
589
|
+
const pathArg = strOr(bget(body, 'path'));
|
|
590
|
+
const known = mcpTargetPaths();
|
|
545
591
|
const hit = known.get(pathArg);
|
|
546
592
|
if (hit === undefined || !name) {
|
|
547
593
|
throw new NotFound(`mcp target ${pathArg || '(none)'}`);
|
|
@@ -686,6 +732,13 @@ export function apiRestore(state, files) {
|
|
|
686
732
|
footprint: state.footprint,
|
|
687
733
|
posture: state.posture,
|
|
688
734
|
mode: state.mode,
|
|
735
|
+
// THE PACK SELECTION IS THE FIFTH, and it is the one that makes this verb a BOUNDARY
|
|
736
|
+
// question rather than a cosmetic one. `expected` rendered at all four packs, and this
|
|
737
|
+
// verb COPIES OUT OF IT: restoring AGENT.md onto an install built `--doctrines craft`
|
|
738
|
+
// wrote a carrier stating doctrine process 5 while `claudeHookGroups` had wired no gate
|
|
739
|
+
// behind it — prompt and boundary disagreeing by a WRITE, the write-side twin of the
|
|
740
|
+
// read-side hole `diffCollect` had (and the reason the panel offered the file at all).
|
|
741
|
+
doctrines: doctrinesForBuild(target),
|
|
689
742
|
}));
|
|
690
743
|
for (const raw of (pyTruthy(files) ? files : [])) {
|
|
691
744
|
const rel = pyStripSpace(pyStr(raw).replace(/\\/g, '/')).replace(/^\/+/, '');
|
|
@@ -765,8 +818,15 @@ export function apiInstallCmd(state, body) {
|
|
|
765
818
|
const bmode = bget(body, 'mode');
|
|
766
819
|
const mode = discoverNames('modes', 'direct').includes(bmode)
|
|
767
820
|
? bmode : (modeOfDir(root) || 'direct');
|
|
821
|
+
// Unspecified means "keep what this install already has", exactly as theme, footprint,
|
|
822
|
+
// posture and mode above do — a rebuild through the console is not a place to silently
|
|
823
|
+
// re-decide the constitution. ⚠ AND A CARRIER WITH NO `Active packs:` MARKER (a pre-2.3
|
|
824
|
+
// install) MUST NOT LAND ON `harness.config.json`: `doctrinesOfDir` answers `null` there,
|
|
825
|
+
// `null` elided the flag, and the generator's config fallback then narrowed the install and
|
|
826
|
+
// took its consent gate with it. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
827
|
+
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
768
828
|
const out = scope === 'global' ? null : String(root);
|
|
769
|
-
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode);
|
|
829
|
+
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines);
|
|
770
830
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'geneseed.mjs'), ...argv] };
|
|
771
831
|
}
|
|
772
832
|
|
|
@@ -862,7 +922,14 @@ export function apiDeployCmd(state, body) {
|
|
|
862
922
|
const pos = discoverNames('postures', 'peer').includes(bpos) ? bpos : 'peer';
|
|
863
923
|
const bmode = bget(body, 'mode');
|
|
864
924
|
const mode = discoverNames('modes', 'direct').includes(bmode) ? bmode : 'direct';
|
|
925
|
+
// Same resolution as `apiInstallCmd` above, and for the same reason: the console's Deploy
|
|
926
|
+
// form sends host/path/theme/footprint/posture/mode and NO pack selection, and nothing stops
|
|
927
|
+
// it landing on a directory that already holds an install. Taking `bodyDoctrines` alone left
|
|
928
|
+
// the flag off, the generator fell back to `harness.config.json`, and deploying onto an
|
|
929
|
+
// existing all-four Claude install dropped it to one pack — measured: 6 hook groups became 5
|
|
930
|
+
// and `PreToolUse::Bash` went with them. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
931
|
+
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
865
932
|
// project-scope emit name == host name (opencode / claude / bob / copilot)
|
|
866
|
-
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode);
|
|
933
|
+
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines);
|
|
867
934
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'geneseed.mjs'), ...argv] };
|
|
868
935
|
}
|